/* ==========================================================
   fuse-search.css — Gator Tobacco Asymmetrical Search Layout
   Design: 12-col grid, sidebar left 3col, results right 9col
   Tokens: inherits from base.css design system
   ========================================================== */

/* ── 1. PAGE SHELL ────────────────────────────────────── */
.search-page-shell {
  display: grid;
  /* One column at every width. The facet panel is off-canvas and mounted
     on <body>, so there is no sidebar track for it to occupy and the
     results list gets the shell's full width — which is what the landing
     already got from body.gs-browsing before this became unconditional. */
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "search-header"
    "results";
  gap: 0;
  min-height: calc(100vh - var(--header-height, 72px));
  position: relative;
  z-index: 30;

  /* Scoped design tokens for this redesign — deliberately NOT merged into
     base.css's site-wide --color-primary family, so nothing outside this
     component is affected. */
  --gt-gold: #D4A017;
  --gt-ink: #F1EBD9;
  --gt-muted: #A89A85;
}

/* Faint Spanish-cedar grain over the shell — inline SVG noise, no external
   asset. pointer-events:none so it can sit above content without ever
   blocking clicks, regardless of the children's own z-index stack. */
.search-page-shell::before {
  content: '';
  /* Fixed + viewport-height, NOT absolute+inset:0 — the shell's scrollable
     height spans the full 200+ product catalog (tens of thousands of px).
     A mix-blend-mode layer stretched (and re-rasterized on every paint)
     across that whole height is a genuine compositing-cost cliff; capping
     it to one viewport keeps the blend cheap regardless of catalog size.
     Same technique .selection-fixed-bg/.selection-vignette already use. */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
  /* Low on purpose: .gt-product-card and .gs-search-header paint their own
     opaque backgrounds over this, so the grain only shows in the negative
     space around cards, never muddying card text or the sticky search
     chrome. pointer-events:none keeps it inert either way. */
  z-index: 1;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* The 240px-at-1024 and single-column-at-768 overrides that used to sit
   here described the sidebar track. The shell has one column at every
   width now, so both are gone. */

/* ── 2. SECTION WRAPPER ───────────────────────────────── */
.search-section {
  padding-top: 6rem;
  padding-bottom: 4rem;
  width: 100%;
}

@media (max-width: 512px) {
  .search-section {
    padding-top: 3.625rem;
  }
}

/* ── 3. SIDEBAR ───────────────────────────────────────── */
/* ── The facet panel ──────────────────────────────────────
   Off-canvas at every width and in every narrowed state, opened by the
   Refine button and by nothing else. This rule used to describe the
   desktop sidebar — grid-area:sidebar, position:sticky, width:100% of a
   280px/240px track — and the off-canvas treatment was layered on top of
   it by a body.gs-browsing block two thousand lines below. There is no
   sidebar any more, so the off-canvas shape is the base shape, and the
   phone block below overrides it into a bottom sheet.

   top is 4rem, not var(--header-height): that token is referenced four
   times in this file and defined nowhere, so it has always resolved to its
   72px fallback against a 64px header. The panel therefore opened 8px low
   and .gt-breadcrumb (bottom edge 131px) sat across its whole header row.
   .gs-search-header already writes the literal 4rem two hundred lines down
   for exactly this reason. The other references are left alone rather than
   chased through a shell min-height this change does not touch — but the
   token is phantom, and a fifth reference to it would be too.

   Mounted on <body> by mountFacetDrawer(), which this depends on: inside
   .search-page-shell, position:fixed resolved against #overscroll-canvas
   rather than the viewport, and z-index:9999 was clamped under the
   breadcrumb by the shell's own stacking context. See that function. */
#mobile-filter-drawer {
  position: fixed;
  top: 4rem;
  left: 0;
  bottom: 0;
  height: auto;
  width: min(340px, 86vw);
  max-width: 340px;
  background: var(--color-surface-container-low);
  border-right: 1px solid var(--color-outline-variant);
  box-shadow: 4px 0 40px rgba(0, 0, 0, 0.55);
  overflow-y: auto;
  overflow-x: hidden;
  padding: 1.5rem 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  z-index: 9999;
  scrollbar-width: thin;
  scrollbar-color: var(--color-outline-variant) transparent;

  /* 101%, not 100%: the panel carries a 1px right border and a large
     box-shadow, and at exactly 100% the border's own pixel can survive at
     the screen edge as a hairline on a fractional-DPR display. */
  transform: translateX(-101%);
  transition: transform 0.35s cubic-bezier(0.25, 1, 0.5, 1);
}

body.mobile-drawer-active #mobile-filter-drawer {
  transform: translateX(0);
}

#mobile-filter-drawer::-webkit-scrollbar {
  width: 4px;
}
#mobile-filter-drawer::-webkit-scrollbar-thumb {
  background: var(--color-outline-variant);
  border-radius: 4px;
}

/* Sidebar header row */
.gs-sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 1.25rem 1rem;
  border-bottom: 1px solid var(--color-outline-variant);
  margin-bottom: 0.5rem;
}

.gs-sidebar-title {
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

#gs-reset-all {
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-primary);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.25rem 0;
  opacity: 0.75;
  transition: opacity 0.2s;
}
#gs-reset-all:hover {
  opacity: 1;
}

/* Mobile sidebar — off-canvas drawer */
@media (max-width: 768px) {
  /* Core Overlay Framework Container - Keep Locked and Rigid */
  #mobile-filter-drawer {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    /* The base rule is now the desktop off-canvas PANEL, which caps itself
       at 340px. The sheet is full-bleed, and width:100% alone does not undo
       a max-width — without this the sheet opened 340px wide on a 375px
       phone, with 35px of page showing down its right edge. */
    max-width: none;
    height: 85vh; /* Maintain design specification layout */
    background: var(--bg-charcoal, #141210);
    z-index: 9999;
    display: flex;
    flex-direction: column; /* Split header section from options scroll view */
    transform: translateY(100%);
    transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
    
    /* CRITICAL: Ensure touch interactions pass directly through to child elements */
    pointer-events: auto; 
    border-right: none;
    box-shadow: 0 -4px 40px rgba(0, 0, 0, 0.6);
    padding-top: 0;
    top: auto; /* Fix: Override desktop top styling to allow off-screen translate */
  }

  body.mobile-drawer-active #mobile-filter-drawer {
    transform: translateY(0);
  }

  /* Scroll Window Body - Target this for scrolling content execution */
  .drawer-body {
    flex-grow: 1;
    /* CRITICAL: Force vertical scrolling properties on the option stack */
    overflow-y: auto !important; 
    overflow-x: hidden;
    padding: 1.5rem 1rem 4rem 1rem; /* Extra bottom padding to clear mobile safe area bars */
    
    /* Enable native iOS kinetic inertial touch acceleration mechanics */
    -webkit-overflow-scrolling: touch; 
  }

  /* Material's full-screen-dialog header order: the dismiss icon leads, the
     destructive action trails. Both used to sit at the right edge — "CLEAR
     ALL" ending 12px shy of the × — so the pair read as one control and a
     visitor reaching for "close the sheet" could just as easily wipe every
     active facet. Opposite ends of the sheet is roughly 300px of separation
     on a phone, and the outlined pill below makes "Clear all" declare itself
     a button rather than a caption hanging off the ×. */
  .gs-sidebar-header {
    min-height: 4.25rem;
    padding: 0 1.25rem 0 4.25rem; /* Clear absolute close button (now left) */
  }

  #gs-reset-all {
    min-height: 2.5rem; /* matches the close button, so they read as a pair */
    padding: 0 0.875rem;
    border: 1px solid var(--color-outline);
    border-radius: 100px;
    opacity: 1;
    color: var(--color-primary);
  }
  #gs-reset-all:active {
    background: var(--color-surface-container-high);
  }
}

/* Body scroll lock when sidebar open */
body.mobile-drawer-active {
  /* Freeze the background canvas layout safely under the active overlay sheet */
  overflow: hidden !important;
  position: fixed;
  width: 100%;
}

/* ── 4. SIDEBAR OVERLAY ───────────────────────────────── */
#gs-sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.65);
  z-index: 190;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

@media (max-width: 768px) {
  #gs-sidebar-overlay {
    display: block;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s;
  }
  #gs-sidebar-overlay.gs-overlay--visible {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Sidebar close button (mobile) */
#gs-sidebar-close {
  display: none;
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: var(--color-surface-container);
  border: 1px solid var(--color-outline);
  border-radius: 50%;
  width: 2.25rem;
  height: 2.25rem;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--color-on-surface-variant);
  transition: background 0.2s, color 0.2s;
  z-index: 10;
}
#gs-sidebar-close:hover {
  background: var(--color-surface-container-high);
  color: var(--color-on-surface);
}

@media (max-width: 768px) {
  #gs-sidebar-close {
    display: flex;
    /* Leading, not trailing — see the .gs-sidebar-header note above. Declared
       down here rather than beside that note because the base rule above is
       an equal-specificity #id and comes later in the file, so an override
       written up there silently loses width/height/top to it. */
    left: 1rem;
    right: auto;
    top: 0.875rem;
    width: 2.5rem;  /* 40px — the sheet's primary dismiss, sized to be hit */
    height: 2.5rem;
  }
}

/* The panel is off-canvas at EVERY width now, so the dismiss control and
   the scrim are no longer phone-only. Above 769px the close button used to
   be revealed by a body.gs-browsing rule while .gs-sidebar-header kept its
   base padding, which reserves nothing for it: the 36px circle landed at
   right:1rem (287-323px) directly on top of "Clear all" (254-319px). The
   two controls were drawn one over the other, which is the mangling.

   Fixed the way the phone sheet already had been: the dismiss leads, the
   destructive action trails, and the header row pads itself clear of the
   absolute box. Same reasoning, same numbers, now at one width instead of
   two. */
@media (min-width: 769px) {
  #gs-sidebar-close {
    display: flex;
    left: 1rem;
    right: auto;
    top: 0.875rem;
  }

  .gs-sidebar-header {
    min-height: 3.5rem;
    padding: 0 1.25rem 0.75rem 4rem;
  }

  #gs-sidebar-overlay {
    display: block;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s;
  }
  #gs-sidebar-overlay.gs-overlay--visible {
    opacity: 1;
    pointer-events: auto;
  }
}

/* ── 5. FACET GROUPS ──────────────────────────────────── */
.gs-facet-group {
  border-bottom: 1px solid rgba(85, 67, 54, 0.35);
}

.gs-facet-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.875rem 1.25rem;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--color-on-surface);
  transition: background 0.15s;
}
.gs-facet-heading:hover {
  background: rgba(255, 183, 125, 0.04);
}

