﻿/* ================================================================
   KOHINOOR ART JEWELLERS â€” Site Stylesheet
   Theme: warm cream / beige / soft purple / gold
   ================================================================ */

:root {
  --cream:        #F6F0E1;
  --cream-soft:   #EFE5D0;
  --beige:        #E2D2B2;
  --beige-deep:   #B49C6E;
  --white:        #FDFAF2;
  --purple:       #532F70;
  --purple-deep:  #371E4E;
  --purple-soft:  #8D6FAC;
  --plum:         #281634;
  --gold:         #8C6920;
  --gold-soft:    #DDBC77;
  --ink:          #221931;
  --ink-soft:     #463B55;

  --font-display: "Cormorant Garamond", "Playfair Display", serif;
  --font-body:    "Jost", "Segoe UI", sans-serif;
  --font-accent:  "Marcellus", serif;
  --font-numeric: "Poppins", "Segoe UI", sans-serif;

  --ease-fluid:   cubic-bezier(0.22, 1, 0.36, 1);
  --shadow-soft:  0 18px 50px -18px rgba(75, 46, 99, 0.22);
  --shadow-card:  0 10px 35px -12px rgba(75, 46, 99, 0.18);
  --radius:       18px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { scroll-behavior: smooth; overflow-x: clip; }

body {
  font-family: var(--font-body);
  background: var(--cream);
  color: var(--ink);
  line-height: 1.7;
  overflow-x: clip;
}

/* The native cursor is only hidden once main.js has actually built the custom
   one, and it adds .has-custom-cursor at that moment. Hiding it unconditionally
   meant that any path where the custom cursor did not initialise — a coarse
   pointer, or the lite/static effect tiers, which skip it — left the visitor
   with no visible pointer at all. Tie the two together so they cannot drift. */
/* input/textarea/select need their own selector, not just inheritance: UA
   stylesheets set cursor:text/auto directly on form controls, which wins over
   an inherited value from body regardless of the body rule's specificity —
   so text boxes kept showing the native text-beam stacked on the custom dot. */
body.has-custom-cursor,
body.has-custom-cursor .btn,
body.has-custom-cursor .pill,
body.has-custom-cursor input,
body.has-custom-cursor textarea,
body.has-custom-cursor select { cursor: none; }

::selection { background: var(--purple-soft); color: var(--white); }

/* height: auto is required now that media() emits width/height attributes.
   Without it, any image that max-width scales down keeps its attribute height
   and comes out squashed. More specific rules (.ph-photo, .social-icon, ...)
   set their own height and are unaffected. */
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; color: var(--purple-deep); line-height: 1.15; }
h1 { font-size: clamp(2.6rem, 6vw, 5rem); }
h2 { font-size: clamp(2rem, 4vw, 3.2rem); }
h3 { font-size: clamp(1.3rem, 2.2vw, 1.7rem); }

.container { width: min(1180px, 92%); margin: 0 auto; }

section { padding: clamp(4rem, 9vw, 7.5rem) 0; position: relative; }

/* ----------------------------------------------------------------
   Custom cursor
---------------------------------------------------------------- */
.cursor-dot, .cursor-ring {
  position: fixed; top: 0; left: 0;
  pointer-events: none; z-index: 9999;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
.cursor-dot {
  width: 8px; height: 8px;
  background: var(--purple);
  transition: width .25s var(--ease-fluid), height .25s var(--ease-fluid), background .25s;
}
.cursor-ring {
  width: 38px; height: 38px;
  border: 1.5px solid rgba(94, 59, 126, 0.65);
  transition: width .35s var(--ease-fluid), height .35s var(--ease-fluid),
              border-color .35s, background .35s, opacity .35s;
}
body.cursor-hover .cursor-dot { width: 0; height: 0; }
body.cursor-hover .cursor-ring {
  width: 64px; height: 64px;
  background: rgba(201, 162, 75, 0.14);
  border-color: var(--gold);
}
body.cursor-text .cursor-ring { opacity: 0; }
body.cursor-text .cursor-dot { width: 3px; height: 26px; border-radius: 2px; background: var(--gold); }

/* ----------------------------------------------------------------
   Reveal-on-scroll animations
---------------------------------------------------------------- */
/* No will-change here on purpose. It applies to every revealing element on the
   page — dozens per page — and promotes them all to their own compositor layer
   permanently, since nothing ever removes it once the reveal has finished. On a
   low-memory phone that alone is a large, permanent cost. Browsers already
   promote an element for the duration of a transform/opacity transition. */
.reveal {
  opacity: 0;
  transform: translateY(46px);
  transition: opacity .9s var(--ease-fluid), transform .9s var(--ease-fluid);
  transition-delay: var(--d, 0s);
}
.reveal.in-view { opacity: 1; transform: none; }

.reveal-left  { opacity: 0; transform: translateX(-60px); transition: all 1s var(--ease-fluid); transition-delay: var(--d, 0s); }
.reveal-right { opacity: 0; transform: translateX(60px);  transition: all 1s var(--ease-fluid); transition-delay: var(--d, 0s); }
.reveal-scale { opacity: 0; transform: scale(.88);        transition: all 1s var(--ease-fluid); transition-delay: var(--d, 0s); }
.reveal-left.in-view, .reveal-right.in-view, .reveal-scale.in-view { opacity: 1; transform: none; }

/* word-by-word title rise */
.split-lines .line {
  display: block; overflow: hidden;
}
.split-lines .line > span {
  display: inline-block;
  transform: translateY(110%);
  transition: transform 1.1s var(--ease-fluid);
  transition-delay: var(--d, 0s);
}
.split-lines.in-view .line > span { transform: translateY(0); }


/* ----------------------------------------------------------------
   Header / nav
---------------------------------------------------------------- */
.site-header {
  position: fixed; inset: 0 0 auto 0; z-index: 1000;
  padding: 1.1rem 0;
  transition: box-shadow .4s, padding .4s;
}
/* Was backdrop-filter: blur(14px) — a full-width, position:fixed layer that
   the compositor had to re-sample and re-blur the page behind on every
   scroll frame, on every page, for as long as the header stayed .scrolled
   (i.e. almost the entire visit past the first 40px). A near-opaque solid
   reads the same at a glance and costs nothing per frame. */
.site-header::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: rgba(244, 237, 222, .97);
  opacity: 0; transition: opacity .4s var(--ease-fluid);
}
.site-header.scrolled::before { opacity: 1; }
.site-header.scrolled {
  box-shadow: 0 6px 30px -12px rgba(75, 46, 99, 0.18);
  padding: .65rem 0;
}
.nav-wrap { display: flex; align-items: center; justify-content: space-between; }

.brand { display: flex; align-items: center; gap: .8rem; }
.brand-mark {
  width: 44px; height: 44px; border-radius: 50%;
  background: linear-gradient(135deg, var(--purple) 0%, var(--purple-deep) 100%);
  color: var(--gold-soft);
  display: grid; place-items: center;
  font-family: var(--font-display); font-size: 1.35rem; font-weight: 700;
  box-shadow: var(--shadow-card);
}
.brand-name { font-family: var(--font-accent); font-size: 1.12rem; letter-spacing: .04em; color: var(--purple-deep); }
.brand-name small { display: block; font-family: var(--font-body); font-size: .62rem; letter-spacing: .34em; text-transform: uppercase; color: var(--gold); }

.nav-links { display: flex; gap: 2rem; align-items: center; }
.nav-links a {
  font-size: .82rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); position: relative; padding: .3rem 0;
}
.nav-links a::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 100%; height: 1.5px; background: var(--gold);
  transform: scaleX(0); transform-origin: right;
  transition: transform .45s var(--ease-fluid);
}
.nav-links a:hover::after, .nav-links a.active::after { transform: scaleX(1); transform-origin: left; }
.nav-links a.active { color: var(--purple); }

.nav-cta {
  background: var(--purple); color: var(--white) !important;
  padding: .6rem 1.4rem !important; border-radius: 100px;
  transition: background .35s, transform .35s var(--ease-fluid), box-shadow .35s;
}
.nav-cta::after { display: none; }
.nav-cta:hover { background: var(--purple-deep); transform: translateY(-2px); box-shadow: var(--shadow-card); }

.nav-deco { display: none; } /* mobile-menu backdrop only — shown via the max-width media query below */

.nav-toggle { display: none; background: none; border: none; cursor: pointer; z-index: 1100; }
.nav-toggle span {
  display: block; width: 26px; height: 2px; margin: 6px 0;
  background: var(--purple-deep); transition: .4s var(--ease-fluid);
}

@media (max-width: 920px) {
  .nav-toggle { display: block; }
  .nav-links {
    position: fixed; inset: 0; overflow: hidden;
    background: linear-gradient(160deg, var(--cream) 0%, var(--beige) 100%);
    flex-direction: column; justify-content: center; gap: 2.2rem;
    transform: translateY(-100%);
    visibility: hidden;
    transition: transform .6s var(--ease-fluid), visibility 0s .6s;
  }
  .nav-links.open { transform: translateY(0); visibility: visible; transition: transform .6s var(--ease-fluid); }
  .nav-links a { font-size: 1.05rem; position: relative; z-index: 2; }
  body.nav-open .nav-toggle span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
  body.nav-open .nav-toggle span:nth-child(2) { opacity: 0; }
  body.nav-open .nav-toggle span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

  /* ---- animated backdrop behind the mobile menu ---- */
  .nav-deco {
    position: absolute; inset: 0; z-index: 0;
    pointer-events: none; overflow: hidden;
    display: block;
  }
  /* soft breathing radial glow */
  .nav-deco::after {
    content: ""; position: absolute;
    top: 50%; left: 50%; width: 120vw; height: 120vw;
    transform: translate(-50%, -50%);
    background: radial-gradient(circle at 50% 50%,
      rgba(168, 127, 46, .14) 0%,
      rgba(94, 59, 126, .08) 38%,
      transparent 68%);
    opacity: 0;
  }
  .nav-deco-logo {
    position: absolute; top: 50%; left: 50%;
    width: min(78vw, 420px); height: auto;
    transform: translate(-50%, -50%);
    opacity: 0;
    filter: grayscale(15%);
    transition: opacity .9s var(--ease-fluid);
  }
  .nav-deco-gem {
    position: absolute; color: var(--gold);
    font-size: 1rem; font-style: normal; line-height: 1;
    opacity: 0;
    transition: opacity .9s var(--ease-fluid);
  }
  .nav-deco-gem.g1 { top: 14%; left: 16%; font-size: 1.4rem; }
  .nav-deco-gem.g2 { top: 22%; right: 14%; font-size: .85rem; }
  .nav-deco-gem.g3 { bottom: 24%; left: 12%; font-size: 1rem; }
  .nav-deco-gem.g4 { bottom: 16%; right: 18%; font-size: 1.6rem; }
  .nav-deco-gem.g5 { top: 50%; right: 8%; font-size: .7rem; }

  /* reveal + animate only while the menu is open */
  .nav-links.open .nav-deco::after {
    opacity: 1; animation: navDecoBreathe 7s var(--ease-fluid) infinite;
  }
  .nav-links.open .nav-deco-logo {
    opacity: .07;
    animation: navDecoSpin 60s linear infinite;
  }
  .nav-links.open .nav-deco-gem {
    opacity: .22; animation: navGemFloat 9s ease-in-out infinite;
  }
  .nav-links.open .nav-deco-gem.g1 { animation-duration: 8s;  animation-delay: -1s; }
  .nav-links.open .nav-deco-gem.g2 { animation-duration: 11s; animation-delay: -4s; opacity: .16; }
  .nav-links.open .nav-deco-gem.g3 { animation-duration: 7.5s; animation-delay: -2.5s; opacity: .2; }
  .nav-links.open .nav-deco-gem.g4 { animation-duration: 12s; animation-delay: -6s; opacity: .18; }
  .nav-links.open .nav-deco-gem.g5 { animation-duration: 9.5s; animation-delay: -3s; opacity: .14; }
}

@keyframes navDecoSpin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes navDecoBreathe {
  0%, 100% { opacity: .55; transform: translate(-50%, -50%) scale(.92); }
  50%      { opacity: 1;   transform: translate(-50%, -50%) scale(1.08); }
}
@keyframes navGemFloat {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-14px) rotate(25deg); }
}

@media (prefers-reduced-motion: reduce) {
  .nav-links.open .nav-deco::after,
  .nav-links.open .nav-deco-logo,
  .nav-links.open .nav-deco-gem { animation: none; }
  .nav-links.open .nav-deco-logo { opacity: .06; }
}

/* ----------------------------------------------------------------
   Buttons
---------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: .6rem;
  font-family: var(--font-body); font-size: .85rem;
  letter-spacing: .16em; text-transform: uppercase;
  padding: 1rem 2.3rem; border-radius: 100px;
  border: none; cursor: pointer;
  position: relative; overflow: hidden;
  transition: transform .4s var(--ease-fluid), box-shadow .4s, color .4s;
}
.btn::before {
  content: ""; position: absolute; inset: 0;
  transform: translateY(101%); border-radius: inherit;
  transition: transform .45s var(--ease-fluid);
  z-index: 0;
}
.btn > * , .btn { z-index: 1; }
.btn span { position: relative; z-index: 1; }

.btn-primary { background: var(--purple); color: var(--white); }
.btn-primary::before { background: var(--purple-deep); }
.btn-gold { background: var(--gold); color: var(--plum); }
.btn-gold::before { background: var(--gold-soft); }
.btn-ghost { background: transparent; color: var(--purple-deep); box-shadow: inset 0 0 0 1.5px var(--purple-soft); }
.btn-ghost::before { background: var(--purple); }
.btn-ghost:hover { color: var(--white); box-shadow: inset 0 0 0 1.5px var(--purple); }

.btn:hover { transform: translateY(-3px); box-shadow: var(--shadow-soft); }
.btn:hover::before { transform: translateY(0); }

/* ----------------------------------------------------------------
   Image placeholders (replace with real photos later)
---------------------------------------------------------------- */
.ph {
  position: relative; overflow: hidden;
  border-radius: var(--radius);
  background:
    linear-gradient(135deg, var(--beige) 0%, var(--cream-soft) 45%, var(--beige-deep) 100%);
  display: grid; place-items: center;
  min-height: 220px;
  isolation: isolate;
}
/* The shimmer animates background-position, which repaints the element on every
   frame and cannot be composited. It marks an empty admin slot, so it ran on
   every unfilled placeholder on the page — on screen or not — for the whole
   visit. main.js adds .is-onscreen only while the slot is actually in view. */
.ph::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(110deg, transparent 35%, rgba(251,246,234,.3) 50%, transparent 65%);
  background-size: 250% 100%;
}
.ph.is-onscreen::before {
  animation: ph-shimmer 3.2s var(--ease-fluid) infinite;
}
.ph::after {
  content: ""; position: absolute; inset: 0;
  border: 1px dashed rgba(94, 59, 126, .28);
  border-radius: inherit; pointer-events: none;
}
@keyframes ph-shimmer {
  0%   { background-position: 130% 0; }
  100% { background-position: -130% 0; }
}
.ph .ph-label {
  position: relative; z-index: 1;
  text-align: center; padding: 1rem;
  color: var(--ink-soft);
}
.ph .ph-label .ph-icon { font-size: 2rem; display: block; margin-bottom: .4rem; opacity: .65; }
.ph .ph-label small {
  display: block; font-size: .68rem; letter-spacing: .26em;
  text-transform: uppercase; margin-top: .2rem; color: var(--purple);
}
.ph-tall   { aspect-ratio: 3 / 4;  min-height: 0; }
.ph-wide   { aspect-ratio: 16 / 9; min-height: 0; }
.ph-square { aspect-ratio: 1 / 1;  min-height: 0; }
.ph-portrait { aspect-ratio: 4 / 5; min-height: 0; }

/* ----------------------------------------------------------------
   Page hero (inner pages)
---------------------------------------------------------------- */
.page-hero {
  padding: clamp(5.5rem, 8vw, 7rem) 0 clamp(1.2rem, 2.5vw, 2rem);
  background:
    radial-gradient(900px 420px at 85% -10%, rgba(150, 120, 180, 0.32), transparent 60%),
    radial-gradient(700px 380px at 8% 110%, rgba(168, 127, 46, 0.18), transparent 60%),
    linear-gradient(180deg, var(--cream-soft) 0%, var(--cream) 100%);
  text-align: center;
}
.page-hero h1 { font-size: clamp(2rem, 4vw, 3.2rem); }
.page-hero .eyebrow { justify-content: center; }
.page-hero p.lead {
  max-width: 640px; margin: 0.9rem auto 0;
  font-size: 1rem; color: var(--ink-soft);
}
/* pull the first content section up into the first screen */
.page-hero + section { padding-top: clamp(1.5rem, 3vw, 2.5rem); }

.eyebrow {
  display: inline-flex; align-items: center; gap: .8rem;
  font-size: .74rem; letter-spacing: .34em; text-transform: uppercase;
  color: var(--gold); font-weight: 500;
}
.eyebrow::before { content: ""; width: 38px; height: 1.5px; background: var(--gold); }

