/* ============================================
   Blog — single post + listing.

   Two component groups:
     - .gs-blog-post / .gs-blog-post__*  Single post layout
                                          (hero + body + cover).
     - .gs-blog-body                      Body typography scope.
                                          Covers every rich-text
                                          element a HubSpot blog
                                          editor can produce —
                                          headings, paragraphs,
                                          lists, blockquotes, code
                                          blocks, inline code,
                                          images, tables, links.
                                          Confidence-styled so the
                                          editor's inline-style
                                          cruft gets overridden.

   The blog index/listing chrome reuses .gs-listing-* from listing.css
   (same component pattern as events/webinars).
   ============================================ */

/* ─── Single-post layout ────────────────────────────────────── */

.gs-blog-post {
  /* Outer article wraps the hero + body. The closing CTA lives
     OUTSIDE this article so it spans the full section width. */
}

.gs-blog-post {
  background: var(--color-bg);
}

/* Hero — title + meta first, then cover image, all left-aligned at
   the same width as the post body below. Top padding on the layout
   wrapper (below) clears the static header (which sits in normal
   flow above it) plus adds breathing room before the title. */
.gs-blog-post__intro {
  /* Explicit width, not just max-width — as a grid item, some
     older posts' title/meta content was sizing to fit-content
     instead of stretching to the grid track (repro'd on GitOps and
     Grafana test posts specifically; root mechanism not pinned
     down, but an explicit width reliably fixes it regardless). */
  width: 100%;
  max-width: var(--content-narrow);
  margin: 0 auto 2rem;
  padding: 0 1.5rem;
}

.gs-blog-post__eyebrow {
  font-size: 0.8125rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-brand-accent);
  margin: 0 0 1rem;
}

.gs-blog-post__title {
  font-size: var(--text-h2);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  line-height: var(--leading-snug);
  margin: 0 0 1.5rem;
  text-wrap: balance;
}

.gs-blog-post__meta {
  font-size: 15px;
  color: var(--color-fg-body);
  margin: 0;
}

.gs-blog-post__author-avatar {
  display: inline-block;
  width: 30px;
  height: 30px;
  /* No border-radius/circle crop and no grey fill - the two team
     avatars this was built for (Team Honey Badger, The Team @ Giant
     Swarm) are their own pre-composed brand assets, not photos
     needing a circular frame. contain (not cover) still matters:
     both assets are wider/taller than their own visible mark, so
     cover would crop into them the same way it cropped the badger
     illustration before this was added. */
  object-fit: contain;
  vertical-align: middle;
  margin-right: 0.5rem;
}

/* Matches the blog-wide link convention (§ coral by default,
   underline on hover only) - not the sitewide inherit-color one. */
.gs-blog-post__meta a {
  color: var(--color-brand-accent);
  font-weight: 500;
  text-decoration: none;
}
.gs-blog-post__meta a:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Cover image — same width as the text column below it, follows
   title + meta (title, then meta, then image). */
.gs-blog-post__cover {
  width: 100%;
  max-width: var(--content-narrow);
  margin: 0 auto;
  padding: 0 1.5rem;
}

.gs-blog-post__cover img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: 16px;
}

/* ─── Sidebar layout — author card (left) + share/CTA (right) ──
   Only activates at genuinely wide viewports (--content-max
   itself, 1280px). Below that there isn't room for two 160px+
   rails alongside the 720px reading column without squeezing
   either into uselessness, so everything collapses to one stacked
   column instead — title/image/body first, then both rails below.
   Markup order is intro → cover → body → left rail → right rail
   (so the collapsed/mobile stack reads content-first); the grid
   below re-places each into its visual column explicitly via named
   areas, independent of that source order. Rails span all three
   content rows so they start flush with the title, not with body. */
.gs-blog-post__layout {
  position: relative;
  /* +3rem over --content-max, not the plain token - .gs-header's own
     max-width:content-max applies to a padding-FREE inner element
     (layouts/header.css's .gs-header__inner), with padding on a
     separate outer wrapper. This element has no such split - its own
     1.5rem each-side padding (desktop block below) is INSIDE this
     same max-width box (border-box sitewide), so content-max here
     would cap the *padding-inclusive* box 48px narrower than header's
     effective content width - matching exactly at the ~1280px
     boundary (confirmed OK there) but drifting wider apart the
     further past it the viewport goes (confirmed: 24px off at
     1600px, not the ~2.5px this looked like near the boundary).
     +3rem compensates for exactly that padding, restoring parity at
     every width, not just near the boundary - column-gap widens by
     the same 3rem below so the grid's own fixed-width columns still
     fill this exactly, with no leftover slack inside it. */
  max-width: calc(var(--content-max) + 3rem);
  margin: 0 auto;
  padding-top: 8rem;
}