.gs-facet-chevron {
  font-size: 1.1rem;
  color: var(--color-on-surface-variant);
  /* Hardware-accelerated rotation — toggled by .is-collapsed on parent */
  transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
  transform: rotate(0deg);
  will-change: transform;
}

.gs-facet-group.is-collapsed .gs-facet-chevron {
  transform: rotate(180deg);
}

.gs-facet-list {
  padding: 0.25rem 0 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  /* Smooth height + opacity collapse */
  max-height: 600px;
  overflow: hidden;
  transition:
    max-height 0.35s cubic-bezier(0.25, 1, 0.5, 1),
    opacity 0.3s ease;
  opacity: 1;
}

.gs-facet-group.is-collapsed .gs-facet-list {
  max-height: 0;
  opacity: 0;
  pointer-events: none;
  /* Collapse padding with the list */
  padding-top: 0;
  padding-bottom: 0;
}

/* Brand carries ~47 values — well past the 600px expand cap above, which would
   silently clip most of the roster now that the group opens by default. Give
   this one list its own scroll; the collapsed override still needs the id to
   outrank #facet-list-brand's specificity. */
#facet-list-brand {
  max-height: 340px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--color-outline-variant) transparent;
}
#facet-list-brand::-webkit-scrollbar {
  width: 4px;
}
#facet-list-brand::-webkit-scrollbar-thumb {
  background: var(--color-outline-variant);
  border-radius: 4px;
}

.gs-facet-group.is-collapsed #facet-list-brand {
  max-height: 0;
  overflow: hidden;
}

/* Individual facet option */
.gs-facet-option {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.4rem 1.25rem;
  cursor: pointer;
  border-radius: 0;
  transition: background 0.15s;
  position: relative;
}
.gs-facet-option:hover {
  background: rgba(255, 183, 125, 0.06);
}

/* Hidden native checkbox */
.gs-facet-checkbox {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}

/* Custom checkbox box */
.gs-facet-check-box {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  border: 1.5px solid var(--color-outline);
  border-radius: 3px;
  background: transparent;
  transition: background 0.18s, border-color 0.18s;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.gs-facet-checkbox:checked + .gs-facet-check-box {
  background: var(--color-primary-container);
  border-color: var(--color-primary-container);
}

.gs-facet-checkbox:checked + .gs-facet-check-box::after {
  content: '';
  display: block;
  width: 4px;
  height: 8px;
  border: 2px solid var(--color-on-primary);
  border-top: none;
  border-left: none;
  transform: rotate(45deg) translate(-1px, -1px);
}

.gs-facet-label {
  font-family: var(--font-body);
  font-size: 0.825rem;
  color: var(--color-on-surface-variant);
  flex: 1;
  line-height: 1.4;
  transition: color 0.15s;
}

.gs-facet-option:hover .gs-facet-label,
.gs-facet-checkbox:checked ~ .gs-facet-label {
  color: var(--color-on-surface);
}

.gs-facet-count {
  font-family: var(--font-label);
  font-size: 0.7rem;
  color: var(--color-outline);
  font-weight: 600;
  min-width: 1.5rem;
  text-align: right;
}

/* ── 6. SEARCH HEADER ZONE ────────────────────────────── */
.gs-search-header {
  grid-area: search-header;
  /* Grid items default to min-width:auto, sized to their content's
     intrinsic minimum. The quick-filter chip row's nowrap labels give
     this content a wide intrinsic minimum (~650px) — without this, the
     shell's 1fr track refuses to shrink below that on narrow viewports,
     the row's own overflow-x:auto never gets a chance to kick in, and
     the oversized header gets silently clipped by the page's
     overflow:clip instead of the chips scrolling as intended. */
  min-width: 0;
  padding: 1.5rem 2rem 1rem;
  border-bottom: 1px solid var(--color-outline-variant);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;

  /* ── STICKY CATCH ARCHITECTURE ─────────────────────────
     align-self: start is the critical grid fix:
     without it, a grid child stretches to fill the row
     height which silently breaks position: sticky.
     top matches the fixed .site-header height (desktop 64px,
     mobile 60px) so the bar pins immediately below the nav.
  ─────────────────────────────────────────────────────── */
  -webkit-position: sticky; /* legacy Safari */
  position: -webkit-sticky;
  position: sticky;
  /* -1px: the fixed nav's rect and this sticky offset are computed
     independently by the compositor, and on fractional-DPR viewports
     they can round to different device pixels, leaving a hairline gap
     of raw page background visible between the two bars. Catching 1px
     early pulls this bar's opaque background under the nav's bottom
     edge, permanently sealing the seam regardless of rounding. */
  top: calc(4rem - 1px); /* desktop: .site-header height */
  align-self: start;
  width: 100%;

  /* Opaque background prevents scrolling cards from bleeding through */
  background-color: var(--color-surface, #181210);

  /* Clear the sidebar (z-index 40) and fixed bg (0) but stay
     below the site-header (z-index 50 → 999999 via transitions.css) */
  z-index: 110;

  /* Subtle underline shadow to signal elevation over scrolled content */
  box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.55),
              0 1px 0 rgba(85, 67, 54, 0.2);

  /* Smooth shadow appearance transition as element catches */
  transition: box-shadow 0.25s ease;
}

@media (max-width: 1023px) {
  .gs-search-header {
    top: calc(var(--nav-height-mobile, 60px) - 1px); /* mobile .site-header height, sealed 1px early — see desktop rule above */
  }
}

@media (max-width: 768px) {
  .gs-search-header {
    padding: 1rem 1rem 0.75rem;
  }
}

/* Search bar row */
.gs-search-bar-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* ═══════════════════════════════════════════════════════════════════════
   QUICK-FILTER CHIP ROW
   JS-injected beneath the search bar — see buildQuickFilterChips() in
   js/fuse-search.js. The row holds ~45 chips across six families and is
   ordered per visitor (js/chip-affinity.js); the SET is never filtered,
   so the row always overflows and always has to say that it does.

   The shell is the positioning context for the two edge caps; the row
   itself is the scroll container. They are separate elements because a
   cap pinned inside a scroll container scrolls away with the content.
   ═══════════════════════════════════════════════════════════════════════ */
.gs-quick-filters-shell {
  position: relative;
  /* Matches the negative margin the row carries below, so the shell does
     not reintroduce the inset the row is at pains to cancel out. */
  margin-top: 0.75rem;
  /* The expand toggle is a SIBLING of the scroller, not an overlay on it.
     Floating it over the row meant reserving a strip of the row's own width
     to stop it covering a chip — and on a centred block that strip has to
     be mirrored on the left or the optical centre shifts, which cost 19% of
     a 375px phone's width and five visible chips. As a sibling it takes its
     30px once, from outside, and the block centres in what is left. */
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
/* min-width: 0 or the scroller refuses to shrink below its content and
   pushes the toggle off the end of the header. */
.gs-quick-filters-shell > .gs-quick-filters-row {
  flex: 1 1 auto;
  min-width: 0;
}
/* Against a tall wrapped block, centring the toggle vertically parks it in
   the middle of nowhere; the top-right corner reads as a panel control. */
.gs-quick-filters-shell.is-expanded { align-items: flex-start; }

.gs-quick-filters-row {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  /* Setting overflow-x alone forces the UA to compute overflow-y as auto
     too (a scroll container can't leave one axis "visible") — which was
     clipping each chip's focus/tap illumination ring flush against the
     row's own top/bottom edge. Padding opens up room for that ring
     inside the (now unavoidable) clip box; the equal-and-opposite
     negative margin cancels the padding back out so the row's position
     and gap within the header stack are pixel-identical to before. */
  padding: 0.3rem 0.35rem 0.45rem;
  margin: -0.3rem -0.35rem -0.3rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(212, 160, 23, 0.3) transparent;
  /* A horizontal flick inside the row must not chain into the page's own
     vertical scroll, and must not trigger the browser's back-swipe. */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  /* Proximity snap, and ONLY on pointer devices — see the touch override
     further down. It tidies where the chevrons land; on a thumb it is the
     single biggest reason a flick dies early. */
  scroll-snap-type: x proximity;
  /* MUST match the horizontal padding above. The snapport is the padding
     box, so without this the browser snaps chip 1's left edge to 5.6px
     rather than to 0 — the row then rests permanently 5.6px scrolled,
     which reads as has-overflow-start and puts a "scroll left" chevron
     on screen pointing at nothing. Insetting the snapport by the same
     amount makes chip 1's snap position exactly scrollLeft 0. */
  scroll-padding-inline: 0.35rem;
  scroll-behavior: smooth;
}
.gs-quick-filters-row::-webkit-scrollbar {
  height: 4px;
}
.gs-quick-filters-row::-webkit-scrollbar-thumb {
  background: rgba(212, 160, 23, 0.3);
  border-radius: 4px;
}

/* ── Edge fade ──────────────────────────────────────────────────────────
   Applied by class from the MEASURED scroll position (updateChipEdges()),
   one side at a time, and never on a row that fits — a row with nothing
   beyond its edge would otherwise sit there dimming its own last chip for
   no reason. Same mechanism as .cal-rail--scrollable in the portals.

   Not motion, so it is NOT suppressed under prefers-reduced-motion: it is
   a static edge treatment that happens to change when the row does, and a
   visitor who has asked for less animation still needs to know the strip
   continues. */
.gs-quick-filters-row.has-overflow-start.has-overflow-end {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 2.25rem,
                      #000 calc(100% - 2.25rem), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 2.25rem,
                      #000 calc(100% - 2.25rem), transparent);
}
.gs-quick-filters-row.has-overflow-start:not(.has-overflow-end) {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 2.25rem);
          mask-image: linear-gradient(90deg, transparent, #000 2.25rem);
}
.gs-quick-filters-row.has-overflow-end:not(.has-overflow-start) {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 2.25rem), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 2.25rem), transparent);
}

/* ── Edge chevrons ──────────────────────────────────────────────────────
   The fade says "there is more this way"; the chevron says "and here is
   how you get there". Hidden by JS (the `hidden` attribute) whenever the
   row is resting against that edge, so neither cap is ever a dead target.

   tabindex="-1" in the markup: every chip is already reachable with Tab,
   and a keyboard visitor arrowing along the row scrolls it natively — two
   extra stops that duplicate that would be noise, not access. */
