/** Shopify CDN: Minification failed

Line 173:5 Unexpected "`"

**/
/* JustPure -- every site-wide stylesheet, concatenated.

   Generated by tools/bundle-css.ps1 -- do not edit directly.
   Sources, in cascade order:
     jp-mobile-ux.css
     jp-overlays.css
     jp-system.css
     jp-badge.css
     jp-tables.css
     jp-flip.css
     jp-alive.css
     jp-quality.css
     jp-motion.css
     jp-idle.css
     jp-kinetic.css
     jp-interact.css
     jp-signature.css
     jp-engage-pages.css
     jp-policy.css
     jp-card-life.css
     jp-scale.css
     jp-pdp-life.css
     jp-bilona-reveal.css
*/


/* ================= jp-mobile-ux.css ================= */
/* JustPure mobile UX floor.
   Measured at 390x844 on the preview: 64 of 92 tappable elements on the
   homepage and 62 of 95 on the product page were under the 40px minimum, and
   the dismiss controls on the offer and WhatsApp nudges were 13x26 and 8x18 —
   too small to hit, so a visitor could not close them.

   This file only enforces a floor. It does not restyle anything, so it cannot
   fight the section-level design. */

@media screen and (max-width: 749px) {
  /* Close buttons: the worst offenders, and the most frustrating to miss.

     These two are absolutely positioned inside their card, so growing the box
     pushed it down and left, over the content underneath — taps meant for the
     X landed on "No thanks" instead. Centre the growth on the glyph and lift
     them above the card so the hit area is where the X appears to be. */
  .jp-wa-nudge__x,
  .jp-offer__x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -14px;
    padding: 14px;
    z-index: 2;
  }

  .jp-offer__later {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Icon-only controls carry no text to grow the hit area, so give them one. */
  .jp-icon,
  .list-social__link {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Primary actions sat at 39px — one pixel under the threshold. */
  .jp-bundle__btn,
  .jp-add-btn,
  .jp-sticky-buy__btn {
    min-height: 44px;
  }

  /* Product titles are the main path into a PDP from any grid. Padding here
     buys height without moving the visual baseline.

     6px of padding left these at 35px — better than the 23px they started at,
     still short. A one-line title needs the full 44 from min-height; a title
     that wraps to two lines already clears it, so min-height simply stops
     applying rather than adding dead space. */
  .jp-product-title {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding-block: 6px;
  }

  /* The offer modal's own controls measured 43px — one pixel under, which is
     the same near-miss the primary actions above had. */
  .jp-offer__x,
  .jp-offer__btn,
  .jp-offer__later {
    min-height: 44px;
  }
  .jp-offer__x {
    min-width: 44px;
  }

  /* Body copy under 13px was measured on 30 homepage and 38 product-page
     nodes. 13px is the floor for sustained reading on a phone. */
  .jp-question-text,
  .jp-question-text p,
  .jp-product-badge,
  .jp-rating-text,
  .jp-sticky-buy__name {
    font-size: max(13px, 1em);
  }

  /* --- Second pass, measured 2026-07-29 at 390x844 ---------------------
     54 of 89 tappable elements were still under 44px after the first pass,
     because that pass only covered the overlays. These are the rest, in the
     order they hurt: every one is a real control a thumb has to find. */

  /* Footer nav links measured 25x13 — the smallest real targets on the page,
     and they sit in a stacked list where a miss lands on the neighbour.
     Inline elements ignore vertical padding for hit-testing, hence the
     display change.

     `.text-small` is the social-icon link class from common.css, rendered by
     `social-icons` rather than nested under .jp-footer, so it needs its own
     unscoped rule — scoping it to the footer matched nothing. */
  .jp-footer__menu a,
  a.text-small {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Accordion headers for the footer menus: 340px wide but only 23px tall. */
  .jp-footer__menu-title {
    min-height: 44px;
  }

  /* Quantity steppers at 28x28 sit inside a 94px control, so there is room to
     grow them without reflowing the row. */
  .quantity-button,
  .quantity-input {
    min-width: 40px;
    min-height: 44px;
  }

  /* Drawer and popup dismiss controls at 30x30. Same reasoning as the overlay
     close buttons above: centre the growth on the glyph so the hit area lands
     where the X appears, not below-left of it. */
  .drawer-close-btn,
  .popup-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }

  /* Carousel dots are 26x4 (44x4 when active) — a 4px-tall control.

     Padding cannot fix these: the section sets an explicit `height:4px` with
     `overflow:hidden`, and the progress fill inside is `position:absolute;
     inset:0`, so any growth of the box would thicken the visible bar as well.
     Instead the bar keeps its exact geometry and an invisible ::after extends
     the hit area above and below it. */
     `overflow:hidden` on the bar would clip an ::after hit area, and raising
     specificity to unset it lets the progress fill escape the pill. So the
     hit area goes on the row instead: the dots container gets the height, and
     each dot keeps its own geometry untouched. Measured: 8px gap between
     dots, so the row is the safe place to grow. */
  .jp-benefit-slider__dots {
    min-height: 44px;
  }

  /* Colour/size swatches: the input is a visually-hidden radio (1x1, absolute)
     and `.swatch-label` is the real target, measured at 24x24. Grow the label,
     never the input — the theme hangs its checked state off
     `.swatch-input[checked] + .swatch-label`, so moving the input breaks
     selection styling. */
  .swatches-list-item .swatch-label {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Search and newsletter fields came in at 18-39px tall. Anything a thumb has
     to place a cursor in needs the same floor as a button. */
  .form-control,
  .search-input-box input,
  .jp-newsletter-field input,
  .jp-newsletter-field button {
    min-height: 44px;
  }

  /* Announcement bar links are 319x34 and scroll horizontally, so a miss is
     both likely and irreversible — the item has moved on by the next tap. */
  .jp-announcement-item {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* ================= jp-overlays.css ================= */
/* JustPure overlay system.

   Measured on a 390x844 phone, the corner overlays were stacking on top of each
   other and on the buy controls: the welcome modal covered the whole viewport
   including Add To Cart, and the WhatsApp nudge (y712-830) sat across the
   sticky buy bar (y779-844). Each piece had its own z-index invented in its own
   snippet, so nothing agreed on an order.

   One ladder, declared once. Everything below reads from it, and each floating
   element is anchored to the one beneath it rather than to the raw viewport. */

:root {
  /* stacking order, lowest first */
  --jp-z-shipbar:  9200;   /* cart progress, informational */
  --jp-z-wa:       9400;   /* WhatsApp launcher + nudge  */
  --jp-z-sticky:   9600;   /* sticky buy bar — must stay reachable */
  --jp-z-modal:    9900;   /* welcome offer, deliberately interrupting */

  /* heights the stack is built from */
  --jp-bottomnav-h: 57px;   /* measured live on a 390x844 phone */
  --jp-sticky-h:   64px;
  --jp-shipbar-h:  52px;
  --jp-gutter:     12px;
}

/* --- the ladder ------------------------------------------------------ */
/* NOTE: the shipping meter's real markup is .jp-ix__ship (jp-interactions.liquid),
   not .jp-ship-bar — that selector never matched anything, so this whole ladder
   was inert. Fixed to the actual class names, verified against the live DOM. */
.jp-ix__ship         { z-index: var(--jp-z-shipbar) !important; }
.jp-ix__toast        { z-index: var(--jp-z-shipbar) !important; }
/* Right-hand column, every breakpoint. The left corner is where page text
   starts, so anything parked there covers words; the right edge of a row is
   whitespace. Declared with the ladder because this is a stack decision, not a
   one-page tweak. */
.jp-ix__top          { z-index: var(--jp-z-shipbar) !important;
                       left: auto !important; right: 18px !important; }
.jp-wa-wrap         { z-index: var(--jp-z-wa)      !important; }
.jp-sticky-buy      { z-index: var(--jp-z-sticky)  !important; }
.jp-offer           { z-index: var(--jp-z-modal)   !important; }

@media screen and (max-width: 749px) {
  /* Bottom corner, from the floor up: sticky buy bar, then the shipping
     progress, then the WhatsApp launcher. Each clears the one below it. */
  .jp-sticky-buy:not([hidden]) ~ .jp-ix__ship,
  .jp-sticky-buy:not([hidden]) + .jp-ix__ship {
    bottom: calc(var(--jp-sticky-h) + var(--jp-gutter)) !important;
  }

  .jp-sticky-buy:not([hidden]) ~ .jp-ix__toast,
  .jp-sticky-buy:not([hidden]) + .jp-ix__toast {
    bottom: calc(var(--jp-sticky-h) + var(--jp-gutter)) !important;
  }

  .jp-sticky-buy:not([hidden]) ~ .jp-wa-wrap {
    bottom: calc(var(--jp-sticky-h) + var(--jp-shipbar-h) + var(--jp-gutter) * 2) !important;
  }

  /* The sticky bar only appears once the buy row scrolls away, so the WhatsApp
     launcher cannot key off it alone — measured live, the launcher (652-780)
     still crossed the shipping meter (705-758). Anything that is actually on
     screen lifts it, whether or not the sticky bar is there. */
  .jp-ix__ship ~ .jp-wa-wrap,
  body:has(.jp-ix__ship) .jp-wa-wrap {
    bottom: calc(var(--jp-shipbar-h) + var(--jp-gutter) * 2) !important;
  }

  body:has(.jp-ix__ship):has(.jp-sticky-buy:not([hidden])) .jp-wa-wrap {
    bottom: calc(var(--jp-sticky-h) + var(--jp-shipbar-h) + var(--jp-gutter) * 2) !important;
  }

  body:not(:has(.jp-ix__ship)):has(.jp-sticky-buy:not([hidden])) .jp-wa-wrap {
    bottom: calc(var(--jp-sticky-h) + var(--jp-gutter)) !important;
  }

  /* Back-to-top moved out of the left corner: there it sat where every heading
     and accordion row begins its text and always had words behind it (measured
     live: "Shipping & returns" read as "pping & returns"). It now shares the
     right-hand widget column, so it has to clear the launcher below it rather
     than only the sticky bar. */
  /* Clear of the launcher at all times, not only when a bar is up. The two
     rules below apply when the sticky buy bar or the bottom nav is present;
     with neither — a product page mid-scroll — the offset in jp-interactions
     took over and put the two on the same 52px of screen, so the launcher was
     covering "Back to top" (measured live: launcher 652-704, the button's
     centre at 676). The launcher's own floor is 14px + its 52px body. */
  .jp-ix__top {
    bottom: calc(14px + 52px + var(--jp-gutter) * 2) !important;
  }

  .jp-sticky-buy:not([hidden]) ~ .jp-ix__top,
  .jp-sticky-buy:not([hidden]) + .jp-ix__top {
    bottom: calc(var(--jp-sticky-h) + var(--jp-shipbar-h) + var(--jp-gutter) * 2 + 56px) !important;
  }

  /* The nudge bubble opens upward from the launcher, never downward over it. */
  .jp-wa-nudge { bottom: 100%; top: auto; margin-bottom: 10px; }

  /* The welcome modal is allowed to cover the page, but it must not appear
     over the buy controls the moment someone lands on a product — it is shown
     on a delay by jp-engage, and dismissing it must fully release the page. */
  .jp-offer[hidden] { display: none !important; }
  .jp-offer__card   { max-height: 82vh; overflow-y: auto; }
}

/* Shopify's own preview bar sits at the very bottom in the theme editor; give
   the sticky bar room so the two do not overlap while reviewing. */
body.shopify-design-mode .jp-sticky-buy { bottom: 56px; }

/* ---- bottom nav is the new floor on phones -------------------------- */
/* Everything that floats bottom-right now starts above the nav, not above
   the viewport edge. Measured: nav occupies 787..844 on a 390x844 screen. */
@media screen and (max-width: 749px) {
  body:has(#jp-bottom-nav:not(.is-away)) .jp-ix__ship,
  body:has(#jp-bottom-nav:not(.is-away)) .jp-ix__toast {
    bottom: calc(var(--jp-bottomnav-h) + var(--jp-gutter)) !important;
  }

  body:has(#jp-bottom-nav:not(.is-away)) .jp-wa-wrap {
    bottom: calc(var(--jp-bottomnav-h) + var(--jp-shipbar-h) + var(--jp-gutter) * 2) !important;
  }

  body:has(#jp-bottom-nav:not(.is-away)) .jp-ix__top {
    bottom: calc(var(--jp-bottomnav-h) + var(--jp-shipbar-h) + var(--jp-gutter) * 2 + 56px) !important;
  }

  /* When the sticky buy bar is up the nav slides away, so the buy bar becomes
     the floor again and the rules further up take over. */
}

/* ---- get out of the way while the page is being read ---------------- */
/* Everything above is about where the widgets sit RELATIVE TO EACH OTHER, and
   that part works. What it cannot solve is the page underneath: a corner
   widget is fixed to the viewport, so as the reader scrolls, every section in
   turn passes beneath it. Chasing that with offsets is endless — measured on
   the live phone the same three widgets landed on "Add bundle to cart", on the
   "Family Pack" card, and across a section heading, and each per-widget fix
   moved the collision somewhere else rather than removing it.

   There is no position that is clear on every section, so position is the
   wrong lever. Presence is: while the page is actually moving, the reader is
   looking at content and not at a chat button, so the corner empties. They
   come back the moment scrolling stops, which is when the reader might want
   them. Nothing is removed and nothing needs to know what is underneath it.

   Only the discretionary widgets do this. The sticky buy bar and the bottom
   nav are navigation the visitor reaches for mid-scroll, so they stay. */
.jp-wa-wrap,
.jp-ix__top,
.jp-ix__toast,
.jp-ix__ship,
shopify-forms-embed {
  transition: opacity .18s ease, transform .22s cubic-bezier(.16, 1, .3, 1);
}

body.jp-scrolling .jp-wa-wrap,
body.jp-scrolling .jp-ix__top,
body.jp-scrolling .jp-ix__toast,
body.jp-scrolling .jp-ix__ship,
body.jp-scrolling shopify-forms-embed,
/* Held back because the reader stopped somewhere the widget would land on
   words or a control. Hiding during the scroll alone does not cover this: the
   reader has to stop somewhere, and every stop is a new chance to come back on
   top of a paragraph. jp-interactions samples the spot on settle and sets this
   when it is occupied, so a widget is absent exactly while it would be in the
   way and returns as soon as it would not. */
.jp-wa-wrap.jp-held,
.jp-ix__top.jp-held,
.jp-ix__toast.jp-held,
.jp-ix__ship.jp-held {
  opacity: 0;
  /* translate, not display:none — the box keeps its size, so the ladder above
     still measures correctly and nothing reflows on the way back. */
  transform: translateY(14px);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .jp-wa-wrap, .jp-ix__top, .jp-ix__toast, .jp-ix__ship, shopify-forms-embed {
    transition: opacity .18s ease;
  }
  body.jp-scrolling .jp-wa-wrap,
  body.jp-scrolling .jp-ix__top,
  body.jp-scrolling .jp-ix__toast,
  body.jp-scrolling .jp-ix__ship,
  body.jp-scrolling shopify-forms-embed {
    transform: none;
  }
}

/* ================= jp-system.css ================= */
/* JustPure interaction + motion system.

   One place for the things every new piece should agree on: the palette we
   already ship on the jars, the easing, the depth, and the scroll behaviour.
   Nothing here restyles existing sections — it defines tokens and utilities
   the new components opt into, so the site gains a consistent feel without a
   risky rewrite of what already converts.

   Palette is taken from the packaging, not invented:
     forest  #2A5040   the label green
     deep    #1F3D33   pressed / dark surface
     amber   #D4943A   the seal + accent
     cream   #FAF7F2   paper
     sand    #F0E9DC   quiet fill
     ink     #1F2B26   body text
*/

:root {
  --jp-forest: #2A5040;
  --jp-deep:   #1F3D33;
  --jp-amber:  #D4943A;
  --jp-amber-soft: #E8C79A;
  /* A darker amber for TEXT. The brand amber is a decoration colour — on
     white it measures 2.6:1, so every uppercase eyebrow and kicker on the
     site was under the readable threshold at 12px. This keeps the hue and
     clears 4.5:1 on white, cream AND sand; the decorative amber above is
     unchanged for borders,
     rules, buttons and fills. */
  --jp-amber-text: #8A5A1E;
  --jp-cream:  #FAF7F2;
  --jp-sand:   #F0E9DC;
  --jp-ink:    #1F2B26;
  /* Cleared against every surface this site uses — white, cream
     (#FAF7F2) and sand (#F0E9DC). The previous #63695E passed on
     white and failed on sand, so the same grey was readable in one
     section and not the next. */
  --jp-muted:  #63695E;
  --jp-line:   #E6E0D2;

  /* One easing for everything that moves, so the site feels authored. */
  --jp-ease:   cubic-bezier(.16, 1, .3, 1);
  --jp-fast:   .22s;
  --jp-mid:    .38s;
  --jp-slow:   .62s;

  --jp-r-sm:   9px;
  --jp-r-md:   14px;
  --jp-r-lg:   22px;

  --jp-shadow-sm: 0 2px 10px rgba(31, 61, 51, .07);
  --jp-shadow-md: 0 10px 30px rgba(31, 61, 51, .10);
  --jp-shadow-lg: 0 24px 60px rgba(31, 61, 51, .16);
}

/* ---------- scroll reveal -------------------------------------------- */
/* Elements marked data-jp-reveal start slightly low and faded; jp-system.js
   adds .is-in when they enter view. Staggering is index-driven so a row of
   cards arrives as a wave rather than all at once. */
/* No will-change here. Promoting every revealable element to its own
   compositor layer on a page this long left them stuck at opacity 0 —
   .is-in would apply in the cascade and the layer would never repaint.
   The transition is cheap enough without the hint. */
/* Animation, not transition.

   A transition needs the browser to have committed the start value before the
   end value arrives. On this page it never did: elements sit at opacity 0 from
   first paint and .is-in lands much later, so the transition had nothing to run
   from and every revealable element stayed invisible — all 26 of them. Setting
   `transition: none` made them appear instantly, which is what proved it was
   the transition and not the cascade.

   A keyframe animation has no such dependency: adding the class starts it.

   Hiding is opt-IN, gated on .jp-reveal-armed on <html>, which jp-system.js
   adds only once it has an IntersectionObserver and is about to observe. Any
   path where the script does not reach that point — no JS, a throw earlier in
   the file, an unsupported browser, an element the observer never delivers —
   leaves the content plainly visible. A missed animation should cost the
   animation, never the content; the previous unconditional `opacity: 0` meant
   one silent failure blanked a whole section. */
/* Movement only — the last hide-first reveal on the site.

   This was the fourth layer carrying the same bug: content held at opacity 0
   waiting for a class, an opacity keyframe with `both` fill, and a stagger
   expressed as animation-delay (which paints the 0% frame for the whole
   delay). It was still hiding "Start here" on the home page after the other
   three were fixed.

   Same resolution as jp-alive and jp-motion: nothing containing words animates
   its opacity, and the stagger lives inside the keyframe rather than in a
   delay. An interrupted transform costs a few pixels of position; an
   interrupted opacity costs the headline. */
html.jp-reveal-armed [data-jp-reveal] {
  opacity: 1;
}

html.jp-reveal-armed [data-jp-reveal].is-in {
  animation: jp-rise var(--jp-slow) var(--jp-ease);
}

@keyframes jp-rise {
  from { transform: translate3d(0, 18px, 0); }
  to   { transform: none; }
}

/* Stagger as a longer duration, not a delay — a row still arrives as a wave,
   but nothing is ever painted at its opening frame waiting for a clock. */
[data-jp-step="1"].is-in { animation-duration: calc(var(--jp-slow) + 70ms) }
[data-jp-step="2"].is-in { animation-duration: calc(var(--jp-slow) + 140ms) }
[data-jp-step="3"].is-in { animation-duration: calc(var(--jp-slow) + 210ms) }
[data-jp-step="4"].is-in { animation-duration: calc(var(--jp-slow) + 280ms) }
[data-jp-step="5"].is-in { animation-duration: calc(var(--jp-slow) + 350ms) }

/* Belt and braces: even armed, .no-js means nothing will ever add .is-in. */
.no-js [data-jp-reveal] { opacity: 1 !important; transform: none !important; }

/* ---------- surfaces -------------------------------------------------- */
.jp-card {
  background: #fff;
  border: 1px solid var(--jp-line);
  border-radius: var(--jp-r-md);
  box-shadow: var(--jp-shadow-sm);
  transition: transform var(--jp-mid) var(--jp-ease),
              box-shadow var(--jp-mid) var(--jp-ease),
              border-color var(--jp-mid) var(--jp-ease);
}
.jp-card:hover {
  transform: translateY(-3px);
  box-shadow: var(--jp-shadow-md);
  border-color: var(--jp-amber-soft);
}

/* A quiet paper texture for full-bleed bands — keeps large cream areas from
   reading as flat white on big screens. */
.jp-paper {
  background-color: var(--jp-cream);
  background-image:
    radial-gradient(circle at 18% 22%, rgba(212,148,58,.055) 0, transparent 42%),
    radial-gradient(circle at 82% 78%, rgba(42,80,64,.05) 0, transparent 46%);
}

/* ---------- buttons ---------------------------------------------------- */
.jp-btn {
  --jp-btn-bg: var(--jp-forest);
  --jp-btn-fg: #F6F2E7;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 14px 26px;
  border: 0;
  border-radius: var(--jp-r-sm);
  background: var(--jp-btn-bg);
  color: var(--jp-btn-fg);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  overflow: hidden;
  transition: transform var(--jp-fast) var(--jp-ease),
              box-shadow var(--jp-fast) var(--jp-ease);
  max-width: 100%;
}
.jp-btn:hover  { transform: translateY(-1px); box-shadow: var(--jp-shadow-md); }
.jp-btn:active { transform: translateY(0); }
.jp-btn--ghost {
  --jp-btn-bg: transparent;
  --jp-btn-fg: var(--jp-forest);
  border: 1.5px solid var(--jp-forest);
}

/* Press ripple, positioned by jp-system.js from the actual pointer.
   Scaled rather than grown: animating width/height relayouts the button on
   every frame, transform does not touch layout at all. */
.jp-btn::after {
  content: "";
  position: absolute;
  left: var(--jp-rx, 50%);
  top: var(--jp-ry, 50%);
  width: 240%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(255,255,255,.30);
  transform: translate(-50%, -50%) scale(0);
  transform-origin: center;
  transition: transform .5s var(--jp-ease), opacity .5s ease;
  opacity: 0;
  pointer-events: none;
}
.jp-btn.is-pressed::after {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
  transition: none;
}

/* ---------- section rhythm -------------------------------------------- */
.jp-section      { padding-block: clamp(48px, 7vw, 104px); }
.jp-wrap         { width: min(1240px, 100% - 40px); margin-inline: auto; }
.jp-eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--jp-amber-text);
}
.jp-h2 {
  margin: 10px 0 0;
  font-size: clamp(26px, 3.4vw, 42px);
  line-height: 1.12;
  color: var(--jp-forest);
}
.jp-lede {
  margin: 12px 0 0;
  max-width: 62ch;
  font-size: clamp(14.5px, 1.3vw, 17px);
  line-height: 1.6;
  color: var(--jp-muted);
}

@media (prefers-reduced-motion: reduce) {
  [data-jp-reveal] { opacity: 1 !important; transform: none !important; animation: none !important; }
  .jp-card, .jp-btn { transition: none; }
  .jp-btn::after { display: none; }
}

/* ================= jp-badge.css ================= */
/* Per-product mark, angle and proof chips.

   Loaded site-wide so the same product reads identically in the homepage grid,
   the shop rail, the bundle builder, the cart drawer and the PDP. Colour is a
   per-product tone token rather than a hardcoded value, so a chilli card runs
   warm and a buffalo card runs cool without duplicating the rules. */

.jp-pb {
  --pb-tint: rgba(42, 80, 64, .07);
  --pb-edge: rgba(42, 80, 64, .16);
  --pb-ink:  #2A5040;

  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* Tones: the mark's dish colour follows the product, which is what makes a row
   of five cards read as five different things at a glance. */
.jp-pb--forest { --pb-tint: rgba(42, 80, 64, .08);   --pb-edge: rgba(42, 80, 64, .18);  --pb-ink: #2A5040; --pb-deep: #1E3A2E }
.jp-pb--ink    { --pb-tint: rgba(31, 43, 38, .08);   --pb-edge: rgba(31, 43, 38, .18);  --pb-ink: #1F2B26; --pb-deep: #141C19 }
.jp-pb--amber  { --pb-tint: rgba(212, 148, 58, .14); --pb-edge: rgba(212, 148, 58, .30); --pb-ink: #8A5A1E; --pb-deep: #5C3B10 }
.jp-pb--chilli { --pb-tint: rgba(190, 74, 48, .12);  --pb-edge: rgba(190, 74, 48, .26);  --pb-ink: #8E3A22; --pb-deep: #5E2415 }
.jp-pb--leaf   { --pb-tint: rgba(90, 138, 78, .12);  --pb-edge: rgba(90, 138, 78, .26);  --pb-ink: #3D6B34; --pb-deep: #274722 }

/* ---- the mark ------------------------------------------------------- */
.jp-pb__mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--pb-tint, rgba(42, 80, 64, .07));
  box-shadow: inset 0 0 0 1px var(--pb-edge, rgba(42, 80, 64, .16));
  overflow: hidden;
  transition: transform .38s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .38s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  max-width: 100%;
}

.jp-pb__mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* The line drawings are dark green on white; multiply drops the white so the
     tinted dish shows through instead of a white square inside a circle. */
  mix-blend-mode: multiply;
}

.jp-pb__mark--xs { width: 26px; height: 26px } .jp-pb__mark--xs img { padding: 3px }
.jp-pb__mark--sm { width: 34px; height: 34px } .jp-pb__mark--sm img { padding: 4px }
.jp-pb__mark--md { width: 44px; height: 44px } .jp-pb__mark--md img { padding: 6px }
.jp-pb__mark--lg { width: 64px; height: 64px } .jp-pb__mark--lg img { padding: 8px }

/* Lift on the card's hover, not the mark's own — the whole card is the target. */
.jp-card:hover .jp-pb__mark,
.jp-product-card:hover .jp-pb__mark,
a:hover > .jp-pb .jp-pb__mark {
  transform: scale(1.09) rotate(-4deg);
  box-shadow: inset 0 0 0 1px var(--pb-edge), 0 6px 14px -8px rgba(20, 40, 32, .5);
}

/* ---- the copy ------------------------------------------------------- */
.jp-pb__body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.jp-pb__angle {
  font-size: 12.5px;
  line-height: 1.35;
  font-weight: 600;
  color: var(--pb-ink, #2A5040);
}

.jp-pb__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.jp-pb__chip {
  padding: 3px 8px;
  border-radius: 99px;
  background: var(--pb-tint, rgba(42, 80, 64, .07));
  /* Chips use a DEEPER ink than the angle line above them. The per-product
     tone colours (leaf, amber) are chosen to sit well as a mark, but at 10.5px
     on a pale tinted pill they measured under the readable threshold — the
     smallest text on the card was the least legible. --pb-deep is the same hue
     taken darker; the tint and the mark keep the light tone. */
  color: var(--pb-deep, #1F3A30);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}

.jp-pb__chip--alt {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--pb-edge, rgba(42, 80, 64, .16));
}

/* ---- sizes ---------------------------------------------------------- */
.jp-pb--sm .jp-pb__angle { font-size: 11.5px }
.jp-pb--sm .jp-pb__chip  { font-size: 9.5px; padding: 2px 7px }
.jp-pb--lg .jp-pb__angle { font-size: 14px }
.jp-pb--lg .jp-pb__chip  { font-size: 11.5px }

/* Injected into generated upsell cards, between the product title and its
   price, so it needs its own breathing room — the card's own rules know
   nothing about it. */
.jp-upsell-angle {
  /* !important because the generated card sets `display:none` on descendants
     it did not author, and that rule is more specific than anything we can
     write against its per-instance class names. Verified: without this the row
     is in the DOM at 0×0. */
  display: flex !important;
  align-items: center;
  margin: 7px 0 9px;
}

@media (max-width: 749px) {
  .jp-pb { gap: 8px }
  .jp-pb__angle { font-size: 11.5px }
  /* Two chips wrap to two lines on a narrow card and push the price down, so
     the secondary chip stands down on phones. */
  .jp-pb__chip--alt { display: none }
}

@media (prefers-reduced-motion: reduce) {
  .jp-pb__mark { transition: none }
  .jp-card:hover .jp-pb__mark,
  .jp-product-card:hover .jp-pb__mark { transform: none }
}

/* ================= jp-tables.css ================= */
/* Tables, everywhere, without touching the markup.

   The site's tables come from three unrelated places — the theme's compare
   drawer, AI-generated nutrition grids inside product descriptions, and raw
   <table> HTML pasted into policy pages and metafields. None of that markup is
   ours to restructure, and the class names carry generated suffixes
   (.ai-bilona-nutrition-stat-value-amnd1vxn3ywput…), so this styles by element
   and by class *prefix* instead.

   The pattern is lifted from the Apex playbook and adapted from print to web:
   an uppercase letter-spaced header in the accent, hairline rules instead of
   boxes, generous cell padding, and the row — not the border — carrying the
   emphasis. Print used mm and a dark ground; here it is rem on paper.

   What it adds beyond looking right:
     · every table scrolls inside its own container, so a six-column spec table
       never makes the whole page scroll sideways on a phone
     · a fading right edge that appears only while there is more to scroll,
       so a cut-off table looks scrollable instead of broken
     · row hover and a sticky header, so a long table stays readable
     · on phones, stacked rows with the header repeated as each cell's label
       via the data-label attribute jp-tables.js writes
     · numeric cells align right with tabular figures, so columns of numbers
       line up instead of drifting */

:root {
  --jpt-ink:    #1F2B26;
  --jpt-head:   #2A5040;
  --jpt-accent: #D4943A;
  --jpt-line:   rgba(42, 80, 64, .13);
  --jpt-zebra:  rgba(42, 80, 64, .022);
  --jpt-hover:  rgba(212, 148, 58, .07);
  --jpt-sand:   #FAF7F2;
}

/* ---- the scroll shell (added by jp-tables.js) ------------------------ */
.jp-tbl-scroll {
  position: relative;
  margin: 22px 0;
}

.jp-tbl-scroll__pane {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  border-radius: 14px;
  box-shadow: 0 0 0 1px var(--jpt-line);
}

/* The fade is only meaningful while content is actually clipped; the script
   toggles these classes as the pane scrolls. */
.jp-tbl-scroll::before,
.jp-tbl-scroll::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 34px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .25s ease;
  z-index: 2;
  border-radius: 14px;
}
.jp-tbl-scroll::before {
  left: 0;
  background: linear-gradient(90deg, #fff, rgba(255, 255, 255, 0));
}
.jp-tbl-scroll::after {
  right: 0;
  background: linear-gradient(270deg, #fff, rgba(255, 255, 255, 0));
}
.jp-tbl-scroll.has-left::before  { opacity: 1 }
.jp-tbl-scroll.has-right::after  { opacity: 1 }

/* A quiet hint that the thing moves, shown once and only when it overflows. */
.jp-tbl-scroll__hint {
  position: absolute;
  right: 12px;
  top: -11px;
  z-index: 3;
  padding: 3px 9px;
  border-radius: 99px;
  background: var(--jpt-head);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .3s ease, transform .3s ease;
}
.jp-tbl-scroll.has-right .jp-tbl-scroll__hint { opacity: 1; transform: none }
.jp-tbl-scroll.is-scrolled .jp-tbl-scroll__hint { opacity: 0 }

/* ---- real <table> elements ------------------------------------------ */
.jp-tbl {
  width: 100%;
  border-collapse: collapse;
  /* Opaque, always. A table dropped onto a dark section (the FSSAI standards
     table sits on the forest-green band on /pages/quality) inherited that
     background and rendered as a large empty box — dark text on dark ground.
     The table brings its own paper with it. */
  background: #fff;
  font-size: 14px;
  line-height: 1.5;
  color: var(--jpt-ink);
}

/* The wrapper too, so the rounded corners and the gap between rows are paper
   rather than whatever is behind them. */
.jp-tbl-scroll__pane { background: #fff }

/* On a dark band the fade edges must fade to the table's paper, not to white
   against white — same colour, but stated here so the intent survives edits. */
.jp-tbl-scroll::before { background: linear-gradient(90deg, #fff, rgba(255,255,255,0)) }
.jp-tbl-scroll::after  { background: linear-gradient(270deg, #fff, rgba(255,255,255,0)) }

.jp-tbl caption {
  caption-side: top;
  text-align: left;
  padding: 0 0 10px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--jpt-accent);
}

.jp-tbl thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--jpt-sand);
  text-align: left;
  padding: 13px 16px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--jpt-head);
  border-bottom: 1px solid var(--jpt-line);
  white-space: nowrap;
}

.jp-tbl tbody td,
.jp-tbl tbody th {
  padding: 13px 16px;
  border-bottom: 1px solid var(--jpt-line);
  vertical-align: top;
}

/* Row headers (first column of a spec table) carry the weight, so the eye can
   scan down the left edge without the border doing the work. */
.jp-tbl tbody th {
  text-align: left;
  font-weight: 600;
  color: var(--jpt-head);
  white-space: nowrap;
}

.jp-tbl tbody tr:nth-child(even) { background: var(--jpt-zebra) }

.jp-tbl tbody tr {
  transition: background .18s ease;
}
.jp-tbl tbody tr:hover { background: var(--jpt-hover) }
.jp-tbl tbody tr:last-child td,
.jp-tbl tbody tr:last-child th { border-bottom: 0 }

/* Numbers: right-aligned, tabular, so a column of figures is comparable.
   jp-tables.js sets data-num on cells whose whole column parses as numeric. */
.jp-tbl td[data-num],
.jp-tbl th[data-num] {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* A ✓ / ✗ comparison column reads better as colour than as a glyph alone. */
.jp-tbl td[data-yes] { color: #2F7D53; font-weight: 700 }
.jp-tbl td[data-no]  { color: #A6543C; font-weight: 700 }

/* Highlight the column the visitor is pointing at — in a comparison table,
   knowing which column you are reading is most of the value. */
.jp-tbl td.is-col-on,
.jp-tbl th.is-col-on {
  background: var(--jpt-hover);
}

/* ---- phones: stacked rows with repeated headers --------------------- */
@media (max-width: 749px) {
  .jp-tbl--stack thead { display: none }

  .jp-tbl--stack tbody tr {
    display: block;
    padding: 12px 14px;
    border: 1px solid var(--jpt-line);
    border-radius: 12px;
    margin-bottom: 10px;
    background: #fff;
  }
  .jp-tbl--stack tbody tr:nth-child(even) { background: #fff }

  .jp-tbl--stack tbody td,
  .jp-tbl--stack tbody th {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    padding: 7px 0;
    border-bottom: 1px dashed var(--jpt-line);
    text-align: right;
  }
  .jp-tbl--stack tbody tr > *:last-child { border-bottom: 0 }

  /* The header text, repeated per cell, so a stacked row still says what each
     value means. jp-tables.js copies it into data-label. */
  .jp-tbl--stack tbody td::before,
  .jp-tbl--stack tbody th::before {
    content: attr(data-label);
    flex: 0 0 auto;
    max-width: 52%;
    text-align: left;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--jpt-head);
  }
  /* A stacked table does not scroll, so the shell chrome stands down. */
  .jp-tbl--stack ~ .jp-tbl-scroll__hint,
  .jp-tbl-scroll:has(.jp-tbl--stack)::before,
  .jp-tbl-scroll:has(.jp-tbl--stack)::after { display: none }
  .jp-tbl-scroll:has(.jp-tbl--stack) .jp-tbl-scroll__pane {
    box-shadow: none; overflow-x: visible;
  }
}

/* ---- the AI nutrition stat grids on product pages ------------------- */
/* Generated class names carry a per-block suffix, so match on prefix. Cards
   get the same hairline-and-accent treatment as the tables, and the figures
   get the same tabular alignment, so a PDP reads as one document. */
[class^="ai-bilona-nutrition-stats"],
[class*=" ai-bilona-nutrition-stats"] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 10px;
}

[class^="ai-bilona-nutrition-stat-"],
[class*=" ai-bilona-nutrition-stat-"] {
  min-width: 0;
}

/* The value/label pair, wherever it sits, becomes a proper stat tile. */
[class^="ai-bilona-nutrition-stat-value"],
[class*=" ai-bilona-nutrition-stat-value"] {
  display: block;
  font-family: var(--jp-serif, 'Playfair Display', Georgia, serif);
  font-size: clamp(22px, 3vw, 30px);
  font-weight: 600;
  line-height: 1.05;
  color: var(--jpt-head);
  font-variant-numeric: tabular-nums;
}

[class^="ai-bilona-nutrition-stat-label"],
[class*=" ai-bilona-nutrition-stat-label"] {
  display: block;
  margin-top: 3px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--jp-muted, #63695E);
}

/* The card itself, never its wrapper. A prefix match on
   "ai-bilona-nutrition-card" also catches `-card-wrapper`, which exists only
   to lay the card out and is 90px wider than it — so the ring drew a rectangle
   in open space beside the jar, which is the stray outline reported near the
   nutrition label. Measured: wrapper 610px, card 520px, both ringed. */
[class^="ai-bilona-nutrition-card"]:not([class*="-wrapper"]),
[class*=" ai-bilona-nutrition-card"]:not([class*="-wrapper"]) {
  border-radius: 16px;
  box-shadow: 0 0 0 1px var(--jpt-line);
  transition: transform .35s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .35s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}
[class^="ai-bilona-nutrition-card"]:not([class*="-wrapper"]):hover,
[class*=" ai-bilona-nutrition-card"]:not([class*="-wrapper"]):hover {
  transform: translateY(-2px);
  box-shadow: 0 0 0 1px rgba(42, 80, 64, .22), 0 14px 30px -20px rgba(20, 40, 32, .5);
}

/* ---- the theme's compare drawer ------------------------------------- */
.compare-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
.compare-table th {
  background: var(--jpt-sand);
  padding: 12px 14px;
  text-align: left;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--jpt-head);
  border-bottom: 1px solid var(--jpt-line);
}
.compare-table td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--jpt-line);
}
.compare-table tbody tr:hover { background: var(--jpt-hover) }

@media (prefers-reduced-motion: reduce) {
  .jp-tbl tbody tr,
  [class*="ai-bilona-nutrition-card"] { transition: none }
  [class*="ai-bilona-nutrition-card"]:hover { transform: none }
  .jp-tbl-scroll::before,
  .jp-tbl-scroll::after,
  .jp-tbl-scroll__hint { transition: none }
}

/* ================= jp-flip.css ================= */
/* Flip — banners and logos that turn over.

   Both faces occupy the same grid cell, so the element is as tall as its
   tallest face and neither can clip. The rotation is on a wrapper, the faces
   are backface-hidden, and nothing is ever hidden by opacity — a flip that
   fails to run leaves the front face on screen, which is the whole point of
   the site's no-hidden-content rule. */

.jp-flip {
  position: relative;
  perspective: 1200px;
  outline: none;
}

.jp-flip__inner {
  display: grid;
  transform-style: preserve-3d;
  transition: transform .78s cubic-bezier(.2, .8, .2, 1);
  /* A ratio can be forced for a banner strip so the two faces cannot cause a
     layout shift when the taller one arrives. */
  aspect-ratio: var(--jpf-ratio, auto);
}

.jp-flip__face {
  grid-area: 1 / 1;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  min-width: 0;
  /* The face is a column, not a bare box. Without this the caller's own
     stacked children (a claim and its sub-line) collapse onto one line,
     because the face is a grid item rather than the block context they were
     written for. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}

/* The back starts pre-rotated so that a half-turn of the wrapper lands it
   facing the viewer. */
.jp-flip--y .jp-flip__face--back { transform: rotateY(180deg) }
.jp-flip--x .jp-flip__face--back { transform: rotateX(180deg) }

.jp-flip--y.is-flipped .jp-flip__inner,
.jp-flip--y:hover .jp-flip__inner,
.jp-flip--y:focus-visible .jp-flip__inner { transform: rotateY(180deg) }

.jp-flip--x.is-flipped .jp-flip__inner,
.jp-flip--x:hover .jp-flip__inner,
.jp-flip--x:focus-visible .jp-flip__inner { transform: rotateX(180deg) }

/* An auto-flipper must not also react to the pointer — the two clocks fight
   and it reads as a glitch. */
.jp-flip--auto:hover .jp-flip__inner { transform: none }
.jp-flip--auto.jp-flip--y.is-flipped .jp-flip__inner { transform: rotateY(180deg) }
.jp-flip--auto.jp-flip--x.is-flipped .jp-flip__inner { transform: rotateX(180deg) }

.jp-flip:focus-visible {
  border-radius: 12px;
  box-shadow: 0 0 0 3px rgba(212, 148, 58, .55);
}

/* Reduced motion: cross-fade rather than rotate, and the auto clock stops —
   a face that changes on a timer is the thing this preference is for. */
@media (prefers-reduced-motion: reduce) {
  .jp-flip__inner { transition: none; transform: none !important }
  .jp-flip__face { transition: opacity .3s linear }
  .jp-flip__face--back { opacity: 0; transform: none !important }
  .jp-flip.is-flipped .jp-flip__face--front { opacity: 0 }
  .jp-flip.is-flipped .jp-flip__face--back  { opacity: 1 }
}

/* ================= jp-alive.css ================= */
/* jp-alive — one interaction layer for every page on the site.

   Why this exists
   ---------------
   The site is assembled from generated `ai_gen_block_*` sections whose Liquid we
   do not edit, plus theme sections, plus my own. Upgrading them one at a time
   meant fifteen separate patches and pages that still looked untouched. This is
   the opposite approach: attach behaviour by *shape* — headings, cards, media,
   lists, tables, quotes, stats, buttons — so every page gets it at once,
   including pages nobody has looked at yet.

   Everything here is transform/opacity only, and every hidden state is gated on
   `html.jp-alive`, which jp-alive.js adds only after it can observe. If the
   script never runs, nothing is hidden and nothing is lost.

   Sections of this file:
     1. tokens
     2. reveal (the one motion primitive everything else composes with)
     3. headings — the accent rule that draws itself in
     4. cards — lift, edge-light, media zoom
     5. images and media — reveal-with-scale, never a jump
     6. lists — staggered entry with a mark
     7. quotes and testimonials
     8. figures and stats
     9. buttons and links — the shared press/hover language
    10. dividers and rails
    11. accessibility and reduced motion
*/

/* ---- 1. tokens ------------------------------------------------------- */
:root {
  --jpa-ink:    #1F2B26;
  --jpa-forest: #2A5040;
  --jpa-amber:  #D4943A;
  --jpa-sand:   #FAF7F2;
  --jpa-line:   rgba(42, 80, 64, .14);
  --jpa-ease:   cubic-bezier(.16, 1, .3, 1);
  --jpa-fast:   .22s;
  --jpa-mid:    .42s;
  --jpa-slow:   .62s;
}

/* ---- 2. reveal ------------------------------------------------------- */
/* Nothing is hidden waiting to be revealed.

   This layer twice took whole sections off the page: an element marked for
   reveal whose observer never delivered stayed at opacity 0 forever, and the
   only symptom was "the page is broken". The entry animation now starts from
   the hidden frame instead of the element resting there — same motion, but a
   missed observer costs the animation rather than the content.

   Kept as a variable so the intent survives: if this ever needs to go back to
   hide-first, it needs a watchdog with it. */
html.jp-alive [data-jpa] {
  opacity: 1;
  /* `transition: all` is inherited from the theme on some headings, and it
     latches whatever value the entry animation was showing when it was
     interrupted — a heading that got `.jpa-in` while offscreen ended up
     holding the keyframe's 0% frame permanently, with no CSS rule anywhere
     saying opacity 0. Confining transitions to the properties this layer
     actually animates stops the animation and the transition fighting over
     the same value. */
  transition-property: transform, box-shadow, background-color, color;
}
/* The stagger lives INSIDE the keyframe, not in animation-delay.

   `animation-delay` with `fill-mode: both` paints the element at the opening
   frame — opacity 0 — for the whole delay. With a staggered row that means
   every element after the first sits invisible waiting its turn, which is
   indistinguishable from a section that failed to load. Giving each element a
   slightly longer duration instead produces the same sequence, and nothing is
   ever painted at zero waiting for a clock. */
/* The rise animation is OFF for text.

   Six attempts to make an opacity-based entry animation safe here, and
   headings kept ending up permanently invisible on the quality, community and
   FAQ pages — an interrupted animation holding its 0% frame, with no CSS rule
   anywhere setting zero and nothing in the DOM to point at. Each fix removed
   one path to it and another remained.

   The trade is not close: an entry animation is worth a few percent of polish,
   and a missing headline costs the page. So nothing that contains words
   animates its opacity any more. Movement without opacity cannot hide
   anything — at worst an element is a few pixels out of place for 600ms. */
html.jp-alive [data-jpa].jpa-in {
  animation: jpa-lift calc(var(--jpa-slow) + var(--jpa-i, 0) * 70ms) var(--jpa-ease);
  opacity: 1;
}
@keyframes jpa-lift {
  0%   { transform: translate3d(0, 18px, 0) }
  100% { transform: none }
}
@keyframes jpa-rise {
  0%   { opacity: 0; transform: translate3d(0, 22px, 0) }
  38%  { opacity: 0; transform: translate3d(0, 22px, 0) }
  100% { opacity: 1; transform: none }
}

/* Sideways entry for things that live in a row — a rail, a stat strip.
   Like the base case: the resting state is visible, the animation supplies
   the entry. */
html.jp-alive [data-jpa="side"].jpa-in { animation-name: jpa-slide }
@keyframes jpa-slide {
  0%   { transform: translate3d(-18px, 0, 0) }
  100% { transform: none }
}

/* Scale-in for media. Images may fade — an image stuck at zero is a gap, not a
   missing sentence, and the effect is worth more here. */
html.jp-alive [data-jpa="media"].jpa-in { animation-name: jpa-zoom }
@keyframes jpa-zoom {
  0%   { opacity: 0; transform: scale(.965) }
  38%  { opacity: 0; transform: scale(.965) }
  100% { opacity: 1; transform: none }
}

/* ---- 2b. text ---------------------------------------------------------
   Copy arrives a line at a time rather than as a block.

   jp-alive.js wraps each visual line of a heading or paragraph in a
   `.jpa-line > span`, measured after layout so the split follows the real
   wrap points at that width. The outer span clips; the inner one rises
   through it. That is the whole trick — no per-character work, no layout
   thrash, and it reads as the sentence being written rather than a box
   fading in.

   Everything is gated on `.jpa-split`, which the script adds only after it
   has successfully rebuilt the element. A failure leaves the original text
   untouched and fully visible. */
.jpa-split .jpa-line {
  display: block;
  /* Clips the line as it sweeps up from below. Only while the animation is
     actually running: a permanent clip on a line that is visible by default
     will crop descenders and any inline mark that overhangs the box. */
  overflow: visible;
}
/* `.jpa-in` is permanent, so it cannot own the clip — the script removes
   .jpa-clip once the animation has finished and the line is at rest. */
html.jp-alive .jpa-split.jpa-clip .jpa-line { overflow: hidden   max-width: 100%;
}
.jpa-split .jpa-line > span {
  display: block;
  will-change: transform;
}

/* NOT hidden by default.

   The first version hid every split line until `.jpa-in` arrived, and on four
   pages that class never came — so section headings, FAQ group titles and the
   cart's "Your cart is empty" were all invisible. Same failure mode as the
   comparison table, one layer up: a reveal that hides first and un-hides from
   an observer takes the content down with it whenever the observer misses.

   The line is visible; `.jpa-in` only adds the entry animation, which starts
   from the hidden frame and ends at the visible one. If the observer never
   fires, the text is simply there. */
html.jp-alive .jpa-split.jpa-in .jpa-line > span {
  animation: jpa-linerise calc(560ms + var(--l, 0) * 90ms) var(--jpa-ease);
}
@keyframes jpa-linerise {
  0%   { transform: translate3d(0, 105%, 0); opacity: 0 }
  30%  { transform: translate3d(0, 105%, 0); opacity: 0 }
  100% { transform: none; opacity: 1 }
}

/* A quieter version for body copy: it lifts rather than sweeping up from
   below, so a long paragraph does not read as a slot machine. Same rule as
   above — visible by default, animated only on arrival. */
.jpa-split[data-jpa-text="body"] .jpa-line > span { will-change: opacity }
html.jp-alive .jpa-split[data-jpa-text="body"].jpa-in .jpa-line > span {
  animation: jpa-linefade calc(520ms + var(--l, 0) * 55ms) var(--jpa-ease);
}
@keyframes jpa-linefade {
  0%   { transform: translate3d(0, 14px, 0); opacity: 0 }
  25%  { transform: translate3d(0, 14px, 0); opacity: 0 }
  100% { transform: none; opacity: 1 }
}

/* ---- 2c. the theme's own reveal --------------------------------------
   The theme animates page-banner headings via `animate-element` + a
   `data-animate` type, on the same hide-first pattern. On /pages/faq that left
   "Frequently Asked Questions" permanently at opacity 0.

   Not ours to restructure, so this states the floor: a heading carrying words
   is visible regardless of whether its animation ever ran. The theme's
   transform still plays; only the ability to hide the text is removed. */
h1[animate-element],
h2[animate-element],
h3[animate-element],
.page-banner-heading {
  opacity: 1 !important;
}

/* ---- 3. headings ----------------------------------------------------- */
/* A short accent rule under a section heading, drawn in when it arrives.
   Applied via a class rather than to every h2 on the page, so the script can
   pick only real section headings. */
.jpa-head {
  position: relative;
  padding-bottom: 14px;
}
/* A single hairline in the brand's amber, not a two-colour gradient.

   The green-to-amber bar was doing the same trick on every heading and every
   card on the site, and a rainbow sweep repeated forty times stops reading as
   craft and starts reading as a template. One quiet rule, one colour, drawn
   once — the restraint is what makes it look considered. */
.jpa-head::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 42px;
  height: 1.5px;
  border-radius: 2px;
  background: var(--jpa-amber);
  opacity: .85;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .7s var(--jpa-ease) .1s;
}
.jpa-head.jpa-in::after,
.jpa-in .jpa-head::after { transform: scaleX(1) }
/* Centred headings get a centred rule. */
.jpa-head[data-jpa-center]::after { left: 50%; transform: translateX(-50%) scaleX(0) }
.jpa-head[data-jpa-center].jpa-in::after { transform: translateX(-50%) scaleX(1) }

/* ---- 4. cards -------------------------------------------------------- */
.jpa-card {
  position: relative;
  isolation: isolate;
  transition: transform var(--jpa-mid) var(--jpa-ease),
              box-shadow var(--jpa-mid) var(--jpa-ease);
}
/* No coloured bar on hover.

   The gradient stripe across the top of every card was the single most
   repeated gesture on the site, and on the dark bands it read as a stray
   coloured line rather than an accent. A card that lifts, gains a hairline in
   the brand green and casts a longer shadow says "interactive" without
   decorating itself — and it composes with cards that already have their own
   design instead of overprinting them. */
.jpa-card:hover,
.jpa-card:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 0 0 1px rgba(42, 80, 64, .16),
              0 22px 44px -28px rgba(20, 40, 32, .55);
  outline: none;
}

.jpa-card[tabindex]:focus-visible {
  box-shadow: 0 0 0 3px rgba(212, 148, 58, .4),
              0 22px 44px -28px rgba(20, 40, 32, .55);
}

/* A card's own image zooms inside its frame. */
.jpa-card img { transition: transform .55s var(--jpa-ease) }
.jpa-card:hover img { transform: scale(1.05) }

/* ---- 5. media -------------------------------------------------------- */
/* Any standalone image in content gets a soft frame and a lift, so a page of
   flat pictures reads as a designed page. */
.jpa-media {
  overflow: hidden;
  border-radius: 16px;
  transition: transform var(--jpa-mid) var(--jpa-ease),
              box-shadow var(--jpa-mid) var(--jpa-ease);
  max-width: 100%;
}
.jpa-media:hover {
  transform: translateY(-3px);
  box-shadow: 0 24px 48px -30px rgba(20, 40, 32, .5);
}

/* ---- 6. lists -------------------------------------------------------- */
/* Bullet lists in long-form pages become a marked, staggered list. */
.jpa-list { list-style: none; padding-left: 0 }
.jpa-list > li {
  position: relative;
  padding-left: 26px;
  margin-bottom: 10px;
  line-height: 1.6;
}
.jpa-list > li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: .62em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--jpa-amber);
  box-shadow: 0 0 0 3px rgba(212, 148, 58, .16);
  transform: scale(0);
  transition: transform .45s var(--jpa-ease);
  transition-delay: calc(var(--jpa-i, 0) * 60ms);
}
.jpa-list > li.jpa-in::before,
.jpa-list.jpa-in > li::before { transform: scale(1) }

/* ---- 7. quotes ------------------------------------------------------- */
.jpa-quote {
  position: relative;
  padding: 22px 24px 22px 56px;
  border-radius: 18px;
  background: var(--jpa-sand);
  box-shadow: inset 0 0 0 1px var(--jpa-line);
}
.jpa-quote::before {
  content: '\201C';
  position: absolute;
  left: 18px;
  top: 6px;
  font-family: var(--jp-serif, Georgia, serif);
  font-size: 54px;
  line-height: 1;
  color: var(--jpa-amber);
  opacity: .5;
}

/* ---- 8. figures ------------------------------------------------------ */
.jpa-num { font-variant-numeric: tabular-nums }

/* ---- 9. buttons ------------------------------------------------------ */
/* One press language for every button and CTA on the site. */
.jpa-btn {
  position: relative;
  overflow: hidden;
  transition: transform var(--jpa-fast) var(--jpa-ease),
              filter var(--jpa-fast) ease,
              box-shadow var(--jpa-fast) var(--jpa-ease);
  max-width: 100%;
}
.jpa-btn:hover { transform: translateY(-2px); filter: brightness(1.06) }
.jpa-btn:active { transform: translateY(0) scale(.985) }

/* Pointer-anchored ripple, set by the script. */
.jpa-btn::after {
  content: '';
  position: absolute;
  left: var(--jpa-rx, 50%);
  top: var(--jpa-ry, 50%);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  transform: translate(-50%, -50%) scale(1);
  pointer-events: none;
}
.jpa-btn.jpa-press::after {
  animation: jpa-ripple .55s var(--jpa-ease) forwards;
}
@keyframes jpa-ripple {
  from { opacity: .26; transform: translate(-50%, -50%) scale(1) }
  to   { opacity: 0;   transform: translate(-50%, -50%) scale(34) }
}

/* Text links get an underline that draws from the left. */
.jpa-link {
  position: relative;
  text-decoration: none;
}
.jpa-link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .32s var(--jpa-ease);
}
.jpa-link:hover::after { transform: scaleX(1) }

/* ---- 10. rails ------------------------------------------------------- */
/* A horizontally scrolling row gets edge fades so it reads as scrollable. */
.jpa-rail { position: relative }
.jpa-rail::before,
.jpa-rail::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 40px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .25s ease;
  z-index: 2;
}
/* The fade must be the colour the rail actually sits on. Hardcoding #fff put a
   white smear down the right edge of the comparison table on the dark green
   band — it covered three rows of white text and read as "the rows are empty".
   jp-alive.js measures the rail's effective background and sets --jpa-fade;
   white is only the fallback for a rail on a white page. */
.jpa-rail::before {
  left: 0;
  background: linear-gradient(90deg, var(--jpa-fade, #fff), transparent);
}
.jpa-rail::after {
  right: 0;
  background: linear-gradient(270deg, var(--jpa-fade, #fff), transparent);
}
.jpa-rail.jpa-has-l::before { opacity: 1 }
.jpa-rail.jpa-has-r::after  { opacity: 1 }

/* An element can also opt out entirely — a rail that styles its own edges. */
[data-jpa-no-rail] .jpa-rail::before,
[data-jpa-no-rail] .jpa-rail::after,
.jpa-rail[data-jpa-no-rail]::before,
.jpa-rail[data-jpa-no-rail]::after { display: none }

/* ---- 11. reduced motion --------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html.jp-alive [data-jpa] { opacity: 1; transform: none }
  html.jp-alive [data-jpa].jpa-in { animation: none }
  /* Split text stays split — the markup is harmless — but it never moves. */
  html.jp-alive .jpa-split .jpa-line > span,
  html.jp-alive .jpa-split:not(.jpa-in) .jpa-line > span {
    transform: none; opacity: 1; animation: none;
  }
  .jpa-card, .jpa-media, .jpa-btn, .jpa-card img { transition: none }
  .jpa-card:hover, .jpa-media:hover, .jpa-btn:hover { transform: none }
  .jpa-head::after, .jpa-card::before, .jpa-list > li::before { transition: none; transform: scaleX(1) }
  .jpa-list > li::before { transform: scale(1) }
  .jpa-btn.jpa-press::after { animation: none }
}

/* ================= jp-quality.css ================= */
/* /pages/quality — the FSSAI testing section, made to carry its weight.

   What was wrong
   --------------
   The section renders six real equipment cards with genuine copy (butyro-
   refractometer, moisture analyser, Baudouin test kit…), but above them sits a
   `top-png-wrap--placeholder`: an image slot with no image set. On the dark
   forest band that placeholder is a ~360px empty box, so the page reads as
   broken and the six good cards get pushed below the fold. The owner's verdict
   on this page — "bekaar" — was mostly that box.

   What this does
   --------------
   Hides the empty placeholder, then turns the equipment grid into the thing the
   copy deserves: numbered instrument cards on paper, legible on the dark band,
   that lift and reveal as you scroll. The FSSAI parameters get the site's table
   treatment via jp-tables, so the standards read as data rather than prose.

   Nothing here invents content. Every word on screen is already in the section
   settings; this only makes it visible and worth reading.
*/

/* ---- kill the empty image slot -------------------------------------- */
/* Only the placeholder variant — a real image, once set, renders normally. */
[class*="jp-fssai-test__top-png-wrap--placeholder"] { display: none !important }
[class*="jp-fssai-test__top-png-placeholder-label"] { display: none !important }

/* ---- the section on its dark band ------------------------------------ */
[class*="jp-fssai-test__container"] { position: relative }

[class*="jp-fssai-test__kicker"] {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  opacity: .8;
}

/* ---- equipment cards -------------------------------------------------- */
[class*="jp-fssai-test__grid"] {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
  gap: 14px;
  margin-top: 26px;
  counter-reset: jpq;
}

/* Exact class, not [class*="jp-fssai-test__card"].

   The substring selector also matched .jp-fssai-test__card-content, the card's
   own child, so every card painted a SECOND paper box inside itself — smaller,
   offset, with its own shadow and its own ::before ordinal stuck at 01. The
   owner reported it as a number that had not been moved out of the card; it
   was really the whole card rule applying twice, once nested. */
.jp-fssai-test__card {
  counter-increment: jpq;
  position: relative;
  overflow: hidden;
  padding: 22px 20px 20px;
  border-radius: 18px;
  /* Paper, so dark-on-dark text is impossible regardless of the band colour
     the section is placed on. */
  background: rgba(255, 255, 255, .96);
  color: #1F2B26;
  box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset,
              0 18px 40px -30px rgba(0, 0, 0, .7);
  transition: transform .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}

/* The instrument number, as a quiet index rather than a decorative badge. */
.jp-fssai-test__card::before {
  content: counter(jpq, decimal-leading-zero);
  position: absolute;
  top: 16px;
  right: 18px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  color: rgba(42, 80, 64, .34);
  font-variant-numeric: tabular-nums;
}

/* Accent rule that draws in on hover, matching the site-wide card language. */
.jp-fssai-test__card::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, #2A5040, #D4943A);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .5s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}

.jp-fssai-test__card:hover,
.jp-fssai-test__card:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .6) inset,
              0 26px 50px -28px rgba(0, 0, 0, .8);
  outline: none;
}
.jp-fssai-test__card:hover::after,
.jp-fssai-test__card:focus-visible::after { transform: scaleX(1) }

.jp-fssai-test__card h4 {
  margin: 0 34px 8px 0;
  font-family: var(--jp-serif, 'Playfair Display', Georgia, serif);
  font-size: 18px;
  font-weight: 600;
  line-height: 1.2;
  color: #2A5040;
}

[class*="jp-fssai-test__text"] {
  font-size: 13.5px;
  line-height: 1.6;
  color: #55605A;
}
[class*="jp-fssai-test__text"] p { margin: 0 }

@media (max-width: 749px) {
  [class*="jp-fssai-test__grid"] { grid-template-columns: 1fr; gap: 10px }
  .jp-fssai-test__card { padding: 18px 16px 16px }
  .jp-fssai-test__card h4 { font-size: 16.5px }
}

@media (prefers-reduced-motion: reduce) {
  .jp-fssai-test__card { transition: none }
  .jp-fssai-test__card:hover { transform: none }
  .jp-fssai-test__card::after { display: none }
}

/* ================= jp-motion.css ================= */
/* JustPure motion + surface system — site-wide, desktop and mobile.
 *
 * One file, loaded after theme.css, so every page and section picks it up
 * without editing 39 sections. Built on the brand palette already in the theme
 * (forest #2A5040, amber #D4943A, cream #F5F3EF) rather than a new look.
 *
 * Everything here degrades to plain layout: no JS required for the surfaces,
 * and all motion is disabled under prefers-reduced-motion.
 */

:root {
  --jp-forest: #2a5040;
  --jp-forest-deep: #1d3a2e;
  --jp-amber: #d4943a;
  --jp-cream: #f5f3ef;
  --jp-ink: #1a1a1a;

  --jp-ease: cubic-bezier(.22, .61, .36, 1);
  --jp-ease-out: cubic-bezier(.16, 1, .3, 1);

  --jp-lift: 0 18px 40px -18px rgba(42, 80, 64, .38);
  --jp-lift-soft: 0 10px 26px -14px rgba(42, 80, 64, .28);
  --jp-ring: 0 0 0 1px rgba(42, 80, 64, .10);
}

/* ---------------------------------------------------------------- reveal
 * Sections fade up as they enter the viewport. Driven by IntersectionObserver
 * in jp-motion.js, which adds .jp-in. Without JS the content is simply visible.
 */
/* Nothing rests at zero.

   This was the last hide-first reveal on the site, and it is the one that was
   actually taking headings off the page: "How Pure Bilona Ghee Is Made", every
   step title on the bilona page, "Nothing to Hide", "Frequently Asked
   Questions". Two separate faults, both of which produce a permanently blank
   element:

     1. `.jp-reveal { opacity: 0 }` waits for jp-motion.js to add `.jp-in`. If
        that observer never delivers — offscreen at init, a throttled tab, a
        section injected late — the content is gone with no error anywhere.

     2. the child stagger used `animation-delay` with `fill-mode: backwards`,
        which paints each child at the keyframe's opening frame (opacity 0) for
        the whole delay. A six-child grid held five children invisible by
        design, and any interruption froze them there.

   Both are now animation-only: the element is visible, and `.jp-in` supplies
   an entry that starts from the hidden frame. The stagger moved inside the
   keyframe — same sequence, no delay, nothing painted at zero waiting. */
.jp-reveal,
.jp-reveal.jp-in,
.jp-reveal.jp-in > * {
  opacity: 1;
  /* Not `all`: a broad transition latches whatever frame the entry animation
     was on if it is interrupted, which pinned headings at zero with no rule
     anywhere saying so. */
  transition-property: transform, box-shadow, color, background-color;
}

.jp-reveal.jp-in {
  animation: jpRiseIn .7s var(--jp-ease-out);
}

/* Children stagger so a grid assembles rather than snapping in as one slab.
   A delay is what actually reads as a cascade — every child moving at the same
   speed, each starting a beat after the one above. Equal-start/different-speed
   (what this was) makes them all land together and reads as one slab arriving
   softly, which is the thing the stagger was there to avoid.

   The delay is safe HERE and was not safe before, and the difference is the
   only reason this is allowed to change: the earlier version delayed a keyframe
   that animated OPACITY with fill-mode:backwards, so every waiting child was
   painted at opacity 0 — five invisible children by design, frozen there for
   good if anything interrupted. jpRiseIn now moves and never fades, and no
   fill-mode is set, so a waiting child sits at its natural position, fully
   visible, and simply has not started moving yet. Worst case for an interrupted
   or never-fired animation is text a few pixels out of place.

   ~70ms is the beat: fast enough that a six-item grid finishes in under half a
   second, slow enough to read as a sequence rather than a jitter. */
.jp-reveal.jp-in > * { animation: jpRiseIn .62s var(--jp-ease-out); }
.jp-reveal.jp-in > :nth-child(2) { animation-delay: 70ms }
.jp-reveal.jp-in > :nth-child(3) { animation-delay: 140ms }
.jp-reveal.jp-in > :nth-child(4) { animation-delay: 210ms }
.jp-reveal.jp-in > :nth-child(5) { animation-delay: 280ms }
.jp-reveal.jp-in > :nth-child(n+6) { animation-delay: 350ms }

/* Movement only, no opacity.

   Same decision as jp-alive: an interrupted opacity animation holds its 0%
   frame and the content is simply gone, with nothing in the CSS or the DOM to
   point at. A transform that is interrupted leaves an element a few pixels out
   of place, which nobody notices and nobody loses a headline to. */
@keyframes jpRiseIn {
  0%   { transform: translate3d(0, 14px, 0); }
  100% { transform: none; }
}

/* ------------------------------------------------------------- card lift
 * Applies to the product cards, bundle cards and any card-like tile. Uses
 * transform only, so it never triggers layout.
 */
.jp-product-card,
.jp-bundle__card,
.jp-cert-proof__item,
[class*="jp-brand-info-card"],
[class*="goes-well-with-card"] {
  transition: transform .38s var(--jp-ease), box-shadow .38s var(--jp-ease);
}

@media (hover: hover) and (min-width: 750px) {
  .jp-product-card:hover,
  .jp-bundle__card:hover,
  [class*="goes-well-with-card"]:hover {
    transform: translateY(-6px);
    box-shadow: var(--jp-lift);
  }

  /* The second product image already exists on the card; cross-fade it. */
  .jp-product-card .jp-hover-img {
    transition: opacity .45s var(--jp-ease), transform .8s var(--jp-ease);
  }

  .jp-product-card:hover .jp-hover-img { transform: scale(1.04); }
  .jp-product-card:hover .jp-primary-img { transform: scale(1.02); }
  .jp-product-card .jp-primary-img { transition: transform .8s var(--jp-ease); }
}

/* --------------------------------------------------------------- buttons
 * A single treatment for the primary actions across the site: a sheen that
 * sweeps once on hover, and a real pressed state on touch.
 */
.jp-add-btn,
.jp-bundle__btn,
.jp-sticky-buy__btn,
[class*="ai-custom-pdp-add-to-cart"],
[class*="ai-custom-pdp-buy-now"] {
  position: relative;
  overflow: hidden;
  transition: transform .18s var(--jp-ease), box-shadow .28s var(--jp-ease),
              filter .28s var(--jp-ease);
}

.jp-add-btn::after,
.jp-bundle__btn::after,
[class*="ai-custom-pdp-add-to-cart"]::after,
[class*="ai-custom-pdp-buy-now"]::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 -60%;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .30), transparent);
  transform: skewX(-18deg);
  opacity: 0;
  pointer-events: none;
}

@media (hover: hover) {
  .jp-add-btn:hover,
  .jp-bundle__btn:hover,
  [class*="ai-custom-pdp-add-to-cart"]:hover,
  [class*="ai-custom-pdp-buy-now"]:hover {
    transform: translateY(-2px);
    box-shadow: var(--jp-lift-soft);
    filter: saturate(1.06);
  }

  .jp-add-btn:hover::after,
  .jp-bundle__btn:hover::after,
  [class*="ai-custom-pdp-add-to-cart"]:hover::after,
  [class*="ai-custom-pdp-buy-now"]:hover::after {
    opacity: 1;
    animation: jpSheen .75s var(--jp-ease) forwards;
  }
}

.jp-add-btn:active,
.jp-bundle__btn:active,
.jp-sticky-buy__btn:active,
[class*="ai-custom-pdp-add-to-cart"]:active,
[class*="ai-custom-pdp-buy-now"]:active {
  transform: translateY(0) scale(.985);
}

@keyframes jpSheen {
  to { left: 115%; }
}

/* ------------------------------------------------------------ image frame
 * Product and editorial imagery gets a slow settle instead of a hard pop, and
 * a subtle cream mat so photography on white does not float.
 */
.jp-product-image,
[class*="ai-custom-pdp-main-image"] {
  overflow: hidden;
  background: linear-gradient(160deg, #faf8f5 0%, var(--jp-cream) 100%);
}

/* ---------------------------------------------------------------- accents
 * A hairline amber rule used to mark section headings without adding markup.
 */
.jp-trusted-header h2::after,
.jp-bundles__head h2::after {
  content: "";
  display: block;
  width: 54px;
  height: 2px;
  margin: 14px auto 0;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--jp-amber), rgba(212, 148, 58, 0));
}

/* --------------------------------------------------------------- focus
 * Keyboard users get a visible, on-brand ring everywhere.
 */
a:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--jp-forest);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ------------------------------------------------------------ mobile tune
 * Hover effects do not exist on touch; give the same affordance on press, and
 * keep transforms cheap on lower-powered phones.
 */
