/* ============================================================
   NORMIL — "THE SUITE"  ·  v3 immersive
   Dark-dominant, gold-forward, glossy, editorial.

   Built from the Vision questionnaire:
     Q26 immersive · Q21 moody + warm golden + luxury editorial
     Q31 gold throughout · Q32 glossy · Q27/Q28 subtle slow reveals
     Q29 large editorial images · Q38 per-product accent

   Obeys design-system/MASTER.md for type, motion, layout, restraint.
   Hand-authored. No framework, no dependencies.
   ------------------------------------------------------------
   IMAGE SLOTS — every editorial plate falls back to a rendered
   candle-glow field. Drop a photograph in by setting --img:

     .plate--hero       { --img: url("/assets/img/hero.webp"); }
     .plate--experience { --img: url("/assets/img/experience.webp"); }
     .plate--story-1    — already wired to the supplied founder portrait
     .card__plate      { --img: url(...) per product, inline }
     .mood i           { --img: url(...) per mood tile, inline }

   Nothing else changes when the photography arrives.
   ============================================================ */

/* ---------- 1. TOKENS ---------- */
:root {
  /* Grounds — warm near-blacks. Never neutral black; the suite is lamp-lit */
  --ink:       #14100D;   /* deepest ground: hero, candle care, contact, footer */
  --espresso:  #1E1813;   /* secondary ground: featured, collection */
  --umber:     #2B221B;   /* lifted ground: ritual, insets, cards */
  --umber-hi:  #362A21;   /* hover / raised surface */

  /* The exhale — one light movement, for long-form reading */
  --cream:     #F6F1EA;   /* client brand colour — nav, buttons, text on dark */

  /* The Our Story ground only. Two variants are kept so the choice flips in
     one line. Body ink deepens with the ground: cocoa #6B5B52 falls to 4.35:1
     on the moodier one and stops clearing AA, so it cannot simply be reused. */
  --story-ground: #F3E6CC;   /* AMBER  — warm and bright */
  --story-ink:    #6B5B52;   /* 5.24:1 on amber */
  /* MOODIER alternative — swap both lines together, never one:
     --story-ground: #E2D2B4;
     --story-ink:    #54463E;    6.08:1  */
  --linen:     #EFE8DE;
  --charcoal:  #2B2B2B;   /* client brand colour — text on cream */
  --cocoa:     #6B5B52;   /* client brand colour — body on cream ONLY */

  /* Ink on dark */
  --taupe:     #B8A89A;   /* client brand colour — 8.21:1 on ink */
  --sage:      #A8B2A1;   /* client brand colour — 8.60:1 on ink */

  /* Gold — a material AND, on this ground only, a legal text colour.
     #B08C50 measures 6.05:1 on --ink, 5.62:1 on --espresso, 4.98:1 on --umber.
     This is what makes Q31 "gold throughout" possible without breaking WCAG.
     It still FAILS on --cream (2.78:1) — hairlines only there. */
  --gold:      #B08C50;   /* sampled from the logo. Never brighten toward #FFD700 */
  --gold-hi:   #D4B57C;   /* specular highlight only — the gloss (Q32) */
  --gold-lo:   #7A6038;   /* rule shadow, gives metal its edge */

  /* Per-product accents — dark-ground variants of the approved five.
     Same hues, lifted in value so each clears 4.5:1 on ink.
     Originals (#A8837C etc.) are cream-ground values; Santal Noir's
     #4A3C34 measures 1.79:1 on ink and must never be used here. */
  --a-hibiscus: #C9A49B;  /* 8.35:1 */
  --a-golden:   #C7A365;  /* 7.98:1 */
  --a-quiet:    #BDAC97;  /* 8.57:1 */
  --a-santal:   #9C8271;  /* 5.27:1 */
  --a-matcha:   #9AAE92;  /* 7.97:1 */

  /* ---------- TYPE ----------
     The families are unchanged and deliberately so: the supplied logo IS a
     Didone wordmark over a light letterspaced geometric sans, so Bodoni Moda +
     Jost is the mark's own type, not a guess. Swapping either would put the
     page in a different voice from the logo sitting above it.

     What was wrong was the setting, not the fonts. An audit of the rendered
     page found 40% of all text set in letterspaced uppercase across ten
     different tracking values, 80 elements at 11px, and weight 300 on nearly
     everything. That combination — tiny wide caps everywhere, uniformly thin —
     is the house style of generated "luxury" layouts, and it reads as a
     template no matter which fonts carry it. The tokens below replace the
     ad-hoc values with a system. */
  --display: "Bodoni Moda", "Didot", "Times New Roman", serif;
  --sans:    "Jost", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Modular scale, 1.25 — seven steps instead of fifteen arbitrary sizes */
  --t-xs:   0.78rem;    /* 12.5px — the floor. Nothing sets smaller than this */
  --t-sm:   0.875rem;
  --t-base: 1.0625rem;
  --t-md:   1.25rem;
  --t-lg:   clamp(1.5rem, 2.2vw, 1.875rem);
  --t-xl:   clamp(1.9rem, 3.4vw, 2.6rem);
  --t-2xl:  clamp(2.1rem, 5.2vw, 4.25rem);
  --t-3xl:  clamp(2.9rem, 8.4vw, 7.5rem);

  /* Three tracking values. Only three. Every letterspaced thing picks one. */
  --track-eyebrow: .26em;   /* section eyebrows and the logo's own dot band */
  --track-caps:    .16em;   /* every other uppercase label, buttons included */
  --track-display: -.02em;  /* Bodoni at size — Didones want to close up */

  /* Motion — lofi jazz tempo (Q5). Nothing hurries, nothing overshoots */
  --dur-reveal: 900ms;
  --dur-mid:    420ms;
  --dur-micro:  220ms;
  --ease-out:   cubic-bezier(0.22, 1, 0.36, 1);
  --rise:       28px;
  --stagger:    90ms;

  /* Layout */
  --container: 1280px;
  --narrow:    68ch;
  --nav-h:     72px;

  --z-nav: 10; --z-menu: 20; --z-drawer: 30; --z-cart: 40; --z-modal: 50;

  /* Film grain — keeps large dark fields from banding, and reads as photography */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.34'/%3E%3C/svg%3E");
}
@media (min-width: 1024px) { :root { --nav-h: 88px; } }

/* On a phone the secondary step carries real reading content — the acronym
   notes, the spec lines, the footer — so it comes up to 16px rather than
   sitting at the desktop 14. MASTER §3: body text never below 16px on mobile.
   --t-xs is left alone; it only ever sets uppercase labels, never prose. */
@media (max-width: 767px) { :root { --t-sm: 1rem; } }

/* ---------- 2. RESET ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 28px);
}
body {
  background: var(--ink);
  color: var(--cream);
  font-family: var(--sans);
  /* 400, not 300. Light type on a dark ground optically thins — a weight that
     looks refined on white looks underexposed and cheap on near-black. The
     thin weight is kept for large lede text only, where there is enough mass
     for it to read as restraint rather than as a rendering problem. */
  font-weight: 400;
  font-size: var(--t-base);
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img, svg, video { display: block; max-width: 100%; height: auto; }

/* Page lock for the bag, the product modal and the mobile menu. --sbw is the
   width the scrollbar was occupying; reserving it stops the page shifting
   sideways — and reflowing to a different scroll position — the moment an
   overlay opens. The fixed nav needs the same compensation or it jumps too. */
body.is-locked { overflow: hidden; padding-right: var(--sbw, 0px); }
body.is-locked .nav { padding-right: var(--sbw, 0px); }
button, input, textarea, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
ul, ol { list-style: none; padding: 0; }
a { color: inherit; text-decoration: none; }