.gs-chip-edge {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid rgba(212, 160, 23, 0.28);
  border-radius: 50%;
  background: var(--color-surface-container, #1c1512);
  color: var(--gt-gold);
  cursor: pointer;
  opacity: 0.92;
  transition: background 0.2s, border-color 0.2s, opacity 0.2s;
  -webkit-tap-highlight-color: transparent;
}
.gs-chip-edge:hover {
  background: rgba(212, 160, 23, 0.18);
  border-color: var(--gt-gold);
  opacity: 1;
}
/* `display: flex` above outranks the UA's `[hidden] { display: none }` on
   specificity, so updateChipEdges() setting .hidden = true did nothing and
   both caps painted permanently — a "scroll left" chevron sat at the start
   of a row already resting at scrollLeft 0, and both survived into expanded
   mode where there is no edge at all. Any element given a display value by
   a class has to restate this. */
.gs-chip-edge[hidden] { display: none; }

.gs-chip-edge--start { left: -0.35rem; }
.gs-chip-edge--end   { right: -0.35rem; }
.gs-chip-edge .material-symbols-outlined {
  font-size: 1.15rem;
  line-height: 1;
}
/* The caps are a pointer convenience. On touch the thumb IS the control,
   and two floating buttons sitting on top of the first and last chip cost
   more than they give. */
@media (hover: none) {
  .gs-chip-edge { display: none; }
}

/* ── The two lines ──────────────────────────────────────────────────────
   js/fuse-search.js always emits the chips inside two .gs-chip-line
   wrappers. Everywhere except the phone strip they dissolve — the chips
   become direct flex children of the row and the single-line layout is
   untouched. Only the mobile block below turns them into real lines. */
.gs-chip-line { display: contents; }

/* ── The chips ──────────────────────────────────────────────────────────── */
.gs-quick-chip {
  flex-shrink: 0;
  scroll-snap-align: start;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(212, 160, 23, 0.2);
  border-radius: 100px;
  padding: 0.35rem 0.75rem;
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--gt-gold);
  cursor: pointer;
  white-space: nowrap;
  /* font-weight dropped from the list: it isn't smoothly interpolable on
     a static (non-variable) font, so transitioning it just snaps at the
     midpoint instead of animating — a stutter, not a fade. */
  transition: background 0.2s, border-color 0.2s, transform 0.2s;
  /* Chrome/Android paints its own tap-highlight rectangle over touch
     targets on tap; on a rounded pill it flashes as a squared-off flash
     a frame before the real illumination (focus ring) settles in. */
  -webkit-tap-highlight-color: transparent;
}
.gs-quick-chip:hover,
.gs-quick-chip.is-active {
  background: rgba(212, 160, 23, 0.15);
  border-color: var(--gt-gold);
}
.gs-quick-chip:hover {
  /* A 1px lift, not a scale: scaling a pill inside a scroll container
     changes its scrollWidth and makes the edge state flicker. */
  transform: translateY(-1px);
}
.gs-quick-chip.is-active {
  font-weight: 800;
}

/* Family treatments. Brand and origin chips carry no hash — they are
   proper nouns and read as labels; the hash belongs on the ones that read
   as tags. Everything else stays one gold family so the row is a row, not
   six colour-coded groups fighting the cards below it for attention. */
.gs-quick-chip--brand {
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 0.66rem;
}
.gs-quick-chip--origin {
  letter-spacing: 0.05em;
}

/* The leaf swatch is the same colour the result card's wrapper dot uses
   (wrapperSwatchColor() serves both), so a #Maduro chip and the cards it
   returns agree on sight. Decoration only — aria-hidden in the markup,
   and the label stands alone without it. */
.gs-chip-swatch {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--chip-swatch, #6B5D52);
  /* A far stronger ring than .gt-card-swatch's 0.08, and deliberately so.
     The four maduro-family swatches are near-black (#2B1B17, #1A1110) on a
     near-black chip, so at the card's ring weight they vanish — and a row
     where #ConnecticutShade shows a bright dot and #Maduro shows nothing
     reads as a rendering fault rather than as a dark wrapper. On a card it
     does not matter, because the swatch there sits beside the wrapper's
     own name; a chip has no such label to fall back on. The ring makes an
     unlit disc legible AS an unlit disc. */
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.38),
              inset 0 1px 1px rgba(255, 255, 255, 0.18);
  flex-shrink: 0;
}
.gs-chip-flag {
  font-size: 0.8rem;
  line-height: 1;
}

/* Scoped rather than named .sr-only: this sheet loads on collection.html,
   and .sr-only is already defined independently in admin.css and
   product.css. A fourth global of the same name is how they drift — same
   reasoning as .cta-visually-hidden in home.css. */
.gs-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Micro-motion ───────────────────────────────────────────────────────
   Three separate signals, all of them decoration over a row that already
   works, and all of them off under prefers-reduced-motion (bottom of this
   block).

   1. THE STAGGER. Chips fade up in sequence on first paint, capped at ten
      (--chip-delay, set inline by the renderer). A row that arrives all at
      once is furniture; a row that arrives in sequence is a strip, and the
      eye follows it to the right — which is where the rest of it is. Same
      language as .gt-breadcrumb-item's gt-bc-in. */
@keyframes gs-chip-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.gs-quick-chip {
  animation: gs-chip-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: var(--chip-delay, 0ms);
}
/* The hover lift must not fight the entrance transform mid-animation. */
.gs-quick-chip:hover { animation-play-state: paused; }

/* 2. THE NUDGE. Once per session, ~600ms after paint, and only when the
      row actually overflows: the strip eases right and settles back. It
      is the single most economical way to say "this moves" — the strip
      moving. Applied to the track by transform rather than by scrolling,
      so it cannot leave the row at a scroll offset the visitor did not
      ask for if the class is torn off mid-animation. */
@keyframes gs-chip-nudge {
  0%   { transform: translateX(0); }
  38%  { transform: translateX(-34px); }
  100% { transform: translateX(0); }
}
.gs-quick-filters-row.is-nudging .gs-quick-chip {
  animation: gs-chip-nudge 1150ms cubic-bezier(0.33, 0, 0.2, 1) both;
  animation-delay: 0ms;
}

/* 3. THE DRAG. A grab cursor is the desktop half of the same message the
      nudge carries. `user-select: none` only while dragging, so a chip
      label is still selectable at rest. */
.gs-quick-filters-row.is-dragging {
  cursor: grabbing;
  scroll-behavior: auto;   /* a drag sets scrollLeft directly — smooth would lag the pointer */
  user-select: none;
}
.gs-quick-filters-row.is-dragging .gs-quick-chip { cursor: grabbing; }
@media (hover: hover) and (pointer: fine) {
  .gs-quick-filters-row { cursor: grab; }
}

@media (prefers-reduced-motion: reduce) {
  /* The fade and the chevrons stay — they are edge treatment, not
     animation, and they are the half of the affordance that actually
     carries the information. Only the moving parts go. */
  .gs-quick-chip,
  .gs-quick-filters-row.is-nudging .gs-quick-chip {
    animation: none;
  }
  .gs-quick-chip:hover { transform: none; }
  .gs-quick-filters-row { scroll-behavior: auto; }
}

/* ── Expand toggle ──────────────────────────────────────────────────────
   Pinned at the trailing edge, above the fade, at every width — on a phone
   it is the only trailing control, since the chevrons are pointer-only. */
.gs-chip-expand {
  /* In normal flow as a flex sibling of the scroller — see the shell. */
  flex: 0 0 auto;
  margin-right: -0.35rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid rgba(212, 160, 23, 0.28);
  border-radius: 50%;
  background: var(--color-surface-container, #1c1512);
  color: var(--gt-gold);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
  -webkit-tap-highlight-color: transparent;
}
.gs-chip-expand:hover,
.gs-quick-filters-shell.is-expanded .gs-chip-expand {
  background: rgba(212, 160, 23, 0.18);
  border-color: var(--gt-gold);
}
.gs-chip-expand .material-symbols-outlined {
  font-size: 1.15rem;
  line-height: 1;
}
/* The chevrons are still absolutely positioned over the scroller, and the
   scroller no longer reaches the shell's right edge — the toggle occupies
   that. Pull the end cap back to the scroller's own edge. */
@media (hover: hover) and (pointer: fine) {
  .gs-chip-edge--end { right: 2.25rem; }
}

/* ── Expanded: a block, not a strip ─────────────────────────────────────
   Same 57 chips, same order — only the container's shape changes. Capped
   at 42vh with its own vertical scroll so it can never swallow the phone
   whole; on the tallest rosters that still shows well over half at once. */
.gs-quick-filters-shell.is-expanded .gs-quick-filters-row {
  display: flex;
  flex-wrap: wrap;
  /* Centred, so the block reads as a composition rather than as a list
     that ran out. Left-aligned wrapping leaves a ragged right edge and
     strands the last row's one or two chips hard against the left margin,
     which is the least premium thing a panel of 57 pills can do. */
  justify-content: center;
  /* stretch, not the flex default's effect on mixed heights: brand chips
     run a smaller font and come out 1px shorter, and a row of pills where
     some are 27px and some 28px reads as sloppy rather than as variety. */
  align-items: stretch;
  align-content: flex-start;
  /* Tight. This pane is an information display, not a rail — its job is to
     put the whole roster in front of the eye at once, and every pixel of
     row gap is a pixel of roster pushed below the fold. The rail above
     keeps its generous spacing because that is the surface a thumb
     actually flicks. */
  gap: 0.26rem 0.26rem;
  padding-top: 0.35rem;
  padding-bottom: 0.4rem;
  overflow-x: hidden;
  overflow-y: auto;
  /* Half the viewport, and no more: this lives inside a sticky header, so
     an uncapped block would grow to ~680px and park itself over the
     results it exists to filter. At 50vh a phone shows two thirds of the
     roster at once and the rest is one short vertical nudge — against 8.6
     screens of horizontal flicking, which is what it replaces. */
  max-height: 50vh;
  /* A nested scroller inside a sticky header will hand its overflow to the
     page and scroll the results out from under the visitor mid-gesture. */
  overscroll-behavior: contain;
  /* The mask marks a horizontal edge that no longer exists here. */
  -webkit-mask-image: none;
          mask-image: none;
  /* No clearance strip: the toggle sits outside this box now (see the
     shell), so the whole width is chips and the centring is true. */
  scroll-snap-type: none;
}
.gs-quick-filters-shell.is-expanded .gs-quick-chip {
  /* Free to wrap; in the strip they are flex: 0 0 auto via flex-shrink. */
  flex: 0 0 auto;
  /* FITTING MORE PER ROW IS THE LEVER. Rows are the unit of height here, so
     shaving each pill's side padding buys whole rows back: at 0.75rem the
     mean chip is 94px and a 917px pane holds 9 of them; at 0.5rem it is
     ~85px and holds 10, which is a row off the block for every ten chips.
     Vertical padding comes down with it, but far less — the font size is
     untouched, because this pane was asked to be legible before it was
     asked to be dense. */
  padding: 0.28rem 0.5rem;
  gap: 0.32rem;
}
/* The swatch is the one thing that does not scale with the pill; at the
   tighter padding a 0.5rem dot plus its gap is a tenth of a chip's width. */
.gs-quick-filters-shell.is-expanded .gs-chip-swatch {
  width: 0.44rem;
  height: 0.44rem;
}

@media (max-width: 768px) {
  /* ONE LINE, matching the pointer layout — owner decision, 2026-09-15,
     for consistency with desktop and to give the rail back the ~34px the
     second line was spending.

     This reverses the two-line split, and the arithmetic that motivated
     that split has not changed: 57 chips in a single strip is ~5,778px
     against a 354px frame, so the far end is around 16 screens of
     flicking rather than 8. What makes one line affordable is that
     reaching the far end is no longer the way anyone gets there —
     chip-affinity.js orders the strip so the chips this device actually
     wants sit at the head, the expand toggle puts the whole roster on
     screen at once, and the landing's "Explore by character" doorways
     group the same roster by family a screen below. Three routes to a
     chip that is not in the first handful, none of them a long flick.

     Keep .gs-chip-line as `display: contents` here rather than deleting
     the wrappers: js/fuse-search.js always emits chips inside the two
     line elements, and dissolving them in CSS is what lets the strip be
     one flow without the renderer knowing about layout. */
  .gs-chip-line { display: contents; }

  .gs-quick-chip {
    /* Tighter than the desktop pill: at 0.8rem of side padding the mean
       chip is 94px, and 57 of those is most of the problem above. */
    padding: 0.38rem 0.62rem;
    font-size: 0.68rem;
    gap: 0.3rem;
  }
  .gs-quick-chip--brand { font-size: 0.63rem; }
}

/* A thumb, not a cursor: momentum is the whole interaction, and proximity
   snap is what cuts a fling short — the browser decelerates to the nearest
   snap point, and with a snap point every ~90px there is always one close
   enough to land on. Dropped entirely here. The chevrons that snap existed
   to tidy are pointer-only anyway, so nothing is left to tidy. */
@media (hover: none) {
  .gs-quick-filters-row {
    scroll-snap-type: none;
    /* Let the platform's own fling curve run to its natural end. */
    scroll-behavior: auto;
  }
  .gs-quick-chip { scroll-snap-align: none; }
}

/* The Refine button — the ONLY way into the facet panel, at every width and
   in every narrowed state, so it is never conditionally displayed. */
#gs-mobile-filter-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--color-surface-container);
  border: 1px solid var(--color-outline);
  border-radius: 8px;
  padding: 0.625rem 1rem;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-on-surface);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
  white-space: nowrap;
  flex-shrink: 0;
}
#gs-mobile-filter-btn:hover {
  background: var(--color-surface-container-high);
  border-color: rgba(255, 183, 125, 0.25);
}
#gs-mobile-filter-btn .material-symbols-outlined {
  font-size: 1.1rem;
  color: var(--color-primary);
}