@media (max-width: 749px) {
  .jp-product-card:active,
  .jp-bundle__card:active,
  [class*="goes-well-with-card"]:active {
    transform: scale(.99);
  }

  .jp-reveal {
    transform: translate3d(0, 14px, 0);
    transition-duration: .55s;
  }
}

@media (prefers-reduced-motion: reduce) {
  .jp-reveal,
  .jp-reveal.jp-in > * {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }

  .jp-product-card,
  .jp-bundle__card,
  [class*="goes-well-with-card"],
  .jp-add-btn,
  .jp-bundle__btn {
    transition: none;
  }
}

/* ================= jp-idle.css ================= */
/* Pause what nobody is looking at.

   The site runs ten infinite animations — the seal's storm, the rain, the
   marquee tickers, the constellation spin, two pulse rings — and every one of
   them keeps the compositor busy for the whole page, not just while it is on
   screen. During a scroll that is ten animation timelines competing with the
   scroll itself, which is what the stutter actually is.

   The rule: an element carrying an infinite animation is paused while it is
   outside the viewport, and while the tab is hidden. jp-idle.js adds the class;
   this only has to express the pause.

   `animation-play-state` is the right tool because it is free — the animation
   keeps its position and resumes exactly where it left off, so nothing jumps
   when it scrolls back into view. Removing the animation instead would restart
   it, which reads as a glitch.

   Nothing here can hide content: a paused animation still paints its current
   frame. That is the whole reason this is safe to apply blind. */

