/* ═══════════════════════════════════════════════════════
   NEG — style.css
   Jewelry catalog storefront. No cart/checkout — every path
   ends at either a WhatsApp deep link or the contact form.
═══════════════════════════════════════════════════════ */

/* ─── TOKENS ──────────────────────────────────────────── */
/* Light theme: a very light, almost-white page background. --color-surface
   sits a touch brighter (pure white) than --color-bg so cards/panels still
   read as "raised" off the page; --color-ink is a dark charcoal for text.
   The nav is white (matching the rest of the light theme) everywhere
   except while transparent over the hero photo, where it needs light text
   instead -- see .navbar--transparent's own override further down. The
   footer keeps its own separate dark-charcoal tokens (not tied to the nav
   ones) since it stays dark regardless. */
:root {
  --color-bg:          #FFFFFF;
  --color-surface:     #FFFFFF;
  --color-ink:          #221F1C;
  --color-ink-soft:     rgba(34, 31, 28, 0.65);
  --color-ink-faint:    rgba(34, 31, 28, 0.45);
  --color-border:       rgba(34, 31, 28, 0.10);
  --color-accent:       #B08D3F;
  --color-accent-soft:  #C9A55C;
  --color-nav-bg:       #FFFFFF;
  --color-nav-ink:      #221F1C;
  /* CHANGE LOG 2026-09-25: was #221F1C (the same warm charcoal as
     --color-ink) -- darkened to a near-black at the user's request, since
     that value read as charcoal-brown rather than a true black and was
     competing with the gold accents instead of setting them off. Shared
     by the split-menu, the navbar's own menu-open state, the footer, and
     page-transition.js's cover tiles, so this one change reaches all of
     them for a consistent "brand dark" rather than several slightly
     different near-blacks. */
  --color-footer-bg:    #141210;
  --color-footer-ink:   #FAF9F6;
  --color-hover:        rgba(34, 31, 28, 0.05);

  --font-display: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --font-heading: 'Blackpearl Serif', Georgia, 'Times New Roman', serif;
  /* CHANGE LOG 2026-09-15: was 'Montserrat', -apple-system,
     BlinkMacSystemFont, 'Segoe UI', sans-serif -- sitewide body/UI font
     (paragraphs, buttons, form inputs, nav links, footer) swapped from
     Montserrat to Instrument Serif at the user's explicit request, so
     the whole site reads as one serif typeface family rather than a
     serif/sans pairing. SUPERSEDED by the next log entry below. */
  /* CHANGE LOG 2026-09-17: was 'Instrument Serif', Georgia,
     'Times New Roman', serif (the log directly above) -- sitewide body/UI
     font swapped again, this time to Instrument Sans, to match the font
     used in the About page's mission/vision/values paragraphs and match
     Instrument Serif with its own real sans-serif pairing everywhere
     instead of an all-serif site. --font-display/--font-heading (used
     for large display headings like "Our Story" and the Philosophy
     pull-quote) are untouched -- those were explicitly requested as
     serif treatments on their own. Instrument Sans is now loaded on all
     12 HTML pages (previously only our-story.html). To roll back: restore
     the line below to 'Instrument Serif', Georgia, 'Times New Roman',
     serif and drop "&family=Instrument+Sans:wght@400;500;600" from the
     Google Fonts <link> on all 12 pages. */
  --font-body:    'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-sans: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* A percentage gutter (not a fixed max-width) so the gap to the viewport
     edge scales with the screen instead of being pinned to one absolute
     value that only looks right at one resolution: ~65px on a 2560px-wide
     2K/27" screen and ~51px on a 1366px-wide 15.6" laptop both work out to
     roughly the same 2.5% per side. */
  --gutter: 2.5%;
  /* Used by .page-wrap -- a fixed (not percentage) content-width system
     for sections that need to line up with each other regardless of
     .container's own %-based --gutter (Our Story's hero title and the
     Philosophy section below it, which must share one left edge). Same
     max-width formula the reference design used: capping the box at
     content-width-plus-its-own-padding means the padding itself is what
     stops shrinking further on ultra-wide screens, not the content. */
  --page-width: 1600px;
  --page-padding: 5rem;
  /* Shared by .mission-block__media (the fixed 800x600 photo) and
     .philosophy__text (which must be exactly that same width, right-
     aligned to match it) -- one value so the two can't drift apart. */
  --story-media-width: 800px;
  --nav-height:       84px;
  --radius-sm: 4px;
  --radius-md: 10px;
  --radius-lg: 20px;

  --shadow-sm: 0 2px 10px rgba(34, 31, 28, 0.06);
  --shadow-md: 0 8px 30px rgba(34, 31, 28, 0.10);

  --ease-smooth: cubic-bezier(0.22, 1, 0.36, 1);
  /* A slower, symmetric ease-in-out for the reel carousel specifically --
     unlike --ease-smooth (an ease-OUT, meant for one-shot reveals), cards
     sliding past each other need to decelerate on ENTRY too, or an
     incoming and an outgoing card moving at different perceived speeds
     reads as the "overlapping / snappy" clash this was tuned to avoid. */
  --ease-carousel: cubic-bezier(0.65, 0, 0.35, 1);
  /* Used only by the homepage hero-reveal intro (js/hero-reveal.js) --
     same two curves as the Codegrid reference it was adapted from
     (GSAP CustomEase("hop", "0.9,0,0.1,1") / ("glide", "0.8,0,0.2,1")
     are just named cubic-beziers, so the numbers carry over verbatim
     without needing GSAP itself, matching this site's plain-CSS-
     transitions convention -- see page-transition.js's own comment on
     the same decision). */
  --ease-hop: cubic-bezier(0.9, 0, 0.1, 1);
  --ease-glide: cubic-bezier(0.8, 0, 0.2, 1);
}

/* ─── FONTS ───────────────────────────────────────────── */
/* Blackpearl is a caps-only/unicase display face (no distinct lowercase
   forms -- lowercase input just renders as capitals), used deliberately
   for every h1/h2 on the site per the brand's own choice. */
@font-face {
  font-family: 'Blackpearl Serif';
  src: url('../fonts/BlackpearlSerif-w1ElP.otf') format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ─── RESET ───────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body {
  width: 100%;
  min-height: 100%;
  background: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
}
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
input, textarea, select { font: inherit; color: inherit; }
ul { list-style: none; }
h1, h2 {
  font-family: var(--font-heading);
  font-weight: 400;
  line-height: 1.15;
}
h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
}

/* ─── SMOOTH SCROLL WRAPPER ────────────────────────────── */
/* See js/smooth-scroll.js -- everything except the fixed navbar/mobile
   menu lives in here so it can be painted at an eased offset instead of
   jumping to match the real scroll position instantly. Must stay
   position:fixed (not sticky/absolute) so it's removed from normal flow
   entirely -- document.body's own height is set by JS to match this
   element's real content height, which is what keeps native scrolling
   (scrollbar, wheel, keyboard, touch) working normally underneath the
   illusion. When JS never runs (or prefers-reduced-motion is set, which
   the JS itself also checks and skips), this reverts to plain static
   flow so the page is never accidentally clipped to one viewport's
   height with no way to reach the rest of it. */
#smoothWrapper {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 1;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  #smoothWrapper { position: static; }
}

.container {
  width: calc(100% - (var(--gutter) * 2));
  margin-inline: auto;
}

/* See --page-width/--page-padding above. */
.page-wrap {
  max-width: calc(var(--page-width) + var(--page-padding) * 2);
  margin-inline: auto;
  padding-inline: var(--page-padding);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ─── BUTTONS ─────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 15px 30px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Without this, the label inherits body's line-height:1.6 -- flex
     align-items:center then centers that oversized line box, but
     flip-letter's own vertical-align:top (see its comment in this file)
     aligns the actual glyphs to the TOP of that box, not its middle, so
     the visible text sits a couple px off-center instead of truly
     centered. line-height:1 removes the extra slack entirely. */
  line-height: 1;
  transition: transform 0.25s var(--ease-smooth), background 0.25s, color 0.25s, border-color 0.25s;
  white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn--primary {
  background: var(--color-ink);
  color: var(--color-bg);
  /* Its own hover already swaps the background TO gold -- a gold flip-
     letter reveal would vanish against its own hover background, so
     this one gets the dark ink used elsewhere as its settled color
     instead of the sitewide default gold. */
  --flip-gold: var(--color-ink);
}
.btn--primary:hover { background: var(--color-accent); color: var(--color-ink); }

.btn--outline {
  background: transparent;
  color: var(--color-ink);
  border: 1px solid var(--color-ink);
  /* Hovers to a dark background, so it needs the softer gold used
     elsewhere against dark backgrounds, not the sitewide light-bg
     default. */
  --flip-gold: var(--color-accent-soft);
}
.btn--outline:hover { background: var(--color-ink); color: var(--color-bg); }

.btn--whatsapp {
  background: #25D366;
  color: #0b2e17;
}
.btn--whatsapp:hover { background: #1ebd5a; }

.btn--icon {
  width: 22px; height: 22px;
}

.btn-group { display: flex; gap: 16px; flex-wrap: wrap; }
.btn-group--center { justify-content: center; }

/* ─── NAVBAR ──────────────────────────────────────────── */
/* On this brand's real site the logo sits dead-center regardless of how
   much text sits on either side of it -- a 3-column grid (not flex
   space-between) is what keeps it centered instead of merely "between
   whatever's left/right", which drifts off-center the moment the two
   side groups have different widths (they do: 3 links left, 2 right). */
.navbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  height: var(--nav-height);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding-inline: var(--gutter);
  background: var(--color-nav-bg);
  color: var(--color-nav-ink);
  box-shadow: 0 1px 0 rgba(34, 31, 28, 0.06);
  transition: transform 0.35s var(--ease-smooth), background 0.3s, box-shadow 0.3s, color 0.3s;
  /* Themeable per-state via custom properties rather than hardcoded
     colors, so .navbar--transparent (over the hero photo) can flip these
     to light without every rule below needing its own override. */
  --nav-link-color: rgba(34, 31, 28, 0.7);
  --nav-hover-bg: var(--color-hover);
}
.navbar.is-hidden { transform: translateY(-100%); }

/* Homepage only: starts transparent over the hero photo (matching the
   client's original site) with just enough of a top scrim for the nav
   text/icons to stay legible over any image, then solidifies (matching
   every other page's white nav) once the page scrolls past the hero --
   see nav.js's scroll handler. */
.navbar--transparent:not(.is-solid) {
  background: linear-gradient(to bottom, rgba(20, 18, 16, 0.55), rgba(20, 18, 16, 0));
  color: #FAF9F6;
  box-shadow: none;
  --nav-link-color: rgba(250, 249, 246, 0.85);
  --nav-hover-bg: rgba(250, 249, 246, 0.14);
}

/* While the split menu is open, the navbar goes fully opaque dark
   (matching the menu's own background) regardless of its transparent/
   solid state -- otherwise the transparent gradient's partial opacity
   lets the menu's own scrolling link text show through it, which reads
   as a glitch rather than an intentional see-through effect. */
#navbar.navbar--menu-open {
  background: var(--color-footer-bg);
  color: var(--color-footer-ink);
  box-shadow: none;
  --nav-link-color: var(--color-footer-ink);
  --nav-hover-bg: rgba(250, 249, 246, 0.14);
}

.navbar__logo {
  grid-column: 2;
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: inherit; /* follows .navbar's own color, which flips per state */
}
.navbar__logo span { color: var(--color-accent-soft); }

.navbar__group {
  display: flex;
  align-items: center;
  gap: 20px;
}
.navbar__group--left { grid-column: 1; justify-self: start; }
.navbar__group--right { grid-column: 3; justify-self: end; }

.nav-btn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: inherit; /* follows .navbar's own color, which flips per state */
  transition: background 0.2s, color 0.2s, transform 0.2s var(--ease-smooth);
}
.nav-btn:hover { background: var(--nav-hover-bg); transform: translateY(-2px); }
.nav-btn svg, .nav-btn img { width: 20px; height: 20px; }

/* Always visible now (was mobile-only) -- the full-screen split menu is
   the site's only nav mechanism at every width, not just a mobile
   fallback for an inline desktop link row (which no longer exists). */
.nav-btn--menu { display: flex; }

.hamburger {
  width: 20px; height: 14px;
  position: relative;
}
.hamburger .bar {
  position: absolute; left: 0; width: 100%; height: 1.5px;
  background: currentColor; transition: transform 0.3s var(--ease-smooth), opacity 0.2s;
}
.hamburger .bar--1 { top: 0; }
.hamburger .bar--2 { top: 6px; }
.hamburger .bar--3 { top: 12px; }
.hamburger.is-open .bar--1 { transform: translateY(6px) rotate(45deg); }
.hamburger.is-open .bar--2 { opacity: 0; }
.hamburger.is-open .bar--3 { transform: translateY(-6px) rotate(-45deg); }

.page-nav { height: calc(var(--nav-height) + 1px); }

