/* ==========================================================================
   iMagnet · layout and components
   Every color and size comes from tokens.css. Nothing hardcoded.
   Written with CSS logical properties throughout so the same rules serve
   Hebrew RTL, Arabic RTL and English LTR without a second stylesheet.
   ========================================================================== */

/* --- Reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  /* No browser or WordPress/Hello-Elementor default may flash a coloured
     highlight when a finger taps brand chrome. -webkit-tap-highlight-color is
     inherited, so setting it transparent here clears it for every element —
     the header logo, the opening mark/disc, the stage, every link and button.
     This is the fix for the owner's "coloured circle appears around the logo
     on tap" on the live site: the disc was cleared last round, but the header
     logo (which plays the opening on tap) still carried the device default. */
  -webkit-tap-highlight-color: transparent;
}
body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--f-he);
  font-size: var(--t-body);
  font-weight: var(--fw-light);
  line-height: 1.85;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, p, figure { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; }

/* HELLO ELEMENTOR'S reset.css IS A STARTER SKIN, NOT A RESET. Neutralised
   2026-08-18. It ships, in the parent theme this store is a child of:
       a                                   { color: #c36 }
       a:hover, a:active                   { color: #336 }
       [type=button], [type=submit], button{ border: 1px solid #c36; color: #c36 }
       ...:focus, ...:hover                { background-color: #c36; color: #fff }
   #c36 is rgb(204, 51, 102), and it is the pink in the owner's two photographs:
   the credit-card trigger in the first, the menu's close button in the second.
   It walked between them because ON A PHONE A TAP IS :focus, and :focus stays
   until something else is tapped.
   Our components all answer :hover, so hover was covered by accident. :focus
   never was. Neither was the border: measured on the live checkout, five of the
   nine buttons were carrying a 1px magenta border at rest, including both
   wallet buttons and the credit-card trigger.
   Written in the SAME selector shape as the rules it answers, so it wins on
   order alone and any component with a class still outranks it, and touching
   only the three properties that leak. No padding, no radius, no geometry, so
   nothing moves.
   This replaces an earlier attempt that named four controls one by one (see the
   gallery focus ring in woo-brand.css). A list can only ever cover what somebody
   remembered; this covers every button on the site, including tomorrow's.

   AND THE REASON OUR EXISTING RESET NEVER HELD: `button { color: inherit }` a
   few lines above is specificity (0,0,1), while Hello writes the SAME rule as
   `[type=button], [type=submit], button`, and an attribute selector counts at
   class level. So for every button carrying type="button" or type="submit" —
   which is every button this theme prints, including .nav-toggle and
   .pay__trigger — Hello's (0,1,0) outranked our (0,0,1) and the magenta text
   colour won. Matching the selector shape is what makes this hold. */
[type="button"], [type="submit"], button { border-color: transparent; color: inherit; }
[type="button"]:focus, [type="button"]:hover,
[type="submit"]:focus, [type="submit"]:hover,
button:focus, button:hover {
  background-color: transparent;
  color: inherit;
}
a:hover, a:active { color: inherit; }

:focus-visible {
  outline: 2px solid var(--c-ring);
  outline-offset: 3px;
}
/* The ring's colour is inherited, not chosen per component: --c-ring is the deep
   green at :root (12.95:1 on ivory) and flips to gold inside anything painted on
   the deep green (8.23:1 there). Before this, the ring was the gold everywhere
   and measured 1.57:1 on ivory — under the 3:1 WCAG 2.4.11 asks of a focus
   indicator, so keyboard focus was effectively invisible across most of the
   store. Listing the dark containers here rather than each control inside them
   means a control added to the footer or the header tomorrow is correct with no
   further thought. */
.band-dark,
.topbar,
.site-header { --c-ring: var(--c-accent); }
/* A ring is shown ONLY for genuine keyboard focus (:focus-visible above, kept
   for WCAG 2.4.7). Any focus left by a pointer press — a mouse click or a tap —
   gets no ring, globally, so no UA/theme default can leak a coloured outline
   onto the opening's interactive elements. This is the desktop half of the
   owner's "ring around the logo": the header logo's click is preventDefault-ed
   to play the opening, so focus stays on it, and without this the gold accent
   ring drew a rectangle around the logo on a plain click. Higher specificity
   than a bare a:focus / button:focus theme default, so it wins the reset. */
:focus:not(:focus-visible) { outline: none; }

/* --- Primitives ----------------------------------------------------------- */
.container {
  max-inline-size: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--s-7); }
.section--tight { padding-block: var(--s-6); }
/* Pulls a section up toward the one above it. Used on the collection so it sits
   closer to the value strip. */
.section--flush-top { padding-block-start: var(--s-5); }

.band-dark {
  background: var(--c-primary);
  color: var(--c-on-dark);
}

/* A green band that carries a jungle-and-gold scene instead of a flat fill, so
   the section reads as part of the same world as the hero rather than a slab of
   colour. The foliage is a real photograph; a flat near-black-green scrim rides
   over it so the ivory copy keeps its contrast (the scrim is darker than the
   base green, so text is at least as legible as it was, verified on rendered
   pixels). The scene stays subordinate: present as depth and light, never
   loud enough to fight the words. */
.band-dark--scene {
  /* The same transparent foliage frame and the same green as the top banner, so
     every green scene band across the site reads as one identical treatment. */
  background:
    url(../../assets/web/scene-shop-frame.webp) center / cover no-repeat,
    #0b2013;
}

/* --- Scene banners on interior pages (visual audit 2026-07-23) -------------
   A generated jungle-and-gold scene behind a page's opening title, so no
   interior page opens on bare ivory. scene-shop / scene-story each carry a
   dark ramp baked into the RIGHT of the frame (the RTL copy side), so only a
   light overall wash is added here and contrast is measured on real pixels. */
.page-hero {
  display: grid;
  align-content: center;
  min-block-size: 340px;
  /* Run full-bleed up under the fixed header (cancelling the body's top pad),
     then pad the copy back down so it clears the header. Same move the home
     hero makes, so the header floats on the banner and its logo/pills flip to
     the on-dark treatment via the is-dark marker the script reads. */
  margin-block-start: calc(-1 * var(--chrome-h, 76px));
  padding-block: calc(var(--chrome-h, 76px) + var(--s-6)) var(--s-7);
  background: center / cover no-repeat var(--c-primary);
  color: var(--c-on-dark);
  border-block-end: 1px solid var(--c-line-on-dark);
}
/* The banner opens over the header, so the header starts in on-dark mode; the
   script then flips --np back to 1 as the ivory grid scrolls under the bar. */
html:has(.page-hero) { --np: 0; }
.page-hero .eyebrow { color: var(--c-accent); }
.page-hero .title { margin-block-end: var(--s-3); }
.page-hero .lede { color: var(--c-on-scene-muted); max-inline-size: 54ch; }
/* The shop banner is a transparent foliage frame laid over the solid brand
   green (the green is the CSS colour, not baked into the art), with the copy
   centred in the clean middle the leaves frame. */
.page-hero--shop {
  background-image: url(../../assets/web/scene-shop-frame.webp);
  /* Matched to the green the lower scene band actually renders (side-1 under
     its 0.18 scrim), so the two green bands on the page read as one colour. */
  background-color: #0b2013;
  text-align: center;
}
.page-hero--shop .lede { margin-inline: auto; }
/* UNUSED as of P4, kept only so a future banner-style story page has its
   treatment ready. The APPROVED story page has no .page-hero at all: it is copy
   on the ivory page framed by .foliage-rail down each outer edge (see the Story
   page block further down). Worth stating, because the P4 brief was written
   believing this rule was the story page's hero and sent the contrast check
   after a scrim that nothing renders. If you are measuring the story page,
   measure the rails against the LINE BOXES of the copy, not this. */
.page-hero--story { background-image: linear-gradient(to left, rgba(10, 22, 14, 0.62), rgba(10, 22, 14, 0.16) 55%, rgba(10, 22, 14, 0.04)), url(../../assets/web/scene-story.webp); background-position: center 40%; }
[dir="ltr"] .band-dark--scene { background-position: center; }

/* The muted copy on a scene band is over a photograph, not a flat fill, so it
   takes the on-scene muted value (0.78) the hero already uses, never the flat
   0.62 which is calibrated for solid green and drops below the 4.5:1 floor on
   the lit gold of the scene. Measured worst case on the band: 5.15:1. */
.band-dark--scene .lede,
.band-dark--scene .story__body { color: var(--c-on-scene-muted); }
/* On a phone the scene band showed no foliage at all: scene-shop-frame is a
   foliage FRAME with a clear centre, and a narrow viewport with `cover` crops
   straight to that empty middle, so the leaves at the edges are lost. On the
   phone, use the full foliage scene (the same image the story-page hero uses)
   under a flat dark scrim instead — the plants are actually visible, and the
   scrim keeps the muted ivory copy legible over the lit leaves. The scrim is
   0.78, not lighter: measured on the rendered pixels, the copy sits over a
   blown-out leaf highlight (~rgb(252,256,243)), and only a scrim this dark keeps
   the 0.78-muted copy clear of the 4.5:1 floor there (worst 5.6:1; a 0.62 scrim
   failed at 3.4:1). The foliage still reads clearly as texture underneath. */
@media (max-width: 900px) {
  .band-dark--scene {
    background:
      linear-gradient(rgba(10, 22, 14, 0.78), rgba(10, 22, 14, 0.78)),
      url(../../assets/web/scene-story.webp) center / cover no-repeat,
      #0b2013;
  }
}

/* Gold fails contrast on ivory, so the eyebrow is green on light and gold
   only on the deep green, where it measures 8.2:1. */
.eyebrow {
  font-size: var(--t-eyebrow);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-primary);
  margin-block-end: var(--s-4);
}
/* Every deep-green surface, not only .band-dark. The hero paints the same
   green from its own rule, so an eyebrow there was #14301F on #14301F: a
   1:1 ratio and literally invisible. Gold on the green measures 8.2:1. */
.band-dark .eyebrow,
.hero .eyebrow { color: var(--c-accent); }

/* The brand wordmark. Always the Latin display face, mixed case (never the
   eyebrow's uppercase), so MaxVolmer reads as a name, not a shout. */
.brand {
  font-family: var(--f-la);
  text-transform: none;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* When the brand IS the eyebrow (hero, collection), it keeps the eyebrow colour
   but drops the uppercase and heavy tracking, which suit the sans, not a serif. */
.eyebrow--brand {
  font-family: var(--f-la);
  text-transform: none;
  letter-spacing: 0.05em;
  font-size: 1.05rem;
}

/* Skip to content. Standard 5568 asks that a keyboard user be able to pass the
   navigation, and the store ships an accessibility statement, so this is a
   requirement rather than a nicety. Off screen until it takes focus. */
.skip-link {
  position: fixed;
  inset-block-start: var(--s-2);
  inset-inline-start: var(--s-2);
  z-index: 50;
  padding: 0.7rem 1.3rem;
  background: var(--c-accent);
  color: var(--c-primary);
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  border-radius: var(--radius-sm);
  translate: 0 -400%;
}
.skip-link:focus { translate: 0 0; }

/* Visually hidden, still read aloud. Carries state that color alone implies. */
.vh {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}

/* Latin scent names. Never uppercase the brand name with this class. */
.latin {
  font-family: var(--f-la);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-latin);
  text-transform: uppercase;
}

.title {
  font-size: var(--t-title);
  font-weight: var(--fw-light);
  line-height: 1.25;
}

.lede {
  font-size: var(--t-subtitle);
  font-weight: var(--fw-light);
  line-height: 1.7;
  color: var(--c-muted-text);
}
.band-dark .lede { color: var(--c-on-dark-muted); }

.rule {
  border: 0;
  border-block-start: 1px solid var(--c-hairline);
  margin: 0;
}
.band-dark .rule { border-block-start-color: var(--c-line-on-dark); }

/* --- Buttons -------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0.95rem 2.1rem;
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  letter-spacing: 0.04em;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease),
              border-color 0.3s var(--ease);
}
.btn--gold {
  background: var(--c-accent);
  color: var(--c-primary);
}
.btn--gold:hover,
.btn--gold:focus { background: var(--c-accent-soft); }

.btn--ghost {
  background: transparent;
  color: var(--c-accent);
  border-color: var(--c-line-on-dark);
}
.btn--ghost:hover,
.btn--ghost:focus {
  border-color: var(--c-accent);
  background: rgba(var(--rgb-accent), 0.08);
}

.btn--ink {
  background: var(--c-primary);
  color: var(--c-on-dark);
}
.btn--ink:hover,
.btn--ink:focus { background: var(--c-primary-soft); }

/* Secondary action on ivory. Gold text on ivory fails contrast, so the
   light-background outline button uses the green ink instead. */
.btn--outline {
  background: transparent;
  color: var(--c-primary);
  border-color: var(--c-primary);
}
.btn--outline:hover,
.btn--outline:focus { background: var(--c-primary); color: var(--c-on-dark); }

/* --- The arch ------------------------------------------------------------- */
/* The signature form. Taken from the logo mark: two uprights joined by a
   semicircular crown. Used in exactly two places, hairline only, never
   filled: the hero frame and the scent card. */
.arch {
  position: relative;
  border: 1px solid var(--c-arch-on-dark);
  background: rgba(244, 237, 225, 0.035);
  border-radius: var(--arch-crown) var(--arch-crown) var(--radius) var(--radius);
  display: grid;
  place-items: end center;
  overflow: hidden;
  transition: border-color 0.5s var(--ease);
}
.arch::before {
  /* light held inside the arch. The brand promise is a presence that stays
     in the air, so the bottle sits in a glow rather than on a gradient. */
  content: "";
  position: absolute;
  inset-block-end: 12%;
  inset-inline-start: 50%;
  translate: -50% 0;
  inline-size: 82%;
  aspect-ratio: 1;
  background: radial-gradient(circle, rgba(var(--rgb-accent), 0.20) 0%, transparent 68%);
  pointer-events: none;
}
/* The interior deepens toward its floor. Enki is near-white glass, and on the
   flat ivory it had almost nothing to sit against: the card read as a hole
   rather than as a bottle. A vertical wash gives every bottle a floor to stand
   on, costs no request, and applies to all six cards equally so the grid still
   reads as one set.
   Measured on rendered screenshots rather than judged by eye: the interior was
   flat to within 1.2 luminance points before this and runs 8.3 points deeper
   at the floor after it. 0.16 was tried and rejected. It separates the bottle
   better still, but it turns the arch into a grey-green panel and this arch is
   specified as hairline only, never filled. */
.arch--light {
  border-color: rgba(var(--rgb-accent), 0.45);
  background: linear-gradient(180deg,
    rgba(var(--rgb-primary), 0.012) 0%,
    rgba(var(--rgb-primary), 0.10) 100%);
}
.arch--light::before {
  background: radial-gradient(circle, rgba(var(--rgb-accent), 0.18) 0%, transparent 68%);
}

.arch__bottle {
  position: relative;
  inline-size: 76%;
  margin-block-end: 8%;
  filter: drop-shadow(0 22px 34px rgba(var(--rgb-primary), 0.30));
}


/* A boxed shot is a 1000 square while the arch is portrait, so it cannot use
   the bottle's sizing: at 76% of the arch width a square composition renders
   the bottle inside it tiny. It sits wider and lower instead. */
.arch__pack {
  position: relative;
  inline-size: 94%;
  margin-block-end: 12%;
  filter: drop-shadow(0 18px 28px rgba(var(--rgb-primary), 0.26));
}

/* Placeholder for packshots still in transit. Every product now has real
   photography, so this survives only for a future product added before its
   shoot lands. */
.arch__pending {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--s-3);
  margin-block-end: 26%;
  text-align: center;
}
.arch__pending img { inline-size: 96px; opacity: 0.26; }
.arch__pending span {
  font-size: var(--t-meta);
  letter-spacing: 0.08em;
  color: var(--c-muted-text);
}

/* --- Header --------------------------------------------------------------- */
/* Deep green, and on the home page it runs straight into the hero so the
   two read as one field. */
/* The header is fixed now, so the document has to make room for it or every
   page but the home page starts underneath it. --chrome-h is measured at
   runtime rather than written down, because the preview flag counts toward it
   and will not exist in the theme. The fallback is the header's own height at
   its usual size, so a page with the script blocked is merely a few pixels off
   rather than broken. The opening cancels this padding on purpose: running
   under the header is the whole point of a transparent one. */
body { padding-block-start: var(--chrome-h, 76px); }

/* Fixed and transparent, so the opening frame is a full screen of photograph
   with the brand floating on it rather than a photograph with a green bar
   stapled above it. It earns a background only once there is content behind it
   to separate from.
   backdrop-filter is the part doing the work: over foliage a flat translucent
   panel turns leaf detail into noise behind the nav, and the blur turns it back
   into a surface. -webkit- prefix included because Safari still wants it. */
