/* AUTO-GENERATED by assets/build.php - do not edit this file directly.
   Edit the numbered source files in assets/css/src/ instead, then run:
   php assets/build.php */

:root{
  /* Dark palette lifted from the live Shopify store (shop.tuangcoffee.com,
     Horizon theme config/settings_data.json): background #241109, foreground #f4eadd. */
  --clr-terracotta:      #C1502A;
  --clr-terracotta-dark: #9A3D1F;
  --clr-ink:             #F4EADD;
  --clr-ink-soft:        #D8CFC0;
  --clr-bg:              #241109;
  --clr-bg-alt:          #2C160C;
  --clr-surface:         #34190F;
  --clr-panel-dark:      #1A0B05;
  --clr-moss:            #6E7350;
  --clr-moss-rgb:        110,115,80;
  --clr-gray-light:      #ADA79C;
  --clr-gray-light-rgb:  173,167,156;
  --clr-gold:            #D9A441;
  --clr-line:            rgba(244,234,221,0.14);
  /* Warmer, lighter browns for section gradients - the flat near-black
     background (#241109) reads as heavy over a long scroll, so these give
     sections somewhere to gradient *toward* without leaving the dark/premium
     palette entirely (no light mode - just less flat). */
  --clr-bg-warm:         #341B0E;
  --clr-bg-warm-2:       #3D2011;

  --font-display: "Anton", "Arial Narrow", sans-serif;
  --font-body:    "Poppins", -apple-system, sans-serif;
  --font-mono:    "IBM Plex Mono", monospace;

  --container: 1200px;
  --radius-sm: 4px;
  --radius-md: 10px;
  --nav-h: 70px;
  --announce-h: 38px;

  --ease: cubic-bezier(.22,.61,.36,1);

  /* Fixed (never overridden by [data-theme="light"]) - for text/icons that
     sit on the navbar, announce bar, search panel, or footer specifically.
     Those four stay a dark brown "anchor chrome" in both themes (see the
     [data-theme="light"] block below), so their own text must stay light
     regardless of theme rather than following --clr-ink/--clr-ink-soft,
     which flip to dark-on-light and would go illegible there. */
  --clr-ink-on-dark:      #F4EADD;
  --clr-ink-soft-on-dark: #D8CFC0;
  --clr-line-on-dark:     rgba(244,234,221,0.14);
}

/* Light theme - the site above is dark by default (no attribute = dark,
   unchanged from before this existed). [data-theme="light"] on <html> is
   set by the toggle in the navbar (see main_1.js) and persisted to
   localStorage. Terracotta/gold/moss stay identical across both themes -
   only surfaces, ink, and line opacity invert. Photo scrims (hero/card
   gradients, modal close buttons sitting on top of images) intentionally
   stay fixed-dark in both themes - same reason a photo viewer's own
   controls don't reflow with the app's theme. */
[data-theme="light"]{
  --clr-ink:             #2B2019;
  --clr-ink-soft:        #6B5D4F;
  --clr-bg:              #FAF3E7;
  --clr-bg-alt:          #F2E6D3;
  --clr-surface:         #FFFFFF;
  --clr-panel-dark:      #1A0B05;
  --clr-gray-light:      #8A7F70;
  --clr-line:            rgba(43,32,20,0.14);
  --clr-bg-warm:         #F0E2CB;
  --clr-bg-warm-2:       #E8D6B8;
}

*, *::before, *::after{ box-sizing:border-box; }
html{ scroll-behavior:smooth; overflow-x:hidden; }
html, body{ margin:0; padding:0; overflow-x:hidden; }
body{
  font-family:var(--font-body);
  color:var(--clr-ink);
  background:var(--clr-bg);
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
/* Faint film-grain overlay across the whole page - breaks up flat color
   banding on the dark gradients, sits above everything (z-index:9999) but
   pointer-events:none so it never blocks clicks. */
body::before{
  content:"";
  position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:9999; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
  opacity:.04; mix-blend-mode:overlay;
}
img{ max-width:100%; display:block; }
a{ color:inherit; text-decoration:none; }
ul{ list-style:none; margin:0; padding:0; }
button{ font-family:inherit; cursor:pointer; }
input, textarea, select{ font-family:inherit; }
:focus-visible{ outline:2px solid var(--clr-terracotta); outline-offset:3px; }

.container{
  width:100%;
  max-width:var(--container);
  margin:0 auto;
  padding:0 24px;
}

.eyebrow{
  font-family:var(--font-mono);
  font-size:.72rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--clr-terracotta);
  display:inline-flex;
  align-items:center;
  gap:10px;
  margin-bottom:18px;
}
.eyebrow::before{
  content:"";
  width:28px; height:1px;
  background:var(--clr-terracotta);
  display:inline-block;
}

h1,h2,h3,h4{
  font-family:var(--font-display);
  /* Anton ships one weight (400, already very heavy/condensed by design) -
     asking for 600 here would make the browser fake-bold it, blurring the
     letterforms instead of using Anton's real weight. */
  font-weight:400;
  color:var(--clr-ink);
  margin:0 0 .5em;
  letter-spacing:0;
}
h2.section-title{
  font-size:clamp(2.1rem, 4.6vw, 3.4rem);
  line-height:1.05;
  max-width:14ch;
}
p{ margin:0 0 1em; color:var(--clr-ink-soft); }
p.lead{ font-size:1.08rem; }

.btn{
  display:inline-flex;
  align-items:center;
  gap:10px;
  padding:14px 28px;
  border-radius:999px;
  font-size:.92rem;
  font-weight:600;
  letter-spacing:.01em;
  border:1px solid transparent;
  transition:transform .35s var(--ease), background .3s, color .3s, border-color .3s;
}
.btn-primary{
  background:var(--clr-terracotta);
  color:#fff;
}
.btn-primary:hover{ background:var(--clr-terracotta-dark); transform:translateY(-2px); }
.btn-outline{
  background:transparent;
  border-color:currentColor;
  color:var(--clr-ink);
}
.btn-outline:hover{ background:var(--clr-terracotta); color:#fff; border-color:var(--clr-terracotta); transform:translateY(-2px); }
.btn-outline.light{ color:#fff; }
.btn-outline.light:hover{ background:#fff; color:var(--clr-bg); }
.btn-sm{ padding:9px 18px; font-size:.82rem; gap:6px; }
/* Top announcement strip - sits above the navbar. Only shown while the page
   is scrolled to the top; slides up and out once the user scrolls past the
   navbar's is-scrolled threshold, and the navbar takes over row 0. */
.announce-bar{
  position:fixed;
  top:0; left:0; right:0;
  z-index:1001;
  height:var(--announce-h);
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--clr-panel-dark);
  border-top:1px solid var(--clr-line-on-dark);
  border-bottom:1px solid var(--clr-line-on-dark);
  transition:transform .3s var(--ease), opacity .3s var(--ease);
}
.announce-bar.is-scrolled{
  transform:translateY(-100%);
  opacity:0;
  pointer-events:none;
}
.announce-bar p{
  margin:0;
  font-family:'Poppins', sans-serif;
  font-weight:400;
  font-size:.76rem;
  letter-spacing:.01em;
  color:var(--clr-ink-soft-on-dark);
  text-align:center;
  padding:0 16px;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
  max-width:100%;
}
.announce-bar a{
  color:var(--clr-gold);
  font-weight:500;
  margin-left:6px;
  text-decoration:underline;
  text-underline-offset:2px;
}
.announce-bar a:hover{ color:#fff; }

.navbar{
  position:fixed;
  top:var(--announce-h); left:0; right:0;
  z-index:1000;
  height:var(--nav-h);
  display:flex;
  /* flex-start + an explicit top padding (instead of align-items:center)
     keeps the gap above the logo as-is but lets the box's bottom edge sit
     closer to the logo - trimmed on request, not flush against it. */
  align-items:flex-start;
  padding-top:10px;
  background:rgba(36,17,9,0);
  transition:background .4s var(--ease), box-shadow .4s var(--ease), height .3s var(--ease), top .3s var(--ease);
}
.navbar.is-scrolled{
  top:0;
  background:color-mix(in srgb, var(--clr-panel-dark) 92%, transparent);
  backdrop-filter:blur(10px);
  box-shadow:0 1px 0 var(--clr-line-on-dark);
  height:64px;
}
.navbar .container{
  display:flex;
  align-items:center;
  justify-content:space-between;
}
.nav-logo{ display:flex; align-items:center; gap:10px; }
/* Wordmark text removed - the logo image now carries the brand alone, so it
   runs bigger than before to stay legible/prominent on its own. */
/* Brand mark ships as solid terracotta - brightness(0) flattens any opaque
   pixel to black regardless of source hue, then invert(1) flips that to
   pure white, so it reads clean against the dark hero/navbar without a
   separate white logo asset. */
.nav-logo img{ height:56px; width:auto; transition:height .3s var(--ease), filter .3s var(--ease); filter:brightness(0) invert(1); }
.navbar.is-scrolled .nav-logo img{ height:44px; }
/* Light theme: logo renders terracotta instead of white (owner request
   2026-08-19) - same brightness(0) flatten-to-silhouette trick as the dark
   variant, then a filter chain tuned to land on --clr-terracotta (#C1502A)
   instead of inverting to white. Applies regardless of scroll position -
   terracotta reads fine both over the dark hero scrim (unscrolled) and the
   light navbar chrome (scrolled, see below). */
[data-theme="light"] .nav-logo img{
  filter: brightness(0) saturate(100%) invert(33%) sepia(84%) saturate(1274%) hue-rotate(346deg) brightness(94%) contrast(91%);
}

/* Light theme, scrolled state only: the navbar's solid chrome flips from
   the fixed dark "anchor chrome" (used everywhere else - announce bar,
   footer, mobile menu overlay - by design, see 00-base.css) to a light
   surface, so menu text/icons need to flip from the fixed on-dark cream
   tokens to the theme-aware --clr-ink (dark brown in light theme) to stay
   legible. Scoped to .is-scrolled only - unscrolled navbar sits transparent
   over the hero photo's fixed-dark scrim, where cream text is still correct
   in both themes, so that state is untouched. */
[data-theme="light"] .navbar.is-scrolled{
  background: color-mix(in srgb, var(--clr-surface) 94%, transparent);
  box-shadow: 0 1px 0 var(--clr-line);
}
[data-theme="light"] .navbar.is-scrolled .nav-menu a,
[data-theme="light"] .navbar.is-scrolled .nav-icon-btn,
[data-theme="light"] .navbar.is-scrolled #theme-toggle .theme-icon-moon,
[data-theme="light"] .navbar.is-scrolled #theme-toggle .theme-icon-sun{
  color: var(--clr-ink);
}
[data-theme="light"] .navbar.is-scrolled .nav-toggle span{
  background: var(--clr-ink);
}
[data-theme="light"] .navbar.is-scrolled .nav-icon-btn:hover{
  background: rgba(43,32,20,.08);
}
[data-theme="light"] .navbar.is-scrolled .nav-icon-btn[aria-expanded="true"]{
  background: rgba(43,32,20,.1);
}
[data-theme="light"] .navbar.is-scrolled #theme-toggle{
  /* Plain 10% fill + the standard hairline border read as almost invisible
     against this navbar's near-white background - bumped both so the rail
     itself stays legible, not just the thumb/icons. */
  background: rgba(43,32,20,.16);
  border-color: rgba(43,32,20,.4);
}
[data-theme="light"] .navbar.is-scrolled #theme-toggle:hover{
  background: rgba(43,32,20,.22);
}

.nav-menu{ display:flex; align-items:center; gap:38px; }
.nav-menu a{
  font-size:.9rem;
  font-weight:500;
  color:var(--clr-ink-on-dark);
  position:relative;
  padding:6px 0;
}
.nav-menu a::after{
  content:"";
  position:absolute; left:0; bottom:0;
  width:0; height:1.5px;
  background:var(--clr-terracotta);
  transition:width .3s var(--ease);
}
.nav-menu a:hover::after,
.nav-menu a.active::after{ width:100%; }