/* ─── SPLIT MENU ──────────────────────────────────────────
   Full-screen takeover menu (replaces the old dropdown-panel
   nav-menu-panel), ported from a GSAP/SplitText reference into
   plain CSS transitions + vanilla JS (see js/split-menu.js).
   Two background halves rotate in from fully off-screen
   (transform-origin at their own inner edge, rotated 180°+
   scaled out) to reveal a two-tone split background; link text
   is masked (overflow hidden on the wrapper, the link itself
   translated below its own box) and slides up into view,
   staggered via per-item transition-delay set in JS -- the
   vanilla equivalent of the reference's SplitText line-mask
   reveal, simpler here since every link is already one line
   with no need for real multi-line splitting. Sits BELOW the
   navbar's own z-index (100) so the toggle button/logo stay
   visible and clickable above the open menu, matching the
   reference's own stacking (nav above menu). */
.split-menu {
  position: fixed;
  inset: 0;
  z-index: 95;
  overflow: hidden;
  pointer-events: none;
  color: var(--color-footer-ink);
  /* Both halves the same color -- reads as one solid panel with no
     visible seam down the middle, while the left/right pieces still
     rotate open independently underneath. */
  --split-left: var(--color-footer-bg);
  --split-right: var(--color-footer-bg);
}
.split-menu.is-open { pointer-events: auto; }
/* Hidden whenever fully closed and idle -- both bg panels sit at
   scaleX(0), mathematically zero width, which (like the page-transition
   overlay's own scaleX(0) blocks) can leave a persistent rendering
   hairline at the collapse point even though nothing should be visible
   there. js/split-menu.js adds .is-visible right away on open and holds
   it through the close transition, only removing it once the close
   animation has actually finished. */
.split-menu:not(.is-visible) { visibility: hidden; }

.split-menu__bg {
  position: absolute;
  inset: 0;
}
.split-menu__bg-left, .split-menu__bg-right {
  position: absolute;
  top: 0;
  /* Slightly over 50%, not exactly -- two adjacent 50%-width boxes can
     round to a combined 99% or 101% of an odd-pixel viewport width
     (browser sub-pixel rounding), leaving a hairline gap at the seam
     that shows whatever's behind the menu straight through it, even
     with both halves the same color. A 1px overlap on each side closes
     that gap outright regardless of rounding direction; harmless since
     both halves render identically anyway. */
  width: calc(50% + 1px);
  height: 100%;
  overflow: hidden;
}
.split-menu__bg-left { left: 0; }
.split-menu__bg-right { right: 0; }
/* Was rotate(180deg) scale(2,2) -- replicated a real, reported rendering
   defect (a persistent hairline visible only while the menu sits closed,
   not reproducible in this project's own headless testing, so treated
   as a real GPU-compositing quirk with that specific transform combo
   rather than dismissed). Plain scaleX from each panel's own inner edge
   (the center seam) is a far simpler, already-proven-safe technique on
   this site (same shape as the page-transition tile wipe) and gives the
   identical "unfurl from the center" visual with much less to go wrong. */
.split-menu__bg-left-inner, .split-menu__bg-right-inner {
  position: absolute;
  inset: 0;
  transition: transform 0.8s var(--ease-carousel);
  transform: scaleX(0);
}
.split-menu__bg-left-inner {
  background: var(--split-left);
  transform-origin: 100% 50%;
}
.split-menu__bg-right-inner {
  background: var(--split-right);
  transform-origin: 0% 50%;
}
.split-menu.is-open .split-menu__bg-left-inner,
.split-menu.is-open .split-menu__bg-right-inner {
  transform: scaleX(1);
}

/* ─── SPLIT MENU LAYOUT -- "Editorial Index" ─────────────────────
   Main pages (New In / Categories / About Us / ...) read as a
   left-aligned, numbered contents list (magazine-index style) rather
   than a centered block of links; Collections drops from a full
   vertical column down to a small secondary tag row underneath, set
   off by a thin rule. Chosen over two earlier directions (a "Dominant
   Hierarchy" oversized single column, and an "Asymmetric Zigzag"
   alternating layout) that were prototyped side-by-side in a
   since-removed menu-lab.html test page. Same numbering + tag-row
   treatment is reused on mobile (see the @media block below) rather
   than dropping it -- it was already narrow enough to fit. The right
   half of the desktop split (visually distinct via .split-menu__bg's
   own two-tone panels) is reserved for .split-menu__gallery. */
.split-menu__items {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  padding-left: 4vw;
}
.split-menu__col {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.5rem;
}
.split-menu__col:not(.split-menu__col--collections) {
  counter-reset: menu-index;
}
.split-menu__link {
  overflow: hidden;
  counter-increment: menu-index;
  display: flex;
  align-items: center;
  gap: 1.25rem;
}
/* Only the main-pages column counts -- Collections links reuse
   .split-menu__link too but never increment/show a number, since they
   read as a tag row, not a numbered list. */
.split-menu__col--collections .split-menu__link { counter-increment: none; }
.split-menu__link::before {
  content: counter(menu-index, decimal-leading-zero);
  font-family: var(--font-body);
  font-size: 14px;
  letter-spacing: 0.08em;
  color: var(--flip-gold);
  opacity: 0.6;
}
.split-menu__col--collections .split-menu__link::before { content: none; }
.split-menu__link a {
  position: relative;
  display: block;
  /* Extra bottom padding reserves room, inside this box, for the hover
     underline below -- .split-menu__link's own overflow:hidden (needed
     for the slide-up entrance) would otherwise clip anything sitting
     outside the anchor's own box. */
  padding-bottom: 8px;
  /* Same face as every h1/h2 (Blackpearl, a caps-only display serif --
     see its @font-face comment above), not the body font. font-weight
     dropped to 400 to match: it's the only weight that font actually
     ships, so 600 was just triggering the browser's synthetic-bold
     fallback. */
  font-family: var(--font-heading);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: clamp(22px, 2.6vw, 40px);
  color: inherit;
  transform: translateY(110%);
  transition: transform 0.6s var(--ease-carousel);
}
.split-menu__col--collections {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.5rem;
  /* Deliberately well short of the gallery's own edge (the split sits
     at 50vw) -- a wider cap here narrowed the visual gap between the
     text block and the gallery to almost nothing. Wrapping the
     Collections list into more (shorter) rows is the tradeoff. */
  max-width: 24vw;
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(250, 249, 246, 0.15);
}
.split-menu__col--collections .split-menu__link a {
  font-family: var(--font-body);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 13px;
}
.split-menu.is-open .split-menu__link a { transform: translateY(0); }

/* Hover: the flip-letter effect below (word flips up into gold, left to
   right). .is-active (the current page's own link, set by split-menu.js
   on the wrapping .split-menu__link) reuses the exact same flip-up-to-
   gold state, permanently settled. */
.split-menu__link.is-active .flip-letter__face--front { transform: translateY(-100%); }
.split-menu__link.is-active .flip-letter__face--back { transform: translateY(0); }

.split-menu__footer {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  padding: 2rem;
  display: flex;
  /* flex-start, not space-between -- the copyright column that used to
     sit on the other end was removed, leaving just the social links. */
  justify-content: flex-start;
  align-items: flex-end;
}
.split-menu__footer-col {
  display: flex;
  gap: 1.75rem;
  overflow: hidden;
}
.split-menu__footer-col a, .split-menu__footer-col p {
  display: block;
  font-family: var(--font-body);
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: rgba(250, 249, 246, 0.75);
  transform: translateY(110%);
  transition: transform 0.5s var(--ease-carousel);
}
.split-menu__footer-col a:hover { color: var(--color-footer-ink); }
.split-menu.is-open .split-menu__footer-col a,
.split-menu.is-open .split-menu__footer-col p { transform: translateY(0); }

/* ─── FLIP-LETTER HOVER (generic, reused sitewide) ───────────────
   api.js's wrapFlipLetters() splits any element's text into small
   overflow:hidden boxes, each holding two stacked copies of that letter
   (the real one on top, a gold duplicate directly below it). On hover
   both slide up together: the real letter exits off the top while the
   gold one slides into its place -- reads as the word "flipping up"
   into gold, left to right, each letter's own transition-delay (set
   inline by JS, a small "gutter" between them) staggering the wave
   across the word. The incoming gold letters share the exact same
   per-letter delay as the outgoing real ones, since both faces of one
   letter are set together.

   --flip-gold defaults to the sitewide accent (right for buttons/links
   on this site's normal light backgrounds); dark-photo/dark-panel
   contexts (hero, split-menu, footer) override it to the softer accent
   used everywhere else on dark backgrounds, for contrast. Triggers on
   :hover for any element, or permanently via .is-active (see
   .split-menu__link.is-active above) for a "currently selected" state
   with no hover needed. */
.flip-letter {
  display: inline-block;
  position: relative;
  overflow: hidden;
  height: 1em;
  vertical-align: top;
}
.flip-letter__face {
  display: block;
  line-height: 1;
  transition: transform 0.4s var(--ease-smooth);
}
.flip-letter__face--front { transform: translateY(0); }
.flip-letter__face--back {
  position: absolute;
  top: 0;
  left: 0;
  color: var(--flip-gold, var(--color-accent));
  transform: translateY(100%);
}
a:hover .flip-letter__face--front,
button:hover .flip-letter__face--front { transform: translateY(-100%); }
a:hover .flip-letter__face--back,
button:hover .flip-letter__face--back { transform: translateY(0); }

.hero, .split-menu, .footer-wrapper { --flip-gold: var(--color-accent-soft); }

@media (max-width: 960px) {
  .split-menu__bg-right { display: none; }
  .split-menu__bg-left { width: 100%; }
  /* Both the items column and the footer switch from the desktop's
     absolute-positioned/pinned layout to normal document flow here --
     with up to a dozen+ Collections links, the desktop layout's fixed
     100vh box (no scroll) would overflow and visually collide with the
     bottom-pinned footer instead of just growing taller. Scrolling the
     menu itself (not just .split-menu__items) is what actually lets the
     footer end up below all the links rather than overlapping them. */
  .split-menu {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Fixed, not absolute, here specifically -- .split-menu itself now
     scrolls (its content can grow taller than 100vh with enough
     Collections tags), and an absolutely-positioned background sized to
     .split-menu's own box would stay pinned to that original 100vh box
     while the actual content scrolls past it, leaving the real page
     visible through the bottom once scrolled past that point. Fixed
     positioning escapes the scroll entirely, always covering the true
     viewport regardless of how far the content inside is scrolled. */
  .split-menu__bg { position: fixed; }
  .split-menu__items {
    position: static;
    min-height: 100%;
    /* column, not column-reverse -- main pages (New In / Categories /
       About Us / ...) are the primary nav and belong above the
       Collections tag list, not below it. */
    flex-direction: column;
    justify-content: flex-start;
    padding: calc(var(--nav-height) + 2.5rem) 2rem 3rem;
    gap: 3rem;
  }
  .split-menu__col { flex: none; gap: 1.1rem; }
  /* The 30vw cap from the desktop layout (sized against the right
     half's own gallery panel, which doesn't exist here) would crush the
     tag row into a narrow column on a phone-width screen. */
  .split-menu__col--collections {
    max-width: 100%;
    gap: 0.6rem 1.25rem;
    margin-top: 0.5rem;
    padding-top: 1.25rem;
  }
  .split-menu__footer {
    position: static;
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
    padding: 1.5rem 2rem 2.5rem;
  }
  .split-menu__footer-col { flex-direction: column; gap: 8px; }
}

/* ─── SPLIT MENU GALLERY (right half, desktop only) ──────────────
   An endless vertical marquee -- a column of photos drifting upward
   forever, like the Milkinside reference this was modeled on. The
   track holds the same image list twice back to back; animating its
   own translateY from 0 to -50% moves exactly one full set's worth of
   height, so the moment the first set has scrolled out of view the
   (identical) second set is sitting exactly where the first started --
   the loop point is invisible. Sits inside .split-menu, which is
   already z-index 95 vs #navbar's 100 (see .split-menu's own comment
   above), so the images pass under the header for free -- no extra
   z-index needed here. Hidden on mobile (no second panel there; the
   mobile media query above already drops .split-menu__bg-right
   entirely). */
.split-menu__gallery {
  position: absolute;
  inset: 0 0 0 50%;
  overflow: hidden;
}
.split-menu__gallery-track {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px;
  animation: split-menu-gallery-scroll 55s linear infinite;
}
/* Paused while the menu itself is closed/closing -- it's invisible
   then anyway (opacity 0 below, plus .split-menu's own visibility
   toggle), so running it is wasted GPU work. */
.split-menu:not(.is-open) .split-menu__gallery-track {
  animation-play-state: paused;
}
.split-menu__gallery-item {
  flex: none;
  width: 100%;
  height: 26vh;
  min-height: 210px;
}
.split-menu__gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1s var(--ease-smooth);
}
.split-menu.is-open .split-menu__gallery-item img { opacity: 1; }
/* Bottom-only fade -- the lowest tile dissolves into the footer
   instead of cutting off hard, matching the reference. The top tile is
   left to hard-clip against the menu's own top edge (no fade there),
   also matching it. */