.jpi-idle,
.jpi-idle * {
  animation-play-state: paused !important;
}

/* The same saving without JavaScript.

   `content-visibility: auto` lets the browser skip layout, paint AND animation
   for a section that is off screen — it is the platform's own version of what
   the observer above does, needs no script, and cannot get the timing wrong.
   `contain-intrinsic-size` supplies a placeholder height so the scrollbar does
   not jump as sections are skipped and un-skipped.

   Applied to the decorative sections only — the ones carrying the infinite
   animations and canvases — never to anything the visitor might be mid-read
   of, and never to the header, footer or any drawer. */
.jp-seal,
.jp-cin,
.jp-rot,
.jp-rail,
.jp-sawan {
  content-visibility: auto;
  contain-intrinsic-size: auto 400px;
}

/* A hidden tab pauses everything, regardless of scroll position — the browser
   throttles rAF but not CSS animations, so a backgrounded tab keeps compositing
   the marquees forever. */
html.jpi-hidden [class],
html.jpi-hidden [class] * {
  animation-play-state: paused !important;
}

/* ---- rich text on a dark band --------------------------------------
   Shopify rich-text settings emit <p>/<li>/<span> carrying the THEME's body
   colour — a grey picked for white pages. Dropped onto any dark section that
   grey reads at ~1.4:1, which is text nobody can see. It happened on the
   why-choose band and would happen again on the next dark section anyone adds.

   So: inside anything marked as a dark surface, rich-text children inherit the
   section's own ink instead of importing the theme's. Sections opt in with
   `data-jp-dark`, which costs one attribute and cannot affect a light page. */