/* Active filter count badge on mobile filter button */
.gs-filter-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.2rem;
  height: 1.2rem;
  padding: 0 0.3rem;
  background: var(--color-primary-container);
  color: var(--color-on-primary);
  border-radius: 100px;
  font-size: 0.65rem;
  font-weight: 800;
  line-height: 1;
  margin-left: 0.1rem;
}

/* The <=768px block that used to reveal this button is gone: the base rule
   above shows it at every width now. */

/* Search input wrapper */
.gs-input-wrapper {
  flex: 1;
  /* Flex items default to min-width:auto, which refuses to shrink below
     the input's content width (padding + kbd badge) — on narrow viewports
     that's wider than the space left after the mobile filter button,
     silently clipped by the page's overflow:clip. min-width:0 lets it
     actually shrink to fit. */
  min-width: 0;
  position: relative;
  display: flex;
  align-items: center;
}

.gs-search-icon {
  position: absolute;
  left: 0.875rem;
  font-size: 1.1rem;
  color: var(--color-outline);
  pointer-events: none;
  transition: color 0.2s;
}

#gs-search-input {
  width: 100%;
  background: var(--color-surface-container);
  border: 1px solid var(--color-outline);
  border-radius: 10px;
  padding: 0.75rem 4.25rem 0.75rem 2.75rem;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 400;
  color: var(--color-on-surface);
  outline: none;
  transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
}

#gs-search-input::placeholder {
  color: var(--color-outline);
  font-weight: 400;
}

/* WebKit paints its own cancel control inside a type="search" field the
   moment it has a value — a second, differently-styled × sitting a few
   pixels left of #gs-clear-btn, with the UA accent colour and none of the
   brand's. Two identical-meaning controls in one field is a coin flip for
   the visitor, so the native one goes and the brand one carries the job.
   Removing it also stops the field from ever showing a control the CSS
   below can't size to a real touch target. */
#gs-search-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

/* One selector per rule, deliberately. Current Chrome has dropped the three
   sibling pseudo-elements below — CSS.supports('selector(...)') reports false
   for each — and an unrecognised selector anywhere in a comma-separated list
   invalidates the WHOLE rule, which silently took the cancel-button
   suppression down with it when all four shared one block. Kept separately
   because older WebKit/Safari still paints these. */
#gs-search-input::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}
#gs-search-input::-webkit-search-results-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}
#gs-search-input::-webkit-search-results-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

@media (pointer: coarse) {
  #gs-search-input {
    /* 16px floor. iOS Safari zooms the layout in when a focused input's
       text is smaller than this and does not zoom back out on blur, so a
       single tap on a 15px field leaves the whole page magnified with the
       sticky header cropped. Raising the type is the fix; adding
       maximum-scale to the viewport meta is not — that disables pinch-zoom
       for everyone. */
    font-size: 1rem;
  }
}

#gs-search-input:focus {
  border-color: var(--gt-gold);
  background: var(--color-surface-container-high);
  box-shadow: 0 0 0 3px rgba(212, 160, 23, 0.14), 0 0 20px rgba(212, 160, 23, 0.25);
}

#gs-search-input:focus ~ .gs-search-icon,
.gs-input-wrapper:focus-within .gs-search-icon {
  color: var(--gt-gold);
}

/* Ctrl+K / ⌘K hint badge — shares the input's right-edge slot with
   #gs-clear-btn below; the two are mutually exclusive (see syncClearButton()
   in fuse-search.js), so only one is ever visible at a time. */
.gs-kbd-badge {
  position: absolute;
  right: 0.75rem;
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.4rem;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--color-outline-variant);
  border-radius: 4px;
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--color-on-surface-variant);
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.15s;
}

.gs-kbd-badge.is-hidden,
#gs-search-input:focus ~ .gs-kbd-badge {
  opacity: 0;
}

/* Clear button inside input */
#gs-clear-btn {
  position: absolute;
  right: 0.75rem;
  background: var(--color-surface-container-high);
  border: 1px solid var(--color-outline-variant);
  border-radius: 50%;
  width: 1.5rem;
  height: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--color-on-surface-variant);
  transition: background 0.2s, color 0.2s;
}
#gs-clear-btn:hover {
  background: var(--color-surface-container-highest);
  color: var(--color-on-surface);
}
#gs-clear-btn .material-symbols-outlined {
  font-size: 0.9rem;
}
#gs-clear-btn[hidden] {
  display: none;
}

@media (pointer: coarse) {
  /* The visible circle stays 24px — a bigger one would crowd the field —
     but a 24px tap target is well under the 44px minimum, and this is the
     control that wipes the query, so a near-miss is expensive. The pseudo
     extends the hit box without touching layout. It reaches at most 46px
     in from the input's right edge, still clear of the 68px right padding
     the text stops at, so it can never swallow a tap meant to place the
     caret at the end of a query. */
  #gs-clear-btn::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 2.75rem;
    height: 2.75rem;
    transform: translate(-50%, -50%);
  }
}

#gs-clear-btn,
#gs-mobile-filter-btn,
#gs-sidebar-close,
#gs-reset-all {
  /* Chrome/Android flashes a squared-off grey rectangle over a tapped
     control a frame before the element's own :hover/:active styling
     settles — on a circle or a pill it reads as a glitch. */
  -webkit-tap-highlight-color: transparent;
}

/* Results meta row */
.gs-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-height: 1.5rem;
}

#gs-results-count {
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
}

/* Active filter tags bar */
#gs-active-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.25rem;
}

#gs-active-tags[hidden] {
  display: none;
}

.gs-active-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: rgba(217, 119, 6, 0.12);
  border: 1px solid rgba(217, 119, 6, 0.3);
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--color-primary);
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s;
}
.gs-active-tag:hover {
  background: rgba(217, 119, 6, 0.22);
  border-color: rgba(217, 119, 6, 0.5);
}

/* Quick-filter preset tags — same pill, gold accent instead of amber so
   they read as a distinct filter type from facet tags at a glance. */
.gs-active-tag--preset {
  background: rgba(212, 160, 23, 0.12);
  border-color: rgba(212, 160, 23, 0.35);
  color: var(--gt-gold, #D4A017);
}
.gs-active-tag--preset:hover {
  background: rgba(212, 160, 23, 0.22);
  border-color: rgba(212, 160, 23, 0.55);
}

/* Filters within one family are OR'd; families are AND'd (see
   runSearch()). The group is the visible form of that: same-family pills
   sit tight together with a lowercase "or" between them, and the ordinary
   bar gap separates one family from the next — which reads as "and"
   without printing the word, the way a filter bar always has. Without
   this, three pills in a row claim a uniform relationship that is not the
   one the search applies, and the visitor is left to infer it from a
   result count that went UP when they expected it to go down.

   A family holds BOTH controls — a Refine-panel checkbox and a chip
   naming the same property land in one group, because that is how they
   now compose. */
.gs-active-group {
  display: inline-flex;
  align-items: center;
  /* Wraps INTERNALLY, unlike when a group held two or three chips: the
     Brand family can now carry every brand the Refine panel offers, and
     four pills already overrun a 375px bar. What must not break across a
     line is "or" and the pill it introduces — .gs-active-pill binds those
     two, so the group can wrap anywhere without stranding a dangling
     "or" at the end of a line. */
  flex-wrap: wrap;
  gap: 0.3rem;
  flex: 0 1 auto;
}

.gs-active-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  flex: 0 0 auto;
}