.split-menu__gallery::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 30%;
  background: linear-gradient(to bottom, transparent, var(--color-footer-bg));
  pointer-events: none;
}
@media (max-width: 960px) {
  .split-menu__gallery { display: none; }
}

/* ─── HERO ────────────────────────────────────────────── */
/* Full-bleed editorial photo behind the nav, full-height content split
   top (headline) / bottom (tagline+CTA) via .hero__content's own
   space-between -- structure ported from the "page reveal" reference
   (its .hero/.hero-content/.hero-header/.hero-social), replacing this
   site's earlier bottom-left-anchored layout. The scrim is this site's
   own addition, not the reference's: it guarantees the overlay text
   stays legible no matter how busy/light the photo behind it is,
   instead of relying on the photo itself being dark enough throughout. */
.hero {
  position: relative;
  height: 100svh;
  min-height: 620px;
  overflow: hidden;
  color: #fff;
}
.hero__bg {
  position: absolute;
  inset: 0;
}
.hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 65% center;
}
.hero__scrim {
  position: absolute;
  inset: 0;
  /* Anchored to the bottom-left corner (where .hero__content sits) instead
     of a band across the full width -- darkens just enough behind the
     text for legibility and fades out before reaching the rest of the
     photo, rather than dimming the whole lower half. */
  background: radial-gradient(120% 100% at 0% 100%, rgba(20, 18, 16, 0.82) 0%, rgba(20, 18, 16, 0.45) 30%, rgba(20, 18, 16, 0) 60%);
  /* Hidden until hero-reveal.js flags it .is-visible (same moment as
     .hero__content), then grows in from the bottom edge upward -- reads
     as the shadow rising to meet the text as it fades in, rather than
     just popping in already-dark. */
  clip-path: inset(100% 0 0 0);
  transition: clip-path 0.9s var(--ease-hop);
}
.hero__scrim.is-visible { clip-path: inset(0% 0 0 0); }

/* ─── HERO PRELOADER (homepage load-in intro, js/hero-reveal.js) ──
   Covers the hero completely until the image row underneath is ready:
   a progress bar fills left to right, exits toward the right (same
   "hairline that grows then collapses toward its other edge" idea as
   page-transition.js's own blocks), then this whole layer rises away
   (clip-path collapsing from the bottom up) to reveal the hero. Sits
   above .hero-reveal (z-index) so the image row can already be
   sliding in from off-screen underneath this while it's still rising --
   matching the reference's own overlap, where the images are visibly
   in motion well before the cover has fully cleared. Durations/easings
   are set inline by hero-reveal.js (see its own comment on why), not
   here -- these rules just define the properties being animated. */
.hero-preloader {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: var(--color-bg);
  clip-path: inset(0 0 0% 0);
}
.hero-preloader__bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: left center;
}

/* ─── HERO REVEAL (homepage load-in intro, js/hero-reveal.js) ────
   5 full-hero-sized images stacked in one spot, each scaled/rotated/
   offset via transform into a small scattered row -- .hero-reveal__img
   is deliberately the SAME inset:0 box as .hero__bg (not a smaller
   card), shrunk down purely via `transform: scale()`, so the centered
   one (the real hero photo) can grow back to `scale(1)` and land in
   exactly .hero__bg's own position with nothing else to animate.

   Starts off-screen (--reveal-enter-x) and slides in to its arranged
   row position (.is-entered) once .hero-preloader begins rising away --
   this only needs to be HIDDEN while off-screen, not while arranged, so
   sitting behind .hero-preloader's opaque cover for the first part of
   its motion is enough; no separate "already arranged from frame one"
   trick needed here (an earlier version skipped the entrance animation
   entirely for that reason, before .hero-preloader existed to hide it).

   This container's own background matters: while the 5 images are small
   (scaled to 0.2, arranged in a row), there's real empty space AROUND
   and BETWEEN them, through which .hero__bg -- the real, final, already-
   fully-rendered hero photo sitting directly underneath -- would
   otherwise show straight through, spoiling the reveal by showing the
   end state early. This solid backdrop (same color as .hero-preloader,
   so the handoff between them is invisible) covers that gap until
   .is-done hides this whole layer at the very end, by which point the
   center image has already grown to cover the entire box anyway. */
.hero-reveal {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--color-bg);
}
.hero-reveal.is-done { display: none; }
.hero-reveal__img {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: 1.25rem;
  transform: translateX(var(--reveal-enter-x, -150vw)) rotate(var(--reveal-rotate, 0deg)) scale(0.2);
  transition: transform 1.2s var(--ease-glide), border-radius 1.2s var(--ease-glide);
}
.hero-reveal__img img { width: 100%; height: 100%; object-fit: cover; }
.hero-reveal.is-entered .hero-reveal__img {
  transform: translateX(var(--reveal-x, 0)) rotate(var(--reveal-rotate, 0deg)) scale(0.2);
}
.hero-reveal.is-spreading .hero-reveal__img {
  transition: transform 1.5s var(--ease-glide), border-radius 1.5s var(--ease-glide);
  transform: translateX(var(--reveal-exit-x)) rotate(var(--reveal-rotate, 0deg)) scale(0.2);
}
.hero-reveal.is-spreading .hero-reveal__img--center {
  border-radius: 0;
  transform: translateX(0) rotate(0deg) scale(1);
}
/* Must match .hero__bg img's own object-position exactly -- both show
   the same photo, and this is the image that grows to `scale(1)` and
   sits directly on top of .hero__bg until .is-done hides this whole
   layer. Without this override it defaulted to object-position's own
   50% center, while .hero__bg is deliberately shifted to keep the
   model/jewelry in frame -- meaning the instant the overlay hid, the
   crop window jumped from centered to 65%, snapping the photo content
   sideways instead of the seamless handoff this section's own comment
   above describes ("nothing else to animate"). */
.hero-reveal__img--center img { object-position: 65% center; }
.hero__content {
  position: absolute;
  inset: 0;
  z-index: 1;
  max-width: 640px;
  padding: 0 var(--gutter) 80px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Deliberately NOT the shared .reveal/.is-visible pattern (which every
     other page's content uses, toggled by the generic scroll-observer in
     nav.js) -- that observer fires the moment this element is on screen,
     which for the hero is immediately on load, before hero-reveal.js's
     own image sequence has finished. Same opacity/transform recipe,
     but owned and timed solely by hero-reveal.js instead. */
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-smooth), transform 0.7s var(--ease-smooth);
}
.hero__content.is-visible { opacity: 1; transform: translateY(0); }
.hero__eyebrow {
  display: inline-block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-accent-soft);
  margin-bottom: 18px;
}
/* "Vertical Blinds" per-character reveal (js/hero-reveal.js splits
   .hero__eyebrow into one span per letter, same technique as
   .hero__title-char). Each letter starts scaleX(0) -- collapsed to a
   vertical sliver, like a closed blind slat -- and opens out to
   scaleX(1). Staggered from BOTH edges inward (the outermost letters
   move first, meeting in the middle), not left to right, per the
   reference's stagger: { from: "edges" } -- see revealHeroEyebrow's own
   comment for how that's computed by hand. "power2.out" is a fairly
   plain quadratic ease-out, close enough to this site's own
   --ease-smooth to reuse rather than add a one-off curve. */
.hero__eyebrow-char {
  display: inline-block;
  transform-origin: center;
  transform: scaleX(0);
  opacity: 0;
  transition: transform 0.4s var(--ease-smooth), opacity 0.4s var(--ease-smooth);
}
.hero__eyebrow-char.is-visible { transform: scaleX(1); opacity: 1; }
.hero__title {
  font-size: clamp(38px, 5.5vw, 64px);
  color: #fff;
  margin: 0 0 30px;
}
.hero__title em {
  font-style: italic;
  color: var(--color-accent-soft);
}
/* "Wave color shift" per-character reveal (js/hero-reveal.js splits
   .hero__title into one span per letter, spaces left as plain text so
   the line can still wrap normally) -- each letter flies up from
   translateY(40px) and shifts from gold to its settled color, staggered
   left to right. Adapted from a GSAP/SplitText snippet into plain CSS
   transitions + per-span inline transition-delay, this site's own
   convention (see page-transition.js's identical stagger technique) --
   "sine.out" is just a gentle ease-out shape, close enough to this
   site's own --ease-smooth to reuse rather than add a one-off curve. */
.hero__title-char {
  display: inline-block;
  opacity: 0;
  transform: translateY(40px);
  /* Starts as the exact same gold "kept" itself settles into
     (--color-accent-soft), not the deeper --color-accent -- so the
     shift reads as "this gold resolves into its final color" instead
     of introducing a second, different gold nobody asked for. Color
     uses `linear`, not --ease-smooth like the other two properties:
     --ease-smooth is a steep ease-OUT (most of its change happens in
     the first fraction of its duration, see .reveal's identical
     curve), which made a longer color duration alone still snap to
     ~final color almost immediately and then sit there unchanged --
     linear is what actually keeps the shift visibly in motion for its
     whole (longer, 1.2s vs the 0.6s move) duration. */
  color: var(--color-accent-soft);
  transition: opacity 0.6s var(--ease-smooth), transform 0.6s var(--ease-smooth), color 1.2s linear;
}
.hero__title-char.is-visible { opacity: 1; transform: translateY(0); color: #fff; }
.hero__title em .hero__title-char.is-visible { color: var(--color-accent-soft); }
/* Reused as-is by reels.js for "In Motion" (see its own comment) --
   but that heading sits on this section's plain white background, not
   the hero's dark photo, so the settled color needs overriding back to
   this site's normal dark text color or it'd finish invisible: white
   text on a white section. */
#reelsHeader .hero__title-char.is-visible { color: var(--color-ink); }
#bridalHeader .hero__title-char.is-visible { color: var(--color-ink); }
#essentialsHeader .hero__title-char.is-visible { color: var(--color-ink); }
#instagramHeader .hero__title-char.is-visible { color: var(--color-ink); }
#storyCtaTitle .hero__title-char.is-visible { color: var(--color-ink); }
#storesHeader .hero__title-char.is-visible { color: var(--color-ink); }
.hero__cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #fff;
  padding-bottom: 6px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.5);
  transition: gap 0.25s var(--ease-smooth), border-color 0.25s, color 0.25s;
  align-self: flex-start;
}
.hero__cta svg { width: 14px; height: 14px; transition: transform 0.25s var(--ease-smooth); }
/* Combines two independent systems on the same letters: the load-time
   "wave color shift" (like .hero__title-char -- position+opacity on
   --ease-smooth, color on linear so it stays visibly in motion, see
   that rule's own comment for why) lives on the OUTER .flip-letter
   wrapper (position/opacity) and the front face (color), while the
   hover-flip-to-gold mechanics (api.js's wrapFlipLetters, same as every
   other button/link) stay on the front/back faces underneath,
   untouched. Splitting it this way avoids the two systems fighting over
   the same `transform`/`transition` on one element -- the wrapper's own
   transform is the load reveal's translateY(40px)->0, the front face's
   own transform is the hover flip's translateY(0)->(-100%), nested and
   independent. --flip-gold (set on .hero, an ancestor) already supplies
   the back face's gold, so only the front face needs its color rules
   here. */
.hero__cta .flip-letter {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s var(--ease-smooth), transform 0.6s var(--ease-smooth);
}
.hero__cta .flip-letter.is-visible { opacity: 1; transform: translateY(0); }
.hero__cta .flip-letter__face--front {
  color: var(--color-accent-soft);
  transition: color 1.2s linear, transform 0.4s var(--ease-smooth);
}
.hero__cta .flip-letter.is-visible .flip-letter__face--front { color: #fff; }
.hero__cta:hover {
  gap: 16px;
  color: var(--color-accent-soft);
  border-color: var(--color-accent-soft);
}
.hero__cta:hover svg { transform: translateX(3px); }

/* Same underline-link recipe as .hero__cta (text flush to the border's
   left edge, arrow flush to its right, gap widens + color shifts to
   the brand gold on hover) but in the sitewide light-bg ink/accent
   colors instead of .hero__cta's white/accent-soft -- for this same
   button style used away from the hero's dark photo (e.g. the Our
   Story page's closing CTAs). */
.link-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--color-border);
  transition: gap 0.25s var(--ease-smooth), border-color 0.25s, color 0.25s;
}
.link-cta svg { width: 14px; height: 14px; transition: transform 0.25s var(--ease-smooth); }
.link-cta:hover {
  gap: 16px;
  color: var(--color-accent);
  border-color: var(--color-accent);
}
.link-cta:hover svg { transform: translateX(3px); }

/* ─── SECTION HEADER ──────────────────────────────────── */
.section {
  padding-block: 90px;
}
.section--tight { padding-block: 60px; }

/* ─── INTER-SECTION SPACING ────────────────────────────────
   A consistent 120px of whitespace between adjacent <section>s
   sitewide, layered on top of each section's own padding-block
   (which already contributes part of that space) rather than
   replacing it -- each rule below adds exactly the margin needed
   to bring that specific pair's total gap to 120px, computed from
   the two sections' existing padding values. The one deliberate
   exception is the homepage hero -> Reels gap, left untouched. */