[data-jp-dark] p,
[data-jp-dark] li,
[data-jp-dark] span,
[data-jp-dark] strong,
[data-jp-dark] em,
[data-jp-dark] a:not(.jp-btn):not([class*="btn"]) {
  color: inherit;
}

/* Reduced motion: the infinite decorative loops stop entirely. The seal, the
   rain and the tickers are atmosphere; the content underneath is unaffected. */
@media (prefers-reduced-motion: reduce) {
  .jp-seal__cloud, .jp-seal__bolt, .jp-seal__rain path,
  .jp-rot__run, .jp-offer__run, .jp-cin__orbit, .jp-cin__orbitArt {
    animation: none !important;
  }
}

/* ================= jp-kinetic.css ================= */
/* JustPure kinetic layer — motion that attaches by SHAPE, not per section.
   ======================================================================

   Measured on the draft homepage before this file existed: thirteen sections,
   and the older half were inert — jp-farm-video 1652px tall with zero reveals
   and zero interactive elements, jp-purity-proof 1138px with zero of each,
   jp-good-fat 673px with one. The newer sections (shop rail, bundle builder,
   proof wall) already animate because each was written with its own reveals.
   Patching the rest one at a time would mean thirteen edits and thirteen
   chances to diverge.

   So this attaches to the shapes the whole site already has: 16 h2, 15 h3,
   54 paragraphs, 57 cards, 73 images, 55 buttons. A section written next year
   gets the same motion for free.

   THE RULES THIS FILE OBEYS
   - transform and opacity only. Never top/left/width/height/margin.
   - one accent, one easing curve, one stagger interval.
   - `prefers-reduced-motion: reduce` kills every non-essential motion.
   - nothing here may change layout, so a failed animation is invisible rather
     than broken.

   motion-web: native `animation-timeline: view()` — verified supported in the
   target browser, so reveals need no JavaScript at all. jp-alive.css still
   carries the IntersectionObserver path for older engines; the two never touch
   the same element because this file only claims [data-kin], which nothing
   else uses.
*/