.nav-cta{ display:flex; align-items:center; gap:22px; }

/* Right-side icon row: Search / Account / Shop - replaces the old text
   "Shop Green Beans" button, minimal SVG icons instead. */
.nav-icons{ display:flex; align-items:center; gap:6px; }
.nav-icon-btn{
  display:flex; align-items:center; justify-content:center;
  width:38px; height:38px;
  border-radius:50%;
  background:none; border:none;
  color:var(--clr-ink-on-dark);
  cursor:pointer;
  transition:background .25s var(--ease), color .25s var(--ease);
}
.nav-icon-btn svg{ width:19px; height:19px; }
.nav-icon-btn:hover{ background:rgba(244,234,221,.1); color:var(--clr-terracotta); }
.nav-icon-btn[aria-expanded="true"]{ background:rgba(244,234,221,.14); color:var(--clr-terracotta); }

/* Theme toggle - built as an on/off switch (track + sliding thumb) instead
   of a plain icon button, so the state reads at a glance instead of relying
   on remembering what the single icon means. The visible icon now reflects
   the CURRENT theme (moon = dark active, sun = light active), not the
   destination - clearer than the old "icon = what clicking does" pattern.
   Thumb sits left in dark (default, no [data-theme] yet - matches the dark
   default so there's no flash/jump on load), slides right in light. */
#theme-toggle{
  position:relative;
  width:50px; height:27px;
  border-radius:999px;
  background:rgba(244,234,221,.14);
  border:1px solid var(--clr-line-on-dark);
  flex-shrink:0;
  padding:0;
}
#theme-toggle:hover{ background:rgba(244,234,221,.14); }
#theme-toggle::before{
  content:"";
  position:absolute;
  top:2px; left:2px;
  width:21px; height:21px;
  border-radius:50%;
  background:var(--clr-terracotta);
  transition:transform .3s var(--ease);
}
[data-theme="light"] #theme-toggle::before{ transform:translateX(23px); }
#theme-toggle svg{
  position:absolute; top:50%;
  width:13px; height:13px;
  transform:translateY(-50%);
  color:var(--clr-ink-on-dark);
  pointer-events:none;
}
#theme-toggle .theme-icon-moon{ left:6px; }
#theme-toggle .theme-icon-sun{ right:6px; display:none; }
[data-theme="light"] #theme-toggle .theme-icon-moon{ display:none; }
[data-theme="light"] #theme-toggle .theme-icon-sun{ display:block; }

/* Search panel - slides down from directly under the navbar, full width.
   Combined Produk + Artikel search, see search.php + 06-header-search.js. */
.nav-search-panel{
  position:fixed;
  top:calc(var(--announce-h) + var(--nav-h));
  left:0; right:0;
  z-index:999;
  background:color-mix(in srgb, var(--clr-panel-dark) 97%, transparent);
  backdrop-filter:blur(10px);
  border-bottom:1px solid var(--clr-line-on-dark);
  max-height:0;
  overflow:hidden;
  opacity:0;
  transition:max-height .35s var(--ease), opacity .3s var(--ease);
}
/* Tall enough for the form row plus ~4 result rows before the internal
   scroll (.nav-search-results) takes over - the panel itself only ever
   animates this one max-height, never its actual content height. */
.nav-search-panel.is-open{ max-height:480px; opacity:1; }
.nav-search-form{
  display:flex; align-items:center; gap:14px;
  padding:22px 0;
  border-bottom:1px solid var(--clr-line-on-dark);
}
.nav-search-form svg{ width:20px; height:20px; color:var(--clr-ink-soft-on-dark); flex-shrink:0; }
.nav-search-form input{
  flex:1;
  background:none; border:none; outline:none;
  font-family:'Poppins', sans-serif;
  font-size:1.1rem;
  color:var(--clr-ink-on-dark);
}
.nav-search-form input::placeholder{ color:var(--clr-ink-soft-on-dark); opacity:.6; }
/* type="search" ships its own native clear-icon in Chrome/Safari/Edge - it
   was rendering directly on top of our own .nav-search-close button right
   next to it (two overlapping "x" glyphs). We already have a custom close
   button, so the native one is redundant - hide it everywhere it can appear. */