.section--tight + .category-row { margin-top: 10px; }   /* Reels' 60 bottom + this section's own 50 top = 120 */
.category-row + .full-sets { margin-top: 70px; }         /* this section's own 50 bottom + this 70 = 120 */
.full-sets + .bridal { margin-top: 120px; }                  /* neither section has its own padding here */
/* This pair deliberately breaks from the 120px system -- asked for an
   exact 80px gap here instead, added on top of (not instead of)
   .essentials' own ~90vh top padding, which is load-bearing for its
   fixed-background reveal effect and was never decorative spacing to
   begin with. This margin is the actual gap; the huge padding after it
   is a separate, unrelated effect. */
.bridal + .essentials { margin-top: 80px; }
/* our-story.html's two .section--tight blocks already total exactly
   120 (60 bottom + 60 top) with no help needed; .philosophy's own
   120px bottom padding (see OUR STORY below) already covers the gap
   before them, same reasoning. */

.section__header {
  text-align: center;
  max-width: 620px;
  margin: 0 auto 50px;
}
.section__eyebrow {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 14px;
}
.section__title { font-size: clamp(28px, 4vw, 40px); margin-bottom: 14px; }
.section__desc { color: var(--color-ink-soft); }
/* Word-level reveal (js/hero-reveal.js's splitIntoWords, used by
   js/reels.js on this section's own header) -- each word fades up on
   its own, staggered, rather than the whole sentence appearing at
   once. Plain fade+lift, no color shift like the char-split effects
   above -- ported from a GSAP/SplitText opacity+y snippet, "power2.out"
   approximated by this site's own --ease-smooth. */
.section__desc-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(15px);
  transition: opacity 0.5s var(--ease-smooth), transform 0.5s var(--ease-smooth);
}
.section__desc-word.is-visible { opacity: 1; transform: translateY(0); }

/* Small uppercase bracketed label, e.g. "[ Our Philosophy ]" / "[ Need
   Help ]" -- a distinct, more understated eyebrow style than
   .section__eyebrow (used sitewide), introduced for the About page's
   Philosophy section and reused wherever that same specific look is
   asked for again. */
.eyebrow-label {
  display: block;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06rem;
  text-transform: uppercase;
  color: #1c1b1a;
}

/* ─── CATEGORY ROW ────────────────────────────────────────
   A simple "shop by category" row: one circular photo + label
   underneath per top-level category, however many exist.
   Horizontally scrollable (scroll-snap, hidden scrollbar)
   rather than wrapping or forcing a fixed column count, so any
   category count works without a layout rewrite. Replaced the
   old pinned-title/parallax "collections-scroll" section per
   the brand's own reference design (a plain static row, no
   scroll-linked motion at all). */
.category-row {
  padding: 26px 0 50px;
}
.category-row__track {
  display: flex;
  /* safe center, not plain center -- plain center silently clips the
     start of overflowing content since scrollLeft can never go negative
     (the exact bug already hit and fixed once on the Bridal carousel). */
  justify-content: safe center;
  gap: clamp(24px, 4vw, 56px);
  overflow-x: auto;
  /* Setting overflow-x without overflow-y forces overflow-y to "auto" too
     (a CSS spec quirk: a "visible" axis paired with a non-visible one is
     coerced to "auto" as well) -- which was clipping the hovered item's
     upward lift at the track's own top edge. This padding-top gives the
     lift somewhere to go that's still inside the track's own padding box
     (padding is never clipped, only content that overflows past it is),
     so the hover motion has room without needing overflow-y visible
     (which the spec won't actually allow here). 24px covers the current
     max lift (18px) with a little headroom; matched by .category-row's
     own reduced top padding (26px) so the total visual gap above the
     row stays the same 50px as before this fix. */
  padding: 24px var(--gutter) 0;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}
.category-row__track::-webkit-scrollbar { display: none; }
.category-row__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  flex: 0 0 auto;
  scroll-snap-align: center;
  /* JS (category-row.js) sets the actual translateY on hover -- this is
     just what animates it, and what resets it back to 0 on mouseleave. */
  transition: transform 0.35s var(--ease-smooth);
}
.category-row__circle {
  display: block;
  width: clamp(110px, 11vw, 160px);
  height: clamp(110px, 11vw, 160px);
  border-radius: 50%;
  overflow: hidden;
  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}
.category-row__circle img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 0.5s var(--ease-smooth);
}
.category-row__item:hover .category-row__circle img { transform: scale(1.08); }
.category-row__name {
  font-family: var(--font-display);
  font-size: 16px;
  color: var(--color-ink);
  white-space: nowrap;
}

@media (max-width: 640px) {
  .category-row__circle { width: 92px; height: 92px; }
  .category-row__name { font-size: 14px; }
}

/* ─── FULL SETS SHOWCASE ───────────────────────────────────
   A two-column section (site's own 2.5% --gutter, not a
   section-specific override like collections-scroll's) --
   a full-height static photo on the left, and an auto-
   rotating product card cycling through the "Full Sets"
   category on the right (see js/full-sets.js). The card's
   own rotation is a plain setInterval, not scroll-linked --
   hovering it pauses the rotation so the hover-revealed
   chevrons/View Details button have time to actually be
   used, and the progress bar underneath tracks how far
   through the whole set of products you've seen (not a
   per-card countdown timer). */
.full-sets {
  display: grid;
  grid-template-columns: 1fr 1fr;
  padding-inline: var(--gutter);
}
.full-sets__image {
  height: 100vh;
  overflow: hidden;
  /* Entrance (js/full-sets.js): the container clips away from the bottom
     up (top-to-bottom reveal line, same --ease-hop language as the hero
     preloader's own clip-path rise) while the photo inside starts scaled
     up and settles to its true size as the clip finishes -- a classic
     "zoomed-in wipe" rather than a plain fade. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1s var(--ease-hop);
}
.full-sets__image.is-visible { clip-path: inset(0 0 0 0); }
.full-sets__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.15);
  transition: transform 1.3s var(--ease-glide);
}
.full-sets__image.is-visible img { transform: scale(1); }

.full-sets__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding-left: 10%;
}
.full-sets__title {
  font-family: var(--font-heading);
  font-size: clamp(28px, 3vw, 40px);
  margin-bottom: 32px;
}
/* Reuses the hero title's per-character wave-color-shift (js splits it
   via splitIntoChars/revealCharsSequential, same as .hero__title and
   #reelsHeader) -- this panel sits on the plain light section bg, not
   the hero's dark photo, so it needs the same ink-color override. */
.full-sets__panel .hero__title-char.is-visible { color: var(--color-ink); }

.full-sets__card-wrap {
  position: relative;
  width: min(380px, 100%);
  /* Own hidden/visible state (not the shared .reveal class) so
     js/full-sets.js can time it together with the image/title reveals
     off one observer, instead of racing against the sitewide generic
     one in api.js which fired independently (and too early). */
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-smooth), transform 0.7s var(--ease-smooth);
}
.full-sets__card-wrap.is-visible { opacity: 1; transform: translateY(0); }
.full-sets__card {
  position: relative;
  display: flex;
  flex-direction: column;
  /* The product photos are shot on white -- isolate creates a local
     stacking context so the media image's mix-blend-mode only blends
     against this card's own bg, not whatever sits behind the section. */
  background: #f8f8f8;
  isolation: isolate;
  border-radius: var(--radius-sm);
  aspect-ratio: 3 / 4;
}
.full-sets__card-media {
  position: relative;
  /* Clips the hover-zoom scale below to this box -- without it, a
     scaled-up image would spill past the media area into the info/
     button region below it. */
  overflow: hidden;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}
.full-sets__card-media img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* Darken blends the photo's own white background into the card's
     near-white bg, so the two read as one continuous surface instead
     of a visible white rectangle floating on off-white. */
  mix-blend-mode: darken;
  /* Both images (this one and .full-sets__card-media-hover) scale up
     together on hover -- see that rule's own comment for why sharing
     one transform, toggled by the same :hover, is what makes the
     fade/zoom read as one coordinated motion instead of two separate
     effects. */
  transform: scale(1);
  transition: transform 0.6s var(--ease-smooth);
}
/* Optional lifestyle/editorial photo (only rendered when a product has
   one -- see product.js's own comment), full-bleed over the plain
   product shot on hover. No contain/darken here: unlike the studio
   shot, this photo already fills its own frame with a real background,
   so cover (not contain) and no blend mode is the correct treatment. */
/* .full-sets__card-media .full-sets__card-media-hover (2 classes), not
   the bare class alone -- needs to out-specificity the earlier
   ".full-sets__card-media img" (1 class + 1 type is already more
   specific than a single class) to actually override its
   max-width/object-fit/mix-blend-mode. */
.full-sets__card-media .full-sets__card-media-hover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  mix-blend-mode: normal;
  opacity: 0;
  transition: opacity 0.6s var(--ease-smooth), transform 0.6s var(--ease-smooth);
}
/* Hover: the lifestyle photo fades in while both it and the (now
   hidden-underneath) base photo scale up together -- reads as "zooming
   in" as the new image reveals. Un-hover reverses both at once: the
   lifestyle photo fades out while everything scales back down, so the
   base photo appears to "zoom back out" into its resting size as it's
   uncovered again -- one coordinated motion, not two independent fades. */
.full-sets__card-wrap:hover .full-sets__card-media-hover { opacity: 1; }
.full-sets__card-wrap:hover .full-sets__card-media img { transform: scale(1.08); }

/* Spotlight variant (see createShowcaseCard's own comment in main.js) --
   the lifestyle photo as a permanent full-bleed background instead of a
   hover reveal, with the plain studio shot floating over it in its own
   small white tile near the bottom-left. Positioned there specifically
   to clear the jewelry itself in the reference lifestyle photos (worn on
   a raised wrist/hand, roughly centered) rather than covering it. */
.full-sets__card-media--spotlight .full-sets__card-bg-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform 0.6s var(--ease-smooth);
}
.full-sets__card-media--spotlight .full-sets__card-product-img {
  position: absolute;
  left: 14px;
  bottom: 14px;
  width: 48%;
  aspect-ratio: 1 / 1;
  max-width: none;
  max-height: none;
  object-fit: contain;
  background: #f8f8f8;
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
  padding: 10px;
  /* Overrides the darken blend the base ".full-sets__card-media img" rule
     sets for the plain single-photo card above -- this tile sits on its
     own opaque white background, not blended against the photographic
     scene behind it, so darken would just make it fade into the scene's
     own tones instead of reading as a solid floating card. */
  mix-blend-mode: normal;
}
.full-sets__card-wrap:hover .full-sets__card-media--spotlight .full-sets__card-bg-img { transform: scale(1.08); }
.full-sets__card-info {
  margin-top: 12px;
  padding: 0 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.full-sets__card-name {
  font-family: var(--font-sans);
  font-size: 18px;
}
.full-sets__card-carat {
  font-size: 13px;
  color: var(--color-ink-soft);
}
.full-sets__card-color {
  width: 30px;
  height: 12px;
  border-radius: 2px;
  border: 1px solid var(--color-border);
}

/* Bare chevrons, hidden until the card is hovered -- hugging the card's
   own left/right edges rather than sitting inside it. */
.full-sets__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  color: var(--color-ink);
  padding: 8px;
  opacity: 0;
  transition: opacity 0.25s ease, color 0.2s ease, transform 0.2s ease;
}
.full-sets__card-wrap:hover .full-sets__arrow { opacity: 1; }
.full-sets__arrow:hover { color: var(--color-accent); transform: translateY(-50%) scale(1.15); }
.full-sets__arrow svg { width: 22px; height: 22px; display: block; }
.full-sets__arrow--prev { left: -34px; }
.full-sets__arrow--next { right: -34px; }

/* Now lives inside the card itself, between the media and the info block
   -- a normal flex-column participant (not absolutely positioned), so it
   always occupies its own real height even at opacity:0. That's what
   keeps the info block below it from jumping when the button fades in
   on hover; unlike the old outside-the-card version, no reserved-space
   hack is needed here. */
