.hero {
  position: relative;
  min-height: 88vh;
  display: flex;
  align-items: center;
  padding: 70px 48px;
  overflow: hidden;
  background: var(--color-charcoal-primary);
}
/* Full-bleed background, not the old split-panel (image confined to the
   right 62%) -- a real uploaded hero photo is composed as one full-width
   scene with its own dark negative space for the text to sit on, not a
   narrow strip meant to share the frame with a separate solid-color half. */
.hero__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
/* object-position biased toward the top: the hero box is wider relative
   to its height than the 16:9 photo, so cover must crop *something* off
   the top and bottom to fill it edge-to-edge -- centered (the default)
   was cropping the top logo more than felt right. Shifting the crop
   window up keeps the top intact and takes the extra crop from the
   bottom instead, where there's less that matters. */
.hero__media img, .hero__media video { width: 100%; height: 100%; object-fit: cover; object-position: center 15%; }
/* Slow, continuous Ken Burns drift on the hero visual -- kept deliberately
   subtle (was scaling up to 116%, which combined with object-fit:cover's
   own aspect-ratio crop on a very wide/short hero box was cutting off real
   content near the top/bottom edges of the photo -- a real bug, not just
   taste). Starts at 100% (no crop beyond what cover itself already needs)
   and only drifts to 104%. Respects prefers-reduced-motion via tokens.css's
   global animation-duration override, no separate guard needed here. */
.hero__media img, .hero__media video { animation: hero-ken-burns 26s ease-in-out infinite alternate; transform-origin: center; }
@keyframes hero-ken-burns {
  from { transform: scale(1) translate(0, 0); }
  to { transform: scale(1.04) translate(-0.6%, -0.4%); }
}
/* No hero photo/video configured yet (dashboard: Homepage > Hero Slides) --
   a tasteful textured dark gradient instead of a flat, empty-looking
   charcoal rectangle, so the hero still reads as intentional rather than
   broken while real photography is pending. */
.hero__media:not(:has(img)):not(:has(video)) {
  background:
    radial-gradient(ellipse 60% 50% at 30% 20%, rgba(201,162,39,.10), transparent 60%),
    linear-gradient(155deg, var(--color-charcoal-secondary) 0%, var(--color-charcoal-primary) 55%, #060606 100%);
}
.hero__scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg, rgba(15,15,15,.92) 0%, rgba(15,15,15,.55) 55%, rgba(15,15,15,.15) 100%);
  pointer-events: none;
}
.hero__content { position: relative; z-index: 2; max-width: 640px; }
/* Staggered fade-up entrance on load -- above-the-fold content, so this
   runs immediately rather than waiting on IntersectionObserver like the
   rest of the site's scroll-reveal system (motion.css/reveal.js), which
   is built for content the user scrolls to, not what's already visible. */