/* ---------- 3. ACCESSIBILITY ---------- */
:focus-visible { outline: 2px solid var(--gold-hi); outline-offset: 4px; border-radius: 1px; }
.on-cream :focus-visible { outline-color: var(--charcoal); }
.skip-link {
  position: absolute; left: 8px; top: -120px; z-index: 100;
  background: var(--cream); color: var(--ink); padding: 12px 20px;
  font-size: var(--t-xs); letter-spacing: var(--track-caps); text-transform: uppercase; font-weight: 500;
  transition: top var(--dur-micro) var(--ease-out);
}
.skip-link:focus { top: 8px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---------- 4. TYPOGRAPHY ---------- */

/* THE "fi" FIX — read this before changing it.
   Bodoni Moda is a Didone: the italic 'f' carries a ball terminal that lands at
   roughly the same height as a following dotted 'i'. Unligatured, "fine" and
   "profitable" render as though the i has two dots. The font ships a proper fi
   ligature that replaces the pair with a single glyph; browsers only apply it
   reliably when it is asked for explicitly, so it is requested here for every
   piece of display type. Removing this brings the double dot straight back. */
h1, h2, h3, h4, .quote, .acr__words, .acr__letter, .card__name,
.citem__name, .dropcap, .scripture p, blockquote.pull, .menu a {
  font-variant-ligatures: common-ligatures contextual;
  font-feature-settings: "liga" 1, "clig" 1, "calt" 1;
}

h1, h2, h3, h4 {
  font-family: var(--display); font-weight: 400;
  line-height: 1.04; letter-spacing: var(--track-display);
  /* Bodoni is an optical-size family: the display cut has the hairline thins a
     Didone is prized for, the text cut thickens them so they survive at small
     sizes. Letting the browser pick per size is most of what makes real
     Didone typesetting look expensive rather than spindly. */
  font-optical-sizing: auto;
}
h1 { font-size: var(--t-3xl); line-height: .95; letter-spacing: -.03em; }
h2 { font-size: var(--t-2xl); line-height: 1.0; }
h3 { font-size: var(--t-xl); }
h4 { font-size: var(--t-lg); }

/* THE CAPS RULE.
   One class, one job: a single short eyebrow at the top of a section. This is
   the only place the widest tracking is allowed, because it echoes the logo's
   own tagline band. Everything else that used to shout in 11px caps now reads
   in sentence case. */
.eyebrow {
  font-family: var(--sans); font-weight: 500;
  font-size: var(--t-xs); text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
  color: var(--gold); line-height: 1.7;
}
.on-cream .eyebrow { color: var(--story-ink); }

/* Small uppercase labels — field labels, note keys, footer column heads.
   Tracked once, at one size, so they finally look like one family of labels. */
.label {
  font-family: var(--sans); font-weight: 500;
  font-size: var(--t-xs); text-transform: uppercase;
  letter-spacing: var(--track-caps); line-height: 1.6;
}

.lede {
  font-size: var(--t-md); font-weight: 300; line-height: 1.6;
  color: var(--taupe); max-width: var(--narrow);
}
.body  { color: var(--taupe); max-width: var(--narrow); }
.on-cream .lede, .on-cream .body { color: var(--story-ink); }

.quote {
  font-family: var(--display); font-style: italic; font-weight: 400;
  font-size: var(--t-xl); line-height: 1.28; letter-spacing: -.015em;
}

/* Gold dot separators — the device from the logo's tagline band. The dots do
   the work; the words no longer have to shout in caps to earn their place.
   Set in sentence case at reading size, this became a line you can actually
   read at a glance instead of a decorative band you skip. */
.dotline {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .4rem 1rem;
  font-size: var(--t-sm); letter-spacing: 0; line-height: 1.6;
  color: var(--taupe);
}
/* The dot belongs to the item BEFORE it, not the one after. As a ::before it
   travelled with the following phrase, so a wrapped line began with a floating
   dot and the whole thing read as a bullet list. Trailing it instead keeps the
   line break clean. */
.dotline li { display: flex; align-items: center; gap: .55rem 1rem; }
.dotline li:not(:last-child)::after {
  content: ""; width: 3px; height: 3px; border-radius: 50%;
  background: var(--gold); flex: none; margin-left: 1rem;
}
.on-cream .dotline { color: var(--story-ink); }

/* ---------- 5. THE GOLD HAIRLINE (the site's one ornament) ---------- */
.rule {
  height: 1px; border: 0; width: 100%;
  background: linear-gradient(90deg,
    transparent, var(--gold-lo) 12%, var(--gold) 45%, var(--gold-hi) 50%,
    var(--gold) 55%, var(--gold-lo) 88%, transparent);
  opacity: .85;
}
.rule--short { width: 64px; background: linear-gradient(90deg, var(--gold-hi), var(--gold-lo)); opacity: 1; }
.on-cream .rule { opacity: .55; }

/* Centred hairline with a single dot — marks the Story/Values/Proof transitions */
.rule-dot { display: flex; align-items: center; gap: 1.25rem; }
.rule-dot::before, .rule-dot::after { content: ""; flex: 1; height: 1px; background: var(--gold); opacity: .45; }
.rule-dot span { width: 4px; height: 4px; border-radius: 50%; background: var(--gold); flex: none; }

/* ---------- 6. LAYOUT ---------- */
.wrap { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: 20px; }
@media (min-width: 768px)  { .wrap { padding-inline: 32px; } }
@media (min-width: 1024px) { .wrap { padding-inline: 48px; } }

section { position: relative; }
.pad    { padding-block: clamp(5rem, 11vw, 9rem); }
.pad-lg { padding-block: clamp(6.5rem, 14vw, 11rem); }

.ground-ink      { background: var(--ink); }
.ground-espresso { background: var(--espresso); }
.ground-umber    { background: var(--umber); }
.ground-cream    { background: var(--story-ground); color: var(--charcoal); }

/* Grain over every dark field.
   .hero-scroll.is-scrub is in this list because the featured band is lifted up
   under the pinned stage, and the strip of hero left showing above it during
   the hand-off has to carry the same texture — otherwise the overlap boundary
   reads as a hairline across a flat field. Same reason it is in the ambient
   light list below. Both only apply while the scrub is live. */
.ground-ink::before, .ground-espresso::before, .ground-umber::before,
.hero-scroll.is-scrub::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image: var(--grain); mix-blend-mode: overlay; opacity: .32;
}

/* ---------- AMBIENT CANDLE-LIGHT ----------
   One warm pool of light that follows the visitor down the whole page instead
   of living only in the hero. --gx/--gy are written by JS from scroll position
   (every device) and from the pointer where there is one, so it works on a
   phone with no cursor.

   `background-attachment: fixed` is what makes it continuous: the gradient is
   painted relative to the VIEWPORT, not to each section, so the light appears
   to stay with the reader as sections scroll past it rather than restarting at
   every section boundary.

   It sits on the section's ::after — above the ground colour, below the text
   (content is z-index 1) — so it can never reduce contrast on type.

   Colour is real candlelight, roughly 1900K: rgb(255,166,87). Warmer and
   weaker than the amber-gold it replaces. */
.ground-ink::after, .ground-espresso::after, .ground-umber::after,
.hero-scroll.is-scrub::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background:
    radial-gradient(46vmax 34vmax at var(--gx, 62%) var(--gy, 45%),
      rgba(255, 166, 87, .085) 0%,
      rgba(255, 146, 66, .045) 38%,
      transparent 70%) fixed,
    radial-gradient(22vmax 18vmax at var(--gx, 62%) var(--gy, 45%),
      rgba(255, 198, 140, .05) 0%, transparent 65%) fixed;
}
/* On the one light section the same light would read as a stain, so it is a
   whisper — just enough that the warmth does not stop at the section edge. */
.ground-cream { position: relative; }
.ground-cream::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: radial-gradient(46vmax 34vmax at var(--gx, 62%) var(--gy, 45%),
    rgba(214, 154, 74, .05) 0%, transparent 68%) fixed;
}
@media (prefers-reduced-motion: reduce) {
  /* The glow still exists, it just stops chasing anyone */
  :root { --gx: 62% !important; --gy: 45% !important; }
}
.ground-ink > *, .ground-espresso > *, .ground-umber > *, .ground-cream > * { position: relative; z-index: 1; }

/* Asymmetric editorial splits — never centred symmetry */
.split { display: grid; gap: clamp(2.5rem, 6vw, 5rem); align-items: center; }
@media (min-width: 900px) {
  .split--7-5 { grid-template-columns: 7fr 5fr; }
  .split--5-7 { grid-template-columns: 5fr 7fr; }
  .split--8-4 { grid-template-columns: 8fr 4fr; }
}

/* ---------- 7. THE PLATE — editorial image slot with a rendered fallback ----------
   With --img set it is a photograph. Without one it renders a lamp-lit field:
   a warm glow rising off a dark ground, grained and vignetted. Intentional,
   never a grey box. */
.plate {
  display: block; position: relative; overflow: hidden;
  background-color: var(--ink);
  aspect-ratio: 4 / 5;
  --glow-x: 50%; --glow-y: 72%;
  --glow-1: rgba(214,171,106,.30);
  --glow-2: rgba(150,105,55,.16);
}
.plate::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(58% 44% at var(--glow-x) var(--glow-y), var(--glow-1) 0%, transparent 68%),
    radial-gradient(120% 90% at 50% 118%, var(--glow-2) 0%, transparent 62%),
    linear-gradient(180deg, rgba(0,0,0,.55) 0%, transparent 42%, rgba(0,0,0,.45) 100%),
    var(--img, none) center / cover no-repeat;
}
.plate::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--grain); opacity: .4; mix-blend-mode: overlay;
}
.plate--wide { aspect-ratio: 16 / 10; }
.plate--tall { aspect-ratio: 3 / 4; }
.plate--bleed { aspect-ratio: 21 / 9; }
@media (max-width: 767px) { .plate--bleed { aspect-ratio: 4 / 3; } }

/* --- A plate holding a real photograph ---
   The glow gradients exist to make an empty plate look lit. Over an actual
   photo they read as haze, so this modifier strips them back to a light
   vignette and lets the image carry itself. */
.plate--photo::before {
  background:
    radial-gradient(120% 85% at 50% 45%, transparent 55%, rgba(0,0,0,.28) 100%),
    var(--img, none) center / cover no-repeat;
}
.plate--photo::after { opacity: .16; }

/* --- PRODUCT PHOTOGRAPHY ---
   Client-supplied, one shot per candle, all 3:4 portrait — which is why the
   card and the modal hero both use .plate--tall: the frame matches the file,
   so `cover` never crops the vessel or the label out of shot.
   Pair each with .plate--photo so the rendered glow steps aside. */