.site-header {
  position: fixed;
  /* Below the preview flag, which is also fixed and paints above this. At 0 the
     two overlapped and the flag covered the top half of the header: on desktop
     they cleared each other by luck, and at 390px the flag wraps to two lines
     and swallowed the nav entirely. --flag-h is 0 in the theme, where the flag
     does not exist, so this line costs production nothing. */
  inset-block-start: var(--flag-h, 0px);
  inset-inline: 0;
  z-index: 100;
  color: var(--c-on-dark);
  /* The bar itself is never a surface on desktop: no fill, no hairline, no
     frame. The only surfaces in the header are the pills, so the photograph
     runs unbroken behind it.
     That moves a real problem onto the pills rather than removing it. A bar
     used to be solid here because a transparent one measured 1.53:1 over the
     ivory sections, and ivory links on light glass over ivory is the same
     failure. --np is the answer: see the nav pill below. */
  background: transparent;
  border-block-end: 0;
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease);
}

/* "What is under the header right now."
   0 means a dark region is behind it, so the mark can be the light one and the
   pills can stay a thin smoked glass that lets the scene through. 1 means it is
   over something light, so the mark flips to the dark one and the pills bring
   their own darkness, because ivory links on light glass over ivory is ivory on
   ivory. Everything in the bar reads this one number.
   The script keeps it current as you scroll past each section. These two are
   the value before it runs and the value it falls back to: light is the safe
   default because pills that carry their own darkness are legible on anything,
   and a page that opens on the photograph says so up front.
   Both of these sit on the root, which is also where the script writes, so the
   live value simply overrides them as an inline style. On body they would win
   against it for everything inside body, which is the whole page. */
:root { --np: 1; }
/* Static defaults for the fixed-chrome heights. site.js measures the exact values
   at runtime and overrides these inline on <html>, but a permanent strip now ships
   in the theme (the announcement ticker) and on every preview page (the approval
   flag), so the first paint — and any no-JS visitor — must already lay out under
   both the strip and the header: without these the header would render at top:0
   beneath the fixed strip and the body padding would be short by the strip's
   height. The strip is one --t-meta line inside var(--s-2) padding top and bottom;
   76px is the header's resting height, the value the rest of the file falls back
   to. */
:root {
  --header-h: 76px;
  /* 2.5rem = 40px, the strip's height, and it closes exactly:
       2 x var(--s-2) padding            = 2 x 0.5rem            = 1.0000rem
       one --t-meta line at line-height 1.85 = 0.8125 x 1.85     = 1.5031rem
                                                          total  = 2.5031rem
     The TEXT line box sets it. Not the icon: at 1.15em the icon is 0.9344rem
     (~15px), well inside the ~24px text line, so it never governs — worth being
     exact about, because the next person to tighten the strip's line-height will
     trust this comment, and a plausible-but-wrong derivation is how the earlier
     2.25rem (4px short, leaving the first paint that much out until site.js
     measured the real value) survived as long as it did.
     Kept in rem, not px, because every input above is root-relative, so the
     strip genuinely scales with a visitor's text size and a px default would
     drift the moment anyone changed it. Verified identical for every promise at
     360/390/430/768/1440/2560 — they share one grid cell, so that is structural
     rather than lucky. */
  --flag-h: 2.5rem;
  --chrome-h: calc(76px + 2.5rem);
}
html:has(.journey) { --np: 0; }
/* Opt in only where a full-bleed dark scene runs underneath. :has() is load
   bearing and also chosen for how it fails: a browser without it keeps the
   solid header, which is legible on every page. */
/* The preview flag is position:static above a fixed header, so the page needs
   to be pushed down by the header's own height or the first section hides
   under it. Only pages that are not the journey need this: the journey is
   deliberately full-bleed under the header. */
/* Three columns rather than space-between, because the nav has to be centred on
   the bar and not merely placed between two groups: with space-between it drifts
   whenever the cart label or the language segment changes width, and it would
   move again the day a third link is added. The outer columns are equal and
   flexible, the nav is its own width, so the centre is the centre.
   In RTL the first column is the right one, which is why the toggle sits first
   in the markup: on a phone it wants the far side from the cart, and on desktop
   it is not there at all and the logo takes the column. */
.site-header__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-4);
  padding-block: var(--s-3);
}
.site-header__logo {
  justify-self: start;
  position: relative;
  display: inline-flex;
}
.site-header__logo img { block-size: 52px; inline-size: auto; display: block; }
/* The second mark stacks on the first and the two cross fade on --np, the same
   number that darkens the pills. The first one is what sizes the link, so it
   stays in flow and only the second is lifted out of it. */
.site-header__logo img + img {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
}
.logo--on-dark { opacity: calc(1 - var(--np, 1)); }
.logo--on-light { opacity: var(--np, 1); }
.site-header__logo img { transition: opacity 0.3s var(--ease); }

/* The links sit on their own surface rather than directly on the photograph.
   Glass and not a solid fill: the bar is over foliage that is meant to be seen,
   so the pill lifts the text off it without cutting a black hole in the scene.
   It reads as almost nothing over a dark plate, which is the intent, and it is
   what carries the links' contrast once the bar behind it went transparent. */
.nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-self: center;
  gap: var(--s-5);
  font-size: var(--t-small);
  padding: 0.7rem var(--s-5);
  border-radius: 999px;
  /* The brand green, not a smoked black. Glass here means tinted and blurred,
     and the tint is the brand's own colour so the bar belongs to the palette
     rather than reading as a grey panel dropped on the photograph.
     It is a tint and not a veil of ivory: a light veil brightened the leaf
     behind the links and pushed the gold of the active one to 4.48:1, under
     the floor. */
  background-color: rgba(20, 48, 31, 0.62);
  border: 1px solid rgba(244, 237, 225, 0.12);
  backdrop-filter: blur(16px) saturate(115%);
  -webkit-backdrop-filter: blur(16px) saturate(115%);
}
/* The rest of the darkness, for when there is nothing dark behind the pill. It
   is a layer rather than a second colour on the element because a background
   cannot cross fade and this has to: the tone changes while you scroll past a
   section edge, and a step there is visible. */
.nav::before,
.lang-switch::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: rgba(20, 48, 31, 0.92);
  opacity: var(--np, 1);
  transition: opacity 0.3s var(--ease);
  pointer-events: none;
}
/* Positioned, so they paint above the layer rather than under it. */
.nav > *,
.lang-switch > * { position: relative; }
.nav a {
  padding-block: var(--s-1);
  border-block-end: 1px solid transparent;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.nav a:hover, .nav a[aria-current="page"] {
  color: var(--c-accent);
  border-block-end-color: var(--c-accent);
}

.site-header__end {
  display: flex;
  align-items: center;
  justify-self: end;
  gap: var(--s-3);
  font-size: var(--t-small);
}

/* A switch, so it has to look like one thing with two halves rather than like
   two links that happen to be adjacent. The filled half is where you are and
   the empty half is the one tap that changes it. */
.lang-switch {
  position: relative;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  /* Same two layers as the nav pill, and for the same reason. */
  background-color: rgba(10, 22, 14, 0.34);
  background-image: linear-gradient(
    rgba(10, 22, 14, calc(var(--np, 1) * 0.62)),
    rgba(10, 22, 14, calc(var(--np, 1) * 0.62)));
  border: 1px solid rgba(244, 237, 225, 0.12);
  backdrop-filter: blur(16px) saturate(115%);
  -webkit-backdrop-filter: blur(16px) saturate(115%);
}
.lang-switch__seg {
  display: grid;
  place-items: center;
  min-inline-size: 34px;
  block-size: 28px;
  padding-inline: 0.5rem;
  border-radius: 999px;
  font-size: var(--t-meta);
  letter-spacing: 0.06em;
  /* The on-scene 78%, never the flat 62%. This bar is over a photograph and the
     flat value measured 4.30:1 on a lit leaf, under the 4.5:1 floor. */
  color: var(--c-on-scene-muted);
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease);
}
.lang-switch__seg.is-on {
  background: var(--c-accent);
  color: var(--c-primary);
  font-weight: var(--fw-medium);
}
.lang-switch__seg:not(.is-on):hover { color: var(--c-on-dark); }

/* The one filled control on the bar. Gold, pill, and the only thing here that
   is not glass, because it is the only thing here that is an action. */
.cart-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0.62rem 1.3rem;
  border-radius: 999px;
  background: var(--c-accent);
  color: var(--c-primary);
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  letter-spacing: 0.04em;
  white-space: nowrap;
  transition: background-color 0.3s var(--ease);
}
.cart-btn:hover,
.cart-btn:focus { background: var(--c-accent-soft); }
/* The cart sits at the far side of the button, which in RTL means last in the
   markup. It is the object the button is about, so it is the larger of the two
   things in there and the word is the caption. */
.cart-btn__cart {
  position: relative;
  display: block;
  flex: none;
  inline-size: 24px;
  block-size: 24px;
}
.cart-btn__icon {
  inline-size: 24px;
  block-size: 24px;
  display: block;
}
/* On the cart, not next to it. Pulled up and out so it reads as a badge stuck
   on the corner rather than as part of the drawing. */
.cart-btn__count {
  position: absolute;
  inset-block-start: -7px;
  inset-inline-end: -8px;
  display: grid;
  place-items: center;
  min-inline-size: 17px;
  block-size: 17px;
  padding-inline: 3px;
  border-radius: 999px;
  background: var(--c-primary);
  color: var(--c-accent);
  font-size: 11px;
  line-height: 1;
  font-weight: var(--fw-medium);
}

/* Below 900px the nav was simply display:none, which left a phone with no
   navigation at all: six links unreachable, and the only way to any of them
   was the one CTA in the hero. This is the missing half of that rule. */
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  inline-size: 44px;                /* a real 44px target, not a 24px icon */
  block-size: 44px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  inline-size: 24px;
  block-size: 2px;
  margin-inline: auto;
  border-radius: var(--radius-sm);
  background: var(--c-on-dark);
  transition: transform 0.3s var(--ease), opacity 0.2s var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* --- Hero ----------------------------------------------------------------- */
/* One full-bleed photograph that the copy and the bottle stand inside. It was
   a 360px framed arch first, and that was wrong: a whole generated scene was
   showing through a 48px strip down each side of the bottle, which reads as a
   texture rather than as a place. The plates are now generated with a
   deliberate empty half and the copy sits in it. */
.hero {
  position: relative;
  isolation: isolate;          /* keeps the plate and scrim out of the page's stacking order */
  display: grid;
  align-items: center;
  min-block-size: min(80vh, 780px);
  background: var(--c-primary);
  color: var(--c-on-dark);
  padding-block: var(--s-6) var(--s-7);
  overflow: hidden;
}
/* The wrapper exists only to carry the art direction, so it must not become a
   box between .hero and the image it positions. */
.hero picture { display: contents; }
.hero__plate {
  position: absolute;
  inset: 0;
  z-index: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* Anchored to the empty half, not centred. When the viewport is proportionally
     narrower than the plate, cover crops horizontally, and centring would walk
     the foliage into the copy at exactly the widths nobody tests. Anchoring
     here keeps the empty half whole and takes the crop out of the foliage,
     where it costs a narrower band behind the bottle and nothing else. */
  object-position: 100% 50%;
}
/* Hebrew and Arabic both set the copy on the right, which is where the plate's
   empty half already is. English arrives in early August and flips the layout,
   and one mirror handles it: object-position resolves before the transform, so
   the crop still keeps the empty half and the mirror simply moves it to the
   left, under the copy. Foliage has no handedness, so nothing looks reversed. */
[dir="ltr"] .hero__plate { transform: scaleX(-1); }

/* Contrast here is a legal requirement, not a preference, so it is enforced in
   CSS rather than left to whatever the generator produced. Measured on the
   shipped plate by check-plate-deadzone.mjs: the wide plate's copy zone is
   already 15.87:1 for solid ivory and 6.63:1 for the 62% muted colour with no
   scrim at all, so on desktop this is insurance for odd viewport ratios. On
   the phone plate it is load bearing: the muted colour measures 3.01:1 bare
   and 4.80:1 under the scrim. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to left,
    rgba(10, 22, 14, 0.55) 0%,
    rgba(10, 22, 14, 0.34) 34%,
    rgba(10, 22, 14, 0) 66%);
}
[dir="ltr"] .hero::after {
  background: linear-gradient(to right,
    rgba(10, 22, 14, 0.55) 0%,
    rgba(10, 22, 14, 0.34) 34%,
    rgba(10, 22, 14, 0) 66%);
}

.hero__grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1.35fr 0.65fr;
  align-items: center;
  gap: clamp(2rem, 6vw, 5rem);
}
.hero__title {
  font-size: var(--t-display);
  font-weight: var(--fw-light);
  line-height: 1.14;
  margin-block-end: var(--s-4);
  text-wrap: balance;
}
.hero__sub {
  font-size: var(--t-subtitle);
  /* Not --c-on-dark-muted. This line sits on a photograph now, and the flat
     background value fails the contrast floor on a phone. See tokens.css. */
  color: var(--c-on-scene-muted);
  max-inline-size: 40ch;
  margin-block-end: var(--s-5);
}
/* Two hero actions: the featured bottle (gold) and the full collection
   (ghost, which is built for the dark scene). They wrap on a narrow phone. */
.hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}
.hero__figure { justify-self: center; margin: 0; }
/* Free standing in the scene rather than inside a frame. The shadow is black
   and not the green used on ivory cards, because here it falls on foliage:
   its job is separating the glass from the leaves behind it, not implying a
   floor the photograph does not contain. */
.hero__bottle {
  display: block;
  /* 300 rather than 340. The opening rests the bottle at 0.92 of this, so what
     is actually on screen goes from 313px to 276px: present, and no longer
     competing with the foliage for the picture.
     The vw arm is untouched. It only takes over below about 480px, which is a
     width where the phone rules have replaced this sizing anyway. */
  inline-size: min(300px, 62vw);
  block-size: auto;
  filter: drop-shadow(0 26px 46px rgba(0, 0, 0, 0.60));
}

/* --- The opening ---------------------------------------------------------- */
/* A full screen of foliage with the mark standing in it, which resolves into
   the hero as you scroll: the leaves recede, the mark grows and dissolves, the
   photograph underneath takes over, and the copy and the bottle rise into it.
   Built by wrapping the hero rather than rebuilding it, deliberately. The hero
   is the finished state, its contrast is measured on rendered pixels, and this
   only decides when it becomes visible. That also means the reduced-motion
   path is not a second design to maintain: it is the hero, unwrapped.
   One number crosses the JS/CSS seam, --p, the scroll progress from 0 to 1.
   Everything else is a band expressed here, so the timing is tunable in the
   stylesheet by whoever is looking at it rather than buried in a script. */
.journey {
  position: relative;
  background: var(--c-primary);
  /* Scrub distance is the part beyond one screen. 130vh means the opening
     takes a little over one full flick on a phone. */
  block-size: calc(100vh + 130vh);
  block-size: calc(100svh + 130vh);
  margin-block-start: calc(-1 * var(--chrome-h, 76px));   /* sit under the fixed header */
}
.journey__stage {
  position: sticky;
  inset-block-start: 0;
  block-size: 100vh;
  block-size: 100svh;                 /* svh so mobile browser chrome cannot jump the stage */
  overflow: hidden;
}
/* The header floats over the photograph, so the photograph has to give it
   something to sit on. This plate's upper left is its brightest area, which is
   exactly where the cart and language links sit, and a translucent bar plus a
   blur was not enough: the nav measured below the 4.5:1 floor there.
   Sits above the intro rather than under it so it holds at every point of the
   opening, not only once the plate has dissolved. It stops short of the header's
   own height plus a soft tail, so it reads as light falling off rather than as
   a band. */
.journey__stage::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 8;
  block-size: calc(var(--chrome-h, 76px) + 72px);
  background: linear-gradient(to bottom,
    rgba(10, 22, 14, 0.78) 0%,
    rgba(10, 22, 14, 0.62) 46%,
    rgba(10, 22, 14, 0) 100%);
  pointer-events: none;
}
.journey .hero {
  block-size: 100%;
  min-block-size: 0;
  /* Centre the copy in the screen the visitor sees, not in the whole stage. The
     stage is a full 100svh and runs up under the fixed header, but the base
     .hero centres its content in that whole height, so the header's 132px counts
     as empty space above the copy and the copy is pulled up into it: measured,
     the eyebrow sat 76px below the header with 240px of void under the button,
     top heavy enough that the eyebrow lined up with the logo.
     Offsetting the top padding by the chrome height moves the centre of the
     padding box down to the middle of [chrome, stage bottom], which is the
     middle of the visible area. The symmetric s-5 is breathing room on each side
     of that region. --chrome-h is measured and includes the preview flag, so
     this follows the flag out in production and re-centres on the header alone,
     with the 92px fallback covering the first paint before the measure lands. */
  padding-block: calc(var(--chrome-h, 92px) + var(--s-5)) var(--s-5);
}

