/* ============================================
   Filterable card listing — shared chrome.

   Page-specific bits:
     - .gs-listing-section   Card grid band on the muted bg. Holds the
                            filter bar + the grid. Single page (no
                            pagination); client-side JS hides cards
                            that don't match the current filter state.
     - .gs-listing-filters   Filter bar — three pill-shaped <select>s
                            on the left + a search input on the right.
     - .gs-listing-grid /
       .gs-listing-card       3-up card grid (image + label + title +
                            description). Lazy images so the full 59
                            cards don't all fetch at once.

   Filter state is driven by data-* attributes on each card:
       data-listing-type, data-listing-language, data-listing-year
   matched against the form's current values in js/modules/listing-filter.js.
   ============================================ */

/* ─── Filter bar ─────────────────────────────────────────── */

/* Full-width filter bar wrapper. Sits flush below the dark hero and
   above the grid. Slim — just enough vertical padding to breathe;
   nothing pill-shaped. The bar's white bg + bottom hairline carry
   the visual separation from the muted grid section below.

   Previously this was `position: sticky` so it pinned below the
   persistent header chrome on scroll. Now non-sticky — the chrome
   stack (announcement bar + sticky header) already occupies
   meaningful vertical real estate on its own; layering yet another
   persistent strip below it on long listing pages crowded the
   viewport. The filter is still high enough on the page that users
   encounter it before scrolling into the grid. */
.gs-listing-filterbar {
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  /* Horizontal padding lives on the OUTER so it mirrors the header's
     layout (.gs-header has padding-x, .gs-header__inner only has
     max-width + auto margin). Putting padding-x on the inner would
     inset the controls 24px deeper than the header logo at wide
     viewports, breaking alignment. */
  padding: 0 1.5rem;
}

/* Inner container — same max-width as the header so the leftmost
   filter dropdown lines up exactly under the logo (and the rightmost
   search end-aligns with the CTA). Vertical padding only — horizontal
   spacing comes from the outer. */
.gs-listing-filterbar__inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 1.25rem 0;
}

/* The form inside the bar — no chrome now, just a flex row of the
   inner controls. Container styling lives on .gs-listing-filterbar. */
.gs-listing-filters {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* Drop the muted/white card section's default top padding so the
   sticky bar sits flush against the cards beneath it. Vertical
   padding stays on the bottom so the section still breathes before
   the page footer or next section. */
.gs-listing-section {
  padding-top: 2rem;
}

/* Right-align the search field — dropdowns hug the left, search
   pushes to the far end of the row. `margin-left: auto` on the
   search wrapper gives us that split without a separate column. */
.gs-listing-filters__search {
  margin-left: auto;
  position: relative;
  flex: 0 1 320px;
}

/* ─── Custom dropdown ────────────────────────────────────────
   Native <select> open state is browser-controlled (system menu),
   which fights the page's visual register. So each filter is a
   button + popover listbox that we own end-to-end:

     .gs-listing-filters__field    Wrapper, position:relative so the
                                  menu can anchor to it.
     .gs-listing-filters__btn      Pill button — closed state.
     .gs-listing-filters__menu     Floating panel below the button —
                                  open state. Toggled via [hidden].
     .gs-listing-filters__option   One row in the menu.

   A hidden <input> per field carries the actual value so the form
   data model stays the same as a native <select> for the JS filter
   module. ──────────────────────────────────────────────────── */

.gs-listing-filters__field {
  position: relative;
}

.gs-listing-filters__btn {
  appearance: none;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--color-fg);
  background-color: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0.65rem 2.5rem 0.65rem 1.25rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
  position: relative;
  min-width: 9rem;
  text-align: left;
}

/* Chevron sits in the padding-right gap; rotates 180° when open. */
.gs-listing-filters__btn-chev {
  position: absolute;
  right: 1rem;
  top: 50%;
  width: 12px;
  height: 12px;
  transform: translateY(-50%);
  transition: transform 0.2s ease;
  pointer-events: none;
  color: var(--color-fg);
}