/* ----------------------------------------------------------------
   Home hero
---------------------------------------------------------------- */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex; align-items: center;
  padding: clamp(8rem, 14vw, 10rem) 0 4rem;
  background:
    radial-gradient(1100px 600px at 80% 0%, rgba(150, 120, 180, 0.36), transparent 62%),
    radial-gradient(800px 500px at 0% 100%, rgba(168, 127, 46, 0.2), transparent 60%),
    linear-gradient(165deg, var(--cream-soft) 0%, var(--cream) 55%, var(--beige) 100%);
  overflow: hidden;
}
/* Full-bleed hero background photo (admin-uploaded). The scrim behind the
   text is generated in CSS, not baked into the photo — any image dropped
   into the "Hero background photo" slot gets the same left-to-right fade
   automatically, so it never needs pre-editing. */
.hero.has-bg { background: none; }
.hero-bg { position: absolute; inset: 0; z-index: -3; }
.hero-bg .hero-bg-media {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.hero-bg--mobile { display: none; }
.hero-bg::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(100deg,
    var(--cream-soft) 0%,
    var(--cream-soft) 35%,
    rgba(239, 229, 208, .92) 39%,
    rgba(239, 229, 208, .72) 43%,
    rgba(239, 229, 208, .48) 46%,
    rgba(239, 229, 208, .26) 50%,
    rgba(239, 229, 208, .1)  53%,
    rgba(239, 229, 208, 0)   56%);
}
/* With the copy pulled to the left edge above, the scrim no longer has to
   stay opaque as far across — every percent trimmed here is a percent more
   of the photograph. Text now runs ~4%-36%, so the fade starts after it. */
@media (min-width: 1100px) {
  .hero-bg::after {
    background: linear-gradient(100deg,
      var(--cream-soft) 0%,
      var(--cream-soft) 27%,
      rgba(239, 229, 208, .92) 31%,
      rgba(239, 229, 208, .72) 35%,
      rgba(239, 229, 208, .48) 38%,
      rgba(239, 229, 208, .26) 42%,
      rgba(239, 229, 208, .1)  45%,
      rgba(239, 229, 208, 0)   48%);
  }
}
@media (max-width: 700px) {
  .hero-bg::after {
    background: linear-gradient(180deg,
      var(--cream-soft) 0%,
      var(--cream-soft) 41%,
      rgba(239, 229, 208, .92) 45%,
      rgba(239, 229, 208, .72) 49%,
      rgba(239, 229, 208, .48) 52%,
      rgba(239, 229, 208, .26) 56%,
      rgba(239, 229, 208, .1)  59%,
      rgba(239, 229, 208, 0)   62%);
  }
  .hero-bg--desktop { display: none; }
  .hero-bg--mobile { display: block; }
}
/* The hero text is deliberately NOT held by the site-wide .container.
   .container caps at 1180px, so on a 1920 screen it parks ~370px of gutter
   down each side — on a page whose whole point is a full-bleed photograph,
   that gutter is cream, not picture. The hero column is left-anchored
   instead: the padding below is exactly 4vw, i.e. identical to .container's
   own 92%-width gutter at every width up to ~1283px (where .container stops
   growing), so it still lines up with the nav logo on laptops and phones and
   only pulls away from it on the wide screens that had the dead space. Cap
   at 5.5rem keeps it off the bezel on ultrawides. */
.hero > .hero-grid {
  width: auto; max-width: none; margin: 0;
  padding-inline: clamp(1.4rem, 4vw, 5.5rem);
}
.hero-grid > div { max-width: 620px; }
.hero h1 em { font-style: italic; color: var(--purple); }
.hero .tagline { margin-top: 1.6rem; font-size: 1.12rem; color: var(--ink-soft); max-width: 480px; }
.hero-ctas { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2.4rem; }
@media (max-width: 700px) {
  /* visibility (not display:none) keeps its footprint, so the hero's
     vertical centering doesn't shift the text up when the buttons vanish */
  .hero-ctas { visibility: hidden; pointer-events: none; }
  .hero-grid { transform: translateY(-48px); }
}

/* ----------------------------------------------------------------
   Feature strip (static trust badges, floats over the hero's bottom edge)
---------------------------------------------------------------- */
.feature-strip-wrap { position: relative; z-index: 3; margin-top: -20px; }
.feature-strip {
  background: var(--white);
  border-radius: 20px;
  box-shadow: var(--shadow-soft);
  display: grid; grid-template-columns: repeat(4, 1fr);
  padding: 1.7rem clamp(1.2rem, 4vw, 3rem);
}
.feature-item { display: flex; align-items: center; gap: 1rem; padding: 0 1.2rem; }
.feature-item:not(:first-child) { border-left: 1px solid var(--beige-deep); }
.feature-icon {
  width: 50px; height: 50px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center; font-size: 1.4rem;
  background: linear-gradient(135deg, rgba(110,75,142,.12), rgba(201,162,75,.16));
}
.feature-copy { display: flex; flex-direction: column; }
.feature-copy strong { font-family: var(--font-display); font-size: 1.05rem; color: var(--purple-deep); line-height: 1.2; }
.feature-copy span { font-size: .78rem; color: var(--ink-soft); margin-top: .15rem; }
@media (max-width: 700px) {
  .feature-strip-wrap { display: none; }
}
@media (max-width: 760px) {
  .feature-strip { grid-template-columns: repeat(2, 1fr); gap: 1.4rem 0; border-radius: 16px; }
  .feature-item:not(:first-child) { border-left: none; }
  .feature-item:nth-child(2) { border-left: 1px solid var(--beige-deep); }
  .feature-item:nth-child(3), .feature-item:nth-child(4) { border-top: 1px solid var(--beige-deep); padding-top: 1.2rem; }
  .feature-strip-wrap { margin-top: -10px; }
}
@media (max-width: 480px) {
  .feature-strip { grid-template-columns: 1fr; }
  .feature-item:nth-child(2) { border-left: none; }
  .feature-item:not(:first-child) { border-top: 1px solid var(--beige-deep); padding-top: 1.2rem; border-left: none; }
}

/* ----------------------------------------------------------------
   Section headers
---------------------------------------------------------------- */
.section-head { max-width: 640px; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.section-head.center { margin-inline: auto; text-align: center; }
.section-head.center .eyebrow { justify-content: center; }
.section-head h2 { margin-top: .9rem; }
.section-head p { margin-top: 1rem; color: var(--ink-soft); }

/* ----------------------------------------------------------------
   Trust pillars
---------------------------------------------------------------- */
.pillars { background: var(--white); }
.pillar-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1.4rem;
}
.pillar-card {
  background: var(--cream);
  border: 1px solid var(--beige-deep);
  border-radius: var(--radius);
  padding: 2rem 1.6rem;
  transition: transform .5s var(--ease-fluid), box-shadow .5s, background .5s, border-color .5s;
}
.pillar-card:hover {
  transform: translateY(-10px);
  box-shadow: var(--shadow-soft);
  background: var(--white);
  border-color: var(--gold-soft);
}
.pillar-card .icon {
  width: 54px; height: 54px; border-radius: 14px;
  display: grid; place-items: center; font-size: 1.5rem;
  background: linear-gradient(135deg, rgba(110,75,142,.12), rgba(201,162,75,.16));
  margin-bottom: 1.2rem;
  transition: transform .5s var(--ease-fluid);
}
.pillar-card:hover .icon { transform: rotate(-8deg) scale(1.1); }
.pillar-card h3 { font-size: 1.15rem; margin-bottom: .5rem; }
.pillar-card p { font-size: .9rem; color: var(--ink-soft); }

/* ----------------------------------------------------------------
   Collections preview / cards
---------------------------------------------------------------- */
.collection-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.8rem;
}
.collection-card {
  position: relative; border-radius: var(--radius); overflow: hidden;
  background: var(--white); box-shadow: var(--shadow-card);
  transition: transform .3s var(--ease-fluid), box-shadow .3s;
}
.collection-card:hover { transform: translateY(-10px); box-shadow: var(--shadow-soft); }
.collection-card .ph {
  border-radius: 0; transition: transform .15s var(--ease-fluid);
}
/* No will-change/backface-visibility here on purpose — it used to be pre-
   promoted that way to avoid a one-frame "blurry pop" the instant a hover
   transition starts. But a *permanent* will-change (this ran for the whole
   page lifetime, not just during :hover) tells the browser more changes are
   always imminent, so it stops re-rasterizing the layer's texture at the
   scaled-up resolution once the hover settles — it just keeps stretching the
   original, unscaled raster via the GPU. That reads as content staying
   blurred for as long as the card is hovered, not just for one frame, which
   is strictly worse. The plain `transition: transform` above already gets
   this promoted to a compositor layer the normal way (every browser has done
   this automatically for years), which — unlike will-change — is treated as
   transient: once the transform stops changing, the browser is free to
   re-rasterize the settled, enlarged layer and it comes out sharp.
   Durations were .55s/.5s — the compositor genuinely can't keep a texture
   perfectly crisp while it's actively being stretched mid-transition (that's
   the cost of animating on the compositor thread instead of repainting every
   frame), so a slow transition means a long, clearly-visible soft period.
   Cut close to half: still a visible, eased "grow", just not long enough for
   the eye to register the in-between frames as blur instead of motion. */
.collection-card:hover .ph { transform: scale(1.05); }
.collection-card .card-body { padding: 1.5rem 1.6rem 1.7rem; position: relative; background: var(--white); }
.collection-card .card-body h3 { margin-bottom: .35rem; }
.collection-card .card-body p { font-size: .88rem; color: var(--ink-soft); }
.collection-card .card-tag {
  position: absolute; top: 1rem; left: 1rem; z-index: 2;
  background: rgba(244, 237, 222, .95);
  border: 1px solid var(--beige-deep);
  font-size: .64rem; letter-spacing: .22em; text-transform: uppercase;
  padding: .42rem .9rem; border-radius: 100px; color: var(--purple);
}
.collection-card .card-arrow {
  position: absolute; right: 1.5rem; top: -22px;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--gold); color: var(--plum);
  display: grid; place-items: center; font-size: 1.1rem;
  transition: transform .5s var(--ease-fluid), background .4s;
}
.collection-card:hover .card-arrow { transform: rotate(45deg); background: var(--purple); color: var(--white); }

/* tilt effect host — this is the element main.js applies the
   perspective()/rotateY/rotateX transform to on mousemove. No permanent
   will-change here either, for the same reason as .collection-card .ph
   above: it was tried (to fix a one-frame blur at hover-start) and instead
   caused the card to stay visibly soft the entire time it was tilted/grown,
   confirmed on real hardware — a standing will-change stops the browser from
   ever re-rasterizing the layer once the transform settles. main.js's inline
   `transform` write on mousemove promotes this the normal, transient way. */
.tilt { transform-style: preserve-3d; }

/* ----------------------------------------------------------------
   Story strip (home) & story page
---------------------------------------------------------------- */
.story-strip { background: linear-gradient(180deg, var(--white) 0%, var(--cream-soft) 100%); }
.split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2.4rem, 6vw, 5.5rem); align-items: center; }
.split.flip > :first-child { order: 2; }

/* ================================================================
   KOHINOOR BUMPER OFFER — the once-a-year block
   ================================================================
   A wider stage than the rest of the site, on purpose. `.container` is
   min(1180px, 92%), which on a 1920 screen parks ~370px of dead cream down
   each side — fine for a text column, wrong for the one section built
   around photographs of a hall with 400 people in it. `.container-wide` is
   scoped to this block rather than applied globally so body copy elsewhere
   keeps its measure; here the copy columns get their own max-width instead
   (see .offer-copy p).

   Motion budget: nothing here takes a permanent will-change, nothing
   animates a layout property, and every looping animation sits behind
   `.fx-live` — main.js adds that to [data-offer] only while the section is
   near the viewport, and only at the full/lite tiers, so the static tier
   and prefers-reduced-motion both get a still, complete page. */
.container-wide { width: min(1680px, 93%); margin: 0 auto; }

/* ---------------- hero ---------------- */
.offer-hero {
  position: relative; overflow: hidden; padding: 0;
  min-height: clamp(600px, 86vh, 900px);
  display: flex; align-items: center;
  background: var(--plum);
  /* Establishes a stacking context so .offer-hero-media's negative z-index
     stacks locally (behind this section's own content, above its own
     background) instead of escaping to an ancestor context — where it was
     painting behind the section's background instead of under it. */
  isolation: isolate;
}
/* Inset past the edges so the parallax translate has somewhere to travel
   without ever dragging a bare edge into view. */
.offer-hero-media { position: absolute; inset: -7% 0; z-index: -1; }
.ph-fill { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0; min-height: 0; aspect-ratio: auto; }

/* A graded wash, not a panel. Two layers: a diagonal from the type corner so
   the words always have something to sit on, and a soft floor under the
   ribbon. Nothing rectangular, nothing blurred — there is no edge anywhere
   on the photograph you can point at and call a box. */
.offer-hero-wash {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(100deg, rgba(24,13,8,.93) 0%, rgba(24,13,8,.86) 30%, rgba(24,13,8,.55) 48%, rgba(24,13,8,.16) 68%, rgba(24,13,8,0) 88%),
    linear-gradient(0deg, rgba(20,11,6,.8) 0%, rgba(20,11,6,.22) 20%, rgba(20,11,6,0) 42%);
}
.offer-hero-inner {
  position: relative; z-index: 1;
  /* The bottom value used to be clamp(6rem, 11vw, 8.5rem) so the copy cleared
     the looping prize ribbon that sat along the bottom edge. That ribbon is
     gone, so the extra ~3rem is now just dead photograph under the last line. */
  padding-block: clamp(5rem, 10vw, 7rem) clamp(4.5rem, 8vw, 6rem);
}
.offer-hero-copy { max-width: min(50ch, 46%); }

/* Eyebrow rule that draws itself out from nothing as the line scrolls in —
   scaleX on a 1.5px bar, so it composites and costs nothing. */
.offer-flag { display: flex; align-items: center; gap: 1rem; }
.offer-flag i {
  display: block; width: clamp(40px, 6vw, 84px); height: 1.5px; flex: none;
  background: linear-gradient(90deg, var(--gold-soft), rgba(221,188,119,.15));
  transform: scaleX(0); transform-origin: left center;
  transition: transform 1.1s var(--ease-fluid) .1s;
}
.offer-flag.in-view i { transform: scaleX(1); }
.offer-flag span {
  font-size: .74rem; letter-spacing: .42em; text-transform: uppercase;
  color: var(--gold-soft); font-weight: 500;
}

.offer-hero-title {
  margin-top: 1.1rem;
  font-size: clamp(2.9rem, 6.4vw, 5.6rem);
  line-height: 1.02; letter-spacing: -.015em;
  color: var(--white);
  text-shadow: 0 2px 40px rgba(0,0,0,.45);
}
.offer-hero-text { margin-top: clamp(1.6rem, 3vw, 2.4rem); max-width: 48ch; }
.offer-hero-text p {
  font-size: 1.06rem; line-height: 1.8; color: rgba(253,250,242,.88);
  text-shadow: 0 1px 24px rgba(0,0,0,.5);
}
.offer-hero-text p + p { margin-top: 1rem; }

/* ---- rotating seal ----
   The r=74 arc is ~465 user units around, which is what the default seal
   string is written to fill at this size — keep replacement text to roughly
   the same length, or move font-size/letter-spacing with it. */
/* Pinned to the section, not the text column: it reads as a stamp pressed
   onto the photograph. The radial is what makes it legible over whatever the
   photo happens to put behind it — a ring of gold hairlines alone disappears
   into a light background. */
.offer-seal {
  position: absolute; z-index: 2;
  right: clamp(1.2rem, 4vw, 4rem);
  bottom: clamp(2rem, 4.5vw, 4rem);   /* was 4.5–7rem, to sit above the ribbon */
  width: clamp(150px, 15vw, 210px); aspect-ratio: 1;
  display: grid; place-items: center;
  background: radial-gradient(circle at center, rgba(20,11,6,.66) 40%, rgba(20,11,6,.34) 60%, rgba(20,11,6,0) 76%);
}
.offer-seal svg { width: 100%; height: 100%; overflow: visible; }
.offer-seal text {
  font-family: var(--font-body);
  font-size: 15.5px; letter-spacing: 2px; text-transform: uppercase;
  fill: var(--gold-soft);
}
.offer-seal::before, .offer-seal::after {
  content: ""; position: absolute; border-radius: 50%; pointer-events: none;
}
.offer-seal::before { inset: 13%; border: 1px solid rgba(221,188,119,.5); }
.offer-seal::after  { inset: 17%; border: 1px solid rgba(221,188,119,.22); }
.offer-seal-gem {
  position: absolute; font-size: 1.5rem; line-height: 1;
  color: var(--gold-soft); text-shadow: 0 0 26px rgba(221,188,119,.6);
}

/* ---- looping motion, only while the section is live ----
   The drift is on .ph-fill and the parallax writes .offer-hero-media, so the
   two transforms never contend for the same element. */
@keyframes offerDrift {
  from { transform: scale(1.06) translate3d(-1.2%, .6%, 0); }
  to   { transform: scale(1.15) translate3d(1.4%, -1.2%, 0); }
}
@keyframes offerSealSpin { to { transform: rotate(360deg); } }
.offer-hero.fx-live .ph-fill { animation: offerDrift 32s ease-in-out infinite alternate; }
.offer-hero.fx-live .offer-seal svg { animation: offerSealSpin 30s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .offer-hero.fx-live .ph-fill,
  .offer-hero.fx-live .offer-seal svg { animation: none; }
}

/* Below this the copy runs the full column, so the seal stays down in the
   bottom-right corner where the text has already ended — moving it up to the
   top right ran it straight through the last word of the headline at ~768px. */