/* The opening hero is locked to one screen (the sticky stage), so on a short
   desktop viewport its copy and bottle scale down to fit rather than clipping the
   bottle base and the buttons at the fold. The added announcement ticker makes
   the chrome taller, which is what tightened this on laptop-height screens. The
   bottle becomes height-driven (inline-size auto) so it keeps its aspect ratio. */
@media (min-width: 901px) and (max-height: 820px) {
  .journey .hero__title { font-size: clamp(1.85rem, 4.6vh, var(--t-display)); }
  .journey .hero__sub { margin-block-end: var(--s-4); }
  .journey .hero__bottle { inline-size: auto; block-size: min(46vh, 466px); }
}

/* The site opens as one photograph with the mark in it and nothing else. The
   header, and the preview strip above it, arrive on the same gesture that takes
   the mark away: the chrome assembles itself as the brand dissolves, instead of
   sitting over the picture from the first frame.
   The band starts where the mark starts leaving, 0.24, and completes before the
   mark is fully gone at 0.46, so the two read as one handover rather than as a
   gap with nothing on screen.
   Opacity only, never display or visibility: --chrome-h is measured from these
   two elements, and the negative pull that lets the photograph run under the
   header is derived from it. Taking them out of flow would move the thing they
   sit on and the opening would jump at the moment it is meant to settle.
   Scoped to .has-journey, so reduced motion and a failed script get the header
   they always had, fully visible, immediately. */
html.has-journey .site-header,
html.has-journey .preview-flag,
html.has-journey .topbar {
  opacity: clamp(0, calc((var(--p, 0) - 0.24) / 0.22), 1);
}
/* A keyboard reaches the nav before a scroll does. Following focus into a
   header that cannot be seen is worse than showing it early, so focus brings it
   back whatever the scroll position. */
html.has-journey .site-header:focus-within { opacity: 1; }

.journey__intro {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
  /* Holds full until 0.24, gone by 0.54, so the mark has the screen to itself
     before anything starts resolving. */
  opacity: clamp(0, calc((0.54 - var(--p, 0)) / 0.30), 1);
}
/* With the film running the opening is one continuous camera move that ends on
   the hero, so the intro must NOT dissolve halfway: it plays the whole scrub and
   hands over at the very end. The last frame matches the hero plate to a mean
   of 5.2/255, so that handover is a crossfade between two near-identical
   pictures and reads as nothing at all. Ending on the real plate rather than on
   the canvas is deliberate: the resting hero is the one whose contrast is
   measured, and it stays that way. */
html.has-film .journey__intro {
  opacity: clamp(0, calc((0.90 - var(--p, 0)) / 0.12), 1);
}
.journey__intro picture { display: contents; }
/* display:contents hands the picture's children to the parent's layout, and a
   <source> is one of those children. In a block box that costs nothing, so this
   went unnoticed. In this grid it became a second row: two auto rows, the free
   space split between them by the default stretch, and the mark pushed 115px
   below the centre place-items asked for. It was landing below its own pocket,
   the ring of shadow that exists to keep gold legible on lit leaves, which is
   the part that made this worth chasing rather than a matter of taste.
   Found by the press target, which is centred by the box model and therefore
   did not inherit the same mistake: 230 against 345 in one measurement. */
.journey__intro picture > source,
.hero picture > source { display: none; }
.journey__intro img.journey__plate {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* A slow push in. The leaves come toward you as they dissolve, which is what
     makes the hero underneath feel like it was always further back. */
  transform: scale(calc(1 + var(--p, 0) * 0.14));
}
/* The plate keeps its CSS push even once the film is running. Cancelling it
   looked tidier and was worse: .has-film lands mid scroll, so the plate would
   snap back from its zoom while the canvas was still fading in over it. It is
   fully covered by then anyway, so there is nothing to gain and a visible jump
   to lose. */

/* The camera move. Covers the stage exactly as the plate does, and is painted
   only once the script has frames to paint, so it never flashes empty over the
   plate it is standing on. */
.journey__film {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  display: block;
  opacity: 0;
  transition: opacity 240ms linear;
}
html.has-film .journey__film { opacity: 1; }
/* Gold on foliage is the one contrast case here that no plate solves, because
   every plate has lit leaf edges somewhere. So the mark carries its own pocket
   of shadow rather than depending on where the leaves happened to fall. */
.journey__pocket {
  position: absolute;
  inset: 0;
  background: radial-gradient(46% 46% at 50% 50%,
    rgba(10, 22, 14, 0.82) 0%,
    rgba(10, 22, 14, 0.58) 52%,
    rgba(10, 22, 14, 0) 100%);
}
/* The pocket exists to hold the mark, so it has to leave with the mark. It used
   to get that for free from the intro dissolving at 0.54; now that the intro
   stays for the whole move, a shadow with nothing in it would ride the camera
   all the way down. Same band as the mark, deliberately. */
html.has-film .journey__pocket {
  opacity: clamp(0, calc((0.42 - var(--p, 0)) / 0.24), 1);
}
.journey__mark {
  position: relative;
  z-index: 2;
  /* Sized for a full screen rather than for a slot. At 340px it read as a logo
     dropped on a photograph; the opening is the one moment the brand gets the
     whole screen, and the mark should take it. */
  inline-size: min(36vw, 440px);
  block-size: auto;
  /* The mark gets the screen to itself first. It holds at full opacity while it
     grows, and only once it has grown does it start to go, which is also when
     the camera starts moving and the chrome arrives. Three things on one beat
     rather than a logo that is already dissolving before it has been read.
     Leaves a touch before the plate does, so the screen is foliage alone for a
     moment rather than cutting from logo straight to copy. */
  opacity: clamp(0, calc((0.46 - var(--p, 0)) / 0.22), 1);
  /* Starts clearly under full size and grows through the whole opaque beat, so
     the entrance reads as "the mark grows in" rather than a logo that is already
     at full size and merely dissolves. 0.66 at rest -> ~1.02 by 0.24 (still
     fully opaque, the moment it lands at full) -> ~1.35 as it fades out by 0.46. */
  transform: scale(calc(0.66 + var(--p, 0) * 1.5));
}

/* The opening is driven by scrolling and by nothing else — there is no press-to-
   play control any more (it was removed along with the phone auto-play, so the
   whole opening has one driver, the scroll). The stage therefore shows no
   pointer: it is not a click target.
   While the opening is still scrubbing, the hero's real copy and call to action
   sit underneath the film at opacity 0 but are still in the layout, and the film
   above them has pointer-events:none, so a click during the scrub would fall
   through and could navigate off an invisible link. They are held
   non-interactive until the hero has settled (.is-done), at which point they
   fade in and become clickable together. */
html.has-journey .journey:not(.is-done) .hero__copy,
html.has-journey .journey:not(.is-done) .hero__figure { pointer-events: none; }

/* The copy and the bottle arrive last, and share one band so they move as one
   thing rather than as two elements that happen to animate. */
.journey .hero__copy,
.journey .hero__figure {
  --r: clamp(0, calc((var(--p, 0) - 0.46) / 0.30), 1);
  opacity: var(--r);
  transform: translateY(calc((1 - var(--r)) * 26px));
}
/* Later with the film, because the camera is still travelling at 0.46 and copy
   riding a moving frame reads as a caption on someone else's footage.
   It starts where the handover above ends rather than under it. The intro is a
   layer above the hero, so copy that rises while the film is still opaque rises
   invisibly: the first cut of this had the band at 0.74 and the whole hero
   appeared in one jump in the last few percent, which is the pop this timing
   exists to avoid.
   Finishes at 0.90, not 1.0, and that is the whole point of this change. The
   opening used to assemble on the very last pixel of the scrub: measured frame
   by frame, both clips are dead still from about 0.88 on, diff from the last
   frame under 1/255, yet the bottle only reached full size at 1.0. So the one
   moment the hero was whole was the moment the section released, and on a phone,
   which is flicked rather than nudged, you went straight past it. Ending the
   rise at 0.90 leaves 0.90 to 1.0 as a hold: the finished hero, pinned, while
   there is still scroll left to rest on it. */
html.has-film .journey .hero__copy,
html.has-film .journey .hero__figure {
  --r: clamp(0, calc((var(--p, 0) - 0.78) / 0.12), 1);
}
/* The bottle is not a third line of copy, so it stops sharing the copy's move.
   It comes up from below the frame, slightly small, and reaches full size as it
   arrives, which reads as the object being set down in the pocket the camera
   opened rather than being switched on in place.
   It starts a little before the copy and travels further, so it leads and the
   words settle around it.
   The origin is the base of the bottle, not its centre: growing from the centre
   makes glass inflate, growing from the foot makes it stand up. */
html.has-film .journey .hero__figure {
  /* Rises 0.76 to 0.90, a touch ahead of the copy, and comes to rest where the
     camera does. It is the element the owner could not see on a phone, because
     it finished at 1.0 with no scroll left to hold it. It now finishes at 0.90
     and is held, at full size, through the last tenth of the scrub. It still
     starts behind the intro, which is opaque until 0.78 and gone by 0.90, so it
     spends its first sliver hidden and then emerges rising as the film clears
     rather than popping in once it has. */
  --r: clamp(0, calc((var(--p, 0) - 0.76) / 0.14), 1);
  transform-origin: 50% 100%;
  /* Starts smaller and comes to rest below full size, so the arrival is a real
     change of scale and the bottle sits in the scene rather than filling it.
     0.70 to 0.92, not 0.86 to 1: the travel is more than twice what it was and
     the resting size leaves the foliage room to be the picture. */
  transform:
    translateY(calc((1 - var(--r)) * 96px))
    scale(calc(0.70 + var(--r) * 0.22));
}
/* The load-time rise animation would fight the scroll reveal for the same
   properties, and it has nothing to add here: the journey is the entrance. */
.journey .rise { animation: none; opacity: 1; transform: none; }

/* Without JS there is no --p, so every band would sit at its zero and the page
   would open on foliage with the copy invisible and no way to reveal it. The
   journey therefore only exists once a script has confirmed it can drive it.
   Everything above is scoped to that class; this is the state before it. */
/* The negative pull is deliberately NOT reset here. It was at first, and that
   left the transparent header floating over the bare strip of body background
   that the padding had opened, with nothing behind its links: measured at
   1.53:1 for every pixel of the bar. A transparent header needs the dark
   photograph underneath it in every state, not only when the opening runs. */
html:not(.has-journey) .journey { block-size: auto; }
html:not(.has-journey) .journey__stage { position: static; block-size: auto; }
html:not(.has-journey) .journey__intro { display: none; }
html:not(.has-journey) .journey .hero { block-size: auto; min-block-size: min(80vh, 780px); }
html:not(.has-journey) .journey .hero__copy,
html:not(.has-journey) .journey .hero__figure { --r: 1; }

/* Reduced motion lands on exactly the same place as no JS: the finished hero,
   with its measured contrast, and no scroll hijack. The script does not add
   .has-journey when the query matches, so this needs no rules of its own. */

/* --- Value strip ---------------------------------------------------------- */
.values {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-block: 1px solid var(--c-hairline);
}
.values__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--s-5) var(--s-4);
  text-align: center;
  font-size: var(--t-small);
  color: var(--c-text);
}
.values__item + .values__item { border-inline-start: 1px solid var(--c-hairline); }
/* The four store reassurances carry the same line-icon language as the product
   page trust bar: check-circle, delivery, padlock, gift box. */
.values__icon {
  inline-size: 26px;
  block-size: 26px;
  fill: none;
  stroke: var(--c-primary);
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  margin-block-end: var(--s-3);
}
.values__item strong {
  display: block;
  font-weight: var(--fw-medium);
  margin-block-end: var(--s-1);
}
.values__item span { color: var(--c-muted-text); }
/* Four marks wrap to a 2x2 on a phone. The single-row inline dividers would
   then land in the wrong places, so the odd cells drop their start border and
   the second row gains a top border. */
@media (max-width: 720px) {
  .values { grid-template-columns: 1fr 1fr; }
  .values__item:nth-child(odd) { border-inline-start: 0; }
  .values__item:nth-child(n+3) { border-block-start: 1px solid var(--c-hairline); }
}

/* --- Section head --------------------------------------------------------- */
.section-head {
  max-inline-size: 62ch;
  margin-block-end: var(--s-6);
}
.section-head--center {
  margin-inline: auto;
  text-align: center;
}
.section-head .title { margin-block-end: var(--s-3); }

/* --- Scent grid ----------------------------------------------------------- */
.scents {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 3vw, 2.75rem);
}
/* A column, so the price rule can be pinned to the bottom. Hook lines differ
   in length between scents, and with plain blocks a two-line hook pushed that
   card's price 25px below its neighbours' and broke the row's baseline. */
.scent {
  display: flex;
  flex-direction: column;
  block-size: 100%;
}
.scent .arch {
  aspect-ratio: 3 / 3.9;
  margin-block-end: var(--s-4);
}

/* The signature interaction: the bottle is pulled up toward the crown of
   the arch. The brand is called iMagnet. */
@media (prefers-reduced-motion: no-preference) {
  .scent .arch__bottle { transition: transform 0.55s var(--ease), opacity 0.45s var(--ease); }
  .scent .arch__box { transition: opacity 0.45s var(--ease); }
  .scent:hover .arch__bottle,
  .scent:focus-visible .arch__bottle { transform: translateY(-14px); }
}
.scent:hover .arch--light { border-color: var(--c-accent); }

/* Hovering reveals the box the bottle comes in. Savour and Pour Homme are the
   same olive at card size, and the box is what separates them: a different
   colour each, and five of six carry a readable name. It is also the gifting
   proof, which is this store's commercial angle.
   This rides the gesture that already lifts the bottle instead of adding a
   second signature: the bottle is pulled away and what is under it is the box.
   Two deliberate choices. It is a background rather than an <img>, so a screen
   reader hears each product once instead of twice. And it lives inside a hover
   query, so a phone never downloads 221KB of boxes it has no way to reveal.
   Which is also the limit: on a phone this does nothing at all. */
/* display:none on a touch device, so the box is never even fetched there.
   Paired with loading="lazy" this keeps 221KB of boxes off a phone that has no
   way to reveal them. It was first built as a background-image driven by a
   custom property, which was wrong: a relative url() inside a custom property
   resolves against the stylesheet consuming the var(), not the document
   declaring it, so ../assets/ resolved from css/ and pointed at
   site-preview/assets, which does not exist. An <img> src has no such trap. */
.arch__box { display: none; }

@media (hover: hover) and (pointer: fine) {
  .scent .arch__box {
    display: block;
    position: absolute;
    inset-block-end: 10%;
    /* Centred with inset-inline + auto margins, never with
       inset-inline-start:50% plus translate:-50%. That idiom shifts twice in
       the same direction under RTL and threw the box almost entirely outside
       the arch, where overflow:hidden ate it. The same trap is documented for
       .info__bubble further down this file. */
    inset-inline: 0;
    margin-inline: auto;
    inline-size: 94%;
    opacity: 0;
    pointer-events: none;
  }
  .scent:hover .arch__box,
  .scent:focus-visible .arch__box { opacity: 1; }
  /* :not(:only-child) — the bottle only fades out when there is a box to fade
     IN behind it. A card whose arch holds the bottle alone (the related-scent
     strip on the product page, and any live product with a featured image but
     no gift-box gallery shot) would otherwise hover to an empty arch. */
  .scent:hover .arch__bottle:not(:only-child),
  .scent:focus-visible .arch__bottle:not(:only-child) { opacity: 0; }
}

.scent__cat {
  font-size: var(--t-meta);
  letter-spacing: 0.1em;
  color: var(--c-muted-text);
  margin-block-end: var(--s-1);
}
/* The brand line sits above each scent name, in the brand green rather than the
   muted gender line, so the card reads brand then product. It is a dedicated
   line because more brands than MaxVolmer may join the shelf later. */
.scent__brand {
  font-size: var(--t-meta);
  font-weight: var(--fw-medium);
  letter-spacing: 0.14em;
  color: var(--c-primary);
  margin-block-end: var(--s-2);
}
.scent__name {
  font-size: 1.05rem;
  color: var(--c-primary);
  margin-block-end: var(--s-2);
}
.scent__hook {
  font-size: var(--t-small);
  line-height: 1.7;
  margin-block-end: var(--s-3);
  text-wrap: pretty;
}
.scent__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin-block-start: auto;   /* pins the price rule to the card's bottom edge */
  padding-block-start: var(--s-3);
  border-block-start: 1px solid var(--c-hairline);
}
.scent__price {
  font-size: 1.15rem;
  font-weight: var(--fw-book);
  color: var(--c-primary);
}
.scent__family { font-size: var(--t-meta); color: var(--c-muted-text); }

/* --- Story ---------------------------------------------------------------- */
.story__inner {
  max-inline-size: 60ch;
  margin-inline: auto;
  text-align: center;
}
.story__inner .title { margin-block-end: var(--s-4); }
.story__body {
  color: var(--c-muted-text);
  font-size: var(--t-subtitle);
  margin-block-end: var(--s-5);
  text-wrap: pretty;
}
/* On the deep green the body flips to the on-dark muted ink. The home page's
   #story teaser is a dark band and reads the same as before; the story page
   adds light sections, where the ivory-on-ivory default would be invisible. */