.gs-active-or {
  font-family: var(--font-label);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: lowercase;
  color: var(--gt-gold, #D4A017);
  opacity: 0.55;
  padding: 0 0.05rem;
  user-select: none;
}

.gs-tag-remove {
  font-size: 0.85rem;
  opacity: 0.7;
}

/* ── 7. RESULTS ZONE ──────────────────────────────────── */
.gs-results-zone {
  grid-area: results;
  /* Declared as a variable because the landing's phone rails have to
     cancel exactly this inset to run edge-to-edge and then restore it as
     their own scroll padding (see .gl-grid under 600px). Two literals
     that must agree are two literals that will eventually disagree. */
  --gs-zone-pad: 2rem;
  padding: 1.5rem var(--gs-zone-pad) 4rem;
  overflow-y: auto;

  /* The result count/height changes on every keystroke. Without this,
     the browser's scroll-anchoring heuristic "helpfully" re-adjusts
     window scroll position on the next layout tick to compensate for the
     shrinking content above the fold — fighting the explicit scroll
     correction fuse-search.js applies in runSearch(), and leaving the
     user scrolled past the new (shorter) result set. */
  overflow-anchor: none;
}

@media (max-width: 768px) {
  .gs-results-zone {
    --gs-zone-pad: 0.75rem;
    padding: 1rem var(--gs-zone-pad) 3rem;
  }
}

/* Results grid — standalone product cards, no brand wrapper boxes.
   Grid items stretch (default) so every card in a row shares the same
   height; .gt-card-chip's margin-top:auto then pins the bottom-most
   element flush regardless of how much title/description text precedes it. */
#gs-results-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: clamp(1rem, 2.5vw, 1.75rem);
}

@media (max-width: 480px) {
  #gs-results-grid {
    grid-template-columns: 1fr;
  }
}

/* ── 8. PRODUCT CARD ──────────────────────────────────────
   Standalone per-vitola cards — no brand wrapper box. Each card is its
   own grid item; nothing groups multiple products together. */
.gt-product-card {
  display: flex;
  flex-direction: column;
  border-radius: 14px;
  overflow: hidden;
  text-decoration: none;
  /* Grid items get an automatic minimum size equal to their content's
     min-content width unless overridden — .gt-card-stat-value's
     white-space:nowrap (needed for its ellipsis truncation) would otherwise
     propagate a wide min-content up through the column-flex chain and force
     #gs-results-grid's minmax(300px, 1fr) track wider than 300px. */
  min-width: 0;
  /* Metallic gradient edge: a dual-background trick, not border-image —
     border-image ignores border-radius and would leave sharp corners on
     this 14px-rounded card. The padding-box layer fills the interior at
     the previous .glass-container tone; the border-box layer paints only
     the 1px transparent-border ring with the gold→dark gradient. */
  border: 1px solid transparent;
  background:
    linear-gradient(rgba(24, 18, 16, 0.78), rgba(24, 18, 16, 0.78)) padding-box,
    linear-gradient(135deg, rgba(212, 160, 23, 0.35), rgba(80, 50, 20, 0.1)) border-box;
  transition:
    transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
    background 0.35s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.35s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* Reveal-on-scroll base state (opacity/translateY) comes from the global
   .reveal-on-scroll rule in components.css — only the faster, card-grid-
   appropriate transition timing is overridden here. */
.gt-product-card.visible {
  transition:
    opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

.gt-product-card:hover,
.gt-product-card:focus-visible {
  transform: translateY(-4px);
  background:
    linear-gradient(rgba(24, 18, 16, 0.85), rgba(24, 18, 16, 0.85)) padding-box,
    linear-gradient(135deg, rgba(212, 160, 23, 0.55), rgba(80, 50, 20, 0.15)) border-box;
  box-shadow:
    0 12px 40px rgba(0, 0, 0, 0.35),
    0 12px 32px -8px rgba(212, 160, 23, 0.12);
  outline: none;
}

/* Media — same rounded-frame/object-fit/hover-zoom language as the
   product detail page's .product-image-frame/.cigar-asset-thumb, so a
   card and its detail page read as the same visual system. */
.gt-card-media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--color-surface-container-lowest);
  flex-shrink: 0;
  overflow: hidden;
}

.gt-card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
  will-change: transform;
}

.gt-product-card:hover .gt-card-img {
  transform: scale(1.045);
}

.gt-card-media-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(13, 11, 9, 0.5), transparent 55%);
  pointer-events: none;
}

/* Flagship badge (TOP SELLER / NN+ RATED) — see flagshipBadge() in
   fuse-search.js; gated on real rating/isTopSeller data, so this has
   nothing to render on any card until that data exists. */
.gt-card-badge {
  position: absolute;
  top: 0.625rem;
  right: 0.625rem;
  background: rgba(15, 12, 10, 0.85);
  border: 1px solid rgba(212, 160, 23, 0.4);
  color: var(--gt-gold);
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.25rem 0.55rem;
  border-radius: 100px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* ── LIVE STOCK BADGE ──────────────────────────────────────────────────────
   Painted from /api/stock by js/fuse-search.js. Sits top-LEFT so it can never
   collide with the flagship badge at top-right — one card can legitimately
   carry both.

   Cards are never hidden or reordered for being unavailable. The shop's full
   range is a different claim from today's shelf, and this badge is what tells
   the two apart; removing the card would erase the first claim to make the
   second. Same reasoning as migration 007 for sold-out rewards. */
.gt-card-stock {
  position: absolute;
  top: 0.625rem;
  left: 0.625rem;
  background: rgba(15, 12, 10, 0.85);
  border: 1px solid currentColor;
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.25rem 0.55rem;
  border-radius: 100px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* The same three hues the product page's availability chip uses, so a shopper
   who has seen one recognises the other. */
.gt-card-stock--out-of-stock  { color: #d89a86; }
.gt-card-stock--special-order { color: #ffb77d; }
.gt-card-stock--arriving-soon { color: #8fb0c4; }

/* The compact row's thumbnail is 100px square — a pill overflows it, so the
   badge becomes a band across the foot of the image instead. */
.gt-compact-media { position: relative; }

.gt-compact-media .gt-card-stock {
  top: auto;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 0;
  border-width: 1px 0 0;
  text-align: center;
  font-size: 0.55rem;
  letter-spacing: 0.04em;
  padding: 0.2rem 0.15rem;
}

/* Body */
.gt-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.125rem 1.25rem 1.25rem;
  flex: 1;
  min-width: 0;
}

.gt-card-brand-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Optional per-brand emblem — only rendered when BRAND_LOGOS has an entry
   for the brand (fuse-search.js:brandEmblemHtml); no logo assets exist
   yet, so this rule has nothing to style on the live site today. */
.gs-brand-emblem {
  width: 1.25rem;
  height: 1.25rem;
  object-fit: contain;
  flex-shrink: 0;
  border-radius: 3px;
}

.gt-card-brand {
  font-family: var(--font-label);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gt-gold);
}

.gt-card-title {
  font-family: var(--font-headline);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.32;
  color: var(--color-on-surface);
}

.gt-card-desc {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--gt-muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Mini spec grid — Wrapper / Size */
.gt-card-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem 0.75rem;
  margin-top: 0.25rem;
}

.gt-card-stat {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}

.gt-card-stat-label {
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
  opacity: 0.7;
}

.gt-card-stat-value {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-body);
  font-size: 0.85rem;
  color: var(--gt-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gt-card-swatch {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  flex-shrink: 0;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
}

/* Strength meter */
.gt-card-strength {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.35rem;
}

.gt-card-strength-label {
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--color-on-surface-variant);
}

/* Still used by .gt-compact-strength-mini (compact search row) below. */
.gt-strength-icon {
  font-size: 0.8rem;
  color: var(--color-outline-variant);
  font-variation-settings: 'FILL' 0, 'wght' 500, 'GRAD' 0, 'opsz' 20;
}

.gt-strength-icon--filled {
  color: var(--gt-gold);
  font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
}

.gt-card-strength-track {
  display: block;
  height: 4px;
  background: var(--color-surface-variant);
  border-radius: 100px;
  overflow: hidden;
}

.gt-card-strength-fill {
  display: block;
  height: 100%;
  background: linear-gradient(to right, var(--color-primary), var(--color-primary-container));
  border-radius: 100px;
  box-shadow: 0 0 8px rgba(255, 183, 125, 0.4);
}

/* Origin chip — margin-top:auto pins it to the card's bottom edge so a
   row of cards with uneven title/description lengths still lines up
   along the bottom, since #gs-results-grid stretches items to equal height. */
.gt-card-chip {
  align-self: flex-start;
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: rgba(37, 30, 28, 0.55);
  border: 1px solid rgba(85, 67, 54, 0.5);
  border-radius: 100px;
  padding: 0.2rem 0.6rem;
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--color-on-surface-variant);
}

/* ── 8b. COMPACT RESULT LIST — any active narrowing ───────
   Horizontal rows swapped in for #gs-results-grid the moment the shopper
   narrows the catalog at all — a live text query, a facet checkbox or a
   quick-filter chip (see isCompactMode in renderResults()) — so several
   results are visible at once instead of one gallery card filling the
   viewport. Row height is tuned to ~130px so ~3.5 rows show per mobile
   viewport (measured against the space left after the sticky search header
   + fixed bottom nav), trading a bit of density for a genuinely premium-sized
   photo, title and description versus the old ultra-slim 65px strip. The extra
   class beats the plain #gs-results-grid rules (incl. their @media
   override) on specificity, so this applies at every breakpoint. */
#gs-results-grid.gs-results-grid--compact {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.gt-compact-card {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding: 1rem 1.15rem;
  border-radius: 14px;
  text-decoration: none;
  min-width: 0;
  border: 1px solid transparent;
  background:
    linear-gradient(rgba(24, 18, 16, 0.78), rgba(24, 18, 16, 0.78)) padding-box,
    linear-gradient(135deg, rgba(212, 160, 23, 0.28), rgba(80, 50, 20, 0.08)) border-box;
  transition:
    transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
    background 0.3s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

.gt-compact-card:hover,
.gt-compact-card:focus-visible {
  transform: translateX(5px);
  background:
    linear-gradient(rgba(24, 18, 16, 0.85), rgba(24, 18, 16, 0.85)) padding-box,
    linear-gradient(135deg, rgba(212, 160, 23, 0.55), rgba(80, 50, 20, 0.15)) border-box;
  box-shadow:
    0 10px 28px rgba(0, 0, 0, 0.32),
    0 8px 24px -6px rgba(212, 160, 23, 0.16);
  outline: none;
}

/* Stagger-in entrance — replaces the scroll-triggered .reveal-on-scroll
   treatment (irrelevant here since the whole slim list sits in view at
   once); index-driven animation-delay is set inline per row in
   buildCompactCard(). */
@keyframes gt-compact-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.gt-compact-reveal {
  animation: gt-compact-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .gt-compact-reveal { animation: none; }
}

.gt-compact-media {
  width: 100px;
  height: 100px;
  border-radius: 12px;
  overflow: hidden;
  flex-shrink: 0;
  background: var(--color-surface-container-lowest);
}
.gt-compact-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
}
.gt-compact-card:hover .gt-compact-img {
  transform: scale(1.06);
}

.gt-compact-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.gt-compact-brand {
  font-family: var(--font-label);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gt-gold);
}