@media (max-width: 1100px) {
  .offer-hero-copy { max-width: 62ch; }
}

/* Phones: the side-lit wash cannot work once the copy runs the full width —
   two paragraphs of lead at that measure fill the screen, so darkening enough
   to read them means darkening the photograph out of existence (which is what
   a full-height wash did here). Split the section instead: the photograph gets
   a real band of its own at the top, fading into plum, and the copy sits on
   the plum below it. Full-bleed band, not a floating panel.

   --hero-bleed is the slack the parallax translate needs at both ends of the
   band. The wash covers exactly the same box and reaches opaque plum before
   the band ends, so the image's moving bottom edge is never visible. */
@media (max-width: 720px) {
  .offer-hero { --hero-img: 54vh; --hero-bleed: 80px; align-items: stretch; min-height: 0; }
  .offer-hero-media {
    inset: calc(-1 * var(--hero-bleed)) 0 auto;
    height: calc(var(--hero-img) + var(--hero-bleed) * 2);
  }
  /* One --hero-bleed taller than the image band, because the parallax moves
     the band and the wash does not: the image's bottom edge can sit anywhere
     between --hero-img and --hero-img + 2 * --hero-bleed, and the wash has to
     be opaque across all of it or a bare strip of photo shows below the fade.
     The opaque run at the bottom is therefore 2 * --hero-bleed deep. */
  .offer-hero-wash {
    inset: calc(-1 * var(--hero-bleed)) 0 auto;
    height: calc(var(--hero-img) + var(--hero-bleed) * 3);
    background: linear-gradient(0deg,
      var(--plum) 0%, var(--plum) 22%,
      rgba(40,22,52,.8) 36%, rgba(24,13,8,.34) 66%, rgba(24,13,8,.14) 100%);
  }
  .offer-hero-inner { padding-block: calc(var(--hero-img) - 4.5rem) clamp(3.5rem, 10vw, 5rem); }
  /* The rotating ring of words is off on phones. At this width it had to shrink
     to 122px across to fit beside the headline, which puts its ~465 units of
     arc text at a size nobody reads, over a photograph, while spinning — and it
     is one more infinitely-animating composited layer on the devices least able
     to afford it. Hidden rather than merely not animated: display:none takes
     the element, its two hairline rings and its radial halo out of paint
     entirely. */
  .offer-seal { display: none; }
}

/* ---------------- how it works ---------------- */
.offer-steps-label { margin-bottom: 0; }
.offer-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: clamp(2rem, 4vw, 3.4rem); margin-top: 2.2rem; }
.offer-step { border-top: 1.5px solid var(--gold-soft); padding-top: 1.5rem; }
.offer-step .step-no { font-size: 2.6rem; margin-bottom: .8rem; }
.offer-step-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .6rem; }
.offer-step-icon { font-size: 1.3rem; }
.offer-step h3 { font-size: 1.08rem; }
.offer-step p { font-size: 1.05rem; line-height: 1.7; color: var(--ink-soft); }

/* The wide container would otherwise run body copy out past 100 characters a
   line. Cap the measure on the copy itself rather than narrowing the
   container back down — the photographs are the reason it is wide. */
.offer-copy p { font-size: 1.06rem; line-height: 1.75; max-width: 62ch; }

/* ---------------- prizes ---------------- */
/* --feat-h lives here, not on .prize-feature, because .prize-side-frame
   below needs it too and is a sibling of .prize-top (a CSS variable only
   inherits down the tree, not sideways) — see the aspect-ratio comment on
   .prize-side-frame for why they share it. */
.prize-section { padding-top: clamp(3rem, 7vw, 5.5rem); --feat-h: clamp(440px, 70vh, 800px); }
.prize-top {
  display: grid; grid-template-columns: minmax(0, 1.22fr) minmax(0, .78fr);
  gap: clamp(2.2rem, 5vw, 4.5rem); align-items: center;
}
/* One source of truth for the feature height: the offset hairline behind the
   photo has to match the *photo*, not the <figure>, or it runs down past the
   caption and reads as a stray box around the whole thing. */
.prize-feature { position: relative; }
/* An offset gold hairline behind the photo — the frame a jeweller's press ad
   would use. Decorative, one static border, no layer of its own. Offset up and
   to the right specifically: down-left would run its bottom edge straight
   through the caption, and the "2026 Winner" tag already owns the top left. */
.prize-feature::before {
  content: ""; position: absolute; z-index: 0;
  top: clamp(-30px, -2.2vw, -16px); right: clamp(-30px, -2.2vw, -16px);
  width: 100%; height: var(--feat-h);
  border: 1px solid rgba(140,105,32,.35); border-radius: var(--radius);
  pointer-events: none;
}
/* The frame clips; the wrapper inside it is what data-parallax translates,
   so that wrapper is inset past the frame and never exposes an edge. */
.prize-feature-frame {
  position: relative; z-index: 1;
  height: var(--feat-h);
  border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-soft);
}
.prize-feature-frame > div { position: absolute; inset: -7% 0; }
.ph-feature { height: 100%; min-height: 0; aspect-ratio: auto; border-radius: 0; }
.prize-feature figcaption {
  position: relative; z-index: 1;
  margin-top: 1rem; padding-left: 1.1rem;
  font-size: .9rem; color: var(--ink-soft); border-left: 2px solid var(--gold-soft);
}
.prize-tag {
  position: absolute; top: -.85rem; left: 1.6rem; z-index: 3;
  background: var(--gold); color: var(--cream);
  font-size: .66rem; letter-spacing: .18em; text-transform: uppercase;
  padding: .42rem 1rem; border-radius: 100px; white-space: nowrap;
  box-shadow: 0 10px 24px -10px rgba(40,22,52,.6);
}
.prize-row {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(1.2rem, 2.6vw, 2.2rem);
  margin-top: clamp(1.6rem, 4vw, 3rem);
}
/* Same aspect ratio as .prize-feature-frame, not an independently clamped
   height: both read var(--feat-h) and .prize-side-frame scales it down by
   how much narrower its own grid column is, so the two boxes stay the same
   shape instead of the side ones going flat. At >=981px .prize-top is two
   columns (photo gets 1.22 of the 1.22+.78 split) and .prize-row is two
   equal columns, so a side column is ~82% as wide as the main one (0.5 /
   0.61) -> 0.82. Between 721-980px .prize-top has stacked to one column
   (photo now full width) while .prize-row is still two columns, so a side
   column is ~half the main one -> 0.5. Below 721px .prize-row is
   display:none (the .prize-phone markup takes over), so there is no
   narrower tier to add. */
.prize-side-frame {
  position: relative;
  height: calc(var(--feat-h) * 0.82);
  border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-card);
}
.prize-side-frame > div { position: absolute; inset: -7% 0; }
.ph-side { height: 100%; min-height: 0; aspect-ratio: auto; border-radius: 0; }

@media (max-width: 980px) {
  .prize-top { grid-template-columns: 1fr; }
  .prize-feature::before { display: none; }
  .prize-section { --feat-h: clamp(380px, 58vh, 560px); }
  .prize-side-frame { height: calc(var(--feat-h) * 0.5); }
}
@media (max-width: 620px) {
  .prize-row { grid-template-columns: 1fr; }
}

/* ---- the three prize photographs, phone edition ----
   Both desktop frames are fixed-HEIGHT boxes that the photo fills with
   object-fit: cover. That is right on a wide screen, where the frame is
   roughly as wide as the picture. On a phone the container is ~363px across
   while --feat-h is still ~500px, so the frame turns PORTRAIT and a
   landscape photograph loses width off the sides — the picture reads as both
   cropped and small. Hence a separate block: same three images, laid out for
   the shape a phone actually is, backed by the *_mobile upload slots
   (story.offer_moment1/2/3_mobile in image-defaults.js) so this crop can be
   a genuinely different photo, not just a CSS reflow of the desktop one.

   3:4 (portrait), not 4:3 — a phone screen is itself portrait, so a taller
   frame fills more of it without needing extreme vertical scroll, and it
   matches what a phone camera shoots by default.

   The strip goes full-bleed (margin-left: calc(50% - 50vw) against
   .container-wide's 93%), because on a 390px screen the widest a photo can
   possibly be is the screen. All three sit together below the copy instead
   of being split photo / copy / photo / photo down the page. */
.prize-phone { display: none; }

@media (max-width: 720px) {
  .prize-feature, .prize-row { display: none; }
  /* The winner's photo is the whole point of this section (same reasoning as
     the desktop .prize-top/.prize-row split): it is the only flex child that
     isn't inside a scroll row, so it stretches to the full 100vw width.
     The two supporting shots live in .prize-phone-side beneath it — sized
     well short of full width so the size difference from the winner's photo
     reads at a glance, not just from the caption/tag being absent. */
  .prize-phone {
    display: flex; flex-direction: column; gap: 1.4rem;
    width: 100vw; margin-left: calc(50% - 50vw);
    margin-top: clamp(1.8rem, 6vw, 2.6rem);
  }
  .prize-phone-frame {
    position: relative; overflow: hidden;
    aspect-ratio: 3 / 4;
    box-shadow: var(--shadow-card);
  }
  /* .ph.has-img sets min-height:220px at a higher specificity than a lone
     class, so it needs answering explicitly — otherwise a filled slot refuses
     to go below 220px and blows out the 3:4 frame on a narrow handset. */
  .ph-pshot, .ph-pshot.has-img { height: 100%; min-height: 0; aspect-ratio: auto; border-radius: 0; }
  /* Sits inside the frame now, not hanging off a corner of it — there is no
     margin to hang into once the strip runs edge to edge. */
  .prize-phone-card .prize-tag { top: .9rem; left: .9rem; }
  .prize-phone-card figcaption {
    width: min(1680px, 93%); margin: .85rem auto 0; padding-left: 1.1rem;
    font-size: .9rem; color: var(--ink-soft);
    border-left: 2px solid var(--gold-soft);
  }
  /* Native horizontal scroll, not the pinned/scrubbed GSAP .hscroll-stage
     machinery elsewhere — two cards don't need a ScrollTrigger, just a
     scroll-snap row, same pattern as .hscroll-section.fx-static's fallback.
     80vw (capped so it never balloons past a tablet-width phone) leaves the
     next card peeking in at the right edge as a scroll affordance, and
     still reads clearly smaller than the winner's full-bleed 100vw photo
     above. .prize-phone-frame's own aspect-ratio: 3/4 scales its height
     with this width, so the two cards keep their shape, just bigger. */
  .prize-phone-side {
    display: flex; gap: 1rem;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x mandatory; scroll-padding-left: 6vw;
    padding: 0 6vw;
    scrollbar-width: none;
  }
  .prize-phone-side::-webkit-scrollbar { display: none; }
  .prize-phone-side .prize-phone-card {
    flex: 0 0 80vw; max-width: 360px;
    scroll-snap-align: center;
  }
}

@media (max-width: 880px) {
  .split { grid-template-columns: 1fr; }
  .split.flip > :first-child { order: 0; }
}
.split .ph { box-shadow: var(--shadow-soft); }

.stat-row { display: flex; flex-wrap: wrap; gap: 2.6rem; margin-top: 2.4rem; }
.stat .num {
  font-family: var(--font-display); font-size: 2.6rem; font-weight: 700;
  color: var(--purple); line-height: 1;
}
.stat .num sup { font-size: 1.2rem; color: var(--gold); }
.stat .lbl { font-size: .76rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft); margin-top: .3rem; }

/* timeline (our story page) */
.timeline { position: relative; max-width: 760px; margin: 0 auto; padding-left: 2.4rem; }
.timeline::before {
  content: ""; position: absolute; left: 7px; top: 6px; bottom: 6px;
  width: 2px; background: linear-gradient(var(--gold), var(--purple-soft));
}
.timeline-item { position: relative; padding-bottom: 3rem; }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-item::before {
  content: ""; position: absolute; left: -2.4rem; top: 8px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--cream); border: 3px solid var(--gold);
  transition: background .4s, transform .4s var(--ease-fluid);
}
.timeline-item:hover::before { background: var(--gold); transform: scale(1.25); }
.timeline-item .year {
  font-family: var(--font-accent); color: var(--gold);
  letter-spacing: .2em; font-size: .9rem;
}
.timeline-item h3 { margin: .3rem 0 .5rem; }
.timeline-item p { color: var(--ink-soft); max-width: 560px; }

/* quote block */
.quote-block {
  background: var(--purple-deep);
  border-radius: calc(var(--radius) + 8px);
  padding: clamp(2.5rem, 6vw, 4.5rem);
  text-align: center; color: var(--cream);
  position: relative; overflow: hidden;
}
.quote-block::before {
  content: "\201C"; position: absolute; top: -2rem; left: 1.5rem;
  font-family: var(--font-display); font-size: 14rem; color: rgba(201,162,75,.18);
  line-height: 1;
}
.quote-block blockquote {
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.4rem, 3vw, 2.1rem); max-width: 760px; margin: 0 auto;
  color: var(--cream);
}
.quote-block cite {
  display: block; margin-top: 1.6rem; font-style: normal;
  font-size: .8rem; letter-spacing: .26em; text-transform: uppercase; color: var(--gold-soft);
}

/* ----------------------------------------------------------------
   Services
---------------------------------------------------------------- */
.service-list { display: grid; gap: 1rem; }
.service-item {
  display: flex; align-items: center; gap: 1.4rem;
  background: var(--white); border-radius: 14px;
  padding: 1.3rem 1.6rem;
  border: 1px solid transparent;
  transition: transform .45s var(--ease-fluid), border-color .45s, box-shadow .45s;
}
.service-item:hover { transform: translateX(10px); border-color: var(--gold-soft); box-shadow: var(--shadow-card); }
.service-item .icon { font-size: 1.6rem; }
.service-item h3 { font-size: 1.1rem; }
.service-item p { font-size: .86rem; color: var(--ink-soft); }

/* ----------------------------------------------------------------
   App section
---------------------------------------------------------------- */
.app-section {
  background:
    radial-gradient(700px 420px at 110% 10%, rgba(201,162,75,.2), transparent 60%),
    linear-gradient(160deg, var(--plum) 0%, var(--purple-deep) 60%, var(--purple) 100%);
  color: var(--cream);
}
.app-section h2, .app-section h3 { color: var(--white); }
.app-section .eyebrow { color: var(--gold-soft); }
.app-section p { color: rgba(251,247,240,.9); }
.app-feature {
  display: flex; gap: 1.1rem; align-items: flex-start;
  padding: 1.1rem 0; border-bottom: 1px solid rgba(251,247,240,.12);
}
.app-feature:last-child { border-bottom: 0; }
.app-feature .icon {
  flex: 0 0 46px; height: 46px; border-radius: 12px;
  background: rgba(201,162,75,.18); color: var(--gold-soft);
  display: grid; place-items: center; font-size: 1.25rem;
  transition: transform .45s var(--ease-fluid), background .45s;
}
.app-feature:hover .icon { transform: rotate(-8deg) scale(1.12); background: rgba(201,162,75,.32); }
.store-badges { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 2rem; }
.store-badge {
  display: inline-flex; align-items: center; gap: .8rem;
  background: rgba(251,247,240,.08);
  border: 1px solid rgba(251,247,240,.22);
  border-radius: 14px; padding: .8rem 1.5rem;
  transition: background .4s, transform .4s var(--ease-fluid), border-color .4s;
}
.store-badge:hover { background: rgba(251,247,240,.16); transform: translateY(-4px); border-color: var(--gold-soft); }
.store-badge .icon { font-size: 1.7rem; }
.store-badge img.icon { width: 32px; height: 32px; object-fit: contain; flex: 0 0 32px; }
.ig-icon { width: 22px; height: 22px; object-fit: contain; display: inline-block; vertical-align: -5px; }
.contact-card .icon .ig-icon { width: 26px; height: 26px; vertical-align: middle; }
.store-badge small { display: block; font-size: .62rem; letter-spacing: .16em; text-transform: uppercase; color: rgba(251,247,240,.72); }
.store-badge strong { font-family: var(--font-accent); font-weight: 400; letter-spacing: .04em; }

.phone-mock {
  width: min(300px, 80%); margin: 0 auto;
  border-radius: 38px; padding: 14px;
  background: linear-gradient(135deg, #3A2150 0%, var(--plum) 45%, #1A0F23 100%);
  box-shadow: 0 30px 70px -20px rgba(0,0,0,.5), inset 0 0 0 2px rgba(201,162,75,.35);
}
.phone-mock .ph { border-radius: 26px; aspect-ratio: 9 / 19; min-height: 0; }

/* ----------------------------------------------------------------
   Blog cards
---------------------------------------------------------------- */
.blog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.8rem; }
.blog-card {
  background: var(--white); border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: transform .3s var(--ease-fluid), box-shadow .3s;
  display: flex; flex-direction: column;
}
.blog-card:hover { transform: translateY(-10px); box-shadow: var(--shadow-soft); }
.blog-card .ph {
  border-radius: 0; aspect-ratio: 16/10; min-height: 0; transition: transform .15s var(--ease-fluid);
}
/* no permanent will-change — same reasoning as .collection-card .ph, see there */
.blog-card:hover .ph { transform: scale(1.05); }
.blog-card .card-body { padding: 1.6rem 1.7rem 1.8rem; display: flex; flex-direction: column; flex: 1; }
.blog-meta {
  display: flex; gap: 1rem; font-size: .68rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--gold); margin-bottom: .7rem;
}
.blog-card h3 { font-size: 1.28rem; margin-bottom: .6rem; transition: color .35s; }
.blog-card:hover h3 { color: var(--purple); }
.blog-card p { font-size: .9rem; color: var(--ink-soft); flex: 1; }
.read-more {
  margin-top: 1.2rem; font-size: .76rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--purple); display: inline-flex; align-items: center; gap: .5rem;
}
.read-more .arr { transition: transform .4s var(--ease-fluid); }
.blog-card:hover .read-more .arr { transform: translateX(8px); }