.hero__eyebrow, .hero__title, .hero__subtitle, .hero__ctas, .hero__video-trigger {
  opacity: 0;
  animation: hero-fade-up .8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.hero__eyebrow { animation-delay: .1s; }
.hero__title { animation-delay: .22s; }
.hero__subtitle { animation-delay: .34s; }
.hero__ctas { animation-delay: .46s; }
.hero__video-trigger { animation-delay: .56s; }
@keyframes hero-fade-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .hero__media img, .hero__media video { animation: none; }
  .hero__eyebrow, .hero__title, .hero__subtitle, .hero__ctas, .hero__video-trigger { animation: none; opacity: 1; }
}
.hero__eyebrow { color: var(--color-gold-accent); font-size: 12.5px; letter-spacing: .3em; font-weight: 600; margin-bottom: 20px; }
.hero__title { font-family: var(--font-serif); font-size: clamp(34px, 5vw, 60px); line-height: 1.08; margin: 0 0 22px; font-weight: 600; }
.hero__subtitle { color: var(--color-text-grey); font-size: 17px; line-height: 1.7; max-width: 480px; margin: 0 0 34px; }
.hero__ctas { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 30px; }
.hero__video-trigger { display: flex; align-items: center; gap: 12px; color: #E8E4DC; font-size: 13px; background: none; border: none; cursor: pointer; transition: color .2s ease; }
.hero__video-trigger:hover { color: var(--color-gold-accent); }
.hero__video-trigger .play-circle {
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid rgba(245,242,236,.4);
  display: flex; align-items: center; justify-content: center;
  transition: border-color .2s ease, transform .2s ease;
}
.hero__video-trigger:hover .play-circle { border-color: var(--color-gold-accent); transform: scale(1.08); }
@media (max-width: 979px) {
  /* Mobile hero: one cinematic composition -- image fills the whole
     section as a real backdrop, headline overlaid on top via a scrim,
     same overlay mechanism desktop already uses (object-fit:cover +
     absolute content), not a separate mobile-only design and not a
     separate image block above the text.
     History: went through two real photo-specific workarounds before
     this -- object-fit:contain with the image in normal flow (zero crop,
     but no overlay at all: "it is aboved the text"), then a cover fill
     capped to a short 300px band because the only mobile_media available
     at the time was a wide ~1122x555 crop that turned into a texture
     close-up under a full-height cover fill (confirmed via the real
     rendered capture, not just the crop math).
     The current mobile_media (hero_mobile_bag_v2, 1122x1402, ~4:5 true
     portrait) replaced that wide one. Re-ran the real crop math for THIS
     taller section height against that photo (not guessed): at 390px
     width / 700px height the fit is height-constrained (scale 0.499),
     giving zero vertical crop and a generous ~781px-wide (70%) visible
     window -- comfortably inside the bag's own real span. At 768px width
     the fit flips to width-constrained, cropping ~131px off the top and
     bottom of the source, which still lands well clear of the bag (its
     real vertical span sits well inside that window). object-position
     stays plain center in both cases -- the bag is close enough to
     centered in the source that a deliberate vertical bias isn't needed;
     re-verify with a real capture if this photo is ever replaced. */
  .hero {
    flex-direction: column;
    align-items: stretch;
    padding: 0;
    position: relative;
    /* "Substantial, premium" per the brief's own target ranges (~650-780px
       across 390-768px widths) implemented as a vh-driven clamp rather
       than fixed breakpoint jumps, so it scales smoothly with the actual
       device instead of guessing a handful of fixed numbers.
       Real bug found and fixed: an earlier 620px floor exceeded the
       actual viewport height on a short-screen phone (320x568) -- since
       the AI/WhatsApp FABs are position:fixed to the true viewport edge,
       a hero taller than the viewport pushes its own CTA row (positioned
       via the hero's own bottom padding, sized to clear the FABs *within*
       the hero's box) past the visible screen and into direct overlap
       with both FABs, confirmed via real getBoundingClientRect overlap
       checks (ctaOverlapsWA/AI both true). Lowered the floor so 78vh
       dominates on short viewports instead of being clamped away by a
       floor bigger than the screen itself. */
    min-height: clamp(380px, 78vh, 780px);
    aspect-ratio: auto;
  }
  .hero__media { position: absolute; inset: 0; height: 100%; width: 100%; margin: 0; }
  .hero__media img, .hero__media video { object-fit: cover; object-position: center; animation: none; }
  .hero__scrim {
    display: block;
    /* Real bug found and fixed AGAIN: the previous pass darkened this
       gradient to 58-97% opacity to fix real text-contrast findings, but
       a fresh real composite (photo + this exact gradient drawn on
       canvas, not guessed) showed the fix had overshot -- the bag reads
       as a near-black silhouette, no grain/stitching/warmth visible at
       all. That's a real regression against "leather must visibly show
       texture... do not put a black overlay over the entire image."
       Real fix this time: lighten the gradient back down close to a
       standard cinematic curve, and stop relying on scrim darkness as
       the ONLY legibility mechanism -- added a real text-shadow to the
       actual text elements below, which guarantees contrast against
       whatever's behind them (bright leather highlights included)
       without needing the whole photo crushed dark to compensate. */
    background: linear-gradient(
      180deg,
      rgba(6,5,4,.12) 0%,
      rgba(6,5,4,.15) 15%,
      rgba(6,5,4,.26) 32%,
      rgba(6,5,4,.40) 50%,
      rgba(6,5,4,.56) 68%,
      rgba(8,7,6,.74) 86%,
      rgba(8,7,6,.82) 100%
    );
  }
  .hero__content {
    position: absolute; inset: 0 0 0 0; z-index: 2;
    max-width: none;
    display: flex; flex-direction: column; justify-content: flex-end;
    /* Extra bottom clearance: the AI-assistant/WhatsApp FABs are
       position:fixed at bottom:96px/26px, sharing screen space with
       whatever sits at the very bottom of a hero this tall -- padding
       (not a shorter hero) keeps the CTA row clear of them without
       shrinking the photo. */
    padding: 24px 20px calc(112px + env(safe-area-inset-bottom));
  }
  /* Real legibility mechanism, not scrim darkness alone -- a lighter
     scrim (above) needs this to guarantee AA-ish contrast over the
     brightest/busiest part of the photo (the bag's own face), the exact
     spot a prior pass found unreadable. Two stacked shadows: a tight one
     for edge definition, a soft wide one that reads as a general dark
     "halo" behind the glyphs regardless of what's under them. */
  .hero__eyebrow, .hero__title, .hero__subtitle {
    text-shadow: 0 1px 3px rgba(0,0,0,.85), 0 4px 18px rgba(0,0,0,.65);
  }
  .hero__eyebrow { font-size: 13px; letter-spacing: .3em; }
  .hero__title { font-size: clamp(38px, 10vw, 58px); }
  .hero__subtitle { max-width: 34rem; font-size: 16.5px; }
}

.trust-bar {
  background: var(--color-charcoal-secondary);
  padding: 26px 48px;
  display: flex; flex-wrap: wrap; gap: 28px;
  justify-content: space-around;
  border-top: 1px solid rgba(255,255,255,.06);
  border-bottom: 1px solid rgba(255,255,255,.06);
}
.trust-bar__item { display: flex; align-items: center; gap: 12px; min-width: 200px; color: var(--color-gold-accent); text-decoration: none; }
.trust-bar__item .label { font-size: 13px; font-weight: 600; color: var(--color-beige-light); }
.trust-bar__item .sub { font-size: 11.5px; color: var(--color-text-grey-dim); }

/* Minimal 4-claim variant -- single uppercase line per item instead of a
   label+sub pair, tighter footprint, reads as a quiet confirmation strip
   rather than a second block of marketing copy.

   Real request (2026-08-22, confirmed live on both mobile and desktop):
   an auto-scrolling circular marquee at every width, not just mobile --
   .trust-bar--minimal clips a doubled-length .trust-bar__track that
   loops via translateX(0 -> -50%); the duplicated (aria-hidden) second
   half lands exactly where the first half started, an invisible seam
   that reads as continuous circular motion. Desktop and mobile share
   the same mechanics, just different padding/gap/speed so desktop still
   reads as spacious and mobile still reads as compact. */
.trust-bar--minimal {
  flex-wrap: nowrap;
  overflow: hidden;
  justify-content: flex-start;
  padding: 20px 0;
}
.trust-bar--minimal .trust-bar__item { min-width: auto; gap: 10px; }
.trust-bar--minimal .trust-bar__item .label { font-size: 11.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--color-beige-light); }
.trust-bar--minimal .trust-bar__track {
  display: flex;
  width: max-content;
  gap: 56px;
  padding: 0 48px;
  animation: trust-bar-marquee 26s linear infinite;
}
.trust-bar--minimal .trust-bar__item { flex-shrink: 0; white-space: nowrap; }