.full-sets__view-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  margin: 12px 16px 0;
  background: var(--color-bg);
  color: var(--color-ink);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: capitalize;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.3s ease, transform 0.3s ease, background 0.2s, color 0.2s;
  pointer-events: none;
}
.full-sets__card-wrap:hover .full-sets__view-btn {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.full-sets__view-btn:hover { background: var(--color-ink); color: var(--color-bg); }

.full-sets__progress {
  width: 50%;
  margin: 40px auto 0;
  height: 2px;
  background: var(--color-border);
  border-radius: 2px;
  overflow: hidden;
}
.full-sets__progress-fill {
  height: 100%;
  width: 0%;
  background: var(--color-ink);
  transition: width 0.4s var(--ease-smooth);
}

@media (max-width: 960px) {
  .full-sets {
    grid-template-columns: 1fr;
  }
  .full-sets__image { height: 60vh; }
  .full-sets__panel { padding: 50px var(--gutter) 0; }
  .full-sets__card-wrap { width: 100%; max-width: 380px; margin: 0 auto; }
  .full-sets__arrow { display: none; }
}

/* ─── BRIDAL COLLECTION ────────────────────────────────────
   A horizontally-scrolling row of large (800x800) product
   photos, each with a frosted-glass info widget pinned to its
   top-left corner and a short customer-review blurb sitting
   below the photo. Built on native scroll-snap (not a custom
   transform-driven engine like the reel carousel) since this
   one doesn't need infinite looping or eased momentum -- just
   plain, robust paging through however many bridal-tagged
   products exist. */
.bridal-carousel {
  position: relative;
  margin-top: 40px;
  padding-inline: var(--gutter);
}
.bridal-carousel__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.bridal-carousel__viewport::-webkit-scrollbar { display: none; }
.bridal-carousel__track {
  display: flex;
  gap: 8px;
  /* "safe" falls back to start-alignment once the items overflow the
     viewport -- plain "center" would still try to center overflowing
     content, permanently clipping whatever spills past the scroll
     container's unreachable (negative) start edge, since scrollLeft
     can never go below 0. Confirmed this really happened with 2 items
     at 800px each in a ~1520px viewport: ~40px of the first item's
     review text and stars were clipped and unreachable by scrolling. */
  justify-content: safe center;
}

.bridal-item {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
}
.bridal-item__image {
  position: relative;
  width: 800px;
  max-width: 90vw;
  aspect-ratio: 1 / 1;
  background: var(--color-surface);
  overflow: hidden;
  /* Entrance (js/bridal.js): same top-to-bottom clip + zoom-settle
     recipe as .full-sets__image, staggered per item (transition-delay
     set inline, 0.5s per box, left to right) instead of all at once. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1s var(--ease-hop);
}
.bridal-item__image.is-visible { clip-path: inset(0 0 0 0); }
.bridal-item__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.15);
  transition: transform 1.3s var(--ease-glide);
}
.bridal-item__image.is-visible img { transform: scale(1); }

/* Frosted glass, pinned 24px from the image's own top-left corner --
   always exactly half the (position:relative) image's own width, so it
   scales with the image rather than snapping between fixed breakpoints. */
.bridal-item__widget {
  position: absolute;
  top: 24px;
  left: 24px;
  width: 50%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #c6c6c633;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid #d4d4d4;
  border-radius: 0;
  padding: .8rem 1.6rem .8rem .8rem;
  /* Entrance: blurs into focus while fading in (its own `filter`, not
     the permanent frosted-glass `backdrop-filter` above) -- delay set
     inline by js/bridal.js, matching its image box's own stagger. */
  opacity: 0;
  filter: blur(12px);
  transition: opacity 0.6s var(--ease-smooth), filter 0.6s var(--ease-smooth);
}
.bridal-item__widget.is-visible { opacity: 1; filter: blur(0); }
/* Groups the thumbnail+name+swatch together on the left so
   justify-content:space-between on the widget only pushes the View
   Details button to the far right, not all three children apart. */
.bridal-item__widget-main {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.bridal-item__thumb {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  background: var(--color-bg);
  overflow: hidden;
}
.bridal-item__thumb img { width: 100%; height: 100%; object-fit: cover; }
.bridal-item__info {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.bridal-item__name {
  font-family: var(--font-display);
  font-size: 15px;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bridal-item__color {
  width: 30px;
  height: 12px;
  border-radius: 2px;
  border: 1px solid var(--color-border);
}
.bridal-item__view {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  color: var(--color-ink);
  transition: color 0.2s;
}
.bridal-item__view:hover { color: var(--color-accent); }
.bridal-item__view svg { width: 16px; height: 16px; }

/* Bare chevrons, vertically centered on the fixed-height image row --
   hidden below 960px along with the rest of this site's carousel arrows. */
.bridal-carousel__arrow {
  position: absolute;
  top: 400px;
  transform: translateY(-50%);
  z-index: 3;
  color: var(--color-ink);
  padding: 8px;
  transition: color 0.2s, transform 0.2s;
}
.bridal-carousel__arrow:hover { color: var(--color-accent); }
.bridal-carousel__arrow svg { width: 26px; height: 26px; display: block; }
.bridal-carousel__arrow--prev { left: calc(var(--gutter) - 10px); }
.bridal-carousel__arrow--next { right: calc(var(--gutter) - 10px); }

@media (max-width: 960px) {
  .bridal-carousel__arrow { display: none; }
}

/* ─── HOMEPAGE SHOWCASE GRID ───────────────────────────────
   Reuses the exact same .bridal-item card (image + frosted-
   glass widget with thumb/name/color swatch/view button) built
   for the Bridal Collection carousel -- just laid out here as a
   2-column, 2-row grid instead of a horizontal scroll-snap
   carousel, and without the review blurb underneath (that's
   specific to Bridal's own testimonial content, not generic
   product listings). Update: superseded the .bridal-item card
   with the .full-sets__card one instead (see the "ESSENTIALS"
   block below). Update 2: cards must be the *same size* as
   Full Sets' own (380px), not stretched to fill a wide column --
   fixed 380px grid tracks (not 1fr) is what keeps them
   identical, and .essentials__panel's own width is sized around
   exactly 3 of these columns (see that block). */
.showcase-grid {
  display: grid;
  /* minmax(0, 380px), not a bare 380px -- lets tracks shrink together if
     the viewport (or .essentials__panel's own capped width) is ever
     narrower than 3 full 380px cards + gaps, instead of overflowing. */
  grid-template-columns: repeat(3, minmax(0, 380px));
  justify-content: center;
  gap: 48px 32px;
}

@media (max-width: 960px) {
  .showcase-grid { grid-template-columns: repeat(2, minmax(0, 380px)); }
}
@media (max-width: 640px) {
  .showcase-grid { grid-template-columns: minmax(0, 380px); }
}

/* A small white "tag" that can be placed over any full-sets-style card
   (e.g. "Signature") -- optional, only rendered when a card is given
   one. Not part of the original Full Sets carousel card at all; added
   here specifically for showcase-grid usage. */
.full-sets__card-badge {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 2;
  background: #fff;
  color: #000;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: capitalize;
  padding: 5px 10px;
  /* The card media is a white studio photo on a near-white (#f8f8f8)
     card -- a plain white badge with no edge would be nearly invisible
     wherever it lands, hence a real shadow (not just a border, which
     could still disappear against a light corner of the photo). */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}

/* ─── ESSENTIALS (fixed-background reveal) ─────────────────
   The homepage Featured section: a full-bleed photo that stays
   pinned to the viewport while a white content panel (title,
   description, the showcase grid, the Discover button) scrolls
   up over it from below -- like CSS background-attachment:fixed,
   but computed in JS (see initEssentialsParallax() in main.js)
   because a *real* background-attachment:fixed would be scoped
   to the nearest transformed ancestor, not the true viewport,
   the moment it's forced to live inside #smoothWrapper's own
   constant transform (same root cause as collections-scroll's
   sticky-pin conflict, same fix shape: read this element's own
   getBoundingClientRect() every animation frame and counter-
   transform it back to a fixed screen position). The section's
   own padding-top/padding-bottom is what controls how much of
   the photo is revealed before the panel begins and after it
   ends; overflow:hidden on the section clips the (otherwise
   unbounded) counter-transform to those bounds. */
.essentials {
  position: relative;
  overflow: hidden;
  padding-top: 90vh;
  padding-bottom: 35vh;
}
.essentials__bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  z-index: 0;
}
.essentials__bg img,
.essentials__bg picture { width: 100%; height: 100%; }
.essentials__bg img { object-fit: cover; }

/* Width is deliberately NOT full-bleed -- sized to exactly fit 3 of the
   showcase grid's own fixed-380px cards (2 gaps between them) plus a
   flat 50px of padding on each side, per the reference: the panel reads
   as a bounded white "card" floating over the fixed photo, which stays
   visible on its left/right edges too, not just above and below it. */
.essentials__panel {
  position: relative;
  z-index: 1;
  background: var(--color-bg);
  max-width: calc(380px * 3 + 32px * 2 + 50px * 2);
  margin-inline: auto;
  padding: 90px 50px;
}
/* The sitewide .container's own gutter-based inset would double up with
   the padding already applied above -- inside this panel it just needs
   to fill the already-padded box. */
.essentials__panel .container {
  width: 100%;
}
.essentials .section__title em { font-style: italic; }

@media (max-width: 960px) {
  /* The fixed-background illusion (and the large reveal padding it
     needs) is a desktop-only flourish -- disabled here in favor of a
     plain static image up top, matching how this site's other scroll-
     linked effects (chevron carousels, etc.) also fall back to
     something simpler on small screens. */
  .essentials {
    padding-top: 0;
    padding-bottom: 0;
  }
  .essentials__bg {
    position: static;
    height: 60vh;
  }
  .essentials__panel {
    max-width: none;
    padding-block: 50px;
    padding-inline: 0;
  }
  .essentials__panel .container {
    width: calc(100% - (var(--gutter) * 2));
  }
}

/* Dark by default, inverts to a plain white background with black
   typography on hover -- a literal color swap, not the sitewide accent
   color .btn--primary:hover uses. */
.btn-discover {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 16px 40px;
  background: var(--color-ink);
  color: var(--color-bg);
  /* Transparent (not absent) so the border hover-in below doesn't add
     2px to the button's box and shift surrounding layout. */
  border: 1px solid transparent;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* See .btn's own comment on why this matters for flip-lettered text. */
  line-height: 1;
  transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
}
.btn-discover:hover {
  background: #fff;
  color: #000;
  border-color: #000;
}

/* ─── PRODUCT GRID / CARDS ────────────────────────────── */
/* Card design (3:4 off-white card, mix-blend-mode-darkened photo,
   hover-revealed "View Details" button) matches .full-sets__card, but
   sizing is its own thing here: a max of 3 fluid columns that grow/
   shrink to fill the row (not a fixed 380px), stepping down to 2 then 1
   on smaller screens, with a tight 8px gap both directions. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
/* Catalog page's View toggle (3/4/5) bumps the column count above the
   default 3 -- higher-specificity than the media queries below on
   purpose, so those still win and collapse everything down on small
   screens regardless of which density was chosen. */
.product-grid.product-grid--cols-4 { grid-template-columns: repeat(4, 1fr); }
.product-grid.product-grid--cols-5 { grid-template-columns: repeat(5, 1fr); }
@media (max-width: 960px) {
  .product-grid,
  .product-grid.product-grid--cols-4,
  .product-grid.product-grid--cols-5 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .product-grid,
  .product-grid.product-grid--cols-4,
  .product-grid.product-grid--cols-5 { grid-template-columns: 1fr; }
}
.product-card {
  position: relative;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  background: #f8f8f8;
  isolation: isolate;
  border-radius: var(--radius-sm);
  aspect-ratio: 3 / 4;
}
.product-card__media {
  position: relative;
  overflow: hidden;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}
.product-card__media img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  mix-blend-mode: darken;
  transform: scale(1);
  transition: transform 0.6s var(--ease-smooth);
}
/* See .full-sets__card-media-hover's own comment (same technique,
   same reason for the 2-class selector, and the same coordinated
   fade+zoom on hover/un-hover). */
.product-card__media .product-card__media-hover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
  mix-blend-mode: normal;
  opacity: 0;
  transition: opacity 0.6s var(--ease-smooth), transform 0.6s var(--ease-smooth);
}
.product-card:hover .product-card__media-hover { opacity: 1; }
.product-card:hover .product-card__media img { transform: scale(1.08); }
.product-card__view-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  margin: 12px 16px 0;
  background: var(--color-bg);
  color: var(--color-ink);
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: capitalize;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.3s ease, transform 0.3s ease, background 0.2s, color 0.2s;
  pointer-events: none;
}
.product-card:hover .product-card__view-btn {
  opacity: 1;
  transform: translateY(0);
}
.product-card__info {
  margin-top: 12px;
  padding: 0 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.product-card__category {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
}
.product-card__name {
  font-family: var(--font-sans);
  font-size: 18px;
  font-weight: 500;
}
.product-card__price {
  font-size: 14px;
  color: var(--color-ink-soft);
}

.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: var(--color-ink-soft);
}

/* ─── REEL CAROUSEL ───────────────────────────────────────
   Vertical (9:16) video carousel for YouTube/Vimeo reels.
   Slides are absolutely positioned (not native scroll) so the
   visible window always holds a whole number of full-size
   slides -- no cropped/partial slide ever peeks at the edge --
   and indices wrap, so it loops infinitely in both directions.
   --reel-item-width is set from JS (viewport-relative, so the
   active card is ~19% of the viewport width on any screen). */
.reel-carousel { position: relative; }

.reel-carousel__viewport {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.reel-slide {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--reel-item-width);
  aspect-ratio: 9 / 16;
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Dark regardless of the site's own theme -- these host video content
     (dark player chrome by convention), and a dark placeholder also keeps
     an unloaded poster visible as "a card is here" against a light page,
     the same way a light --color-surface card would on a dark page. */
  background: #1a1a1a;
  cursor: pointer;
  /* Position + scale together are JS-driven via a single `transform:
     translateX() scale()` (see reels.js's setSlideTransform) -- animating
     `left` forces a layout recalc on every frame for every moving slide,
     which is what made this feel heavy; transform is compositor-only. */
  transition: transform 0.85s var(--ease-carousel);
  will-change: transform;
}
.reel-slide.is-active { cursor: default; }

/* Inactive slides dim via a dark scrim overlay rather than element opacity
   -- opacity blends toward whatever the page background is (looked moody
   dimming to black on a dark page, but washes out to a pale, faded look
   dimming to white on this light one); a fixed dark scrim looks the same
   regardless of the site's own theme. */
.reel-slide::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  background: rgba(0, 0, 0, 0.4);
  opacity: 1;
  pointer-events: none;
  transition: opacity 0.85s var(--ease-carousel);
}
.reel-slide.is-active::after { opacity: 0; }