.blog-featured { margin-bottom: 3rem; }
.blog-featured .split { align-items: stretch; }
.blog-featured .card-body { display: flex; flex-direction: column; justify-content: center; }

/* filter pills */
.filter-row { display: flex; flex-wrap: wrap; gap: .7rem; justify-content: center; margin-bottom: 3rem; }
.pill {
  font-size: .74rem; letter-spacing: .16em; text-transform: uppercase;
  padding: .55rem 1.3rem; border-radius: 100px;
  background: var(--white); color: var(--ink-soft);
  border: 1px solid var(--beige-deep);
  transition: all .35s var(--ease-fluid);
  cursor: pointer;
}
.pill:hover, .pill.active { background: var(--purple); color: var(--white); border-color: var(--purple); transform: translateY(-2px); }

/* ----------------------------------------------------------------
   Contact
---------------------------------------------------------------- */
/* ---- Visit Us hero ----
   The generic .page-hero left this page as a tall empty band of cream with
   three centred lines floating in it. Two things fill it now. First, a hairline
   doorway drawn behind the headline — the headline is "Door's open", so the
   frame is the joke and the decoration at once, and two nested arches plus a
   gem at the apex cost nothing but a couple of borders. Second, the three
   actions a visitor came for, right under the lead.

   .visit-deco does its own clipping. Clipping .visit-hero instead would be the
   obvious move and the wrong one — an overflow on an ancestor turns it into a
   scroll container, which silently breaks the sticky form column below. */
.visit-hero {
  position: relative;
  padding: clamp(4.5rem, 7vw, 6rem) 0 clamp(2.4rem, 4.5vw, 3.6rem);
  /* Flat ramp only, ending on exactly the body colour. .page-hero paints its two
     tinted radials here as well and they stop dead at the section edge — a hard
     horizontal seam straight across the page. Everything tinted lives on
     .visit-deco below, which fades out before it reaches that edge. */
  background: linear-gradient(180deg, var(--cream-soft) 0%, var(--cream) 100%);
  text-align: center;
}
/* Both the colour wash and the doorway, on one layer that dissolves into the
   floor. The mask is what makes the arch legs fade out rather than being
   chopped off at the section boundary, and it hides the seam at the same time. */
.visit-deco {
  position: absolute; inset: 0; overflow: clip; pointer-events: none;
  background:
    radial-gradient(900px 420px at 85% -10%, rgba(150, 120, 180, 0.32), transparent 60%),
    radial-gradient(700px 380px at 8% 108%, rgba(168, 127, 46, 0.20), transparent 62%);
  -webkit-mask-image: linear-gradient(180deg, #000 62%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 62%, transparent 100%);
}
.visit-arch {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: clamp(4.6rem, 6.5vw, 6.4rem);
  width: clamp(300px, 34vw, 520px); height: clamp(340px, 38vw, 540px);
  border: 1.5px solid rgba(140, 105, 32, .22);
  border-bottom: none;
  border-radius: 50% 50% 0 0 / 32% 32% 0 0;
  background: radial-gradient(70% 60% at 50% 30%, rgba(221, 188, 119, .13), transparent 70%);
}
.visit-arch-in {
  top: calc(clamp(4.6rem, 6.5vw, 6.4rem) + 14px);
  width: clamp(268px, 30vw, 464px); height: clamp(326px, 36vw, 526px);
  border-color: rgba(140, 105, 32, .12);
  background: none;
}
.visit-gem {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: calc(clamp(4.6rem, 6.5vw, 6.4rem) - .85rem);
  font-size: .9rem; line-height: 1; color: var(--gold);
  opacity: .5;
}
.visit-hero-inner { position: relative; z-index: 1; }
.visit-hero .eyebrow { justify-content: center; }
.visit-hero h1 { font-size: clamp(2rem, 4vw, 3.2rem); }
.visit-hero p.lead {
  max-width: 660px; margin: .9rem auto 0;
  font-size: 1rem; color: var(--ink-soft);
}
.visit-actions {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .8rem; margin-top: clamp(1.6rem, 3vw, 2.2rem);
}
.visit-actions .btn { padding: .85rem 1.9rem; font-size: .78rem; }

/* The grid starts immediately: the hero already carries the page's breathing
   room, so a second helping of top padding here is what pushed the form down
   off the first screen. */
.visit-body { padding-top: clamp(1.2rem, 2.5vw, 2rem); }

.contact-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: clamp(2.4rem, 5vw, 4.5rem); align-items: start; }

/* Sticky, not pinned. The left column is five cards tall, so in the old layout
   the form scrolled away before you had finished reading them. Sticky is pure
   CSS — no scroll handler, no ScrollTrigger pin, nothing to desynchronise — and
   it keeps the form on screen for the whole column. The offset clears the
   sticky site header. */
.visit-form-col { position: sticky; top: 6.5rem; }

@media (max-width: 980px) {
  .contact-grid { grid-template-columns: 1fr; }
  /* Form first on a phone, for the same reason it is sticky on desktop: nobody
     should have to scroll past five cards to find the enquiry box. */
  .visit-form-col { position: static; order: -1; margin-bottom: 2rem; }
}

/* The "Get Directions →" link added to the address card. .contact-card a is
   already styled as body-weight ink-soft; this is the one link in the block
   that is a call to action rather than a phone number, so it reads as one. */
.contact-card .card-link {
  display: inline-block; margin-top: .55rem;
  font-size: .8rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--gold); font-weight: 500;
}
.contact-card .card-link:hover { color: var(--purple); }

.contact-card {
  display: flex; gap: 1.2rem; align-items: flex-start;
  background: var(--white); border-radius: 14px; padding: 1.4rem 1.5rem;
  margin-bottom: 1rem; border: 1px solid var(--beige-deep);
  transition: transform .45s var(--ease-fluid), border-color .45s, box-shadow .45s;
}
.contact-card:hover { transform: translateX(8px); border-color: var(--gold-soft); box-shadow: var(--shadow-card); }
.contact-card .icon {
  flex: 0 0 48px; height: 48px; border-radius: 12px; font-size: 1.3rem;
  display: grid; place-items: center;
  background: linear-gradient(135deg, rgba(110,75,142,.12), rgba(201,162,75,.16));
}
.contact-card h3 { font-size: 1.05rem; }
.contact-card p, .contact-card a { font-size: .92rem; color: var(--ink-soft); }
.contact-card a:hover { color: var(--purple); }

.contact-form {
  background: var(--white); border-radius: calc(var(--radius) + 6px);
  padding: clamp(1.8rem, 4vw, 3rem); box-shadow: var(--shadow-card);
}
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; }
@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }
.field { margin-bottom: 1.2rem; }
.field label {
  display: block; font-size: .72rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-soft); margin-bottom: .45rem;
}
.field input, .field select, .field textarea {
  width: 100%; font-family: var(--font-body); font-size: .95rem;
  padding: .85rem 1.1rem; color: var(--ink);
  background: var(--cream); border: 1.5px solid var(--beige-deep);
  border-radius: 10px; outline: none;
  transition: border-color .35s, box-shadow .35s;
}
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--purple-soft);
  box-shadow: 0 0 0 4px rgba(110,75,142,.1);
}
.field textarea { resize: vertical; min-height: 130px; }

/* ----------------------------------------------------------------
   CTA band
---------------------------------------------------------------- */
.cta-band {
  background:
    radial-gradient(600px 300px at 15% 0%, rgba(150,120,180,.38), transparent 60%),
    linear-gradient(135deg, var(--purple-deep), var(--purple));
  border-radius: calc(var(--radius) + 10px);
  padding: clamp(2.6rem, 6vw, 4.6rem);
  text-align: center; color: var(--white);
  overflow: hidden; position: relative;
}
.cta-band h2 { color: var(--white); }
.cta-band p { color: rgba(251,247,240,.9); max-width: 540px; margin: 1rem auto 2rem; }

/* ----------------------------------------------------------------
   Footer
---------------------------------------------------------------- */
.site-footer {
  background: var(--plum); color: rgba(251,247,240,.88);
  padding: clamp(3.5rem, 7vw, 5.5rem) 0 0;
  margin-top: clamp(3rem, 7vw, 5rem);
}
.footer-grid {
  display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr 1fr;
  gap: 2.6rem; padding-bottom: 3rem;
}
@media (max-width: 880px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer-grid { grid-template-columns: 1fr; } }

.footer-address a { display: inline-flex; align-items: flex-start; gap: .55rem; }
.footer-address .maps-icon {
  width: 18px; height: 24px; object-fit: contain; flex: none;
  margin-top: 2px;
}
.footer-social a { display: inline-flex; align-items: center; gap: .7rem; }
.footer-social .social-icon {
  width: 32px; height: 32px; object-fit: contain;
  flex: none;
}
/* facebook badge has a hair more internal margin than the others — nudge it up */
.footer-social .social-icon[src*="facebook"] { transform: scale(1.1); }

.site-footer h4 {
  font-family: var(--font-accent); font-weight: 400; color: var(--gold-soft);
  letter-spacing: .14em; text-transform: uppercase; font-size: .85rem;
  margin-bottom: 1.2rem;
}
.site-footer a { transition: color .3s, padding-left .3s var(--ease-fluid); }
.site-footer a:hover { color: var(--gold-soft); padding-left: 6px; }
.site-footer li { margin-bottom: .6rem; font-size: .92rem; }
.footer-brand .brand-name { color: var(--cream); }
.footer-brand p { font-size: .9rem; margin-top: 1rem; max-width: 280px; }
.footer-tagline { font-family: var(--font-display); font-style: italic; color: var(--gold-soft); font-size: 1.05rem; margin-top: 1.2rem; }

.footer-bottom {
  border-top: 1px solid rgba(251,247,240,.12);
  padding: 1.4rem 0; font-size: .78rem;
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;
}

/* Float cluster */
.float-cluster {
  position: fixed; right: 1.6rem; bottom: 1.6rem; z-index: 900;
  width: 56px;
}

/* the always-visible backdrop / trigger — the four icons cluster on top of it */
.fab-toggle {
  width: 56px; height: 56px; border-radius: 50%; border: none;
  background: linear-gradient(135deg, var(--purple), var(--purple-deep));
  box-shadow: 0 14px 30px -8px rgba(75, 46, 99, .5);
  position: relative; display: block; padding: 0;
  transition: box-shadow .4s;
}
.fab-close {
  position: absolute; inset: 0; opacity: 0; transform: scale(.4);
  transition: opacity .3s var(--ease-fluid), transform .4s var(--ease-fluid);
}
.fab-close::before, .fab-close::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 22px; height: 2.5px; border-radius: 3px; background: var(--gold-soft);
}
.fab-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.fab-close::after  { transform: translate(-50%, -50%) rotate(-45deg); }

/* The four real buttons. Collapsed = shrunk + tilted, clustered on top of the
   toggle (the --tx/--ty/--rot below are tuned for 56px buttons + .75rem gap so
   each lands in a loose 2x2 over the toggle). Open = transform:none, so the
   SAME elements glide up to their natural column spots and grow to full size. */
.fab-items {
  position: absolute; right: 0; bottom: 100%;
  display: flex; flex-direction: column; align-items: center; gap: .65rem;
  padding-bottom: .85rem;
  pointer-events: none;            /* never blocks the toggle */
}
.fab-items > a {
  pointer-events: none;
  transition: transform .55s var(--ease-fluid), box-shadow .4s;
  transition-delay: calc(var(--i, 0) * .05s);   /* nearest button leads */
  transform: translate(var(--tx), var(--ty)) scale(.34) rotate(var(--rot));
}
/* Collapsed cluster: each button's natural open slot is 68px higher than the
   one below it, so the base --ty (80 + 68*index) lands every icon on the same
   spot over the toggle. The extra few px + the --tx scatter then push each one
   a different way so all six peek out instead of stacking — irregular on
   purpose (no neat grid), with a random-feeling tilt per icon. */
.yt-float  { --tx:   6px; --ty: 360px; --rot: -12deg; --i: 5; --open-rot:  -7deg; }
.fb-float  { --tx:  -7px; --ty: 317px; --rot:   8deg; --i: 4; --open-rot:   9deg; }
.wa-float  { --tx:  11px; --ty: 241px; --rot: -11deg; --i: 3; --open-rot:  -5deg; }
.ig-float  { --tx: -12px; --ty: 182px; --rot:   7deg; --i: 2; --open-rot:   6deg; }
.rev-float { --tx:  12px; --ty: 133px; --rot:  14deg; --i: 1; --open-rot: -10deg; }
.maps-float{ --tx: -11px; --ty:  71px; --rot:  -9deg; --i: 0; --open-rot:   4deg; }

/* No-background variant: just the transparent PNG logos, no disc behind them.
   A soft drop-shadow keeps each logo legible on any page background. */
.wa-float, .maps-float, .ig-float, .rev-float, .fb-float, .yt-float {
  width: 50px; height: 50px;    /* smaller box shrinks both icon and spacing */
  display: grid; place-items: center;
  padding: 2.5px;
  filter: drop-shadow(0 6px 12px rgba(0,0,0,.30));
}
.maps-float { padding: 6px; }   /* maps pin reads big — keep it the smallest */
/* hover wiggle lives on the image so it never fights the link's morph transform */
.wa-float img, .maps-float img, .ig-float img, .rev-float img, .fb-float img, .yt-float img {
  width: 100%; height: 100%; object-fit: contain;
  transition: transform .4s var(--ease-fluid);
}

/* ---- open state ----
   .open is the tap path and works on every device. */
.float-cluster.open .fab-items > a { transform: rotate(var(--open-rot, 0deg)); pointer-events: auto; }
.float-cluster.open .fab-close { opacity: 1; transform: scale(1); }
.float-cluster.open .fab-toggle { box-shadow: 0 16px 34px -8px rgba(75, 46, 99, .62); }

/* ---- hover-to-open, mouse only ----
   Gated on `html.fine-pointer`, which main.js sets from
   `(hover: hover) and (pointer: fine)` and then REMOVES on the first touch
   `pointerdown`. A media query alone was not enough: plenty of Android devices
   and hybrid laptops answer `(hover: hover)` truthfully-ish, and a tap there
   fires an emulated hover. The class self-corrects the moment a real finger
   arrives, which no media query can do.

   The bigger half of the bug: `.fab-items` had `pointer-events: auto`
   unconditionally inside the old query. It is a ~366px tall invisible column
   standing above the 56px button (six 50px links plus gaps — the collapsed
   icons only *look* clustered on the toggle, their layout boxes stay up in
   that column). So on any device that claimed hover, tapping empty space well
   above the button landed on .fab-items and opened the menu.

   The bridge only ever existed to stop the menu closing while the mouse
   travels from the toggle up to an icon, which means it is only needed once
   the menu is already open. Scoped to :hover it costs nothing when closed:
   hovering the 56px toggle opens the menu, which makes the bridge live, which
   keeps :hover true as the pointer moves up. Closed, there is no hit area
   above the button at all — on any device, however it answers the query. */
html.fine-pointer .float-cluster:hover .fab-items { pointer-events: auto; }
html.fine-pointer .float-cluster:hover .fab-items > a { transform: rotate(var(--open-rot, 0deg)); pointer-events: auto; }
html.fine-pointer .float-cluster:hover .fab-close { opacity: 1; transform: scale(1); }
html.fine-pointer .float-cluster:hover .fab-toggle { box-shadow: 0 16px 34px -8px rgba(75, 46, 99, .62); }
html.fine-pointer .wa-float:hover img,
html.fine-pointer .ig-float:hover img,
html.fine-pointer .fb-float:hover img { transform: scale(1.12) rotate(8deg); }
html.fine-pointer .maps-float:hover img,
html.fine-pointer .rev-float:hover img,
html.fine-pointer .yt-float:hover img { transform: scale(1.12) rotate(-8deg); }

/* Google reviews badge in strand section */
.google-rev-badge {
  position: absolute; left: 2rem; bottom: 2rem; z-index: 10;
  display: flex; align-items: center; gap: .5rem;
  background: rgba(255,255,255,.12); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.2); border-radius: 2rem;
  padding: .45rem .9rem .45rem .6rem;
  color: #fff; font-size: .82rem; font-weight: 600; letter-spacing: .02em;
  text-decoration: none;
  transition: background .3s, transform .4s var(--ease-fluid);
}
.google-rev-badge img { width: 22px; height: 22px; object-fit: contain; }
.google-rev-badge:hover { background: rgba(255,255,255,.22); transform: scale(1.05); }

/* back-to-top progress */
.progress-bar {
  position: fixed; top: 0; left: 0; height: 3px; width: 0%;
  background: linear-gradient(90deg, var(--gold), var(--purple));
  z-index: 1200;
}