.band-dark .story__body { color: var(--c-on-dark-muted); }
.story__inner .btn { display: inline-flex; }

/* Story page: tropical foliage rails down both outer edges.
   iMagnet jungle foliage, colour-matched to the site leaves and rebuilt as a
   vertically seamless mirror loop (transparent PNGs in ../assets/story-page/).
   The rails sit ABOVE the flat section colours and BELOW the text, so the leaves
   spill from the green bands onto the ivory sections as one continuous motif
   down the scroll. They reach UP behind the transparent fixed header so the
   foliage begins at the very top of the page, not below the menu. */
.page-story main { position: relative; }
.foliage-rail {
  position: absolute;
  inset-block-start: calc(-1 * var(--chrome-h, 76px));  /* start at the page top, behind the header */
  inset-block-end: 0;
  inline-size: clamp(72px, 15vw, 280px);   /* full opacity at every size, incl. mobile */
  z-index: 0;
  pointer-events: none;
  background-repeat: repeat-y;
  background-size: 100% auto;
}
/* WebP, not the PNG masters. The masters are 806x5376 RGBA at ~4.95 MB EACH, and
   the rail renders at most 280px wide (56px on a phone), so shipping them made
   the story page a 10.3 MB cold load on mobile for two decorative tiles: 98% of
   the page's weight, at a 14x oversample. Re-encoded by
   scripts/build-rail-webp.mjs, which mirrors the top half rather than resizing
   the whole tile, because these are exact vertical mirrors and that symmetry is
   what makes repeat-y seamless. Phones take a 160px cut instead (below). */
.foliage-rail--left  { left: 0;  background-image: url(../../assets/story-page/foliage-rail-left.webp);  background-position: left top; }
.foliage-rail--right { right: 0; background-image: url(../../assets/story-page/foliage-rail-right.webp); background-position: right top; }

/* The copy stays centred and its width tracks the viewport, so it always sits
   in the clear centre the two rails frame and never touches a leaf, on mobile
   or desktop. 58vw leaves ~21vw of clear gutter each side; the leaves reach at
   most ~12vw in, so there is always daylight between the words and the plants. */
.page-story .container { position: relative; z-index: 1; }
.page-story .page-head,
.page-story .story__inner {
  max-inline-size: min(56ch, 58vw);
  margin-inline: auto;
  text-align: center;
}
.page-story .page-head .lede { max-inline-size: min(52ch, 56vw); margin-inline: auto; }

/* Bold words inside the running copy carry the full ink (not the muted body
   colour) so the emphasis actually pops and guides the eye. */
.story__body strong,
.lede strong { font-weight: var(--fw-medium); color: var(--c-text); }
.band-dark .story__body strong,
.band-dark .lede strong { color: var(--c-on-dark); font-weight: var(--fw-medium); }

/* On phones the leaves stay at full strength but slim, and the copy narrows to
   a centred column with clear daylight on both sides. */
@media (max-width: 720px) {
  /* Owner, 2026-07-28: the leaves should read bigger on a phone. 56px -> 88px.
     The constraint is not taste, it is the 5568 contrast floor: the rails sit
     BEHIND the copy (z-index 0 against the container's 1), so a leaf that
     reaches under a line of text stops that text sitting on flat ivory and the
     measured worst pixel moves off the background it was cleared against. The
     copy column therefore narrows in step, from 56vw to 54vw.
     88px was tried first and gave the leaves the presence he asked for, but at
     50vw the h1 broke into six two-word lines on a 390px phone, which is a worse
     page than the one that started with thinner leaves. 72px is a visible +29%
     and costs the copy almost nothing. Re-measured on rendered pixels at
     320/360/390/430: every worst pixel still the flat ivory. */
  .foliage-rail { inline-size: 72px; }
  /* A 56px rail has no use for a 560px tile. The phone cut is 160px (still ~2.9x
     for any DPR) and about a seventh of the bytes, which matters most exactly
     where the connection is worst. */
  .foliage-rail--left  { background-image: url(../../assets/story-page/foliage-rail-left-sm.webp); }
  .foliage-rail--right { background-image: url(../../assets/story-page/foliage-rail-right-sm.webp); }
  .page-story .page-head,
  .page-story .story__inner,
  .page-story .page-head .lede { max-inline-size: 54vw; }
}

/* The scene-frame bands (home story teaser, shop hero + closing CTA) carry the
   leaves baked into the background at the section edges. On phones the cover
   crop pulls those leaves inward, so the copy takes extra side room to keep
   clear daylight between the words and the plants. */
@media (max-width: 720px) {
  .band-dark--scene .container,
  .page-hero .container { padding-inline: 18vw; }
}

/* On the story page the side rails run up behind the header, so the logo (at the
   container's right edge) would otherwise sit under the right rail's leaves.
   Nudge it inward, tied to the rail's own reach (~15vw), capped so it never
   drifts too far on wide screens where the leaves are already clear. */
.page-story .site-header__logo { margin-inline-start: clamp(0px, 11vw, 130px); }

/* --- Gifting -------------------------------------------------------------- */
.gift {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: clamp(2rem, 6vw, 5rem);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius);
  padding: clamp(2rem, 5vw, 4rem);
}
.gift__title { margin-block-end: var(--s-3); }
.gift__body { color: var(--c-muted-text); margin-block-end: var(--s-5); }
.gift__figure .arch {
  inline-size: min(280px, 70vw);
  aspect-ratio: 3 / 3.6;
  margin-inline: auto;
}

/* --- Closing + footer ----------------------------------------------------- */
.closing {
  text-align: center;
  padding-block: var(--s-7);
}
.closing .title { margin-block-end: var(--s-5); }

/* Every page ends with a dark band directly above this dark footer, and two
   green blocks in a row read as one undifferentiated field: the closing call
   to action was being swallowed. A gold hairline is the only separator this
   palette needs. */
.site-footer {
  padding-block: var(--s-5) var(--s-4);
  border-block-start: 1px solid var(--c-line-on-dark);
}
.site-footer__grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: var(--s-5);
  padding-block-end: var(--s-4);
}
.site-footer__logo img { block-size: 56px; inline-size: auto; margin-block-end: var(--s-2); }
.site-footer__tag {
  font-size: var(--t-small);
  color: var(--c-on-dark-muted);
  max-inline-size: 32ch;
}
/* The social row sits under the brand: line icons that pick up the muted value
   and warm to gold on hover, the same affordance as the link lists. */
.site-footer__social {
  display: flex;
  gap: var(--s-3);
  list-style: none;
  padding: 0;
  margin-block-start: var(--s-3);
}
.site-footer__social a {
  display: inline-flex;
  color: var(--c-on-dark-muted);
  transition: color .15s ease;
}
.site-footer__social a:hover { color: var(--c-accent); }
.site-footer__social svg {
  inline-size: 20px;
  block-size: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* WhatsApp reads clearest as its solid glyph, so it opts out of the stroke
   treatment the monoline icons share. */
.site-footer__social svg.is-solid { fill: currentColor; stroke: none; }
.site-footer h3 {
  font-size: var(--t-meta);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-eyebrow);
  text-transform: uppercase;
  color: var(--c-accent);
  margin: 0 0 var(--s-3);
}
.site-footer li { margin-block-end: var(--s-2); font-size: var(--t-small); }
.site-footer li a:hover { color: var(--c-accent); }
.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding-block-start: var(--s-3);
  border-block-start: 1px solid var(--c-line-on-dark);
  font-size: var(--t-meta);
  color: var(--c-on-dark-muted);
}
.site-footer__by { display: inline-flex; align-items: center; gap: 0.6ch; }
.site-footer__webside {
  block-size: 15px;
  inline-size: auto;
  display: inline-block;
  vertical-align: middle;
  opacity: 0.85;
}

/* --- Shop page ------------------------------------------------------------ */
.page-head {
  padding-block: var(--s-6) var(--s-5);
  border-block-end: 1px solid var(--c-hairline);
}
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding-block: var(--s-5);
}
.filter {
  padding: 0.5rem 1.25rem;
  font-size: var(--t-small);
  border: 1px solid var(--c-hairline);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: all 0.3s var(--ease);
}
.filter:hover { border-color: var(--c-accent); }
/* Two selectors for one state. In the static preview a filter is a <button>, so
   it says aria-pressed; on the live store each filter is a real destination (a
   product-category archive, or the shop with the category's query), so it is an
   <a> and says aria-current="page" — aria-pressed belongs to toggle buttons, not
   to navigation. Both must look like the approved pressed chip. */
.filter[aria-pressed="true"],
.filter[aria-current="page"] {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: var(--c-on-dark);
}
/* The filter row is a <nav> on the live store, so it needs the anchor reset the
   preview's <button> got for free. */
a.filter { text-decoration: none; color: inherit; display: inline-block; }
a.filter[aria-current="page"] { color: var(--c-on-dark); }

/* --- Product page --------------------------------------------------------- */
.crumbs {
  padding-block: var(--s-4);
  font-size: var(--t-meta);
  color: var(--c-muted-text);
}
.crumbs a:hover { color: var(--c-primary); }

.product {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 6vw, 5rem);
  padding-block: var(--s-5) var(--s-6);
  align-items: start;
}

/* Related products. A green band closes the page, and the cards run in a
   compact variant so the strip supports the product rather than competing
   with it: shorter arch, no hook line, smaller type. */
.related-band { padding-block: var(--s-6) var(--s-4); }
.scents--compact {
  gap: var(--s-4);
  /* Narrower than the container as well as shorter, so the strip reads as a
     footnote to the product rather than a second shop page. */
  max-inline-size: 860px;
  margin-inline: auto;
}
.scents--compact .arch {
  aspect-ratio: 3 / 2.5;
  margin-block-end: var(--s-3);
}
.scents--compact .scent__name { font-size: 0.95rem; }
.scents--compact .scent__price { font-size: 1.05rem; }
.scents--compact .arch__pending img { inline-size: 60px; }
.scents--compact .arch__pending { margin-block-end: 16%; gap: var(--s-2); }

/* Scent cards sitting on the deep green need their own ink. */
.band-dark .scent__name { color: var(--c-on-dark); }
.band-dark .scent__cat,
.band-dark .scent__family,
.band-dark .arch__pending span { color: var(--c-on-dark-muted); }
.band-dark .scent__price { color: var(--c-accent); }
.band-dark .scent__foot { border-block-start-color: var(--c-line-on-dark); }
/* The info column is much taller than the media column, so on desktop the
   media sticks and the right side stops being empty on the way down.
   .product uses align-items:start, which is what lets sticky work here.

   The catch is laptop screens. A stuck element taller than the viewport can
   never be scrolled to the bottom of, so the image gets clipped and stays
   clipped. So the whole media column is capped against the viewport height
   and shrinks to fit rather than being cut. The factor 0.66 is derived from
   what the column actually stacks: the arch at its 3/3.8 ratio, its 16px
   margin, and the thumbnail strip. On a tall desktop 100% wins and nothing
   changes. */
@media (min-width: 901px) {
  .product__media {
    position: sticky;
    inset-block-start: var(--s-5);
    inline-size: min(100%, calc((100vh - 5rem) * 0.66));
    inline-size: min(100%, calc((100dvh - 5rem) * 0.66));
    margin-inline: auto;
  }
}
.product__media .arch {
  aspect-ratio: 3 / 3.8;
  margin-block-end: var(--s-3);
}

/* --- Product gallery ------------------------------------------------------
   The arch becomes the viewport for a horizontal scroll-snap track. The swipe
   on a phone is then the browser's own, which is what makes it smooth on iOS
   Safari, and the arrows and thumbnails are enhancements layered on top. The
   track is laid over the arch's glow rather than replacing the arch, so the
   frame, its hairline and its interior wash are all unchanged. */
.gallery__track {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
  border-radius: inherit;
}
.gallery__track::-webkit-scrollbar { display: none; }
.gallery__slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: grid;
  place-items: end center;   /* the bottle stands on the arch floor, as before */
  min-inline-size: 0;
}
/* The close-up is landscape, so it centres rather than standing. */
.gallery__slide--center { place-items: center; }
.gallery__detail {
  position: relative;
  inline-size: 90%;
  max-block-size: 72%;
  object-fit: contain;
  filter: drop-shadow(0 16px 26px rgba(var(--rgb-primary), 0.22));
}

/* The lifestyle slide: the real bottle inside a generated jungle-and-gold
   scene. Unlike the packshot slides it is a full photograph, so it fills the
   arch edge to edge and inherits its rounded crown, rather than floating on the
   light interior. The scene carries its own light, so it needs none of the
   arch's glow. */
.gallery__slide--scene { place-items: stretch; }
.gallery__scene {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* Circular controls pinned to the arch's vertical centre, where its sides are
   straight and full width so the round radius never clips them. Green fill with
   an ivory chevron reads on the light interior at any slide. Hidden until the
   script marks the gallery ready, and desktop only: a phone swipes. */
/* Relative wrapper around the arch only, so the arrows can be positioned
   against the image's own box and sit just outside it without the arch's
   overflow:hidden clipping them. */
.gallery-frame { position: relative; }
.gallery__arrow {
  position: absolute;
  inset-block-start: 50%;
  translate: 0 -50%;
  z-index: 2;
  display: none;
  place-items: center;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--c-primary);
  color: var(--c-on-dark);
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(var(--rgb-primary), 0.30);
  transition: background-color 0.25s var(--ease);
}
.gallery__arrow:hover,
.gallery__arrow:focus { background: var(--c-primary-soft); }
.gallery__arrow svg { inline-size: 42%; block-size: 42%; }
/* Outside the image, not on top of it. The owner asked for this directly: an
   arrow sitting on the photograph hides a good part of it. The arrows now live
   in .gallery-frame, a relative wrapper around the arch that does NOT clip, so
   they can sit just beyond the arch's edges in the column's own breathing room.
   2.75rem is the arrow's own size, plus a small gap. */
.gallery__arrow--prev { inset-inline-start: calc(-1 * (2.75rem + 0.6rem)); }   /* just past the right edge in RTL */
.gallery__arrow--next { inset-inline-end: calc(-1 * (2.75rem + 0.6rem)); }     /* just past the left edge in RTL  */
@media (min-width: 901px) {
  /* Sibling of the arch now, revealed once the gallery marks itself ready. */
  .gallery.is-ready ~ .gallery__arrow { display: grid; }
}

/* Thumbnails, now selectors rather than decoration. One per slide, the active
   one carries a green border, and the image inside is decorative because the
   button names itself. */
.product__thumbs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-2);
}
.product__thumb {
  aspect-ratio: 4 / 3;   /* square thumbs cost too much of the capped height */
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  background: rgba(var(--rgb-primary), 0.035);
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: border-color 0.25s var(--ease);
}
.product__thumb img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
}
.product__thumb:hover { border-color: var(--c-accent); }
.product__thumb.is-active { border-color: var(--c-primary); }

/* The H1 holds the display name AND the qualifying terms, so the heading
   Google reads carries "בושם", the concentration and the size, while the
   page still leads with the wordmark. */
.product__name {
  margin-block-end: var(--s-2);
  font-weight: var(--fw-light);
}
.product__name-latin {
  display: block;
  font-size: clamp(2rem, 4vw, 2.9rem);
  color: var(--c-primary);
  line-height: 1.1;
}
.product__name-sub {
  display: block;
  margin-block-start: var(--s-2);
  font-family: var(--f-he);
  font-size: var(--t-small);
  font-weight: var(--fw-light);
  letter-spacing: normal;
  text-transform: none;
  color: var(--c-muted-text);
}
.product__cat { font-size: var(--t-meta); letter-spacing: 0.1em; color: var(--c-muted-text); }
.product__hook {
  font-size: var(--t-subtitle);
  line-height: 1.7;
  margin-block: var(--s-4);
  text-wrap: pretty;
}
.product__price {
  font-size: 1.9rem;
  font-weight: var(--fw-book);
  color: var(--c-primary);
  margin-block-end: var(--s-1);
}
.product__vat { font-size: var(--t-meta); color: var(--c-muted-text); margin-block-end: var(--s-4); }

.product__buy {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-block-end: var(--s-4);
}
.product__buy .btn { flex: 1 1 12rem; justify-content: center; }

/* --- Sticky buy bar ------------------------------------------------------
   Once the price and add-to-cart have scrolled past, they follow the reader at
   the foot of the screen. Deep green so it reads as a layer over the ivory page
   rather than part of it, and so the gold button keeps the contrast it has
   everywhere else (ivory on this green measures 12.26:1, gold 8.2:1).
   z-index sits BELOW the header (100) and the announcement strip (101): this is
   the bottom of the screen, but a phone's URL bar collapsing can bring the two
   close, and the chrome should win.
   It is translated out rather than display:none so it can slide, and
   `visibility` follows so nothing inside is tabbable while it is away. */