.gs-blog-post__rail {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

@media (max-width: 1279px) {
  .gs-blog-post__rail {
    max-width: var(--content-narrow);
    margin: 0 auto;
    padding: 0 1.5rem;
  }
}

/* Tablet only: the TOC becomes a real sidebar column again (narrower
   than desktop's, sticky like desktop but without the hard-stop JS -
   scripts/blog-toc.js's width check gates that part off below 1280px,
   so it just relies on plain sticky here, coast and all). Intro/
   cover/body share the rest of the width in column 2. The right rail
   (CTA/share) lands in its own row directly below, in that SAME
   column 2 (not spanning both columns) so it lines up with the
   article text instead of centering under the whole viewport. */
@media (min-width: 768px) and (max-width: 1279px) {
  .gs-blog-post__layout {
    display: grid;
    grid-template-columns: 12rem minmax(0, 1fr);
    grid-template-areas:
      "left intro"
      "left cover"
      "left body";
    column-gap: 2rem;
    row-gap: 0;
    align-items: start;
    padding-left: 1.5rem;
    padding-right: 1.5rem;
  }
  .gs-blog-post__intro,
  .gs-blog-post__cover {
    padding: 0;
  }
  /* Grid items default to min-width: auto, which floors their size at
     their content's natural width - .gs-blog-body inside .gs-blog-
     post__body carries a fixed max-width (--content-narrow) that's
     wider than this column at some viewport widths in this range,
     so without min-width: 0 the column refuses to shrink and the
     text overflows past the viewport's right edge instead of
     wrapping/shrinking to fit. Confirmed by direct measurement
     (body stayed pinned at 635px wide at both 900px and 780px
     viewports) before this fix. */
  .gs-blog-post__intro,
  .gs-blog-post__cover,
  .gs-blog-post__body,
  .gs-blog-post__rail--left {
    min-width: 0;
  }
  .gs-blog-post__intro { grid-area: intro; }
  .gs-blog-post__cover { grid-area: cover; }
  .gs-blog-post__body  { grid-area: body; }
  /* Misdiagnosed on the first pass: .gs-blog-body's max-width +
     margin: 0 auto (unconditional) actually MATCHES intro/cover's own
     max-width: var(--content-narrow); margin: 0 auto (also
     unconditional, see .gs-blog-post__intro/__cover's base rules) -
     all three already share the same 720px centered box at this
     breakpoint. The real mismatch is .gs-blog-post__body's own
     padding: 2rem 1.5rem 6rem (unconditional, separate from
     .gs-blog-body, defined further down the file) - intro/cover get
     their padding zeroed above but body doesn't, so its visible text
     sat 1.5rem narrower on each side inside the same-width box.
     Matches desktop's own padding: 2rem 0 6rem override, just needed
     here too - but plain .gs-blog-post__body isn't enough on its own:
     same specificity as the unconditional base rule, which sits
     LATER in the file (the typography section, below), so it would
     win the tie regardless of this media query. Needs the
     class-doubled selector to actually take effect, not just the
     right value - confirmed the hard way, this exact value alone
     didn't change anything live. */
  .gs-blog-post__body.gs-blog-body {
    padding: 2rem 0 6rem;
  }
  .gs-blog-post__rail--left {
    grid-area: left;
    max-width: none;
    margin: 0;
    padding: 0;
    position: sticky;
    top: calc(var(--announcement-h, 0px) + 6rem);
  }
  /* No grid-area rule for .gs-blog-post__rail--right here - it's
     display: none at this breakpoint (see the Share/CTA inline-copy
     block below), replaced by literal inline copies inside
     .gs-blog-body that need no grid-area/width matching at all. */
}

/* Mobile only: the sticky sidebar TOC doesn't fit a mobile screen, so
   it's hidden entirely and .gs-blog-toc-mobile (below) - a collapsed
   dropdown right after the cover image - stands in for it instead.
   Right rail still stacks at the bottom via the shared
   max-width:1279px rule above. */
@media (max-width: 767px) {
  .gs-blog-post__rail--left {
    display: none;
  }
}

/* Mobile TOC dropdown - built by blog-toc.js from the same post H2s
   as the sidebar version, right down to reusing the same anchor ids.
   Hidden above 767px (the sidebar version takes over there) and
   whenever there are fewer than 2 H2s (same skip condition as the
   sidebar version - the script leaves [hidden] on in that case). */
.gs-blog-toc-mobile {
  display: none;
}
@media (max-width: 767px) {
  .gs-blog-toc-mobile {
    display: block;
  }
  /* Same fix as .gs-blog-toc[hidden] above, same root cause: the
     unconditional display:block right above this beats the browser
     default [hidden]{display:none} rule (author stylesheets always
     win over UA ones), so a headline-less post's sticky drawer
     showed anyway even though JS correctly left [hidden] on. */
  .gs-blog-toc-mobile[hidden] {
    display: none;
  }
  .gs-blog-toc-mobile {
    /* Fixed, full-width strip flush with the viewport's bottom edge -
       the same solid-white-bar-with-one-border treatment
       .gs-header-host--solid .gs-header uses (layouts/header.css),
       just facing the opposite edge (border-top here vs. that one's
       border-bottom) and at the opposite end of the viewport. No
       border-radius, no side margins, no shadow - a flush chrome
       strip, not a floating card. z-index 45 sits below the sticky
       header (50) and mobile menu (60), matching header.css's own
       numeric spacing, so an opened menu/search still correctly
       covers this. env(safe-area-inset-bottom) clears the iOS home-
       indicator gesture area. */
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 45;
    margin: 0;
    padding: 1rem 1.5rem;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
    background: var(--color-bg);
    border-top: 1px solid var(--color-border);
  }
}

.gs-blog-toc-mobile__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  list-style: none;
}
/* Removes the native marker in both marker-model browsers -
   ::marker (Firefox, modern Chrome) and ::-webkit-details-marker
   (Safari) don't overlap, both are needed. */
.gs-blog-toc-mobile__summary::marker,
.gs-blog-toc-mobile__summary::-webkit-details-marker {
  display: none;
}

.gs-blog-toc-mobile__chevron {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  color: var(--color-fg-muted);
}
/* Transform goes on the <path>, not the <svg> itself - confirmed live
   that this browser doesn't apply a CSS transform to an <svg> root
   element at all (identity matrix regardless of transform-box value,
   even set via maximum-specificity inline style), while the same
   transform on a child element inside it works exactly as expected.
   The chevron's own path already sits centered in its 24x24 viewBox
   (M6 9l6 6 6-6 spans x:6-18, y:9-15 - bounding-box center is (12,12),
   the viewBox's center too), so transform-origin: center rotates it
   in place with no visible shift. */
.gs-blog-toc-mobile__chevron path {
  transform-origin: center;
  transition: transform 0.2s ease;
}
.gs-blog-toc-mobile[open] .gs-blog-toc-mobile__chevron path {
  transform: rotate(180deg);
}

/* Positioned as its own panel directly ABOVE the bar (bottom: 100%
   of the fixed .gs-blog-toc-mobile, which this is absolute relative
   to), not below it in normal flow - below would run straight off
   the bottom of the screen, since the summary it's expanding from is
   already pinned to the viewport's bottom edge. Flush against the
   bar (no gap, no border-radius/shadow of its own) so open+closed
   read as one continuous white surface, matching the bar's own flat-
   strip treatment - a border-bottom on the list only, not a full
   border, is what actually separates it from the bar below. */
.gs-blog-toc-mobile__list {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  list-style: none;
  margin: 0;
  padding: 1rem 1.5rem;
  max-height: 50vh;
  overflow-y: auto;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Padding on each link, not just flex gap between them - a real tap
   target on mobile, not just a tight line of text with a sliver of
   gap between each. */
.gs-blog-toc-mobile__list a {
  display: block;
  padding: 0.625rem 0;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--color-fg-body);
  text-decoration: none;
}
.gs-blog-toc-mobile__list a:hover {
  color: var(--color-fg);
}

@media (min-width: 1280px) {
  .gs-blog-post__layout {
    /* Symmetric 1.5rem, matching .gs-header's own unconditional side
       padding (layouts/header.css) - below --content-max, a grid with
       one-sided padding left the right rail flush against the browser
       edge while the header a few hundred px above it kept its usual
       gutter, a visible mismatch once someone scaled down toward the
       tablet breakpoint and watched it happen.
       column-gap is 4.5rem, not the plain 3rem this was originally -
       .gs-blog-post__layout's own max-width is now content-max + 3rem
       (see its base rule) to match .gs-header's gutter at every
       width, not just near the --content-max boundary. That widened
       box needs the grid's own fixed-width columns to fill exactly
       3rem more than before, or the extra space shows up as a real
       gap on the right instead of the right rail sitting flush -
       confirmed algebraically: 13rem + 4.5rem + content-narrow(720px)
       + 4.5rem + 13rem + this rule's own 1.5rem*2 padding sums to
       exactly content-max + 3rem, with zero leftover slack. */
    padding-left: 1.5rem;
    padding-right: 1.5rem;
    display: grid;
    grid-template-columns: minmax(160px, 13rem) minmax(0, var(--content-narrow)) minmax(160px, 13rem);
    grid-template-areas:
      "left  intro  right"
      "left  cover  right"
      "left  body   right";
    column-gap: 4.5rem;
    row-gap: 0;
    align-items: start;
  }
  /* Zero the horizontal padding these carry for the collapsed
     single-column case — the grid column width now defines their
     inset instead, matching .gs-blog-post__body's own override
     below (keeps image/title/meta/body flush at the same x). */
  .gs-blog-post__intro,
  .gs-blog-post__cover {
    padding: 0;
  }
  .gs-blog-post__intro { grid-area: intro; }
  .gs-blog-post__cover { grid-area: cover; }
  .gs-blog-post__body  { grid-area: body; }
  .gs-blog-post__rail--left  { grid-area: left; }
  .gs-blog-post__rail--right { grid-area: right; }
  .gs-blog-post__rail {
    /* Sticks while the (usually much taller) article body scrolls.
       Top offset matches the same math as the header's own
       clearance value elsewhere in the catalog: announcement bar +
       header height + breathing room. */
    position: sticky;
    top: calc(var(--announcement-h, 0px) + 6rem);
    /* Same 6rem as .gs-blog-post__body's own bottom padding, so the
       rail leaves the same trailing gap before the next section
       instead of ending flush against it. */
    padding-bottom: 6rem;
  }
}