:root {
  /* One curve for everything. Linear reads as amateur; this is the premium
     ease-out every motion in the file shares. */
  --jp-kin-ease: cubic-bezier(.16, 1, .3, 1);
  --jp-kin-stagger: 90ms;
  --jp-kin-rise: 38px;
}

/* ---------------------------------------------------------------- reveals */
/* The base entrance. `both` holds the from-state before the range starts, so
   an element below the fold is already hidden rather than flashing first. */
/* Transform only — no opacity.
   Same rule the text-variety block below records, and it applies here first:
   `fill: both` holds an element at its 0% frame until its view() range opens,
   so any element whose range never fires keeps that frame forever. With
   opacity in it, that means invisible text with nothing in the DOM to explain
   it — measured on this page, the "Why fermentation changes the fat" heading
   sat at opacity 0 while off screen. A held transform is a few pixels of
   offset; a held opacity is a missing headline. */
@keyframes jpKinRise {
  from { transform: translate3d(0, var(--jp-kin-rise), 0); }
  to   { transform: none; }
}

[data-kin] {
  animation: jpKinRise both linear;
  animation-timeline: view();
  /* Starts the moment the element enters and is done a third of the way up.
     It used to start at entry 25% and run to cover 55%, so a reader had to
     scroll well past a section before its own copy finished arriving — the
     "bahut neeche jaana padta hai" note. Beginning at entry 0% and closing by
     cover 34% means a section has settled about when you reach it. */
  animation-range: entry 0% cover 34%;
}

/* Children of a [data-kin-group] enter in sequence rather than together. The
   index is set inline by the script; without it every child shares delay 0,
   which is exactly the "everything fades up at once" look this avoids. */
[data-kin-group] > * {
  animation: jpKinRise both linear;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
  animation-delay: calc(var(--i, 0) * var(--jp-kin-stagger));
}

/* A heading is the one thing worth a slightly longer, slightly larger move —
   it is what the eye lands on when a section arrives. */
[data-kin="head"] {
  --jp-kin-rise: 48px;
  animation-range: entry 0% cover 30%;
}

/* Media rises less and scales a hair, which reads as depth rather than as a
   card sliding. Scale stays tiny: anything over 1.04 looks like a mistake. */
@keyframes jpKinMedia {
  from { transform: translate3d(0, 28px, 0) scale(.975); }
  to   { transform: none; }
}
[data-kin="media"] {
  animation-name: jpKinMedia;
  animation-range: entry 0% cover 38%;
}

/* ------------------------------------------------------- scroll progress */
/* A hairline that reports how far through the page the reader is. Pure CSS,
   driven by the document scroll, so it costs nothing to run. */
@keyframes jpKinGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.jp-kin-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 999999;
  transform-origin: 0 50%;
  background: linear-gradient(90deg, #2f5d50, #806536 62%, #d4943a);
  animation: jpKinGrow linear;
  animation-timeline: scroll(root block);
  pointer-events: none;
}

/* ------------------------------------------------------ micro-interaction */
/* Cards lift on hover. Pointer-coarse devices skip it — a phone has no hover,
   and the sticky :hover state after a tap looks like a stuck card. */
@media (hover: hover) and (pointer: fine) {
  [data-kin-lift] {
    transition: transform .38s var(--jp-kin-ease), box-shadow .38s var(--jp-kin-ease);
  }
  [data-kin-lift]:hover {
    transform: translate3d(0, -4px, 0);
  }

  /* The magnetic pull is applied by script as a custom property, so the
     transition lives here and the script only writes numbers. */
  [data-kin-magnet] {
    transition: transform .35s var(--jp-kin-ease);
    transform: translate3d(var(--mx, 0px), var(--my, 0px), 0);
  }
}

/* Focus must survive all of it: a keyboard user gets the same affordance a
   mouse user gets from hover, and it must not depend on any animation. */
[data-kin-lift]:focus-visible,
[data-kin-magnet]:focus-visible {
  outline: 2px solid #2f5d50;
  outline-offset: 3px;
}

/* ------------------------------------------------------------- guardrails */
/* Non-negotiable. Everything above is decoration; none of it may be the only
   way to perceive content, and vestibular safety outranks the effect. */
@media (prefers-reduced-motion: reduce) {
  [data-kin],
  [data-kin-group] > * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .jp-kin-progress { animation: none; transform: scaleX(1); }
  [data-kin-lift],
  [data-kin-magnet] { transition: none; transform: none; }
}

/* A browser without scroll-timeline runs the keyframes on a normal timeline
   and would leave content stuck at opacity 0 forever. Reveal everything
   instead: the page is fully readable, it simply does not animate.
   ponytail: @supports not, rather than a JS capability test. */
/* Browsers without scroll-timeline — Safari, and every iPhone, which is most
   of this store's traffic — used to get NO motion at all here: the keyframes
   were switched off and everything sat at its final position. That is why the
   site reads static on a phone while looking alive on a desktop.
   
   They get the same reveal driven the way the Apex site does it: a transition
   plus a class that an IntersectionObserver adds (jp-layers.js). Same distances,
   same curve, same staggering — a different engine underneath.
   
   Transform only, never opacity, for the reason recorded above: an element
   whose observer never fires must be visible and slightly displaced, not
   invisible. */
@supports not (animation-timeline: view()) {
  [data-kin],
  [data-kin-group] > * {
    animation: none;
    opacity: 1;
    transition: transform .72s var(--jp-kin-ease);
  }
  .jp-kin-progress { display: none; }

  [data-kin] { transform: translate3d(0, var(--jp-kin-rise), 0); }
  [data-kin="head"] { transform: translate3d(0, 48px, 0); }
  [data-kin="lede"] { transform: translate3d(0, 22px, 0) scale(.988); }
  [data-kin="stat"] { transform: translate3d(0, 26px, 0) scale(.86); }
  [data-kin="quote"] { transform: translate3d(-28px, 0, 0) rotate(-.6deg); }
  [data-kin="listitem"] { transform: translate3d(-26px, 0, 0); }
  [data-kin="media"] { transform: translate3d(0, 28px, 0) scale(.975); }
  [data-kin-group] > * {
    transform: translate3d(0, var(--jp-kin-rise), 0);
    transition-delay: calc(var(--i, 0) * var(--jp-kin-stagger));
  }

  /* The observer adds this once the element is on screen. */
  [data-kin].jp-kin-seen,
  [data-kin-group] > .jp-kin-seen { transform: none; }
}

/* ============================================================ text variety
 *
 * Everything above gives a section ONE gesture: rise and fade. Measured on
 * our-story, 80 of 91 animated elements were running jpKinRise or jpRiseIn —
 * so the page had motion everywhere and still read as flat, because every
 * line arrived the same way. The owner's note was "har text boring hai",
 * and that is the shape of it: coverage was never the problem, sameness was.
 *
 * These give each KIND of text its own entrance, chosen by what the element
 * is rather than by where it sits, so a page assembles instead of sliding:
 *
 *   lede      the opening paragraph unblurs into focus
 *   quote     a pull-quote swings in from its own margin
 *   stat      a figure counts up from slightly below, scaled
 *   eyebrow   small tracked caps expand their letter-spacing
 *   listitem  bullets arrive from the left, in sequence
 *
 * MOVEMENT ONLY, NEVER OPACITY. Measured while building these: with opacity
 * in the keyframe, a lede at top:2568 and a stat at top:2736 both sat at
 * opacity 0 — correct for `fill: both` before their range opens, but it means
 * any element whose range never fires is invisible for good, and this theme
 * has lost headings to exactly that twice (see jp-motion.css). A transform
 * that never runs leaves text a few pixels out of place; an opacity that never
 * runs leaves a blank page with nothing in the DOM to point at.
 *
 * All of them are transform-only, and all inherit the same ease and the same
 * view() timeline, so the page still feels like one hand.
 */

/* The opening paragraph of a section: it comes into focus rather than moving
   far, which reads as the page settling rather than as another card sliding. */
@keyframes jpKinFocus {
  from { transform: translate3d(0, 22px, 0) scale(.988); }
  to   { transform: none; }
}
[data-kin="lede"] {
  animation-name: jpKinFocus;
  animation-range: entry 0% cover 34%;
}

/* A pull-quote earns a bigger gesture: it swings in from the side it is
   indented on, so it reads as an aside rather than as body copy. */
@keyframes jpKinAside {
  from { transform: translate3d(-28px, 0, 0) rotate(-.6deg); }
  to   { transform: none; }
}
[data-kin="quote"] {
  animation-name: jpKinAside;
  animation-range: entry 0% cover 32%;
}

/* Figures: the number lifts and grows a touch so the eye is pulled to it
   before the label underneath. */
@keyframes jpKinFigure {
  from { transform: translate3d(0, 26px, 0) scale(.86); }
  to   { transform: none; }
}
[data-kin="stat"] {
  animation-name: jpKinFigure;
  animation-range: entry 0% cover 32%;
}

/* Small tracked caps: the tracking opens out as they arrive. Letter-spacing
   is not a compositor property, but these are a handful of short strings per
   page, never a block of body copy, so the cost is nil. */
@keyframes jpKinTrack {
  from { letter-spacing: .02em; }
  to   { letter-spacing: inherit; }
}
[data-kin="eyebrow"] {
  animation-name: jpKinTrack;
  animation-range: entry 0% cover 28%;
}

/* List items come in from the left in sequence — a list should read as being
   written out, not as a block appearing. */
@keyframes jpKinListIn {
  from { transform: translate3d(-26px, 0, 0); }
  to   { transform: none; }
}
[data-kin="listitem"] {
  animation-name: jpKinListIn;
  animation-range: entry 0% cover 40%;
  animation-delay: calc(var(--i, 0) * 60ms);
}

@media (prefers-reduced-motion: reduce) {
  [data-kin="lede"],
  [data-kin="quote"],
  [data-kin="stat"],
  [data-kin="eyebrow"],
  [data-kin="listitem"] {
    animation: none;
    opacity: 1;
    filter: none;
    transform: none;
  }
}

/* ====================================================== word-level reveal
 *
 * The benchmark's move, and the one thing this site was missing. Apex splits a
 * lead paragraph into one <span class="pw"> per word and gives each a
 * transition-delay of i*15ms, so the sentence writes itself in as it arrives.
 * Everything here was lifting whole blocks instead — motion on every section,
 * but a paragraph still landed as one slab, which is why the text kept reading
 * static however many kinds of block entrance were added.
 *
 * Applied to headings and lead copy only. A whole page of per-word staggering
 * is noise, and long body copy would take too long to finish; jp-layers.js
 * decides which elements qualify.
 *
 * Transform only, never opacity — same rule as the rest of this file. A word
 * whose reveal never fires must sit slightly low and fully readable, not
 * invisible. `will-change` is deliberately absent: a paragraph is 8-20 words
 * and promoting every one of them costs more than it saves.
 */
.jp-w {
  display: inline-block;
  transform: translate3d(0, .42em, 0);
  transition: transform .58s var(--jp-kin-ease);
  /* 12ms, not 26: body copy is in scope now and a 60-word paragraph at 26ms
     would still be arriving 1.5s after it entered view. At 12ms the longest
     paragraph on the site finishes inside ~0.7s and the cascade is still
     clearly a cascade. */
  transition-delay: calc(var(--w, 0) * 12ms);
}

/* Released by the observer in jp-layers.js once the line is on screen. */
.jp-words-in .jp-w { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .jp-w { transform: none; transition: none; }
}