.reel-slide__poster {
  position: absolute;
  inset: 0;
  z-index: 1;
  transition: opacity 0.3s ease;
}
.reel-slide__poster img { width: 100%; height: 100%; object-fit: cover; }
.reel-slide__poster.is-hidden { opacity: 0; pointer-events: none; }

.reel-slide__play {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 54px; height: 54px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
}
.reel-slide__play svg { width: 20px; height: 20px; margin-left: 3px; }

.reel-slide__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: none;
  z-index: 0;
}

.reel-slide__sound {
  position: absolute;
  top: 12px; right: 12px;
  z-index: 3;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.reel-slide__sound svg { width: 15px; height: 15px; }

/* Bare chevrons (no circular button chrome), hugging the active slide's
   own edges rather than the carousel's outer edges -- their position is
   derived from --reel-item-width so they track the active card at any
   screen size. Never disabled: the carousel loops infinitely. */
.reel-carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  color: var(--color-ink);
  padding: 8px;
  transition: color 0.2s, transform 0.2s;
}
.reel-carousel__arrow:hover { color: var(--color-accent); transform: translateY(-50%) scale(1.15); }
.reel-carousel__arrow svg { width: 28px; height: 28px; display: block; }
.reel-carousel__arrow--prev { left: calc(50% - (var(--reel-item-width) / 2) - 40px); }
.reel-carousel__arrow--next { right: calc(50% - (var(--reel-item-width) / 2) - 40px); }

.reel-carousel__dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 26px;
}
.reel-dot {
  width: 8px; height: 8px;
  border-radius: 999px;
  background: var(--color-ink-faint);
  transition: width 0.3s var(--ease-smooth), background 0.3s;
}
.reel-dot.is-active { width: 22px; background: var(--color-accent); }

@media (max-width: 480px) {
  .reel-carousel__arrow { display: none; }
}

/* ─── SHOP LAYOUT ─────────────────────────────────────────
   catalog.html's sidebar (Category/Tag checkboxes with counts,
   sourced straight from /api/categories and /api/tags'
   `productsCount`) + toolbar (item count, Sort By, column-
   density View toggle). Availability/Price/Color sections from
   the reference design were dropped -- this catalog has no
   stock concept, and price/color are only set on a handful of
   products -- leaving Category + Tag as the real filterable
   facets, so the reference's sidebar pattern is repurposed
   around those instead. */
.shop-layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 48px;
  align-items: start;
}
.shop-sidebar {
  /* Deliberately not `position: sticky` -- #smoothWrapper (this site's
     inertia-scroll wrapper) is `position: fixed` with its own JS-driven
     transform standing in for real scroll movement, and native sticky
     positioning on a descendant stacks with that transform instead of
     being replaced by it, so the sidebar would drift out of place as
     the page "scrolls". A two-section filter list scrolling normally
     with the page is a fine tradeoff to avoid that whole class of bug. */
  display: flex;
  flex-direction: column;
  gap: 32px;
}
.shop-sidebar__heading {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin-bottom: 16px;
}
.shop-sidebar__close { display: none; }
.shop-filter-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.shop-filter-item {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  cursor: pointer;
}
.shop-filter-item--child { padding-left: 20px; }
.shop-filter-item input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--color-ink);
  cursor: pointer;
}
.shop-filter-item__count {
  margin-left: auto;
  font-size: 12px;
  color: var(--color-ink-faint);
}
.shop-sidebar__clear {
  align-self: flex-start;
  font-size: 12px;
  color: var(--color-ink-faint);
  text-decoration: underline;
}
.shop-sidebar__clear:hover { color: var(--color-ink); }

.shop-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 24px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--color-border);
}
.shop-toolbar__left {
  display: flex;
  align-items: center;
  gap: 10px;
}
.shop-toolbar__filter-btn {
  display: none; /* only shown on the mobile drawer layout, see below */
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.shop-toolbar__count {
  font-size: 13px;
  color: var(--color-ink-faint);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.shop-toolbar__right {
  display: flex;
  align-items: center;
  gap: 24px;
  margin-left: auto;
}
.shop-sort__label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  margin-right: 10px;
}
.shop-sort { display: flex; align-items: center; }
.shop-sort .custom-select__trigger { min-width: 150px; }

.shop-view { display: flex; gap: 4px; }
.shop-view__btn {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--color-ink-faint);
}
.shop-view__btn:hover { color: var(--color-ink); }
.shop-view__btn.is-active {
  color: var(--color-ink);
  border-color: var(--color-border);
}

.shop-sidebar__backdrop {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 89;
  background: rgba(0, 0, 0, 0.4);
}

@media (max-width: 960px) {
  .shop-layout { grid-template-columns: 1fr; }
  .shop-toolbar__filter-btn { display: flex; }
  .shop-sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(320px, 82vw);
    z-index: 90;
    background: var(--color-bg);
    padding: 28px 24px;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 0.3s var(--ease-smooth);
  }
  .shop-sidebar.is-open { transform: translateX(0); }
  .shop-sidebar__close {
    display: block;
    margin-bottom: 20px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .shop-sidebar__backdrop.is-open { display: block; }
}

.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 40px;
}
.tag-chip {
  padding: 8px 18px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  font-size: 13px;
  font-weight: 500;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.tag-chip:hover, .tag-chip.is-active {
  background: var(--color-ink);
  color: var(--color-bg);
  border-color: var(--color-ink);
}

/* ─── CUSTOM SELECT ───────────────────────────────────── */
.form-select { display: none; }
.custom-select { position: relative; }
.custom-select__trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 18px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  min-width: 170px;
  justify-content: space-between;
  background: var(--color-surface);
}
.custom-select__trigger::after {
  content: '';
  width: 8px; height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform 0.2s;
}
.custom-select.is-open .custom-select__trigger::after { transform: rotate(-135deg) translateY(2px); }
.custom-select__value.is-placeholder { color: var(--color-ink-faint); }
.custom-select__panel {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  min-width: 100%;
  background: var(--color-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: 8px;
  z-index: 20;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 0.18s, transform 0.18s;
  max-height: 280px;
  overflow-y: auto;
}
.custom-select.is-open .custom-select__panel {
  opacity: 1; transform: translateY(0); pointer-events: auto;
}
.custom-select__option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  white-space: nowrap;
}
.custom-select__option:hover { background: var(--color-hover); }
.custom-select__option.is-selected { color: var(--color-accent); font-weight: 600; }

/* ─── PRODUCT DETAIL ──────────────────────────────────── */
.breadcrumb {
  font-size: 12px;
  color: #1c1b1a;
  margin-bottom: 30px;
}
.breadcrumb a:hover { color: var(--color-accent); }
.breadcrumb__sep { margin: 0 6px; }

/* Widths/gap match a reference layout exactly: media flex-basis/max-
   width both 70%, details flex:0 0 auto at calc(100% - 70% - gap/2),
   with a 35rem floor -- the parent's real 70px gap plus a 70%+30%
   split would overflow by exactly one gap's worth, so the reference
   pre-shrinks the details column by half the gap and leaves the media
   column's flex-shrink:1 to absorb the rest automatically. */
.product-hero {
  display: flex;
  gap: 70px;
  margin-bottom: 90px;
}
.product-hero__media {
  flex: 1 1 70%;
  max-width: 70%;
  min-width: 0;
  /* All gallery images shown at once (no click-to-swap main+thumbs
     anymore) -- a plain 2-up grid, 12px both directions. */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  align-content: start;
}
.product-hero__gallery-item {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: #f8f8f8;
  /* Scopes mix-blend-mode below to this box's own background, not
     whatever sits behind the whole page -- same reason .product-card
     needs it. */
  isolation: isolate;
}
/* Plain studio shots (the main photo + everything in the uploaded
   gallery except the dedicated lifestyle photo) get the same contain +
   darken-blend treatment as the small card thumbnails, so the white
   backdrop seamlessly matches this box's own #f8f8f8 instead of
   showing a hard-edged white rectangle. No border-radius, per the
   reference. */
.product-hero__gallery-item img { display: block; width: 100%; height: 100%; object-fit: contain; mix-blend-mode: darken; }
/* The lifestyle/editorial photo is the one exception -- it already
   fills its own frame with a real (non-white) background, so cover +
   no blend mode is the correct treatment, same as the product card's
   hover image. Letterboxing or darken-blending it would either show an
   odd gray margin around an on-model photo or dull its highlights for
   no benefit. */
.product-hero__gallery-item--lifestyle img { object-fit: cover; mix-blend-mode: normal; }
/* Desktop keeps the plain grid above; the swipeable single-image
   carousel + this thumbnail strip only exist below 960px -- see that
   media query for the rest of this component's rules. */
.product-hero__mobile-thumbs { display: none; }
.product-hero__info {
  flex: 0 0 auto;
  width: calc(100% - 70% - 35px);
  min-width: 35rem;
  padding: 0 100px;
}

.product-hero__category {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 12px;
}
.product-hero__name {
  font-family: var(--font-sans);
  font-size: 34px;
  color: #1c1b1a;
  margin-bottom: 14px;
}
.product-hero__price { font-size: 14px; color: var(--color-ink-soft); margin-bottom: 22px; }
.product-hero__desc {
  font-family: var(--font-sans);
  font-size: 14px;
  color: #1c1b1a;
  margin-bottom: 24px;
}

.product-specs {
  list-style: none;
  border-top: 1px solid var(--color-border);
  margin-bottom: 28px;
}
.product-specs li {
  display: flex;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid var(--color-border);
  font-family: var(--font-sans);
  /* Base size for both spans in the row -- the value (unlabeled span)
     needs exactly this ("tab info": 14px), and .spec-label below only
     overrides font-size ("tab title": 16px), inheriting this same
     family/color rather than repeating them. */
  font-size: 14px;
  color: #1c1b1a;
}
.product-specs .spec-label { font-size: 16px; }

.product-hero__tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 28px; }
.product-hero__actions { display: flex; gap: 14px; flex-wrap: wrap; }
/* Matches .btn-discover's own box (the Featured section's "Discover All
   Designs" button) -- square corners, wider horizontal padding --
   instead of the sitewide pill shape, while keeping each button's own
   color (WhatsApp green / outline) via the existing .btn--whatsapp/
   .btn--outline modifiers. */
.product-hero__actions .btn {
  border-radius: 0;
  padding: 16px 40px;
}

/* ─── RELATED ──────────────────────────────────────────── */
.related__title { font-size: 24px; margin-bottom: 30px; }

/* ─── CONTACT ─────────────────────────────────────────── */
/* Section 1 of the redesigned contact page: a left info column
   (WhatsApp/Email/Hours) beside a right form column, per an explicit
   spec -- 70px top padding, 90px bottom padding (both shared across the
   whole grid row so the two columns start/end flush with each other,
   not just the form column), a fully edge-to-edge divider closing the
   section out (deliberately NOT a contained .page-wrap divider like the
   About page's -- asked for full width here specifically). */
.contact-form-section { padding: 0; }
.contact-form-section__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  /* padding-top/bottom only -- a `padding` shorthand here would zero out
     .page-wrap's own padding-inline (left/right) since this rule is
     declared later in the cascade and shorthand always sets all four
     sides, even the ones you don't mean to touch. */
  padding-top: 70px;
  padding-bottom: 90px;
}
.contact-form-section__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(32px, 4vw, 48px);
  margin: 10px 0 12px;
}
.contact-form-section__desc { color: var(--color-ink-soft); margin-bottom: 36px; }
.contact-form-section__divider {
  width: 100%;
  margin: 0;
  border: none;
  border-top: 1px solid var(--color-border);
}

.contact-info__item { margin-bottom: 28px; }
.contact-info__label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  margin-bottom: 6px;
}
.contact-info__value { font-size: 16px; }
.contact-info__value a:hover { color: var(--color-accent); }

/* Name+Email share a row; every other field spans the full width --
   .form-group--full is the span-both-columns override. 16px gap both
   directions per spec (CSS grid's single `gap` already covers both). */
.contact-form-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
.contact-form-fields .form-group--full { grid-column: 1 / -1; }