.gs-listing-filters__btn[aria-expanded="true"] .gs-listing-filters__btn-chev {
  transform: translateY(-50%) rotate(180deg);
}

.gs-listing-filters__btn:hover,
.gs-listing-filters__btn[aria-expanded="true"] {
  border-color: var(--color-brand-accent);
}

.gs-listing-filters__btn:focus-visible {
  outline: none;
  border-color: var(--color-brand-accent);
  box-shadow: 0 0 0 3px rgba(255, 119, 0, 0.18);
}

/* Active-filter style — when a value is selected (not the empty
   placeholder), `.is-filtered` class is added by JS. Coral fg +
   border tells the user "this dropdown is contributing to the
   current result set" without needing a separate chip row. */
.gs-listing-filters__btn.is-filtered {
  border-color: var(--color-brand-accent);
  color: var(--color-brand-accent);
}
.gs-listing-filters__btn.is-filtered .gs-listing-filters__btn-chev {
  color: var(--color-brand-accent);
}

/* Menu — floats below the button. Higher z-index than card hover
   shadows. Padding is on the <ul>; row hover is on the option. */
.gs-listing-filters__menu {
  list-style: none;
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  min-width: 100%;
  margin: 0;
  padding: 0.5rem;
  background: var(--color-bg);
  border-radius: 16px;
  box-shadow: 0 12px 32px rgba(0, 27, 49, 0.12);
  z-index: 20;
  /* Long facet lists (year, with 9 entries) get a scroll cap so the
     menu doesn't run off the section bottom. */
  max-height: 18rem;
  overflow-y: auto;
}

.gs-listing-filters__menu[hidden] {
  display: none;
}

.gs-listing-filters__option {
  padding: 0.5rem 1rem;
  border-radius: 8px;
  font-size: var(--text-sm);
  color: var(--color-fg);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* The blog listing's options are links — they navigate to HubSpot's own
   /blog/topic/<slug> and /blog/archive/<year> URLs instead of driving a
   client-side filter (see blog-filters.js for why). An inline <a>
   ignores the vertical padding above and its hover fill stops at the
   text, so the rows render visibly tighter than the events menu they're
   meant to match. Block-level fixes both; the underline reset stops the
   global link style showing through. */
a.gs-listing-filters__option {
  display: block;
  text-decoration: none;
}

.gs-listing-filters__option:hover {
  background: var(--color-bg-muted);
}

.gs-listing-filters__option[aria-selected="true"] {
  color: var(--color-brand-accent);
  font-weight: var(--weight-bold);
}

/* Search input — same pill shape, with a search icon on the right. */
.gs-listing-filters__search-input {
  width: 100%;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--color-fg);
  background-color: var(--color-bg);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2386929f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><line x1='20' y1='20' x2='16' y2='16'/></svg>");
  background-repeat: no-repeat;
  background-position: right 1rem center;
  background-size: 16px 16px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 0.65rem 2.5rem 0.65rem 1.25rem;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.gs-listing-filters__search-input:hover {
  border-color: var(--color-brand-accent);
}

.gs-listing-filters__search-input:focus {
  outline: none;
  border-color: var(--color-brand-accent);
  box-shadow: 0 0 0 3px rgba(255, 119, 0, 0.18);
}

.gs-listing-filters__search-input::placeholder {
  color: var(--color-fg-muted, #86929f);
}

/* ─── Card grid ──────────────────────────────────────────── */

.gs-listing-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

/* Single card — white tile with rounded corners, image on top,
   body below. Hover lifts the whole card and deepens the shadow
   (same recipe as .gs-learn-blog-card). */
.gs-listing-card {
  background: var(--color-bg);
  border: 0;
  border-radius: 20px;
  overflow: hidden;
  transition:
    box-shadow 0.3s ease,
    transform 0.3s ease;
}

.gs-listing-card:hover {
  transform: translateY(-6px);
  box-shadow: 0 12px 24px rgba(0, 38, 69, 0.1);
}

/* JS sets this when the card doesn't match the current filters.
   Display:none drops it out of the grid entirely so remaining
   cards reflow into the leading slots — feels like a fresh result
   set, not a sieve with gaps. */
.gs-listing-card.is-hidden {
  display: none;
}

.gs-listing-card__link {
  display: flex;
  flex-direction: column;
  height: 100%;
  text-decoration: none;
  color: inherit;
}

.gs-listing-card__media {
  aspect-ratio: 1200 / 627;
  overflow: hidden;
  background: #f0f1f4;
}

.gs-listing-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gs-listing-card__body {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.75rem 1.75rem 1.25rem;
  flex: 1;
}

/* Coral uppercase eyebrow — "CONFERENCE | JUNE 30, 2026" */
.gs-listing-card__label {
  font-size: 0.8125rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-brand-accent);
  margin: 0;
}

.gs-listing-card__title {
  font-size: var(--text-h5);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  line-height: var(--leading-normal);
  margin: 0;
  text-wrap: pretty;
}

.gs-listing-card__desc {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-fg-body);
  margin: 0;
  text-wrap: pretty;
}