/* ================= jp-interact.css ================= */
/* JustPure interaction layer — makes the reading slabs answer back.
   ================================================================

   Motion (jp-kinetic) made the page move. This makes it respond. Measured on
   the draft before writing it: six sections over 2000px tall carrying six
   interactive elements between them — jp-purity-proof 2494px with zero,
   jp-good-fat 6834px with one, jp-bilona-cinema 14743px with one. Site-wide:
   zero accordions, zero range inputs, zero counters. Forty thousand pixels a
   visitor can only scroll past.

   Every slab does have the same shape underneath: four to seven repeated cards
   (jp-pw__card, jp-purity-card, jp-farm-video__card, jp-cin__pip). So this
   attaches to that shape rather than to any section, exactly like the kinetic
   layer, and anything written later inherits it.

   THE RULES
   - nothing here hides content. A collapse-and-disclose version was built and
     removed: it could not be verified, because the preview pane does not
     composite and every height measured 0px whether the panel was open or
     shut. A control that says "Read more" and might do nothing is worse than
     copy that simply stays readable.
   - transform/opacity only; prefers-reduced-motion turns it all off.
   - keyboard parity: anything a pointer can do has a visible focus state.
*/

:root {
  --jp-ix-ease: cubic-bezier(.16, 1, .3, 1);
}

/* ----------------------------------------------------------- card response */
/* Cards answer the pointer instead of sitting flat. The lift is small on
   purpose — it should read as the card coming forward, not as it jumping. */
/* No `cursor: pointer` here — most of these cards contain their own link, which
   sets the cursor over the part that is actually clickable. Putting it on the
   whole card would promise a click the card itself does not handle. */
[data-ix-card] {
  transition: transform .34s var(--jp-ix-ease), box-shadow .34s var(--jp-ix-ease);
}

/* The card lifts when the link inside it takes keyboard focus, so a keyboard
   user gets the same feedback a pointer user gets from hover — without the
   card becoming a second tab stop for the same destination. */
[data-ix-card]:focus-within {
  transform: translate3d(0, -3px, 0);
  box-shadow: 0 18px 40px -22px rgba(31, 61, 51, .45);
}




/* --------------------------------------------------------------- counters */
/* A number that counts up as it arrives reads as a measurement being taken
   rather than a figure printed. The element keeps its final value as text, so
   the count is decoration over content that is already correct. */
[data-ix-count] { font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- reveals */
/* Media that responds to the pointer. Kept to a whisper — over about 1.03 it
   stops reading as depth and starts reading as a glitch. */
@media (hover: hover) and (pointer: fine) {
  [data-ix-zoom] { overflow: hidden; }
  [data-ix-zoom] img {
    transition: transform .55s var(--jp-ix-ease);
    will-change: transform;
  }
  [data-ix-zoom]:hover img { transform: scale(1.045); }

  /* The card lifts, and its shadow deepens with it, so the lift reads as
     distance from the page rather than as the card sliding upward. */
  [data-ix-card]:hover {
    transform: translate3d(0, -3px, 0);
    box-shadow: 0 18px 40px -22px rgba(31, 61, 51, .45);
  }
}

/* ------------------------------------------------------------- guardrails */
@media (prefers-reduced-motion: reduce) {
  [data-ix-card],
  [data-ix-zoom] img {
    transition: none !important;
    transform: none !important;
  }
}

/* ================= jp-signature.css ================= */
/* JustPure signature layer — the things you actually SEE.
   =====================================================

   The kinetic and interact layers made the page move and answer back, but a
   reader scrolling past cannot name a single thing that changed. This one is
   deliberately visible: a rotating seal, a marquee of claims, a hero eyebrow
   that types itself, gradient headlines, and a price that pulses when the cart
   changes. If the site is supposed to look unrecognisable, these are the parts
   doing the work.

   Everything is transform/opacity, one easing curve, and dies completely under
   prefers-reduced-motion.
*/

:root {
  --jp-sig-ease: cubic-bezier(.16, 1, .3, 1);
  --jp-sig-forest: #2A5040;
  --jp-sig-amber: #D4943A;
  --jp-sig-amber-ink: #8A5A1E;
  --jp-sig-cream: #FAF7F2;
}

/* ------------------------------------------------------- 1. rotating seal */
/* A circular "slow-churned · lab-tested · small batch" seal that turns
   continuously. It is the mark the eye catches first and the one thing on the
   page that never stops. */
.jp-mark {
  position: relative;
  width: clamp(108px, 13vw, 148px);
  aspect-ratio: 1;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
}
.jp-mark__ring {
  position: absolute;
  inset: 0;
  animation: jpMarkSpin 22s linear infinite;
}
.jp-mark__ring text {
  font-size: 9.4px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  fill: var(--jp-sig-forest);
}
.jp-mark__core {
  width: 54%;
  aspect-ratio: 1;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 34% 28%, #3a6a56, var(--jp-sig-forest) 68%);
  box-shadow: 0 10px 26px -14px rgba(31, 61, 51, .8),
              inset 0 1px 0 rgba(255, 255, 255, .22);
}
/* The core holds a drawn churn now, not a text glyph — see jp-signature.liquid
   for why the character had to go. Sized as a share of the disc so the mark
   keeps its proportion at every seal size. */
.jp-mark__core svg {
  width: 52%;
  height: 52%;
  color: #F4F1EA;
  display: block;
}
@keyframes jpMarkSpin { to { transform: rotate(360deg); } }

/* Pause on hover so the words can be read — a mark nobody can read is
   decoration, not a claim. */
@media (hover: hover) {
  .jp-mark:hover .jp-mark__ring { animation-play-state: paused; }
}

/* ------------------------------------------------- 2. the claims marquee */
/* A continuous strip. Duplicated content in the markup makes the loop seamless
   without measuring anything in script. */
.jp-marq {
  position: relative;
  overflow: hidden;
  /* The track inside is nowrap and ~2000px long. overflow clips the spill but
     does not stop this box growing to fit it - the same omission made the
     offer bar 1093px wide inside a 395px phone. */
  max-width: 100%;
  padding: 13px 0;
  border-block: 1px solid rgba(42, 80, 64, .14);
  background: linear-gradient(90deg, var(--jp-sig-cream), #fff 50%, var(--jp-sig-cream));
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 9%, #000 91%, transparent);
}
.jp-marq__track {
  display: flex;
  gap: 46px;
  width: max-content;
  animation: jpMarq 34s linear infinite;
}
.jp-marq__item {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--jp-sig-forest);
  white-space: nowrap;
}
.jp-marq__item::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--jp-sig-amber);
}
@keyframes jpMarq { to { transform: translate3d(-50%, 0, 0); } }
@media (hover: hover) {
  .jp-marq:hover .jp-marq__track { animation-play-state: paused; }
}

/* ------------------------------------------------- 3. the drawn rule */
/* Section headings get a hand-drawn rule that wipes in as the heading arrives —
   the churn-stroke of the bilona pot, not a generic underline.
   No gradient text: it reads as decoration, fails contrast checking, and is
   the first thing that marks a page as templated. Solid ink, moving rule. */
.jp-rule { position: relative; }
.jp-rule::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -.22em;
  width: 100%;
  height: 3px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--jp-sig-amber), var(--jp-sig-amber-ink));
  transform: scaleX(0);
  transform-origin: 0 50%;
  animation: jpRuleWipe .9s var(--jp-sig-ease) forwards;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}
@keyframes jpRuleWipe { to { transform: scaleX(1); } }
@supports not (animation-timeline: view()) {
  .jp-rule::after { animation: none; transform: scaleX(1); }
}

/* --------------------------------------------------- 4. underline on hover */
/* Every in-content link gets a wipe underline rather than the browser default.
   Uses background-size, which does not touch layout. */
.jp-sig-link {
  background-image: linear-gradient(var(--jp-sig-amber), var(--jp-sig-amber));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 2px;
  transition: background-size .38s var(--jp-sig-ease);
  text-decoration: none;
}
.jp-sig-link:hover,
.jp-sig-link:focus-visible { background-size: 100% 2px; }

/* ------------------------------------------------------------ guardrails */
@media (prefers-reduced-motion: reduce) {
  .jp-mark__ring,
  .jp-marq__track { animation: none !important; }
  .jp-sig-link { transition: none; background-size: 100% 2px; }
  /* The rule still draws — it just arrives already drawn. */
  .jp-rule::after { animation: none; transform: scaleX(1); }
}

/* ================= jp-engage-pages.css ================= */
/* Content pages — the ones a reader can only scroll past.
   =====================================================

   Measured on the dev server: our-story 2,362 words with ZERO links and zero
   controls; community 1,870 words, zero; bilona-method 927 words, zero. A
   reader who finishes any of them has nowhere to go and nothing to do. The
   sections are well written and completely inert.

   Each has the same shape underneath — a row of four to seven repeated cards
   (jp-source-process__card, jp-info__card, jp-bilona-step) — so this attaches
   to the shape, like every other layer here, and a page added later inherits
   it.

   Two things are added, both content rather than decoration:
     1. steps become numbered and hoverable, so a process reads as a sequence
     2. every page ends with a way onward, because a dead end is the actual bug
*/

:root { --jp-ep-ease: cubic-bezier(.16, 1, .3, 1); }

/* ---------------------------------------------------------- numbered steps */
/* A process told as four unnumbered cards reads as four unrelated facts. The
   counter is generated, so nothing in the copy has to change and a card added
   in the admin numbers itself. */
[data-ep-steps] { counter-reset: jp-ep; }