/* Table of contents (left rail) — built at runtime from the post's
   own H2s (js/modules/blog-toc.js). Starts [hidden]; the script
   only reveals it once there are ≥2 headings to list, since a
   single-section post has nothing worth a contents card.
   No box, no background, no pills — plain text on the page, a thin
   guide-rule down the left edge, and the currently-read section
   marked by coral text + a coral segment of that rule (scroll-spy).
   One moving accent, nothing else competing with it.

   Long posts can have far more than fits in the sticky rail's own
   height (it can't pin past its own container edge, so a very tall
   list just gets clipped at the top or bottom depending on scroll
   position) - .gs-blog-toc__scroll caps the list's own height and
   scrolls internally instead, with up/down arrow buttons (JS toggles
   them on only when there's real overflow, and disables/hides each
   one at its respective end) rather than a visible native scrollbar -
   same mechanism as the capabilities-tabs rail (platform-
   capabilities.css), vertical instead of horizontal. */
.gs-blog-toc {
  display: flex;
  flex-direction: column;
}

/* The class's own display:flex otherwise beats the browser default
   [hidden]{display:none} rule (author stylesheets always win over
   UA stylesheets, regardless of selector specificity) - without this,
   a post with fewer than 2 h2s (JS leaves [hidden] in place, see
   blog-toc.js) still shows the bare "Table of contents" label with
   an empty list under it. */
.gs-blog-toc[hidden] {
  display: none;
}

.gs-blog-toc__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--color-fg-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 1.5rem;
}

/* Arrows sit in their own narrow column to the LEFT of the list
   (up top, down bottom), not as separate rows above/below it -
   plain coral icons, no circle/border. */
.gs-blog-toc__body {
  display: flex;
  flex-direction: row;
  gap: 0.5rem;
}

.gs-blog-toc__arrows {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  flex: 0 0 auto;
}

.gs-blog-toc__scroll {
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  overflow-x: hidden;
  max-height: 13rem;
  scrollbar-width: none;
}

.gs-blog-toc__scroll::-webkit-scrollbar {
  display: none;
}

.gs-blog-toc__arrow {
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color-brand-accent);
  cursor: pointer;
}

.gs-blog-toc__arrow svg {
  width: 14px;
  height: 14px;
}

.gs-blog-toc__arrow:hover {
  opacity: 0.7;
}

.gs-blog-toc__arrow[disabled] {
  opacity: 0;
  pointer-events: none;
}

/* Shown only when the list actually overflows (JS-toggled) - a short
   TOC that already fits never gets arrows. */
.gs-blog-toc--overflow .gs-blog-toc__arrow {
  display: inline-flex;
}

.gs-blog-toc__list {
  list-style: none;
  margin: 0;
  padding: 0 0 0 1rem;
  border-left: 1.5px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.gs-blog-toc__list a {
  display: block;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--color-fg-muted);
  text-decoration: none;
  transition: color 0.15s ease;
}

.gs-blog-toc__list a:hover {
  color: var(--color-fg);
}

.gs-blog-toc__list a.is-active {
  color: var(--color-brand-accent);
  font-weight: var(--weight-medium);
  /* Redraws just this item's segment of the shared guide-rule
     coral (negative-margin trick to reach back over the <ol>'s own
     border-left) — the highlight travels with scroll-spy instead
     of recoloring the whole rail. */
  margin-left: -1.08rem;
  padding-left: calc(1rem - 1.5px);
  border-left: 1.5px solid var(--color-brand-accent);
}

/* "More from this author" - always present in the left rail (unlike
   the TOC, which needs 2+ headings), stacked below it when both
   exist. Reuses .gs-blog-toc__label for the heading; the list here
   mirrors .gs-blog-toc__list's guide-rule/link styling but skips the
   scroll cap/arrows since it's always capped at a handful of items,
   never a full article's worth of headings. */
.gs-blog-author-posts {
  display: flex;
  flex-direction: column;
}

/* Exclusive fallback, not a permanent fixture - only shows when the
   TOC doesn't (fewer than 2 headings), never stacked below it. Same
   :not([hidden]) reasoning as the mobile-drawer fix above: the TOC
   nav stays in the DOM either way, just [hidden], and a plain +
   combinator matches DOM structure regardless of visibility. */
.gs-blog-toc:not([hidden]) + .gs-blog-author-posts {
  display: none;
}

/* Guide-rule is back (matches .gs-blog-toc__list), but plain and
   static - no coral "active" variant like the TOC's, since nothing
   here tracks scroll position. Numbered instead of icon-prefixed:
   a repeated identical icon added no information: three coral
   numbers read as "top picks" and cost nothing extra to author since
   they're a CSS counter, not per-item markup. */
.gs-blog-author-posts__list {
  list-style: none;
  counter-reset: author-posts;
  margin: 0;
  padding: 0 0 0 1rem;
  border-left: 1.5px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.gs-blog-author-posts__list li {
  counter-increment: author-posts;
}

.gs-blog-author-posts__list a {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--color-fg-muted);
  text-decoration: none;
  transition: color 0.15s ease;
}

.gs-blog-author-posts__list a::before {
  content: counter(author-posts) ".";
  flex: 0 0 auto;
  color: var(--color-brand-accent);
  font-weight: var(--weight-medium);
}

.gs-blog-author-posts__list a:hover {
  color: var(--color-fg);
}

/* Share block (right rail, top) — same circular-icon convention as
   the footer socials (currentColor SVG, coral + lighter bg on
   hover), scoped separately since these are share-intent links,
   not the company's own social profiles. */
.gs-blog-share {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.gs-blog-share__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--color-fg-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0;
}

.gs-blog-share__icons {
  display: flex;
  gap: 0.5rem;
}

.gs-blog-share__icons a {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid #d0d0d0;
  color: var(--color-fg);
  transition: color 0.15s ease;
}

.gs-blog-share__icons a:hover {
  color: var(--color-brand-accent);
}

.gs-blog-share__icons svg {
  width: 16px;
  height: 16px;
  display: block;
}

/* CTA card (right rail, bottom) — not a solid-fill gradient. The
   gradient is reserved for the single most important action on a
   view (color-system rule); that's the closing CTA at the end of
   the article, not this persistent sidebar card that's visible
   through the whole scroll. See the "glow card" comment below for
   how it stays visually distinct without a loud fill. */
/* Inline copies of both Share and the CTA card live inside .gs-blog-body
   itself (see post.html) so they're guaranteed to share the article
   text's own box/padding below 1280px - no CSS math to keep them aligned
   with the text column, unlike the aside version (1280px+ only, its own
   real sidebar column). Hidden by default, shown only below 1280px; the
   whole aside is hidden there instead, so exactly one set is ever
   visible - not just its two children individually, since both of the
   aside's children now have an inline counterpart. */
.gs-blog-share--inline,
.gs-blog-sidebar-cta--inline {
  display: none;
}
@media (max-width: 1279px) {
  .gs-blog-share--inline {
    display: flex;
  }
  .gs-blog-sidebar-cta--inline {
    display: block;
  }
  .gs-blog-post__rail--right {
    display: none;
  }
}