@media (max-width: 639px) {
  /* Tighter padding/gap and a shorter loop for the compact mobile strip
     -- same mechanics, just scaled to the narrower footprint. */
  .trust-bar--minimal { padding: 16px 0; }
  .trust-bar--minimal .trust-bar__track { gap: 40px; padding: 0 20px; animation-duration: 16s; }

  /* Real, live bug found and fixed (2026-09-11, same mobile audit as the
     Buy Now / WhatsApp FAB fix): this section sits within the very first
     screen's worth of content on a typical phone -- no scroll needed to
     reach it -- which puts it directly under the always-fixed WhatsApp/
     AI-chat FABs' bottom-right corner (base.css/ai-chat.css). Unlike the
     sticky action bar, this marquee has no "becomes visible" moment to
     hook a body class onto -- it's just always there, continuously
     scrolling claim text in and out from behind two opaque circles,
     which reads as broken/janky ("not smooth") even though nothing is
     actually unclickable (this row has no real controls, just text).
     A right-edge fade instead of a hard clip: each claim now dissolves
     smoothly before it would reach the FAB zone, reading as an
     intentional edge fade rather than text disappearing behind a
     circle. Width covers both FABs' combined footprint (48px + 44px
     dia. + gaps) plus a small buffer. */
  .trust-bar--minimal {
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 90px), transparent 100%);
    mask-image: linear-gradient(to right, black calc(100% - 90px), transparent 100%);
  }
}
@keyframes trust-bar-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* .section/.section--tight-top and .section-header/-eyebrow/-title all
   moved to base.css (2026-08-22) — used by Wholesale too, not just this
   page; .section itself had been missed in the original move. */