/* IMAGE FILENAME CONVENTION — keep this for every photograph added later.
       normil-<candle>-<scent>-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg
   Brand, then the specific product, then the scent, then the category terms.
   The product-specific half leads because a filename describes THAT image;
   five files sharing one generic name tell a search engine nothing about which
   candle it is looking at. Lowercase, hyphens, no underscores, no numbering.

   These URLs are permanent. A filename is an address — changing one after it
   is indexed throws away whatever ranking it earned, so replacements reuse the
   name and rely on the shorter cache window set in netlify.toml. */
.p-hibiscus-veil { --img: url("/assets/img/products/normil-hibiscus-veil-lily-hibiscus-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg"); }
.p-quiet-morning { --img: url("/assets/img/products/normil-quiet-mornings-lavender-apricot-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg"); }
.p-golden-sage   { --img: url("/assets/img/products/normil-golden-sage-pineapple-sage-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg"); }
.p-santal-noir   { --img: url("/assets/img/products/normil-santal-noir-sandalwood-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg"); }
.p-matcha-muse   { --img: url("/assets/img/products/normil-matcha-muse-matcha-lemongrass-honey-luxury-non-toxic-candle-smells-like-perfume-coconut-soy-wooden-wick.jpg"); }

/* These are lit, styled shots on their own warm grounds — they do not need the
   heavy top-and-bottom scrim the empty plates used to build depth. A light
   vignette only, so the card still sits into the dark page. */
.plate--product::before {
  background:
    radial-gradient(115% 80% at 50% 45%, transparent 58%, rgba(0,0,0,.30) 100%),
    var(--img, none) center / cover no-repeat;
}
.plate--product::after { opacity: .12; }

/* The founder portrait. Shot 2:3, so the frame is 2:3 — the whole photograph is
   visible, never cropped. Capped against the viewport so it lands complete on
   screen at any size rather than running past the fold and forcing a scroll to
   see the rest of it. */
.plate--story-1 {
  --img: url("/assets/img/normil-founder-house-of-normil-luxury-non-toxic-candle-hand-poured-florida.jpg");
  /* The frame matches the photograph's own 1409x2000, so cover-fill crops
     nothing. Change this ratio if the portrait is ever reshot. */
  aspect-ratio: 1409 / 2000;
  max-height: 74svh;
  width: auto; max-width: 100%;
  margin-inline: auto;
}
@media (max-width: 899px) {
  .plate--story-1 { max-height: 62svh; }
}

/* The vessel — a single rendered object on plain ground, per the restraint doctrine.
   Replaced wholesale the moment product photography lands. */
.vessel {
  position: absolute; left: 50%; bottom: 14%; transform: translateX(-50%);
  width: min(38%, 190px); aspect-ratio: 4 / 5; z-index: 1;
  border-radius: 4px 4px 10px 10px;
  background:
    linear-gradient(100deg,
      rgba(255,255,255,.02) 0%, rgba(255,255,255,.16) 18%, rgba(255,255,255,.03) 34%,
      transparent 55%, rgba(0,0,0,.30) 100%),
    linear-gradient(180deg, var(--vessel, #3A2E25) 0%, #1C1611 100%);
  box-shadow: 0 40px 60px -30px rgba(0,0,0,.9), inset 0 1px 0 rgba(255,255,255,.10);
}
.vessel::before { /* flame */
  content: ""; position: absolute; left: 50%; top: -13%;
  width: 7px; height: 15px; transform: translateX(-50%);
  border-radius: 50% 50% 46% 46%;
  background: radial-gradient(60% 60% at 50% 68%, #FFE9BD 0%, var(--gold-hi) 45%, rgba(176,140,80,0) 100%);
  filter: blur(.4px);
  box-shadow: 0 0 26px 10px rgba(212,181,124,.30), 0 0 60px 26px rgba(176,140,80,.16);
}
.vessel::after { /* gold band — the gloss */
  content: ""; position: absolute; left: 0; right: 0; top: 22%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold-lo) 15%, var(--gold-hi) 50%, var(--gold-lo) 85%, transparent);
}

/* ---------- 8. NAV ---------- */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 25;   /* above the menu overlay so the close control stays reachable */
  height: var(--nav-h);
  display: flex; align-items: center;
  background: transparent;
  transition: background var(--dur-mid) var(--ease-out), border-color var(--dur-mid) var(--ease-out);
  border-bottom: 1px solid transparent;
}
.nav.is-stuck {
  background: rgba(20,16,13,.94);
  border-bottom-color: rgba(176,140,80,.28);
}
@supports (backdrop-filter: blur(4px)) {
  .nav.is-stuck { background: rgba(20,16,13,.86); backdrop-filter: blur(14px) saturate(120%); }
}
.nav__in { display: flex; align-items: center; justify-content: space-between; gap: 1rem; width: 100%; }
.nav__logo { display: flex; align-items: center; min-height: 44px; }
.nav__logo img { height: 34px; width: auto; }
@media (min-width: 1024px) { .nav__logo img { height: 42px; } }

.nav__links { display: none; gap: clamp(1.5rem, 2.6vw, 2.75rem); }
@media (min-width: 900px) { .nav__links { display: flex; } }
.nav__links a {
  font-size: var(--t-xs); text-transform: uppercase; letter-spacing: var(--track-caps); font-weight: 500;
  color: var(--cream); opacity: .82; position: relative; padding-block: .5rem;
  transition: opacity var(--dur-micro) var(--ease-out);
}
.nav__links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: var(--gold); transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-mid) var(--ease-out);
}
.nav__links a:hover { opacity: 1; }
@media (hover: hover) and (pointer: fine) { .nav__links a:hover::after { transform: scaleX(1); } }

.nav__tools { display: flex; align-items: center; gap: .25rem; }
.icon-btn {
  width: 44px; height: 44px; display: grid; place-items: center;
  color: var(--cream); position: relative;
  transition: color var(--dur-micro) var(--ease-out);
}
.icon-btn:hover { color: var(--gold-hi); }
.icon-btn svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.4; }
.cart-badge {
  position: absolute; top: 6px; right: 4px;
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: 9px;
  background: var(--gold); color: var(--ink);
  font-size: .62rem; font-weight: 500; letter-spacing: .04em;
  display: none; place-items: center;
}
.cart-badge.is-on { display: grid; }
.nav__burger { display: grid; }
/* Hamburger becomes an X while the menu is open */
#burger svg path { transition: transform var(--dur-micro) var(--ease-out); transform-origin: 12px 12px; }
#burger[aria-expanded="true"] svg path:nth-of-type(1) { transform: translateY(4px) rotate(45deg); }
#burger[aria-expanded="true"] svg path:nth-of-type(2) { transform: translateY(-4px) rotate(-45deg); }
@media (min-width: 900px) { .nav__burger { display: none; } }

/* Full-screen mobile menu.
   Everything is sized to fit one screen with no scrolling — the links take the
   space they need, the verse is pinned to the base by the auto margin. */
.menu {
  position: fixed; inset: 0; z-index: var(--z-menu);
  background: var(--ink); padding: calc(var(--nav-h) + 1.5rem) 20px 2rem;
  display: flex; flex-direction: column; gap: 0;
  overflow: hidden;
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid);
}
.menu.is-open { opacity: 1; visibility: visible; }
.menu__links { display: flex; flex-direction: column; justify-content: center; flex: 1; min-height: 0; }
.menu a {
  font-family: var(--display); font-size: clamp(1.65rem, 7.4vw, 2.6rem);
  line-height: 1.1; padding-block: .42em;
  border-bottom: 1px solid rgba(176,140,80,.2);
}
.menu a:last-of-type { border-bottom: 0; }

/* Q40 — faith stays minimal and unlabelled. A reference, not a sermon. */
.menu__verse {
  margin-top: auto; padding-top: 1.5rem; flex: none;
  display: flex; align-items: center; gap: 1rem;
  font-size: var(--t-xs); text-transform: uppercase; letter-spacing: var(--track-caps); font-weight: 500;
  color: var(--gold); opacity: .7;
}
.menu__verse::before {
  content: ""; width: 26px; height: 1px; background: var(--gold); flex: none;
}
/* Short viewports (landscape phones) — tighten rather than scroll */
@media (max-height: 620px) {
  .menu a { font-size: clamp(1.2rem, 5.4vw, 1.6rem); padding-block: .3em; }
  .menu { padding-top: calc(var(--nav-h) + .75rem); }
}

/* ---------- 9. BUTTONS ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .7rem;
  min-height: 52px; padding: 0 2.2rem;
  font-size: var(--t-sm); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps);
  border: 1px solid var(--gold); color: var(--ink);
  background: linear-gradient(180deg, var(--gold-hi) 0%, var(--gold) 52%, var(--gold-lo) 100%);
  position: relative; overflow: hidden;
  transition: color var(--dur-mid) var(--ease-out);
}
/* Specular sweep — the glossy finish (Q32).
   Runs continuously, on a long cycle, so a button reads as a button on touch
   devices where there is no hover to reveal it. This is a deliberate exception
   to MASTER §4's ban on infinite animation: client-requested, and the only
   looping motion on the site. Kept slow and low-contrast so it registers
   peripherally rather than blinking for attention. */