/* Both of these need a class-doubled selector (.gs-blog-body .foo, not
   just .foo) to reliably outrank same-specificity base rules regardless
   of source order - margin-top:10px on .gs-blog-sidebar-cta (written for
   sitting in the aside's own flex gap, not needed as its own spacing
   here) and margin:0 0 1.25rem on .gs-blog-body p (gs-blog-share__label
   is a <p>, picking up the article's own paragraph spacing now that
   it's a genuine descendant, on top of its own margin:0). */
.gs-blog-body .gs-blog-sidebar-cta--inline {
  margin-top: 3rem;
}
.gs-blog-body .gs-blog-share__label {
  margin: 0;
}
/* Centered, below the CTA card - simpler than replicating the aside's
   own label-above-icons layout/spacing inline. */
.gs-blog-share--inline {
  align-items: center;
  margin-top: 2rem;
}

/* .gs-blog-body a's persistent-coral link convention (below) would
   otherwise repaint these two - both are <a> elements, and this is now
   a genuine descendant of .gs-blog-body once inlined - overriding their
   own deliberate styling (dark-outlined button matching desktop per
   PATTERNS.md's "tried coral, visually rejected" note; neutral-grey
   icon circles). Extra class in each selector outranks the single-class
   `.gs-blog-body a` rule regardless of source order. */
.gs-blog-body a.gs-blog-sidebar-cta__btn,
.gs-blog-body a.gs-blog-sidebar-cta__btn:hover {
  color: inherit;
  text-decoration: none;
}
.gs-blog-body .gs-blog-share__icons a {
  color: var(--color-fg);
}
.gs-blog-body .gs-blog-share__icons a:hover {
  color: var(--color-brand-accent);
  text-decoration: none;
}

.gs-blog-sidebar-cta {
  position: relative;
  margin-top: 10px;
  border-radius: 16px;
  padding: 1.75rem 1.75rem 1.75rem;
  color: var(--color-fg);
  /* Same logic as .gs-vs-approach (vs.css) — a light card, a solid
     coral border, and a small coral eyebrow badge overlapping the
     top edge as the "this is a highlighted moment" indicator,
     instead of leaning on the border color itself to carry that
     meaning. Replaces an earlier gradient-outline "glow card" pass
     that didn't land. */
  background: var(--color-bg);
  border: 2px solid var(--color-brand-accent);
  box-shadow: 0 8px 24px rgba(246, 89, 84, 0.12);
}

.gs-blog-sidebar-cta__eyebrow {
  position: absolute;
  top: -12px;
  left: 1.5rem;
  background: var(--color-brand-accent);
  color: #fff;
  font-size: 0.6875rem;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
}

.gs-blog-sidebar-cta__title {
  font-size: var(--text-h5);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  color: var(--color-fg);
  margin: 0.5rem 0 0.75rem;
}

.gs-blog-sidebar-cta__lede {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--color-fg-body);
  margin: 0 0 1.25rem;
}

/* .gs-blog-sidebar-cta__btn — sizing from .gs-btn--sm; plain
   .gs-btn--secondary (dark-outlined) is enough on its own now that
   the card's border/eyebrow already carry the coral accent. Full
   width so it spans the card - .gs-btn's justify-content: center
   keeps the label centered. */
.gs-blog-sidebar-cta__btn {
  width: 100%;
}

/* Body — narrow prose column for readability. Background stays
   white (matches the hero), so the article reads as one continuous
   page from title down to the closing CTA. */
.gs-blog-post__body {
  background: var(--color-bg);
  padding: 2rem 1.5rem 6rem;
}

@media (min-width: 1280px) {
  .gs-blog-post__body {
    padding: 2rem 0 6rem;
  }
}

/* ─── Blog body typography (the important part) ──────────────
   Scoped under .gs-blog-body so all body-prose rules ONLY apply
   inside the article body, not to other rich-text content on the
   page (homepage section copy, learn-content blocks, etc.). All
   selectors are confident enough to override HubSpot editor's
   inline styles — that's the whole point of the scope. */

.gs-blog-body {
  max-width: var(--content-narrow);
  margin: 0 auto;
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
  color: var(--color-fg-body);
}

/* Headings — clear hierarchy + generous top margin so sections
   chunk visually. H2 is the major break, H3 is the sub-step. */
.gs-blog-body h2 {
  /* Blog-only override, not the --text-h2 token: post section headings
     sit one level up from the old h3 (SEO — no skipped heading level
     under the post's h1) but should read at the same visual weight the
     h3 size had, not the much larger default h2 scale. */
  font-size: 1.5rem;
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  /* --leading-tight (1.15) read as cramped once a heading wraps to
     two lines - matches the mobile override just below, which
     already uses --leading-snug (1.3) for the same reason. */
  line-height: var(--leading-snug);
  margin: 0;
  padding: 1.5rem 0 1.5rem;
  /* Plain wrap, not balance - matches the mobile override below,
     which already moved off balance after it broke a short heading
     a word early. Same content type, same call either way. */
  text-wrap: wrap;
  /* Anchor target for TOC clicks - same mechanism as .gs-learn-anchor
     (learn-content.css), so native <a href="#id"> navigation lands
     below the fixed header on its own, animated by the sitewide
     scroll-behavior: smooth rule. Letting the browser drive this
     natively (not a JS-forced instant scrollTo) is what actually
     animates - see blog-toc.js for why the old click-intercept
     approach didn't.

     Flat 96px, NOT +var(--announcement-h) like the learn-page
     version: on this page the announcement bar isn't sticky (only
     `.gs-header-host`, ~66px, stays fixed while scrolling - measured
     directly, confirmed via getComputedStyle), so adding announcement
     height on top over-scrolled past the heading by ~50px, landing
     below the TOC's own scroll-spy trigger line and leaving the
     previous item highlighted. */
  scroll-margin-top: 96px;
}

.gs-blog-body h2:first-child {
  padding-top: 0;
}

/* Part label — a heading that introduces a GROUP of sections rather
   than a section of its own ("2024 predictions in review", sitting
   above "The 100% club", "Strong signals", …). As a plain h2 it read
   as a peer of the sections beneath it, which is what made an h2
   immediately followed by another h2 look wrong.

   Deliberately NOT restyled: same size, weight and colour as any other
   h2. The distinction is carried entirely by hairline rules above and
   below, which is enough to read as a divider without inventing a
   second heading voice. (Earlier passes tried an uppercase coral
   eyebrow and then a filled tinted band; both were dropped for being
   louder than the content under them.) The emoji lives in the post
   content, not here, since it is editorial rather than structural.

   Stays an <h2>: keeps the document outline honest and keeps the label
   in the runtime-built TOC.

   Qualified with .gs-blog-body h2 (0,2,1) so it beats the base h2 rule
   regardless of source order - see HUBSPOT-NOTES.md §28. */