/* ----------------------------------------------------------------
   Logo & brand
---------------------------------------------------------------- */
.brand-logo {
  height: 64px; width: auto; display: block;
  transition: height .35s var(--ease-fluid);
}
.brand-wordmark {
  height: 46px; width: auto; display: block;
  transition: height .35s var(--ease-fluid);
}
.site-header.scrolled .brand-logo { height: 50px; }
.site-header.scrolled .brand-wordmark { height: 36px; }
@media (max-width: 560px) {
  .brand-logo { height: 48px; }
  .brand-wordmark { height: 34px; }
}
.footer-logo-chip {
  display: inline-flex; align-items: center; gap: .7rem;
  background: #fff; border-radius: 14px;
  padding: .65rem 1rem; box-shadow: var(--shadow-card);
}
.footer-logo-chip .chip-logo { width: 44px; display: block; }
.footer-logo-chip .chip-name { width: 140px; display: block; }

/* ----------------------------------------------------------------
   Uploaded photos inside placeholder slots
---------------------------------------------------------------- */
.ph.has-img::before, .ph.has-img::after { display: none; }
.ph .ph-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.ph.has-img { min-height: 220px; }
.ph-tall.has-img, .ph-wide.has-img, .ph-square.has-img, .ph-portrait.has-img { min-height: 0; }

/* ----------------------------------------------------------------
   Shared FX bits (step text crossfade, counters)
---------------------------------------------------------------- */
.fx-stage {
  position: relative; height: 100vh; height: 100dvh;
  display: flex; align-items: center; overflow: hidden;
}
.orbit-step, .flip-step {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: center;
  opacity: 0; transform: translateY(70px);
  transition: opacity .6s var(--ease-fluid), transform .6s var(--ease-fluid);
  pointer-events: none;
}
.orbit-step.is-active, .flip-step.is-active { opacity: 1; transform: none; pointer-events: auto; }
.orbit-step.is-prev, .flip-step.is-prev { opacity: 0; transform: translateY(-70px); }
.orbit-step h2, .flip-step h2 { margin-top: .9rem; }
.orbit-step p, .flip-step p { margin-top: 1.1rem; color: var(--ink-soft); max-width: 460px; }
.step-no {
  /* var(--font-body) (Jost) on purpose, not --font-display: Cormorant's "1" is
     a bare vertical stroke with top/bottom serifs, which outlined via
     text-stroke below is indistinguishable from a capital "I" ("01" read as
     "0I"). Jost's "1" carries a diagonal flag that survives the outline. */
  font-family: var(--font-body); font-size: 4.2rem; font-weight: 600; line-height: 1;
  color: transparent; -webkit-text-stroke: 1.5px var(--gold);
  margin-bottom: .6rem; display: block;
}
.orbit-count, .flip-count {
  position: absolute; left: 50%; bottom: 1.6rem; transform: translateX(-50%);
  font-family: var(--font-accent); letter-spacing: .3em;
  font-size: .8rem; color: var(--ink-soft); z-index: 5;
}
.orbit-count b, .flip-count b { color: var(--purple); font-weight: 400; }

/* ----------------------------------------------------------------
   HOME â€” Golden Orbit
---------------------------------------------------------------- */
.orbit-section { position: relative; padding: 0; }
.orbit-stage {
  position: relative; height: 100vh; height: 100dvh;
  display: flex; align-items: center; overflow: hidden;
  background: linear-gradient(180deg, var(--white) 0%, var(--cream-soft) 100%);
}
/* The section pins full-height behind the fixed header, so centring the orbit
   in the whole viewport tucks the top (active) card under the header bar.
   Reserve the header's height up top so the orbit centres in the visible area.
   Desktop only — the mobile layout pushes the wheel down via its own margin. */
@media (min-width: 921px) {
  .orbit-stage { padding-top: 90px; }
}
.orbit-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem); align-items: center; width: 100%;
}
.orbit-copy { position: relative; min-height: 380px; }
.orbit-wheelbox { position: relative; display: grid; place-items: center; }
.orbit-wheel {
  /* also cap by viewport height: the active card rides one radius above the
     wheel centre, so on short windows an unconstrained wheel pushes that card
     past the top of the pinned viewport (and under the fixed header). 52vh
     keeps the whole wheel — active card included — on-screen. */
  position: relative; width: min(34vw, 460px, 52vh); aspect-ratio: 1;
}
.orbit-card {
  position: absolute; top: 50%; left: 50%;
  width: 46%; aspect-ratio: 1; margin: -23% 0 0 -23%;
}
.orbit-card-inner {
  width: 100%; height: 100%;
  /* no transition: the scale is a continuous function of scroll progress,
     rewritten by main.js on essentially every frame. A CSS transition here
     restarts on each of those writes and keeps chasing a target that has
     already moved again, which reads as a soft/blurred scale instead of a
     crisp one — the same mechanism the <=880px and lite/static overrides
     below already disable it for; this just makes that the one true rule
     instead of a tier-specific patch. */
  backface-visibility: hidden;
}
/* Only while the section is actually near the viewport — see the .fx-live
   observer in main.js.

   The card and its inner were each promoted separately (will-change on both,
   plus a translateZ(0) on the inner), so every card cost two full-size GPU
   textures for the entire life of the page whether or not the wheel was
   anywhere near the screen. One layer per card, only when it is needed: the
   inner is what main.js scales per frame, and it carries the card's whole
   subtree with it. */
.orbit-section.fx-live .orbit-card-inner { will-change: transform; }
.orbit-card .ph { height: 100%; box-shadow: var(--shadow-card); }
.orbit-card.on .ph { box-shadow: var(--shadow-soft); }
.orbit-ring {
  position: absolute; width: 118%; aspect-ratio: 1; border-radius: 50%;
  border: 1.5px dashed rgba(168, 127, 46, .45);
  transform: rotate(calc(var(--p, 0) * 360deg));
}
.orbit-ring::before {
  content: "\25C6"; position: absolute; top: -10px; left: 50%;
  color: var(--gold); font-size: .75rem;
}
.orbit-ring-2 {
  width: 86%; border-color: rgba(94, 59, 126, .25);
  transform: rotate(calc(var(--p, 0) * -260deg));
}
.orbit-ring-2::before { color: var(--purple-soft); }
.orbit-center {
  position: absolute; width: 92px; height: 92px; border-radius: 50%;
  background: #fff; box-shadow: var(--shadow-soft);
  display: grid; place-items: center; overflow: hidden; z-index: 3;
}
.orbit-center img { width: 72%; }

.orbit-section.fx-static { padding: clamp(4rem, 9vw, 7.5rem) 0; }
.orbit-section.fx-static .orbit-stage { position: static; height: auto; display: block; }
.orbit-section.fx-static .orbit-grid { grid-template-columns: 1fr; }
.orbit-section.fx-static .orbit-copy { min-height: 0; }
.orbit-section.fx-static .orbit-step {
  position: static; opacity: 1; transform: none; pointer-events: auto; margin-bottom: 2.2rem;
}
.orbit-section.fx-static .orbit-count { display: none; }
.orbit-section.fx-static .orbit-ring, .orbit-section.fx-static .orbit-ring-2,
.orbit-section.fx-static .orbit-center { display: none; }
.orbit-section.fx-static .orbit-wheel {
  width: 100%; aspect-ratio: auto;
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
}
.orbit-section.fx-static .orbit-card {
  position: static; width: 100%; margin: 0; opacity: 1 !important; transform: none !important;
}

/* ----------------------------------------------------------------
   COLLECTIONS â€” horizontal scroll-jack gallery
---------------------------------------------------------------- */
.hscroll-section { position: relative; padding: 0; }
.hscroll-stage {
  position: relative; height: 100vh; height: 100dvh;
  overflow: hidden; display: flex; align-items: center;
  background: linear-gradient(160deg, var(--plum) 0%, var(--purple-deep) 100%);
}
.hscroll-track {
  display: flex; align-items: center; gap: clamp(2rem, 5vw, 5rem);
  padding: 0 clamp(2rem, 8vw, 8rem);
}
.hpanel {
  flex: 0 0 auto; width: min(78vw, 560px);
  display: grid; grid-template-columns: 1fr 1fr; gap: 1.6rem; align-items: center;
  color: var(--cream); position: relative;
}
.hpanel-no {
  position: absolute; top: -1.2em; left: -.2em; z-index: 0;
  font-family: var(--font-display); font-size: 7rem; font-weight: 700; line-height: 1;
  color: transparent; -webkit-text-stroke: 1.5px rgba(221, 188, 119, .5);
}
.hpanel-img { position: relative; z-index: 1; }
/* main.js writes a fresh transform to the track and to every .hpanel-img on
   every scroll frame, so both want pre-promoting to avoid a late
   layer-promotion pop mid-slide. Scoped to .fx-live (see main.js) because the
   panels are a wide off-screen strip — the whole point of the section — and
   promoting all of them for the life of the page was the largest single block
   of layer memory on the product page. */
.hscroll-section.fx-live .hscroll-track,
.hscroll-section.fx-live .hpanel-img { will-change: transform; }
.hpanel-body { position: relative; z-index: 2; }
.hpanel-img .ph { box-shadow: 0 30px 70px -25px rgba(0, 0, 0, .6); }
.hpanel-body h3 { color: var(--white); font-size: 1.6rem; }
.hpanel-body p { margin-top: .8rem; color: rgba(251, 246, 234, .9); font-size: .95rem; }
.hpanel-intro, .hpanel-end {
  display: block; width: min(70vw, 480px); text-align: left;
}
.hpanel-intro h2, .hpanel-end h2 { color: var(--white); margin-top: .9rem; }
.hpanel-intro .eyebrow { color: var(--gold-soft); }
.hpanel-intro .eyebrow::before { background: var(--gold-soft); }
.hpanel-sub { margin-top: 1rem; color: rgba(251, 246, 234, .78); }
.hscroll-arrow {
  display: inline-block; margin-top: 1.6rem; font-size: 2.2rem; color: var(--gold-soft);
  animation: nudge 1.6s var(--ease-fluid) infinite;
}
@keyframes nudge { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(16px); } }
.hpanel-end { text-align: center; }
.hpanel-end .btn { margin-top: 1.8rem; }
.hscroll-progress {
  position: absolute; left: 10%; right: 10%; bottom: 2rem; height: 2px;
  background: rgba(251, 246, 234, .15); border-radius: 2px;
}
.hscroll-progress span {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--gold-soft), var(--gold)); border-radius: 2px;
}

.hscroll-section.fx-static .hscroll-stage {
  position: static; height: auto; padding: clamp(3rem, 8vw, 5rem) 0;
}
.hscroll-section.fx-static .hscroll-track {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory; padding: 0 6vw 1.4rem;
}
.hscroll-section.fx-static .hpanel { scroll-snap-align: center; width: 82vw; grid-template-columns: 1fr; }
.hscroll-section.fx-static .hscroll-progress { display: none; }

/* ----------------------------------------------------------------
   OUR STORY â€” golden thread timeline
---------------------------------------------------------------- */
.timeline.thread { position: relative; }
.timeline.thread::before { display: none; } /* SVG replaces the static spine */
.thread-svg {
  position: absolute; top: 0; left: -43px; width: 100px; height: 100%;
  overflow: visible; pointer-events: none;
}
.thread-path-bg {
  fill: none; stroke: rgba(94, 59, 126, .15); stroke-width: 2;
}
/* Was filter: drop-shadow(0 0 6px ...) on .thread-path itself — main.js
   rewrites its strokeDashoffset every scroll frame as the reveal tip moves,
   so a live filter meant re-rasterizing a blurred stroke over the section's
   entire scroll length on every frame. .thread-path-glow fakes the same
   glow with a plain wide, low-opacity, unfiltered stroke underneath,
   sharing the same dashoffset — a second cheap paint instead of one
   expensive one. */
.thread-path-glow {
  fill: none; stroke: #B8893A; stroke-width: 7; stroke-linecap: round;
  opacity: .32;
}
.thread-path {
  fill: none; stroke: #B8893A; stroke-width: 2.5; stroke-linecap: round;
}
.timeline-item.thread-node::before { display: none; }
.thread-gem {
  position: absolute; left: -2.55rem; top: 2px;
  font-size: 1rem; color: var(--beige-deep);
  transform: scale(.7); transition: color .5s, transform .5s var(--ease-fluid), text-shadow .5s;
}
.thread-node.lit .thread-gem {
  color: var(--gold); transform: scale(1.25) rotate(45deg);
  text-shadow: 0 0 14px rgba(168, 127, 46, .8);
}
.thread-node { opacity: .45; transition: opacity .6s var(--ease-fluid); }
.thread-node.lit { opacity: 1; }

/* ----------------------------------------------------------------
   OUR APP â€” 3D phone flip
---------------------------------------------------------------- */
.flip-section { position: relative; padding: 0; }
.flip-stage {
  position: relative; height: 100vh; height: 100dvh;
  display: flex; align-items: center; overflow: hidden;
  background:
    radial-gradient(700px 420px at 110% 10%, rgba(168, 127, 46, .22), transparent 60%),
    linear-gradient(160deg, var(--plum) 0%, var(--purple-deep) 60%, var(--purple) 100%);
}
.flip-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem); align-items: center; width: 100%;
}
.flip-copy { position: relative; min-height: 380px; }
.flip-step h2 { color: var(--white); }
.flip-step p { color: rgba(251, 246, 234, .92); }
.flip-step .eyebrow { color: var(--gold-soft); }
.flip-step .eyebrow::before { background: var(--gold-soft); }
.flip-step .step-no { -webkit-text-stroke-color: var(--gold-soft); }
.flip-count { color: rgba(251, 246, 234, .72); }
.flip-count b { color: var(--gold-soft); }

.flip-phonebox {
  position: relative; display: grid; place-items: center;
  perspective: 1400px;
}
.flip-phonebox::before {
  content: ""; position: absolute; width: 130%; aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle, rgba(221, 188, 119, .22) 0%, rgba(150, 120, 180, .12) 45%, transparent 70%);
  pointer-events: none;
}
.flip-phonebox::after {
  content: ""; position: absolute; width: 108%; aspect-ratio: 1; border-radius: 50%;
  border: 1.5px dashed rgba(221, 188, 119, .35); pointer-events: none;
}
/* Every dimension of the phone body is derived from --pw, so the whole thing
   scales as one object. It used to be a 280px shell with a hardcoded 14px
   bezel, 38px radius and +/-9px of depth; at the 150px mobile width those same
   absolute values became 9.3% / 25% / 12% of the width instead of 5% / 13.5% /
   6.4%, so the phone rendered nearly twice as thick and far more rounded than
   on desktop — which is what read as "short and fat" on a handset. Only --pw
   changes per breakpoint now. */
.flip-phone {
  --pw: 280px;
  --slab: calc(var(--pw) * 0.032);   /* half-thickness, so the body is 6.4% deep */
  --bezel: calc(var(--pw) * 0.05);
  --rad: calc(var(--pw) * 0.135);
  --screen-rad: calc(var(--pw) * 0.093);
  width: min(var(--pw), 70%); transform-style: preserve-3d;
}
/* preserve-3d here too, not just on .flip-phone — otherwise this
   intermediate wrapper can flatten the compound rotation (shell x face)
   into a 2D context in some engines, and the per-face static rotateY set in
   main.js stops compounding with the shell's rotation correctly. */
.flip-screen { position: relative; aspect-ratio: 9 / 19; transform-style: preserve-3d; }
.flip-face {
  position: absolute; inset: 0;
  /* no transition, on purpose: main.js writes visibility here at each face
     boundary. With a transition running, a momentary scroll-speed overshoot
     right at a boundary (very easy to trigger scrolling fast into a section
     for the first time) restarts the fade before it finishes, so the middle
     face can flicker in and out without ever visibly landing — the exact
     "screen 2 gets skipped" bug. Same fix as .orbit-card-inner. */
  backface-visibility: hidden;
  /* Hidden by default, with the first face shown, so the correct screen is up
     before main.js has said anything — and stays correct if it never runs at
     all. Without this the faces are all visible and paint order (DOM order)
     picks the winner, which is the last face: the section used to open on
     screen 3. initFlip takes over from here. */
  visibility: hidden;
}
.flip-face:first-child { visibility: visible; }
.flip-face .ph { height: 100%; border-radius: var(--screen-rad); aspect-ratio: auto; min-height: 0; }
/* Was backdrop-filter: blur(6px) — main.js rewrites this element's
   transform every scroll frame (see initFlip), so a moving,
   per-frame-transformed, backdrop-blurred pill was close to worst-case for
   the compositor: a fresh backdrop region re-blurred every frame. Bumped
   the fill's own opacity to keep the "glass pill" look without it. */
.flip-chip {
  position: absolute; z-index: 3;
  background: rgba(251, 246, 234, .16); border: 1px solid rgba(221, 188, 119, .4);
  color: var(--gold-soft); font-size: .8rem; letter-spacing: .08em;
  padding: .5rem 1rem; border-radius: 100px;
}
/* Both are written per scroll frame by initFlip, and both only exist to be
   written while this section is on screen — see the .fx-live observer. */
.flip-section.fx-live .flip-phone,
.flip-section.fx-live .flip-chip { will-change: transform; }
.flip-chip:nth-of-type(1) { top: 16%; left: 4%; }
.flip-chip:nth-of-type(2) { top: 30%; right: 2%; }
.flip-chip:nth-of-type(3) { bottom: 26%; left: 0; }
.flip-chip:nth-of-type(4) { bottom: 12%; right: 6%; }