.gt-compact-title {
  font-family: var(--font-headline);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--color-on-surface);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gt-compact-desc {
  font-family: var(--font-body);
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--gt-muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.gt-compact-meta {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-body);
  font-size: 0.74rem;
  color: var(--gt-muted);
  overflow: hidden;
  margin-top: auto;
  padding-top: 0.15rem;
}

.gt-compact-meta-item {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  white-space: nowrap;
  flex-shrink: 0;
}
.gt-compact-meta-wrapper {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gt-compact-side {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  margin-left: auto;
}

.gt-compact-strength-mini {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
}
.gt-compact-strength-mini .gt-strength-icon {
  font-size: 0.72rem;
}

/* Gradient strength bar — mobile-only stand-in for .gt-compact-strength-mini
   (see the ≤560px block below, where the icon column is dropped for lack of
   width). Hidden by default so tablet/desktop keeps the icon-row reading. */
.gt-compact-strength-meter {
  display: none;
  width: 100%;
  margin: 0.1rem 0 0.15rem;
}

.gt-compact-strength-meter-track {
  display: block;
  width: 100%;
  height: 5px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 100px;
  overflow: hidden;
}

.gt-compact-strength-meter-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-container) 60%, #7c2d12 100%);
  border-radius: 100px;
}

.gt-compact-arrow {
  font-size: 1.3rem;
  color: var(--color-outline);
  transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1), color 0.25s;
}
.gt-compact-card:hover .gt-compact-arrow {
  transform: translateX(3px);
  color: var(--gt-gold);
}

/* Below ~560px there isn't width for a 5-icon strength column alongside a
   100px photo without squeezing the title/description column too hard —
   drop it in favor of the full-width .gt-compact-strength-meter gradient
   bar in the body stack instead, so strength stays visible without eating
   into the photo/title width. Clamping the description to 1 line (from 2)
   frees the vertical room the new bar needs, keeping the row's total
   content height roughly where it was. Origin/size in the meta row fold away
   progressively narrower still, in ascending order of how much a shopper
   needs them mid-search. The card's own size (padding/media/gap) stays put
   at every width — this is the deliberate "bigger, fewer per screen" scale
   the whole component was resized for, so mobile must not quietly revert
   to the old cramped dimensions. */
@media (max-width: 560px) {
  .gt-compact-strength-mini { display: none; }
  .gt-compact-strength-meter { display: block; }
  .gt-compact-desc { -webkit-line-clamp: 1; }
}
@media (max-width: 480px) {
  .gt-compact-meta-origin { display: none; }
}
@media (max-width: 380px) {
  .gt-compact-card { gap: 0.85rem; padding: 0.85rem 0.9rem; }
  .gt-compact-media { width: 84px; height: 84px; }
  .gt-compact-meta-size { display: none; }
}

/* ── 9. STATES — LOADING, NO RESULTS, ERROR ───────────── */
#gs-loading {
  grid-column: 1 / -1;
  padding: 3rem 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  color: var(--color-on-surface-variant);
}

#gs-loading[hidden] { display: none; }

.gs-loading-spinner {
  width: 36px;
  height: 36px;
  border: 3px solid var(--color-outline-variant);
  border-top-color: var(--color-primary-container);
  border-radius: 50%;
  animation: gs-spin 0.8s linear infinite;
}

@keyframes gs-spin {
  to { transform: rotate(360deg); }
}

#gs-no-results {
  grid-column: 1 / -1;
  background: rgba(37, 30, 28, 0.3);
  border: 1px dashed rgba(255, 183, 125, 0.15);
  border-radius: 12px;
  padding: 3.5rem 2rem;
  text-align: center;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

#gs-no-results[hidden] { display: none; }

.gs-no-results-icon {
  color: var(--color-primary);
  font-size: 2.5rem;
  margin-bottom: 0.875rem;
}

.gs-no-results-title {
  font-family: var(--font-headline);
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-on-surface);
  margin-bottom: 0.5rem;
}

.gs-no-results-text {
  font-family: var(--font-body);
  font-size: 0.9rem;
  color: var(--color-on-surface-variant);
  max-width: 24rem;
  margin: 0 auto;
  line-height: 1.6;
  font-weight: 400;
}

.gs-error-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 3rem;
  color: var(--color-on-surface-variant);
}

/* ── 10. REVEAL ON SCROLL — base state ───────────────── */
/* .gt-product-card carries the global .reveal-on-scroll class (opacity:0 +
   translateY, see components.css), so it starts hidden without needing its
   own base rule here — only the .visible transition speed is overridden
   above (section 8). The .visible class itself is added by
   rebindRevealObserver(), so search-rendered cards honour the same
   animation contract as static page elements. */

/* ── 11. SELECTION PAGE INTEGRATION SHIM ─────────────── */
/* When the search shell is injected INSIDE .selection-section,
   provide proper separation from the disclaimer wrapper above */
.selection-section .search-page-shell {
  margin-top: 2rem;
  border-top: 1px solid var(--color-outline-variant);
}

/* ── Sticky overrides for the selection-page context ─────
   Reinforce sticky positioning and elevate z-index above the
   selection-page vignette (z: 10) and fixed bg (z: 0) layers.
   The `position: relative; z-index: 30` previously applied here
   has been removed — it was overriding the sticky declaration
   and demoting it to a normal in-flow block element.
───────────────────────────────────────────────────────── */
.selection-section .gs-search-header {
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  /* Do NOT set position: relative here — it would override sticky */
  z-index: 110;
  /* Solidify the background against the selection-page surface */
  background-color: var(--bg-charcoal, #181210);
}

.selection-section .gs-results-zone {
  position: relative;
  z-index: 30;
}

/* Sidebar z-index stack must clear the fixed background plate (z-index: 0)
   and the vignette overlay (z-index: 10) */
.selection-section #mobile-filter-drawer {
  z-index: 40;
}

@media (max-width: 768px) {
  /* On mobile the sidebar is fixed + full-height, must be above everything */
  .selection-section #mobile-filter-drawer {
    z-index: 9999;
  }

  /* On mobile, the search header is the top element (sidebar is off-canvas).
     It should pin immediately under the fixed site-header. */
  .selection-section .gs-search-header {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}

@media (max-width: 512px) {
  .selection-section .gs-search-header,
  .selection-section .gs-results-zone {
    padding-left: 0.75rem;
    padding-right: 0.75rem;
  }
  .selection-section .search-page-shell {
    margin-top: 1.25rem;
  }
}

/* ══════════════════════════════════════════════════════════
   8c. THE BROWSE LANDING — the un-narrowed state
   Markup shells are baked into collection.html; contents are written
   by js/fuse-search.js §8b. See that file's header for the model.

   Two rules govern everything below:

   1. EVERY .gl-* block that a class gives a display value to must
      restate [hidden] { display: none }. `[hidden]` is a UA rule of the
      lowest possible specificity and a class beats it — the chip row's
      edge chevrons shipped visible for exactly this reason
      (CLAUDE.md §8), and this file now has nine hideable sections.

   2. Nothing here depends on the IntersectionObserver. main.js collects
      .reveal-on-scroll once at DOM ready, so cards written into these
      sections afterwards would never receive .visible and would sit at
      opacity:0 forever. The override below gives them a visible resting
      state and a CSS stagger instead, which is also what lets the first
      screen of this page be complete the moment it paints.
   ══════════════════════════════════════════════════════════ */

/* ── browsing chrome ─────────────────────────────────────
   The shell no longer changes shape between browsing and narrowed: the
   facet panel is off-canvas in both, so the shell is single-column in
   both, and the landing and the results list get the same full width.

   What used to live here was a set of body.gs-browsing overrides that
   turned the in-layout sidebar into an off-canvas panel while browsing
   and let it spring back into the layout the moment anything narrowed.
   That spring-back is what made the panel appear to open itself: type a
   character or tick one chip and a 280px facet column materialised beside
   the results, with nobody having asked for it. The panel now opens on
   the Refine button and on nothing else.

   The cost, stated plainly: a desktop visitor no longer has the facets
   permanently in view beside their results and has to re-open the panel
   for each pass. That is the trade the owner asked for. The scrim and the
   close control are what make it survivable, and both are unconditional
   below. */

/* The button used to be revealed by these two rules and hidden by default,
   and it carried two labels that swapped on body.gs-browsing: "Refine"
   while browsing, "Filters" once narrowed. Both arrangements described a
   sidebar that folded away and came back, and neither survives it being
   off-canvas in every state.

   Hidden-by-default meant the control vanished the moment a chip or a
   keystroke narrowed the page — at exactly the point the visitor has a
   result set worth filtering, and with the panel now off-canvas, that left
   no way into the facets at all above 769px. It is display:flex in its own
   rule now, unconditionally.

   The label is likewise fixed at "Refine". The swap encoded a difference
   between the two states; the button does the identical job in both now,
   so renaming itself mid-session was noise. */

/* ── the landing container ────────────────────────────── */
#gs-landing {
  display: flex;
  flex-direction: column;
  gap: clamp(2.5rem, 5vw, 4rem);
  /* Above .search-page-shell::before's cedar-grain overlay, same as the
     cards and the sticky header, so the blend never muddies this text. */
  position: relative;
  z-index: 2;
}
#gs-landing[hidden] { display: none; }

.gl-sec {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}
.gl-sec[hidden] { display: none; }

/* Cards written into the landing must not wait on an observer that has
   already run. Visible at rest, with a short staggered entrance of
   their own — capped at six because no landing grid holds more, and the
   browse-all drawer opts out entirely (283 cards cannot stagger). */
#gs-landing .reveal-on-scroll {
  opacity: 1;
  transform: none;
  transition: none;
}
#gs-landing .gl-grid > *,
#gs-landing .gl-rail > * {
  /* TRANSFORM ONLY — the entrance must never own opacity.
     An animation that never advances holds its `from` keyframe forever,
     and a page whose cards are only visible once an animation has run is
     the same defect as a section that only appears once an observer has
     fired (CLAUDE.md §8) — it just fails through a different mechanism.
     Anything that stops animations mid-flight (a backgrounded tab at
     paint time, a headless capture, a suspended renderer) would leave a
     fading entrance stuck at invisible. Animating position alone means
     the worst case is a card that did not slide, not a card that is not
     there. */
  animation: gl-rise 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
}
#gs-landing .gl-grid > :nth-child(1) { animation-delay: 0ms; }
#gs-landing .gl-grid > :nth-child(2) { animation-delay: 60ms; }
#gs-landing .gl-grid > :nth-child(3) { animation-delay: 120ms; }
#gs-landing .gl-grid > :nth-child(4) { animation-delay: 180ms; }
#gs-landing .gl-grid > :nth-child(5) { animation-delay: 240ms; }
#gs-landing .gl-grid > :nth-child(n+6) { animation-delay: 300ms; }
#gs-landing #gl-all-grid > * { animation: none; }