@keyframes sheen {
   0%  { transform: translateX(-130%); }
  22%  { transform: translateX(130%); }
 100%  { transform: translateX(130%); }   /* long rest between passes */
}
.btn::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,.38) 48%, transparent 62%);
  transform: translateX(-130%);
  animation: sheen 5s var(--ease-out) infinite;
}
/* Stagger the pass so a row of buttons never flashes in unison */
.btn:nth-of-type(even)::after { animation-delay: 1.6s; }
@media (hover: hover) and (pointer: fine) {
  .btn:hover::after { animation-duration: 2.2s; }
}

.btn--ghost {
  background: transparent; color: var(--gold); border-color: rgba(176,140,80,.5);
  transition: border-color var(--dur-mid) var(--ease-out), color var(--dur-mid) var(--ease-out);
}
.btn--ghost:hover { border-color: var(--gold); color: var(--gold-hi); }
/* Ghost buttons sit on a dark ground — a white sweep would blow out, so the
   pass is a soft gold wash at a fraction of the strength */
.btn--ghost::after {
  background: linear-gradient(105deg, transparent 32%, rgba(212,181,124,.20) 48%, transparent 64%);
}
.on-cream .btn--ghost::after {
  background: linear-gradient(105deg, transparent 32%, rgba(43,43,43,.09) 48%, transparent 64%);
}
.on-cream .btn--ghost { color: var(--charcoal); border-color: rgba(43,43,43,.35); }
.on-cream .btn--ghost:hover { border-color: var(--charcoal); color: var(--charcoal); }

/* A quiet text link. In caps it read as another label competing with the
   button beside it; in sentence case it reads as the aside it is. */
.link-quiet {
  display: inline-flex; align-items: center; gap: .7rem;
  font-size: var(--t-base); letter-spacing: 0;
  color: var(--gold); padding-block: .8rem; min-height: 44px;
}
.link-quiet::after {
  content: ""; width: 26px; height: 1px; background: currentColor;
  transition: width var(--dur-mid) var(--ease-out);
}
.link-quiet:hover::after { width: 42px; }
.on-cream .link-quiet { color: var(--charcoal); }

/* ---------- 10. REVEALS (Q27, Q28) ---------- */
.rv { opacity: 0; transform: translateY(var(--rise)); }
.rv.is-in {
  opacity: 1; transform: none;
  transition: opacity var(--dur-reveal) var(--ease-out), transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--d, 0ms);
}

/* ---------- 11. HERO ---------- */
/* The hero is a scroll-scrubbed stage and the film is its GROUND, not a panel
   sitting inside it. At rest the footage is blurred and dimmed behind the
   words; the first touch of scroll lifts the blur, brings the brightness back
   and clears the copy, so the clip reveals itself. The last stretch of the pin
   dissolves the whole stage into the ground colour of the section below.

   Every value JS animates is declared here first, on .hero, as a resting
   value. With no JS, under reduced motion, or on a failed load, these ARE the
   hero — nothing below depends on the script having run.

   `overflow: hidden` must NOT go back on .hero. It used to live here, to
   contain the glow, and an overflow:hidden ANCESTOR silently makes
   position:sticky do nothing — the symptom reads as a broken video rather than
   as the CSS bug it is. It lives on .hero__stage. */
.hero {
  position: relative;
  background: var(--ink);
  overflow: visible;

  --hero-blur:     18px;   /* how far out of focus the film sits */
  --hero-bright:   .66;    /* and how far down — the two read as one veil */
  --hero-sat:      .86;
  --hero-zoom:     1.10;
  --hero-scrim:    1;      /* strength of the wash under the words */
  --hero-copy:     1;
  --hero-dissolve: 0;
  --hero-handoff:  1;      /* 1 = the stage is the picture, 0 = handed over */
}
.hero__stage {
  position: relative; overflow: hidden;
  display: flex; align-items: center;
  padding-top: calc(var(--nav-h) + clamp(1.25rem, 3.5vh, 2.5rem));
  padding-bottom: clamp(2.25rem, 6vh, 4rem);
}
/* The tall section and the pin only appear once JS has confirmed the clip has
   a duration worth scrubbing. Until then this is an ordinary hero. */
/* The ground turns espresso for the pin because the featured band is pulled up
   underneath the stage and the strip of hero still showing above it has to be
   the SAME colour — see the hand-off further down. Nothing else is affected:
   while the stage is pinned it covers the viewport, so this colour is only
   ever seen during the hand-off. */
.hero-scroll.is-scrub { height: 280vh; background: var(--espresso); }
.hero-scroll.is-scrub .hero__stage {
  position: sticky; top: 0; min-height: 100svh;
  /* Explicit, and load-bearing. The stage and the section below are both
     position:relative with z-index auto, which means DOCUMENT ORDER decides
     which paints on top — and document order puts the featured band above the
     film. Once the band overlaps the stage that is visible as the next section
     painted straight over the hero. Two is enough: the band's own content sits
     at z-index 1 and the page's chrome starts at --z-nav: 10. */
  z-index: 2;
  opacity: var(--hero-handoff);
}

/* ----- the film, full bleed ----- */
.hero__media {
  position: absolute; inset: 0; z-index: 0; margin: 0;
  background: var(--ink);
  filter: blur(var(--hero-blur)) brightness(var(--hero-bright)) saturate(var(--hero-sat));
  /* The zoom is not decoration. A blur samples past the edges of its own box
     and leaves a soft transparent rim; scaling up pushes that rim off screen.
     It shrinks back to 1 exactly as the blur reaches 0. */
  transform: scale(var(--hero-zoom));
}
.hero__media video, .hero__media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
/* The still carries the hero until the scrub takes over, then steps aside */
.hero__media video.hero-video { display: none; }
.is-scrub .hero__media video.hero-video { display: block; }
.is-scrub .hero__media img.hero-still { visibility: hidden; }

/* Once the veil has fully lifted the filter is removed outright rather than
   left running at blur(0) brightness(1). A filter that resolves to a no-op is
   still a per-frame compositing pass over a full-screen video, and this is the
   stretch where the scrub is doing the most work. */
.hero-scroll.is-clear .hero__media { filter: none; transform: none; }

/* ----- the scrim -----
   What makes the words legible over moving footage. It follows the COPY, not
   the blur: full strength while there is something to read, lifting as the
   words leave, so clear footage is never left sitting under a wash that is no
   longer protecting anything. */
.hero__scrim {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  opacity: var(--hero-scrim);
  /* Narrow screens are the hard case: the words run edge to edge, straight
     across the middle of the frame, which is exactly where the vessel is.
     Washing the whole stage dark enough for 12px gold to sit on it put the
     film out altogether. So this layer stays moderate and the protection the
     text needs is a pool that belongs to the copy — see .hero__copy::before
     below. This one only has to keep the nav legible and hold the edges. */
  background:
    linear-gradient(to bottom,
      rgba(20,16,13,.88) 0%, rgba(20,16,13,.62) 26%,
      rgba(20,16,13,.58) 66%, rgba(20,16,13,.70) 100%),
    radial-gradient(135% 105% at 50% 48%, rgba(20,16,13,0) 24%, rgba(20,16,13,.46) 100%);
}
@media (min-width: 900px) {
  /* On a wide screen the words hold the left and the vessel is centre frame,
     so the wash turns directional — heavy under the type, gone by the far
     side, where the footage is meant to be seen. */
  .hero__scrim {
    background:
      linear-gradient(97deg,
        rgba(20,16,13,.94) 0%, rgba(20,16,13,.89) 30%, rgba(20,16,13,.64) 52%,
        rgba(20,16,13,.24) 74%, rgba(20,16,13,0) 100%),
      linear-gradient(to bottom, rgba(20,16,13,.64) 0%, rgba(20,16,13,0) 34%),
      radial-gradient(130% 110% at 62% 50%, rgba(20,16,13,0) 36%, rgba(20,16,13,.54) 100%);
  }
}

/* The pool under the words, narrow screens only.
   A wash strong enough for the eyebrow — gold at 12px, the least forgiving
   pair on the page — has to be near-solid when it is spread over the whole
   stage, and then there is no film left to see. Carried by the copy instead,
   the same protection covers a fifth of the frame and the rest of it stays
   open. It is a soft pool rather than a panel: no edge anywhere, so it reads
   as light falling off and not as a box behind the type.

   It lives INSIDE .hero__copy, so the opacity that clears the words clears
   this with them — the pool can never outlast what it was protecting. */
@media (max-width: 899px) {
  .hero__copy { position: relative; isolation: isolate; }
  .hero__copy::before {
    content: ""; position: absolute; z-index: -1; pointer-events: none;
    /* It bleeds past both side edges, so the only edges that could show are
       the top and the bottom — and both ends of the gradient are transparent,
       so neither does. The strong part covers the eyebrow and the headline,
       which is where the vessel is brightest and the type is smallest. */
    inset: -6rem -24px -2rem -24px;
    background:
      linear-gradient(to bottom,
        rgba(20,16,13,0) 0%, rgba(20,16,13,.84) 12%,
        rgba(20,16,13,.80) 54%, rgba(20,16,13,.50) 84%,
        rgba(20,16,13,0) 100%);
  }
}