.form-group { margin-bottom: 0; }
.form-label { /* kept for a11y -- visually-hidden in the markup, see contact.html */ }
.form-input, .form-textarea, .contact-select {
  width: 100%;
  padding: 13px 16px;
  border: none;
  border-radius: var(--radius-sm);
  background: #F1F1F1;
  font-size: 14px;
  color: var(--color-ink);
  transition: box-shadow 0.2s;
}
.form-input:focus, .form-textarea:focus, .contact-select:focus {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-accent);
}
.form-input::placeholder, .form-textarea::placeholder { color: var(--color-ink-faint); }
.form-textarea { display: block; min-height: 130px; resize: vertical; } /* textarea defaults to inline-block, which leaves a few px of baseline slop below it */
/* Named .contact-select, not .form-select -- that name is already taken
   sitewide by the custom-select.js pattern (a native <select> it hides
   and replaces with a styled panel, e.g. the catalog's Sort By dropdown)
   and would have been hidden with nothing to show it, since this select
   is plain CSS with no JS enhancement behind it. */
.contact-select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23847F78' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 40px;
  color: var(--color-ink);
}
.contact-select:invalid { color: var(--color-ink-faint); } /* the disabled placeholder option */

.contact-form-section__submit {
  display: block;
  margin: 24px auto 0;
  border-radius: var(--radius-sm);
}

.form-note {
  font-size: 12px;
  color: var(--color-ink-faint);
  margin-top: 12px;
}
.form-status {
  margin-top: 16px;
  font-size: 14px;
  display: none;
}
.form-status.is-visible { display: block; }
.form-status--success { color: #2f7a3d; }
.form-status--error { color: #b5432f; }

.contact-prefill-note {
  background: var(--color-surface);
  border-radius: var(--radius-md);
  padding: 14px 18px;
  font-size: 13px;
  margin-bottom: 24px;
  display: none;
}
.contact-prefill-note.is-visible { display: block; }

/* ─── OUR STORY ─────────────────────────────────────────
   Full-bleed photo hero (same shape as the homepage .hero: a
   fixed-height image with the transparent navbar floating over
   it, see navbar--transparent on our-story.html's <nav>) with
   the title flush to the bottom-left corner. */
.story-hero {
  position: relative;
  height: 640px;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  color: #fff;
}
.story-hero__bg {
  position: absolute;
  inset: 0;
  /* Explicit white backdrop -- without it, the browser's own gray
     image-decode placeholder can show through while the photo is still
     loading/decoding and the clip-path below hasn't opened yet. */
  background: var(--color-bg);
  /* Same clip (top-to-bottom) + zoom-settle recipe as .full-sets__image
     and the Bridal image boxes -- see js/our-story.js. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1s var(--ease-hop);
}
.story-hero__bg.is-visible { clip-path: inset(0 0 0 0); }
.story-hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.15);
  transition: transform 1.3s var(--ease-glide);
}
.story-hero__bg.is-visible img { transform: scale(1); }
.story-hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(20, 18, 16, 0.55) 0%, rgba(20, 18, 16, 0.1) 40%, rgba(20, 18, 16, 0) 65%);
}
.story-hero__title {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(40px, 6vw, 72px);
  width: 100%;
  padding-bottom: 60px;
  margin: 0;
  color: #fff;
}

/* The Philosophy section: a left eyebrow label beside a large
   serif statement on the right, per an explicit reference/spec
   (100px 50px 120px padding, exact figures given directly rather
   than derived from the sitewide 120px inter-section rhythm --
   same kind of deliberate one-off as .bridal + .essentials
   above). Horizontal spacing lives on .page-wrap (see --page-width
   above), not here, so it lines up exactly with .story-hero__title
   above it -- .container's own %-based --gutter would drift out of
   alignment with a fixed-width capped block like this one. The
   divider below it is deliberately NOT full-bleed (unlike the
   reference it was modeled on) -- contained to the same .page-wrap. */
/* No bottom padding here -- the 50px gap down to the first .mission-block
   is that block's own top padding (see below), since the same 50px unit
   repeats between every block/divider pair that follows. */
.philosophy { padding: 100px 0 0; }
.philosophy__grid {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0; /* per reference: its .gap-style utility resolves to 0 at every breakpoint here */
}
.philosophy__text {
  width: var(--story-media-width);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(26px, 3vw, 42px);
  line-height: 1.3;
}
.philosophy__text em { font-style: italic; }
.philosophy__divider {
  /* NOT .page-wrap's own padding-inline -- padding never insets a bare
     border (it sits at the box's outer edge regardless of padding), so
     a content-less <hr> needs its box literally narrowed by width/
     max-width instead, to actually line up with the padded content
     around it. */
  width: calc(100% - var(--page-padding) * 2);
  max-width: var(--page-width);
  margin-top: 60px;
  margin-inline: auto;
  border: none;
  border-top: 1px solid var(--color-border);
}

@media (max-width: 768px) {
  .story-hero { height: 460px; }
  .philosophy { padding: 70px 0 0; }
  .philosophy__grid { flex-direction: column; gap: 20px; }
  .philosophy__text { width: 100%; } /* the 50% width is a desktop-two-column thing only */
}

/* Three duplicated blocks (Mission / Vision / Values) right after the
   Philosophy section, each: 50px down from the divider above it, an
   image-right/text-left grid vertically centered, 50px more down to its
   own trailing divider -- so the 50px unit is this block's own top AND
   bottom padding, and the divider between two blocks carries no margin
   of its own. */
.mission-block { padding: 50px 0; }
.mission-block__grid {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 40px;
}
.mission-block__content { max-width: 400px; }
.mission-block__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 34px;
  color: #1c1b1a;
  margin: 0 0 20px;
}
.mission-block__text {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  color: #1c1b1a;
}
.mission-block__list {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  color: #1c1b1a;
}
/* Fixed 800x600 "on pc" per spec, not fluid -- flex-shrink:0 keeps it at
   that exact size against .mission-block__content's own 400px cap
   rather than the two fighting over flexible space. */
.mission-block__media {
  position: relative;
  width: 800px;
  height: 600px;
  flex-shrink: 0;
  border-radius: 0;
  overflow: hidden;
  background: var(--color-surface);
  /* Same clip (top-to-bottom) + zoom-settle recipe as .full-sets__image
     and the Bridal image boxes -- see js/our-story.js. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1s var(--ease-hop);
}
.mission-block__media.is-visible { clip-path: inset(0 0 0 0); }
/* Parallax layer: 30% taller than its own frame (.mission-block__media,
   which clips it via overflow:hidden and never itself moves), so
   js/our-story.js can slide it a little within that extra headroom as
   the page scrolls -- the background drifting independently of its
   static parent, not the whole card moving. Kept as its own element,
   separate from the <img>, so this translateY (set inline, continuously,
   by JS) never collides with the img's own CSS-driven scale-settle
   transform below. */
.mission-block__parallax {
  position: absolute;
  inset: -15% 0;
  will-change: transform;
}
.mission-block__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.15);
  transition: transform 1.3s var(--ease-glide);
}
.mission-block__media.is-visible img { transform: scale(1); }
/* .hero__title-char settles to gold/white by default (right for the
   hero's dark photo) -- this section's own explicit dark ink instead,
   matching .mission-block__title's own already-declared color exactly
   so there's no visible color mismatch the instant the reveal finishes. */
.mission-block__title .hero__title-char.is-visible { color: #1c1b1a; }
.mission-block__divider {
  /* Same reasoning as .philosophy__divider above -- width/max-width, not
     .page-wrap's padding-inline, is what actually lines a bare border up
     with the padded content around it. */
  width: calc(100% - var(--page-padding) * 2);
  max-width: var(--page-width);
  margin-inline: auto;
  border: none;
  border-top: 1px solid var(--color-border);
}

@media (max-width: 768px) {
  .mission-block { padding: 40px 0; }
  .mission-block__grid { flex-direction: column; gap: 24px; }
  .mission-block__content { max-width: 100%; }
  .mission-block__media { order: -1; width: 100%; height: auto; aspect-ratio: 4 / 3; } /* fixed 800x600 is a desktop-only spec */
  .mission-block__title { font-size: 28px; }
}

/* ─── FAQ ─────────────────────────────────────────────── */
.faq-list { max-width: 780px; margin: 0 auto; }
.faq-item {
  border-bottom: 1px solid var(--color-border);
}
.faq-item__question {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px 0;
  font-family: var(--font-display);
  font-size: 19px;
  text-align: left;
}
.faq-item__icon {
  width: 20px; height: 20px;
  flex-shrink: 0;
  position: relative;
}
.faq-item__icon::before, .faq-item__icon::after {
  content: '';
  position: absolute;
  background: currentColor;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.faq-item__icon::before { width: 14px; height: 1.5px; }
.faq-item__icon::after { width: 1.5px; height: 14px; transition: transform 0.25s var(--ease-smooth); }
.faq-item.is-open .faq-item__icon::after { transform: translate(-50%, -50%) rotate(90deg) scale(0); }

.faq-item__answer {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s var(--ease-smooth);
}
.faq-item__answer p { padding-bottom: 22px; color: var(--color-ink-soft); max-width: 65ch; }

/* ─── INSTAGRAM MARQUEE ────────────────────────────────────
   An admin-managed (not live-API) "Follow us on Instagram" strip
   -- photos alternating with "@HANDLE"/"FOLLOW US" text, auto-
   scrolling in a seamless loop (see js/instagram.js -- the track's
   own content is duplicated once so translateX(-50%) resets
   invisibly). Hovering the strip grays out every photo except the
   one under the cursor -- pure CSS, no JS needed for that part. */
.instagram {
  padding-block: 90px;
}
/* contact.html's own copy of the homepage's Instagram marquee, per an
   explicit spec: 100px above it (from the contact form's divider) and
   120px below it (before the footer) -- 10px/30px on top of the
   component's own 90px padding-block, added as margin rather than
   touching that shared padding so the homepage's usage is untouched. */
.instagram--contact {
  margin-top: 10px;
  margin-bottom: 30px;
}
.instagram__header {
  text-align: center;
  margin-bottom: 60px;
}
.instagram__title {
  font-size: clamp(24px, 3vw, 34px);
  margin-top: 10px;
}
.instagram__title a {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.instagram__viewport {
  overflow: hidden;
  width: 100%;
}
.instagram__track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: instagram-scroll 45s linear infinite;
}
.instagram__track:hover { animation-play-state: paused; }

@keyframes instagram-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Used by .split-menu__gallery-track -- see its own comment for why
   -50% (not -100%) is the loop point. */
@keyframes split-menu-gallery-scroll {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}

.instagram-item {
  flex-shrink: 0;
  display: block;
  width: 240px;
  aspect-ratio: 3 / 4;
  margin-inline: 16px;
  overflow: hidden;
}
.instagram-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(0);
  transition: filter 0.35s ease;
}
/* Rules must be in this order: the second (more specific-by-source-order,
   equal-specificity) rule is what lets the hovered item win the tie and
   stay in color while its siblings gray out. */
.instagram__track:hover .instagram-item img { filter: grayscale(1); }
.instagram__track .instagram-item:hover img { filter: grayscale(0); }

.instagram-label {
  flex-shrink: 0;
  /* Fixed + centered so "@NEG" and "FOLLOW US" occupy the exact same slot
     width -- otherwise the gap between pictures visibly varies with
     whichever word happens to render there, since the words themselves
     are different widths. */
  width: 150px;
  text-align: center;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 20px;
  color: var(--color-ink-soft);
  white-space: nowrap;
}

@media (max-width: 640px) {
  .instagram-item { width: 160px; margin-inline: 10px; }
  .instagram-label { width: 110px; font-size: 16px; }
}

/* ─── FOOTER ──────────────────────────────────────────── */
/* Sticky-bottom reveal footer, per the standard technique used by real
   Shopify themes (this exact recipe -- main content at a higher
   z-index, footer sticky-bottom at a lower one -- was given directly,
   including the reference production values below): #mainContent
   (everything on the page except the footer) sits ABOVE the footer via
   a higher z-index and scrolls normally; .footer-wrapper sticks to the
   bottom of the viewport at a LOWER z-index, so the still-scrolling
   page visually covers it like a curtain until #mainContent's own
   bottom edge passes, uncovering the footer underneath in one clean
   reveal. Both .footer-wrapper's own content (subscribe/links/
   copyright) and .footer-reveal (the wordmark) live INSIDE the same
   sticky element -- confirmed by prototyping in isolation that once a
   combined sticky block like this engages, whatever's visible at that
   instant is frozen for its whole stuck duration (no further internal
   scrolling), so .footer-reveal is deliberately kept short (not a full
   viewport) so the *whole* footer reveals together in one clean
   transition, rather than some of it staying permanently hidden above
   the visible window the way a viewport-tall reveal would.
   .footer-wrapper would normally just be plain CSS position:sticky;
   bottom:0, but this site renders everything inside #smoothWrapper's
   own constant transform (see smooth-scroll.js) -- native sticky's
   "stuck" position would get an extra, constantly-changing offset from
   that transform (the same conflict already solved for collections-
   scroll's title pin), so it's a JS-computed transform instead (see
   initFooterReveal() in footer.js), recomputed every animation frame,
   using #mainContent's own (untransformed-by-JS) rect as the reference
   point for where the footer's "natural" un-stuck position would be.
   Deliberately its own color tokens, not the nav's -- the footer
   always stays dark charcoal regardless of the nav's own state. */
#mainContent {
  position: relative;
  z-index: 10;
  /* Guarantees the sticky-reveal compensation below always has real,
     non-zero scroll distance to draw from -- without this, since
     .footer-wrapper is the very last thing on the page with nothing
     after it, the page's total scroll distance runs out at almost
     exactly the same instant the footer's own natural (unstuck) rest
     position already matches its stuck position (mathematically
     inevitable for a last-element sticky-bottom footer, confirmed by
     measuring it directly: the two came out within ~3px of each other,
     leaving no real room for compensation to ever apply). This 200px
     of otherwise-invisible extra height is what supplies that room. */
  padding-bottom: 200px;
}
.footer-wrapper {
  position: relative;
  z-index: 9;
  /* Matches the reference implementation's own sub-pixel fix for native
     sticky (a 1px gap/flicker right as it engages) -- kept here as the
     correct fallback for when JS isn't driving the transform (e.g.
     prefers-reduced-motion, where #smoothWrapper reverts to static and
     this could act as genuine position:sticky again). */
  bottom: 1px;
  margin-top: -1px;
  transform: translateY(1px);
  background: var(--color-footer-bg);
  color: rgba(250, 249, 246, 0.85);
  border-radius: 0;
  padding: 70px 0 30px;
  /* CHANGE LOG 2026-09-25: the whole footer (this element) is now
     exactly one viewport tall, at the user's request, so it fills the
     entire screen -- "black cinematic" -- once fully revealed. This
     still respects the block comment above: the reveal mechanism
     freezes whatever's visible the instant it engages, with no further
     internal scroll -- but since the total is now precisely one
     viewport, everything is simultaneously visible by construction,
     for any viewport height. min-height (a floor, not a fixed size) is
     the sitewide default so mobile -- where .footer__top's 4 columns
     stack into one and genuinely need more than 100vh -- is always free
     to grow taller instead of clipping; the desktop-only rule below
     upgrades this to a real height specifically where the 50/50 split
     needs it (see that rule's own comment for why). */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
/* was flex-shrink:0 (natural content height, .footer-reveal alone
   stretching to fill the rest) -- now flex:1 on both (with min-height:0,
   overriding flexbox's own default min-height:auto floor), splitting
   the footer exactly in half regardless of .container's real content
   height, per the user's "keep it balanced" request. Centering this
   half's own content vertically (display:flex here too) matters just as
   much as the 50/50 split itself: the real content is much shorter than
   its 400px allocation, and left top-aligned (the default) all that
   leftover space collected as one dead gap underneath it instead of
   framing it evenly the way .footer-reveal's own centered logo already
   is. */
.footer-wrapper > .container {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.footer-reveal {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--color-footer-bg);
}
/* flex-basis:0% (the 50/50 split above) only resolves correctly against
   a flex container with a DEFINITE height -- min-height alone (a floor
   computed from content, not a fixed value) isn't definite enough, so
   browsers fall back to sizing each half by its own content instead of
   splitting evenly (confirmed: .container's real ~459px content stayed
   459px regardless of flex:1 until this rule was added). Scoped to
   desktop, matching initFooterReveal()'s own 961px breakpoint in
   footer.js, since that's the only width where this even needs to be
   exact -- the curtain-reveal is disabled below it, and mobile's
   .footer__top stacks into a single, much taller column that a hard
   50/50 split would clip. */
@media (min-width: 961px) {
  .footer-wrapper { height: 100vh; }
}
.footer-reveal__logo {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(60px, 14vw, 200px);
  line-height: 1;
  color: var(--color-footer-ink);
  letter-spacing: 0.02em;
}
.footer__top {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: 40px;
  padding-bottom: 40px;
}
.footer__blurb {
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 1.4;
  color: var(--color-footer-ink);
  max-width: 34ch;
  margin-bottom: 20px;
}
.footer__subscribe {
  display: flex;
  max-width: 380px;
  border: 1px solid rgba(250, 249, 246, 0.25);
}
.footer__subscribe-input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: none;
  padding: 12px 14px;
  color: var(--color-footer-ink);
  font-size: 14px;
}
.footer__subscribe-input::placeholder { color: rgba(250, 249, 246, 0.5); }
.footer__subscribe-btn {
  flex-shrink: 0;
  width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-footer-ink);
  color: var(--color-footer-bg);
  transition: background 0.2s, color 0.2s;
}
.footer__subscribe-btn:hover { background: var(--color-accent-soft); }
.footer__subscribe-btn svg { width: 16px; height: 16px; }
.footer__subscribe-note {
  font-size: 13px;
  color: rgba(250, 249, 246, 0.7);
  margin-top: 10px;
}
.footer__fineprint {
  font-size: 12px;
  color: rgba(250, 249, 246, 0.5);
  margin-top: 12px;
  max-width: 36ch;
}
.footer__fineprint a { color: rgba(250, 249, 246, 0.7); text-decoration: underline; }
.footer__brand { /* kept for other pages that may still reference it */
  font-family: var(--font-display);
  font-size: 24px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-footer-ink);
  margin-bottom: 14px;
}
.footer__brand span { color: var(--color-accent-soft); }
.footer__text { font-size: 14px; margin-bottom: 8px; color: rgba(250, 249, 246, 0.65); }
.footer__title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-footer-ink);
  margin-bottom: 16px;
}
.footer__link { display: block; font-size: 14px; margin-bottom: 10px; color: rgba(250, 249, 246, 0.75); transition: color 0.2s; }
.footer__link:hover { color: var(--color-accent-soft); }
.footer__socials { display: flex; gap: 10px; margin-top: 20px; }
.footer__social {
  width: 34px; height: 34px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(250, 249, 246, 0.08);
  color: var(--color-footer-ink);
}
.footer__social svg { width: 16px; height: 16px; }
.footer__divider { height: 1px; background: rgba(250, 249, 246, 0.12); }
.footer__bottom {
  padding-top: 24px;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 12px;
  color: rgba(250, 249, 246, 0.5);
}