@keyframes gl-rise {
  from { transform: translateY(14px); }
  to   { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  #gs-landing .gl-grid > *,
  #gs-landing .gl-rail > * { animation: none; }
}

/* ── section heads ────────────────────────────────────── */
.gl-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--color-outline-variant);
}

.gl-head-text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.gl-kicker {
  margin: 0;
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
}

.gl-sub {
  margin: 0;
  font-family: var(--font-body);
  font-size: 0.8rem;
  color: var(--color-on-surface-variant);
  opacity: 0.8;
}

.gl-head-action {
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-primary);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.25rem 0;
  opacity: 0.75;
  transition: opacity 0.2s;
  -webkit-tap-highlight-color: transparent;
}
.gl-head-action:hover,
.gl-head-action:focus-visible { opacity: 1; }

/* ── card grids ───────────────────────────────────────────
   The same track the results grid uses, so a landing card and a search
   card are the same object at the same size and the page does not
   appear to change card design when a filter is pressed. */
.gl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: clamp(1rem, 2.5vw, 1.75rem);
}
.gl-grid[hidden] { display: none; }

/* ── the phone shape: sections scroll down, cards scroll across ──
   Stacked one-per-column, the landing's twelve full cards alone run
   ~4,900px and the whole page measures 10.4 screens on a 375 viewport.
   Turning each section's grid into a snap rail — one card and a peek of
   the next — brings it to roughly five, which is the difference between
   a hub you skim and a second wall.

   This is NOT the mistake the chip rail made. That row failed because 57
   chips in one line is 5,778px of travel with nothing to tell you how
   far along you are, which is why it was split into two packed lines. A
   section rail holds four to eight cards, ends where a thumb can feel it
   end, and carries a heading naming what is in it. Bounded travel with a
   label is a carousel; unbounded travel without one is a maze.

   The peek is load-bearing: a card at 100% width looks like a static
   block and nobody swipes it. 78% leaves ~77px of the next card showing,
   which is what says "there is more this way".

   #gl-all-grid opts out — "browse all 283" is a list by definition, and
   283 cards in a horizontal rail is the maze again. */
@media (max-width: 600px) {
  .gl-grid:not(#gl-all-grid) {
    display: flex;
    grid-template-columns: none;
    gap: 0.75rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--color-outline-variant) transparent;
    padding-bottom: 0.35rem;
    /* The zone's own side padding would otherwise clip the rail's last
       card against the viewport edge rather than letting it scroll
       clear; the negative margin lets the track run edge to edge while
       the cards keep their gutter. */
    margin-inline: calc(var(--gs-zone-pad) * -1);
    padding-inline: var(--gs-zone-pad);
    /* MUST match the padding above, and it is not optional under a
       mandatory snap. The snapport is the PADDING BOX, not the content
       box, so with scroll-padding left at its default the browser aligns
       card 1's edge to the padding-box start — which the negative margin
       has just put at the viewport edge. The rail then rests permanently
       12px scrolled with the first photograph bleeding off the side of
       the screen, 12px to the left of its own section heading.

       Insetting the snapport by the same amount makes card 1's snap
       position exactly scrollLeft 0, and its rest position flush with
       the heading above it. Identical mechanism to the chip row's
       scroll-padding-inline (§8 of that block) — a scroll container with
       side padding and snapping needs both numbers or it sits off by
       the padding forever. */
    scroll-padding-inline: var(--gs-zone-pad);
  }
  .gl-grid:not(#gl-all-grid) > * {
    flex: 0 0 78%;
    scroll-snap-align: start;
  }
  .gl-grid:not(#gl-all-grid)::-webkit-scrollbar { height: 4px; }
  .gl-grid:not(#gl-all-grid)::-webkit-scrollbar-thumb {
    background: var(--color-outline-variant);
    border-radius: 4px;
  }

  /* The tail keeps its column: it is the "show me everything" list. */
  #gl-all-grid { grid-template-columns: 1fr; }
}

@media (max-width: 480px) {
  #gl-all-grid { grid-template-columns: 1fr; }
}

/* ── the trail rail ───────────────────────────────────────
   Half a product card, deliberately: a way back to something already
   seen is not a pitch, which is the same call viewer-history.js's own
   rail makes on the product page. */
.gl-rail {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--color-outline-variant) transparent;
  /* The rail is short — eight cards, not 57 chips — so it ends where a
     thumb can feel it end and snapping helps rather than stalling the
     flick. (The chip row drops snap on touch for the opposite reason.) */
  scroll-snap-type: x proximity;
  padding-bottom: 0.35rem;
  overscroll-behavior-x: contain;
}
.gl-rail::-webkit-scrollbar { height: 4px; }
.gl-rail::-webkit-scrollbar-thumb {
  background: var(--color-outline-variant);
  border-radius: 4px;
}

.gl-trail-card {
  flex: 0 0 auto;
  width: 190px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  border: 1px solid var(--color-outline-variant);
  border-radius: 12px;
  overflow: hidden;
  background: var(--color-surface-container-low);
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.3s;
}
.gl-trail-card:hover,
.gl-trail-card:focus-visible {
  transform: translateY(-3px);
  border-color: rgba(212, 160, 23, 0.45);
}

.gl-trail-plate {
  display: block;
  aspect-ratio: 4 / 3;
  background: var(--color-surface-container);
  overflow: hidden;
}
.gl-trail-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.gl-trail-plate--mono {
  display: grid;
  place-items: center;
}
.gl-trail-monogram {
  font-family: var(--font-headline);
  font-size: 1.6rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: rgba(212, 160, 23, 0.34);
}

.gl-trail-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.6rem 0.7rem 0.75rem;
  min-width: 0;
}
.gl-trail-brand {
  font-family: var(--font-label);
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
}
.gl-trail-name {
  font-family: var(--font-headline);
  font-size: 0.82rem;
  line-height: 1.3;
  color: var(--color-on-surface);
  /* Two lines, then ellipsis — a rail card must not set its own height
     from the longest cigar name in the trail. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.gl-trail-meta {
  font-size: 0.66rem;
  color: var(--color-on-surface-variant);
  opacity: 0.8;
}

/* ── tonight's pick ───────────────────────────────────────
   The one section that describes a cigar rather than listing it. */
.gl-spot {
  display: grid;
  grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2rem);
  align-items: start;
  padding: clamp(1rem, 2.5vw, 1.6rem);
  border: 1px solid rgba(212, 160, 23, 0.22);
  border-radius: 16px;
  background:
    linear-gradient(135deg,
      var(--color-surface-container) 0%,
      var(--color-surface-container-low) 60%,
      var(--color-surface-container-lowest) 100%);
}
.gl-spot[hidden] { display: none; }

@media (max-width: 700px) {
  .gl-spot { grid-template-columns: 1fr; }
}

.gl-spot-plate {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  overflow: hidden;
  background: var(--color-surface-container-high);
}
.gl-spot-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gl-spot-text {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  min-width: 0;
}
.gl-spot-brand {
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
}
.gl-spot-name {
  margin: 0;
  font-family: var(--font-headline);
  font-size: clamp(1.15rem, 2.4vw, 1.5rem);
  font-weight: 600;
  line-height: 1.2;
}
.gl-spot-name a {
  color: var(--color-on-surface);
  text-decoration: none;
}
.gl-spot-name a:hover,
.gl-spot-name a:focus-visible { color: var(--color-primary); }

.gl-spot-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  font-size: 0.75rem;
  color: var(--color-on-surface-variant);
}

.gl-spot-strength {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  max-width: 320px;
}
.gl-spot-strength-label {
  font-family: var(--font-label);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-on-surface-variant);
  white-space: nowrap;
}
.gl-spot-strength-track {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: var(--color-surface-container-high);
  overflow: hidden;
}
.gl-spot-strength-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--color-primary-container), var(--color-primary));
}

/* The three thirds. A <dl>, because that is what this is: a term and
   the account of it, three times. */
.gl-phases {
  margin: 0.35rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.gl-phase {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: 0.75rem;
  align-items: start;
}
.gl-phase dt {
  font-family: var(--font-label);
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
  padding-top: 0.15rem;
}
.gl-phase dd {
  margin: 0;
  font-size: 0.83rem;
  line-height: 1.5;
  color: var(--color-on-surface-variant);
}
@media (max-width: 520px) {
  .gl-phase { grid-template-columns: 1fr; gap: 0.15rem; }
}

/* ── doorways ─────────────────────────────────────────────
   The chip roster, grouped and named. Not cards: these are entrances,
   so they get a flat tinted panel rather than the card's border/shadow
   treatment — the page should not read as four more cigars. */
.gl-doors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0.9rem;
}
.gl-doors[hidden] { display: none; }

.gl-door {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: 1rem 1.05rem 1.1rem;
  border-radius: 12px;
  border: 1px solid var(--color-outline-variant);
  background: linear-gradient(155deg,
    var(--color-surface-container) 0%,
    var(--color-surface-container-low) 100%);
}

.gl-door-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
}
.gl-door-title {
  font-family: var(--font-headline);
  font-size: 1rem;
  font-weight: 600;
  color: var(--color-on-surface);
}
.gl-door-count {
  font-size: 0.66rem;
  color: var(--color-on-surface-variant);
  opacity: 0.75;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.gl-door-blurb {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--color-on-surface-variant);
  opacity: 0.85;
}

.gl-door-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.15rem;
}
/* The pill's whole appearance comes from .gs-quick-chip, which the markup
   now carries — pill, border, gold, the leaf swatch and the origin flag.
   This block is only the delta a doorway needs on top of it, so a restyle
   of the top row lands here without anyone remembering to copy it. */

/* Wrapping, not a scroll rail. .gs-quick-chip is authored for a single
   horizontal strip, so it ships flex-shrink:0 and a snap alignment that
   mean nothing inside a wrapped tile. */
.gl-door-pill {
  flex-shrink: 1;
  scroll-snap-align: none;
  min-width: 0;
}

/* No entrance animation here. .gs-quick-chip's gs-chip-in fades from
   opacity 0 with fill-mode `both`, so a renderer that never advances the
   animation would hold these pills invisible — the rule §8b states for
   the landing's own cards, applied to a component borrowed from a
   surface that does take that risk. The row can afford it above the
   fold; a section four screens down cannot. */