.nav-search-form input[type="search"]::-webkit-search-cancel-button,
.nav-search-form input[type="search"]::-webkit-search-decoration{
  -webkit-appearance:none;
  appearance:none;
}
.nav-search-form input[type="search"]::-ms-clear{ display:none; width:0; height:0; }
.nav-search-close{
  background:none; border:none; color:var(--clr-ink-soft-on-dark);
  font-size:1.4rem; line-height:1; cursor:pointer; padding:4px 8px;
}
.nav-search-close:hover{ color:var(--clr-terracotta); }
.nav-search-results{
  padding:14px 0;
  font-size:.85rem;
  color:var(--clr-ink-soft-on-dark);
  max-height:400px;
  overflow-y:auto;
}
.nav-search-results:empty{ padding:0; }
.nav-search-empty{ padding:8px 2px; }
.nav-search-list{ display:flex; flex-direction:column; gap:2px; }
.nav-search-item{
  display:flex; align-items:center; gap:12px;
  padding:8px;
  border-radius:8px;
  color:var(--clr-ink-on-dark);
  transition:background .2s var(--ease);
}
.nav-search-item:hover{ background:rgba(244,234,221,.08); }
.nav-search-item img{
  width:40px; height:40px;
  border-radius:6px;
  object-fit:cover;
  flex-shrink:0;
}
.nav-search-item-info{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.nav-search-item-info strong{
  font-size:.88rem; font-weight:600;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.nav-search-item-info span{ font-size:.76rem; color:var(--clr-ink-soft-on-dark); }

@media (max-width:640px){
  .nav-icon-btn{ width:34px; height:34px; }
  .nav-icon-btn svg{ width:17px; height:17px; }
}

.nav-toggle{
  display:none;
  flex-direction:column;
  gap:5px;
  background:none; border:none; padding:8px;
  outline:none;
}
.nav-toggle:focus, .nav-toggle:focus-visible{
  outline:none;
}
.nav-toggle span{ width:22px; height:2px; background:var(--clr-ink-on-dark); border-radius:2px; }

.mobile-menu{
  position:fixed; inset:0;
  background:var(--clr-bg);
  z-index:999;
  display:flex; flex-direction:column;
  align-items:flex-start;
  /* flex-start, not center: centered content that's taller than the
     container can overflow past its own top padding when combined with
     overflow-y:auto (the overlap into the fixed navbar seen in testing).
     flex-start + top padding guarantees the first item never starts above
     that padding line, regardless of how many menu items there are. */
  justify-content:flex-start;
  gap:22px;
  padding:calc(var(--announce-h) + var(--nav-h) + 24px) 32px 32px;
  overflow-y:auto;
  transform:translateY(-100%);
  transition:transform .45s var(--ease);
}
.mobile-menu.is-open{ transform:translateY(0); }
.mobile-menu a{ font-family:var(--font-display); font-size:1.4rem; font-weight:400; }
.mobile-menu .btn{ margin-top:12px; font-size:0.85rem; padding:12px 24px; }

.hero{
  position:relative;
  min-height:100vh;
  display:flex;
  align-items:flex-end;
  /* Guaranteed clearance under the fixed announce bar + navbar stacked above
     it. Without this, hero copy that grows tall on wide screens - h1 is sized
     off vw, so it can get very large on wide laptops even when the viewport
     isn't short - can visually collide with the bars above it. Padding is
     always respected by flex children regardless of how tall the content
     block gets, so this holds at any viewport width/height combination. */
  padding-top:calc(var(--announce-h) + var(--nav-h) + 24px);
  /* Trimmed down from 140px - with the eyebrow tag and old "Shop Green Beans"
     nav button gone, the whole content block is shorter now, so less bottom
     cushion is needed to keep the two action buttons guaranteed above the
     fold (no scroll) on both common desktop and mobile viewport heights. */
  padding-bottom:56px;
  overflow:hidden;
}
.hero-bg{
  position:absolute; inset:0;
  overflow:hidden;
}
/* Plain single-image hero (shop.php, journey.php -
   anywhere with a static `<img>` right inside .hero-bg
   instead of the .hero-slide wrapper below). The direct-child selector
   keeps this from ever matching images nested inside .hero-slide. */
.hero-bg > img{
  width:100%; height:100%;
  object-fit:cover;
  object-position:center 32%;
  transform:scale(1.06);
  animation:heroZoom 16s ease-out forwards;
}
/* Auto-slider (index.php only, for now): each slide is a full-bleed
   absolutely-positioned layer, stacked on top of each other. Crossfade +
   Ken Burns zoom are both driven by GSAP (assets/js/main_1.js, heroSlider)
   purely through opacity/transform - never width/height/top/left - so the
   browser only ever has to composite, not reflow, on every animated frame. */
.hero-slide{
  position:absolute; inset:0;
  opacity:0;
}
.hero-slide.is-active{ opacity:1; z-index:1; }
.hero-slide img{
  width:100%; height:100%;
  object-fit:cover;
  object-position:center 32%;
  transform:scale(1.06);
  will-change:transform;
}
/* Used by the plain single-image .hero-bg > img rule above. The GSAP
   slider drives its own zoom per-slide in JS instead of this keyframe. */
@keyframes heroZoom{ to{ transform:scale(1); } }
.hero::before{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(20,15,10,.4) 0%, rgba(20,15,10,.25) 35%, rgba(20,15,10,.92) 100%);
  z-index:1;
}
.hero .container{ position:relative; z-index:2; }
.hero-tag{
  font-family:var(--font-mono);
  font-size:.78rem;
  letter-spacing:.22em;
  text-transform:uppercase;
  color:#EADFC9;
  display:flex; align-items:center; gap:12px;
  margin-bottom:28px;
}
.hero-tag::before{ content:""; width:36px; height:1px; background:var(--clr-gold); }
/* Shared column width for the title + description, so the description box
   never runs wider than the title above it - also keeps the whole copy
   group hugging the left edge instead of spreading toward the middle of
   the frame, where the hero photos' subjects tend to sit. */
.hero .container{ --hero-copy-width: 620px; }
.hero h1{
  color:#fff;
  /* Trimmed down from clamp(3rem, 8.5vw, 7.2rem) - the old max size ran
     text straight across the subjects in the hero photo on wide screens. */
  font-size:clamp(2.4rem, 5.5vw, 4.8rem);
  line-height:1.04;
  letter-spacing:-.02em;
  max-width:var(--hero-copy-width);
  /* Anton only ships weight 400 - see the h1-h4 rule near the top of this
     file for the full explanation. This selector is more specific than that
     one (.hero h1 beats the bare h1,h2,h3,h4 list), so it needs its own
     explicit 400 or it'll ask the browser to fake-bold an already-bold font. */
  font-weight:400;
}
.hero h1 em{ font-style:italic; color:#EFC182; font-weight:400; }
.hero p{
  color:#E9E1D2;
  /* Matches .hero h1's max-width exactly (both read off the same
     --hero-copy-width var) instead of the old independent 44ch, which ran
     wider than the title and back under the photo's subjects. */
  max-width:var(--hero-copy-width);
  font-size:1.05rem;
  margin-top:22px;
}
.hero-actions{
  display:flex; gap:16px; flex-wrap:wrap;
  margin-top:32px;
}

/* Short viewports (landscape phones, a resized/short browser window) - 100vh
   plus this much hero copy can overflow past the top of the section and
   collide with the fixed navbar above it. Shrink everything to fit instead
   of trusting min-height:100vh to always have enough room. */
@media (max-height: 480px){
  .hero{ min-height:auto; padding-top:calc(var(--announce-h) + var(--nav-h) + 20px); padding-bottom:24px; }
  .hero-tag{ margin-bottom:10px; }
  .hero h1{ font-size:1.5rem; line-height:1.08; }
  .hero p{ font-size:.85rem; margin-top:10px; }
  .hero-actions{ margin-top:14px; gap:10px; }
  .hero-actions .btn{ padding:9px 18px; font-size:.78rem; }
  .hero-scroll{ display:none; }
}

@media (max-width: 640px){
  .hero p{
    font-size:1rem;
    margin-top:16px;
    /* Truncation removed on request - description reads in full now, no
       -webkit-line-clamp. .hero{align-items} below still bottom-anchors the
       whole group instead of centering, which is what actually protects
       the action buttons from the fold now that the text can run long. */
  }
  .hero-actions{ margin-top:20px; }
}

/* Marquee ticker - a thin scrolling strip that bridges the hero into the
   Story section, the first "this is not the same site" signal past the fold. */
.marquee{
  position:relative;
  overflow:hidden;
  background:var(--clr-panel-dark);
  border-top:1px solid var(--clr-line-on-dark);
  border-bottom:1px solid var(--clr-line-on-dark);
  padding:18px 0;
}
.marquee-track{
  display:flex;
  width:max-content;
  animation:marqueeScroll 32s linear infinite;
}
.marquee-track span{
  display:inline-flex;
  align-items:center;
  gap:18px;
  font-family:var(--font-mono);
  font-size:.82rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--clr-ink-soft-on-dark);
  padding:0 28px;
  white-space:nowrap;
}
.marquee-track span::after{ content:"\2022"; color:var(--clr-terracotta); margin-left:18px; }
@keyframes marqueeScroll{ from{ transform:translateX(0); } to{ transform:translateX(-50%); } }
@media (prefers-reduced-motion: reduce){
  .marquee-track{ animation:none; }
}

.hero-scroll{
  position:absolute; bottom:28px; left:0; right:0;
  z-index:2;
  display:flex; justify-content:center;
}
.hero-scroll span{
  width:1px; height:52px;
  background:linear-gradient(180deg, rgba(255,255,255,.9), rgba(255,255,255,0));
  display:block;
  animation:scrollLine 2s ease-in-out infinite;
}
@keyframes scrollLine{ 0%,100%{ opacity:.3; } 50%{ opacity:1; } }

/* Two very soft radial glows (terracotta, gold) instead of a flat fill - just
   enough warmth to keep a long scroll from feeling like one dark wall, low
   enough opacity that nothing glares or fights the content on top of it. */
.section{
  padding:120px 0;
  position:relative;
  /* --glow1-x/y/--glow2-x/y default to the original fixed positions; GSAP
     (assets/js/animations.js) tweens them slowly for a living-atmosphere
     effect - falls back to this static state with no JS/reduced-motion. */
  background:
    radial-gradient(ellipse 900px 500px at var(--glow1-x, 12%) var(--glow1-y, -5%), rgba(193,80,42,.035), transparent 60%),
    radial-gradient(ellipse 800px 550px at var(--glow2-x, 88%) var(--glow2-y, 105%), rgba(217,164,65,.025), transparent 60%),
    var(--clr-bg);
}
/* Alternating sections get an actual gradient band (dark -> warm brown ->
   dark) through the middle, so the rhythm between .section/.section-alt
   reads as light-dark variation while scrolling, not just a color swap. */
.section-alt{
  background:
    radial-gradient(ellipse 700px 460px at var(--glow1-x, 85%) var(--glow1-y, -5%), rgba(193,80,42,.03), transparent 60%),
    radial-gradient(ellipse 650px 480px at var(--glow2-x, 15%) var(--glow2-y, 105%), rgba(217,164,65,.02), transparent 60%),
    linear-gradient(180deg, var(--clr-bg-alt) 0%, var(--clr-bg-warm) 50%, var(--clr-bg-alt) 100%);
}
.section-head{
  display:flex;
  justify-content:space-between;
  align-items:flex-end;
  gap:40px;
  margin-bottom:64px;
  flex-wrap:wrap;
}
.section-head p{ max-width:38ch; margin:0; }

.story-grid{
  position:relative;
  display:grid;
  grid-template-columns:.85fr 1.15fr;
  gap:72px;
  align-items:center;
}
/* Giant faint outline word behind the section - the kind of editorial
   signature move that makes a section unmistakably "not a generic template." */
.story-bgword{
  position:absolute;
  top:-64px; left:50%;
  transform:translateX(-50%);
  font-family:var(--font-display);
  font-size:clamp(4.5rem, 15vw, 12rem);
  font-weight:400;
  color:transparent;
  -webkit-text-stroke:1px var(--clr-line);
  white-space:nowrap;
  z-index:0;
  pointer-events:none;
  user-select:none;
  line-height:1;
}
.story-media, .story-copy{ position:relative; z-index:1; }
.story-media{ position:relative; overflow:hidden; border-radius:var(--radius-md); }
.story-media img{
  border-radius:var(--radius-md);
  width:100%;
  aspect-ratio:4/5;
  object-fit:cover;
  filter:sepia(.14) saturate(1.1) contrast(1.05);
}
.story-media .story-badge{
  position:absolute;
  bottom:-28px; left:-28px;
  background:var(--clr-terracotta);
  color:#fff;
  padding:22px 26px;
  border-radius:var(--radius-md);
  max-width:220px;
  box-shadow:0 20px 40px -12px rgba(154,61,31,.45);
}
.story-badge strong{ display:block; font-family:var(--font-display); font-weight:400; font-size:2.1rem; line-height:1; }
.story-badge span{ font-size:.78rem; opacity:.85; letter-spacing:.02em; }

.story-copy p{ font-size:1.02rem; }

/* Stat pair for the plantation/harvest facts, sits under the Our Process
   section-head rather than bundled into Our Story. */
.process-stats{
  display:grid;
  grid-template-columns:repeat(2,minmax(180px,240px));
  gap:32px;
  margin:0 0 48px;
}
.process-stats div strong{
  display:block;
  font-family:var(--font-display);
  font-weight:400;
  font-size:1.7rem;
  color:var(--clr-terracotta);
}
.process-stats div span{ font-size:.82rem; color:var(--clr-ink-soft); }

/* Single café-location stat, sits with the address block in Contact. */
.location-stat{ margin-bottom:18px; }
.location-stat strong{
  display:block;
  font-family:var(--font-display);
  font-weight:400;
  font-size:1.4rem;
  color:var(--clr-terracotta);
}
.location-stat span{ font-size:.82rem; color:var(--clr-ink-soft); }
/* ---------- Journey - a set of standalone photo stories, not a numbered
   process. ---------- */
.journey{ padding-top:0; }
.journey-card{
  position:relative;
  display:flex;
  flex-direction:column;
  border-radius:var(--radius-md);
  overflow:hidden;
  outline:none;
  background:var(--clr-surface);
}
/* Arrows hidden on request (2026-08-13) - with only 4 process cards, they
   all already fit on screen with nothing to scroll to, so the arrows had
   nothing to do. Markup/JS left untouched (still float over the left/right
   edges of .journey-scroll when re-enabled) - flip this back to
   `display:contents` once enough cards are added that scrolling is needed
   again. */
.journey-nav{ display:none; }
.journey-arrow{
  position:absolute;
  top:calc(50% + 18px);
  transform:translateY(-50%);
  z-index:3;
  width:44px; height:44px; border-radius:50%;
  background:rgba(26,11,5,.65);
  backdrop-filter:blur(3px);
  border:1px solid rgba(244,234,221,.25);
  display:flex; align-items:center; justify-content:center;
  font-size:1.3rem; line-height:1; color:var(--clr-ink-on-dark);
  cursor:pointer;
  box-shadow:0 6px 18px rgba(0,0,0,.35);
  transition:background .3s, border-color .3s;
}
.journey-arrow.prev{ left:8px; }
.journey-arrow.next{ right:8px; }
.journey-arrow:hover{ background:var(--clr-terracotta); border-color:var(--clr-terracotta); color:#fff; }

/* Story cards (scrolling row): text panel above, photo below - same structure
   as the reference layout, just recolored to the site's dark palette instead
   of copying its bright per-category colors. */
.journey-scroll{ position:relative; padding-bottom:100px; }
.journey-cards{
  display:flex;
  gap:18px;
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  scroll-padding-inline:50%;
  padding:4px 4px 8px;
  cursor:grab;
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.journey-cards::-webkit-scrollbar{ display:none; }
.journey-cards.is-dragging{ cursor:grabbing; scroll-snap-type:none; user-select:none; }
.journey-cards .journey-card{
  flex:0 0 240px;
  scroll-snap-align:center;
}
.journey-card-info{ padding:18px 18px 8px; }
.journey-card-info h3{ font-family:var(--font-display); font-size:1.1rem; margin:0 0 8px; color:var(--clr-ink); }
.journey-card-desc{
  font-size:.85rem;
  line-height:1.5;
  color:var(--clr-ink-soft);
  margin:0;
}
/* ~20% taller than a plain 4:3 photo, so the image fills more of the card
   underneath the (now always-fully-visible, variable-length) text above it. */
.journey-card-media{ position:relative; aspect-ratio:10/9; margin-top:auto; overflow:hidden; }
.journey-card-media img{
  width:100%; height:100%;
  object-fit:cover;
  /* Ties photos shot in very different light (misty highlands, tin-roof
     sheds, warm indoor bokeh) into one consistent, slightly warm palette. */
  filter:sepia(.14) saturate(1.1) contrast(1.05);
  transition:transform .6s var(--ease);
  /* Browsers natively let you drag an <img> (ghost-drag to desktop/another
     tab, cursor shows a "no drop" badge) - that native drag was hijacking
     our custom click-and-drag scroll whenever the pointer started on top of
     a photo instead of the card's empty space. Disabling native image drag
     leaves our JS drag-to-scroll (main_1.js) as the only thing responding. */
  -webkit-user-drag:none;
  user-select:none;
  will-change:transform;
}
.journey-card:hover .journey-card-media img{ transform:scale(1.08); }
/* Steps added before their real photo is ready (e.g. via "Manage Process
   Steps") get a labeled placeholder instead of a missing/broken image -
   same treatment as .location-card-media--placeholder. */
.journey-card-media--placeholder{
  width:100%; height:100%;
  display:flex; align-items:center; justify-content:center;
  text-align:center;
  padding:20px;
  background:var(--clr-bg-alt);
}
.journey-card-media--placeholder span{
  font-family:var(--font-mono);
  font-size:.72rem;
  letter-spacing:.02em;
  color:var(--clr-ink-soft);
}
/* ---------- Portal Card teaser (2x2): homepage-only preview for content
   that would otherwise grow unbounded (Articles now, Activities later once
   its nav tab comes back). Just the 2 latest content tiles, side by side -
   the homepage itself never grows past these 2 no matter how much content
   exists in the DB. "Explore The Journey" sits below as its own centered
   link (.portal-teaser-cta), not a third grid tile. Sized at 60% of the
   original 260px tile height. */
.portal-teaser{
  display:grid;
  grid-template-columns:repeat(2, 1fr);
  grid-template-rows:156px;
  gap:20px;
}
.portal-teaser-cta{
  display:flex;
  justify-content:center;
  margin-top:24px;
}
.portal-card{
  position:relative;
  display:block;
  border-radius:var(--radius-md);
  overflow:hidden;
  border:1px solid var(--clr-line);
  background:var(--clr-surface);
}
.portal-card img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
  transition:transform .5s var(--ease);
}
.portal-card:hover img{ transform:scale(1.06); }
.portal-card::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(to top, rgba(26,11,5,.85), rgba(26,11,5,.05) 55%);
}
.portal-card-info{ position:absolute; left:0; right:0; bottom:0; padding:18px; z-index:1; }
.portal-card-info .portal-cat{
  font-family:var(--font-mono); font-size:.68rem;
  text-transform:uppercase; letter-spacing:.06em;
  color:var(--clr-gold); display:block; margin-bottom:4px;
}
.portal-card-info h4{ font-size:1.05rem; margin:0; color:var(--clr-ink-on-dark); line-height:1.3; }

/* Solid terracotta, sized to its own text (not a big decorative shape) -
   now sits in .portal-teaser-cta below the 2 tiles instead of occupying a
   grid tile of its own. Matches .btn-primary's color/hover exactly (was a
   translucent frosted-glass pill, read as too pale next to the solid
   "Explore All Coffees" button above it - owner request 2026-08-19). */
.portal-cta-label{
  position:relative;
  display:inline-flex; align-items:center; gap:8px;
  padding:14px 30px;
  white-space:nowrap;
  border-radius:999px;
  font-family:var(--font-display);
  font-size:1.05rem; font-weight:400;
  color:#fff;
  background:var(--clr-terracotta);
  border:1px solid transparent;
  transition:transform .3s var(--ease), background .3s var(--ease);
}
.portal-cta-label:hover{
  background:var(--clr-terracotta-dark);
  transform:translateY(-2px);
}
/* ---------- Journey: asymmetric bento grid + <dialog> modal ----------
   Used by .journey-grid (journey.php) to lay out .bento-tile children.
   activities.php had a matching .activities-bento grid before it was
   deleted 2026-08-18 (Activities is now just a filter label on this page). */
.journey-grid{
  display:grid;
  grid-template-columns:repeat(4, 1fr);
  grid-auto-rows:190px;
  grid-auto-flow:dense;
  gap:16px;
}
.bento-tile{
  position:relative;
  display:block;
  width:100%; height:100%;
  padding:0; margin:0;
  border:1px solid var(--clr-line);
  border-radius:var(--radius-md);
  overflow:hidden;
  cursor:pointer;
  background:var(--clr-surface);
  font:inherit; text-align:left; color:inherit;
}
.bento-tile.is-lg{ grid-column:span 2; grid-row:span 2; }
.bento-tile.is-wide{ grid-column:span 2; grid-row:span 1; }
.bento-tile.is-tall{ grid-column:span 1; grid-row:span 2; }
.bento-tile img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
  transition:transform .5s var(--ease);
}
.bento-tile:hover img{ transform:scale(1.06); }
.bento-tile::after{
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(to top, rgba(26,11,5,.85), rgba(26,11,5,.05) 55%);
}
.bento-tile-info{ position:absolute; left:0; right:0; bottom:0; padding:16px; z-index:1; }
.bento-tile-info .bento-cat{
  font-family:var(--font-mono); font-size:.68rem;
  text-transform:uppercase; letter-spacing:.06em;
  color:var(--clr-gold); display:block; margin-bottom:4px;
}
/* line-clamp is a safety net, not just a mobile fix - .bento-tile-info is
   absolutely positioned with no fixed height, so a long title with nothing
   capping it can grow taller than the tile itself and get sliced off by the
   tile's own overflow:hidden. Capping at 2 lines + ellipsis means a long
   title trims cleanly instead of overflowing/getting clipped. */