.flip-section.fx-static { padding: clamp(4rem, 9vw, 7.5rem) 0; }
.flip-section.fx-static .flip-stage { position: static; height: auto; display: block; padding: 3rem 0; }
.flip-section.fx-static .flip-grid { grid-template-columns: 1fr; }
.flip-section.fx-static .flip-copy { min-height: 0; }
.flip-section.fx-static .flip-step {
  position: static; opacity: 1; transform: none; pointer-events: auto; margin-bottom: 2.2rem;
}
.flip-section.fx-static .flip-count { display: none; }
.flip-section.fx-static .flip-chip { display: none; }
.flip-section.fx-static .flip-phonebox { margin-top: 2rem; }
.flip-section.fx-static .flip-face { opacity: 0; visibility: hidden; }
.flip-section.fx-static .flip-face:first-child { opacity: 1; visibility: visible; }

/* ----------------------------------------------------------------
   BLOGS â€” card deck stack
---------------------------------------------------------------- */
.deck { position: relative; max-width: 880px; margin: 0 auto; }
.deck-card {
  position: sticky; top: calc(96px + var(--i, 0) * 18px);
  display: grid; grid-template-columns: 1fr 1.2fr;
  background: var(--white); border-radius: var(--radius); overflow: hidden;
  box-shadow: 0 -10px 40px -18px rgba(62, 35, 84, .35), var(--shadow-soft);
  margin-bottom: 5vh; min-height: 320px;
  transform-origin: top center;
  text-decoration: none; color: inherit;
}
/* position: sticky already earns these a layer while they are stuck; the hint
   only needs to be up while the deck is on screen and main.js is scaling them. */
.deck.fx-live .deck-card { will-change: transform; }
.deck-card:last-child { margin-bottom: 0; }
/* Dimming overlay for the stacking effect. main.js drives --deck-dim as each
   card is covered by the next. This replaces a per-frame
   `filter: brightness()` write: .deck-card is position:sticky and therefore
   already its own compositor layer, and changing `filter` on it forced a full
   re-raster of that layer every frame. Animating a child's opacity instead is
   handled by the compositor with no repaint. */
.deck-card::after {
  content: ""; position: absolute; inset: 0;
  border-radius: inherit; pointer-events: none;
  background: #1a0f24;
  opacity: var(--deck-dim, 0);
}
.deck-media .ph { height: 100%; border-radius: 0; min-height: 240px; }
.deck-body { padding: 2rem 2.2rem; display: flex; flex-direction: column; justify-content: center; }
.deck-body h3 { font-size: 1.5rem; margin-bottom: .7rem; transition: color .35s; }
.deck-card:hover h3 { color: var(--purple); }
.deck-body p { color: var(--ink-soft); font-size: .94rem; }
.deck-body .read-more { margin-top: 1.2rem; }
.deck-card:hover .read-more .arr { transform: translateX(8px); }
@media (max-width: 720px) {
  .deck-card { grid-template-columns: 1fr; min-height: 0; }
  .deck-media .ph { min-height: 170px; }
  .deck-body { padding: 1.4rem 1.5rem; }
}

.blog-card-link { display: flex; flex-direction: column; height: 100%; color: inherit; }

/* ----------------------------------------------------------------
   Blog article page
---------------------------------------------------------------- */
.post-hero { padding-bottom: clamp(2rem, 4vw, 3rem); }
.post-back {
  font-size: .8rem; letter-spacing: .18em; text-transform: uppercase; color: var(--purple);
}
.post-back:hover { color: var(--gold); }
.post-wrap { padding: 0 0 clamp(3rem, 7vw, 5rem); }
.post-container { max-width: 820px; }
.post-cover { margin-bottom: clamp(2rem, 5vw, 3.2rem); box-shadow: var(--shadow-soft); }
.post-body { font-size: 1.06rem; line-height: 1.9; color: var(--ink); }
.post-body p { margin-bottom: 1.4rem; }
.post-body h2, .post-body h3 { margin: 2.2rem 0 1rem; }
.post-body blockquote {
  border-left: 3px solid var(--gold); padding: .4rem 0 .4rem 1.4rem; margin: 1.8rem 0;
  font-family: var(--font-display); font-style: italic; font-size: 1.25rem; color: var(--purple-deep);
}
.post-body ul, .post-body ol { margin: 0 0 1.4rem 1.4rem; }
.post-body a { color: var(--purple); text-decoration: underline; }

/* ----------------------------------------------------------------
   Misc additions
---------------------------------------------------------------- */
.form-success {
  background: #e3efdb; color: #2c5e2a; border: 1px solid #b9d4ab;
  border-radius: 10px; padding: .9rem 1.2rem; margin-bottom: 1.4rem; font-size: .92rem;
}
.store-badge-light {
  background: var(--white); border-color: var(--beige-deep); color: var(--ink);
}
.store-badge-light small { color: var(--ink-soft); }
.store-badge-light:hover { background: #fff; }

@media (max-width: 880px) {
  .orbit-grid, .flip-grid { grid-template-columns: 1fr; }
}

/* ================================================================
   GSAP-ERA EFFECTS (zoom word, era walk, invitation,
   word splits, letters, flip glow)
   ================================================================ */

/* ---------- HOME: Offers / Latest Banners slideshow ---------- */
.banners-section { padding: clamp(2rem, 4vw, 3.5rem) 0; background: var(--cream-soft); }
.banner-stage {
  position: relative; overflow: hidden;
  width: 100%;
  border-radius: 18px;
  box-shadow: var(--shadow-soft);
}
/* The first slide sits in normal flow and sets the stage height from the
   image's own size (no forced ratio, no empty bars). The rest stack on top
   of it, absolutely positioned, for the cross-fade. */
/* No will-change: opacity here, for the same reason .reveal does not have one
   (see the note at the top of this file). Browsers already promote an element
   for the duration of an opacity transition, and these slides sit at the top
   of the homepage — the hint kept every slide on its own full-width texture
   for the whole visit to buy a promotion that lasts .85s every few seconds. */
.banner-slide {
  opacity: 0;
  transition: opacity .85s cubic-bezier(.4,0,.2,1);
}
.banner-slide:not(:first-child) { position: absolute; inset: 0; }
.banner-slide.is-active { opacity: 1; z-index: 1; }
.banner-link { display: block; width: 100%; height: 100%; }
/* first slide (in flow) shows the image at its natural ratio → sets the height */
.banner-slide:first-child .banner-media { width: 100%; height: auto; }
/* the stacked slides fill that same height */
.banner-slide:not(:first-child) .banner-media {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.banner-media { display: block; }
.banner-link { height: 100%; }
.banner-slide:first-child .banner-link { height: auto; }
/* the viewport is the same width as the banner; the arrows are placed in the
   page gutters just OUTSIDE the stage, so they never cover the image and the
   banner keeps its full width */
.banner-viewport { position: relative; }
.banner-arrow {
  position: absolute; top: 50%; z-index: 901;
  transform: translateY(-50%);
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--purple);
  color: var(--gold-soft);
  border: none; cursor: pointer;
  font-size: 1.6rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  transition: background .2s, transform .2s;
  box-shadow: var(--shadow-soft);
}
.banner-arrow:hover { background: var(--purple-deep); transform: translateY(-50%) scale(1.1); }
.banner-prev { left: -58px; }
.banner-next { right: -58px; }
/* dots */
.banner-dots {
  position: absolute; bottom: .9rem; left: 50%; z-index: 901;
  transform: translateX(-50%);
  display: flex; gap: .45rem; align-items: center;
}
.banner-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: rgba(255,255,255,.5);
  border: none; cursor: pointer; padding: 0;
  transition: background .25s, transform .25s;
}
.banner-dot.on {
  background: var(--gold-soft);
  transform: scale(1.35);
}
/* faded preview strip of every banner, below the slideshow */
.banner-thumbs {
  display: flex; flex-wrap: wrap; gap: .7rem;
  margin-top: 1rem; justify-content: center;
}
.banner-thumb {
  flex: 0 0 auto;
  width: clamp(90px, 12vw, 150px);
  aspect-ratio: 2 / 1;
  border: 2px solid transparent;
  border-radius: 8px;
  overflow: hidden;
  padding: 0; cursor: pointer;
  background: var(--cream);
  opacity: .45;
  filter: saturate(.85);
  transition: opacity .3s, transform .3s, border-color .3s, box-shadow .3s;
}
.banner-thumb:hover { opacity: .85; transform: translateY(-2px); }
.banner-thumb.on {
  opacity: 1;
  filter: none;
  border-color: var(--gold);
  box-shadow: var(--shadow-soft);
}
.banner-thumb-media {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  display: block;
}
/* desktop (landscape) slideshow shows by default; the phone (portrait) one is
   swapped in at the mobile breakpoint further down */
.banner-variant--mobile { display: none; }
/* the portrait stage gets a comfortable max width so it never blows up to full
   container width on the largest phones */
.banner-stage--mobile { max-width: 420px; margin-inline: auto; }

/* ---------- OUR STORY: Era Walk ---------- */
.era-section { padding: 0; position: relative; }
/* The cream -> gold wash across the era walk is two stacked flat fills whose
   *opacity* is crossfaded, not one background-color rewritten as progress
   changes. initEra used to write backgroundColor (quantized to 50 steps, but
   still 50 times per pass) and each of those repainted a full 100vh pinned
   surface — pure fill rate, which is what a weak mobile GPU is worst at.
   Opacity on an already-composited layer costs nothing to change. */
.era-stage {
  position: relative; height: 100vh; height: 100dvh;
  display: flex; align-items: center; overflow: hidden;
  background: #EFE5D0; /* first stop of the old bgInterp scale */
}
.era-stage::before,
.era-stage::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
}
.era-stage::before { background: #F3E8CC; opacity: var(--era-mid, 0); }
.era-stage::after  { background: #EFDEB9; opacity: var(--era-end, 0); }
.era-stage > * { position: relative; z-index: 1; }
.era-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem); align-items: center; width: 100%;
}
.era-yearbox {
  position: relative; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 2rem;
}
.era-numwrap { position: relative; }
.era-medal {
  /* The photograph is the point of this section, so it gets the room: was
     min(24vw, 300px). The grid column is half the container, so 30vw still
     leaves the dashed ring at inset -16px well clear of the year numeral
     above it and of the copy column beside it. */
  position: relative; width: min(30vw, 380px, 44vh); aspect-ratio: 1;
  perspective: 800px;
}
.era-medal::before {
  content: ""; position: absolute; inset: -16px; border-radius: 50%;
  border: 1.5px dashed rgba(168, 127, 46, .55);
  animation: medal-spin 16s linear infinite;
}
.era-medal::after {
  content: "\25C6"; position: absolute; top: -28px; left: 50%; transform: translateX(-50%);
  color: var(--gold); font-size: .7rem;
}
@keyframes medal-spin { to { transform: rotate(360deg); } }
.era-photo {
  position: absolute; inset: 0; border-radius: 50%;
  opacity: 0; transform: rotateY(95deg) scale(.8);
  transition: opacity .6s var(--ease-fluid), transform .65s var(--ease-fluid);
}
.era-photo .ph {
  height: 100%; border-radius: 50%; overflow: hidden;
  border: 5px solid var(--gold-soft); box-shadow: var(--shadow-soft);
}
.era-photo .ph .ph-label { font-size: .7rem; }
.era-photo .ph .ph-icon { font-size: 1.4rem; }
.era-photo.on { opacity: 1; transform: rotateY(0) scale(1); }
.era-num {
  position: absolute; inset: 0;
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(4.5rem, 11vw, 10rem);
  color: transparent; -webkit-text-stroke: 2px #B8893A;
  display: grid; place-items: center;
}
.era-num-ghost { /* reserves layout width for the absolute counter */
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(4.5rem, 11vw, 10rem); visibility: hidden;
}
.era-copy { position: relative; min-height: 340px; }
.era-step {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; justify-content: center;
  opacity: 0; transform: translateY(60px);
  transition: opacity .55s var(--ease-fluid), transform .55s var(--ease-fluid);
  pointer-events: none;
}
.era-step.is-active { opacity: 1; transform: none; pointer-events: auto; }
.era-step.is-prev { opacity: 0; transform: translateY(-60px); }
.era-step h2 { margin-top: .9rem; }
.era-step p:not(.eyebrow) { margin-top: 1.1rem; color: var(--ink-soft); max-width: 460px; }
.era-count {
  position: absolute; left: 50%; bottom: 1.6rem; transform: translateX(-50%);
  font-family: var(--font-accent); letter-spacing: .3em;
  font-size: .8rem; color: var(--ink-soft);
}
.era-count b { color: var(--purple); font-weight: 400; }
@media (max-width: 880px) {
  .era-grid { grid-template-columns: 1fr; gap: 1rem; align-content: center; }
  .era-num, .era-num-ghost { font-size: clamp(3.4rem, 16vw, 6rem); }
  .era-copy { min-height: 300px; }
  .era-step { justify-content: flex-start; }
}

/* ---------- CONTACT: gold underline (drawn by scroll) ---------- */
.gold-underline {
  display: block; height: 3px; width: 130px; margin-bottom: 1.4rem;
  background: linear-gradient(90deg, var(--gold), var(--gold-soft));
  border-radius: 2px; transform-origin: left;
  transform: scaleX(0);
  transition: transform 1s var(--ease-fluid) .35s;
}
/* The rule draws itself as the form arrives. This was a GSAP scaleX tween inside
   the old pinned initInvite timeline; with that gone it rides the .in-view flag
   the reveal observer already puts on the form. */
.contact-form.in-view .gold-underline { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) { .gold-underline { transform: none; transition: none; } }

/* ---------- h2 word-split masks ---------- */
.wm { display: inline-block; overflow: hidden; vertical-align: bottom; }
/* one span per word of every h2 on the page — will-change here meant ~100
   permanently promoted layers on the home page alone. GSAP promotes each word
   for the duration of its own tween, which is all this ever needed. */
.wm .wi { display: inline-block; }