/* ----- the dissolve -----
   The ground of the section below, laid over the whole picture — copy included,
   because this is the picture dissolving and not a tint on it.

   It no longer goes all the way to opaque. Taking it to 1 and THEN clearing the
   stage leaves a moment of flat colour between the two, which is the thing the
   hand-off below exists to remove; it stops around half way, far enough to take
   the brightness out of the clip's last frame, and the stage clearing the rest
   of the way is what completes the dissolve. The ceiling lives in suite.js as
   DISSOLVE_MAX. */
.hero__dissolve {
  position: absolute; inset: 0; z-index: 5; pointer-events: none;
  background: var(--espresso);
  opacity: var(--hero-dissolve);
}

/* ----- the hand-off -----
   WHY THE SECTION BELOW IS PULLED UP INTO THE HERO.

   A pinned hero hands over badly by default, and the reason is geometric, not
   a matter of easing. At the moment the pin releases, the stage fills the
   viewport and the section below begins exactly AT the fold — its first line
   of type another ~185px past that, behind its own top padding. So the
   dissolve lands on a screen of flat colour and sits there for the best part
   of two hundred pixels of scroll before any of the next section arrives.
   Measured on the live site before this change: pin released at y=1613, first
   glyph on screen at y=1798.

   No amount of fading fixes that, because there is nothing on screen to fade
   TO. The section has to be brought up underneath the stage BEFORE the
   dissolve ends, and then revealed by fading the stage out — not by waiting
   for it to scroll away.

   So the band is lifted into the hero's last stretch, where the stage hides it
   completely (the stage is opaque for the whole pin: the film first, then the
   dissolve over it). --hero-handoff then takes the stage to zero across the
   last few per cent of the pin. Because the dissolve is already the exact
   ground colour of the band behind it, the only thing that visibly changes is
   that the type arrives: film, then colour, then words, with no frame of empty
   colour between them.

   The lift is a fraction of the viewport rather than a pixel count on purpose.
   It has to land the heading at about the same height on a phone as on a
   desktop, and the hero's own height is in vh too, so the two stay in step.
   It shortens the page by exactly the overlap, which is the intent. */
.hero-scroll.is-scrub + section { margin-top: -68vh; }
/* A touch more on a narrow screen, because the heading wraps to two lines
   there and its own block is shorter: the same lift would land it at 61% of
   the screen against 55% on a desktop. Measured, not guessed. */
@media (max-width: 899px) {
  .hero-scroll.is-scrub + section { margin-top: -62vh; }
}

/* Invisible is not inert. At opacity 0 the stage still covers the viewport and
   still sits above the band underneath it, so it would swallow every click
   meant for a product card. Set for the whole hand-off rather than only at the
   end, because by then there is nothing in the stage to click: the two CTA
   links went to visibility:hidden with the copy, long before this. */
.hero-scroll.is-handing .hero__stage { pointer-events: none; }

/* A hairline that fills as the clip advances — the only chrome, and it goes
   once the clip is done. */
.hero-progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px; z-index: 3;
  background: rgba(255,255,255,.10);
  opacity: 0; transition: opacity var(--dur-mid) var(--ease-out);
}
.is-scrub .hero-progress { opacity: 1; }
.is-scrub.is-done .hero-progress { opacity: 0; }
.hero-progress > span {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi));
}

/* Copy clears as the clip starts moving and returns on the way back up */
.hero-scroll.is-scrub .hero__copy {
  opacity: var(--hero-copy);
  transform: translateY(calc((1 - var(--hero-copy)) * -14px));
  will-change: opacity, transform;
}
/* At zero opacity the two links in the CTA are still clickable and still in
   the tab order — invisible controls floating over the film. visibility takes
   them out of both, and is applied only once they are already fully
   transparent, so nothing is ever seen disappearing. */
.hero-scroll.is-copy-out .hero__copy { visibility: hidden; pointer-events: none; }

/* The glow's parallax is written as an inline transform by the ambient-light
   code, which assumes an ordinary hero scrolling past. Against a stage that
   pins for nearly three screens it would drift the light clean out of frame.
   A stylesheet !important beats an inline style that has none. */
.hero-scroll.is-scrub .hero__bg { transform: none !important; }

@media (prefers-reduced-motion: reduce) {
  .hero-scroll.is-scrub { height: auto; background: var(--ink); }
  .hero-scroll.is-scrub .hero__stage {
    position: static; min-height: 0;
    z-index: auto; opacity: 1;
  }
  .hero-scroll.is-scrub .hero__copy { opacity: 1; transform: none; }
  /* No pin means no hand-off to cover, so the band keeps its own top margin
     and the hero keeps its own ground. */
  .hero-scroll.is-scrub + section { margin-top: 0; }
  .hero-scroll.is-scrub::before, .hero-scroll.is-scrub::after { content: none; }
  /* No scroll means no reveal, so the resting veil is eased off: enough blur
     to hold the words, little enough that the candle is still a candle. */
  .hero { --hero-blur: 7px; --hero-bright: .62; --hero-sat: .92; --hero-zoom: 1.04; }
}

.hero__bg {
  position: absolute; inset: -10% 0 -10%; z-index: 1; will-change: transform;
  /* Set by JS from pointer / touch position; these are the resting values */
  --mx: 68%; --my: 42%;
}
/* The two warm pools are a fraction of what they were. They were painted to
   make an empty dark hero feel lit — there is a candle being poured back there
   now, and a drawn glow on top of a filmed one reads as a filter over the
   footage rather than as light in the room. They are kept, weak, so the warmth
   does not stop dead at the edges of the frame. The black wash at the top is
   untouched: it is what the nav is legible against. */
.hero__bg::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(44% 36% at var(--gx, 62%) var(--gy, 45%), rgba(255,166,87,.08) 0%, transparent 68%),
    radial-gradient(26% 20% at var(--gx, 62%) var(--gy, 45%), rgba(255,205,150,.04) 0%, transparent 62%),
    radial-gradient(70% 60% at 24% 88%, rgba(120,84,44,.07) 0%, transparent 66%),
    radial-gradient(100% 100% at 50% 0%, rgba(0,0,0,.6) 0%, transparent 55%);
}
.hero__bg::after {
  content: ""; position: absolute; inset: 0;
  background-image: var(--grain); opacity: .4; mix-blend-mode: overlay;
}
.hero .wrap { position: relative; z-index: 3; }
.hero__grid { display: grid; width: 100%; }
/* The copy is capped so it stays inside the heavy end of the directional
   scrim and leaves the centre of the frame — where the vessel is — clear. */
@media (min-width: 900px) { .hero__grid { grid-template-columns: minmax(0, 54%); } }
.hero h1 { max-width: 15ch; }
.hero h1 em { font-style: italic; color: var(--gold-hi); }
.hero__sub { margin-top: 1.75rem; max-width: 46ch; color: var(--taupe); }
.hero__cta { margin-top: 2.5rem; display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; align-items: center; }
/* The hero eyebrow is the only type on the page that changed colour when the
   film went behind the words, and it had to. --gold is 6.05:1 on flat ink, but
   at 12px over a lit, blurred candle it measured 4.4:1 — under the 4.5 it
   needs — and no amount of extra wash moved it, because the wash and the
   brightest part of the footage were already fighting each other. --gold-hi
   measures 7.1:1 on the same ground.

   It is not a new colour on this page: the headline's "fine perfume" is
   already set in it, two lines below. Reading as a lighter gold where the
   light is actually falling is the point rather than the compromise. */
.hero__eyebrow { margin-bottom: 1.75rem; color: var(--gold-hi); }
/* The "Scroll" cue is gone. It existed to tell the visitor there was more below
   a full-height hero; the progress hairline now says the same thing while it
   is the scroll that is actually doing something. */

/* ---------- 12. SECTION HEAD ---------- */
.shead { display: grid; gap: 1.25rem; margin-bottom: clamp(3rem, 6vw, 4.5rem); max-width: 62ch; }
.shead .rule--short { margin-bottom: .25rem; }

/* ---------- 13. THE ACRONYM ---------- */
.acr { display: grid; gap: 0; }
.acr__row {
  display: grid; gap: .35rem 2.5rem; align-items: baseline;
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  border-top: 1px solid rgba(176,140,80,.22);
}
.acr__row:last-child { border-bottom: 1px solid rgba(176,140,80,.22); }
/* Wider letter column: "N O" was overflowing a 4.5rem track and shoving the
   phrase off its own baseline. Notes are pulled in from the far right so the
   eye travels a short distance instead of the full container width. */
@media (min-width: 768px)  { .acr__row { grid-template-columns: 7rem minmax(0,1fr) minmax(0,34ch); gap: .35rem 3rem; align-items: baseline; } }
@media (min-width: 1200px) { .acr__row { grid-template-columns: 8rem minmax(0,1fr) minmax(0,38ch); } }
.acr__letter {
  font-family: var(--display); font-size: clamp(2.2rem, 4vw, 3rem);
  color: var(--gold); line-height: 1; letter-spacing: .02em;
}
/* The phrase moves OUT of Bodoni and into Jost Light.
   Bodoni is a Didone: its thin strokes are hairlines, and at this middling size
   they break up on low-density screens and in bright daylight — which is most
   phones, most of the time. A light geometric sans holds an even stroke at any
   density, and set large with open spacing it reads every bit as expensive
   while staying legible on every device. The gold Bodoni initials stay, so the
   brand's Didone voice still leads the line and the contrast between the two
   does the decorative work the serif used to do alone. */