.buybar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 99;
  background: var(--c-primary);
  color: var(--c-on-dark);
  border-block-start: 1px solid var(--c-line-on-dark);
  padding-block: var(--s-3);
  /* Clear of a phone's home indicator. */
  padding-block-end: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  transform: translateY(100%);
  visibility: hidden;
}
.buybar.is-on { transform: none; visibility: visible; }
@media (prefers-reduced-motion: no-preference) {
  .buybar { transition: transform 0.35s var(--ease), visibility 0.35s var(--ease); }
}
.buybar__inner {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
/* The name and price share a block that takes the free space, so the stepper and
   the button stay put at the far end whatever the name's length. */
.buybar__id {
  flex: 1 1 auto;
  min-inline-size: 0;             /* lets a long name ellipsize instead of pushing */
  display: flex;
  flex-direction: column;
  line-height: 1.25;
}
.buybar__name {
  font-size: var(--t-meta);
  /* The FLAT-green muted (62%, 5.6:1), not the on-scene one (78%). The bar is a
     solid fill, not a photograph, and the two values exist precisely so a flat
     surface is not given the scene's compensation. */
  color: var(--c-on-dark-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.buybar__price {
  font-size: 1.15rem;
  color: var(--c-on-dark);
}
/* The stepper inverts onto the dark bar: its own borders and glyphs are drawn
   for the ivory page. */
.buybar .qty { border-color: var(--c-line-on-dark); flex: none; }
.buybar .qty__btn { color: var(--c-on-dark); }
.buybar .qty__btn:hover,
.buybar .qty__btn:focus { background: rgba(255, 255, 255, 0.09); }
.buybar .qty__input { color: var(--c-on-dark); border-inline-color: var(--c-line-on-dark); }
.buybar .btn { flex: 0 1 14rem; justify-content: center; }
/* Scoped by .buybar (0,2,0) on purpose, not written as a bare .buybar__form
   (0,1,0). In the theme this form also carries WooCommerce's conventional `cart`
   class, and the design's own `.cart` rule — the CART PAGE's two-column grid with
   a ~64px gap — is later in this file, so at equal specificity it would win and
   split the stepper from the button. Exactly the collision woo-brand.css already
   documents for .product__buy.cart. */
.buybar .buybar__form {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin: 0;
  grid-template-columns: none;
  padding-block: 0;
}

@media (max-width: 560px) {
  /* On a phone the button is what matters; the stepper shrinks to its glyphs and
     the name line is dropped so the price and the button both keep their size.
     Every row item also gets `min-inline-size: 0`. Without it a flex item will
     not shrink below its own min-content, and the row here is price + stepper +
     button: on a 360px phone their combined min-content exceeded the container,
     so the row OVERFLOWED instead of fitting, pushing the minus button and the
     price off the side of the screen and slicing the quantity to a sliver that
     reads as a dot. In the theme there is an extra flex level (the real
     add-to-cart <form> wrapping the stepper and the button), which has the same
     default and has to be told the same thing. */
  .buybar { padding-block: var(--s-2); padding-block-end: calc(var(--s-2) + env(safe-area-inset-bottom, 0px)); }
  .buybar__inner { gap: var(--s-2); }
  .buybar__name { display: none; }
  .buybar__id { flex: 0 0 auto; min-inline-size: 0; }
  .buybar .buybar__form { flex: 1 1 auto; min-inline-size: 0; gap: var(--s-2); }
  .buybar .qty__btn { inline-size: 2rem; }
  /* Wider than the buttons so a two-digit quantity still fits. */
  .buybar .qty__input { inline-size: 2.5rem; }
  .buybar .btn { flex: 1 1 auto; min-inline-size: 0; padding-inline: var(--s-2); white-space: nowrap; }
}

/* Quantity stepper. Sized so the two buttons clear a 24px touch target and
   the whole control matches the add-to-cart height beside it. */
.qty {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--c-primary);
  border-radius: var(--radius-sm);
  flex: none;
}
.qty__btn {
  inline-size: 2.75rem;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--c-primary);
  font-size: 1.15rem;
  line-height: 1;
  transition: background-color 0.25s var(--ease);
}
.qty__btn:hover,
.qty__btn:focus { background: rgba(var(--rgb-primary), 0.07); }
.qty__input {
  inline-size: 3rem;
  border: 0;
  border-inline: 1px solid var(--c-hairline);
  background: transparent;
  text-align: center;
  font: inherit;
  font-size: var(--t-small);
  color: var(--c-text);
  /* An input carries browser padding and a min width of its own, and a number
     input additionally reserves room for a spinner on engines that ignore
     `appearance: textfield`. On a narrow control that is enough to squeeze the
     digit down to a smudge. Reset all three so the width declared above is the
     width the digit actually gets. */
  padding: 0;
  min-inline-size: 0;
  -moz-appearance: textfield;
  appearance: textfield;
}
.qty__input::-webkit-outer-spin-button,
.qty__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* The three reassurances. The owner asked for these to carry weight, so
   they are a bordered three-up with icons rather than a thin caption row. */
.trust {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border: 1px solid var(--c-hairline);
  background: var(--c-surface);
  border-radius: var(--radius);
  overflow: hidden;                 /* so the inner dividers stop at the rounded corners */
  margin-block-end: var(--s-5);
}
.trust__item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3);
  font-size: var(--t-meta);
  line-height: 1.45;
  color: var(--c-text);
}
.trust__item + .trust__item { border-inline-start: 1px solid var(--c-hairline); }
.trust__item svg {
  inline-size: 20px;
  block-size: 20px;
  flex: none;
  stroke: var(--c-primary);
  fill: none;
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.product__desc { margin-block-end: var(--s-5); text-wrap: pretty; }

/* Fragrance pyramid. Head, heart, base genuinely are a sequence in
   perfumery, so the ordered structure carries real information. */
.pyramid__intro {
  font-size: var(--t-small);
  color: var(--c-muted-text);
  margin-block-end: var(--s-4);
}

/* Drawn as an actual pyramid. Each row owns its own band, so the bands can
   never drift out of line with their text the way a single background
   shape would. The three tints ascend with depth, which is real: the base
   is what is still on the skin hours later. A 2px gap does the separating,
   never a stroke. */
.pyramid { margin-block-end: var(--s-5); display: grid; gap: 2px; }
.pyramid__row {
  display: grid;
  grid-template-columns: 104px 3.5rem 1fr;
  gap: var(--s-3);
  align-items: center;
}
.pyramid__band {
  block-size: 34px;
  background: var(--c-primary);
}
.pyramid__row:nth-child(1) .pyramid__band {
  clip-path: polygon(50% 0, 66% 100%, 34% 100%);
  opacity: 0.22;
}
.pyramid__row:nth-child(2) .pyramid__band {
  clip-path: polygon(34% 0, 66% 0, 83% 100%, 17% 100%);
  opacity: 0.45;
}
.pyramid__row:nth-child(3) .pyramid__band {
  clip-path: polygon(17% 0, 83% 0, 100% 100%, 0 100%);
  opacity: 0.78;
}
.pyramid__label {
  font-size: var(--t-meta);
  letter-spacing: 0.1em;
  color: var(--c-primary);   /* was gold: 2.1:1 on ivory, fails as text */
  font-weight: var(--fw-medium);
  display: inline-flex;
  align-items: center;
}
.pyramid__notes { font-size: var(--t-small); }

.spec {
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  letter-spacing: 0.04em;
  margin-block-end: var(--s-5);
}

details.faq {
  border-block-end: 1px solid var(--c-hairline);
}
details.faq summary {
  cursor: pointer;
  padding-block: var(--s-3);
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
}
details.faq summary::-webkit-details-marker { display: none; }
details.faq summary::after {
  content: "+";
  color: var(--c-primary);   /* a control affordance needs 3:1; gold gives 2.1 */
  font-size: 1.2rem;
  line-height: 1;
}
details.faq[open] summary::after { content: "\2212"; }
details.faq p {
  padding-block-end: var(--s-4);
  font-size: var(--t-small);
  color: var(--c-muted-text);
  max-inline-size: 66ch;
}

/* --- Info bubble ---------------------------------------------------------- */
/* Opens on hover for mouse and on focus for keyboard and touch, so one
   component covers all three without JavaScript. */
.info {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
  margin-inline-start: var(--s-2);
}
.info__btn {
  inline-size: 18px;
  block-size: 18px;
  border: 1px solid var(--c-muted);
  border-radius: 50%;
  background: transparent;
  color: var(--c-muted-text);
  font-family: var(--f-la);
  font-size: 11px;
  font-weight: var(--fw-medium);
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: help;
  padding: 0;
  transition: border-color 0.25s var(--ease), color 0.25s var(--ease);
}
.info__btn:hover { border-color: var(--c-primary); color: var(--c-primary); }

/* Anchored by its inline-start edge, not centred. Centring with
   inset-inline-start:50% plus a physical translate double-shifts in RTL and
   pushes the bubble off the left edge. Anchoring makes it open along the
   reading direction and stay inside the text column in both directions. */
.info__bubble {
  position: absolute;
  inset-block-start: calc(100% + 10px);
  inset-inline-start: 0;
  inline-size: min(288px, 74vw);
  padding: var(--s-3) var(--s-3);
  background: var(--c-primary);
  color: var(--c-on-dark);
  border-radius: 3px;
  font-size: var(--t-meta);
  font-weight: var(--fw-light);
  line-height: 1.65;
  text-align: start;
  letter-spacing: normal;
  text-transform: none;
  opacity: 0;
  visibility: hidden;
  translate: 0 4px;
  transition: opacity 0.22s var(--ease), translate 0.22s var(--ease),
              visibility 0.22s;
  z-index: 5;
}
.info__bubble::before {
  content: "";
  position: absolute;
  inset-block-end: 100%;
  inset-inline-start: 4px;
  border: 5px solid transparent;
  border-block-end-color: var(--c-primary);
}
.info:hover .info__bubble,
.info:focus-within .info__bubble {
  opacity: 1;
  visibility: visible;
  translate: 0 0;
}

/* --- Performance metrics -------------------------------------------------- */
/* Longevity and sillage are single ratios against a limit, so each is a
   meter, not a chart. One hue: the fill is the brand green, the track a
   lighter step of the same green. The number is always printed as text, so
   no value is reachable only by colour. */
.metrics {
  border-block: 1px solid var(--c-hairline);
  padding-block: var(--s-4);
  margin-block-end: var(--s-5);
  display: grid;
  gap: var(--s-4);
}

.metric__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin-block-end: var(--s-2);
}
.metric__label {
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  display: inline-flex;
  align-items: center;
}
.metric__value {
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  font-variant-numeric: tabular-nums;
}
.metric__value b {
  font-weight: var(--fw-medium);
  color: var(--c-text);
  font-size: var(--t-small);
}

.meter {
  block-size: 6px;
  border-radius: 3px;
  background: rgba(var(--rgb-primary), 0.12);
  overflow: hidden;
}
.meter__fill {
  display: block;
  block-size: 100%;
  background: var(--c-primary);
  border-start-end-radius: 4px;
  border-end-end-radius: 4px;
}
.metric__note {
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  margin-block-start: var(--s-2);
}

/* Categorical rows: season, time of day, gender. Nominal categories, so
   every active chip shares one colour rather than a ramp, and the label
   text is always present so state never rests on colour alone. */
.metric--chips .metric__head { margin-block-end: var(--s-3); }
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.34rem 0.85rem;
  border: 1px solid var(--c-hairline);
  border-radius: 999px;
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  background: transparent;
}
.chip.is-on {
  background: var(--c-primary);
  border-color: var(--c-primary);
  color: var(--c-on-dark);
}
/* Icons inherit currentColor, so they invert with the chip automatically.
   Hairline strokes only, no fills: at 14px a filled glyph turns to mud. */
.chip svg {
  inline-size: 14px;
  block-size: 14px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.85;
}

/* --- Section head, secondary ---------------------------------------------
   A left-anchored eyebrow + title used above a form column or a story band,
   lighter than .page-head so it groups a subsection rather than opening a page. */
.subhead { margin-block-end: var(--s-5); }
.subhead .title { font-size: var(--t-subtitle); margin-block-start: var(--s-2); }

/* --- FAQ page ------------------------------------------------------------- */
/* Reuses the product page's details.faq accordion, grouped into headed sets. */
.faq-groups { max-inline-size: 760px; margin-inline: auto; }
.faq-group + .faq-group { margin-block-start: var(--s-6); }
.faq-group > .eyebrow { margin-block-end: var(--s-3); }
/* A hairline over the first item so each set reads as a bounded list, matching
   the bottom border every row already carries. */
.faq-group .faq:first-of-type { border-block-start: 1px solid var(--c-hairline); }
/* The homepage FAQ teaser: a short set of questions that ends in a link to the
   full knowledge centre, centred under the accordion. */
.faq-home__cta { text-align: center; margin-block-start: var(--s-5); }

/* --- Forms (contact, checkout) -------------------------------------------
   Fields sit on white so they read as an input against the ivory page. The
   focus ring is a legal requirement under 5568, not decoration, so it is a
   green border plus a soft halo that shows on any field. Every control clears
   a 48px touch target. All spacing logical, so the grid holds in He, Ar and
   later En with no second rule. */
.form { display: grid; gap: var(--s-4); }
.field { display: grid; gap: var(--s-2); }
.field > label {
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  color: var(--c-text);
}
.field input,
.field textarea,
.field select {
  font: inherit;
  font-size: var(--t-body);
  color: var(--c-text);
  background: var(--c-surface);
  /* --c-muted, not --c-hairline. A hairline is 1.30:1 on the white field and
     1.10:1 against the ivory page, and WCAG 1.4.11 asks 3:1 for the boundary of
     a control you are expected to find and click into. --c-muted measures
     3.82:1 on the field and 3.47:1 against the page. It is the token marked
     NON-TEXT ONLY, which is exactly what a border is. Dividers elsewhere keep
     the hairline: they carry no interaction. */
  border: 1px solid var(--c-muted);
  border-radius: var(--radius-sm);
  padding: 0.8rem 1rem;
  min-block-size: 3rem;
  inline-size: 100%;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.field textarea { min-block-size: 8.5rem; resize: vertical; line-height: 1.6; }
/* The label carries the meaning, so the placeholder is a hint and can be the
   muted-text value, which still measures 5.4:1 rather than the 3.5:1 muted. */
.field input::placeholder,
.field textarea::placeholder { color: var(--c-muted-text); opacity: 1; }
.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(var(--rgb-primary), 0.12);
}
.field--wide { grid-column: 1 / -1; }
.field__hint { font-size: var(--t-meta); color: var(--c-muted-text); }
/* The visible "required" marker. Quiet, but present: the `required` attribute
   alone is announced to a screen reader and invisible to everyone else, so a
   sighted visitor would only find out which fields matter by failing. */
.field__req {
  font-size: var(--t-meta);
  font-weight: var(--fw-book);
  color: var(--c-muted-text);
  letter-spacing: 0;
  text-transform: none;
}
/* The FAQ group labels are real headings now (h2), so the eyebrow treatment has
   to survive a heading's own font-size and margins. */
h2.eyebrow { font-size: var(--t-eyebrow); }

/* What a submitted form says back. Two states, one shape: a panel with a tinted
   inline-start edge, sitting as the form's first row so the form's own grid gap
   spaces it. The live theme prints whichever applies after a submit; the preview
   ships both hidden so the design is approvable, the same convention the cart's
   empty state uses. Colour is never the only signal — each state carries words,
   and the error state also marks its fields with aria-invalid. */
.form__notice {
  grid-column: 1 / -1;
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  border-inline-start: 3px solid var(--c-primary);
  border-radius: var(--radius-sm);
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-body);
  line-height: 1.7;
  color: var(--c-text);
}
.form__notice--error { border-inline-start-color: var(--c-danger); }
/* A field the server rejected. The ring is the danger colour rather than the
   focus gold, so an invalid field reads as invalid even before it is focused. */
.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: var(--c-danger); }

/* --- Contact page --------------------------------------------------------- */
.contact {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: clamp(2rem, 6vw, 5rem);
  padding-block: var(--s-5) var(--s-6);
  align-items: start;
}
/* `minmax(0, 1fr)` and not the implicit `auto`. Corrected 2026-08-19.
   An implicit grid track is `auto`, whose MINIMUM is the widest item's
   min-content, and one of these cards carries an unbreakable string: the
   Instagram address, 281px of it. The track grew to 393px inside a 350px
   wrapper, so all four cards hung 43px past the page and the contact page
   scrolled sideways on a phone. `min-inline-size: 0` on the card's body did not
   help, because the string is what has no smaller size, not the box around it.
   Measured live at 360: 53px of sideways scroll, and 23px at 390.

   This half stops the TRACK exceeding its container. The value's own
   overflow-wrap below is the other half, and both are needed: cap the track
   alone and the text spills out of the card instead of off the page. */