/* ---------- Blogs: scattered letters title ---------- */
.deck-intro {
  position: relative; min-height: 58vh;
  display: grid; place-items: center; overflow: hidden;
}
.deck-title { font-size: clamp(2.4rem, 6vw, 4.4rem); position: relative; z-index: 2; }
/* one span per letter — same reasoning as .wm .wi above */
.deck-title .ltr { display: inline-block; white-space: pre; }
.intro-ghost {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  font-family: var(--font-display); font-weight: 700; line-height: 1;
  font-size: clamp(5rem, 16vw, 13rem); white-space: nowrap;
  text-transform: uppercase; letter-spacing: .06em;
  color: transparent; -webkit-text-stroke: 1.5px rgba(207, 187, 148, .55);
  z-index: 0; pointer-events: none;
}
.intro-floats { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.intro-float {
  position: absolute; width: 58px; height: 58px; border-radius: 50%;
  display: grid; place-items: center; font-size: 1.5rem;
  background: var(--white); border: 1.5px solid var(--gold-soft);
  box-shadow: var(--shadow-card);
}
.intro-float:nth-child(1) { top: 12%; left: 12%; }
.intro-float:nth-child(2) { top: 16%; right: 13%; }
.intro-float:nth-child(3) { bottom: 18%; left: 18%; }
.intro-float:nth-child(4) { bottom: 12%; right: 18%; }
.intro-float:nth-child(5) { top: 44%; left: 5%; }
.intro-float:nth-child(6) { top: 48%; right: 6%; }
.deck-card { rotate: calc((var(--i, 0) - 1.5) * 0.7deg); }

/* ---------- Our App: glass glow sweep ---------- */
.flip-phone { position: relative; }
.flip-glow {
  position: absolute; inset: var(--bezel); z-index: 6; border-radius: var(--screen-rad);
  overflow: hidden; pointer-events: none;
}
/* Was filter: blur(7px) on top — main.js writes --gx every scroll frame
   while this section is pinned, so the filter meant re-rasterizing a
   blurred layer every frame for the whole phone-flip scroll. The gradient's
   own stops are widened here to fake the same soft edge with a plain,
   compositor-only translateX. */
.flip-glow::before {
  content: ""; position: absolute; top: -25%; bottom: -25%; left: 0; width: 60%;
  background: linear-gradient(100deg,
    transparent 18%,
    rgba(251, 246, 234, .05) 38%,
    rgba(255, 253, 246, .16) 50%,
    rgba(251, 246, 234, .05) 62%,
    transparent 82%);
  transform: translateX(calc(var(--gx, -130) * 1%)) rotate(8deg);
}

/* ---------- the phone's body ----------
   This used to be a stack of border-only frames spread along Z (19 of them,
   then 7, then 4), faking a solid side by hoping the gaps read as one body.
   It is now an actual box: a bezel frame on each face, and one solid wall down
   each long edge. Cheaper (two of the four elements are slivers instead of
   full-size rounded borders) and *correct* — at 90deg, where the old stack
   showed a handful of separate hairlines with gaps between them, there is now
   a real side to look at.

   Only the left and right walls exist. The phone rotates about Y and nothing
   else, so the top and bottom edges are never turned toward the viewer. */
.flip-edge {
  position: absolute; inset: 0;
  transform-style: preserve-3d; pointer-events: none;
}
.edge-face {
  position: absolute; inset: 0;
  border-radius: var(--rad);
  border: var(--bezel) solid var(--plum);
}
.edge-front { transform: translateZ(var(--slab)); }
.edge-back  { transform: translateZ(calc(var(--slab) * -1)); border-color: #170d20; }
.edge-wall {
  position: absolute;
  /* Inset by exactly the corner radius, because that is where the straight
     part of a rounded rectangle's vertical edge begins and ends: the flat run
     goes from y = R to y = H - R. Insetting by less (this was a flat 2.5%)
     ran the bar past the curve, so its ends stuck out beyond the phone's
     silhouette at top and bottom. The corners are left to the two face
     outlines, which is what gives a real phone its rounded shoulder anyway. */
  top: var(--rad); bottom: var(--rad);
  width: calc(var(--slab) * 2);
  background: linear-gradient(90deg, #170d20, #2a1838 42%, #1d1129);
  border-radius: var(--slab);   /* domed caps, so it eases into the corner arc */
  /* centred on the phone's own edge, then swung into the x-plane about its own
     centre — so it spans exactly -slab..+slab in Z and meets both faces */
  transform: rotateY(90deg);
}
.edge-wall-l { left: calc(var(--slab) * -1); }
.edge-wall-r { right: calc(var(--slab) * -1); }
.flip-section.fx-static .flip-edge { display: none; }
@media (max-width: 880px) {
  .flip-grid { align-content: center; }
  /* only the base width — bezel, radius and slab depth all follow from it */
  .flip-phone { --pw: 150px; width: min(var(--pw), 42%); }
  .flip-chip { display: none; }
  .flip-copy { min-height: 290px; }
  .flip-step h2 { font-size: 1.5rem; }
  /* Pin to a STABLE viewport height: the base 100dvh grows/shrinks as the
     browser toolbar slides in on scroll-up, which re-centres the pinned orbit
     and tucks the top card under the header. Plain 100vh is fixed to the
     large (toolbar-hidden) height and never changes mid-scroll, so the wheel
     stays put in both directions. */
  .orbit-stage { height: 100vh; }
  .orbit-grid { align-content: center; gap: 1rem; }
  .orbit-wheel { width: min(58vw, 290px, 38vh); }
  /* the active card rides one radius above the wheel centre — leave room
     for it so it doesn't poke above the pinned viewport */
  .orbit-wheelbox { order: -1; margin-top: clamp(60px, 17vw, 95px); }
  .orbit-copy { min-height: 300px; }
  .orbit-step { justify-content: flex-start; }
  .orbit-step h2 { font-size: 1.5rem; }
  .step-no { font-size: 2.6rem; }
  /* Same trap as the orbit: 100dvh shrinks as the toolbar slides in on
     scroll-up, re-centring the pinned panel so its top numeral tucks under
     the header. Pin to the stable 100vh, and reserve the header's height up
     top so the centred block always clears it (header padding + logo). */
  .hscroll-stage { height: 100vh; padding-top: clamp(80px, 18vw, 120px); }
  .hpanel { grid-template-columns: 1fr; width: 80vw; gap: 1rem; }
  .hpanel-img { width: min(56vw, 230px); margin: 0 auto; }
  .hpanel-body h3 { font-size: 1.25rem; }
  .hpanel-body p { font-size: .85rem; }
  /* Single column below 880px, so the medal is competing with the year numeral
     and the copy for one 100vh stage rather than with a neighbouring column —
     hence a share of the width (was min(46vw, 210px)) rather than a fixed cap
     that left most of a phone screen empty beside it. */
  .era-medal { width: min(64vw, 300px); }
  .era-yearbox { gap: 1.2rem; }
}
/* ---- short viewports ----
   The era stage is a 100vh pinned panel carrying a year numeral, a circular
   photograph and a block of copy, all under a fixed header. Below about 780px
   of viewport height that does not fit and the numeral slides up behind the
   header. This predates the photograph being enlarged — measured at 375x667 it
   was already 21px into the header at the old min(46vw, 210px) — but a bigger
   photo makes it plainer, so it is fixed here rather than left alone.

   Same trap and the same fix as .hscroll-stage above: reserve the header height
   on the stage. The extra parts are giving the photo a share of the HEIGHT as
   well as the width (a circle is the one element here that grows in both axes
   at once) and trimming the copy reservation, which is sized for the longest of
   the three steps and is generous at this width. */
@media (max-width: 880px) and (max-height: 780px) {
  .era-stage { padding-top: clamp(76px, 13vh, 110px); }
  .era-medal { width: min(64vw, 300px, 26vh); }
  .era-copy { min-height: 250px; }
  .era-num, .era-num-ghost { font-size: clamp(2.9rem, 13vw, 4.6rem); }
  .era-step p:not(.eyebrow) { font-size: .92rem; }
  /* Nudged down and shrunk so the last line of the longest step clears it.
     Do NOT "fix" a collision here by cutting .era-copy min-height: the steps
     are absolutely positioned and overflow that box, so once the text is taller
     than the reservation, shrinking it moves the centred group DOWN faster than
     it shortens it and the collision gets worse. Keep the reservation at or
     above the real text height and take the room from elsewhere. */
  .era-count { bottom: .6rem; font-size: .7rem; }
}

.era-section.fx-static .era-photo { position: static; opacity: 1; transform: none; display: none; }
.era-section.fx-static .era-photo:first-child { display: block; }
.era-section.fx-static .era-medal { margin: 0 auto; }

/* ---------- Static fallback (no GSAP / reduced motion) ---------- */
.era-section.fx-static { padding: clamp(4rem, 9vw, 7.5rem) 0; }
.era-section.fx-static .era-stage { position: static; height: auto; display: block; }
.era-section.fx-static .era-grid { grid-template-columns: 1fr; }
.era-section.fx-static .era-copy { min-height: 0; }
.era-section.fx-static .era-step {
  position: static; opacity: 1; transform: none; pointer-events: auto; margin-bottom: 2.2rem;
}
.era-section.fx-static .era-yearbox .era-num { position: static; display: block; }
.era-section.fx-static .era-num-ghost, .era-section.fx-static .era-count { display: none; }


/* ----------------------------------------------------------------
   OUR STORY — journey timeline images (right column per node)
---------------------------------------------------------------- */
.timeline.thread { max-width: 1060px; }
.thread-node {
  display: grid; grid-template-columns: 1fr minmax(240px, 320px);
  gap: clamp(1.6rem, 4vw, 3rem); align-items: center;
}
.tl-media {
  opacity: 0; transform: translateX(70px) scale(.94) rotate(1.5deg);
  transition: opacity .8s var(--ease-fluid) .12s, transform .8s var(--ease-fluid) .12s;
}
.thread-node.lit .tl-media { opacity: 1; transform: none; }
.tl-media .ph { box-shadow: var(--shadow-soft); border: 4px solid var(--white); }
.thread-node.lit .tl-media .ph {
  box-shadow: 0 18px 50px -18px rgba(140, 105, 32, .45), 0 0 0 1px rgba(168, 127, 46, .35);
}
@media (max-width: 760px) {
  .thread-node { grid-template-columns: 1fr; }
  .tl-media { transform: translateY(40px) scale(.96); }
}

/* ----------------------------------------------------------------
   COLLECTION DETAIL — item cards, thumbs, lightbox
---------------------------------------------------------------- */
.item-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 1.8rem; }
.item-card {
  background: var(--white); border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: transform .55s var(--ease-fluid), box-shadow .55s;
}

/* ---- skip rendering off-screen grid cards ----
   content-visibility: auto lets the browser skip layout AND paint for a card
   that is off screen, which on a long grid is most of them, on every frame.

   It is scoped to pages with no pinned ScrollTrigger, and that scoping is
   load-bearing rather than cautious. A skipped card reports its
   contain-intrinsic-size, not its real height, so the document height changes
   as cards scroll in — while a pinned section's start/end are resolved once,
   at refresh, from exactly that document height. On /product the grid sits
   above [data-hscroll], so the pin would drift by the accumulated difference
   as the reader scrolled down to it. ScrollTrigger has no way to notice.

   If a browser does not support :has(), the whole selector is invalid and no
   card gets the optimization — which is the safe direction to fail in. */
body:not(:has([data-orbit], [data-reviews], [data-flip], [data-hscroll], [data-era], [data-deck]))
  :is(.collection-card, .item-card, .blog-card) {
  content-visibility: auto;
  contain-intrinsic-size: auto 420px;
}
.item-card:hover { transform: translateY(-8px); box-shadow: var(--shadow-soft); }
.item-stage { position: relative; cursor: pointer; }
.item-stage .ph { border-radius: 0; }
.item-thumbs { display: flex; gap: .5rem; padding: .7rem .9rem 0; flex-wrap: wrap; }
.item-thumb {
  width: 52px; height: 52px; border-radius: 8px; overflow: hidden; padding: 0;
  border: 2px solid transparent; background: none; cursor: pointer; opacity: .65;
  transition: border-color .3s, opacity .3s;
}
.item-thumb img, .item-thumb video { width: 100%; height: 100%; object-fit: cover; }
.item-thumb.on, .item-thumb:hover { border-color: var(--gold); opacity: 1; }
.item-body { padding: 1.2rem 1.5rem 1.6rem; }
.item-price {
  display: inline-block; font-family: var(--font-accent); color: var(--gold);
  font-size: 1.02rem; letter-spacing: .04em; margin: .2rem 0 .4rem;
}
.item-body p { font-size: .88rem; color: var(--ink-soft); }
.item-body .read-more { margin-top: .9rem; }

/* lightbox */
.lb {
  position: fixed; inset: 0; z-index: 2000; background: rgba(34, 25, 49, .92);
  display: none;
}
.lb.open { display: block; }

/* The stage is the clipping frame for the zoom. The picture is scaled with a
   transform rather than by changing its width, so zooming is a composite of an
   already-rasterized layer instead of a re-layout — and overflow:hidden here is
   what stops a zoomed-in photo from spilling over the arrows and the close
   button. touch-action:none hands every finger to the pinch/pan handlers; the
   overlay is fixed, so there is nothing behind it to scroll anyway. */
.lb-stage {
  position: absolute; inset: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  touch-action: none;
}
.lb img, .lb video {
  max-width: min(86vw, 980px); max-height: 84vh; border-radius: 12px;
  box-shadow: 0 40px 120px -30px #000;
  transform-origin: center center;
}
/* No transition on transform: main.js rewrites it on every wheel notch and on
   every pointermove, and a transition there would chase a target that has
   already moved on — the same rule that governs the per-frame writes elsewhere
   in this file. */
.lb img { cursor: zoom-in; }
/* Promotion only while the overlay is actually up — .lb is built once, on the
   first open, and then lives in the DOM forever; an unconditional will-change
   here would hold a full-size GPU texture for the rest of the visit. */
.lb.open img { will-change: transform; }
.lb.zoomed img { cursor: grab; }
.lb.zoomed img:active { cursor: grabbing; }

.lb-btn {
  position: absolute; z-index: 2; background: none; border: none; color: var(--gold-soft);
  font-size: 2.4rem; cursor: pointer; padding: 1rem; line-height: 1;
}
.lb-prev { left: 2vw; top: 50%; transform: translateY(-50%); }
.lb-next { right: 2vw; top: 50%; transform: translateY(-50%); }
.lb-close { top: 1.2rem; right: 1.6rem; font-size: 1.8rem; }

.lb-hint {
  position: absolute; z-index: 2; left: 50%; bottom: 1.4rem; transform: translateX(-50%);
  margin: 0; padding: .5rem 1.1rem; border-radius: 100px;
  background: rgba(20, 11, 6, .55); border: 1px solid rgba(221, 188, 119, .28);
  font-size: .72rem; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(246, 240, 225, .8); white-space: nowrap; pointer-events: none;
  transition: opacity .5s var(--ease-fluid);
}
.lb.touched .lb-hint { opacity: 0; }
@media (max-width: 620px) { .lb-hint { display: none; } }

/* ----------------------------------------------------------------
   COLLECTIONS — customer reviews: rotating golden thread
---------------------------------------------------------------- */
.reviews-section { position: relative; padding: 0; z-index: 1; }
.reviews-stage {
  position: relative; height: 100vh; height: 100svh; overflow: hidden;
  display: flex; flex-direction: column; justify-content: center; padding-top: 4rem;
  background:
    radial-gradient(800px 460px at 50% -10%, rgba(141, 111, 172, .25), transparent 60%),
    linear-gradient(180deg, var(--plum) 0%, var(--purple-deep) 100%);
}
.rev-head h2 { color: var(--white); }
.rev-head .eyebrow { color: var(--gold-soft); }
.rev-head .eyebrow::before { background: var(--gold-soft); }
.rev-scene { position: relative; flex: 1; min-height: 420px; }
.rev-thread { position: absolute; left: 50%; top: 0; bottom: 0; width: 0; z-index: 2; }
/* animation-play-state starts paused — main.js only sets .fx-live (via the
   shared IntersectionObserver) while this pinned section is actually in view,
   so three strands are not swaying for the entire time the section merely
   exists in the DOM.

   No drop-shadow: this element sways on an infinite transform animation AND
   takes a per-frame `translate` write from initReviews, and a filtered element
   has to re-rasterize its shadow on every one of those frames — three of them,
   continuously, for the whole pinned section. Exactly the mistake already
   fixed on .thread-path. The glow is faked the same way it was there: a wider,
   softer, unfiltered gradient that costs one flat paint instead. */
.strand {
  position: absolute; top: -4%; bottom: -4%; left: 0; width: 6px; border-radius: 3px;
  background:
    linear-gradient(180deg, transparent, var(--gold-soft) 12%, #B8893A 50%, var(--gold-soft) 88%, transparent)
      no-repeat center / 2px 100%,
    linear-gradient(180deg, transparent, rgba(216, 184, 108, .28) 12%, rgba(216, 184, 108, .38) 50%, rgba(216, 184, 108, .28) 88%, transparent);
  margin-left: -2px;
  animation: strand-sway 5.5s ease-in-out infinite;
  animation-play-state: paused;
}
.reviews-section.fx-live .strand {
  animation-play-state: running;
  will-change: transform;
}
.strand:nth-child(2) { animation-delay: -1.8s; opacity: .7; width: 5px; }
.strand:nth-child(3) { animation-delay: -3.6s; opacity: .5; width: 4px; }
@keyframes strand-sway {
  0%, 100% { transform: translateX(-5px); }
  50%      { transform: translateX(5px); }
}
.rev-dust { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.rev-photo, .rev-quote {
  position: absolute; left: 50%; top: 50%; margin: 0;
  opacity: 0;
}
/* Static z-index by side. initReviews used to write zIndex per element per
   frame, and a z-index change re-sorts the stacking context rather than just
   moving a layer — one of the few writes in that render loop that the
   compositor could not absorb. The sides alternate deterministically, so the
   only thing that actually varies is whether an element is currently behind
   the thread: that is now the single .behind class below. Strands sit at
   z-index 2 and the dust canvas at 3, so 1 / 4 keep straddling them. */
.rev-photo, .rev-quote { z-index: 4; }
.rev-photo.behind, .rev-quote.behind { z-index: 1; }
/* ...and only the one currently facing the reader is interactive. All four
   photos are laid out at exactly the same point (place() resolves x to
   -R*cos(rot) for every index -- see main.js), so they are a perfect stack
   that only opacity distinguishes; the quotes likewise. An opacity:0 element
   still answers a click, and at rest none of them carry .behind, so the
   z-index above cannot separate them either and the hit test falls through
   to whichever is last in the DOM. That swallowed every tap on .rev-play.
   initReviews toggles .is-front from the same visibility value it writes to
   opacity. .fx-static lays the reviews out as an ordinary grid where they
   no longer overlap and initReviews never runs, so it opts straight back in
   -- as does any tier where the orbit is skipped, since staticFX() puts
   .fx-static on [data-reviews] in every one of those paths. */
.rev-photo, .rev-quote { pointer-events: none; }
.rev-photo.is-front, .rev-quote.is-front,
.reviews-section.fx-static .rev-photo,
.reviews-section.fx-static .rev-quote { pointer-events: auto; }
.reviews-section.fx-live .rev-photo,
.reviews-section.fx-live .rev-quote { will-change: transform, opacity; }
.rev-photo {
  width: clamp(240px, 30vw, 400px); aspect-ratio: 1; border-radius: 50%;
  overflow: hidden; border: 4px solid var(--gold-soft);
  /* 60px of blur on a 346px circle is a large, repeatedly-invalidated paint,
     and eight of these move at once. Tightened; visually near-identical
     against the dark plum stage. */
  box-shadow: 0 14px 30px -14px rgba(0, 0, 0, .7), 0 0 0 10px rgba(221, 188, 119, .12);
}
/* Depth cue for anything currently on the far side of the thread. Was a
   per-element `filter: blur(1.5px) brightness(.7)` written from the render
   loop — a filter forces the layer to be re-rasterized rather than just
   re-composited. A flat overlay's opacity is compositor-only. */
.rev-photo::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: rgba(20, 10, 28, .34);
  opacity: 0; transition: opacity .35s linear; pointer-events: none; z-index: 2;
}
.rev-photo.behind::after { opacity: 1; }
/* position:absolute + inset:0 (not an in-flow width/height:100%) so the frame
   is always exactly the parent circle's own box, resolved directly against
   its containing block — never dependent on the video's own intrinsic w/h
   attributes or on percentage-height resolution through aspect-ratio, which
   is where this used to go wrong for video specifically (an <img> is
   display:block by the global rule; a <video> defaults to display:inline,
   with no such rule of its own). border-radius:inherit is belt-and-braces
   against the well-known WebKit bug where a <video>'s own compositor layer
   ignores an ancestor's overflow:hidden clip and paints past it — without
   it, a video review photo can render as a stretched oval instead of the
   circle every image review gets. */
.rev-photo img, .rev-photo video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: inherit; display: block;
}
/* The play button sits centered over the circle until playback starts —
   just the triangle, no backdrop plate; a drop-shadow keeps it legible over
   whatever the poster frame happens to look like. z-index 3 clears the
   video (auto/0) and the depth-dimming ::after (2). Click handling lives on
   the whole .rev-photo figure in main.js (not a listener on this button),
   so it isn't actually load-bearing for the tap to register — it only
   needs to look clickable. */
.rev-play {
  position: absolute; inset: 0; margin: auto; z-index: 3;
  width: 30%; aspect-ratio: 1; min-width: 42px;
  display: grid; place-items: center;
  border: none; background: none; padding: 0;
  color: var(--cream); cursor: pointer;
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, .7)) drop-shadow(0 0 16px rgba(0, 0, 0, .5));
  transition: transform .25s;
}
.rev-play svg { width: 100%; height: 100%; fill: currentColor; margin-left: 10%; }
html.fine-pointer .rev-play:hover { transform: scale(1.1); }
.rev-photo.is-playing .rev-play { opacity: 0; pointer-events: none; }
.rev-initial {
  display: grid; place-items: center; width: 100%; height: 100%;
  font-family: var(--font-display); font-size: 3.4rem; color: var(--gold-soft);
  background: linear-gradient(150deg, var(--purple) 0%, var(--purple-deep) 100%);
}
.rev-quote { width: min(40vw, 430px); color: var(--cream); }
.rev-quote p {
  font-family: var(--font-display); font-style: italic;
  font-size: clamp(1.05rem, 1.7vw, 1.45rem); line-height: 1.55;
}
.rev-stars { display: block; color: var(--gold-soft); letter-spacing: .25em; margin-bottom: .8rem; }
.rev-stars .dim { opacity: .25; }
.rev-quote cite {
  display: block; margin-top: 1rem; font-style: normal; font-size: .78rem;
  letter-spacing: .24em; text-transform: uppercase; color: var(--gold-soft);
}
.rev-count {
  position: absolute; left: 50%; bottom: 1.6rem; transform: translateX(-50%);
  font-family: var(--font-accent); letter-spacing: .3em; font-size: .8rem;
  color: rgba(251, 246, 234, .72);
}
.rev-count b { color: var(--gold-soft); font-weight: 400; }
@media (max-width: 880px) {
  .rev-quote { width: min(72vw, 340px); text-align: center; }
  /* Paired with main.js's R() swing radius for the `lite` layout (see the
     comment there): the two are solved together so the circle clears BOTH
     the thread at center and the viewport edge, with a little margin on
     each side, rather than crowding one or clipping the other. */
  .rev-photo { width: clamp(165px, 45vw, 232px); }
}
/* phones: photo and quote stay side-by-side at the same vertical center
   (like desktop) and swing to opposite sides over a shorter radius — see
   Rq in main.js. They used to stack (photo top, quote below, centered) with
   almost no horizontal swing, which left the quote sitting on top of the
   thread instead of in its own half, and left a dead gap under the photo. */