.gs-blog-body h2.gs-blog-part {
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

/* Extra breathing room, applied per-instance rather than to every part
   label: a label that opens a genuinely new part of the article (the
   pivot from reviewing last year to forecasting the next) wants more
   separation than one that follows the intro. */
.gs-blog-body h2.gs-blog-part--spaced {
  margin-top: 30px;
  margin-bottom: 30px;
}

/* Standalone achievement banner - a badge/logo + headline in a dark
   rounded box, for calling out something like a certification within
   the body copy. One-off, not a base rule.

   Text/title use plain <div>s, not <p>/<h2> - .gs-blog-body already
   has strong opinions on those tags' margin/color/line-height (see
   the h2 img specificity note this replaces, one instance of the
   same problem already this session), and a scoped class here would
   still lose ties on shared properties since .gs-blog-body p/h2
   (0,1,1) outrank a single class (0,1,0) regardless of source order.
   Plain divs have no such competing rule to fight. */
.gs-blog-cert-banner {
  display: flex;
  align-items: center;
  gap: 2rem;
  /* Same dark-blue waves ground as .gs-cta (sections/cta.css) - the
     sitewide "this is a moment" treatment. Color stays underneath as
     a fallback so the box never renders as a light hole if the SVG
     is slow or blocked. */
  background-color: var(--color-bg-dark);
  background-image: url('https://430224.fs1.hubspotusercontent-eu1.net/hubfs/430224/Illustrations/gs23-dark-blue-waves-bg.svg');
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  border-radius: 16px;
  padding: 2rem;
  margin: 2rem 0;
}

/* Both image selectors are qualified with .gs-blog-body img (0,2,1)
   rather than the bare class (0,1,0) - .gs-blog-body img (0,1,1) sets
   border-radius: 12px and margin: 2rem auto for every body image and
   would otherwise win these outright regardless of source order,
   which is exactly why the earlier "remove the radius" pass looked
   like it did nothing. Third instance of this same trap today. */
.gs-blog-body img.gs-blog-cert-banner__badge {
  flex: 0 0 auto;
  width: 150px;
  height: auto;
  display: block;
  margin: 0;
  border-radius: 0;
}

.gs-blog-cert-banner__text {
  flex: 1 1 auto;
  min-width: 0;
}

/* 1.5rem matches .gs-blog-body h2's own size - a callout inside the
   article shouldn't out-shout the section heading above it. */
.gs-blog-cert-banner__title {
  font-size: 1.5rem;
  font-weight: var(--weight-bold);
  color: #ffffff;
  line-height: var(--leading-snug);
  margin: 0 0 1rem;
}

.gs-blog-cert-banner__quote {
  font-size: var(--text-sm);
  font-style: italic;
  color: rgba(255, 255, 255, 0.85);
  line-height: var(--leading-relaxed);
  margin: 0 0 1rem;
}

.gs-blog-cert-banner__author {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.gs-blog-body img.gs-blog-cert-banner__avatar {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: block;
  margin: 0;
  border-radius: 50%;
  object-fit: cover;
}

.gs-blog-cert-banner__attribution {
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.75);
  line-height: var(--leading-snug);
}

/* Qualified with .gs-blog-body (0,2,1) - .gs-blog-body strong further
   down this file is an exact specificity tie (0,1,1) and wins on
   source order alone, repainting the name in --color-fg (near-black)
   against the dark banner. Fourth instance of this trap today. */
.gs-blog-body .gs-blog-cert-banner__attribution strong {
  display: block;
  color: #ffffff;
  font-weight: var(--weight-bold);
  margin-bottom: 0.2rem;
}

@media (max-width: 600px) {
  .gs-blog-cert-banner {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1.5rem;
  }
  /* The author row is its own flex container, so text-align above
     doesn't move it - it needs its own main-axis centering. The
     cluster (avatar + name/title) centres as a unit, but the text
     inside it stays left-aligned so both lines line up against the
     avatar rather than centring independently of it. */
  .gs-blog-cert-banner__author {
    justify-content: center;
  }
  .gs-blog-cert-banner__attribution {
    text-align: left;
  }
}

/* FAQ accordion inside a post - the shared component (faq.css) is a
   white card with only a 1px border, which disappears against the
   post body's own white ground. Softer/lower than the house card
   HOVER elevation (0 12px 24px rgba(0,38,69,.1), used by
   .gs-listing-card et al) since this is a resting state, same navy
   hue rather than a neutral black.

   Deliberately scoped to .gs-blog-body (0,2,0) instead of touching
   .gs-faq itself: that component backs 10 live templates, and two of
   them (/developer-platforms/, /industrial-iot/) have the identical
   white-on-white blend. Marat's call 2026-08-12 was blog-only for
   now; promoting this to faq.css would fix those two as well. */
.gs-blog-body .gs-faq {
  box-shadow: 0 4px 16px rgba(0, 38, 69, 0.06);
}

/* Hardcoded like h2 above, and for the same reason. h2 was pulled down
   to 1.5rem so section headings keep the visual weight the old h3 had
   (see its comment), but the levels BELOW it were never re-based - h3
   stayed on --text-h3 (28px) and so rendered LARGER than the h2 above
   it. Inverted hierarchy, desktop only; the mobile block further down
   happened to avoid it by shifting h2 and h3 down together.

   It stayed invisible because cleaning rule 8 promotes every h3 to h2,
   so h3 barely survives into a migrated post - but it would bite any
   hand-written post immediately. Fixed 2026-08-12.

   Not a token: --text-h3 is 28px and there is no 20px step in the
   scale, so this sits between h2 (24px) and h4 (18px) explicitly. */
.gs-blog-body h3 {
  font-size: 1.25rem;
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  line-height: var(--leading-normal);
  margin: 2.5rem 0 1rem;
  text-wrap: balance;
}

.gs-blog-body h4 {
  font-size: var(--text-h5);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  margin: 2rem 0 0.75rem;
}

/* h5 had no blog-scoped rule at all, so it inherited the global scale
   and landed on 18px - identical to h4 above, making the two levels
   indistinguishable. Drops to body-text size; at this depth the
   heading reads as a heading through weight and colour rather than
   size. On mobile h4 is already 16px, so both bottom out together
   there - accepted, since going below body text for a heading would
   read as fine print. */
.gs-blog-body h5 {
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  margin: 1.75rem 0 0.5rem;
}

/* A heading directly followed by another heading (h2 then h4 for a
   numbered subsection, say) was stacking a lot of dead space: h2 uses
   PADDING rather than margin, so it can't margin-collapse with the
   heading after it - measured 56px between the two, against 12px from
   a heading to its own paragraph.

   Cutting the second heading's top margin down to a token amount
   leaves the first heading's own bottom spacing to do most of the
   separating: a subheading belongs to the heading above it, so it
   should sit closer to it than a new section would. Applies to any
   heading pair, since the same stacking happens at every level.

   0.5rem rather than 0: zeroing it read as slightly too tight once
   live, so this adds a small amount back - h2 to h4 lands at 32px
   total (24px of h2 padding + 8px), against the 56px it started at
   and the 12px a heading gets to its own paragraph.

   One exception, in the second selector: a .gs-blog-part label
   followed by a heading SMALLER than h2. The arithmetic above leans on
   the first heading's own bottom spacing doing most of the separating,
   and that stops holding for a part label - its 24px of bottom padding
   sits INSIDE its bottom border, so none of it lands between the
   border and what follows. A following h2 is fine either way, since it
   brings its own 24px of top padding; a smaller heading has only
   margin, so cutting that to 8px left the divider looking like it had
   lost its padding. Those keep their own margin (32px for an h4).

   Hence part-label-then-h2 is still listed, so the pairing that the
   2025 predictions post already ships on stays exactly as signed off. */
.gs-blog-body
  :is(h1, h2, h3, h4, h5, h6):not(.gs-blog-part)
  + :is(h2, h3, h4, h5, h6),
.gs-blog-body h2.gs-blog-part + h2 {
  margin-top: 0.5rem;
}

/* Paragraphs — generous bottom margin, body-text color. */
.gs-blog-body p {
  margin: 0 0 1.25rem;
  text-wrap: pretty;
}

/* Lists — indented, comfortable line-height per item. */
.gs-blog-body ul,
.gs-blog-body ol {
  margin: 0 0 1.5rem;
  padding-left: 1.5rem;
}

/* Nested ordered lists step down the marker hierarchy. Without this a
   sub-list restarts at "1." under its parent's "4.", so the two levels
   are indistinguishable and the sub-items read as siblings. */
.gs-blog-body ol ol {
  list-style-type: lower-alpha;
}

.gs-blog-body ol ol ol {
  list-style-type: lower-roman;
}

.gs-blog-body li {
  margin: 0 0 0.5rem;
}

.gs-blog-body li > p:last-child {
  margin-bottom: 0;
}

/* Inline code — small monospace pill. Includes the .highlighter-
   rouge variant from Rouge/Jekyll-style HubSpot content. Both
   render identically — there's no semantic reason for two. Pill
   background/border deliberately deeper than a typical muted
   surface (not --color-bg-muted / --color-border, both too close to
   white to read as a distinct element) - confirmed low-contrast
   directly against a live test post 2026-07-18. Stays neutral, not
   coral - that call was made and confirmed separately. */
.gs-blog-body code,
.gs-blog-body code.highlighter-rouge {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.875em;
  font-weight: 500;
  background: var(--color-bg-muted-deep);
  border: 1px solid var(--color-fg-muted);
  border-radius: 6px;
  padding: 0.1em 0.4em;
  color: var(--color-fg);
  /* Avoid inline code wrapping in unfortunate places */
  white-space: nowrap;
}

/* Code block — full-width, dark bg, monospace. Inside a <pre>,
   <code> resets to no-pill since the block itself carries the
   styling. */
.gs-blog-body pre {
  background: var(--color-bg-dark);
  color: #e6edf3;
  padding: 1.25rem 1.5rem;
  border-radius: 12px;
  overflow-x: auto;
  /* Top margin as well as bottom: plenty of migrated posts carry the
     paragraph above a code block as a BARE TEXT NODE rather than a
     <p>, so there is no preceding bottom margin to lean on and the
     block sits flush against the prose. Where a real <p> does precede
     it the two margins collapse to one 1.5rem gap, so this changes
     nothing in that case. Found in "Securing the software supply
     chain with Sigstore", 2026-09-29. */
  margin: 1.5rem 0;
  /* Same stack as inline .gs-blog-body code. Without it this rule set
     no font-family at all, so a code BLOCK fell through to the browser's
     generic `monospace` while inline code used the curated stack - which
     on macOS means Courier next to SF Mono. Both are 14px, but the
     differing x-height and weight read as two different font sizes on
     the same page. `pre code` below inherits from here, so setting it
     once on the block covers both. */
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.875rem;
  line-height: 1.6;
}

.gs-blog-body pre code,
.gs-blog-body pre code.highlighter-rouge {
  font-family: inherit;
  font-size: inherit;
  background: transparent;
  border: 0;
  padding: 0;
  color: inherit;
  white-space: pre;
}

/* Blockquote — left rule + slightly inset body. Used for pull
   quotes / "key insight" callouts. */
.gs-blog-body blockquote {
  margin: 2rem 0;
  padding: 0.5rem 0 0.5rem 1.5rem;
  border-left: 3px solid var(--color-brand-accent);
  font-style: italic;
  color: var(--color-fg);
}

.gs-blog-body blockquote p:last-child {
  margin-bottom: 0;
}

/* Body images — figure or img. Constrained to the prose column
   width, soft top/bottom margin for breathing room. Figure captions
   sit centered below.

   NO border-radius, ever. Standing rule (Marat, 2026-08-12): blog
   post body images never get rounded corners. This rule used to set
   12px, which rounded every diagram, screenshot and flowchart in
   every post - wrong for content images, and it was also the thing
   that quietly rounded the cert banner's badge until that got an
   explicit override. Don't reintroduce it here, and don't add it to
   a descendant selector either.

   Out of scope for that rule, deliberately: .gs-blog-post__cover img
   (the featured/hero image above the article, not a body image) keeps
   its 16px, and component images inside the body that own their shape
   set it themselves (e.g. the cert banner's circular avatar). */
.gs-blog-body img {
  display: block;
  max-width: 100%;
  height: auto;
  /* Left-aligned, not centred. The `auto` this replaces centred every
     body image, which only ever looked deliberate on images wide enough
     to fill the column - anything narrower (an inline mark, a small
     diagram) floated in the middle of the prose with uneven gutters.
     Left edge now lines up with the text. Matches .gs-blog-body figure
     img, which was already margin-0 and so already left-aligned. */
  margin: 2rem 0;
}

.gs-blog-body figure {
  margin: 2rem 0;
}

/* HubSpot embed wrappers (GIPHY, video) ship with an inline
   `margin: auto` that centres them, against the standing rule above
   that body images sit left-aligned. The wrapper is a div, not an img,
   so the rule above never reaches it, and the inline style means a
   plain override loses - hence !important, the one case it's for. */
.gs-blog-body .hs-embed-wrapper {
  margin-left: 0 !important;
  margin-right: auto !important;
}

/* An image sitting directly under a heading double-counts the gap. A
   heading spends 24px as padding-bottom, and the image then adds its
   own 2rem on top of that, landing at 56px - against the 24px a
   paragraph in the same position gets, since a paragraph contributes no
   margin of its own. The result reads as mysterious empty space with
   nothing in it, which is exactly how it was reported.

   Zero the image's top margin in that one position so a heading is
   followed by an image at the same distance as it is by prose. Three
   selectors because the image arrives in three shapes: bare, wrapped in
   the <p> HubSpot's editor emits, or inside a figure. */
.gs-blog-body :is(h1, h2, h3, h4, h5, h6) + img,
.gs-blog-body :is(h1, h2, h3, h4, h5, h6) + p > img:first-child,
.gs-blog-body :is(h1, h2, h3, h4, h5, h6) + figure {
  margin-top: 0;
}

.gs-blog-body figure img {
  margin: 0 0 0.75rem;
}

/* Left-aligned, matching the body images they caption. Centred until
   2026-08-19, which was left over from when body images were centred
   too - once those moved to left-aligned, a centred caption sat off to
   one side of the image it belonged to. */
.gs-blog-body figcaption {
  text-align: left;
  font-size: var(--text-sm);
  color: var(--color-fg-body);
  font-style: italic;
}

/* Spotify players — full column width, fixed height.

   HubSpot wraps every oEmbed in .hs-embed-wrapper with two inline
   styles: max-width 540px on the wrapper, and padding-bottom 56.25%
   on the box inside it. Both are inline, so overriding them needs
   !important - specificity alone can't win.

   The 16:9 padding-bottom box is right for video but wrong here: a
   Spotify player is a fixed-height widget, so tying its height to a
   ratio of its width means widening the embed also stretches it.
   Dropping the ratio and pinning the height instead means going full
   width changes only the width.

   The pinned height is deliberately not the 304px that 16:9-of-540
   used to produce. Spotify's own oEmbed API reports 456 by 152 as the
   canonical player size, and our embeds request 152 to match, so 304px
   handed the player twice the height it asks for - it drew the largest
   card it could afford (~220px) and left the remaining ~80px as empty
   player background, reading as a big gap under every embed. 232px
   hugs that card instead.

   Scoped with :has() to Spotify specifically - the same wrapper
   carries YouTube and other video embeds elsewhere in the blog,
   which genuinely do want the 16:9 ratio. */
.gs-blog-body .hs-embed-wrapper:has(iframe[src*="open.spotify.com"]) {
  max-width: none !important;
  /* Matches the 1.5rem of breathing room an h2 puts below itself (it
     spends that as padding rather than margin, since its own margin is
     zeroed). Without this the player sits flush against the paragraph
     that follows - the wrapper's inline `margin: auto` shorthand sets
     margin-bottom inline as well, so this needs !important to land. */
  margin-bottom: 1.5rem !important;
}

.gs-blog-body
  .hs-embed-wrapper:has(iframe[src*="open.spotify.com"])
  .hs-embed-content-wrapper > div {
  padding-bottom: 0 !important;
  height: 232px;
}

/* Links — deliberate, content-type-specific exception to the
   sitewide inline-link convention (§6.15: inherit color, coral only
   on hover). Blog posts mix plain prose, links, and inline code in
   the same paragraph with no competing coral elements nearby, so a
   persistent coral link reads cleanly. This does NOT extend to pages
   like learn-content, which already lean on coral for structure
   (chapter kickers, etc.) - adding persistent coral links there
   competes with that existing signal instead of complementing it.
   Confirmed by direct comparison 2026-07-18: works on blog, doesn't
   on learn pages. Rouge inline code stays its current neutral grey -
   explicitly left alone, not recolored to match.
   Underline is hover-only, not default (2026-07-20 revision) - coral
   color alone is the resting signal, underline confirms on hover.
   No transition and no opacity/color change on hover - underline is
   the ONLY hover cue, appearing instantly (text-decoration-line
   isn't animatable anyway). */
.gs-blog-body a {
  color: var(--color-brand-accent);
  text-decoration: none;
}

.gs-blog-body a:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Linked inline code — a technical term that's both a code pill
   and a hyperlink. Coral border + coral text + transparent bg marks
   it as a link at rest, distinct from a plain code pill's neutral
   grey/filled look. Hover adds the same underline convention as
   regular body-text links, nothing pill-specific.
   Note: :has()'s own specificity contribution is just its argument's
   ("code" = one type selector), so matching the base rule's
   .highlighter-rouge variant below is needed to actually outrank it
   — that variant carries an extra class the plain one doesn't. */
.gs-blog-body a:has(code) {
  text-decoration: none;
}

.gs-blog-body a:has(code) code,
.gs-blog-body a:has(code) code.highlighter-rouge {
  border-color: var(--color-brand-accent);
  color: var(--color-brand-accent);
  background: transparent;
}

.gs-blog-body a:has(code):hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Tables — a full grid of borders, header row colored darker than
   the rest. No zebra striping - tried, wasn't wanted. */
.gs-blog-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 1.5rem;
  font-size: var(--text-sm);
}