.contact-methods { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
/* A method is a card; when its value exists later (a wa.me link, a tel:) the
   whole card becomes the anchor and lights its border on hover. Inert now,
   because there is no real number to point at yet. */
.contact-method {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
}
a.contact-method { transition: border-color 0.25s var(--ease); }
a.contact-method:hover { border-color: var(--c-accent); }
.contact-method__icon {
  flex: none;
  inline-size: 46px; block-size: 46px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(var(--rgb-primary), 0.06);
}
.contact-method__icon svg {
  inline-size: 22px; block-size: 22px;
  stroke: var(--c-primary); fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
.contact-method__body { display: grid; gap: 2px; min-inline-size: 0; }
.contact-method__label {
  font-size: var(--t-meta);
  letter-spacing: 0.04em;
  color: var(--c-muted-text);
}
/* `anywhere` rather than `break-word`, because these values are exactly the
   strings that have no break opportunity in them: an address like
   info@imagnet.co.il and a URL like https://instagram.com/imagnet.co.il. It also
   feeds back into the track sizing above, since `anywhere` is the one value that
   also lowers the element's min-content contribution. */
.contact-method__value {
  font-size: var(--t-body);
  color: var(--c-text);
  font-weight: var(--fw-medium);
  overflow-wrap: anywhere;
}
/* The value that has not been filled yet reads plainly as pending, not as a
   real detail a visitor could act on. */
.contact-method__value--pending { color: var(--c-muted-text); font-weight: var(--fw-book); }

/* --- Content page (legal shells: terms, privacy, shipping, accessibility) -
   The frame only. The four pages carry no real copy: a plugin injects the
   legal text into .prose later. .prose therefore styles arbitrary injected
   headings, paragraphs and lists for a comfortable RTL read, and the preview
   shows a marked-empty placeholder so the shell does not read as broken. */
.content-page { padding-block: var(--s-5) var(--s-7); }
.content-page .container { max-inline-size: 820px; }
.content-page__title {
  font-size: var(--t-title);
  font-weight: var(--fw-light);
  line-height: 1.2;
  margin-block-end: var(--s-3);
}
.content-page__meta { font-size: var(--t-meta); color: var(--c-muted-text); margin-block-end: var(--s-5); }
.prose { color: var(--c-text); }
.prose > * + * { margin-block-start: var(--s-4); }
.prose h2 {
  font-size: var(--t-subtitle);
  font-weight: var(--fw-medium);
  line-height: 1.3;
  color: var(--c-primary);
  margin-block-start: var(--s-5);
}
.prose h3 { font-size: var(--t-body); font-weight: var(--fw-medium); margin-block-start: var(--s-4); }
.prose p, .prose li { font-size: var(--t-body); line-height: 1.8; max-inline-size: 68ch; }
.prose ul, .prose ol { padding-inline-start: var(--s-4); display: grid; gap: var(--s-2); }
.prose a { color: var(--c-primary); text-decoration: underline; text-underline-offset: 3px; }
.prose__placeholder {
  border: 1px dashed var(--c-hairline);
  border-radius: var(--radius-sm);
  padding: var(--s-5);
  color: var(--c-muted-text);
  font-size: var(--t-small);
  line-height: 1.7;
  text-align: center;
}

/* --- Cart + checkout (static mockups) ------------------------------------
   Non-functional on purpose: their job is to prove WooCommerce's own Cart and
   Checkout blocks can be styled to this design, and to be the CSS target at
   migration. So every surface is a real brand surface, nothing is invented. */
.cart {
  display: grid;
  grid-template-columns: 1.5fr 0.7fr;
  gap: clamp(2rem, 5vw, 4rem);
  padding-block: var(--s-5) var(--s-6);
  align-items: start;
}
.cart__lines { border-block-start: 1px solid var(--c-hairline); }
.cart-line {
  display: grid;
  grid-template-columns: 84px 1fr auto;
  gap: var(--s-4);
  align-items: center;
  padding-block: var(--s-4);
  border-block-end: 1px solid var(--c-hairline);
}
.cart-line__media {
  inline-size: 84px;
  aspect-ratio: 3 / 4;
  background: rgba(var(--rgb-primary), 0.035);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  display: grid; place-items: center; overflow: hidden;
}
.cart-line__media img { inline-size: 78%; block-size: 78%; object-fit: contain; }
.cart-line__body { display: grid; gap: var(--s-2); min-inline-size: 0; }
/* The stepper is inline-flex, but as a grid item it would stretch to the whole
   column under the default justify-self:stretch. Anchor it to its intrinsic
   width at the inline start (the right, in RTL). */
.cart-line__body .qty { justify-self: start; }
.cart-line__title { font-size: var(--t-body); }
.cart-line__meta { font-size: var(--t-meta); color: var(--c-muted-text); }
.cart-line__end { display: grid; justify-items: end; gap: var(--s-2); text-align: end; }
.cart-line__price { font-size: var(--t-body); font-weight: var(--fw-medium); white-space: nowrap; }
.cart-line__remove {
  background: none; border: 0; padding: 0;
  color: var(--c-muted-text);
  font-size: var(--t-meta);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cart-line__remove:hover { color: var(--c-primary); }

/* The empty state. Shipped in the markup as a hidden variant so the owner can
   see it and the WordPress build has the target, without a second page. */
.cart-empty {
  text-align: center;
  padding-block: var(--s-7);
  border-block: 1px solid var(--c-hairline);
}
.cart-empty p { color: var(--c-muted-text); margin-block: var(--s-3) var(--s-5); }
.is-hidden { display: none; }

/* --- Mini cart, the add-to-cart answer -----------------------------------
   WooCommerce's stock confirmation is a notice injected at the TOP of the page.
   It reflows the layout on every add, it does not belong to this design, and on
   a phone it lands off screen when the tap came from the sticky buy bar, so the
   reader presses "הוסיפו לעגלה" and sees nothing happen at all. This panel
   answers where the eye already is.

   One shell, two behaviours, and that split is deliberate. In the preview the
   body holds sample lines, which is what the owner approves. In the theme the
   body and the header count are registered WooCommerce FRAGMENTS, so the same
   markup carries the live cart and WooCommerce itself refreshes it on every add.
   Nothing about the design is duplicated between the two.

   Without JS none of this runs: the form posts exactly as before and
   WooCommerce shows its own notice, branded in woo-brand.css. The panel is a
   progressive layer over a path that already worked, never a replacement for it.

   z-index 200 clears the header (100), the announcement strip (101) and the
   sticky buy bar (99) — a dialog that a fixed strip can cross is not a dialog. */
.minicart {
  position: fixed;
  inset: 0;
  z-index: 200;
}
.minicart[hidden] { display: none; }

.minicart__scrim {
  position: absolute;
  inset: 0;
  background: rgba(var(--rgb-primary), 0.42);
  opacity: 0;
  transition: opacity 0.28s var(--ease);
}

/* inset-inline-end resolves to the LEFT in RTL, which is the side the cart
   button sits on in the header. The panel comes out from under its own trigger
   rather than from the opposite edge of the screen. */
.minicart__panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: min(25.5rem, 92vw);
  background: var(--c-bg);
  border-inline-start: 1px solid var(--c-hairline);
  box-shadow: -18px 0 48px rgba(var(--rgb-primary), 0.14);
  display: grid;
  grid-template-rows: auto 1fr auto;
  transform: translateX(calc(100% * var(--minicart-dir, -1)));
  transition: transform 0.34s var(--ease);
  max-block-size: 100%;
}
[dir="ltr"] .minicart__panel { --minicart-dir: 1; }

.minicart.is-open .minicart__scrim { opacity: 1; }
.minicart.is-open .minicart__panel { transform: translateX(0); }

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

.minicart__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-4) var(--s-3);
  border-block-end: 1px solid var(--c-hairline);
}
.minicart__title {
  font-size: var(--t-subtitle);
  font-weight: var(--fw-light);
}
.minicart__close {
  background: none;
  border: 0;
  padding: var(--s-2);
  margin: calc(var(--s-2) * -1);
  color: var(--c-muted-text);
  cursor: pointer;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
}
.minicart__close:hover { color: var(--c-primary); }
.minicart__close svg { inline-size: 1.25rem; block-size: 1.25rem; }

/* The scrolling region. min-block-size:0 is what lets a grid row actually
   shrink and scroll instead of pushing the footer off the panel. */
.minicart__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  min-block-size: 0;
  padding-inline: var(--s-4);
}

/* The lines reuse .cart-line wholesale, only tighter: same media plate, same
   type, same rules. One definition of what a cart row looks like. */
/* One row, at every width, and this is an override of a deliberate rule rather
   than a disagreement with it. Below 560px the CART PAGE drops the price under
   the title on purpose, because there a line also carries a quantity stepper and
   a remove control, and all four fight for 390px. The mini cart's line has
   neither: it is a confirmation, not an editor. So the constraint that rule
   exists to solve is absent here, and inheriting it only costs vertical space in
   a panel where height is how many items the reader can see at once.

   Measured before deciding: at 390 the panel is 359 wide and the row needs about
   237, so one row fits with room. grid-template-areas has to be cleared as well
   as the columns re-declared — the named areas from that media query define
   their own two-column structure, and leaving them set while asking for three
   columns is how a "fixed" grid keeps rendering the old shape. */
.minicart__body .cart-line {
  grid-template-columns: 60px 1fr auto;
  grid-template-areas: none;
  gap: var(--s-3);
  padding-block: var(--s-3);
}
.minicart__body .cart-line__media { inline-size: 60px; grid-area: auto; align-self: center; }
.minicart__body .cart-line__body { grid-area: auto; }
.minicart__body .cart-line__end {
  grid-area: auto;
  justify-items: end;
  text-align: end;
}
.minicart__body .cart-line:last-child { border-block-end: 0; }

.minicart__empty {
  text-align: center;
  color: var(--c-muted-text);
  padding-block: var(--s-6);
  font-size: var(--t-small);
}

.minicart__foot {
  padding: var(--s-4);
  border-block-start: 1px solid var(--c-hairline);
  background: var(--c-surface);
  display: grid;
  gap: var(--s-3);
}
.minicart__total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  font-size: var(--t-body);
}
.minicart__total strong { font-weight: var(--fw-medium); }
.minicart__foot .btn { inline-size: 100%; justify-content: center; }
.minicart__note {
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  text-align: center;
}
.minicart__cont {
  background: none;
  border: 0;
  padding: 0;
  color: var(--c-muted-text);
  font: inherit;
  font-size: var(--t-small);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.minicart__cont:hover { color: var(--c-primary); }

/* While the panel is open the page behind it must not scroll. Set on <html> by
   the script so it survives a body that other code also writes to. */
.is-minicart-open { overflow: hidden; }

/* The add-to-cart button while its request is in flight. The label stays put
   (a button that changes width mid-press moves under the finger); only the
   surface dims, and the control is disabled so a double tap cannot add twice. */
.btn.is-busy { opacity: 0.72; cursor: progress; }

/* Order summary, shared by cart and checkout. Sticks beside a tall column on
   desktop and drops under it on a phone. */
.summary {
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  padding: var(--s-5);
}
@media (min-width: 901px) {
  .summary { position: sticky; inset-block-start: var(--s-5); }
}
.summary__title { font-size: var(--t-subtitle); font-weight: var(--fw-light); margin-block-end: var(--s-4); }
.summary__row {
  display: flex;
  justify-content: space-between;
  gap: var(--s-3);
  padding-block: var(--s-2);
  font-size: var(--t-small);
  color: var(--c-muted-text);
}
.summary__row span:last-child { color: var(--c-text); }
.summary__row--total {
  border-block-start: 1px solid var(--c-hairline);
  margin-block-start: var(--s-2);
  padding-block-start: var(--s-4);
  font-size: var(--t-body);
  color: var(--c-text);
  font-weight: var(--fw-medium);
}
.summary .btn { inline-size: 100%; justify-content: center; margin-block-start: var(--s-4); }
.summary__note { font-size: var(--t-meta); color: var(--c-muted-text); text-align: center; margin-block-start: var(--s-3); }

/* Checkout: a form of stacked sections beside the order summary. */
.checkout {
  display: grid;
  grid-template-columns: 1.25fr 0.75fr;
  gap: clamp(2rem, 5vw, 4rem);
  padding-block: var(--s-5) var(--s-6);
  align-items: start;
}
.checkout__form { display: grid; gap: var(--s-6); }
.checkout__section > h2 { font-size: var(--t-subtitle); font-weight: var(--fw-light); margin-block-end: var(--s-4); }
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }

/* A chosen option in a mockup (a shipping method, the payment gateway). Reads
   as selectable without being a live control. */
.option {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  font-size: var(--t-small);
}
.option + .option { margin-block-start: var(--s-2); }
.option.is-on { border-color: var(--c-primary); box-shadow: inset 0 0 0 1px var(--c-primary); }
.option__dot {
  flex: none;
  inline-size: 18px; block-size: 18px;
  border-radius: 50%;
  border: 1px solid var(--c-muted);
  display: grid; place-items: center;
}
.option.is-on .option__dot { border-color: var(--c-primary); }
.option.is-on .option__dot::after {
  content: "";
  inline-size: 9px; block-size: 9px;
  border-radius: 50%;
  background: var(--c-primary);
}
.option__body { display: grid; gap: 2px; min-inline-size: 0; }
.option__meta { font-size: var(--t-meta); color: var(--c-muted-text); }
.option__price { margin-inline-start: auto; font-weight: var(--fw-medium); white-space: nowrap; }

/* --- Gift wrap add-on (checkout) ------------------------------------------
   Was a static mockup: three wrap slots shown in a chosen state plus a
   handwritten-greeting field, none of it wired to anything. It is now the real
   control, and it replaces WooCommerce's order-notes box on the live checkout.

   Two changes the owner locked on 2026-07-30, both narrowing the mockup to what
   the business can actually honour today:
     - the three wrap slots are gone. They were image placeholders waiting on
       real wrap photography, and shipping them live would have put three
       identical glyphs labelled "עטיפה 1/2/3" on a paying checkout. Their
       styles (.giftwrap__wraps, .wrap*) went with them rather than sitting here
       unreferenced; git holds them for the day the photos exist.
     - the greeting no longer promises handwriting, only a card.

   NO JAVASCRIPT. The panel is revealed by the checkbox's own :checked state
   through a sibling selector, which is why the checkbox is a sibling of the
   panel rather than nested inside the label. That matters more here than
   anywhere else on the site: this is the checkout, the gateway lands on it in
   days, and a reader with a blocked script still has to be able to ask for a
   gift wrap. `display: none` (not `visibility` or a transform) is deliberate —
   a collapsed panel must be out of the tab order and unread by a screen reader,
   not merely invisible. */
.giftwrap { display: grid; gap: var(--s-3); }
/* The checkbox itself is visually replaced by .option's dot, but it stays in the
   layout, focusable, and 1px rather than display:none — a hidden input cannot be
   focused, and the focus ring is drawn on the label below. */
.giftwrap__check {
  position: absolute;
  inline-size: 1px; block-size: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; border: 0;
}
.giftwrap__toggle { cursor: pointer; }
.giftwrap__check:focus-visible + .giftwrap__toggle {
  outline: 2px solid var(--c-primary);
  outline-offset: 2px;
}
/* A checkbox, so the mark is a tick rather than .option__dot's filled circle. */
.giftwrap__toggle .option__dot { border-radius: var(--radius-sm); }
.giftwrap__check:checked + .giftwrap__toggle {
  border-color: var(--c-primary);
  box-shadow: inset 0 0 0 1px var(--c-primary);
}
.giftwrap__check:checked + .giftwrap__toggle .option__dot {
  border-color: var(--c-primary);
  background: var(--c-primary);
}
/* The tick. PHYSICAL border sides on purpose: this is a rotated glyph, not text,
   so it has no reading direction to follow. With the logical pair
   (border-inline-end) the kept sides flip in RTL and the same -45deg rotation
   draws the tick backwards. */
.giftwrap__check:checked + .giftwrap__toggle .option__dot::after {
  content: "";
  width: 9px; height: 5px;
  border: 2px solid var(--c-on-dark);
  border-top: 0; border-right: 0;
  border-radius: 1px;
  transform: rotate(-45deg) translate(1px, -1px);
}
.giftwrap__panel { display: none; }
.giftwrap__check:checked ~ .giftwrap__panel {
  display: grid;
  gap: var(--s-2);
  padding: var(--s-4);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  border-inline-start: 3px solid var(--c-accent);
  border-radius: var(--radius-sm);
}

/* --- The payment area (checkout) -----------------------------------------
   Added 2026-08-18 for the move to Tranzila Hosted Fields, which brings the
   card fields INTO the checkout page and removes the second screen.

   THIS BLOCK IS THE ONE DEFINITION. The static mockup (build-static-pages.mjs)
   and the live WooCommerce checkout print the same `pay__*` class names, so both
   are styled from here rather than from two sets that drift apart. site.css is
   copied verbatim into the theme by build-wp-theme.mjs, so the live page gets
   these exact rules. woo-brand.css only bridges WooCommerce's own wrappers.

   ORDER: wallets, divider, card. The owner chose it on 2026-08-18, and the
   measurement backs it: the only order ever paid in this store (188) was paid
   with Apple Pay, so the proven path goes first.  */