/* When a listing has no per-card description (e.g. webinars, whose
   source data has `desc: ""`), the JS-rendered cards still emit a
   __desc <p> from the universal renderCard template. Hide empty
   ones so they don't allocate a row of flex-gap. Server-rendered
   cards just omit the element entirely, so this rule only matters
   for the lazily-hydrated batch. */
.gs-listing-card__desc:empty {
  display: none;
}

/* Category chip + date/read-time footer row - shared between the
   blog listing page and the "You May Also Like" related-posts cards
   (post.html), not just one or the other. */
.gs-listing-card__chip {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  padding: 0.3rem 0.75rem;
  border-radius: 999px;
  background: var(--gradient-cta);
  color: #fff;
  /* Below the type scale's floor (--text-sm, 14px) on purpose: at 14px
     a long topic like "Cost Optimization in Kubernetes" competes with
     the card title it sits above. A raw value because the scale stops
     at --text-sm and tokens.css is off-limits; 0.8125rem sits one step under it. */
  font-size: 0.8125rem;
  font-weight: var(--weight-medium);
}

.gs-listing-card__meta {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding-top: 1.25rem;
  margin-top: auto;
  border-top: 1px solid var(--color-border);
  font-size: var(--text-sm);
  color: var(--color-fg-muted);
}
/* Separator dot between the date and read time, not a second gap -
   ::before on the second item only, matching the established
   date-·-readtime convention already used as static copy on the
   learn pages (gs-learn-blog-card__meta). */
.gs-listing-card__readtime::before {
  content: '·';
  margin-right: 0.75rem;
}

/* ─── Show-more pagination ───────────────────────────────── */

/* Sits below the grid, centered. JS toggles the button's `hidden`
   attribute — wrapper is always in the layout so the grid doesn't
   reflow when the button appears/disappears. */
.gs-listing-load-more-wrap {
  display: flex;
  justify-content: center;
  margin-top: 2.5rem;
}

.gs-listing-load-more[hidden] {
  display: none;
}

/* ─── Empty state ────────────────────────────────────────── */

/* JS toggles this when no cards match the current filters. Sits
   inside the grid container, full-width across the columns so it
   reads as "the whole result set is empty" rather than "first
   slot is empty". */
.gs-listing-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 4rem 1.5rem;
  color: var(--color-fg-body);
  font-size: var(--text-base);
  display: none;
}

.gs-listing-empty.is-visible {
  display: block;
}

/* ─── Responsive ─────────────────────────────────────────── */