.bento-tile-info h4{
  font-size:1rem; margin:0; color:var(--clr-ink-on-dark); line-height:1.3;
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2;
  overflow:hidden; word-break:break-word;
}
.bento-tile.is-lg .bento-tile-info h4{ font-size:1.3rem; }

/* ---------- Journey filter tabs (journey.php) ---------- */
.journey-filters-sentinel{ height:1px; }
.journey-filters{
  display:flex; gap:10px; margin-bottom:32px; flex-wrap:wrap;
  position:sticky;
  top:64px;
  z-index:10;
  padding:10px 0;
  transition:background-color .3s, backdrop-filter .3s;
}
/* Toggled by an IntersectionObserver on .journey-filters-sentinel above -
   position:sticky itself already handles pinning/unpinning (it naturally
   stops sticking once #journey-section scrolls out of view, no JS needed
   for that part); this class only adds a backdrop once it's actually
   floating over content, so it stays legible against whatever scrolls
   underneath it. */
.journey-filters.is-stuck{
  background:color-mix(in srgb, var(--clr-panel-dark) 75%, transparent);
  backdrop-filter:blur(8px);
  border-radius:0 0 var(--radius-md) var(--radius-md);
}
.journey-filter{
  padding:9px 20px;
  border-radius:999px;
  border:1px solid var(--clr-line);
  background:transparent;
  color:var(--clr-ink-soft);
  font-family:var(--font-mono);
  font-size:.78rem;
  text-transform:uppercase;
  letter-spacing:.04em;
  cursor:pointer;
  transition:background .3s, border-color .3s, color .3s;
}
.journey-filter:hover{ border-color:var(--clr-terracotta); color:var(--clr-ink); }
.journey-filter.is-active{ background:var(--clr-terracotta); border-color:var(--clr-terracotta); color:#fff; }
/* Once stuck, the bar itself becomes the fixed-dark panel-dark chip above -
   its resting/hover text has to switch to the fixed on-dark tokens to match,
   same reasoning as the navbar. */
.journey-filters.is-stuck .journey-filter{ color:var(--clr-ink-soft-on-dark); }
.journey-filters.is-stuck .journey-filter:hover{ color:var(--clr-ink-on-dark); }

/* ---------- Journey floating jump-nav (journey.php) ----------
   Same fixed-overlay tier as .floating-social (bottom-right, WhatsApp) but
   bottom-left so the two never collide. Blurred/translucent so it blends
   with the page's noise-grain overlay instead of sitting as a flat box. */
.journey-jumpnav{
  position:fixed;
  left:20px; bottom:20px;
  z-index:900;
  display:flex; flex-direction:column; gap:8px;
  padding:8px;
  border-radius:999px;
  background:color-mix(in srgb, var(--clr-panel-dark) 55%, transparent);
  backdrop-filter:blur(6px);
  border:1px solid var(--clr-line-on-dark);
}
.journey-jumpnav a{
  padding:8px 16px;
  border-radius:999px;
  font-family:var(--font-mono);
  font-size:.72rem;
  text-transform:uppercase;
  letter-spacing:.04em;
  color:var(--clr-ink-soft-on-dark);
  text-align:center;
  transition:background .3s, color .3s;
}
.journey-jumpnav a:hover{ color:var(--clr-ink-on-dark); }
.journey-jumpnav a.is-active{ background:var(--clr-terracotta); color:#fff; }
@media (max-width:640px){
  .journey-jumpnav{ left:14px; bottom:14px; }
  .journey-jumpnav a{ padding:7px 12px; font-size:.68rem; }
}

/* ---------- Gallery masonry (journey.php) ----------
   CSS-only masonry via column-count - no JS layout library. break-inside:avoid
   keeps a photo from being sliced across two columns. */
.gallery-masonry{
  column-count:3;
  column-gap:16px;
}
.gallery-item{
  display:block;
  width:100%;
  break-inside:avoid;
  margin:0 0 16px;
  padding:0;
  border:none;
  border-radius:var(--radius-md);
  overflow:hidden;
  cursor:pointer;
  background:var(--clr-surface);
}
.gallery-item img{
  width:100%;
  display:block;
  transform:scale(1) translateZ(0);
  transition:transform .6s cubic-bezier(.25,1,.5,1);
  will-change:transform;
}
.gallery-item:hover img{ transform:scale(1.04) translateZ(0); }
/* Focus-dim: hovering one photo dims the rest, pure CSS (no JS listener) via :has(). */
.gallery-masonry:has(.gallery-item:hover) .gallery-item:not(:hover){
  opacity:.5;
  transition:opacity .3s ease;
}

.activity-modal{
  /* Explicit fixed+centered instead of relying on the UA stylesheet's
     default dialog:modal{position:fixed;inset:0;margin:auto} - leaving
     `position` as the only override (as this rule used to) let the dialog
     fall back to flowing in-document at wherever its DOM position happens
     to be, instead of centering as an overlay. That made showModal()'s
     "scroll the dialog into view" behavior jump the page around unpredictably
     (most noticeable clicking a gallery photo near the bottom of the page). */
  position:fixed;
  inset:0;
  margin:auto;
  border:none; border-radius:var(--radius-md); padding:0;
  max-width:640px; width:90vw;
  max-height:85vh;
  background:var(--clr-surface); color:var(--clr-ink);
  overflow:hidden;
}
.activity-modal::backdrop{ background:rgba(26,11,5,.7); backdrop-filter:blur(2px); }
/* GSAP animates this wrapper on open/close, never the <dialog> element itself -
   applying opacity/transform directly to a just-showModal()'d dialog makes
   Chrome silently drop it out of the top layer (no ::backdrop, no `:modal`
   match, dialog.open stays true) with no error or close event fired. Keeping
   the dialog's own inline style untouched avoids that entirely. */
.modal-anim-wrap{ display:block; width:100%; height:100%; }
.activity-modal-media{ aspect-ratio:16/9; overflow:hidden; }
.activity-modal-media img{ width:100%; height:100%; object-fit:cover; }
.activity-modal-body{ padding:24px 28px 28px; }
.activity-modal-body h3{ font-size:1.3rem; margin:0 0 10px; }
.activity-modal-body p{ font-size:.92rem; color:var(--clr-ink-soft); margin:0 0 16px; }

/* Used by the Journey modal (journey.php), which populates this dialog
   structure via JS. activities.php had a matching modal before it was
   deleted 2026-08-18. */
.modal-meta{
  font-family:var(--font-mono);
  font-size:.7rem;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--clr-moss);
  display:flex; gap:10px; margin-bottom:12px;
}
.modal-link{ font-size:.84rem; font-weight:600; color:var(--clr-terracotta); display:inline-flex; align-items:center; gap:6px; }
.modal-link::after{ content:"→"; transition:transform .3s var(--ease); }
.modal-link:hover::after{ transform:translateX(4px); }
.activity-modal-close{
  position:absolute; top:16px; right:16px; z-index:2;
  width:36px; height:36px; border-radius:50%;
  /* Fixed dark chip + white icon regardless of theme, same as the product
     photo slider's own prev/next/dots - it sits over a photo (activities/
     journey tiles, product modal media) more often than not, so it can't
     safely follow --clr-ink (would go invisible: dark ink text on this
     same dark chip in light mode). */
  background:rgba(26,11,5,.55); border:1px solid rgba(244,234,221,.25);
  color:#fff; font-size:1.3rem; font-weight:600; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
}
.activity-modal-close:hover,
.activity-modal-close:focus-visible{
  /* Same solid fill used on hover covers the "make it clear" need on focus
     too - no ring/outline, so it doesn't rely on the site-wide
     :focus-visible outline (which reads as a stray orange circle floating
     outside this already-circular button). */
  background:var(--clr-terracotta); border-color:var(--clr-terracotta); color:#fff;
  outline:none;
}

.products-grid{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:80px;
  align-items:start;
  margin-bottom:100px;
}
/* Green Beans cards, full section width now that Café Ambiance has its own section. */
.product-cards-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));
  gap:24px;
  margin-bottom:24px;
}
/* "Explore All Coffees" under the homepage teaser grid (desktop) - centered
   under the row of cards instead of sitting left-aligned on its own line. */