@media (max-width: 768px) {
  .rev-quote { width: min(40vw, 260px); }
  .google-rev-badge { left: .9rem; bottom: .9rem; }
}
/* static fallback */
.reviews-section.fx-static .reviews-stage { height: auto; padding: clamp(3rem, 8vw, 5rem) 0; }
.reviews-section.fx-static .rev-scene {
  display: grid; gap: 2rem; min-height: 0;
  width: min(820px, 92%); margin: 2rem auto 0;
}
.reviews-section.fx-static .rev-quote {
  position: static; opacity: 1; transform: none !important; margin: 0 auto;
}
/* Stays position:relative here rather than static (unlike .rev-quote above)
   — it still needs to be a containing block for the absolutely positioned
   .rev-play button and video/img fill, just no longer taken out of flow for
   the orbit. relative behaves exactly like static for the parent grid. */
.reviews-section.fx-static .rev-photo {
  position: relative; opacity: 1; transform: none !important; margin: 0 auto;
}
.reviews-section.fx-static .rev-thread, .reviews-section.fx-static .rev-dust,
.reviews-section.fx-static .rev-count { display: none; }
/* The depth-dimming overlay only means anything while the reviews are
   rotating around the thread, and nothing ever sets .behind in this tier —
   too fragile to leave implied, so it's turned off explicitly. */
.reviews-section.fx-static .rev-photo::after { display: none; }

/* ---------- Banner slideshow responsive ---------- */
/* below ~1300px the page gutter is too thin to hold an arrow outside the
   banner, so tuck them just inside the banner edges instead (banner stays
   full width either way) */
@media (max-width: 1300px) {
  .banner-prev { left: .8rem; }
  .banner-next { right: .8rem; }
}
@media (max-width: 880px) {
  .banner-stage { border-radius: 12px; }
  .banner-arrow { width: 36px; height: 36px; font-size: 1.3rem; }
  .banner-thumb { width: clamp(76px, 20vw, 120px); }
}
@media (max-width: 560px) {
  .banner-stage { border-radius: 10px; }
  .banner-prev { left: .5rem; }
  .banner-next { right: .5rem; }
  .banner-thumbs { gap: .5rem; }
  .banner-thumb { width: clamp(64px, 26vw, 110px); }
}
/* phones: hide the landscape slideshow and show the portrait one instead.
   It keeps the auto-scroll timer + dots, but has no arrows or thumbnail strip —
   visitors swipe left/right to move between offers */
@media (max-width: 640px) {
  .banner-variant--desktop { display: none; }
  .banner-variant--mobile { display: block; }
}

/* ================================================================
   RATES — gold & silver prices by purity
   ================================================================ */
.rates-hero .rate-updated {
  display: inline-flex; align-items: center; gap: .55rem;
  margin: 1.6rem auto 0; padding: .45rem 1.1rem;
  background: rgba(253,250,242,.7); border: 1px solid var(--gold-soft);
  border-radius: 100px; font-size: .8rem; letter-spacing: .04em;
  color: var(--ink-soft); backdrop-filter: blur(4px);
}
.rates-hero .rate-updated strong { color: var(--gold); font-weight: 600; }
/* The expanding ring used to be an animated box-shadow spread, which repaints
   the element on every frame of a 2s loop that never ends. Same look from a
   pseudo-element scaling and fading instead — transform and opacity are
   composited, so the loop costs nothing to keep running. */
.rate-pulse {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--gold); position: relative;
}
.rate-pulse::after {
  content: ""; position: absolute; inset: 0;
  border-radius: 50%; background: rgba(140,105,32,.5);
  animation: ratePulse 2s ease-out infinite;
}
@keyframes ratePulse {
  0%   { transform: scale(1); opacity: .5; }
  70%  { transform: scale(3); opacity: 0; }
  100% { transform: scale(3); opacity: 0; }
}

.rate-section {
  padding: clamp(2.5rem, 5vw, 4rem) 0; position: relative;
  /* per-metal accent — gold by default, overridden for silver below */
  --rate-accent: var(--gold);
  --rate-accent-soft: var(--gold-soft);
  --rate-accent-glow: rgba(221, 188, 119, .22);
}
.rate-section.rate-silver {
  /* cool platinum/slate, kept in the purple family so it sits in the theme */
  --rate-accent: #6c6678;
  --rate-accent-soft: #b3adc0;
  --rate-accent-glow: rgba(141, 111, 172, .18);
}
.rate-section.rate-gold {
  background:
    radial-gradient(700px 340px at 88% -10%, rgba(221,188,119,.28), transparent 62%),
    linear-gradient(180deg, var(--cream) 0%, var(--white) 100%);
}
.rate-section.rate-silver {
  background:
    radial-gradient(700px 340px at 10% -10%, rgba(141,111,172,.20), transparent 62%),
    linear-gradient(180deg, var(--white) 0%, var(--cream-soft) 100%);
}

.rate-head {
  display: flex; align-items: center; gap: 1.3rem;
  margin-bottom: clamp(1.8rem, 3vw, 2.6rem);
}
.rate-medal {
  flex: none; width: clamp(58px, 8vw, 76px); height: clamp(58px, 8vw, 76px);
  display: grid; place-items: center; font-size: clamp(1.8rem, 3.5vw, 2.4rem);
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, var(--gold-soft), var(--gold));
  box-shadow: var(--shadow-card), inset 0 2px 6px rgba(255,255,255,.4);
  animation: rateFloat 5s var(--ease-fluid) infinite;
}
.rate-silver .rate-medal {
  background: radial-gradient(circle at 32% 28%, #fbfbfd, #c9c6d2 55%, #8d899a);
}
@keyframes rateFloat {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-8px) rotate(3deg); }
}
.rate-head h2 { font-size: clamp(1.9rem, 3.6vw, 2.8rem); margin: .15rem 0; }
.rate-head .eyebrow { margin: 0; }
.rate-sub { color: var(--ink-soft); font-size: .92rem; }

.rate-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
}
.rate-card {
  position: relative; overflow: hidden;
  background: var(--white); border: 1px solid var(--cream-soft);
  border-radius: var(--radius); padding: 1.8rem 1.6rem 1.6rem;
  box-shadow: var(--shadow-card);
  transition: transform .4s var(--ease-fluid), border-color .4s, box-shadow .4s;
}
/* hover-only, so no layer at all on a touch screen — see .collection-card .ph */
@media (hover: hover) {
  .rate-card { will-change: transform; }
}
.rate-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, transparent, var(--rate-accent-soft), var(--rate-accent), var(--rate-accent-soft), transparent);
  opacity: .85;
}
.rate-card:hover { border-color: var(--rate-accent-soft); box-shadow: var(--shadow-soft); }
/* a slow light sweep across the card on hover */
.rate-shine {
  position: absolute; top: 0; left: -120%; width: 70%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.55), transparent);
  transform: skewX(-18deg); pointer-events: none; transition: left .8s var(--ease-fluid);
}
.rate-card:hover .rate-shine { left: 140%; }

.rate-ribbon {
  display: inline-block; margin-bottom: .9rem;
  font-size: .66rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--rate-accent); background: var(--rate-accent-glow);
  padding: .25rem .7rem; border-radius: 100px;
}
.rate-name {
  font-family: var(--font-accent); color: var(--purple-deep);
  font-size: 1.5rem; margin-bottom: .6rem;
}
.rate-price { display: flex; align-items: baseline; gap: .2rem; color: var(--rate-accent); font-family: var(--font-numeric); }
.rate-cur { font-size: 1.3rem; font-weight: 600; transform: translateY(-.35rem); }
.rate-val { font-weight: 600; font-size: clamp(2rem, 4vw, 2.7rem); line-height: 1; }
.rate-unit { margin-top: .35rem; font-size: .8rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }
.rate-empty { color: var(--ink-soft); font-style: italic; }

/* live-rate notice — the block that sits between the hero and the numbers.
   Styled to look like the most important thing on the page rather than fine
   print, because it is: it is what a visitor should read before they trust a
   figure they arrived with, and it is the only prose here a search engine is
   allowed to quote (everything holding a number carries data-nosnippet —
   see views/rates.ejs). */
.rate-live-wrap { padding: 0 0 clamp(1.4rem, 3vw, 2.2rem); }
.rate-live {
  display: flex; gap: 1.1rem; align-items: flex-start;
  background: linear-gradient(135deg, var(--cream-soft) 0%, var(--beige) 100%);
  border: 1px solid var(--gold-soft);
  border-left: 4px solid var(--gold); border-radius: var(--radius);
  padding: 1.6rem 1.8rem;
}
.rate-live-icon { flex: none; font-size: 1.6rem; color: var(--gold); line-height: 1.2; }
.rate-live h2 {
  font-family: var(--font-accent); font-size: clamp(1.15rem, 2.2vw, 1.45rem);
  color: var(--purple-deep); margin-bottom: .45rem;
}
/* no max-width here, unlike the .offer-copy rule: this band is the same width
   as .rate-disclaimer directly below it and a tighter measure on one of the two
   reads as a mistake rather than as typography. */
.rate-live p { color: var(--ink-soft); font-size: .95rem; }

/* FAQ */
.rate-faq-wrap { padding: clamp(1rem, 3vw, 2rem) 0 clamp(2rem, 4vw, 3rem); }
.rate-faq-title {
  font-family: var(--font-accent); font-size: clamp(1.5rem, 3vw, 2.1rem);
  color: var(--purple-deep); margin-bottom: 1.4rem;
}
.rate-faq { display: grid; gap: 1rem; }
.rate-faq-item {
  background: var(--cream-soft); border: 1px solid var(--beige);
  border-radius: var(--radius); padding: 1.3rem 1.5rem;
}
.rate-faq-item h3 { font-size: 1.05rem; margin-bottom: .4rem; color: var(--purple-deep); }
.rate-faq-item p { color: var(--ink-soft); font-size: .92rem; }

/* disclaimer band */
.rate-disclaimer-wrap { padding: clamp(2rem, 4vw, 3rem) 0; }
.rate-disclaimer {
  display: flex; gap: 1.1rem; align-items: flex-start;
  background: var(--cream-soft); border: 1px solid var(--beige);
  border-left: 4px solid var(--gold); border-radius: var(--radius);
  padding: 1.6rem 1.8rem;
}
.rate-disclaimer-icon {
  flex: none; font-size: 1.5rem; color: var(--gold); line-height: 1.3;
}
.rate-disclaimer h3 { font-size: 1.2rem; margin-bottom: .4rem; }
.rate-disclaimer p { color: var(--ink-soft); font-size: .92rem; }

@media (max-width: 560px) {
  .rate-head { flex-direction: row; align-items: center; gap: 1rem; }
  .rate-grid { grid-template-columns: 1fr 1fr; }
  /* drop the icon gutter rather than the copy — the notice has to stay
     readable at phone width, it is the whole point of the page */
  .rate-live, .rate-disclaimer { padding: 1.3rem 1.2rem; gap: .8rem; }
  .rate-live-icon, .rate-disclaimer-icon { display: none; }
}
@media (max-width: 400px) {
  .rate-grid { grid-template-columns: 1fr; }
}


/* ================================================================
   CAPABILITY TIERS

   <html data-fx="..."> is set by an inline script in partials/head.ejs,
   before this stylesheet applies, so these rules land with no flash and
   without a JS-driven reflow.

     full    everything (the default — no rules here)
     lite    all pinned set pieces still run, but every paint-bound effect
             is removed: no backdrop blur, no layout-animating transitions,
             no infinite repaint loops
     static  as lite, plus main.js skips building pinned/scrubbed triggers
             at all and routes the set pieces to their .fx-static layouts

   data-fx-quality="low" is set by the live frame-rate watchdog. It is
   deliberately limited to the same paint reductions: it must never change
   geometry, because pin spacers are already laid out by the time it fires.
================================================================ */

/* ---- backdrop-filter ----
   The header and .flip-chip cases are both gone from every tier now (see
   their base rules in style.css) — a full-width *fixed* bar re-sampling and
   re-blurring the page behind it every scroll frame, and 4 elements main.js
   moves every scroll frame each re-blurring a fresh backdrop region, were
   never worth their cost even at full tier. What's left here is genuinely
   occasional/static UI, cheap enough to keep at full and only worth losing
   on a confirmed weak device. */
html[data-fx="lite"] .google-rev-badge,
html[data-fx="static"] .google-rev-badge,
html[data-fx-quality="low"] .google-rev-badge,
html[data-fx="lite"] .rates-hero .rate-updated,
html[data-fx="static"] .rates-hero .rate-updated {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: rgba(246, 240, 225, .88);
}

/* ---- layout-animating transitions ----
   padding and height are layout properties, so each of these runs a full
   page layout on every frame for 400ms — and they fire every time the scroll
   position crosses 40px, in both directions. Snap instead of animating. */
html[data-fx="lite"] .site-header,
html[data-fx="static"] .site-header {
  transition: box-shadow .4s;
}
html[data-fx="lite"] .brand-logo,
html[data-fx="static"] .brand-logo,
html[data-fx="lite"] .brand-wordmark,
html[data-fx="static"] .brand-wordmark {
  transition: none;
}

/* ---- endless repaint loops ---- */
html[data-fx="lite"] .ph.is-onscreen::before,
html[data-fx="static"] .ph.is-onscreen::before,
html[data-fx-quality="low"] .ph.is-onscreen::before {
  animation: none;
}
html[data-fx="lite"] .rate-pulse::after,
html[data-fx="static"] .rate-pulse::after,
html[data-fx="lite"] .rate-medal,
html[data-fx="static"] .rate-medal,
html[data-fx="lite"] .era-medal::before,
html[data-fx="static"] .era-medal::before {
  animation: none;
}

/* ---- mobile menu decoration ---- */
html[data-fx="lite"] .nav-links.open .nav-deco-logo,
html[data-fx="static"] .nav-links.open .nav-deco-logo,
html[data-fx="lite"] .nav-links.open .nav-deco-gem,
html[data-fx="static"] .nav-links.open .nav-deco-gem {
  animation: none;
}

/* ---- static tier: reveals stay, but keep them short and cheap ---- */
html[data-fx="static"] .reveal,
html[data-fx="static"] .reveal-left,
html[data-fx="static"] .reveal-right,
html[data-fx="static"] .reveal-scale {
  transition-duration: .5s;
}