@media (max-width: 900px) {
  .gs-listing-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 1.25rem;
  }

  /* Two-row filter bar on tablet/mobile: three dropdowns share row 1
     evenly, search input takes row 2 at full width. Smaller font on
     buttons + search input so the three dropdowns fit comfortably
     even on narrower phones. `flex-wrap` does the row split — the
     dropdowns flex:1 to fill row 1, search flex-basis:100% to break
     onto its own line. */
  .gs-listing-filterbar {
    padding: 0 1rem;
  }
  .gs-listing-filterbar__inner {
    padding: 0.9rem 0;
  }
  .gs-listing-filters {
    gap: 0.5rem;
  }
  .gs-listing-filters__field {
    flex: 1 1 0;
    min-width: 0;
  }
  .gs-listing-filters__btn {
    width: 100%;
    min-width: 0;
    font-size: 0.8125rem;
    padding: 0.55rem 2rem 0.55rem 0.9rem;
  }
  .gs-listing-filters__search {
    flex: 1 1 100%;
    margin-left: 0;
  }
  .gs-listing-filters__search-input {
    font-size: 0.8125rem;
    padding: 0.55rem 2.25rem 0.55rem 1rem;
  }
}

@media (max-width: 600px) {
  .gs-listing-grid {
    grid-template-columns: 1fr;
    max-width: 480px;
    margin: 0 auto;
  }
}

/* ─── Blog listing filter bar ─────────────────────────────── */

/* One dropdown instead of the events page's three, so the trigger is
   given a floor width rather than shrink-wrapping its label. Without
   it a single short control sits marooned at the left of a full-width
   bar and reads as unfinished. */
.gs-blog-filters .gs-listing-filters__field {
  min-width: 15rem;
}

.gs-blog-filters .gs-listing-filters__btn {
  width: 100%;
  justify-content: space-between;
}

/* ─── Blog search typeahead ───────────────────────────────── */

.gs-blog-search {
  position: relative;
}

.gs-blog-search__results {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  width: min(30rem, 90vw);
  /* Scrolls now that there are 12 hits rather than 5. The cap is a
     whole number of rows short of the content so a partial row is
     always visible at the bottom edge - that peek is what reads as
     "there is more", where a clean cut reads as the end of the list. */
  max-height: 32rem;
  overflow-y: auto;
  padding: 0.75rem;
  background: var(--color-bg);
  border-radius: 16px;
  /* Same elevation as the filter dropdown so the two panels read as
     one family rather than two different popovers. */
  box-shadow: 0 12px 32px rgba(0, 27, 49, 0.12);
  z-index: 20;
  text-align: left;
}

.gs-blog-search__results[hidden] {
  display: none;
}

.gs-blog-search__hit {
  display: block;
  padding: 1.25rem 1.125rem;
  border-radius: 8px;
  text-decoration: none;
  color: var(--color-fg);
  transition: background-color 0.15s ease;
}

/* Hairline between hits so a long list reads as separate rows rather
   than one block of text. Not on the last one, and not visible under
   the hover fill's rounded corners. */
.gs-blog-search__hit + .gs-blog-search__hit {
  border-top: 1px solid var(--color-border);
}

.gs-blog-search__hit:hover {
  background: var(--color-bg-muted);
}

.gs-blog-search__hit-title {
  display: block;
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  line-height: 1.35;
}

.gs-blog-search__hit-desc {
  display: block;
  margin-top: 0.5rem;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--color-fg-muted);
  /* Descriptions come back long; two lines is enough to confirm the
     match without the panel turning into a wall of text. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* HubSpot returns the matched words already wrapped - blog-search.js
   converts its span to <mark>. Coral rather than the browser's yellow. */
.gs-blog-search__hit mark {
  background: none;
  color: var(--color-brand-accent);
  font-weight: inherit;
}

.gs-blog-search__empty {
  margin: 0;
  padding: 0.875rem;
  font-size: var(--text-sm);
  color: var(--color-fg-muted);
}