.product-cta-desktop{
  display:flex;
  width:fit-content;
  margin:24px auto 0;
}
.product-card{
  background:var(--clr-surface);
  border:1px solid var(--clr-line);
  border-radius:var(--radius-md);
  overflow:hidden;
  margin-bottom:18px;
  /* Column flex so the footer (stock badge) can pin to the bottom of the
     card via margin-top:auto below, instead of sitting wherever the title/
     tags above it happen to end - cards in the same grid row now line up
     their badges on one baseline regardless of title length. */
  display:flex;
  flex-direction:column;
}
.product-card-media{ aspect-ratio:1/1; overflow:hidden; background:var(--clr-bg-alt); }
.product-card-media img{
  width:100%; height:100%;
  /* contain, not cover - the full product photo shows uncropped, letterboxed
     against the panel's own background on the rare photo whose aspect ratio
     doesn't match the 1:1 card slot (all current Shopify photos are 1:1). */
  object-fit:contain;
  /* Same warm-toned treatment as the journey card photos, so Shopify's own
     product photography (shot under different lighting/backdrops) still
     reads as part of the same site instead of a visibly separate source. */
  filter:sepia(.14) saturate(1.1) contrast(1.05);
}
.product-card-body{ padding:30px; flex:1; display:flex; flex-direction:column; }
.product-card .p-head{ display:flex; justify-content:space-between; align-items:flex-start; gap:16px; margin-bottom:10px; }
.product-card h4{ font-size:1.15rem; margin:0; }
.product-card .price{
  font-family:var(--font-mono);
  color:var(--clr-terracotta);
  font-weight:600;
  white-space:nowrap;
}
.product-card p{ font-size:.9rem; margin:0; }
/* ---------- shop.php category grouping (mobile-only slide, see 12-responsive.css) ----------
   Desktop keeps the plain unified grid exactly as before this feature -
   .shop-jenis-group/.shop-jenis-row use display:contents so their card
   children become direct items of the outer .product-cards-grid, and the
   category title / "coming soon" placeholder stay hidden above the mobile
   breakpoint. */
.shop-jenis-group{ display:contents; }
.shop-jenis-row{ display:contents; }
.shop-jenis-title{ display:none; }
.product-card--placeholder{ display:none; }
.product-tags{ display:flex; gap:8px; margin-top:14px; flex-wrap:wrap; }
.product-tags span{
  font-family:var(--font-mono);
  font-size:.68rem;
  letter-spacing:.04em;
  text-transform:uppercase;
  background:var(--clr-bg-alt);
  color:var(--clr-ink-soft);
  padding:5px 10px;
  border-radius:999px;
}

/* Mobile-only "See All Products" pill, tacked on as the last slide of the
   swipeable row below (see .product-cards-grid mobile override) - same
   frosted-glass pill language as the Journey CTA, but solid/opaque instead
   of translucent so it reads brighter and easier to spot mid-swipe. Hidden
   entirely above the mobile breakpoint (desktop keeps the "Explore All
   Coffees" button under the grid instead). */
.product-card--cta{ display:none; }
.product-cta-pill{
  display:inline-flex; align-items:center; gap:8px;
  padding:14px 28px;
  white-space:nowrap;
  border-radius:999px;
  font-family:var(--font-display);
  font-size:1rem; font-weight:400;
  color:#fff;
  background:linear-gradient(135deg, var(--clr-terracotta), var(--clr-terracotta-dark));
  border:1px solid rgba(244,234,221,.3);
  box-shadow:0 14px 30px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.25);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  transition:transform .3s var(--ease);
}
.product-cta-pill:hover{ transform:scale(1.05); }

/* ---------- Product card footer: Shopify stock badge + buy link ----------
   Only rendered when the product came from Shopify (getShopifyProducts()) -
   local `products` table rows don't have 'available'/'url' so this whole
   block is skipped for those, no layout gap left behind. */
.product-card-footer{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  /* auto pushes this to the bottom of .product-card-body's flex column,
     regardless of how much space the title/tags above take up. */
  margin-top:auto;
  padding-top:16px;
}
.stock-badge{
  display:inline-flex;
  align-items:center;
  gap:7px;
  font-family:var(--font-mono);
  font-size:.68rem;
  text-transform:uppercase;
  letter-spacing:.04em;
  padding:5px 10px;
  border-radius:999px;
}
/* Pulsing dot - a ripple keyframe on box-shadow, colored per state via the
   --pulse-rgb custom property so one animation serves both in-stock/out-of-
   stock instead of two near-duplicate keyframes. */
.stock-badge::before{
  content:"";
  width:7px; height:7px;
  border-radius:50%;
  flex-shrink:0;
  background:rgb(var(--pulse-rgb));
  box-shadow:0 0 0 0 rgba(var(--pulse-rgb), .55);
  animation:stock-pulse 1.8s ease-out infinite;
}
@keyframes stock-pulse{
  0%{ box-shadow:0 0 0 0 rgba(var(--pulse-rgb), .55); }
  70%{ box-shadow:0 0 0 7px rgba(var(--pulse-rgb), 0); }
  100%{ box-shadow:0 0 0 0 rgba(var(--pulse-rgb), 0); }
}
.stock-badge.in-stock{
  --pulse-rgb: var(--clr-moss-rgb);
  background:rgba(var(--clr-moss-rgb), .18);
  color:var(--clr-moss);
}
.stock-badge.out-of-stock{
  --pulse-rgb: var(--clr-gray-light-rgb);
  background:rgba(var(--clr-gray-light-rgb), .16);
  color:var(--clr-gray-light);
}

/* ---------- Shop cart: floating button + <dialog> drawer ----------
   Bottom-left, same slot .journey-jumpnav uses on journey.php - no
   collision since those two pages never both render at once. */
/* Fixed top-right, but only surfaced (opacity/translate) while the products
   grid is in view - toggled via .is-visible by onCartButtonScroll in main_1.js. */