/* THE CONTAINER IS THE STATE, and it is deliberately NOT a grid.
   Live, `.pay-box` is WooCommerce's own payment box, whose children include a
   <style> tag the gateway prints and a <fieldset> we are forbidden to reparent
   (it holds Tranzila's iframes). So the vertical rhythm is done with margins on
   the children rather than with a gap on the parent: a gap would put a fixed
   space between the credit row and the panel it opens, and the two have to meet
   flush to read as one control. The row that opens the panel is the one child
   with no margin under it, and that is the whole trick. */
.pay-box > * { margin-block-end: var(--s-4); }
.pay-box > .pay__trigger { margin-block-end: 0; }
.pay-box > :last-child { margin-block-end: 0; }

/* `.pay` now holds only the wallets and the divider. Everything below the
   divider is a direct child of `.pay-box`, because live it has to be: the card
   panel is the gateway's fieldset and it sits where the gateway put it. */
.pay { display: grid; gap: var(--s-4); }

/* WALLETS SIT IN ONE ROW, and that follows from the device, not from taste.
   Apple Pay only ever appears on an Apple device, Google Pay only on Android,
   and bit appears everywhere. So a real reader sees TWO buttons at most, never
   three, and two side by side is a row rather than a stack that pushes the card
   option below the fold.
   flex, not grid: when only bit qualifies (most desktops) it takes the full
   width on its own instead of leaving a hole where a wallet would have been. */
.pay__wallets { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.pay__wallets > .pay__wallet { flex: 1 1 10rem; }
.pay__wallet {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  /* A FIXED height, not a minimum, and the three marks are centred inside it.
     Each network publishes artwork of a different natural height (Apple's is a
     plate, Google's and bit's are bare wordmarks), so a min-height let the
     content decide and the row came out 54 / 50 / 48px: three buttons that
     should read as one control, visibly not lining up.
     52px also clears the 48px floor that Israeli standard 5568 inherits from
     WCAG 2.5.5, which matters most on the control that takes the money. */
  block-size: 52px;
  padding: 0 1.4rem;
  inline-size: 100%;
  font: inherit;
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease);
}
/* Every wallet is hidden until its own availability check passes. A button that
   cannot pay is worse than a missing one: it is a dead end on the money screen.
   The static mockup overrides this so the layout can be seen and approved. */
.pay__wallet[hidden] { display: none; }
.pay__wallet-name { line-height: 1; }

/* THE MARKS ARE THE PAYMENT NETWORKS' OWN ARTWORK.
   Sized by height and never by width, so each keeps the proportions its owner
   published: all three are trademarks whose shape may not be altered. A common
   cap height is also what makes three logos from three companies read as one
   row rather than as three pasted images.

   The per-mark heights differ because the artwork differs in what it includes.
   Apple's mark is a PLATE with the lockup inside it, so it needs to be taller to
   put the same amount of ink on the button as a bare wordmark does. */
.pay__mark { block-size: 22px; inline-size: auto; flex: none; display: block; }
/* Apple's is a plate with padding built into the artwork, so it has to run
   taller than bit's bare wordmark to put the same amount of ink on the button.
   Matched by eye against bit, not by number. */
.pay__mark--apple { block-size: 36px; }
.pay__mark--bit { block-size: 26px; }

/* Apple and Google both make button appearance a CONDITION of using the
   service, and both supply the art. Black is the variant that is legal on a
   light background in each of their guidelines, which is why all three read as
   one row rather than as three brand colours competing on the checkout.
   ⚠️ The wordmarks in the markup are stand-ins until the official assets are
   dropped in. */
/* All three marks are Latin lockups whose internal order their owners fix. The
   page is RTL and RTL reorders flex children, so the button's own writing
   direction is pinned back to LTR: without this the Apple lockup rendered as
   "Pay " instead of " Pay". */
.pay__wallet { direction: ltr; }

/* Apple publishes the Apple Pay Mark as a WHITE plate carrying the black
   Pay lockup. Rather than recolour a trademark to suit a black button, the
   button is matched to the mark, so the plate disappears into it and the lockup
   sits on the button exactly as Apple draws it. This is Apple's own light button
   style, and the hairline is what keeps a white control from dissolving into the
   ivory page behind it. */