#gs-landing .gl-door-pill {
  animation: none;
}

/* The count the row hasn't room for. Subordinate to the label on purpose
   — it answers "how big is this room", it is not part of the name. */
.gl-door-pill-n {
  font-size: 0.62rem;
  font-weight: 500;
  opacity: 0.55;
  font-variant-numeric: tabular-nums;
}

/* Reads as an opening rather than a filter, because it is one: it opens
   the chip pane instead of narrowing anything. Dashed and unfilled says
   that before the label does. */
.gl-door-pill--more {
  border-style: dashed;
  color: var(--color-on-surface-variant);
  opacity: 0.9;
}
.gl-door-pill--more:hover,
.gl-door-pill--more:focus-visible {
  color: var(--gt-gold);
}

/* ── houses ───────────────────────────────────────────────
   Every brand in the catalog, not only the sixteen with a curated room
   under collection/. The room marker is the curation signal: a room is
   something a brand HAS, never something every brand has, so the mark
   has to be visible or the distinction the policy protects disappears
   into a uniform grid (CLAUDE.md §7). */
/* THREE EVEN COLUMNS, at every width. A wrapped row of pills treats 63
   houses as a tag cloud; these are the makers the shop chose to stock, and
   a grid gives each one the same footprint as the next rather than sizing
   it by how long its name happens to be.

   Held at three on a phone too, deliberately: the point of the grid is
   that every house is presented identically, and re-flowing to two under
   the fold would hand six of them a different footprint from the rest for
   no reason a visitor could see. The name is what absorbs the narrower
   column — see .gl-house-name's clamp and its wrapping rules. */
.gl-houses {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.5rem, 1.2vw, 0.85rem);
}
.gl-houses[hidden] { display: none; }

/* No photograph and no entrance animation — both by instruction. The card
   carries its weight through type and spacing alone, which is why the
   vertical padding is generous relative to the text: strip the image out
   of an editorial card and the whitespace has to do the work the image
   was doing, or what is left reads as a button. */
.gl-house {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 6.25rem;
  padding: clamp(0.85rem, 1.6vw, 1.15rem);
  border-radius: 10px;
  border: 1px solid var(--color-outline-variant);
  background: var(--color-surface-container-low);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.25s, background 0.25s, transform 0.25s;
  -webkit-tap-highlight-color: transparent;
}
.gl-house:hover,
.gl-house:focus-visible {
  border-color: rgba(212, 160, 23, 0.5);
  background: var(--color-surface-container);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .gl-house:hover,
  .gl-house:focus-visible { transform: none; }
}

/* The collection grid's own brand treatment, at a third of the scale:
   same face, same amber. `balance` keeps a three-word house from leaving
   one word alone on a second line, and the break rules are what let a
   column this narrow hold "ACID by Drew Estate" without overflowing. */
.gl-house-name {
  font-family: var(--font-headline);
  font-size: clamp(0.8rem, 1.5vw, 1.05rem);
  line-height: 1.25;
  color: #fef3c7;
  text-wrap: balance;
  overflow-wrap: anywhere;
  hyphens: auto;
}

/* Echoes .collection-card-line — the gold, letterspaced caps that sit
   under every brand name on the collection page. */
.gl-house-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.gl-house-count {
  font-family: var(--font-body);
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-primary);
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The curation signal (CLAUDE.md §7/§9). A room is something a house HAS,
   never something every house has, so the 16 that have one are marked and
   the card behind the mark is warmed — remove this and the distinction the
   policy exists to protect disappears into a uniform grid. */
.gl-house--room {
  border-color: rgba(212, 160, 23, 0.3);
  background: linear-gradient(155deg,
    var(--color-surface-container) 0%,
    var(--color-surface-container-low) 100%);
}
.gl-house-room {
  font-family: var(--font-label);
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
  border: 1px solid rgba(212, 160, 23, 0.35);
  border-radius: 999px;
  padding: 0.1rem 0.35rem;
  white-space: nowrap;
}

/* ── the tail: browse everything ──────────────────────── */
.gl-sec--tail {
  border-top: 1px dashed var(--color-outline-variant);
  padding-top: 1.4rem;
}
.gl-all-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.gl-all-btn {
  font-family: var(--font-label);
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
  background: transparent;
  border: 1px solid rgba(212, 160, 23, 0.45);
  border-radius: 999px;
  padding: 0.5rem 1.1rem;
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
  -webkit-tap-highlight-color: transparent;
}
.gl-all-btn:hover,
.gl-all-btn:focus-visible {
  background: rgba(212, 160, 23, 0.12);
  color: var(--color-on-surface);
}

/* Visually hidden, announced. collection.html loads neither product.css
   nor admin.css, which is where the site's other .sr-only definitions
   live, so the landing carries its own under its own name rather than
   redefining a shared utility this page has never had. */
.gl-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}


/* ══════════════════════════════════════════════════════════════════
   §9  STATIC CATALOG INDEX  —  the crawlable copy of the humidor
   Written by scripts/generate-collection-index.js between sentinels in
   collection.html. Ships VISIBLE, outside #gs-landing, because that
   wrapper carries [hidden] and text extractors drop hidden subtrees —
   which is the whole reason this section exists. renderResults() hides
   it the moment a search narrows, exactly as it hides the landing.
   ══════════════════════════════════════════════════════════════════ */
.gs-index {
  display: block;
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px dashed var(--color-outline-variant);
}
/* §9's rule: any element a class gives a display to must restate this,
   or [hidden] silently stops working. */
.gs-index[hidden] { display: none; }

.gs-index-head { margin-bottom: 1.75rem; }

/* ── the disclosure ───────────────────────────────────────────────
   Emitted <details open>; js/fuse-search.js closes it at <=699px only.
   The summary is therefore invisible-by-irrelevance on desktop — it is
   still there, and still the control, but it is showing an already-open
   section. On a phone it is the 16.9 screens' worth of off-switch. */
.gs-index-disclosure { margin: 0; }

.gs-index-summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  list-style: none;
  padding: 0.6rem 0.85rem;
  margin-bottom: 1.25rem;
  border: 1px solid var(--color-outline-variant);
  border-radius: 0.5rem;
  background: var(--color-surface-container-low, rgba(255, 255, 255, 0.03));
  transition: border-color 0.2s ease, background 0.2s ease;
}
/* Safari still paints its own triangle through list-style. */
.gs-index-summary::-webkit-details-marker { display: none; }

.gs-index-summary:hover,
.gs-index-summary:focus-visible {
  border-color: rgba(255, 183, 125, 0.4);
  background: rgba(255, 183, 125, 0.06);
}

.gs-index-summary-label {
  font-family: var(--font-label, var(--font-body));
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-primary);
}

.gs-index-summary-count {
  font-size: 0.75rem;
  color: var(--color-on-surface-variant);
  margin-left: auto;
}

/* The chevron is drawn rather than set as a Material ligature: an icon font
   inside a <summary> is exactly the raw-ligature-as-link-text case CLAUDE.md
   §5 shields against, and a summary IS the element's accessible name. */
.gs-index-summary::after {
  content: '';
  width: 0.45rem;
  height: 0.45rem;
  border-right: 1.5px solid var(--color-primary);
  border-bottom: 1.5px solid var(--color-primary);
  transform: rotate(45deg) translate(-0.1rem, -0.1rem);
  transition: transform 0.25s ease;
  flex: none;
}
.gs-index-disclosure[open] .gs-index-summary::after {
  transform: rotate(-135deg) translate(-0.15rem, -0.15rem);
}
@media (prefers-reduced-motion: reduce) {
  .gs-index-summary,
  .gs-index-summary::after { transition: none; }
}

.gs-index-title {
  font-size: clamp(1.15rem, 3.4vw, 1.5rem);
  color: var(--color-primary);
  letter-spacing: 0.01em;
  margin: 0 0 0.5rem;
}

.gs-index-sub {
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--color-on-surface-variant);
  max-width: 44rem;
  margin: 0;
}
.gs-index-sub a { color: var(--color-primary); white-space: nowrap; }

/* Columns on the CONTAINER, so whole brand blocks flow between them and a
   house keeps its heading with its cigars. break-inside on the block is what
   stops a column break landing between a brand name and its first vitola. */
.gs-index-cols {
  columns: 1;
  column-gap: 2.5rem;
}
.gs-index-brand {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
  margin: 0 0 1.5rem;
}

.gs-index-brand-name {
  font-family: var(--font-label, var(--font-body));
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-primary-fixed-dim);
  margin: 0 0 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.gs-index-brand-name a { color: inherit; text-decoration: none; }
.gs-index-brand-name a:hover,
.gs-index-brand-name a:focus-visible { color: var(--color-primary); text-decoration: underline; }

/* The curation marker §9 requires stay visible: 16 of the 63 houses have a
   room of their own, and without the marker the grid claims they are all
   the same thing. Mirrors the .gl-house-room chip on the landing. */
.gs-index-room {
  font-family: var(--font-label);
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gt-gold, #D4A017);
  border: 1px solid rgba(212, 160, 23, 0.35);
  border-radius: 999px;
  padding: 0.1rem 0.35rem;
  white-space: nowrap;
}

.gs-index-list { list-style: none; margin: 0; padding: 0; }
.gs-index-list li {
  font-size: 0.875rem;
  line-height: 1.5;
  padding: 0.25rem 0;
}
.gs-index-list a {
  color: var(--color-on-surface);
  text-decoration: none;
}
.gs-index-list a:hover,
.gs-index-list a:focus-visible { color: var(--color-primary); text-decoration: underline; }

/* The spec sits on its own line under the name rather than beside it: at
   phone width "Connecticut Broadleaf Maduro · Medium-Full" beside a long
   vitola name wraps into an unreadable ladder. */
.gs-index-spec {
  display: block;
  font-size: 0.75rem;
  color: var(--color-on-surface-variant);
  opacity: 0.8;
}

@media (min-width: 700px)  { .gs-index-cols { columns: 2; } }
@media (min-width: 1100px) { .gs-index-cols { columns: 3; } }

/* 283 two-line entries is 20.8 screens at 375px, which is most of what the
   browse landing exists to have removed (§9: 187 screens down to 5.0). The
   index is the LAST thing before the footer, so nobody scrolls through it to
   reach anything — but they do scroll past it, so the phone treatment is
   tightened to claw back roughly a fifth of that. It is not enough to make
   the section short; it is enough to make it cheaper to pass. */
@media (max-width: 699px) {
  .gs-index-list li { padding: 0.15rem 0; line-height: 1.35; }
  .gs-index-spec { font-size: 0.6875rem; line-height: 1.3; }
  .gs-index-brand { margin-bottom: 1.1rem; }
}