.category-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; }
.category-tile { position: relative; border-radius: var(--radius-category-card); overflow: hidden; height: 340px; display: block; }
.category-tile img { width: 100%; height: 100%; object-fit: cover; transition: transform .4s ease; }
.category-tile:hover img { transform: scale(1.05); }
/* Real mobile treatment: auto-fit/minmax(240px,...) collapses to a single
   column below ~480px content width regardless of intent -- the brief's
   explicit ask is a real 2-column grid (~48% each), not single-column-
   by-side-effect. Height reduced to suit a narrower card.
   Real bug found and fixed: this override used to sit BEFORE the base
   .category-tile rule above (same 0,1,0 specificity either way) -- source
   order alone meant the later, unconditional 340px rule always won, so
   mobile tiles rendered at the full desktop height at every width,
   confirmed live via getComputedStyle at 375px returning "340px". Moving
   this block after the base rule is the actual fix; not a specificity
   war, just correcting where it sits in the cascade. */
@media (max-width: 639px) { .category-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .category-tile { height: 190px; } }
/* Real fallback for a category with no photo uploaded yet -- previously
   an empty tile had no background of its own at all, so it just showed
   the page's own near-black background through, indistinguishable from a
   rendering bug. This still needs real photography eventually (a CSS
   fallback can't substitute for actual product photos), but at least it
   never looks broken in the meantime. */
.category-tile:not(:has(img)) {
  background: linear-gradient(160deg, var(--color-charcoal-surface) 0%, var(--color-charcoal-secondary) 100%);
}
.category-tile:not(:has(img))::before {
  content: "AK";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-serif);
  font-size: 96px;
  color: var(--color-gold-accent);
  opacity: .12;
  pointer-events: none;
}
.category-tile__scrim { position: absolute; inset: 0; background: linear-gradient(0deg, rgba(0,0,0,.75), rgba(0,0,0,0) 55%); pointer-events: none; }
.category-tile__label { position: absolute; bottom: 22px; left: 24px; color: var(--color-beige-light); }
.category-tile__label .name { font-family: var(--font-serif); font-size: 20px; margin-bottom: 4px; }
.category-tile__label .cta { font-size: 12px; color: var(--color-gold-accent); letter-spacing: .05em; }