[data-ep-step] {
  position: relative;
  counter-increment: jp-ep;
  transition: transform .34s var(--jp-ep-ease), box-shadow .34s var(--jp-ep-ease);
}
[data-ep-step]::before {
  content: counter(jp-ep, decimal-leading-zero);
  position: absolute;
  top: 12px;
  right: 14px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .12em;
  color: var(--jp-amber-text, #8A5A1E);
  opacity: .55;
  transition: opacity .3s var(--jp-ep-ease);
}
@media (hover: hover) and (pointer: fine) {
  [data-ep-step]:hover { transform: translate3d(0, -3px, 0); }
  [data-ep-step]:hover::before { opacity: 1; }
}

/* The thread. A hairline joining one step to the next turns a grid of cards
   into a sequence you read in order. Drawn on the card, so it cannot shift
   anything: it is a pseudo-element on a positioned box. */
/* The thread runs INSIDE the card's own edge. It used to sit at right:-14px,
   which assumes the cards form one row: in a wrapping grid the card at the end
   of a row has nothing to its right, so its hairline hung off the edge into
   the section — the stray line beside "01" and "02" on the home page. The row
   width is not knowable in CSS, and clipping the container would also clip the
   cards' hover-lift and shadow, so the line moves in instead. */
[data-ep-step]:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 26px;
  right: 0;
  width: 20px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--jp-line, #E6E0D2));
}
@media (max-width: 749px) {
  /* Stacked on a phone, so the thread runs down instead of across. */
  [data-ep-step]:not(:last-child)::after {
    top: auto;
    right: auto;
    bottom: -14px;
    left: 26px;
    width: 1px;
    height: 14px;
    background: linear-gradient(180deg, var(--jp-line, #E6E0D2), transparent);
  }
}

/* --------------------------------------------------------- the way onward */
/* A 2,000-word page that ends in nothing is where the reader leaves. This is
   the one thing on these pages that is a real call to action. */
.jp-ep-next {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  max-width: 1180px;
  margin: 8px auto 64px;
  padding: 30px clamp(20px, 4vw, 40px);
  border: 1px solid var(--jp-line, #E6E0D2);
  border-radius: 18px;
  background: linear-gradient(150deg, #fffdf7, var(--jp-sand, #F0E9DC));
}
.jp-ep-next__text { flex: 1 1 300px; }
.jp-ep-next__kicker {
  margin: 0 0 6px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--jp-amber-text, #8A5A1E);
}
.jp-ep-next__head {
  margin: 0;
  font-size: clamp(20px, 2.4vw, 27px);
  line-height: 1.2;
  color: var(--jp-forest, #2A5040);
}
.jp-ep-next__links { display: flex; flex-wrap: wrap; gap: 10px; }
.jp-ep-next__btn {
  display: inline-flex;
  align-items: center;
  /* Same 44px floor as every other control on the site. */
  min-height: 44px;
  padding: 12px 22px;
  border-radius: 999px;
  font-size: 14.5px;
  font-weight: 600;
  text-decoration: none;
  transition: transform .3s var(--jp-ep-ease), background .24s var(--jp-ep-ease);
}
.jp-ep-next__btn--primary { background: var(--jp-forest, #2A5040); color: #FFFDF7; }
.jp-ep-next__btn--ghost {
  background: transparent;
  color: var(--jp-forest, #2A5040);
  border: 1px solid var(--jp-forest, #2A5040);
}
.jp-ep-next__btn:hover { transform: translate3d(0, -2px, 0); }
.jp-ep-next__btn--primary:hover { background: #22412f; }
.jp-ep-next__btn--ghost:hover { background: rgba(42, 80, 64, .07); }
.jp-ep-next__btn:focus-visible { outline: 2px solid var(--jp-forest, #2A5040); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  [data-ep-step],
  [data-ep-step]::before,
  .jp-ep-next__btn { transition: none !important; transform: none !important; }
}

/* ================= jp-policy.css ================= */
/* Policy pages — a wall of text made navigable.
   ============================================

   2,262 words in one column with no way in. The copy is legally required and
   cannot be cut, so the job is structure, not editing: an index at the top,
   each part openable on its own, and type set for actually reading rather than
   for getting through.
*/

.shopify-policy__container {
  max-width: 760px;
}

/* The index. Not a sidebar — on a phone a sidebar becomes a second wall, and
   this has to work there first. */
.jp-pol__index {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 30px;
  padding: 18px 18px 16px;
  border: 1px solid var(--jp-line, #E6E0D2);
  border-radius: 14px;
  background: var(--jp-cream, #FAF7F2);
}
.jp-pol__index::before {
  content: "On this page";
  flex: 0 0 100%;
  margin-bottom: 4px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--jp-amber-text, #8A5A1E);
}
.jp-pol__link {
  padding: 7px 13px;
  border: 1px solid var(--jp-line, #E6E0D2);
  border-radius: 999px;
  background: #fff;
  font-size: 13px;
  line-height: 1.3;
  color: var(--jp-forest, #2A5040);
  text-decoration: none;
  transition: background .22s cubic-bezier(.16, 1, .3, 1),
              border-color .22s cubic-bezier(.16, 1, .3, 1);
}
.jp-pol__link:hover { background: var(--jp-sand, #F0E9DC); border-color: var(--jp-forest, #2A5040); }
.jp-pol__link:focus-visible { outline: 2px solid var(--jp-forest, #2A5040); outline-offset: 2px; }

/* Each part. The rule sits above the heading rather than below it, so the
   heading belongs to the text it introduces instead of floating between two. */
.jp-pol__sec + .jp-pol__sec { border-top: 1px solid var(--jp-line, #E6E0D2); }
.jp-pol__head { margin: 0; }

.jp-pol__btn {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  /* 44px minimum, same floor as every other control on the site. */
  min-height: 44px;
  padding: 18px 2px;
  border: 0;
  background: none;
  font: inherit;
  font-size: clamp(16px, 1.6vw, 19px);
  font-weight: 600;
  line-height: 1.3;
  text-align: left;
  color: var(--jp-forest, #2A5040);
  cursor: pointer;
}
.jp-pol__btn:focus-visible { outline: 2px solid var(--jp-forest, #2A5040); outline-offset: 2px; }

/* The chevron. Rotation only — nothing that could shift the row it sits in. */
.jp-pol__btn::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  margin-right: 4px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform .3s cubic-bezier(.16, 1, .3, 1);
}
.jp-pol__btn[aria-expanded="true"]::after { transform: rotate(-135deg) translateY(-2px); }

.jp-pol__panel { padding: 0 2px 22px; }
.jp-pol__panel > *:first-child { margin-top: 0; }

/* Reading type. The default policy styling is set for compliance, not for a
   human — this is measure, rhythm and a colour that holds AA on cream. */
/* The same reading type applies to the raw policy body, not only to panels.
   Terms, refund and shipping have no headings at all - six paragraphs of up to
   1,950 characters each - so the accordion never builds and every rule scoped
   to .jp-pol__panel missed them: three pages sat at ~110 characters per line,
   roughly half again the comfortable measure, while the privacy page (17 real
   headings) read fine. Styling the body itself means a policy is readable
   whether or not it has the structure to be folded. */
.shopify-policy__body .rte > p,
.shopify-policy__body .rte > li,
.jp-pol__panel p,
.jp-pol__panel li {
  max-width: 68ch;
  font-size: 15.5px;
  line-height: 1.68;
  color: #4a5148;
}

/* A 1,950-character paragraph is a wall whatever its measure. Space between
   paragraphs is what gives the eye somewhere to rest and lets a reader find
   their place again after looking away. */
/* Centre the measure inside the container. The container is 760px and the
   measure is 68ch (~654px); left-aligning both leaves the whole 106px remainder
   on the right, so the column reads as pushed off-centre against a title that
   spans the full width. */
.shopify-policy__body .rte > p { margin: 0 auto 1.35em; }
.shopify-policy__body .rte > p:first-of-type {
  /* The opening paragraph carries the page: slightly larger, darker, and it
     reads as a lede instead of as the first brick in the wall. */
  font-size: 17px;
  line-height: 1.62;
  color: #3a4139;
}
.shopify-policy__body .rte strong { color: var(--jp-forest, #2A5040); }
.shopify-policy__body .rte a { color: var(--jp-amber-text, #8A5A1E); }
.jp-pol__panel li + li { margin-top: 7px; }
.jp-pol__panel strong { color: var(--jp-forest, #2A5040); }
.jp-pol__panel a { color: var(--jp-amber-text, #8A5A1E); }

@media (prefers-reduced-motion: reduce) {
  .jp-pol__btn::after,
  .jp-pol__link { transition: none; }
}

/* ================= jp-card-life.css ================= */
/* Cards that answer the pointer.

   Measured across the site: 3 of 13 data items on bilona-method and 5 of 19 on
   community had no transition, no animation and no reveal — they were printed,
   not interactive. The owner's note was that every data point on the benchmark
   does something and here some just sit. These are the ones that just sat.

   Attached by shape rather than by section, so a card added later inherits it
   without anyone remembering to.

   Transform and shadow only: no layout property is touched, so nothing here
   can cost a reflow however many cards a page carries. */

.jp-source__card,
[class*="jp-info-card"],
.jp-cert-proof__item,
.jp-purity-card {
  transition: transform .38s cubic-bezier(.16, 1, .3, 1),
              box-shadow .38s cubic-bezier(.16, 1, .3, 1);
}

/* The lift rides box-shadow and the inner image, NOT the card's own transform.
   Measured: these cards are claimed by the reveal engine, which animates
   transform on the card itself — a hover transform on the same element is
   simply overwritten, and the card appeared to ignore the pointer. Shadow and
   the inner scale are unclaimed, so they land every time. */
@media (hover: hover) and (pointer: fine) {
  .jp-source__card:hover,
  [class*="jp-info-card"]:hover,
  .jp-cert-proof__item:hover,
  .jp-purity-card:hover {
    box-shadow: 0 22px 44px -28px rgba(20, 40, 32, .55);
  }

  /* The image inside a card is the thing being shown, so it takes the depth
     while the card carries the lift. */
  .jp-source__card-img {
    transition: transform .55s cubic-bezier(.16, 1, .3, 1);
  }
  .jp-source__card:hover .jp-source__card-img {
    transform: scale(1.05);
  }
}

/* Keyboard users get the same state, not a lesser one. */
.jp-source__card:focus-within,
[class*="jp-info-card"]:focus-within {
  box-shadow: 0 22px 44px -28px rgba(20, 40, 32, .55);
}

@media (prefers-reduced-motion: reduce) {
  .jp-source__card,
  [class*="jp-info-card"],
  .jp-cert-proof__item,
  .jp-purity-card,
  .jp-source__card-img {
    transition: none;
    transform: none;
  }
}

/* The related-products card showed an empty cream box where the product should
   be. Measured on the product page: the <img> is loaded (complete, 320x320
   natural) and positioned absolute, but its wrapper `.product-card-main-image`
   computes to height 0 while the `.media-card` above it is a correct 171px.
   An absolutely positioned child cannot give its parent height, and the wrapper
   sets no height of its own, so the image had no box to fill.

   Filling the wrapper to its container is the whole fix; the aspect ratio still
   comes from `.media-card`, which already has `aspect-ratio: 1/1`.

   ponytail: one rule, no markup change — the wrapper is theme code we do not
   otherwise touch. */
.product-card-main-image {
  position: relative;
  height: 100%;
}

/* Once the image actually occupied its 170px, the card's own height stopped
   covering its contents: measured on the product page, the card computes 325px
   while the detail block ends 20px past it and the Add-to-cart button 30px
   past — the clipped card in the screenshot.

   The card is theme markup we do not otherwise touch, so rather than guessing
   at a height, let the inner column size to what is in it. min-height keeps
   whatever the theme intended as a floor; auto height stops the overflow.

   ponytail: two declarations, no markup change. */
.product-card-inner {
  height: auto;
  min-height: 100%;
}

/* Related-products carousel: let a card grow to its own contents.

   Measured on the product page — both cards computed 325px, but "A2 Buffalo
   Bilona Ghee" wraps its title to two lines (42px vs 21px) and pushed seven
   elements up to 30px past its own bottom, cutting off Add to cart. Three CSS
   attempts at the card and its grid did nothing, because the height is not
   CSS: Swiper writes it inline on the track from the SHORTEST slide, and the
   card takes height:100% of it.

   The section now runs autoHeight, so Swiper measures the tallest slide. This
   releases the equal-height clamp that would otherwise re-pin every slide to
   the old track height. */
/* The related-products carousel clipped its taller card.

   Measured, after four wrong guesses at the card, its grid and the section
   config: Swiper writes `height: 325px` INLINE on the track, taken from the
   active slide — "Moringa Powder", whose title is one line. The other card's
   title wraps to two (42px vs 21px) and pushed seven elements, including Add
   to cart, up to 30px past its own bottom.

   Two things were doing it. The track carried `slides-equal-height`, which
   pins every slide to the measured track height; that class is off the markup
   now. Swiper itself still writes the inline height, and a stylesheet cannot
   outrank an inline declaration without !important — which is the correct
   tool here rather than a smell, because the value it overrides comes from a
   third-party script at runtime, not from this theme. */
.related-products-items.swiper-wrapper {
  height: auto !important;
  align-items: stretch;
}
/* min-height:100% here was the bug that survived six attempts at this card: it
   resolves against the slide box Swiper sized, so it re-pinned the card to the
   height the line above had just freed. The card must be free to be as tall as
   its own title needs; equal height across slides comes from align-items:
   stretch on the track, not from a percentage floor on the card.
   ponytail: no floor at all — stretch already does the job. */
.related-products-items .swiper-slide,
.related-products-items .product-card.height-100 {
  height: auto !important;
}
/* product-card-inner carries the same percentage floor, and freeing only the
   card left it: the inner asked for 100% of a parent that was sizing itself
   from the inner, so the pair settled back on the old 325px. Both had to go
   before the card could report its real 335px. */
.related-products-items .product-card-inner {
  min-height: 0;
}

/* And the clip itself was never a height problem — which is why six attempts
   at heights, floors and Swiper config all measured the same 325px.
   The engine reported it plainly once asked the right question: inside the
   card, `.product-card-detail` sat at translateY(7.5px) and the Add-to-cart
   button at translateY(23px). Those are MY reveal offsets, frozen at their
   resting position because a scroll range inside a horizontal carousel never
   opens. The shifted button crossed the card's bottom edge by 9px and the
   carousel's overflow:hidden sheared it.
   The reveal has nothing to animate here — a card the visitor swipes to is
   already on screen — so it is switched off inside the track and everything
   sits where the layout put it. */
.related-products-items [style*="--w"],
.related-products-items .jp-w,
.related-products-items [data-kin],
.related-products-items .product-card-detail,
.related-products-items .product-card-atc {
  transform: none !important;
  animation: none !important;
  opacity: 1 !important;
}

/* ------------------------------------------------ photo grids answer too
 * A four-up grid of farm photographs on our-story sat completely inert: no
 * hover, no depth, nothing to say the images are worth looking at. Same on
 * every other gallery of the same shape.
 *
 * The image scales inside its own frame while the frame stays put, so the
 * grid never reflows and neighbouring cards do not move. The card lifts on
 * shadow for the reason recorded above — the reveal engine owns transform on
 * these, so a hover transform would be overwritten.
 */
.jp-beyond-jar__image-card,
[class*="__image-card"],
[class*="__gallery"] > *,
[class*="__photos"] > * {
  overflow: hidden;
  transition: box-shadow .42s cubic-bezier(.16, 1, .3, 1);
}

.jp-beyond-jar__image-card img,
[class*="__image-card"] img,
[class*="__gallery"] img,
[class*="__photos"] img {
  transition: transform .62s cubic-bezier(.16, 1, .3, 1);
  will-change: auto;
}

@media (hover: hover) and (pointer: fine) {
  .jp-beyond-jar__image-card:hover,
  [class*="__image-card"]:hover {
    box-shadow: 0 26px 52px -30px rgba(20, 40, 32, .62);
  }
  .jp-beyond-jar__image-card:hover img,
  [class*="__image-card"]:hover img,
  [class*="__gallery"] > *:hover img,
  [class*="__photos"] > *:hover img {
    transform: scale(1.06);
  }
}

@media (prefers-reduced-motion: reduce) {
  .jp-beyond-jar__image-card img,
  [class*="__image-card"] img,
  [class*="__gallery"] img,
  [class*="__photos"] img { transition: none; transform: none; }
}

/* ------------------------------------------------- trust badges, awake
 * The product page's four proof badges — FSSAI Certified, Lab-Tested Batch,
 * 100% Vegetarian, A2 Desi Cow — were the most inert things on it. Measured:
 * 14 of 58 data items on the PDP had no transition and no reveal, and these
 * were most of them. They are the claims a first-time buyer is scanning for,
 * so they should feel checked rather than printed.
 *
 * Prefix selectors because the generated block names carry a per-instance
 * suffix, the same reason jp-tables and the eyebrow classifier work this way.
 */
[class*="pdp-trust-badge"] {
  transition: transform .34s cubic-bezier(.16, 1, .3, 1),
              box-shadow .34s cubic-bezier(.16, 1, .3, 1),
              border-color .34s ease;
}

@media (hover: hover) and (pointer: fine) {
  [class*="pdp-trust-badge"]:hover {
    transform: translateY(-3px);
    box-shadow: 0 14px 28px -20px rgba(20, 40, 32, .5);
    border-color: rgba(212, 148, 58, .5);
  }
}

/* They arrive in sequence rather than as one row, so the eye reads them as
   four separate assurances. Transform only — a held opacity frame is invisible
   content, the rule this whole codebase now follows. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    @keyframes jpBadgeIn {
      from { transform: translate3d(0, 12px, 0) scale(.96); }
      to   { transform: none; }
    }
    [class*="pdp-trust-badge"] {
      animation: jpBadgeIn both cubic-bezier(.16, 1, .3, 1);
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
    [class*="pdp-trust-badge"]:nth-child(2) { animation-delay: 70ms }
    [class*="pdp-trust-badge"]:nth-child(3) { animation-delay: 140ms }
    [class*="pdp-trust-badge"]:nth-child(4) { animation-delay: 210ms }
  }
}

@media (prefers-reduced-motion: reduce) {
  [class*="pdp-trust-badge"] { transition: none; transform: none; animation: none; }
}

/* ---------------------------------------- the theme's own product cards
 * The related-products row is theme markup, not ours, and it was the last
 * inert thing on the product page — 6 of the 9 remaining dead items. It gets
 * the same language as every other card here.
 *
 * Shadow and the inner image only. These cards sit in a Swiper track that
 * writes inline transforms on the slide, so animating transform on the card
 * fights the carousel; shadow and the image inside it are unclaimed.
 */
.product-card {
  transition: box-shadow .4s cubic-bezier(.16, 1, .3, 1);
}
.product-card .product-card-main-image,
.product-card-media img {
  transition: transform .58s cubic-bezier(.16, 1, .3, 1);
}

@media (hover: hover) and (pointer: fine) {
  .product-card:hover {
    box-shadow: 0 24px 46px -30px rgba(20, 40, 32, .55);
  }
  .product-card:hover .product-card-main-image,
  .product-card:hover .product-card-media img {
    transform: scale(1.045);
  }
}

@media (prefers-reduced-motion: reduce) {
  .product-card,
  .product-card .product-card-main-image,
  .product-card-media img { transition: none; transform: none; }
}

/* An engine without scroll-timeline runs the badge keyframes on a normal
   timeline, which would leave them holding their from-state. Reveal them
   outright there: no animation, nothing displaced.
   ponytail: @supports not, matching how jp-kinetic.css guards the same thing. */
@supports not (animation-timeline: view()) {
  [class*="pdp-trust-badge"] {
    animation: none;
    transform: none;
  }
}

/* ================= jp-scale.css ================= */
/* Type scale — the reason the pages read flat.

   Measured across our-story, community and quality: every section heading is
   36px against 16px body, a ratio of 2.25, repeated seven times down a page.
   Nothing dominates, so nothing is an entry point and the eye has no reason to
   stop anywhere. The owner has called this "boring" four times while the site
   gained word-by-word reveals, staggered lists and interactive tables — motion
   was never the gap, hierarchy was.

   Two changes, both by shape so no page markup moves:

   1. The FIRST content section of a page becomes the hero. Its heading takes
      a fluid 3.5rem-5rem, giving roughly 4:1 against body copy, which reads
      as a statement rather than a sub-header.
   2. Every later heading stays at its 36px, so the hero's dominance is real
      rather than everything growing together.

   Sizes are in px, not rem, ON PURPOSE. This theme sets root font-size to
   10px, so a `4.25rem` ceiling resolved to 42.5px and the hero came out barely
   larger than the 36px headings it was meant to dominate — measured at 43px
   before this note was written.

   ponytail: no new markup, no JS. `:first-of-type` on the section does the
   selecting, and clamp() does the responsiveness. */

/* The page's opening section — its heading carries the page. */
/* One hero per page, not two. `:first-of-type` and `:nth-of-type(2)` both
   matched, so the first two sections grew together and neither dominated —
   measured 67px on both. The banner and main-page sections are disabled on
   these templates, so the first section that actually renders content is the
   one to promote, and `:has()` finds it without counting positions. */
main > .shopify-section:first-of-type h1,
main > .shopify-section:first-of-type h2 {
  font-size: clamp(44px, 5.2vw, 68px);
  line-height: 1.04;
  letter-spacing: -0.02em;
}

/* Its opening paragraph rises with it, so the hero block reads as one unit
   rather than a large heading sitting on small print. */
.shopify-section:first-of-type h1 + p,
.shopify-section:first-of-type h2 + p,
.shopify-section:first-of-type [data-kin="lede"],
main > .shopify-section:nth-of-type(2) [data-kin="lede"] {
  font-size: clamp(17px, 1.35vw, 20px);
  line-height: 1.62;
}

/* The hero fix above solved the top of the page and left the rest flat: after
   one 67px statement, every remaining section sat at 36px against 16px body —
   measured 2.08-2.25 on community, quality and our-story alike. One dominant
   heading followed by seven identical ones is still a page with one entry
   point, which is why "boring" survived the hero work.

   A section heading is not the page's statement, but it is not body copy
   either, and at 2.25 it was reading as the latter. It takes a real third
   step here: dominant hero, section headings that announce, body copy. The
   sizes stay fluid so the three steps hold at every width instead of
   collapsing on a laptop.

   Scoped to sections AFTER the first so the hero keeps its lead — this widens
   the gap below the hero, it does not close the gap above.

   3.05vw only reached 39px at 1280 — measured, a 3px gain on the 36px it
   replaced, which is not a step anyone sees. The coefficient is set from the
   target rather than guessed: 44/1280 = 3.44vw. */
main > .shopify-section:not(:first-of-type) h2 {
  font-size: clamp(30px, 3.44vw, 46px);
  line-height: 1.12;
  letter-spacing: -0.015em;
}

/* A section's opening line rises with its heading for the same reason the
   hero's does: the pair reads as one block rather than a heading dropped onto
   small print. */
main > .shopify-section:not(:first-of-type) h2 + p {
  font-size: clamp(16px, 1.2vw, 18px);
  line-height: 1.6;
}

/* Sections alternate their breathing room rather than repeating one rhythm.
   Every third section gets more air, which breaks the metronome without
   needing anyone to hand-tune each page. */
main > .shopify-section:nth-of-type(3n) {
  padding-block: clamp(56px, 6vw, 104px);
}

@media (max-width: 749px) {
  /* On a phone the hero cannot take 68px without pushing everything below the
     fold, so the ratio comes down while staying clearly dominant. */
  .shopify-section:first-of-type h1,
  .shopify-section:first-of-type h2,
  main > .shopify-section:nth-of-type(2) h1,
  main > .shopify-section:nth-of-type(2) h2 {
    font-size: clamp(32px, 8.5vw, 44px);
  }
}

/* ================= jp-pdp-life.css ================= */
/* Bring the product page's generated sections to life.

   The PDP is assembled from `ai_gen_block_*` files whose Liquid we do not edit,
   so everything here works by targeting their class *prefixes* (the names carry
   a per-instance suffix). Structure and copy stay theirs; the behaviour is ours.

   What it changes, and why:

   Feature cards ("Four things on the label")
     Four static boxes in a row: the eye has no reason to enter any of them, and
     nothing rewards a hover. Now the card lifts, its mark grows into a tinted
     dish, a hairline accent draws in along the top, and the row arrives as a
     staggered wave on scroll. Each card also becomes keyboard-focusable so the
     same state is reachable without a mouse.

   Nutrition stat grid
     The figures were plain text. They now count up once on first view and sit
     on tabular figures so the column of numbers lines up.

   How-to cards, goes-well cards, review cards
     Same lift-and-accent language, so the whole page reads as one system rather
     than four different agencies.

   All motion is transform/opacity only, and every hidden state is opt-in via
   `html.jp-life` — added by jp-pdp-life.js once it can observe. If the script
   never runs, the page is exactly as it was, fully readable.
*/

/* ---- the reveal, gated on the script having armed it ----------------- */
html.jp-life [data-jp-life] {
  opacity: 0;
  transform: translate3d(0, 20px, 0);
}
html.jp-life [data-jp-life].is-in {
  animation: jp-life-rise .62s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)) both;
  animation-delay: calc(var(--jp-life-i, 0) * 85ms);
}
@keyframes jp-life-rise {
  from { opacity: 0; transform: translate3d(0, 20px, 0) }
  to   { opacity: 1; transform: none }
}

/* ---- feature cards --------------------------------------------------- */
[class*="ai-feature-card"] {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  cursor: default;
  transition: transform .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}

/* The accent draws in from the left along the top edge — scaleX, so it is a
   compositor-only animation rather than a width transition. */
[class*="ai-feature-card"]::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, #2A5040, #D4943A);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .5s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  z-index: 2;
}

[class*="ai-feature-card"]:hover,
[class*="ai-feature-card"]:focus-visible {
  transform: translateY(-5px);
  box-shadow: 0 22px 44px -28px rgba(20, 40, 32, .55);
  outline: none;
}
[class*="ai-feature-card"]:hover::before,
[class*="ai-feature-card"]:focus-visible::before { transform: scaleX(1) }

/* The little line-drawing mark inside each card. */
[class*="ai-feature-icon"],
[class*="ai-feature-card"] img {
  transition: transform .45s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}
[class*="ai-feature-card"]:hover [class*="ai-feature-icon"],
[class*="ai-feature-card"]:hover img {
  transform: scale(1.1) rotate(-4deg);
}

/* Focus ring for keyboard users, since the script makes these focusable. */
[class*="ai-feature-card"][tabindex]:focus-visible {
  box-shadow: 0 0 0 3px rgba(212, 148, 58, .4),
              0 22px 44px -28px rgba(20, 40, 32, .55);
}

/* ---- nutrition figures ---------------------------------------------- */
[class*="ai-bilona-nutrition-stat-value"] {
  font-variant-numeric: tabular-nums;
}

/* ---- how-to / goes-well / review cards ------------------------------- */
[class*="ai-how-to-card"],
[class*="ai-goes-well-card"],
[class*="ai-review-card"] {
  transition: transform .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .4s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}
[class*="ai-how-to-card"]:hover,
[class*="ai-goes-well-card"]:hover,
[class*="ai-review-card"]:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 40px -26px rgba(20, 40, 32, .5);
}

/* An upsell card's image is the thing being sold; give it the zoom. */
[class*="ai-goes-well-card"] img {
  transition: transform .5s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}
[class*="ai-goes-well-card"]:hover img { transform: scale(1.06) }

@media (prefers-reduced-motion: reduce) {
  html.jp-life [data-jp-life] { opacity: 1; transform: none }
  html.jp-life [data-jp-life].is-in { animation: none }
  [class*="ai-feature-card"],
  [class*="ai-how-to-card"],
  [class*="ai-goes-well-card"],
  [class*="ai-review-card"],
  [class*="ai-feature-icon"],
  [class*="ai-goes-well-card"] img { transition: none }
  [class*="ai-feature-card"]:hover,
  [class*="ai-how-to-card"]:hover,
  [class*="ai-goes-well-card"]:hover,
  [class*="ai-review-card"]:hover { transform: none }
  [class*="ai-feature-card"]::before { display: none }
}

/* ------------------------------------------------------- legible eyebrows
 * The generated sections each label themselves with a 12px tracked cap —
 * "WHAT MAKES IT REAL", "THE BILONA PROCESS", "138 CUSTOMER REVIEWS",
 * "THE JUSTPURE FAMILY" — and every one of them was set in a tint that fails
 * AA at that size: measured 3.18:1 to 4.44:1 against their own backgrounds
 * where 4.5 is the floor. Small tracked caps are the least legible text on the
 * page, so they are the last thing that should be faded.
 *
 * The repo's contrast gate could not catch these: it checks fourteen pairs
 * named by hand, and these classes carry per-instance suffixes
 * (.ai-feature-label-aatnwnkhyd1nlr1b6raigenblock8dd786c7) that no fixed list
 * can name. Prefix selectors reach all of them at once, on every page that
 * renders a generated block, which is the same reason jp-tables styles by
 * prefix rather than by class.
 *
 * Forest on light, cream on dark. Both are already the brand's own ink, so
 * this is a legibility floor rather than a restyle.
 *
 * The generated rules live in an inline <style> that the theme prints AFTER
 * this bundle, so a single-class selector loses on source order alone. The
 * doubled attribute selector outranks them on specificity instead — the same
 * reason a narrower breakpoint has to match specificity rather than reach for
 * !important, which would only hide which rule is really winning. */
[class*="ai-feature-label"][class],
[class*="ai-bilona-process-label"][class],
[class*="ai-reviews-label"][class],
[class*="ai-nutrition-label"][class],
[class*="ai-how-to-label"][class],
[class*="ai-goes-well-label"][class] {
  color: var(--jp-forest, #2A5040);
}

/* The newsletter label sits on the forest panel, so it needs the light ink
   instead — orange on forest measured 3.24:1. */
[class*="ai-newsletter-label"][class] {
  color: var(--jp-cream, #F4F1EA);
}

/* ------------------------------------------------- readable amber buttons
 * White on the theme's amber (--buttonBackground, #D9822B) measures 2.93:1
 * where 4.5 is the floor — "Add to cart", "Buy now" and "Choose Options" are
 * the most important text on a shop and were the least legible on it.
 *
 * The token itself is a brand setting that the theme editor owns, so this
 * darkens the amber only where white ink sits on it: #B35F14 is the same hue
 * carried far enough to clear AA (measured 4.6:1) and reads as the same brand
 * colour beside the untouched amber elsewhere.
 *
 * ponytail: one variable override on the buttons, not a repaint of the theme. */
.button,
button.shopify-payment-button__button,
.product-card-atc,
.shopify-payment-button__button--unbranded {
  --buttonBackground: #B35F14;
}

/* The generated blocks paint their own buttons rather than using the token,
   so the override above never reached them: "Buy Now" stayed on #D9822B
   (2.93:1) and the newsletter's "Join Us" on #FF8C42 (2.31:1) — the two
   loudest calls to action on the page, both below the floor. Same darkened
   amber, same doubled-attribute specificity as the labels above. */
[class*="ai-custom-pdp-buy-now"][class],
[class*="ai-newsletter-button"][class] {
  background: #B35F14;
}

/* ================= jp-bilona-reveal.css ================= */
/* The bilona process, revealed one step at a time.

   This is the brand's USP and it appears in four places — the two hand-written
   sections (.jp-bilona-*) and the two generated PDP blocks
   (.ai-bilona-process-*) — so everything here works by class prefix and covers
   all four without touching any of their Liquid.

   The seven steps arrive in sequence as the row enters the viewport: the
   connecting line draws forward, the circle rises and settles, the number pops
   in behind it, the title follows. It reads as the process actually happening
   rather than as seven icons appearing at once.

   MOVEMENT ONLY, NEVER OPACITY — the rule this theme learned the hard way.
   jp-motion.css carries the full account: an interrupted or never-delivered
   opacity animation leaves the element permanently invisible with nothing in
   the CSS or the DOM to point at, and this site lost headings to it twice. A
   transform that never completes leaves a step a few pixels out of place,
   which nobody notices and nobody loses the USP to. Everything below is
   transform + a scaleX on the line; no element ever rests at opacity 0.

   The hidden state is opt-in via `html.jp-bilona-armed`, added by the script
   only once it can observe. If the script never runs, all seven steps are
   exactly where they belong, fully visible. */

html.jp-bilona-armed [class*="jp-bilona-step"],
html.jp-bilona-armed [class*="ai-bilona-process-step"]:not([class*="title"]):not([class*="description"]) {
  transform: translate3d(0, 22px, 0);
  transition: transform .66s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  transition-delay: calc(var(--jp-bil-i, 0) * 110ms);
}

html.jp-bilona-armed [class*="jp-bilona-step"].jp-bil-in,
html.jp-bilona-armed [class*="ai-bilona-process-step"].jp-bil-in {
  transform: none;
}

/* The circle carries the step's mark. It comes in slightly under-scaled so it
   settles into place rather than sliding, which is what makes a sequence read
   as deliberate instead of as a list scrolling past. */
html.jp-bilona-armed [class*="jp-bilona-icon-circle"] {
  transform: scale(.86);
  transition: transform .6s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  transition-delay: calc(var(--jp-bil-i, 0) * 110ms + 60ms);
}
html.jp-bilona-armed .jp-bil-in [class*="jp-bilona-icon-circle"] { transform: none }

/* The number badge arrives a beat after its circle — small and late, so the
   eye is pulled to the count. No overshoot: the brand's own motion rule is that
   premium never bounces, slowness is what reads expensive. A hard decelerate
   from a smaller scale gives the same "lands last" emphasis without the tack. */
html.jp-bilona-armed [class*="jp-bilona-number"] {
  transform: scale(.4);
  transition: transform .52s cubic-bezier(.22, 1, .36, 1);
  transition-delay: calc(var(--jp-bil-i, 0) * 110ms + 200ms);
}
html.jp-bilona-armed .jp-bil-in [class*="jp-bilona-number"] { transform: none }

/* The line between steps draws forward into the next circle, so the sequence
   is visibly connected rather than seven separate arrivals. scaleX keeps it a
   compositor-only animation. */
html.jp-bilona-armed [class*="jp-bilona-line"] {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .55s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  transition-delay: calc(var(--jp-bil-i, 0) * 110ms + 120ms);
}
html.jp-bilona-armed .jp-bil-in [class*="jp-bilona-line"] { transform: none }

/* The title lands last, after its own mark. */
html.jp-bilona-armed [class*="jp-bilona-title"],
html.jp-bilona-armed [class*="ai-bilona-process-step-title"] {
  transform: translate3d(0, 10px, 0);
  transition: transform .55s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
  transition-delay: calc(var(--jp-bil-i, 0) * 110ms + 260ms);
}
html.jp-bilona-armed .jp-bil-in [class*="jp-bilona-title"],
html.jp-bilona-armed .jp-bil-in [class*="ai-bilona-process-step-title"] {
  transform: none;
}

/* Hovering a step brings its mark forward — the process is the story here, so
   the steps should feel touchable rather than printed. */
[class*="jp-bilona-icon-circle"] {
  transition: transform .38s var(--jp-ease, cubic-bezier(.16, 1, .3, 1)),
              box-shadow .38s var(--jp-ease, cubic-bezier(.16, 1, .3, 1));
}
[class*="jp-bilona-step"]:hover [class*="jp-bilona-icon-circle"] {
  transform: translateY(-4px) scale(1.06);
  box-shadow: 0 16px 30px -18px rgba(20, 40, 32, .6);
}

@media (prefers-reduced-motion: reduce) {
  html.jp-bilona-armed [class*="jp-bilona-step"],
  html.jp-bilona-armed [class*="ai-bilona-process-step"],
  html.jp-bilona-armed [class*="jp-bilona-icon-circle"],
  html.jp-bilona-armed [class*="jp-bilona-number"],
  html.jp-bilona-armed [class*="jp-bilona-line"],
  html.jp-bilona-armed [class*="jp-bilona-title"],
  html.jp-bilona-armed [class*="ai-bilona-process-step-title"] {
    transform: none;
    transition: none;
  }
  [class*="jp-bilona-step"]:hover [class*="jp-bilona-icon-circle"] { transform: none }
}

/* ---------------------------------------- the PDP process image, animated
 * The product page renders its seven steps as one flat PNG, so there is no
 * step markup for the stagger above to work on. Two 8s videos were generated
 * from those exact PNGs (Seedance 2.0, 720p) in which the steps build up one
 * at a time, and jp-layers.js swaps each <img> for its matching <video>.
 *
 * The <img> stays in the DOM, hidden, so the alt text still reaches a screen
 * reader and a crawler. The video carries the same PNG as its poster, so a
 * browser that blocks autoplay, or a phone on Low Power Mode, shows exactly
 * the artwork that was there before — never a blank box.
 *
 * ponytail: no wrapper element, no JS animation loop. The video plays once
 * when it scrolls into view and then holds its last frame, which is the
 * finished diagram. */
.jp-bilona-vid {
  width: 100%;
  height: auto;
  display: block;
  object-fit: contain;
}

/* ---- the canvas image-sequence ---------------------------------------- */
/* Replaces the scrubbed <video>. A seek is a decoder round-trip; an already
   decoded frame is a drawImage. See jp-steps-cinema.js for the full account. */
.jp-cine-track {
  position: relative;
  height: 260vh;
}
.jp-cine-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Fill the pin, and let drawImage do the fitting. A <canvas> with no height
   falls back to its 300x150 intrinsic ratio, so on a phone the portrait strip
   was drawn into a 459x230 letterbox inside an 806px pin — a quarter of the
   screen, the rest of the pin showing through. `max-height` does not give an
   element a height; only `height` does. The frames are already contain-fitted
   in draw(), so the element should simply take the space it is given. */
.jp-cine-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Flat until the first frame is decoded, so the section cannot be scrolled
   past before it exists. */
.jp-cine-track--waiting { height: auto }
.jp-cine-track--waiting > .jp-cine-pin { position: static; height: auto }
.jp-cine-track--waiting .jp-cine-canvas { height: 0 }

/* Whichever strip is hidden at this width takes no space — both are wrapped,
   so both build a track. */
.jp-cine-track--mobile { height: 0 }
.jp-cine-track--mobile > .jp-cine-pin { display: none }
@media screen and (max-width: 749px) {
  .jp-cine-track--mobile { height: 260vh }
  .jp-cine-track--mobile > .jp-cine-pin { display: flex }
  .jp-cine-track--desktop.jp-cine-track--has-mobile { height: 0 }
  .jp-cine-track--desktop.jp-cine-track--has-mobile > .jp-cine-pin { display: none }
}

/* The generated block sizes its wrapper to the still it used to hold and
   clips at the section; both must follow the track instead. */
[class*="ai-bilona-process-image-wrap"]:has(.jp-cine-track),
[class*="ai-bilona-process-container"]:has(.jp-cine-track),
[class*="ai-bilona-process"]:has(.jp-cine-track) {
  height: auto;
  min-height: 0;
  max-height: none;
  overflow: visible;
}

@media (prefers-reduced-motion: reduce) {
  .jp-cine-track, .jp-cine-track--mobile { height: auto }
  .jp-cine-pin { position: static; height: auto }
}

/* ---- the scroll track ------------------------------------------------- */
/* jp-layers.js wraps the clip in .jp-bilona-track > .jp-bilona-pin so the
   eight seconds are spent over a real stretch of scrolling. Without a height
   here the track is only as tall as the clip — measured 675px against an
   856px viewport, a span of MINUS 181, so the whole clip resolved inside one
   flick of the wheel. That is the stepping, on every product page including
   cow; the clips were never the problem.

   The first attempt at this blanked content, and the reason is two ancestors
   up: `.ai-bilona-process` carries `overflow:hidden` at its own natural
   height (894px), so a 2568px track inside it was simply clipped away along
   with everything below. The track only works if the boxes containing it are
   allowed to grow with it. */
.jp-bilona-track {
  position: relative;
  height: 260vh;
}

/* Until the clip can be drawn the track takes no extra height, so there is
   nothing to scroll past: a reader cannot get halfway down a two-screen track
   while a 4-5MB clip is still arriving and have the walk start from wherever
   they landed. jp-layers.js drops this class on `loadeddata`. */
.jp-bilona-track--waiting { height: auto }
.jp-bilona-track--waiting > .jp-bilona-pin { position: static; height: auto }

.jp-bilona-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.jp-bilona-pin > .jp-bilona-vid { max-height: 86vh }

/* The generated block sizes its image wrapper to the still it used to hold,
   and clips at the section. Both have to follow the track's height instead. */
[class*="ai-bilona-process-image-wrap"]:has(> .jp-bilona-track),
[class*="ai-bilona-process-container"]:has(.jp-bilona-track),
[class*="ai-bilona-process"]:has(.jp-bilona-track) {
  height: auto;
  min-height: 0;
  max-height: none;
  overflow: visible;
}

/* Whichever clip is hidden at this width must take no space: both are
   wrapped, so both get a track, and the hidden one was holding its full
   height — half the scrolling spent past a display:none clip. */
.jp-bilona-track--mobile { height: 0 }
@media screen and (max-width: 749px) {
  .jp-bilona-track--mobile { height: 260vh }
  .jp-bilona-track--desktop.jp-bilona-track--has-mobile { height: 0 }
}

/* A pinned track is a hostage situation for someone who asked for less
   movement; jp-layers.js already skips building it there. */
@media (prefers-reduced-motion: reduce) {
  .jp-bilona-track, .jp-bilona-track--mobile { height: auto }
  .jp-bilona-pin { position: static; height: auto }
}

/* Match the block's own breakpoint so the right one shows at the right width:
   the wide zigzag on desktop, the tall stack on a phone.

   Hiding the desktop video on a phone is conditional on a mobile one existing.
   The block only renders its mobile <img> when that setting is filled, and this
   product has it empty — so an unconditional rule blanked the whole section on
   a 375px screen: desktop video hidden, mobile video never created, nothing
   left to show. `:has()` lets the desktop video stay when it is the only one. */
.jp-bilona-vid--mobile { display: none }
@media screen and (max-width: 749px) {
  .jp-bilona-vid--mobile { display: block }
  /* Only hide the desktop video when a mobile one actually exists — the script
     adds this class in that case. */
  .jp-bilona-vid--desktop.jp-bilona-vid--has-mobile { display: none }
}

@media (prefers-reduced-motion: reduce) {
  /* Someone who has asked for less motion gets the still artwork back. */
  .jp-bilona-vid { display: none !important }
  .jp-bilona-vid-still { display: block !important }
}