.gs-blog-body th,
.gs-blog-body td {
  text-align: left;
  vertical-align: top;
  padding: 0.75rem 1rem;
  border: 1px solid var(--color-border);
}

/* Rich-text-pasted table cells wrap their content in a <p> (confirmed
   in this table's own source) - that <p>'s own bottom margin (the
   sitewide .gs-blog-body p rule, meant for spacing between article
   paragraphs) adds unwanted extra space inside a cell that already
   has its own padding. Scoped to inside the table specifically, not
   a change to .gs-blog-body p itself - that rule is shared by every
   paragraph in every blog post's prose, not table-specific. */
.gs-blog-body table p {
  margin-bottom: 0;
}

/* First column reads as row labels - bold + black like the header
   row, not the muted background (this is a column treatment, not
   another header row). */
.gs-blog-body table tr td:first-child {
  font-weight: var(--weight-bold);
  color: var(--color-fg);
}

/* Header treatment. Two cases, and they must not both fire on the
   same table:

   1. A real <th> - the semantic, well-formed case.
   2. The table's first row via td - rich-text-pasted tables often use
      a plain <td> with bolded text as their header instead of semantic
      markup (confirmed in "The cloud native era killed VMware's
      lock-in" - no <thead>/<th> anywhere in its source), so keying off
      <th> alone silently no-ops there and the header reads as a data
      row.

   The `:not(:has(th))` guard is what keeps case 2 from also hitting a
   well-formed table: `tr:first-child` matches the first row of EVERY
   row group, so in a table with <thead><tr><th>, the first <tr> inside
   <tbody> is also a first-child and picked up the header background
   too - a visible double header (caught 2026-08-12 in "Why Giant Swarm
   isn't Just Ops", whose markup is properly structured). Restricting
   the positional fallback to tables that contain no <th> at all makes
   the two cases mutually exclusive. `:has()` is already used elsewhere
   in this codebase (see the blog link/code rules). */