.acr__words {
  font-family: var(--sans); font-weight: 300;
  font-size: clamp(1.5rem, 2.8vw, 2.15rem);
  letter-spacing: .01em; line-height: 1.25; color: var(--cream);
}
.acr__note { color: var(--taupe); font-size: var(--t-base); line-height: 1.7; max-width: 40ch; }

/* ---------- 14. PRODUCT CARDS ---------- */
.grid-products {
  display: grid; gap: 3rem 2rem;
  grid-template-columns: 1fr;
}
@media (min-width: 640px)  { .grid-products { grid-template-columns: repeat(2, 1fr); gap: 4rem 2rem; } }
@media (min-width: 1024px) { .grid-products { grid-template-columns: repeat(3, 1fr); gap: 5.5rem 2.5rem; } }
/* Editorial stagger — breaks the mechanical grid, and makes a five-product
   collection read as composition rather than an incomplete row */
@media (min-width: 1024px) { .grid-products > li:nth-child(even) { margin-top: 4rem; } }

.card { display: grid; gap: 0; text-align: left; width: 100%; }

/* The featured trio behaves exactly like a collection card: same modal, same
   copy, same buy control. The original worry was making someone open a candle
   twice — that only applied while details lived further down the page. Now the
   modal carries the purchase, so opening one here finishes the job. */

.card__plate { display: block; position: relative; overflow: hidden; }
.card__plate .plate { transition: transform var(--dur-reveal) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .card:hover .card__plate .plate { transform: scale(1.04); }
}
.card__hair {
  display: block; height: 1px; margin-top: 1.6rem; background: var(--accent, var(--gold));
  transform: scaleX(0); transform-origin: left; width: 40%;
  transition: transform 500ms var(--ease-out) 100ms;
}
@media (hover: hover) and (pointer: fine) { .card:hover .card__hair { transform: scaleX(1); } }
@media (hover: none), (pointer: coarse) { .card__hair { transform: scaleX(1); } }

.card__name { margin-top: 1.1rem; font-family: var(--display); font-size: clamp(1.55rem, 2vw, 1.9rem); }
.card__scent {
  margin-top: .55rem; font-size: var(--t-xs); font-weight: 500; text-transform: uppercase;
  letter-spacing: var(--track-caps); color: var(--accent, var(--taupe));
}
.card__price { margin-top: .7rem; color: var(--taupe); font-size: var(--t-base); }
.card__add { display: block; margin-top: 1.1rem; }
/* Hover reveal only where hover exists. On touch the purchase control is always
   visible — never hide a buy button behind a state most of this traffic cannot reach. */
@media (hover: hover) and (pointer: fine) {
  .card__add { opacity: 0; transform: translateY(8px); transition: opacity 300ms var(--ease-out), transform 300ms var(--ease-out); }
  .grid-products > li:hover .card__add,
  .grid-products > li:focus-within .card__add { opacity: 1; transform: none; }
}

/* ---------- 15. THE PRODUCT MODAL ----------
   Client direction: opening a candle should not send the visitor anywhere.
   Details arrive over the collection, fully populated, and closing returns
   them to exactly the card they tapped — no scroll hunting, no second journey.

   The content itself still lives in the page HTML at load (MASTER §6.55) so it
   stays indexable; JS *moves* the drawer node into the modal and puts it back
   on close. It is never cloned and never injected, so there is only ever one
   copy of each product's copy in the DOM. */
.drawer { display: none; }
#drawers { display: contents; }

/* `display: grid` below would otherwise beat the `hidden` attribute, which sets
   only `display: none` at user-agent weight. Without this the modal is in the
   layout from first paint. */
.pmodal[hidden] { display: none; }
.pmodal {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid; place-items: center;
  padding: clamp(.75rem, 3vw, 2.5rem);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid);
}
.pmodal.is-open { opacity: 1; visibility: visible; }
.pmodal__scrim { position: absolute; inset: 0; background: rgba(8,6,5,.82); }
@supports (backdrop-filter: blur(4px)) {
  .pmodal__scrim { backdrop-filter: blur(6px); }
}
.pmodal__panel {
  position: relative; z-index: 1;
  width: min(100%, 1020px); max-height: 92svh; overflow-y: auto;
  background: var(--espresso);
  border: 1px solid rgba(176,140,80,.32);
  padding: clamp(1.5rem, 4vw, 3.25rem);
  transform: translateY(14px) scale(.99);
  transition: transform var(--dur-mid) var(--ease-out);
  overscroll-behavior: contain;
}
.pmodal.is-open .pmodal__panel { transform: none; }
.pmodal__panel .drawer { display: block; }
.pmodal__panel .drawer__in { overflow: visible; }
.pmodal__panel .drawer__pad { margin-top: 0; padding-top: 0; border-top: 0; }

/* The close control stays reachable however far the panel is scrolled */
.pmodal__close {
  position: sticky; top: 0; z-index: 2;
  float: right; margin: -.5rem -.5rem 0 1rem;
  width: 44px; height: 44px; display: grid; place-items: center;
  color: var(--taupe); background: var(--espresso); border-radius: 50%;
  transition: color var(--dur-micro) var(--ease-out);
}
.pmodal__close:hover { color: var(--gold-hi); }
.pmodal__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.5; }

.drawer__pad { padding: 0; }
.drawer__head { display: grid; gap: .6rem; margin-bottom: 2.5rem; }
.drawer__head h3 { color: var(--cream); }
.drawer__head h3:focus { outline: none; }
.drawer__price { color: var(--taupe); }

.drawer__grid { display: grid; gap: clamp(2rem, 4vw, 3.5rem); }
@media (min-width: 900px) { .drawer__grid { grid-template-columns: 6fr 5fr; align-items: start; } }

.notes { display: grid; gap: 0; margin-block: 2rem; }
.notes div {
  display: grid; grid-template-columns: 5.5rem 1fr; gap: 1.25rem;
  padding-block: .95rem; border-top: 1px solid rgba(255,255,255,.09);
}
.notes div:last-child { border-bottom: 1px solid rgba(255,255,255,.09); }
.notes dt { font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--accent, var(--gold)); }
.notes dd { color: var(--cream); font-weight: 300; }

.qty { display: inline-flex; align-items: center; border: 1px solid rgba(255,255,255,.18); }
.qty button { width: 44px; height: 48px; display: grid; place-items: center; color: var(--taupe); }
.qty button:hover { color: var(--gold-hi); }
.qty span { min-width: 40px; text-align: center; font-size: var(--t-base); }
.buyrow { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; margin-block: 2rem; }

.acc { border-top: 1px solid rgba(255,255,255,.09); }
.acc:last-of-type { border-bottom: 1px solid rgba(255,255,255,.09); }
/* "How to use", "Candle care", "Shipping" are content headings, not labels.
   They were set as 11px wide caps, which is how you make a heading look like
   fine print. Now they read at heading size in the display face. */
.acc__btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding-block: 1.25rem; text-align: left;
  font-family: var(--display); font-size: var(--t-md); letter-spacing: -.01em;
  line-height: 1.3; color: var(--cream);
}
.acc__btn i { width: 11px; height: 11px; position: relative; flex: none; }
.acc__btn i::before, .acc__btn i::after {
  content: ""; position: absolute; inset: 50% 0 auto; height: 1px; background: var(--gold);
  transition: transform var(--dur-micro) var(--ease-out);
}
.acc__btn i::after { transform: rotate(90deg); }
.acc__btn[aria-expanded="true"] i::after { transform: rotate(0deg); }
.acc__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-micro) var(--ease-out); }
.acc__btn[aria-expanded="true"] + .acc__panel { grid-template-rows: 1fr; }
.acc__panel > div { overflow: hidden; }
.acc__panel p { padding-bottom: 1.6rem; color: var(--taupe); font-size: var(--t-base); max-width: 60ch; }

/* Mood board (Q37) — story after details (Q36) */
.mood { margin-top: clamp(2.5rem, 5vw, 4rem); }
.mood__grid { display: grid; gap: .75rem; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 768px) { .mood__grid { grid-template-columns: repeat(4, 1fr); } }
/* Four tiles, four different lighting set-ups — a mood board is a composition,
   not the same frame printed four times. Each accepts --img to become a photo. */
.mood i {
  display: block; aspect-ratio: 3/4; position: relative; overflow: hidden;
  background-color: var(--ink);
  background-image: var(--img, none);
  background-size: cover; background-position: center;
}
.mood i::before { content: ""; position: absolute; inset: 0; }
.mood i::after { content: ""; position: absolute; inset: 0; background-image: var(--grain); opacity: .38; mix-blend-mode: overlay; }