.pay__wallet--apple {
  background: #FFF;
  color: #000;
  border-color: rgba(0, 0, 0, 0.14);
}
.pay__wallet--apple:hover,
.pay__wallet--apple:focus { background: #F2F2F2; }

/* ⛔ .pay__wallet--gpay was here and is gone on purpose, 2026-08-18. Tranzila's
   plugin connects no wallet, so Google Pay was a second hand-built integration
   and the owner chose to drop it. Removed rather than left unreferenced: a dead
   rule is a rule somebody later assumes is load-bearing. */
/* bit's own dark teal, sampled from the logo the owner supplied. The mark is a
   raster that carries that same plate behind the wordmark, so matching the
   button to it makes the plate vanish and leaves only the cyan wordmark. Any
   other button colour would show a visible rectangle behind the logo, which is
   the same trap the brand tokens hit with the green behind the iMagnet mark. */
.pay__wallet--bit {
  background: #00353B;
  color: #39EDF6;
}
.pay__wallet--bit:hover,
.pay__wallet--bit:focus { background: #004A53; }
.pay__wallet:focus-visible {
  outline: 2px solid var(--c-ring);
  outline-offset: 2px;
}
/* Mid-charge. The button is also disabled in the DOM; this is what makes that
   visible, because a control that ignores a tap without looking any different
   reads as a broken page and earns a second tap. */
.pay__wallet.is-busy,
.pay__wallet:disabled { opacity: 0.55; cursor: progress; }

/* The divider. A rule with the words sitting in it, not a line and a caption. */
.pay__or {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--t-meta);
  color: var(--c-muted-text);
  margin: 0;
}
.pay__or::before,
.pay__or::after {
  content: "";
  block-size: 1px;
  background: var(--c-hairline);
}

/* --- Credit card, folded behind one row ----------------------------------
   The card fields open on a click instead of sitting open under the wallets.
   Most readers here pay with a wallet, and empty card fields under the button
   they were about to press is noise on the screen that decides.

   🔴 IT IS FOLDED, NOT HIDDEN, AND THE DIFFERENCE IS THE WHOLE MECHANISM.
   `display: none` was the first version, and it would have shipped a bug. The
   card fields are Tranzila <iframe>s, and an iframe whose ancestor is display:none
   gets a zero-size layout viewport; a hosted field created inside one can come
   up with no dimensions and no way to recover. So the panel keeps its layout box
   and is folded to zero height instead. The iframe inside is laid out at its full
   width the entire time and never has to re-measure.

   `visibility: hidden` is what does the accessibility work the display switch
   used to do: it takes the fields out of the tab order AND out of the
   accessibility tree, so a keyboard or screen-reader user does not walk into a
   panel nobody opened. It is the one property here that is not cosmetic.

   ⛔ AND THE OTHER THING THIS SHAPE BUYS. The live page cannot wrap the
   gateway's fieldset in anything: moving a node that contains an iframe reloads
   it. So the state lives on the CONTAINER (`.pay-box`, which is WooCommerce's own
   payment box live and the mockup's wrapper here) and reaches the panel by
   descent. Nothing has to be reparented for this to work, on either page. */
.pay__trigger {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  inline-size: 100%;
  min-block-size: 52px;
  padding: 0 var(--s-3);
  border: 1px solid var(--c-muted);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  font: inherit;
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  text-align: start;
  cursor: pointer;
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.pay__trigger:hover { border-color: var(--c-primary); }
.pay__trigger:focus-visible {
  outline: 2px solid var(--c-ring);
  outline-offset: 2px;
}
.pay__trigger-icon { inline-size: 22px; block-size: 22px; flex: none; color: var(--c-muted-text); }
.pay__trigger-text { margin-inline-end: auto; }
.pay__trigger-chev {
  inline-size: 18px; block-size: 18px; flex: none;
  color: var(--c-muted-text);
  transition: transform 0.25s var(--ease);
}
/* Open: the chevron points up, and the row squares off into the panel below so
   the two read as one object rather than a row with a box under it. */
.pay-open .pay__trigger {
  border-color: var(--c-primary);
  border-end-start-radius: 0;
  border-end-end-radius: 0;
  border-block-end-color: transparent;
}
.pay-open .pay__trigger .pay__trigger-chev { transform: rotate(180deg); }

/* Folded. Every box property is zeroed here and restored in the open state, so
   a collapsed panel contributes no height, no border and no gap of its own.
   min-inline-size: 0 is for the live page specifically: the panel there is a
   <fieldset>, and a fieldset's default min-inline-size is min-content, which
   would push the checkout column wider than the card number inside it. */
.pay__card {
  block-size: 0;
  min-block-size: 0;
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  visibility: hidden;
}
.pay-open .pay__card {
  display: grid;
  margin-block-end: var(--s-4);
  gap: var(--s-4);
  block-size: auto;
  overflow: visible;
  visibility: visible;
  padding: var(--s-4);
  border: 1px solid var(--c-primary);
  border-block-start: 0;
  border-end-start-radius: var(--radius-sm);
  border-end-end-radius: var(--radius-sm);
}
/* TWO COLUMNS, BECAUSE THE GATEWAY ASKED FOR TWO.
   Tranzila prints WooCommerce's own row classes: `form-row-wide` for the card
   number and the ID, and `form-row-first` + `form-row-last` for the expiry and
   CVV, which are meant to sit side by side. Under a single-column grid that
   pairing is lost and the form becomes four stacked boxes for what is really
   three questions.
   ⚠️ woo-brand.css gives those two rows `inline-size: calc(50% - …)` for Woo's
   float layout. Inside a grid the track is already half, so that width would
   halve it again and leave a CVV box a quarter of the form wide. It is undone
   here, at higher specificity, rather than edited there, because on every other
   Woo form the float version is still correct. */
.pay__card > .form-row { display: grid; gap: var(--s-2); }
.pay-open .pay__card { grid-template-columns: 1fr 1fr; }
.pay-open .pay__card > .form-row { margin: 0; inline-size: auto; }
.pay-open .pay__card > .form-row-wide { grid-column: 1 / -1; }

/* WooCommerce ends its card form with a float-clearing div. Under grid it is not
   clearing anything, it is an empty row adding a gap. */
.pay-open .pay__card > .clear { display: none; }

/* The gateway's own error line under a field, which it fills as the reader
   types. Same treatment as .pay__err: no height while empty, so the form does
   not jump every time a digit lands. */
.pay__card [id^="errors_for_"] {
  font-size: var(--t-meta);
  color: var(--c-danger);
}
.pay__card [id^="errors_for_"]:empty { display: none; }

/* --- bit's sheet ---------------------------------------------------------
   Tranzila's SDK draws the bit payment sheet as a fixed overlay with inline
   styles, at z-index 1000000, and with NO way to close it. A reader who changes
   their mind is left with the checkout behind a panel they cannot dismiss, on
   the screen that takes the money.
   This is the backdrop that goes underneath it: it dims the page so the sheet
   reads as a layer rather than as a rendering fault, and it carries the close
   control the SDK does not provide. Deliberately one step below the SDK's own
   z-index, so their sheet stays on top of it. */
.pay__sheet {
  position: fixed;
  inset: 0;
  z-index: 999999;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
.pay__sheet-close {
  margin-block-start: max(1rem, env(safe-area-inset-top));
  min-block-size: 48px;
  padding: 0 1.6rem;
  border: 0;
  border-radius: 999px;
  background: var(--c-surface);
  font: inherit;
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  cursor: pointer;
}
.pay__sheet-close:focus-visible {
  outline: 2px solid var(--c-ring);
  outline-offset: 2px;
}

/* A card field's caption is a <span>, not a <label>, and that is on purpose.
   The control it names is an <iframe> served by Tranzila, and `for` may only
   point at a labelable element: pointing it at the wrapper <div> is invalid HTML
   and buys a screen-reader user nothing at all. So the caption is a span, and
   the wrapper carries role="group" + aria-labelledby, which does announce.
   Styled to match `.field > label` exactly, because a reader must not be able to
   tell that the card fields are built differently from the address fields. */
.pay__label {
  font-size: var(--t-small);
  font-weight: var(--fw-medium);
  color: var(--c-text);
}

/* A hosted field is an <iframe> served by Tranzila, so it cannot be styled from
   this stylesheet: the type inside it is set through the `styles` option passed
   to TzlaHostedFields.create(). What this rule owns is the BOX around it, and it
   deliberately mirrors `.field input` value for value, so a card field and an
   address field are visibly the same control. If .field input changes, this
   changes with it. */
.pay__field {
  display: flex;
  align-items: center;
  background: var(--c-surface);
  border: 1px solid var(--c-muted);
  border-radius: var(--radius-sm);
  padding: 0 1rem;
  min-block-size: 3rem;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.pay__field iframe {
  inline-size: 100%;
  block-size: 100%;
  min-block-size: 0;
  border: 0;
  display: block;
  background: transparent;
}
/* Tranzila puts these two classes on the container itself as the reader types.
   Valid is deliberately quiet: a green flash on every field is noise. Invalid
   uses the system's one error colour, and never as the only signal, because the
   gateway also writes the message. */
.pay__field.hosted-fields-valid { border-color: var(--c-primary); }
.pay__field.hosted-fields-invalid { border-color: var(--c-danger); }
.pay__field:focus-within {
  border-color: var(--c-primary);
  box-shadow: 0 0 0 3px rgba(var(--rgb-primary), 0.12);
}

/* The error line a field can carry. Empty until the gateway fills it, and it
   holds no space while empty so the form does not jump as it validates. */
.pay__err {
  font-size: var(--t-meta);
  color: var(--c-danger);
  margin: 0;
}
.pay__err:empty { display: none; }

.pay__secure {
  display: flex;
  align-items: start;
  gap: var(--s-2);
  font-size: var(--t-meta);
  line-height: 1.7;
  color: var(--c-muted-text);
  margin: 0;
}
.pay__secure svg { inline-size: 16px; block-size: 16px; flex: none; margin-block-start: 0.2em; }

/* --- Forms + mockups, responsive ----------------------------------------- */
/* minmax(0, 1fr) AND NOT 1fr, on every one of these. Fixed 2026-08-18.
   `1fr` is shorthand for `minmax(auto, 1fr)`, and that `auto` minimum is the
   item's min-content: a single-column grid written as `1fr` therefore refuses to
   shrink below whatever its widest content needs, and simply overflows the
   viewport instead. In the mockup that never showed, because the mockup's own
   fields are narrow. On the live checkout it did: `.checkout` also matches
   WooCommerce's real <form class="checkout woocommerce-checkout">, whose billing
   block measures 368px at its minimum, so the column stayed 368px wide inside a
   315px page and the whole checkout scrolled sideways on every phone. Measured
   at 390px: document 398px against a 375px viewport, 48 elements out of bounds;
   with minmax(0, 1fr), 375 against 375 and none.
   minmax(0, …) lets the track go to zero and the content wrap, which is what a
   one-column mobile layout means. */
@media (max-width: 900px) {
  .contact,
  .cart,
  .checkout { grid-template-columns: minmax(0, 1fr); }
  .field-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  /* The price and remove control drop under the title rather than fighting the
     name for width at 390. media | body across two rows. */
  .cart-line {
    grid-template-columns: 68px 1fr;
    grid-template-areas: "media body" "media end";
    column-gap: var(--s-3);
    row-gap: var(--s-3);
  }
  .cart-line__media { grid-area: media; inline-size: 68px; align-self: start; }
  .cart-line__body { grid-area: body; }
  .cart-line__end {
    grid-area: end;
    justify-items: start;
    text-align: start;
    flex-direction: row;
  }
}

/* --- Preview-only note ---------------------------------------------------- */
/* Fixed, because the header above it is fixed and a static flag would scroll
   out from under it and leave a gap. Preview-only scaffolding: it does not
   exist in the theme, which is exactly why the space it takes is measured at
   runtime into --chrome-h rather than written into a rule as a number. */
.preview-flag {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 101;
  /* The brand green itself, not the step up from it. The strip is meant to read
     as the brand's own colour above the bar rather than as a lighter band. */
  background: var(--c-primary);
  color: var(--c-on-dark-muted);
  font-size: var(--t-meta);
  text-align: center;
  padding-block: var(--s-2);
  letter-spacing: 0.04em;
}
.preview-flag strong { color: var(--c-accent); font-weight: var(--fw-medium); }

/* --- Announcement ticker (the permanent top strip) ----------------------- */
/* Plays the same structural role the preview flag does: a fixed strip above the
   header that --flag-h / --chrome-h are measured from (see site.js), so the
   header sits under it and the home opening runs under both. Content is a
   seamless RTL marquee of the store's promises, each led by a gold line icon.
   The moving copy is aria-hidden and a static .vh list carries the promises to
   assistive tech once, rather than as a duplicated, moving string. */
.topbar {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 101;                 /* above the header (100), exactly like the flag */
  background: var(--c-primary);
  color: var(--c-on-dark);
  overflow: hidden;
  font-size: var(--t-meta);
  letter-spacing: 0.03em;
}
/* The strip is a row: the rotating promise takes the space, the pause control
   sits at its end. Both are centred on the same line, so the strip's height is
   still set by the tallest promise and --flag-h does not move. */
.topbar__inner {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.topbar__viewport {
  overflow: hidden;
  padding-block: var(--s-2);
  flex: 1 1 auto;
  min-inline-size: 0;           /* a flex item must be allowed to shrink */
}

/* The pause control. WCAG 2.2.2 (Level A, and inside ת"י 5568 AA) asks for a
   mechanism to pause anything that auto-updates for more than five seconds, and
   the sufficient technique is G4: a real control. The strip already held still
   under prefers-reduced-motion and paused on hover and on a hidden tab, but it
   contained nothing focusable, so a keyboard-only visitor had no mechanism at
   all. This is that mechanism, and it is deliberately the ONE focusable thing in
   the strip: the moving copy stays aria-hidden and the .vh list still carries
   every promise to assistive tech once, unmoving.
   It is not rendered when there is nothing to pause — one promise, or reduced
   motion, where the strip never rotates. site.js decides that. */
.topbar__pause {
  flex: none;
  display: none;                /* site.js reveals it only when it has a job */
  align-items: center;
  justify-content: center;
  inline-size: 1.9rem;
  block-size: 1.9rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--c-accent);
  cursor: pointer;
}
.topbar__pause.is-live { display: inline-flex; }
.topbar__pause svg {
  inline-size: 0.95em;
  block-size: 0.95em;
  fill: currentColor;
  stroke: none;
}
/* One button, two glyphs: whichever matches the current state is shown. A second
   button would take a second tab stop for the same job.
   The state hook is data-state rather than aria-pressed on purpose: this is a
   play/pause toggle, so the accessible NAME changes with the state ("השהיית…" →
   "המשך…"), the way a media control does. aria-pressed on top of that would have
   a screen reader announce the state twice, once contradicting the name. */
.topbar__pause .topbar__play { display: none; }
.topbar__pause[data-state="paused"] .topbar__play { display: block; }
.topbar__pause[data-state="paused"] .topbar__stop { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .topbar__pause { transition: color 0.2s var(--ease); }
}
.topbar__pause:hover { color: var(--c-accent-soft); }
/* The promises do not scroll: ONE value is shown at a time and is replaced every
   ten seconds, cycling forever. Every item lives in the SAME grid cell, stacked,
   which is what makes the swap safe: the row is as tall as the tallest promise,
   so the strip's height is identical whichever one is showing (and identical
   whether or not a long promise wraps on a narrow phone). A strip that changed
   height would move the whole fixed header, because site.js measures this into
   --flag-h / --chrome-h.
   Without JS the first promise stays visible and readable, unanimated. */
.topbar__rotator {
  display: grid;
  align-items: center;
  justify-items: center;
}
.topbar__item {
  grid-area: 1 / 1;             /* every promise in the one cell */
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding-inline: var(--s-4);
  text-align: center;
  opacity: 0;
  pointer-events: none;
}
.topbar__item:first-child { opacity: 1; }   /* the no-JS state */

/* Once the script takes over, .is-on is the only visible promise. The one
   leaving rises and fades; the one arriving rises in from just below, so the
   strip reads as a single value being replaced rather than two things crossing.
   .is-out is listed after .is-on because both are the same specificity. */
.topbar__rotator.is-built .topbar__item { opacity: 0; transform: translateY(0.5em); }
.topbar__rotator.is-built .topbar__item.is-on { opacity: 1; transform: none; }
.topbar__rotator.is-built .topbar__item.is-out { opacity: 0; transform: translateY(-0.5em); }
/* A gold line icon leads every promise (replaces the old separator dot) and sets
   the visual rhythm of the strip. */
.topbar__ico {
  inline-size: 1.15em;
  block-size: 1.15em;
  flex: none;
  fill: none;
  stroke: var(--c-accent);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The rise itself is motion, so it is opt-in. */
@media (prefers-reduced-motion: no-preference) {
  .topbar__rotator.is-built .topbar__item {
    transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
  }
}
/* Motion sensitivity: the value still changes every ten seconds (that is
   content, not motion) but it swaps in place with nothing sliding. */
@media (prefers-reduced-motion: reduce) {
  .topbar__rotator.is-built .topbar__item,
  .topbar__rotator.is-built .topbar__item.is-on,
  .topbar__rotator.is-built .topbar__item.is-out {
    transform: none;
    transition: none;
  }
}

/* --- Load-in -------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .rise {
    opacity: 0;
    transform: translateY(18px);
    animation: rise 0.9s var(--ease) forwards;
  }
  .rise--2 { animation-delay: 0.12s; }
  .rise--3 { animation-delay: 0.24s; }
  @keyframes rise {
    to { opacity: 1; transform: none; }
  }
}

/* --- Responsive ----------------------------------------------------------- */
@media (max-width: 900px) {
  .nav-toggle { display: flex; }

  /* The whole bar becomes one pill on a phone, with the toggle on one side, the
     logo centred and the cart on the other. The toggle is first in the markup,
     so in RTL it takes the right hand column with no reordering.
     The centre column is the flexible one here, the opposite of desktop, so the
     logo stays centred while the two ends size to their content. */
  .site-header__inner {
    grid-template-columns: auto 1fr auto;
    gap: var(--s-2);
    border-radius: 999px;
    padding: var(--s-2) var(--s-3);
    margin-block: var(--s-2);
    background-color: rgba(244, 237, 225, 0.07);
    /* The brand green, the same tint the desktop nav pill carries, not the
       near-black it used to be: over an ivory section the bar reads as green
       rather than as a black bar dropped on white. */
    background-image: linear-gradient(
      rgba(20, 48, 31, calc(var(--np, 1) * 0.88)),
      rgba(20, 48, 31, calc(var(--np, 1) * 0.88)));
    border: 1px solid rgba(244, 237, 225, 0.12);
    backdrop-filter: blur(16px) saturate(115%);
    -webkit-backdrop-filter: blur(16px) saturate(115%);
  }
  .site-header__logo { justify-self: center; }
  .site-header__logo img { block-size: 40px; }
  /* The mark stays the light cut on a phone, always, and does not cross fade the
     way it does on desktop. The bar here is a pill with a dark fill of its own:
     the photograph shows through it over the hero, and the dark green covers it
     over every ivory section, so the mark is on dark in all of them. The desktop
     switch exists because there the logo sits on the bare bar and meets ivory
     directly; here it would swap in the dark cut over an ivory section and land
     it on the pill's own dark green, dark on dark, which is the one pairing the
     whole tone system is there to avoid. So it is pinned, not switched. */
  .logo--on-dark { opacity: 1; }
  .logo--on-light { opacity: 0; }
  /* Four controls do not fit across 390px without the logo losing the middle,
     so the cart keeps its icon and its count and drops the word. The accessible
     name is on the anchor, so nothing is lost to a screen reader. */
  .cart-btn { padding: 0.45rem 0.7rem; gap: var(--s-1); }
  .cart-btn__label { display: none; }
  .lang-switch__seg {
    min-inline-size: 27px;
    block-size: 24px;
    padding-inline: 0.3rem;
  }
  .nav-toggle { inline-size: 40px; block-size: 40px; }
  /* The drawer hangs off the fixed header, so it is fixed too and starts where
     the header ends. The inline offset is logical, so it opens correctly under
     Hebrew and Arabic now and under English in August without a second rule. */
  .nav.is-open {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    position: fixed;
    inset-block-start: var(--header-h, 76px);
    inset-inline: 0;
    padding: var(--s-4) var(--s-5) var(--s-5);
    /* The brand green, like the desktop nav pill, not a near-black panel: the
       drawer belongs to the palette rather than reading as black on the white
       page behind it. Near-opaque so the content under it stays covered, and
       the cream links measure 12.3:1 on this green. */
    background: rgba(20, 48, 31, 0.98);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    border-block-end: 1px solid var(--c-line-on-dark);
    font-size: var(--t-body);
  }
  .nav.is-open a {
    padding-block: var(--s-3);
    border-block-end: 1px solid rgba(var(--rgb-accent), 0.12);
  }
  .nav.is-open a:last-child { border-block-end: 0; }

  .hero__grid,
  .gift,
  .product { grid-template-columns: 1fr; }
  .hero__figure { order: -1; }

  /* On a phone the opening's stage is exactly one screen and it clips, so the
     hero has to FIT that screen rather than merely start in it. It did not:
     the bottle ran to the bottom edge of the plate and the call to action was
     cut off below the fold entirely, which is not a small crop, it is the one
     thing the hero is for.
     The bottle is therefore capped in svh rather than in px or vw. Its height
     is what competes with the copy for the screen, so height is what has to be
     bounded, and bounding it in the same unit as the stage means it holds on a
     short phone in landscape and on a tall one without a second guess. */
  /* The hero's own vertical padding plus the grid's came to about 317px inside
     an 844px screen, and the bottle's row is what paid for it: it collapsed to
     nothing while the copy took everything. Inside the opening the grid owns
     the spacing, because it is the thing that has to clear the fixed header. */
  /* A longer scrub than the desktop's 130vh. The reveal now finishes at 0.90
     and holds to 1.0, and that hold is a tenth of the scrub: a tenth of 130vh
     is about a finger flick, gone in one gesture. A tenth of this is a real
     stretch of screen with the finished hero pinned in it, which on a phone is
     the difference between resting on the bottle and never landing on it.
     It also slows the camera itself, which reads as a heavier, more deliberate
     push on a small screen. */
  .journey {
    block-size: calc(100vh + 190vh);
    block-size: calc(100svh + 190vh);
  }
  html.has-journey .journey .hero { padding-block: 0; }
  html.has-journey .journey .hero__grid {
    /* The screen, stated outright, in the same unit the stage is built from.
       100% and stretch were both tried first and both left this at its content
       height, 597 inside an 844 screen, because the hero is itself a grid whose
       row is auto: the row sized to this and this asked for the row. Naming the
       height rather than deriving it ends that, and the inner 1fr row below can
       finally resolve, which is what gives the bottle its space back. */
    block-size: 100svh;
    /* The copy takes the height it needs and the bottle takes what is left,
       rather than the bottle taking a share and the copy taking its chances.
       Capping the bottle in svh instead was measured on four phones and the
       button was still cut off on all four, by 12px to 118px: any fixed share
       is a guess about a copy block whose height depends on how the Hebrew
       wraps at that width. This asks the question the other way round and there
       is nothing left to guess.
       If the copy alone ever exceeds the screen the bottle collapses to nothing
       and the words survive, which is the right way for this to fail. */
    grid-template-rows: minmax(0, 1fr) auto;
    /* The hero grid centres its items on a wide screen, which sizes each one to
       its own content. Here the figure's only content is the bottle and the
       bottle is out of flow, so centring gave a row 267px tall holding a box
       0px tall. Stretch is what hands the row's height to the figure. */
    align-items: stretch;
    gap: var(--s-3);
    /* Clears the fixed header at the top, and lifts the group off the bottom
       edge so the bottle stands in the scene the way it does on a wide screen
       instead of sitting on the frame. */
    padding-block: calc(var(--chrome-h, 76px) + var(--s-2)) var(--s-5);
  }
  html.has-journey .journey .hero__figure {
    position: relative;
    min-block-size: 0;          /* or the row refuses to shrink under its image */
    /* Stretch, because the bottle inside it is out of flow: the figure has no
       content left to be as wide as, and centring a box with no width gives a
       box with no width. That rendered the hero with the button finally in
       place and no bottle at all. */
    justify-self: stretch;
  }
  /* Taken out of flow on purpose. A percentage height on an image inside a 1fr
     row is circular, the row sizes to the image and the image to the row, and
     the browser resolves it by falling back to the file's intrinsic 1400px:
     the measured need went UP from 834 to 911 doing it the obvious way.
     Absolute against the figure resolves against a box the grid has already
     given a definite height, so the picture fits the space instead of defining
     it. contain keeps the glass in proportion. */
  html.has-journey .journey .hero__bottle {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    max-inline-size: none;
    object-fit: contain;
  }

  /* The scrim runs bottom up on a phone, because that is where the copy is.
     It was load bearing under the old 4:5 plate: 3.01:1 bare, 4.80:1 under
     this, against a 4.5:1 legal floor. The plate is now the frame the phone's
     camera move comes to rest on, whose lower half is genuinely dark rather
     than merely darker, and the same copy measures 16.22:1 worst pixel. So this
     is margin now rather than the thing carrying the floor. It stays: it costs
     nothing visible on an already dark plate, and it is what keeps the floor
     met if the plate is ever recut. */
  .hero::after,
  [dir="ltr"] .hero::after {
    background: linear-gradient(to top,
      rgba(10, 22, 14, 0.62) 0%,
      rgba(10, 22, 14, 0.55) 42%,
      rgba(10, 22, 14, 0) 74%);
  }
  .scents { grid-template-columns: repeat(2, 1fr); }
  .site-footer__grid { grid-template-columns: 1.2fr 1fr 1fr; gap: var(--s-4); }
  .nav { display: none; }

  /* Below desktop the related strip becomes a one-row carousel instead of
     wrapping onto a second row. Cards are 62% wide so the next one always
     peeks in, which is what tells the reader it scrolls. It bleeds to the
     screen edges: the negative margin only cancels the container's own
     padding, so this never widens the page. */
  /* Doubled class on purpose: the narrower breakpoint later in this file
     also matches .scents, and at equal specificity it would win and put the
     first card in a 1fr column while the rest went to auto columns. */
  .scents.scents--compact {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 62%;
    gap: var(--s-3);
    max-inline-size: none;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline-start: var(--gutter);
    margin-inline: calc(var(--gutter) * -1);
    padding-inline: var(--gutter);
    padding-block-end: var(--s-2);
    scrollbar-width: none;
  }
  .scents--compact::-webkit-scrollbar { display: none; }
  .scents--compact .scent { scroll-snap-align: start; }
  .scents--compact .arch { aspect-ratio: 3 / 3.1; }
}

@media (max-width: 560px) {
  :root { --s-7: 4rem; --s-8: 5rem; }

  /* Two up, not one. Stacked single cards made the home page 7,962px on a
     390 screen, about twenty screenfuls, and four of the six scents sat below
     any reasonable scroll. The grid stays at two columns from the wider
     breakpoint; here the gap tightens and the hook drops to meta size so the
     narrower column still holds a readable line. */
  /* Scoped away from the compact strip on purpose. This breakpoint sits after
     the 900 one, so an unscoped .scent rule wins over .scents--compact at
     equal specificity and would resize the product page's carousel too. */
  .scents:not(.scents--compact) { gap: var(--s-3); }
  .scents:not(.scents--compact) .arch {
    aspect-ratio: 3 / 3.6;
    margin-block-end: var(--s-3);
  }
  .scents:not(.scents--compact) .scent__hook {
    font-size: var(--t-meta);
    line-height: 1.6;
  }
  .scents:not(.scents--compact) .scent__name { font-size: 0.95rem; }
  .scents:not(.scents--compact) .scent__price { font-size: 1.05rem; }
  /* Pins the carousel to what it already renders here, which the old
     single-column rule was setting from outside. */
  .scents--compact .arch { aspect-ratio: 3 / 3.4; }
  .site-footer__grid { grid-template-columns: 1fr 1fr 1fr; gap: var(--s-3); }
  .site-footer__logo img { block-size: 34px; }
  .site-footer__tag { font-size: var(--t-meta); }
  .site-footer__social { flex-wrap: wrap; gap: var(--s-2); margin-block-start: var(--s-2); }
  .site-footer__social svg { inline-size: 17px; block-size: 17px; }
  .site-footer__base { justify-content: flex-start; }
  .pyramid__row { grid-template-columns: 76px 3rem 1fr; gap: var(--s-2); }
  .pyramid__notes { font-size: var(--t-meta); }
  .trust { grid-template-columns: 1fr; }
  .trust__item + .trust__item {
    border-inline-start: 0;
    border-block-start: 1px solid var(--c-hairline);
  }
  .product__buy .btn { flex: 1 1 100%; }

  /* An 18px button is too small an anchor at this width. The bubble is 288px
     wide and hung off the page edge, which made the whole document scroll
     sideways by 58px at 390 even with every bubble shut, because a
     visibility:hidden box still counts toward scrollable overflow. Anchoring
     to the row instead and letting the bubble span it keeps it inside the
     viewport in both reading directions, and an open bubble is no longer cut
     off on a phone. */
  .metric__head, .pyramid__row { position: relative; }
  .metric__head .info, .pyramid__row .info { position: static; }
  .metric__head .info__bubble, .pyramid__row .info__bubble {
    inset-inline: 0;
    inline-size: auto;
  }
}