.gs-blog-body th,
.gs-blog-body table:not(:has(th)) tr:first-child td {
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  background: var(--color-bg-muted-deep);
}

/* Employee-quote cards — opt out of the data-table treatment above.

   These are not data tables. The old theme used a table purely to lay a
   pull-quote card out: quote across the top, then avatar beside
   name/role. Every rule in the tables section above is wrong for them,
   and all five were visibly firing:

   - font-size var(--text-sm) dropped the quote AND the attribution to
     14px, out of step with the surrounding prose
   - the full grid of cell borders drew a box around each cell
   - the first-row header treatment painted a muted grey band behind the
     quote and forced its color back to near-black over the blue, and
     its bold weight inherited down into the quote text
   - td:first-child bolded the avatar cell for the same reason
   - border-collapse: collapse suppresses border-radius on a table
     outright, so the card's rounded dashed outline could not render

   Keyed on .eqh-item, the class the old theme already puts on these, so
   real data tables are untouched. Cell padding/alignment comes from the
   component's own inline styles, which is why padding resets to 0 here
   rather than to a value of ours. */
.gs-blog-body table.eqh-item {
  font-size: var(--text-base);
  margin: 0;
  /* Lets the inline border-radius + dashed border actually paint. */
  border-collapse: separate;
  border-spacing: 0;
}

.gs-blog-body .eqh-item td,
.gs-blog-body .eqh-item tr:first-child td {
  padding: 0;
  border: 0;
  background: none;
  font-weight: inherit;
  color: inherit;
}

/* The quote itself reads at body size. */
.gs-blog-body .eqh-item tr:first-child p {
  font-size: var(--text-base);
}

/* Attribution block: name and role share one paragraph, split by a
   line break, so the role is sized by the paragraph and the name is
   sized back up via its own strong - a step apart rather than two
   equal-weight lines. */
.gs-blog-body .eqh-item tr:last-child p {
  font-size: var(--text-sm);
}

.gs-blog-body .eqh-item tr:last-child strong {
  font-size: var(--text-base);
}

/* .gs-blog-body strong sets color: var(--color-fg) at an exact
   specificity tie with anything less qualified, so the name was being
   repainted near-black over the card. Inherit the white the
   paragraph's own inline color already establishes. Same trap the
   cert-banner attribution rule further up this file documents. */
.gs-blog-body .eqh-item strong {
  color: inherit;
}