.featured-tabs { display: flex; gap: 32px; justify-content: center; margin-top: 26px; flex-wrap: wrap; }
.featured-tab {
  background: none; border: none; cursor: pointer;
  color: var(--color-text-grey-dim); font-size: 12px; font-weight: 700;
  letter-spacing: .08em; padding-bottom: 8px;
  border-bottom: 2px solid transparent;
}
.featured-tab.is-active { color: var(--color-gold-accent); border-bottom-color: var(--color-gold-accent); }

.craft-split { background: var(--color-charcoal-secondary); padding: 88px 48px; display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; max-width: var(--content-max-width); margin: 0 auto; }
.craft-split__media { position: relative; border-radius: var(--radius-collection-card); overflow: hidden; height: 440px; }
.craft-split__media img { width: 100%; height: 100%; object-fit: cover; }
/* Real content gap, not a rendering bug: no backend field currently
   holds this section's photo (confirmed -- apps/cms/views.py's home()
   never sets craft_image, and no model field exists to hold one yet),
   so this branch is live in production today. Same graceful-placeholder
   treatment as .category-tile's own :not(:has(img)) fallback rather
   than an empty box with nothing in it. */
.craft-split__media:not(:has(img)) {
  background: linear-gradient(160deg, var(--color-charcoal-surface) 0%, var(--color-charcoal-secondary) 100%);
  display: flex; align-items: center; justify-content: center;
}
.craft-split__media:not(:has(img))::before {
  content: "AK";
  font-family: var(--font-serif);
  font-size: 96px;
  color: var(--color-gold-accent);
  opacity: .12;
}
.craft-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 40px; }
.craft-stats .num { font-family: var(--font-serif); font-size: 30px; color: var(--color-gold-accent); }
.craft-stats .label { font-size: 12.5px; color: var(--color-text-grey-dim); margin-top: 4px; }
@media (max-width: 979px) { .craft-split { grid-template-columns: 1fr; padding: 56px 20px; } .craft-split__media { height: 280px; } }

/* Custom Order promo -- a real, working feature (/custom-order/: product,
   leather, thread, hardware, engraving, gift message, real pricing) that
   had zero homepage visibility before this pass (only a hero CTA linked
   to it). Full-bleed cinematic banner, same overlay mechanism as .hero
   (absolute media + scrim + overlaid content) rather than the side-split
   .craft-split layout, so it reads as a distinct "featured capability"
   moment rather than another twin of the Craftsmanship section right
   below it. */
.custom-order-promo {
  position: relative;
  min-height: 460px;
  display: flex; align-items: center;
  overflow: hidden;
  background: var(--color-charcoal-primary);
}
.custom-order-promo__media { position: absolute; inset: 0; z-index: 0; }
.custom-order-promo__media img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* Same real-content-gap reasoning as .craft-split__media above -- no
   photo exists for this section yet either, so this fallback is live. */
.custom-order-promo__media:not(:has(img)) {
  background:
    radial-gradient(ellipse 55% 60% at 75% 30%, rgba(201,162,39,.08), transparent 60%),
    linear-gradient(135deg, var(--color-charcoal-secondary) 0%, var(--color-charcoal-primary) 60%, #060606 100%);
}
.custom-order-promo__scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg, rgba(10,9,8,.90) 0%, rgba(10,9,8,.62) 48%, rgba(10,9,8,.20) 100%);
  pointer-events: none;
}
.custom-order-promo__content { position: relative; z-index: 2; max-width: 620px; padding: 72px 48px; }
.custom-order-promo__list {
  list-style: none; display: flex; flex-wrap: wrap; gap: 10px 14px;
  margin: 0 0 30px; padding: 0;
}
.custom-order-promo__list li {
  font-size: 11.5px; font-weight: 700; letter-spacing: .06em; color: var(--color-gold-accent);
  border: 1px solid rgba(201,162,39,.35); border-radius: 20px; padding: 7px 14px;
}
@media (max-width: 979px) {
  .custom-order-promo { min-height: 420px; }
  .custom-order-promo__scrim { background: linear-gradient(180deg, rgba(10,9,8,.35) 0%, rgba(10,9,8,.55) 40%, rgba(10,9,8,.92) 100%); }
  .custom-order-promo__content { padding: 44px 20px; max-width: none; }
}