@media (max-width: 960px) {
  /* The sticky-reveal effect itself is disabled in JS below 960px (see
     footer.js's mq check) -- .footer-wrapper just sits in plain normal
     flow, with the wordmark as a smaller static block within it,
     matching how this site's other scroll-linked effects also simplify
     on small screens. */
  .footer-reveal { height: 22vh; }
}

/* ─── SCROLL REVEAL (JS toggles .is-visible) ─────────── */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-smooth), transform 0.7s var(--ease-smooth);
}
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ─── PAGE TRANSITION (vertical wipe + wordmark, js/page-transition.js) ──
   A fixed, full-viewport row of vertical block tiles sits above
   everything else (even the navbar/split-menu) as a direct child
   of <body>, OUTSIDE #smoothWrapper -- same reasoning as before,
   must never inherit that wrapper's own transform, and needs to
   stay fixed to the true viewport regardless of scroll position.
   Tiles start covering the screen (scaleX(1)) so there's no flash
   of the raw page before the reveal runs. On leave, tiles cover
   from the left edge, a centered "NEG" wordmark wipes into view
   over them, then the browser navigates; on arrival, tiles
   retract toward the right edge to reveal the new page underneath
   (no wordmark on arrival) -- one continuous left-to-right wipe
   across the whole transition, not a symmetric mirror. */
.transition-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  flex-direction: column; /* 4 fat horizontal bands, not side-by-side columns -- see js/page-transition.js's own "BLOCK REVEAL" comment */
  pointer-events: none;
  overflow: hidden;
  /* Hidden whenever idle (the vast majority of the time) -- every block
     sits at scaleX(0) once revealed, mathematically zero width, but GPU
     rasterization of a scaled-to-0 element can still leave a persistent
     sub-pixel anti-aliasing hairline at its collapse point (a known
     scale(0) compositor quirk that `overflow: hidden` alone did not
     clip away in testing). Invisible against the dark footer (same
     color as the blocks) but visible as a thin vertical line over
     anything lighter. visibility:hidden stops the browser from
     painting this element at all while idle, which is the only thing
     that reliably removed the hairline -- js/page-transition.js toggles
     .is-active for the brief window an actual transition is playing. */
  visibility: hidden;
}
.transition-overlay.is-active { visibility: visible; }
.transition-block {
  flex: 1 1 0;
  width: 100%;
  background: var(--color-footer-bg);
  /* Covering by default (no FOUC before JS runs) -- the page-load path
     retracts this to scaleX(0) from the right; the leave path re-covers
     it (scaleX(1) from the left) before navigating. */
  transform: scaleX(1);
  transition: transform 0.5s var(--ease-hop);
}
.transition-logo-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-footer-bg);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease-out;
}
.transition-logo-overlay.is-visible { opacity: 1; }
.transition-logo-text {
  display: inline-block;
  overflow: hidden;
  font-family: var(--font-heading);
  font-size: clamp(48px, 9vw, 130px);
  color: var(--color-footer-ink);
  letter-spacing: 0.04em;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.5s var(--ease-carousel);
}
.transition-logo-overlay.is-visible .transition-logo-text { clip-path: inset(0 0 0 0); }

/* ─── OUR STORES ──────────────────────────────────────── */
/* flex-wrap, not a rigid 2-column grid -- each card is a fixed 800px
   (400+400) block, and how many fit per row depends on the container's
   own width (a %-based --gutter, not a fixed page width), so wrapping
   naturally rather than forcing exactly 2 per row avoids overflow on
   narrower desktop windows. */
.stores-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 40px;
  margin-top: 60px;
}
.store-card {
  display: flex;
}
.store-card__image,
.store-card__info {
  width: 400px;
  height: 500px;
  flex-shrink: 0;
}
.store-card__image {
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--color-surface);
}
.store-card__image img { width: 100%; height: 100%; object-fit: cover; }
.store-card__info {
  background: #f8f8f8;
  /* Text starts top-left: flex-start on both axes (not the previous
     vertical centering), 20px in from the left/right edges. */
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  /* General content gap -- between the name and the first
     branch/contact block, and between branches on the Cairo card.
     Narrower than this specific value: .store-card__contact's own gap
     below, address -> phone. */
  gap: 20px;
  overflow: hidden;
}
.store-card__name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 26px;
  color: #1c1b1a;
}
/* Wraps each address+phone pair so that specific gap (14px) can differ
   from the general content gap (20px) above, which still applies
   between this whole block and whatever comes before it. */
.store-card__contact { display: flex; flex-direction: column; gap: 14px; }
.store-card__address,
.store-card__phone {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  color: #1c1b1a;
}
/* Cairo's own two branches (Obour City + El Gamalia) share one card,
   one city per card as elsewhere -- each gets its own small heading
   above its address/phone. */
.store-card__branch { display: flex; flex-direction: column; gap: 20px; }
.store-card__branch-name {
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #1c1b1a;
}

/* ─── RESPONSIVE ──────────────────────────────────────── */
@media (max-width: 960px) {
  .contact-form-section__grid {
    grid-template-columns: 1fr;
    order: initial;
  }
  .product-hero { flex-direction: column; gap: 30px; }
  .product-hero__media, .product-hero__info {
    max-width: 100%;
    width: 100%;
    min-width: 0;
  }
  /* Swipeable single-image carousel, not the desktop grid -- one
     full-width square slide at a time, finger-scrolled, snapping
     square on each image. */
  .product-hero__media {
    display: flex;
    gap: 0;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
  }
  .product-hero__gallery-item {
    flex: 0 0 100%;
    scroll-snap-align: center;
  }
  .product-hero__mobile-thumbs {
    display: flex;
    gap: 10px;
    margin-top: 12px;
    overflow-x: auto;
  }
  .product-hero__mobile-thumb {
    flex: none;
    width: 56px;
    height: 56px;
    padding: 2px;
    /* Transparent, not absent -- the active frame below adds a real
       border color without shifting the thumbnail's own size/position. */
    border: 1px solid transparent;
    opacity: 0.6;
    transition: opacity 0.2s, border-color 0.2s;
  }
  .product-hero__mobile-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
  /* Gold, matching the sitewide accent -- the "active" frame color this
     component gets to choose (per its own spec, gold or black). */
  .product-hero__mobile-thumb.is-active {
    opacity: 1;
    border-color: var(--color-accent);
  }
  .product-hero__info { padding: 0; }
  .product-hero__name { font-size: 26px; }
  .product-specs .spec-label { font-size: 14px; }
  .contact-form-fields { grid-template-columns: 1fr; }
  .contact-form-fields .form-group--full { grid-column: auto; }
  .footer__top { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 850px) {
  /* The fixed 400px halves don't fit side by side once the viewport
     itself is narrower than roughly two of them plus the page's own
     gutters -- stack instead, at 100% width, still 4:5 on the photo. */
  .store-card { flex-direction: column; width: 100%; max-width: 400px; }
  .store-card__image,
  .store-card__info { width: 100%; height: auto; }
}

@media (max-width: 640px) {
  .footer__top { grid-template-columns: 1fr; }
  .section { padding-block: 60px; }
}