.cart-button-wrap{
  position:fixed; top:calc(var(--announce-h) + var(--nav-h) + 20px); right:20px; z-index:900;
  opacity:0;
  transform:translateY(-8px);
  pointer-events:none;
  transition:opacity .3s var(--ease), transform .3s var(--ease);
}
.cart-button-wrap.is-visible{
  opacity:1;
  transform:translateY(0);
  pointer-events:auto;
}
.cart-button{
  position:relative;
  width:46px; height:46px;
  border-radius:50%;
  background:linear-gradient(135deg, var(--clr-bg-warm), var(--clr-bg-warm-2));
  border:1px solid var(--clr-line);
  display:flex; align-items:center; justify-content:center;
  color:var(--clr-ink-soft);
  cursor:pointer;
  box-shadow:0 6px 18px rgba(0,0,0,.35);
  transition:background .25s, color .25s, border-color .25s;
}
.cart-button:hover{ background:var(--clr-terracotta); color:#fff; border-color:var(--clr-terracotta); }
.cart-button svg{ width:20px; height:20px; }
.cart-count{
  position:absolute; top:-4px; right:-4px;
  min-width:18px; height:18px; padding:0 4px;
  border-radius:999px;
  background:var(--clr-terracotta);
  color:#fff;
  font-family:var(--font-mono);
  font-size:.65rem; font-weight:700;
  display:none; align-items:center; justify-content:center;
}
@media (max-width:640px){ .cart-button-wrap{ top:calc(var(--announce-h) + var(--nav-h) + 14px); right:14px; } }

.cart-modal-body{ padding:24px 28px 28px; max-height:60vh; overflow-y:auto; }
.cart-items{ display:flex; flex-direction:column; gap:16px; }
.cart-item{
  display:grid;
  grid-template-columns:56px 1fr auto auto;
  align-items:center;
  gap:12px;
}
.cart-item img{ width:56px; height:56px; object-fit:cover; border-radius:6px; background:var(--clr-bg-alt); }
.cart-item-info h5{ font-size:.92rem; margin:0 0 4px; line-height:1.3; }
.cart-item-info span{ font-family:var(--font-mono); font-size:.8rem; color:var(--clr-terracotta); }
.cart-item-qty{ display:flex; align-items:center; gap:8px; }
.cart-item-qty button{
  width:26px; height:26px; border-radius:50%;
  border:1px solid var(--clr-line); background:transparent; color:var(--clr-ink);
  cursor:pointer; font-size:.9rem; line-height:1; padding:0;
}
.cart-item-qty button:hover{ border-color:var(--clr-terracotta); color:var(--clr-terracotta); }
.cart-item-qty span{ min-width:16px; text-align:center; font-size:.85rem; }
.cart-item-remove{
  background:transparent; border:none; color:var(--clr-ink-soft);
  font-size:1.2rem; cursor:pointer; line-height:1; padding:4px;
}
.cart-item-remove:hover{ color:var(--clr-terracotta); }
#cart-empty{ text-align:center; color:var(--clr-ink-soft); padding:24px 0; font-size:.9rem; }
.cart-total-row{
  display:flex; justify-content:space-between; align-items:center;
  padding-top:16px; margin-top:16px; border-top:1px solid var(--clr-line);
  font-weight:600;
}
.cart-total-row span:last-child{ color:var(--clr-terracotta); font-family:var(--font-mono); }
#cart-checkout-btn{ width:100%; justify-content:center; margin-top:18px; }
/* ---------- Product detail modal (full description + qty stepper) ----------
   Image left, details right - overrides the stacked media/body layout the
   Activities/Gallery modal uses, since .product-modal is a wider dialog. */
.product-modal{ max-width:820px; }
.product-modal[open] .modal-anim-wrap{ display:flex; }
/* Media panel: background:contain (not cover) so the full product photo
   always shows uncropped, letterboxed against the panel's own background
   when its aspect ratio doesn't match - a slider of one <img> per slide,
   shifted via transform so it can hold every Shopify image, not just the
   featured one. */
.product-modal-media{ flex:0 0 44%; align-self:stretch; overflow:hidden; position:relative; background:var(--clr-bg-alt); }
.product-modal-slider{ display:flex; height:100%; transition:transform .4s cubic-bezier(.25,1,.5,1); }
.product-modal-slide{ flex:0 0 100%; width:100%; height:100%; object-fit:contain; background:var(--clr-bg-alt); cursor:zoom-in; }
.product-modal-nav{
  position:absolute; top:50%; transform:translateY(-50%);
  width:34px; height:34px; border-radius:50%;
  background:rgba(26,11,5,.55); border:1px solid rgba(244,234,221,.25);
  color:#fff; font-size:1.5rem; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center; z-index:2;
}
.product-modal-nav:hover{ background:var(--clr-terracotta); border-color:var(--clr-terracotta); }
.product-modal-prev{ left:10px; }
.product-modal-next{ right:10px; }
.product-modal-dots{ position:absolute; bottom:12px; left:0; right:0; display:flex; justify-content:center; gap:6px; z-index:2; }
.product-modal-dot{ width:7px; height:7px; border-radius:50%; background:rgba(244,234,221,.45); border:none; padding:0; cursor:pointer; }
.product-modal-dot.active{ background:#fff; }

/* ---------- Photo popup ----------
   Clicking the currently-visible slide inside the product modal pops the
   same photo out into its own dialog, a bit bigger than the modal's own
   44%-wide media panel - not a full-screen takeover, not an in-place
   scale/zoom. Browsing (arrows/dots/swipe) carries over, starting on
   whichever photo was clicked. Reuses .product-modal-nav/-dots/-dot as-is
   for a consistent look (see 19-product-modal.js). */
.photo-popup-modal{ max-width:640px; }
.photo-popup-modal[open] .modal-anim-wrap{ display:block; }
.photo-popup-media{ position:relative; width:100%; aspect-ratio:4/3.2; overflow:hidden; background:var(--clr-bg-alt); }
.photo-popup-slider{ display:flex; height:100%; transition:transform .4s cubic-bezier(.25,1,.5,1); }
.photo-popup-slider img{ flex:0 0 100%; width:100%; height:100%; object-fit:contain; background:var(--clr-bg-alt); }
.product-modal-body{ flex:1; padding:28px 32px; max-height:85vh; overflow-y:auto; }
.product-modal-body h3{ font-size:1.3rem; margin:0 0 10px; }
.product-modal-price{ font-family:var(--font-mono); color:var(--clr-terracotta); font-weight:600; display:block; margin-bottom:14px; }
#product-modal-desc{ font-size:.9rem; color:var(--clr-ink-soft); line-height:1.6; margin-bottom:18px; }
#product-modal-desc p{ margin:0 0 12px; }
#product-modal-desc p:last-child{ margin-bottom:0; }
/* Fixed shipping summary appended by renderDescription() (main_1.js) below
   the Read More/Read Less toggle - a centered icon+title card, always one
   short line rather than the raw per-courier text from Shopify. */
.product-modal-shipping{
  margin:0 0 18px;
  padding:20px 22px;
  text-align:center;
  background:linear-gradient(165deg, var(--clr-surface), var(--clr-bg-alt));
  border:1px solid var(--clr-line);
  border-radius:16px;
}
.product-modal-shipping-icon{
  width:32px; height:32px;
  color:var(--clr-gold);
  margin:0 auto 10px;
}
.product-modal-shipping-title{
  display:block;
  color:var(--clr-gold);
  font-family:var(--font-display);
  font-size:1.05rem;
  font-weight:400;
  margin-bottom:12px;
}
.product-modal-shipping-line{
  font-size:.85rem;
  line-height:1.6;
  color:var(--clr-ink-soft);
}
.product-modal-shipping-line + .product-modal-shipping-line{ margin-top:12px; }
.product-modal-shipping-line > strong:first-child{
  display:block;
  color:var(--clr-ink);
  font-size:.82rem;
  margin-bottom:2px;
}
/* Everything from the paragraph after "Moisture" onward (process notes,
   stock policy, About Us) starts collapsed - see renderDescription() in
   main_1.js, which builds this toggle+wrapper pair. */
.product-modal-desc-toggle{
  background:none; border:none; padding:0;
  font-family:var(--font-mono);
  /* ~1.3x the original .78rem, on request */
  font-size:1.02rem;
  color:var(--clr-gold); cursor:pointer;
  margin-bottom:18px;
}
/* Was a fixed #fff "brighten on hover" - invisible in light mode (white on
   cream). Mixing toward --clr-ink instead keeps the same "shift toward the
   theme's strongest ink" intent in both themes: lightens in dark mode
   (ink is cream there), darkens/deepens in light mode (ink is near-black). */
.product-modal-desc-toggle:hover{ color:color-mix(in srgb, var(--clr-gold) 55%, var(--clr-ink) 45%); }
.product-modal-desc-more{ display:none; }
.product-modal-desc-more.is-open{ display:block; }
.product-modal-qty-row{ display:flex; align-items:center; gap:12px; margin-bottom:18px; }
.product-modal-qty-row label{ font-size:.85rem; color:var(--clr-ink-soft); }
.qty-stepper{ display:flex; align-items:center; gap:0; border:1px solid var(--clr-line); border-radius:6px; overflow:hidden; }
.qty-stepper-btn{
  width:34px; height:36px;
  background:var(--clr-bg-alt); border:none; color:var(--clr-ink);
  font-size:1rem; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
}
.qty-stepper-btn:hover{ background:var(--clr-terracotta); color:#fff; }
.qty-stepper input{
  width:48px; padding:8px 4px; text-align:center;
  background:var(--clr-surface); border:none; border-left:1px solid var(--clr-line); border-right:1px solid var(--clr-line);
  color:var(--clr-ink); font-family:var(--font-mono); font-size:.9rem;
  -moz-appearance:textfield;
}
.qty-stepper input::-webkit-outer-spin-button,
.qty-stepper input::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
/* Add to Cart + Buy Now sit right under the price/qty now, stacked (Add to
   Cart on top, Buy Now below) instead of side-by-side, each stretched full
   width with centered text but kept slim (same reduced padding/font-size
   as before, just no longer hugging the label width). */
.product-modal-actions{ display:flex; flex-direction:column; align-items:stretch; gap:10px; margin-bottom:18px; }
.product-modal-actions .btn{ width:100%; justify-content:center; padding:10px 22px; font-size:.85rem; }
/* Sold-out state swaps the two buttons for this single plain, non-clickable
   bar instead of leaving a greyed-out "disabled button" that still looks
   interactive - toggled in openProductModal() (main_1.js). */
.product-modal-soldout{
  display:none;
  width:100%;
  padding:13px 22px;
  border-radius:999px;
  background:#8c7c70;
  color:var(--clr-bg);
  font-size:.9rem;
  font-weight:600;
  text-align:center;
  margin-bottom:18px;
}
.product-modal-add{
  /* 10% lighter than --clr-terracotta, so it stands out against the plain
     terracotta price/tag accents already used elsewhere in the modal. */
  background:color-mix(in srgb, var(--clr-terracotta) 90%, #fff 10%);
  /* Soft terracotta glow instead of a hard drop shadow - reads as premium
     without breaking from the dark theme. */
  box-shadow:0 0 0 1px rgba(193,80,42,.4), 0 0 20px 2px rgba(193,80,42,.4);
}
.product-modal-add:hover{
  background:var(--clr-terracotta);
  box-shadow:0 0 0 1px rgba(193,80,42,.55), 0 0 26px 4px rgba(193,80,42,.55);
  transform:translateY(-1px);
}
.product-modal-add:disabled{ opacity:.5; cursor:not-allowed; box-shadow:none; }
.product-modal-add:disabled:hover{ background:color-mix(in srgb, var(--clr-terracotta) 90%, #fff 10%); transform:none; }
.product-modal-buy:disabled{ opacity:.5; cursor:not-allowed; }

@media (max-width:640px){
  /* .modal-anim-wrap (not .product-modal) is the actual flex container now
     (see the .modal-anim-wrap comment near .activity-modal in this file) -
     targeting .product-modal here was a leftover from before that change and
     had stopped doing anything, silently leaving mobile stuck in a row
     layout with a large empty gap above the photo. */
  .product-modal{ max-height:90vh; overflow-y:auto; }
  .product-modal .modal-anim-wrap{ flex-direction:column; }
  .product-modal-media{ flex:0 0 auto; aspect-ratio:4/3; }
  .product-modal-body{ max-height:none; overflow-y:visible; }
}

/* Product cards are clickable end-to-end to open the detail modal - there's
   no separate quick-add button inside them anymore, so any click on a card
   opens the modal (qty is chosen there before adding to cart). */
[data-product-card]{ cursor:pointer; }

.people-group{ margin-bottom:56px; }
.people-group:last-child{ margin-bottom:0; }
.people-group-title{
  font-family:var(--font-mono);
  font-size:.78rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--clr-terracotta);
  margin:0 0 20px;
}
.people-empty{ font-size:.9rem; color:var(--clr-ink-soft); margin:0; }
.people-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
  gap:24px;
}
.people-card{
  background:var(--clr-surface);
  border:1px solid var(--clr-line);
  border-radius:var(--radius-md);
  padding:24px;
  text-align:center;
}
.people-avatar{
  width:76px; height:76px;
  border-radius:50%;
  background-color:var(--clr-bg-alt);
  background-size:cover;
  background-position:center;
  margin:0 auto 16px;
  border:1px solid var(--clr-line);
}
.people-card h4{ font-size:1rem; margin:0 0 4px; }
.people-role{
  display:block;
  font-family:var(--font-mono);
  font-size:.7rem;
  letter-spacing:.04em;
  text-transform:uppercase;
  color:var(--clr-ink-soft);
  margin-bottom:10px;
}
.people-card p{ font-size:.85rem; margin:0; color:var(--clr-ink-soft); }
/* Visit Us / Coffee Shop section - subtle lighter-brown wash (theme's warm
   tokens, not a new color) to set it apart from the surrounding sections,
   plus a two-column layout: copy on the left (right-aligned), photo carousel
   on the right. */
#cafe{
  background:
    radial-gradient(ellipse 900px 500px at var(--glow1-x, 12%) var(--glow1-y, -5%), rgba(193,80,42,.035), transparent 60%),
    radial-gradient(ellipse 800px 550px at var(--glow2-x, 88%) var(--glow2-y, 105%), rgba(217,164,65,.025), transparent 60%),
    var(--clr-bg-warm-2);
}
.ambiance-carousel{ position:relative; border-radius:var(--radius-md); overflow:hidden; aspect-ratio:4/3.1; max-width:60%; margin:0 auto; }
/* Ambiance carousel reused as the Barito accordion item's photo (merged
   2026-08-19, replacing the old standalone Coffee Shop copy+carousel
   block) - fills .loc-acc-media's own box/aspect-ratio instead of the
   carousel's freestanding sizing above. */
.loc-acc-media .ambiance-carousel{ width:100%; height:100%; aspect-ratio:auto; max-width:none; margin:0; border-radius:0; }
.loc-acc-summary{ font-size:.92rem; color:var(--clr-ink-soft); margin:0 0 4px; }

/* Wholesale contact block (Coffee section, formerly wholesale.php's own
   contact block) - plain in-flow text, not the photo-card .location-info
   look, since it now sits on a normal section background. */
.wholesale-contact{ margin-top:56px; max-width:640px; }
.wholesale-contact h3{ margin-bottom:12px; }
.wholesale-contact-rows{ display:flex; flex-direction:column; gap:14px; margin-top:8px; }
.wholesale-contact-rows .li-row{ display:flex; gap:12px; align-items:center; font-size:1rem; font-weight:500; color:var(--clr-ink); }
.wholesale-contact-rows .li-row svg{ width:22px; height:22px; flex:0 0 22px; color:var(--clr-terracotta); }
.ambiance-track{ position:relative; height:100%; z-index:0; } /* z-index:0 (not just position:relative) is required here so this establishes its own stacking context - otherwise the img z-index values below leak out and out-rank the nav arrows/dots */
.ambiance-track img{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
  clip-path:polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
  z-index:0;
}
.ambiance-track img:first-child{ z-index:1; }
.ambiance-nav{
  position:absolute; bottom:16px; left:50%; transform:translateX(-50%);
  display:flex; gap:8px;
  z-index:2;
}
.ambiance-nav button{
  width:8px; height:8px; border-radius:50%;
  background:rgba(255,255,255,.5);
  border:none; padding:0;
}
.ambiance-nav button.is-active{ background:#fff; width:22px; border-radius:5px; transition:width .3s var(--ease); }
.ambiance-arrow{
  position:absolute; top:50%; transform:translateY(-50%);
  width:38px; height:38px; border-radius:50%;
  background:rgba(255,255,255,.85);
  border:none; display:flex; align-items:center; justify-content:center;
  font-size:1.1rem;
  transition:background .3s;
  z-index:2;
}
.ambiance-arrow:hover{ background:#fff; }
.ambiance-arrow.prev{ left:14px; }
.ambiance-arrow.next{ right:14px; }
.ambiance-caption{
  margin-top:16px;
  font-size:.85rem;
  color:var(--clr-ink-soft);
  display:flex; justify-content:space-between;
  max-width:60%;
  margin-left:auto; margin-right:auto;
}
.ambiance-caption span:first-child{ font-family:var(--font-mono); font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; }

.location-block{
  margin-top:20px;
  display:grid;
  grid-template-columns:1.1fr .9fr;
  gap:0;
}
/* No map column anymore - location-info spans the full block width instead
   of sharing it with the (now removed) iframe. */
.location-block--full{ grid-template-columns:1fr; }
.location-map iframe{ width:100%; height:100%; min-height:340px; border:0; display:block; filter:grayscale(.15) contrast(1.02); }
.location-info{
  /* Was fixed white/cream text - sits directly on #cafe's own theme-aware
     background (no photo behind it since the map iframe was removed), so
     it needs to actually follow the theme, not stay fixed. */
  color:var(--clr-ink);
  padding:36px 32px;
  border-top:1px solid var(--clr-line);
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:18px;
}
.location-info h4{ color:var(--clr-ink); font-size:1.3rem; margin:0; }
.location-info p{ color:var(--clr-ink-soft); font-size:.9rem; margin:0; }
.location-info .li-row{ display:flex; gap:12px; align-items:flex-start; font-size:.88rem; color:var(--clr-ink-soft); }
.location-info .li-row svg{ width:20px; height:20px; flex:0 0 20px; color:var(--clr-terracotta); margin-top:1px; }
/* ---------- Visit Us: location accordion (Flores / Barito / Tebet, dashboard-
   editable via dashboard/locations.php) ----------
   Replaces the old single-location .location-info block (kept above as a fallback
   for the rare case the `locations` table is empty) and an earlier 3-column card
   grid (top-to-bottom accordion reads better once there's more than a couple of
   locations, and keeps every row the same height whether it has a real photo or
   just a placeholder - "simetris" was the explicit ask). Surface/border language
   still matches .product-card/.journey-card rather than inventing a new look. */
.location-accordion{
  margin-top:20px;
  display:flex;
  flex-direction:column;
  gap:14px;
}
.loc-acc-item{
  background:var(--clr-surface);
  border:1px solid var(--clr-line);
  border-radius:var(--radius-md);
  overflow:hidden;
}
.loc-acc-head{
  width:100%;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:20px;
  padding:22px 26px;
  background:none;
  border:none;
  text-align:left;
  color:var(--clr-ink);
}
.loc-acc-head-text{ display:flex; flex-direction:column; gap:6px; }
.loc-acc-name{ font-family:var(--font-display); font-size:1.2rem; color:var(--clr-ink); }
.loc-acc-chevron{
  width:20px; height:20px; flex:0 0 20px;
  color:var(--clr-ink-soft);
  transition:transform .4s var(--ease);
}
.loc-acc-item.is-open .loc-acc-chevron{ transform:rotate(180deg); }
/* grid-template-rows:0fr→1fr animates height without measuring content in JS -
   the collapsed state is a real 0, not max-height:0 fighting a guessed value. */
.loc-acc-panel{
  display:grid;
  grid-template-rows:0fr;
  transition:grid-template-rows .45s var(--ease);
}
.loc-acc-item.is-open .loc-acc-panel{ grid-template-rows:1fr; }
/* Split layout: media takes the left half, flush-filled (object-fit:cover,
   no border/letterbox) instead of a bordered contain-box floating in empty
   space - "flush penuh ke spacenya". Content takes the right half.
   aspect-ratio:11/10 on the media is a deliberate, calculated middle ground:
   Barito's actual photo is 1200x900 (4:3 = 1.333). Displaying it at 1.1
   crops (1 - 1.1/1.333) = ~17.5% of the source - inside the requested
   10-25% range, not the near-0% of the previous "contain" attempt or the
   ~44% the original full-bleed 21/9 banner was cropping. */
/* Two levels below .loc-acc-panel on purpose, not one: .loc-acc-panel-inner
   is the grid item that actually gets clipped to the animating 0fr/1fr
   track, with nothing else on it - no padding, no flex layout. Padding and
   layout live one level deeper on .loc-acc-panel-content instead. Collapsing
   both into a single element (tried first) left the closed state stuck at
   ~53px instead of 0 - the vertical padding was still contributing to the
   grid item's own minimum size even under grid-template-rows:0fr, since the
   auto-min-size-to-0 that overflow:hidden gives you only zeroes out content
   contribution, not the box's own padding. */
.loc-acc-panel-inner{ overflow:hidden; }
.loc-acc-panel-content{
  display:flex;
  /* flex-start (not center) so the copy column's top line lines up with the
     top of the photo, instead of floating vertically centered against it -
     still to the photo's right, just aligned to its top edge now. */
  align-items:flex-start;
  gap:28px;
  padding:24px 26px 28px;
  border-top:1px solid var(--clr-line);
}
.loc-acc-media{
  flex:0 0 42%;
  max-width:380px;
  aspect-ratio:11/10;
  border-radius:var(--radius-sm);
  overflow:hidden;
}
.loc-acc-body{
  flex:1;
  min-width:0;
  display:flex;
  flex-direction:column;
  gap:12px;
}
.location-card-cat{
  font-family:var(--font-mono);
  font-size:.68rem;
  text-transform:uppercase;
  letter-spacing:.06em;
  color:var(--clr-gold);
}
.loc-acc-body .li-row{ display:flex; gap:12px; align-items:flex-start; font-size:.86rem; color:var(--clr-ink-soft); }
.loc-acc-body .li-row svg{ width:19px; height:19px; flex:0 0 19px; color:var(--clr-terracotta); margin-top:1px; }

.location-card-media{ overflow:hidden; background:var(--clr-bg-alt); }
.location-card-media img{
  width:100%; height:100%; object-fit:cover;
  filter:sepia(.14) saturate(1.1) contrast(1.05);
}
/* Flores/Tebet have no site photo yet - a labeled dashed placeholder instead of
   an empty gap, sized identically to a real photo (both use .loc-acc-media's
   fixed aspect-ratio) so the row doesn't look broken or mismatched while the
   real photo is pending. */
.location-card-media--placeholder{
  display:flex; align-items:center; justify-content:center;
  text-align:center;
  padding:20px;
}
.location-card-media--placeholder span{
  font-family:var(--font-mono);
  font-size:.72rem;
  letter-spacing:.02em;
  color:var(--clr-ink-soft);
}

.wa-btn{
  background:#25D366; color:#0b3b21;
  padding:13px 22px; border-radius:999px;
  font-weight:700; font-size:.88rem;
  display:inline-flex; align-items:center; gap:8px;
  width:fit-content;
  transition:transform .3s var(--ease);
}
.wa-btn svg{ width:18px; height:18px; flex:0 0 18px; }
.wa-btn:hover{ transform:translateY(-2px); }

/* .cta-band and .site-footer are the same "dark anchor chrome" family as
   the navbar/announce-bar - background:var(--clr-panel-dark) stays fixed
   dark brown in both themes on purpose (see [data-theme="light"] in
   00-base.css), so their text uses the fixed -on-dark ink tokens rather
   than the theme-swapping ones. */
.cta-band{ background:var(--clr-panel-dark); padding:0; }
.cta-band-inner{
  display:grid;
  grid-template-columns:repeat(3, 1fr);
}
.cta-band-item{
  display:flex;
  flex-direction:column;
  gap:6px;
  padding:38px 28px;
  border-right:1px solid color-mix(in srgb, var(--clr-ink-on-dark) 12%, transparent);
  color:var(--clr-ink-on-dark);
  transition:background .3s var(--ease);
}
.cta-band-item:last-child{ border-right:none; }
.cta-band-item:not(.is-disabled):hover{ background:color-mix(in srgb, var(--clr-ink-on-dark) 6%, transparent); }
.cta-band-item.is-disabled{ opacity:.45; cursor:default; }
.cta-band-icon{ font-size:1.3rem; }
.cta-band-label{
  font-family:var(--font-display);
  font-size:1.15rem;
  font-weight:400;
}
.cta-band-desc{ font-size:.82rem; color:var(--clr-ink-soft-on-dark); }

.site-footer{ background:var(--clr-panel-dark); color:var(--clr-ink-on-dark); padding:80px 0 30px; }
.footer-top{
  display:grid;
  grid-template-columns:1.3fr 1fr 1fr 1fr;
  gap:48px;
  padding-bottom:56px;
  border-bottom:1px solid color-mix(in srgb, var(--clr-ink-on-dark) 12%, transparent);
}
.footer-brand img{ height:40px; margin-bottom:16px; filter:brightness(0) invert(1); }
.footer-brand p{ color:var(--clr-ink-soft-on-dark); font-size:.88rem; max-width:32ch; }
.footer-col h5{
  font-family:var(--font-mono);
  font-size:.72rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--clr-ink-soft-on-dark);
  margin-bottom:18px;
}
.footer-col a{ display:block; font-size:.88rem; color:var(--clr-ink-on-dark); margin-bottom:12px; opacity:.9; }
.footer-col a:hover{ opacity:1; text-decoration:underline; text-underline-offset:3px; }
.footer-social{ display:flex; gap:14px; }
.footer-social a{
  width:38px; height:38px; border-radius:50%;
  border:1px solid color-mix(in srgb, var(--clr-ink-on-dark) 20%, transparent);
  display:flex; align-items:center; justify-content:center;
  font-size:.85rem;
}
/* ---------- Floating social + WhatsApp (bottom-right) ---------- */
.floating-social{
  position:fixed;
  right:20px; bottom:20px;
  z-index:900;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:12px;
}
.floating-social-btn{
  width:46px; height:46px;
  border-radius:50%;
  background:var(--clr-surface);
  border:1px solid var(--clr-line);
  display:flex; align-items:center; justify-content:center;
  color:var(--clr-ink-soft);
  box-shadow:0 6px 18px rgba(0,0,0,.35);
  opacity:0;
  pointer-events:none;
  transition:opacity .5s var(--ease), background .25s, color .25s, box-shadow .25s;
}
.floating-social-btn svg{ width:20px; height:20px; }
.floating-social-btn img{ width:22px; height:22px; object-fit:contain; }
.floating-social.is-visible .floating-social-btn{
  opacity:1;
  pointer-events:auto;
  transition-delay:.2s;
}
.floating-social-btn:hover{
  background:var(--clr-terracotta);
  color:#fff;
  box-shadow:0 8px 22px rgba(193,80,42,.4);
}
@media (max-width:640px){
  .floating-social{ right:14px; bottom:14px; gap:10px; }
  .floating-social-btn{ width:40px; height:40px; }
  .floating-social-btn svg{ width:17px; height:17px; }
}
@media (prefers-reduced-motion: reduce){
  .floating-social-btn{ transition:none; }
}
.footer-social a:hover{ background:var(--clr-terracotta); border-color:var(--clr-terracotta); }
.footer-bottom{
  display:flex; justify-content:space-between; align-items:center;
  padding-top:26px; font-size:.8rem; color:var(--clr-ink-soft-on-dark);
  flex-wrap:wrap; gap:12px;
}
/* [data-reveal] initial/animated state is now set entirely by assets/js/animations.js
   (GSAP). Deliberately no CSS opacity:0 here - if the GSAP CDN fails to load, the
   script's fallback sets inline opacity:1, but a CSS rule here would win the
   cascade over nothing and could leave content invisible with no JS to reveal it. */

@media (max-width: 980px){
  .nav-menu, .nav-cta .btn{ display:none; }
  .nav-toggle{ display:flex; }
  /* Account/Shop icons drop off on mobile - both already reachable from the
     hamburger menu's link list. Search stays - it's a global utility that
     shouldn't require opening the menu first. */
  .nav-icon-btn[aria-label="Account"], .nav-icon-btn[aria-label="Shop"]{ display:none; }
  .story-grid{ grid-template-columns:1fr; gap:56px; }
  .journey-cards .journey-card{ flex-basis:200px; }
  .portal-teaser{ grid-template-rows:120px; }
  .journey-grid{ grid-template-columns:repeat(2, 1fr); grid-auto-rows:160px; }
  .gallery-masonry{ column-count:2; }
  .products-grid{ grid-template-columns:1fr; gap:56px; }
  .people-grid{ grid-template-columns:repeat(2,1fr); }
  .footer-top{ grid-template-columns:1fr 1fr; }
  .cta-band-inner{ grid-template-columns:1fr; }
  .cta-band-item{ border-right:none; border-bottom:1px solid color-mix(in srgb, var(--clr-ink-on-dark) 12%, transparent); }
  .cta-band-item:last-child{ border-bottom:none; }
  /* The 38% desktop shrink on the Café Ambiance carousel only makes sense on
     wide screens - on tablet/mobile it'd be a tiny, hard-to-see photo. */
  .ambiance-carousel, .ambiance-caption{ max-width:100%; }
  .loc-acc-head{ padding:18px 20px; }
  .loc-acc-panel-content{ padding:20px 20px 24px; }
}
@media (max-width: 640px){
  /* Visit Us accordion: side-by-side gets cramped under ~640px - stack
     instead, media flush full-width at the same 11/10 crop ratio (just
     wider now, so taller too). */
  .loc-acc-panel-content{ flex-direction:column; align-items:stretch; gap:16px; }
  .loc-acc-media{ flex:none; max-width:none; }
  /* --nav-h trimmed from the desktop 70px - the logo shrinks below too, so
     the un-scrolled bar doesn't need as much height as desktop's larger
     mark. Frees up vertical space for hero content on short phone
     screens, since every fixed-bar pixel eats into #home's padding-top. */
  :root{ --announce-h:52px; --nav-h:58px; }
  .announce-bar p{
    white-space:normal;
    overflow:visible;
    text-overflow:clip;
    line-height:1.3;
    font-size:.7rem;
  }
  /* Logo made smaller on request - was the same 56px as desktop, which
     read oversized on a phone-width navbar and ate space better spent on
     hero content below. */
  .nav-logo img{ height:38px; }
  .navbar.is-scrolled .nav-logo img{ height:32px; }
  .navbar.is-scrolled{ height:56px; }
  .section{ padding:80px 0; }
  .story-bgword{ display:none; }
  .marquee-track{ animation-duration:22s; }
  /* Coffee - Plantation stats: 4 items don't fit 2-up cleanly on a phone
     anymore, so this becomes the same horizontal swipe-row pattern as
     .product-cards-grid--teaser below - native overflow-x + scroll-snap,
     no drag-JS needed (mobile-only, touch swipe is enough). */
  .process-stats{
    display:flex;
    flex-wrap:nowrap;
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    scroll-padding-inline:20px;
    gap:16px;
    margin-bottom:32px;
    padding:4px 4px 8px;
    scrollbar-width:none;
    -ms-overflow-style:none;
  }
  .process-stats::-webkit-scrollbar{ display:none; }
  .process-stats div{
    flex:0 0 auto;
    min-width:150px;
    scroll-snap-align:center;
  }
  .journey-grid{ grid-template-columns:1fr; grid-auto-rows:auto; }
  .gallery-masonry{ column-count:1; }
  .journey-grid .bento-tile{ aspect-ratio:4/3; }
  .journey-grid .bento-tile.is-lg,
  .journey-grid .bento-tile.is-wide,
  .journey-grid .bento-tile.is-tall{ grid-column:span 1; grid-row:span 1; }
  /* is-lg's desktop-only 1.3rem title size (06-activities-journey.css) is
     sized for a tile spanning 2 columns - on mobile it collapses to the same
     footprint as every other tile above, so the oversized font was the most
     likely thing to overflow/wrap awkwardly in a now much narrower tile. */
  .journey-grid .bento-tile.is-lg .bento-tile-info h4{ font-size:1.05rem; }
  .hero{
    /* Still bottom-anchored (not centered - centered read too high/floaty
       when tried before), but lifted further off the very bottom edge than
       the original 40px - the old value crowded the copy right against the
       photo's most heavily-scrimmed strip, covering whatever's happening in
       the lower part of the frame. 132px clears noticeably more of the
       photo while staying visually anchored to the bottom, not centered. */
    align-items: flex-end;
    padding-bottom: 132px;
  }
  .hero .container{
    padding-top: 90px;
    padding-bottom: 0;
  }
  .hero h1{
    /* Still clearly bigger than .hero p's 1rem below - hierarchy holds. */
    font-size: 2.15rem;
    word-wrap: break-word;
  }
  .hero-tag{
    font-size: 0.65rem;
    letter-spacing: 0.1em;
    flex-wrap: wrap;
  }
  /* Homepage hero only (#home beats the plain .hero class rules above,
     which stay untouched for shop/journey/activities' small 40vh banner
     heroes). On mobile the slider photo no longer has text sitting on
     top of it - image up top as a normal block, copy follows below on
     the page's own background instead of a photo scrim, since text over
     a busy slider photo was hard to read on small screens. */
  #home{
    display: block;
    min-height: auto;
    padding-top: calc(var(--announce-h) + var(--nav-h));
    padding-bottom: 0;
  }
  #home .hero-bg{
    position: relative;
    inset: auto;
    /* All 3 hero photos are true portrait (3:4 - checked their actual pixel
       dimensions). The old fixed 28vh box is a ~2:1 landscape shape, so
       object-fit:cover had to crop out most of each photo's height to fill
       it - not "a bit tight", most of the frame was gone. Bumped to 38vh
       (still nowhere near full 46vh/3:4 height, which is what pushed the
       copy below the fold in the first place) and switched cover→contain
       so the full photo is always visible, letterboxed on --clr-bg (the
       same tone as the content block right below it, so the letterbox
       reads as breathing room, not empty bars). */
    height: 38vh;
    background: var(--clr-bg);
  }
  /* #home's hero is the auto-slider (index.php), so its <img> sits one level
     deeper than the plain single-image hero pattern this selector was copied
     from - .hero-bg > .hero-slide > img, not .hero-bg > img directly. The
     direct-child version silently matched nothing here at all (confirmed by
     testing: a .hero-bg > img rule with zero matches doesn't error, it just
     never applies), which is why the fix below needs its own selector. */
  #home .hero-slide img{
    /* !important on both: assets/js/src/07-hero-slider.js (GSAP) also sets
       an inline transform on every slide's <img> for the Ken Burns zoom -
       a plain rule here loses to that inline style regardless of selector
       specificity, only stylesheet !important beats it. Confirmed necessary
       by testing, not defensive copy-paste. */
    object-fit: contain !important;
    /* The Ken Burns zoom-in (transform:scale(1.06) → 1, 02-hero.css +
       GSAP-driven for slides after the first) would crop the photo's edges
       out of frame for most of its run - directly undoes the no-crop fix
       above. Off on mobile only; desktop's cover crop has enough headroom
       that the animation never clips anything real. */
    animation: none !important;
    transform: none !important;
  }
  #home::before{ content: none; }
  #home .container{
    position: static;
    padding: 18px 20px 32px;
    background: var(--clr-bg);
  }
  #home h1{ color: var(--clr-ink); }
  #home p{ color: var(--clr-ink-soft); }
  #home .btn-outline.light{
    color: var(--clr-ink);
    border-color: var(--clr-ink-soft);
  }
  #home .btn-outline.light:hover{
    background: var(--clr-ink);
    color: var(--clr-bg);
  }
  .story-media .story-badge{ left:0; right:0; margin:0 auto; bottom:-24px; }
  .journey-scroll{ padding-bottom:64px; }
  .journey-cards{ gap:12px; }
  .journey-cards .journey-card{ flex-basis:66vw; }
  .journey-cards .journey-card-media{ aspect-ratio:5/8; }
  .journey-card-info{ padding:14px; }
  .journey-card-info h3{ font-size:1.02rem; margin:0 0 6px; }
  .journey-card-desc{ font-size:.78rem; line-height:1.4; }
  /* Touch swipe already works natively via overflow-x:auto - the arrows are a
     desktop-mouse affordance and just add clutter on small screens. */
  .journey-nav{ display:none; }
  /* Coffee - Products: same horizontal swipe-row pattern as .journey-cards
     above, one row instead of the wrapping grid, ending in the "See All
     Products" pill card. Native overflow-x touch swipe is enough here (no
     drag-JS needed - that's a desktop-mouse affordance and this row is
     mobile-only). */
  /* Scoped to .product-cards-grid--teaser (homepage Coffee-Products section
     only) - shop.php reuses the plain .product-cards-grid/.product-card
     classes for its own full-catalog grid and must keep its normal
     1-column mobile stack, not this swipe-row/3-item cap. */
  .product-cards-grid--teaser{
    display:flex;
    flex-wrap:nowrap;
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    scroll-padding-inline:20px;
    gap:16px;
    padding:4px 4px 8px;
    scrollbar-width:none;
    -ms-overflow-style:none;
  }
  .product-cards-grid--teaser::-webkit-scrollbar{ display:none; }
  .product-cards-grid--teaser .product-card{
    flex:0 0 78vw;
    scroll-snap-align:center;
    margin-bottom:0;
  }
  /* Cap the mobile row at 3 real products, no matter how many exist in the
     catalog - same "never grows unbounded" principle as the Portal Card
     teaser (see .portal-teaser above). Position-based, not a PHP slice, so
     desktop's full grid is untouched; :not() keeps the CTA card (always
     last in the DOM) from ever being caught by this rule. */
  .product-cards-grid--teaser .product-card:nth-child(n+4):not(.product-card--cta){
    display:none;
  }
  .product-card--cta{
    display:flex; align-items:center; justify-content:center;
    flex:0 0 55vw;
    background:transparent;
    border-color:transparent;
  }
  .product-cta-desktop{ display:none; }

  /* ---------- shop.php: category rows (Limited Experimental / Arabica
     Natural / Arabica Honey / Arabica Washed / Robusta Natural) ----------
     Desktop stays the plain unified grid (08-products.css). Below this
     breakpoint each category becomes its own block: a heading, then either
     a horizontal scroll-snap row (2+ products - "ada opsinya") or a single
     static card (exactly 1 product, or the "coming soon" placeholder for a
     category with 0). Same native overflow-x + scroll-snap pattern as
     .product-cards-grid--teaser above - deliberately no custom JS drag/
     transform state, so there's nothing that can leave a slide stuck
     mid-swipe, snap backward on release, or jump to a random slide the way
     a hand-rolled slider can. */
  /* Scoped to .shop-catalog-grid (the extra class shop.php's grid carries),
     not the bare .product-cards-grid - that class is shared with the
     homepage teaser row (.product-cards-grid--teaser), and a bare-class rule
     here previously won the cascade over the teaser's own display:flex
     (same specificity, this rule came later in source order), silently
     breaking the homepage's swipeable row back into a 3-column stack. */
  .shop-catalog-grid{ display:block; }
  .shop-jenis-group{ display:block; margin-bottom:32px; }
  .shop-jenis-group:last-child{ margin-bottom:0; }
  .shop-jenis-title{
    display:block;
    font-family:var(--font-display);
    font-size:1.05rem;
    font-weight:400;
    margin:0 0 12px;
    color:var(--clr-ink);
  }
  .shop-jenis-row--single{ display:block; }
  .shop-jenis-row--single .product-card{ margin-bottom:0; }
  .shop-jenis-row--slide{
    display:flex;
    flex-wrap:nowrap;
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    scroll-padding-inline:20px;
    gap:16px;
    padding:4px 4px 8px;
    scrollbar-width:none;
    -ms-overflow-style:none;
  }
  .shop-jenis-row--slide::-webkit-scrollbar{ display:none; }
  .shop-jenis-row--slide .product-card{
    flex:0 0 78vw;
    scroll-snap-align:center;
    margin-bottom:0;
  }
  .product-card--placeholder{
    display:flex;
    align-items:center;
    justify-content:center;
    min-height:130px;
    text-align:center;
    padding:20px;
    border-style:dashed;
    color:var(--clr-ink-soft);
  }
  .product-card--placeholder h4{ margin-bottom:6px; }
  .product-card--placeholder p{ font-size:.85rem; }
  .portal-teaser{ grid-template-columns:1fr; grid-template-rows:repeat(2, 132px); }
  .people-grid{ grid-template-columns:1fr; }
  .location-block{ grid-template-columns:1fr; }
  .location-map iframe{ min-height:260px; }
  .footer-top{ grid-template-columns:1fr; gap:36px; }
  .section-head{ flex-direction:column; align-items:flex-start; }
}