/* 1 — the object, lit low and centred */
.mood i:nth-child(1)::before {
  background:
    radial-gradient(40% 26% at 50% 74%, color-mix(in srgb, var(--accent, var(--gold)) 70%, #fff) 0%, transparent 72%),
    radial-gradient(90% 70% at 50% 100%, rgba(176,140,80,.20) 0%, transparent 60%),
    linear-gradient(180deg, rgba(0,0,0,.72), transparent 55%);
}
/* 2 — raking light from the upper left, with a horizon */
.mood i:nth-child(2)::before {
  background:
    linear-gradient(158deg, color-mix(in srgb, var(--accent, var(--gold)) 42%, transparent) 0%, transparent 46%),
    linear-gradient(180deg, transparent 56%, rgba(255,255,255,.07) 57%, transparent 58%),
    linear-gradient(180deg, rgba(0,0,0,.35), rgba(0,0,0,.65));
}
/* 3 — near dark, one small specular highlight */
.mood i:nth-child(3)::before {
  background:
    radial-gradient(18% 12% at 70% 30%, rgba(255,255,255,.34) 0%, transparent 70%),
    radial-gradient(70% 60% at 70% 34%, color-mix(in srgb, var(--accent, var(--gold)) 30%, transparent) 0%, transparent 72%),
    linear-gradient(200deg, rgba(0,0,0,.2), rgba(0,0,0,.8));
}
/* 4 — the wide open one, palest of the four */
.mood i:nth-child(4)::before {
  background:
    radial-gradient(120% 90% at 30% 18%, color-mix(in srgb, var(--accent, var(--gold)) 55%, transparent) 0%, transparent 78%),
    linear-gradient(180deg, transparent 40%, rgba(0,0,0,.55));
}

/* ---------- 16. SPEC LINE ---------- */
/* The spec line was twenty separate 11px uppercase fragments across the five
   product modals — on its own, the single largest source of tiny wide caps on
   the site. Sentence case, at reading size. */
.spec {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .4rem 1.1rem;
  padding-block: 1.4rem;
  font-size: var(--t-sm); letter-spacing: 0; line-height: 1.6; color: var(--taupe);
}
.spec li { display: flex; align-items: center; }
.spec li:not(:last-child)::after {
  content: ""; width: 3px; height: 3px; border-radius: 50%;
  background: var(--gold); flex: none; margin-left: 1.1rem;
}

/* ---------- 17. THE STORY MOVEMENT (cream — the one exhale) ---------- */
.story__act { max-width: var(--narrow); }
.story__act p + p { margin-top: 1.4rem; }
.dropcap::first-letter {
  font-family: var(--display); float: left;
  font-size: 4.2em; line-height: .82; padding: .06em .12em 0 0;
  color: var(--charcoal);
}
.scripture { max-width: 46ch; margin-top: 3rem; }
.scripture p { font-family: var(--display); font-style: italic; font-size: clamp(1.5rem, 2vw, 1.75rem); line-height: 1.45; margin-top: 1.5rem; }
.scripture cite { display: block; margin-top: 1rem; font-style: normal; font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--story-ink); }
/* Pull quote. It previously hung outside the container on a negative margin,
   which pushed the first characters off the left edge of the screen at tablet
   width and above. The emphasis now comes from measure and air instead — it
   stays inside the container at every width. */
blockquote.pull {
  margin-block: clamp(2.5rem, 5vw, 4rem);
  max-width: 30ch; border: 0; padding: 0;
  overflow-wrap: break-word;
}
@media (min-width: 1100px) {
  blockquote.pull { max-width: 34ch; padding-left: clamp(1rem, 4vw, 3rem); }
}

.values { display: grid; gap: 2.5rem; }
@media (min-width: 768px) { .values { grid-template-columns: repeat(2, 1fr); gap: 3.5rem 4rem; } }
.values h4 { font-size: clamp(1.5rem, 2vw, 1.75rem); margin-bottom: .7rem; }
.values p { color: var(--story-ink); max-width: 42ch; font-size: var(--t-base); }

.tstm { display: grid; gap: 3rem; }
@media (min-width: 900px) { .tstm { grid-template-columns: repeat(3, 1fr); gap: 3.5rem; } }
.tstm blockquote { display: grid; gap: 1.25rem; }
.tstm cite { font-style: normal; font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--story-ink); }

/* ---------- 18. NEWSLETTER + CONTACT ----------
   These were hairline-underlined fields on a dark ground — elegant, and almost
   invisible. A form the visitor has to look for is a form they do not fill in.
   Each field is now a real, enclosed surface: lifted off the background, with
   its own border and a gold edge plus a soft ring on focus, so it reads as
   something you type into from across the room. */
.field { display: grid; gap: .55rem; }
.field label {
  font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--gold);
}
.field input, .field textarea {
  background: rgba(255,255,255,.035);
  border: 1px solid rgba(255,255,255,.16);
  border-radius: 2px;
  padding: .95rem 1.1rem;
  color: var(--cream); font-size: var(--t-base); font-weight: 400;
  transition: border-color var(--dur-micro) var(--ease-out),
              background-color var(--dur-micro) var(--ease-out),
              box-shadow var(--dur-micro) var(--ease-out);
}
.field input::placeholder, .field textarea::placeholder { color: rgba(184,168,154,.6); }
.field input:hover, .field textarea:hover { border-color: rgba(176,140,80,.5); }
.field input:focus, .field textarea:focus {
  outline: none;
  border-color: var(--gold);
  background: rgba(255,255,255,.06);
  box-shadow: 0 0 0 3px rgba(176,140,80,.18);
}
.field textarea { resize: vertical; min-height: 150px; }

/* The form as an object on the page, not loose fields floating on the ground */
.formcard {
  background: rgba(255,255,255,.025);
  border: 1px solid rgba(176,140,80,.26);
  padding: clamp(1.5rem, 4vw, 2.5rem);
}
.formcard--news { background: rgba(0,0,0,.16); }
.formrow { display: grid; gap: 1.5rem; }
@media (min-width: 768px) { .formrow--2 { grid-template-columns: repeat(2, 1fr); } }
/* Contact details, in the section as well as the footer. Real NAP text, not an
   image — it is what LocalBusiness schema and local search read. */
.contactlist { display: grid; gap: .9rem; }
.contactlist a, .contactlist address {
  color: var(--cream); font-style: normal; font-size: var(--t-base);
  transition: color var(--dur-micro) var(--ease-out);
}
.contactlist a { border-bottom: 1px solid rgba(176,140,80,.4); padding-bottom: 2px; }
.contactlist a:hover { color: var(--gold-hi); border-bottom-color: var(--gold-hi); }
.contactlist address { color: var(--taupe); line-height: 1.6; }

.news__form { display: grid; gap: 1.25rem; max-width: 580px; }
@media (min-width: 620px) { .news__form { grid-template-columns: 1fr auto; align-items: end; } }
.formnote { font-size: var(--t-sm); letter-spacing: 0; text-transform: none; color: var(--taupe); }

/* ---------- 19. CART ---------- */
.scrim {
  position: fixed; inset: 0; z-index: var(--z-cart); background: rgba(10,8,6,.72);
  opacity: 0; visibility: hidden; transition: opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid);
}
.scrim.is-open { opacity: 1; visibility: visible; }
.cart {
  position: fixed; z-index: calc(var(--z-cart) + 1);
  background: var(--espresso); border: 1px solid rgba(176,140,80,.3);
  display: flex; flex-direction: column;
  transition: transform var(--dur-mid) var(--ease-out), opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid);
  opacity: 0; visibility: hidden;
}
@media (max-width: 719px) {
  .cart { left: 0; right: 0; bottom: 0; max-height: 84svh; transform: translateY(100%); border-width: 1px 0 0; }
  .cart.is-open { transform: none; }
}
@media (min-width: 720px) {
  .cart { top: calc(var(--nav-h) + 10px); right: 24px; width: 400px; max-height: calc(100svh - var(--nav-h) - 40px); transform: translateY(-10px); }
  .cart.is-open { transform: none; }
}
.cart.is-open { opacity: 1; visibility: visible; }
.cart__head { display: flex; align-items: center; justify-content: space-between; padding: 1.25rem 1.5rem; border-bottom: 1px solid rgba(255,255,255,.09); }
.cart__head h2 { font-size: var(--t-lg); }
.cart__body { overflow-y: auto; padding: 1.5rem; display: grid; gap: 1.5rem; align-content: start; flex: 1; }
.cart__foot { padding: 1.25rem 1.5rem 1.5rem; border-top: 1px solid rgba(255,255,255,.09); display: grid; gap: 1rem; }
.cart__total { display: flex; justify-content: space-between; align-items: baseline; }
.cart__total span:last-child { font-family: var(--display); font-size: var(--t-lg); }
.citem { display: grid; grid-template-columns: 62px 1fr auto; gap: 1rem; align-items: start; }
.citem__thumb { aspect-ratio: 3/4; }
/* Jost, not Bodoni — Bodoni's hairlines break below 1.5rem (MASTER §3) */
.citem__name { font-family: var(--display); font-size: var(--t-md); font-weight: 400; }
.citem__meta { font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--taupe); margin-top: .3rem; }
.citem__rm { font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--taupe); margin-top: .5rem; }
.citem__rm:hover { color: var(--gold-hi); }
.cart__empty { text-align: left; display: grid; gap: 1rem; color: var(--taupe); }