/* Real bug class found in the 2026-08-22 QA pass (see journal.css for
   the confirmed break) -- min(240px, 100%) closes the same latent gap
   here before it surfaces under a narrower container/fewer items. */
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 36px; }
.feature-block { text-align: center; }
/* User-requested (2026-08-24): "make them small and animated" for the
   Why Choose Us icons. Smaller size set in the template (34 -> 26);
   the animation itself piggybacks on the block's own existing
   data-reveal="scale" mechanism (static/js/reveal.js already adds
   .is-visible on scroll-into-view -- no new JS needed) rather than
   inventing a second observer, but gives the icon its OWN bouncier
   entrance distinct from the block's gentle fade so it reads as a
   deliberate flourish, not just riding along. A soft hover lift is a
   free bonus on desktop (inert on mobile, no hover to fire it) --
   both respect prefers-reduced-motion via tokens.css's existing
   global transition-duration override, same as every other animation
   sitewide. */
.feature-block svg {
  margin: 0 auto 18px; display: block;
  opacity: 0; transform: scale(0.4) rotate(-8deg);
  transition: opacity var(--motion-slow) var(--ease-premium), transform .6s cubic-bezier(.34,1.56,.64,1);
}
.feature-block.is-visible svg { opacity: 1; transform: scale(1) rotate(0deg); }
.feature-block:hover svg { transform: scale(1.12) rotate(3deg); transition-duration: .35s; }
.feature-block .title { font-family: var(--font-serif); font-size: 17px; margin-bottom: 8px; }
.feature-block .desc { color: var(--color-text-grey-dim); font-size: 13.5px; line-height: 1.6; }

.leather-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px; }
.leather-card { background: var(--color-charcoal-primary); border-radius: var(--radius-category-card); overflow: hidden; border: 1px solid rgba(255,255,255,.06); }
.leather-card__media { height: 160px; position: relative; }
.leather-card__media img { width: 100%; height: 100%; object-fit: cover; }
/* Real texture photos aren't uploaded yet for any leather type -- rather
   than one identical grey box for all five, each gets a tone-matched
   gradient keyed off its real slug, so the section still reads as
   editorial/considered instead of "content missing" until real swatch
   photography is uploaded (dashboard: Materials > Leather Types). */