/* Avatar: keep max-width so it fits the 60px cell the markup asks for,
   but drop the 2rem block margin every other body image gets. */
.gs-blog-body .eqh-item img {
  margin: 0;
}

/* Breathing room below the card. The margin has to go on the outer
   flex wrapper, not the table - a margin on the table would sit inside
   the coloured, padded div that forms the card's visible edge. Neither
   wrapper carries a class, hence :has() to reach the right one. */
.gs-blog-body div:has(> div > table.eqh-item) {
  margin-bottom: 1.5rem;
}

/* Horizontal rule — subtle, more breathing room than a default <hr>. */
.gs-blog-body hr {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: 3rem 0;
}

/* Strong / em — preserve emphasis but in body-text color. */
.gs-blog-body strong {
  font-weight: var(--weight-bold);
  color: var(--color-fg);
}

.gs-blog-body em {
  font-style: italic;
}

/* ─── Related posts (closing section, full width) ────────────
   Lives outside .gs-blog-post so it spans the full section width,
   not the narrow reading column. Cards reuse .gs-listing-card
   (same tile the main blog listing uses) — .gs-blog-related__card
   only adds the topic chip + date row this context needs on top. */
.gs-blog-related {
  background: var(--color-bg-muted);
  padding: 5rem 1.5rem;
}

.gs-blog-related__inner {
  max-width: var(--content-dense);
  margin: 0 auto;
}

.gs-blog-related__heading {
  text-align: center;
  margin: 0 0 3rem;
}

.gs-blog-related__title {
  font-size: var(--text-h2);
  font-weight: var(--weight-bold);
  color: var(--color-fg);
  margin: 0;
}

.gs-blog-related__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
}

/* Category chip + date/read-time footer row moved to listing.css
   (.gs-listing-card__chip/__meta/__readtime) - shared with the blog
   listing page's own cards, not specific to this section. */

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

@media (max-width: 900px) {
  .gs-blog-related__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 767px) {
  .gs-blog-related {
    padding: 3rem 1.5rem;
  }
  .gs-blog-related__grid {
    grid-template-columns: 1fr;
  }
  .gs-blog-post__layout {
    padding-top: 6.5rem;
  }
  .gs-blog-post__intro,
  .gs-blog-post__cover {
    /* No header wrapper anymore to provide this externally — each
       element (like .gs-blog-post__body and .gs-blog-post__rail
       below) owns its own mobile inset now. */
    padding: 0 1.25rem;
  }
  .gs-blog-post__title {
    font-size: var(--text-h2);
  }
  .gs-blog-post__body {
    padding: 1.5rem 1.25rem 5rem;
  }
  .gs-blog-post__rail {
    padding: 0 1.25rem;
  }
  .gs-blog-body h2 {
    font-size: var(--text-h3);
    line-height: var(--leading-snug);
    margin: 0;
    padding: 0.5rem 0 1.5rem;
    /* Balanced wrapping reads fine on wide desktop columns, but on a
       narrow mobile line it can break a short heading a word early
       just to even out two lines — reads as not filling the width. */
    text-wrap: wrap;
  }
  /* The rule above trims mobile top padding to 0.5rem, which is fine
     for a plain heading but leaves a part label's text sitting only 8px
     under its own top border - visibly tighter than the 24px below it.
     Restored to match the bottom, so the label stays optically centred
     between its two rules. Part labels only; plain headings keep the
     tighter mobile spacing. */
  .gs-blog-body h2.gs-blog-part {
    padding-top: 1.5rem;
  }
  .gs-blog-body h3 {
    font-size: var(--text-h4);
    margin-top: 2rem;
  }
  .gs-blog-body pre {
    /* Phone-width margin: extend the code block beyond the prose
       padding so long lines have more horizontal room. */
    margin-left: -1.25rem;
    margin-right: -1.25rem;
    border-radius: 0;
  }
}

/* ─── Blog listing pager ─────────────────────────────────────
   Numbered circular pager matching the OLD live blog's look
   (Act3's `blog-pagination`), so the rebuilt blog keeps visual
   continuity with what readers see today. Markup + the sliding
   5-wide window live in templates/blog/listing.html.

   NOTE ON THE ACTIVE STATE: this uses --gradient-cta, which the
   design system otherwise reserves for primary CTAs
   (skills/foundations/color-system). Deliberate, requested match to
   the old blog's orange active pill - not a general licence to use
   the gradient elsewhere. Swapping to --color-accent (the blue UI
   accent, the system-consistent choice for an interactive control)
   is a one-line change if that reading wins later.

   Disabled ends render as spans, not dead anchors, so nothing
   focusable goes nowhere. */
.gs-blog-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 3rem;
}

.gs-blog-pager__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  flex: 0 0 auto;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--color-fg);
  background: var(--color-bg-muted);
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease;
}

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

.gs-blog-pager__item.is-active {
  background: var(--gradient-cta);
  color: #ffffff;
}

/* Gradients can't transition, so keep the CTA-button convention
   (btn.css) and brighten instead of recolouring on hover. */
.gs-blog-pager__item.is-active:hover {
  filter: brightness(1.08);
}

.gs-blog-pager__item--arrow {
  color: var(--color-fg-muted);
}

.gs-blog-pager__item--arrow svg {
  width: 16px;
  height: 16px;
}

.gs-blog-pager__item.is-disabled {
  opacity: 0.35;
  pointer-events: none;
}

/* Third-party embeds that wait for cookie consent (js/modules/consent-embed.js).

   A placeholder card stands where a Spotify player, X post or Giphy
   frame would be, until the visitor accepts cookies or clicks to load
   that one embed. Every selector is qualified with .gs-blog-body, and
   the notice is built from divs plus one link, because a bare class
   loses to the .gs-blog-body element rules above (HUBSPOT-NOTES.md §28).
   The link in particular would otherwise turn coral like body links. */
.gs-blog-body .gs-consent-embed {
  margin: 2rem 0;
}

.gs-blog-body .gs-consent-embed__notice {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--color-border);
  border-radius: 16px;
  background: var(--color-bg-muted);
}

.gs-blog-body .gs-consent-embed__notice[hidden] {
  display: none;
}

/* An X post shows its own text as the fallback, so the notice sits
   under it as a slimmer strip rather than standing in for it. */
.gs-blog-body .gs-consent-embed blockquote + .gs-consent-embed__notice {
  margin-top: 1rem;
}

.gs-blog-body .gs-consent-embed__text {
  flex: 1 1 18rem;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-fg-body);
}

/* Secondary small button (css/components/btn.css), restated at this
   specificity, in Giant Swarm Black - not the gradient (it is not a
   CTA) and not blue (decided 2026-10-28: the card should sit quietly
   in the article rather than read as a link). */
.gs-blog-body a.gs-consent-embed__load {
  display: inline-flex;
  align-items: center;
  padding: 0.65rem 1.4rem;
  border: 1.5px solid var(--color-bg-darkest);
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: 1;
  color: var(--color-bg-darkest);
  text-decoration: none;
  white-space: nowrap;
}

.gs-blog-body a.gs-consent-embed__load:hover {
  color: var(--color-bg-darkest);
  background: rgba(0, 27, 49, 0.06);
}

.gs-blog-body a.gs-consent-embed__load:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.gs-blog-body .gs-consent-embed__frame {
  display: block;
  width: 100%;
  height: 232px; /* Spotify's card; see the Spotify block above */
  border: 0;
  border-radius: 12px;
}