/* ---------- 20. FOOTER ---------- */
.foot { padding-block: clamp(4rem, 8vw, 6.5rem) 2.5rem; }
.foot__grid { display: grid; gap: 3rem; }
@media (min-width: 768px) { .foot__grid { grid-template-columns: 1.4fr 1fr 1fr 1.2fr; gap: 3rem 2.5rem; } }
.foot h3 { font-family: var(--sans); font-size: var(--t-xs); font-weight: 500; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--gold); margin-bottom: 1.4rem; }
.foot ul { display: grid; gap: .8rem; }
.foot a, .foot address { color: var(--taupe); font-style: normal; font-size: var(--t-sm); line-height: 1.8; }
.foot a:hover { color: var(--cream); }
.foot__brand img { height: 66px; width: auto; margin-bottom: 1.5rem; }
.foot__legal { margin-top: 3.5rem; padding-top: 1.75rem; border-top: 1px solid rgba(255,255,255,.09); display: flex; flex-wrap: wrap; gap: .75rem 1.75rem; justify-content: space-between; font-size: var(--t-xs); letter-spacing: 0; color: var(--taupe); }

/* ---------- 21. REDUCED MOTION — non-negotiable ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important; scroll-behavior: auto !important;
  }
  .rv { opacity: 1; transform: none; }
  .card__hair { transform: scaleX(1); }
  .card:hover .card__plate .plate { transform: none; }
  .hero__bg { transform: none !important; }
  /* The looping button sheen is the one infinite animation on the site.
     Under reduced motion it does not run at all — the button keeps its border,
     fill and label, which is what actually identifies it as a control. */
  .btn::after { animation: none; transform: translateX(-130%); }
  .pmodal__panel { transform: none; }
}

/* ============================================================
   22. STORY LAYOUT · VALUES PANEL · CARE · FAQ · WELCOME OFFER
   ============================================================ */

/* ---------- The story, reorganised (desktop only) ----------
   Mobile was never the problem, so mobile is untouched: one column, in order.
   On desktop the prose used to have three different left edges — body text at
   the container edge, the scripture at another, the pull quote pushed further
   still — with the portrait floating beside only the first paragraph. The eye
   had to search for where to go next. Now every word shares one left edge and
   one measure, and the portrait holds its place beside the column. */
.story__grid { display: grid; gap: clamp(2.5rem, 5vw, 4rem); }
.story__col > * + * { margin-top: clamp(2.25rem, 4vw, 3.25rem); }

@media (min-width: 960px) {
  .story__grid { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); align-items: start; }
  .story__col  { max-width: 62ch; }
  /* The portrait stays with the reader through the whole passage rather than
     scrolling away after two paragraphs, which is what left the right-hand
     side of the section empty. */
  .story__figure { position: sticky; top: calc(var(--nav-h) + 2.5rem); }
  /* One left edge for everything in the column. */
  .story__col .scripture,
  .story__col blockquote.pull { margin-left: 0; padding-left: 0; }
  .story__col blockquote.pull { max-width: 30ch; }
}

/* ---------- "What we hold to" — lifted off the cream ----------
   It sat on the same ground as the story above and the testimonials below, so
   three separate ideas read as one long column. A linen panel with gold
   hairlines gives it edges without introducing a new colour. */
.values-panel {
  background: color-mix(in srgb, var(--story-ground) 88%, #fff);
  border-top: 1px solid rgba(176,140,80,.45);
  border-bottom: 1px solid rgba(176,140,80,.45);
  padding: clamp(2.5rem, 6vw, 4.5rem) clamp(1.5rem, 5vw, 3.5rem);
  margin-block: clamp(1rem, 3vw, 2rem);
}
@media (min-width: 960px) {
  /* Pull it slightly wider than the reading column so the edges are unmissable */
  .values-panel { margin-inline: clamp(-1.5rem, -2vw, -0.5rem); }
}

/* ---------- Candle care ---------- */
.care { display: grid; gap: clamp(2rem, 4vw, 3rem); counter-reset: care; }
@media (min-width: 900px) { .care { grid-template-columns: repeat(2, 1fr); gap: 3.5rem 4rem; } }
.care li { display: grid; grid-template-columns: 3.25rem minmax(0,1fr); gap: 1.25rem; align-items: start; }
.care__n {
  font-family: var(--display); font-size: 1.5rem; color: var(--gold);
  line-height: 1.1; padding-top: .1em;
}
.care h3 { font-size: var(--t-lg); margin-bottom: .6rem; }
.care p  { color: var(--taupe); max-width: 46ch; }
.care__safety {
  margin-top: clamp(2.5rem, 5vw, 3.5rem); padding-top: 1.75rem;
  border-top: 1px solid rgba(176,140,80,.3);
  color: var(--gold); max-width: 68ch; font-size: var(--t-sm);
}

/* ---------- FAQ ----------
   Reuses the accordion from the product modal so there is one accordion on the
   site, not two that drift apart. */
.faq { max-width: 78ch; }
.faq .acc__btn { padding-block: 1.5rem; }
.faq .acc__panel p + p { padding-top: .9rem; }
.faq .acc__panel strong { color: var(--cream); font-weight: 500; }
.faq .acc__panel p:last-child { padding-bottom: 1.75rem; }
/* Deep-linked from the footer — offset so the sticky nav never covers the row */
.faq .acc[id] { scroll-margin-top: calc(var(--nav-h) + 2rem); }

/* ---------- Welcome offer ---------- */
.promo[hidden] { display: none; }
.promo {
  position: fixed; inset: 0; z-index: calc(var(--z-modal) + 5);
  display: grid; place-items: center; padding: clamp(1rem, 4vw, 2rem);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid);
}
.promo.is-open { opacity: 1; visibility: visible; }
.promo__scrim { position: absolute; inset: 0; background: rgba(8,6,5,.8); }
@supports (backdrop-filter: blur(4px)) { .promo__scrim { backdrop-filter: blur(6px); } }
.promo__panel {
  position: relative; z-index: 1; width: min(100%, 480px);
  max-height: 92svh; overflow-y: auto;
  background: var(--espresso);
  border: 1px solid rgba(176,140,80,.4);
  padding: clamp(2rem, 5vw, 3rem);
  transform: translateY(14px); transition: transform var(--dur-mid) var(--ease-out);
}
.promo.is-open .promo__panel { transform: none; }
.promo__panel h2 { font-size: var(--t-xl); margin-block: .75rem 1rem; }
.promo__close {
  position: absolute; top: .5rem; right: .5rem;
  width: 44px; height: 44px; display: grid; place-items: center; color: var(--taupe);
  transition: color var(--dur-micro) var(--ease-out);
}
.promo__close:hover { color: var(--gold-hi); }
.promo__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.5; }
.promo__form { display: grid; gap: 1.25rem; margin-top: 1.75rem; }
.promo__form .btn { width: 100%; }
.promo__form + .formnote { margin-top: 1rem; }
.promo__code {
  font-family: var(--display); font-size: clamp(2rem, 7vw, 2.75rem);
  letter-spacing: .04em; color: var(--gold-hi);
  margin-block: .5rem 1.25rem;
  padding: 1rem 1.25rem; text-align: center;
  border: 1px dashed rgba(176,140,80,.55); background: rgba(255,255,255,.03);
}
.promo__actions { display: grid; gap: .5rem; margin-top: 1.75rem; justify-items: start; }
.promo__actions .btn { width: 100%; }

/* Mood tiles holding real photographs: drop the rendered lighting gradients
   (they exist to make an empty tile look lit) back to a light vignette. */
.mood__grid--photo i::before {
  background: linear-gradient(180deg, rgba(0,0,0,.28), transparent 45%, rgba(0,0,0,.3)) !important;
}
.mood__grid--photo i::after { opacity: .14; }
.mood__grid--photo i { opacity: 1 !important; }

/* ---------- LIVE STOCK ----------
   MASTER §1.5 rule 6 bans commerce theatre — countdowns, fake scarcity,
   badges. This is the narrow exception that is genuinely useful rather than
   manipulative, and it is held to three rules:
     · healthy stock says NOTHING. No "40 in stock", no green tick.
     · at or below 3, one quiet line. No colour alarm, no urgency verb.
     · at zero, an unmistakable Sold out, and the buy control is disabled —
       letting someone add an unavailable candle to a bag is the real damage.
   Nothing renders until the endpoint returns real numbers. */
.stock {
  display: none; margin-top: .6rem;
  font-size: var(--t-xs); text-transform: uppercase;
  letter-spacing: var(--track-caps); font-weight: 500;
  color: var(--taupe);
}
.stock.is-low  { display: block; color: var(--gold); }
.stock.is-out  { display: block; color: var(--taupe); }
.drawer .stock { margin-top: 0; margin-bottom: 1rem; }

/* A sold-out card still shows the candle — it is a shelf, not an error */
.is-soldout .card__plate { filter: saturate(.55) brightness(.8); }
.is-soldout [data-add] {
  pointer-events: none; opacity: .4;
  filter: grayscale(1);
}
.is-soldout [data-add]::after { animation: none; }

/* Film inside a plate. The plate keeps its rendered glow underneath, so the
   frame is never an empty black box while the video buffers. */
.plate__video {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
}
/* The plate's own gradients sit ABOVE the film so the scrim still does its job */
.plate:has(.plate__video)::before { z-index: 1; }
.plate:has(.plate__video)::after  { z-index: 2; }