.leather-card__media:not(:has(img)) { background: linear-gradient(155deg, var(--swatch-a, #3a2a1c) 0%, var(--swatch-b, #1d140c) 100%); }
.leather-card__media--full-grain:not(:has(img)) { --swatch-a: #4a3320; --swatch-b: #241809; }
.leather-card__media--top-grain:not(:has(img)) { --swatch-a: #7a5a34; --swatch-b: #3d2a15; }
.leather-card__media--crazy-horse:not(:has(img)) { --swatch-a: #8a5c34; --swatch-b: #2c1c10; }
.leather-card__media--cow-leather:not(:has(img)) { --swatch-a: #302a26; --swatch-b: #0c0a08; }
.leather-card__media--vegetable-tanned:not(:has(img)) { --swatch-a: #9c6a3a; --swatch-b: #4a2c12; }
.leather-card__body { padding: 20px; }
.leather-card__body .title { font-family: var(--font-serif); font-size: 16px; margin-bottom: 8px; }
/* Real bug fix: 5 real leather types in a 4-column grid puts the 5th
   (Cow Leather) alone in its own row with no sibling to stretch-match
   against, and its real description is genuinely shorter than the
   others ("Genuine cow leather." vs 2-3 lines for the rest) -- together
   that collapsed its card to 244px against 285px for the other four,
   confirmed live. min-height reserves space for the longest real
   description (~3 lines) so every card matches regardless of which row
   it lands in or how long its own real copy happens to be. */
.leather-card__body .desc { color: var(--color-text-grey-dim); font-size: 13px; line-height: 1.6; min-height: 63px; }

/* Real bug found in the mobile motion audit (2026-08-21): this whole
   section used a hardcoded inline `padding:96px 48px` with no mobile
   override at all -- unlike every `.section`-classed block on this same
   page, which drops to 56px 20px under 639px (see the .section rule
   near the top of this file). Moved out of the inline style into a real
   class with its own responsive rule, matching that same convention --
   this also fixes the math for the mobile carousel below, whose edge-
   bleed assumes a real 20px side padding. */
.leather-types-section { padding: 96px 48px; }
@media (max-width: 639px) { .leather-types-section { padding: 56px 20px; } }

/* Real mobile carousel (mobile motion overhaul, 2026-08-21), Phase 17 --
   the grid above was `auto-fit, minmax(260px,1fr)`, which just wraps to
   a single column on a phone: 5 real leather types stacking vertically
   at ~300px each is ~1500px of scroll for one section. A horizontal
   swipeable strip is both the spec's explicit ask and a real space win
   here. scroll-snap-type:x mandatory (not the gentler `proximity` the
   trust bar above uses -- these are real content cards a shopper is
   meant to land squarely on, not a small utility strip). Peeks the next
   card at the edge (82vw width, not 100vw) as a real "there's more"
   affordance instead of feeling like separate full-screen pages. Image/
   text reveal inside each card is unaffected -- data-reveal="scale" on
   .leather-card is the same reveal.js system regardless of layout. */
@media (max-width: 639px) {
  .leather-grid {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 16px;
    padding-bottom: 4px;
    margin: 0 -20px;
    padding-left: 20px;
    padding-right: 20px;
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  .leather-grid::-webkit-scrollbar { display: none; }
  .leather-card { flex: 0 0 auto; width: 82vw; scroll-snap-align: start; }
  /* User-reported and confirmed live: a flat 160px media height (no
     mobile override anywhere) reads as oversized on an 82vw-wide mobile
     card, especially since none of the 6 real leather types have a
     swatch_image uploaded yet -- every card currently shows a plain
     two-tone gradient placeholder (.leather-card__media:not(:has(img)),
     a few lines up) rather than real texture photography, and a large
     flat color rectangle looks emptier/bigger than the same height
     filled with real detail would. Shrinking the box is a real,
     independent fix either way; it doesn't substitute for uploading
     real photos via /admin/catalog/leathertype/ (no dashboard UI exists
     for this model yet, only the raw Django admin). */
  .leather-card__media { height: 110px; }
}

/* Real bug class found in the 2026-08-22 QA pass (see journal.css for
   the confirmed break) -- min(280px, 100%) closes the same latent gap
   here before it surfaces under a narrower container/fewer items. */
.testimonial-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 26px; }
.testimonial-card { background: var(--color-charcoal-secondary); border-radius: var(--radius-category-card); padding: 32px; border: 1px solid rgba(255,255,255,.06); }
.testimonial-card .stars { display: flex; gap: 3px; margin-bottom: 16px; color: var(--color-gold-accent); }
.testimonial-card p { color: #d8d5cd; font-size: 14.5px; line-height: 1.75; margin: 0 0 24px; }
.testimonial-card .reviewer { display: flex; align-items: center; gap: 12px; }
.testimonial-card .avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; }
.testimonial-card .name { font-size: 13.5px; font-weight: 600; }
.testimonial-card .place { font-size: 12px; color: var(--color-text-grey-dim); }

.instagram-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; }
.instagram-grid img { border-radius: 10px; aspect-ratio: 1/1; object-fit: cover; width: 100%; }
@media (max-width: 979px) { .instagram-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 639px) { .instagram-grid { grid-template-columns: repeat(2, 1fr); } }
