/* ============================================================
   ENSTACKED GLOBAL UTILITIES
   Reusable section rhythm, typography, and spacing classes
   shared by ALL page templates. Values come exclusively from
   design-tokens.css — never hardcode px here.

   New templates should use the `es-` classes. The `rv-`
   aliases keep the revamped home page (and any page built
   during the revamp) working with the same single system.
   ============================================================ */

/* ── Screen-reader-only text ────────────────────────────────
   Own copy rather than legacy `.screen-reader-text`, which only
   exists in style.css and disappears once a template dequeues it. */
.es-sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Container ──────────────────────────────────────────────
   Named `.es-container` rather than `.container` so it does NOT
   collide with Bootstrap on the ~200 pages still on legacy markup.

   Geometry deliberately MIRRORS Bootstrap's `.container` ladder
   (12px inline padding, max-width capped at 1320px) so a migrated
   section lines up pixel-for-pixel with the rest of the site and
   with the homepage. When Bootstrap is finally dropped, this keeps
   the identical geometry and nothing shifts. */
.es-container {
  width: 100%;
  /* --container-gutter, not a literal, so a full-bleed child can cancel this
     exactly instead of guessing at it. Resolves to the same 12px it always
     was. See the token's note in design-tokens.css. */
  padding-inline: var(--container-gutter, 12px);
  margin-inline: auto;

  /* ── Stated here rather than inherited from Bootstrap ──────
     `width: 100%` plus 12px of inline padding overflows its parent by
     exactly 24px under the CSS default of `content-box`. It does not do that
     today only because bootstrap.min.css sets `box-sizing: border-box` on
     everything and is still enqueued site-wide.

     Section 5.4 of the brief plans to dequeue Bootstrap per template. The
     day that happens for a migrated page, every `.es-container` on it grows
     24px wider than the viewport and the page gets a horizontal scrollbar,
     which is the same class of failure the --container-gutter note in
     design-tokens.css already records once.

     Declaring it here changes nothing while Bootstrap is loaded, because it
     is the value Bootstrap already gives it. It just means the token system
     no longer depends on a stylesheet it is meant to replace.

     NOTE: this closes the dependency for the CONTAINER only. Other `es-`
     components almost certainly rely on the same global reset. Auditing that
     properly is its own job and belongs with the dequeue, not here. */
  box-sizing: border-box;
}

@media (min-width: 576px) {
  .es-container {
    max-width: 540px;
  }
}
@media (min-width: 768px) {
  .es-container {
    max-width: 720px;
  }
}
@media (min-width: 992px) {
  .es-container {
    max-width: 960px;
  }
}
@media (min-width: 1200px) {
  .es-container {
    max-width: 1140px;
  }
}
@media (min-width: 1400px) {
  .es-container {
    max-width: 1320px;
  }
}

/* Narrower variants for forms and long-form reading */
.es-container--narrow {
  max-width: var(--container-md) !important;
}
.es-container--read {
  max-width: var(--reading-width) !important;
}

/* ── Buttons ────────────────────────────────────────────────
   Token rewrite of the site's existing `.com-new-btn` system.
   Geometry, hover lift and the staggered triple-chevron pulse are
   matched exactly, so an `es-` section sitting above a legacy
   section shows the same button. The only differences are
   deliberate: colours resolve through tokens (dark-mode safe) and
   the icon inherits `currentColor` instead of being hardcoded
   white, so one icon works on every button variant.

   No shadow on any variant, in any state, as of 9 Sep 2026. That
   is the one line of the Uniformity Contract this family no longer
   keeps, and it is the owner's instruction, not drift.

   14px vertical padding on a 16px/1 label gives a 48px box,
   clearing the 44px touch-target floor. Restored 9 Sep 2026: this
   had been cut to 9px/15px, which draws a 38px box and misses that
   floor, and it also split this family away from .com-new-btn,
   which still measures 16px / 14px 28px in style.css. The contract
   is that the two are the same button. */
.es-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 9px 14px;
  border: 1px solid transparent;
  border-radius: 50px;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: var(--font-semibold);
  line-height: 20px;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: all var(--duration-slow) var(--ease-out);
}

.es-btn .arrow-icon,
.es-btn__icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--brand-500);
}

.es-btn .arrow-icon path {
  transition: opacity var(--duration-normal) ease;
}

/* Primary — matches .com-new-btn
   ────────────────────────────────────────────────────────────────────────
   Frosted glass, not a solid orange slab. Owner's instruction, 9 Sep 2026.
   The brand is carried by the hairline and the chevrons now, not by 300px
   of fill.

   The ground is NEUTRAL glass (--bg-surface at 76%), not orange glass, and
   that is what makes the rest of the button work: the label holds
   --text-heading at better than 12:1, and the orange chevrons the owner
   asked for are legible because they are not sitting on their own colour.
   An orange-tinted pill would have swallowed them.

   It also fixes the contrast the solid version never had: white on
   --brand-500 measures 3.50:1, which fails AA for a 16px semibold label.
   Dark ink on this ground clears it several times over.

   `backdrop-filter` is the same saturate/blur pairing header.css uses for
   its glass, so the two surfaces are the same material. Browsers without it
   still get the 76% wash, which is why the colour is on `background` rather
   than left to the filter.

   NO SHADOW, in any state. The 0 10px 25px -10px orange glow this used to
   carry is gone site-wide on the same instruction; the Uniformity Contract
   in CLAUDE.md still lists it and needs updating to match. */
.es-btn--primary {
  border-color: var(--brand-500);
  color: var(--text-heading);
  background: color-mix(in srgb, var(--bg-surface) 76%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
}

.es-btn--primary:hover,
.es-btn--primary:focus-visible {
  border-color: var(--brand-500);
  background: color-mix(in srgb, var(--bg-surface) 92%, transparent);
  transform: translateY(-2px);
}

/* The chevrons are the brand mark on this button now that the fill is not.
   --brand-500 rather than --brand-700: this is a graphical object answering
   the 3:1 bar, not text, and it clears that on the frosted ground. */
.es-btn--primary .arrow-icon,
.es-btn--primary .es-btn__icon {
  color: var(--brand-500);
}

/* Secondary — matches .com-new-btn-white */
.es-btn--secondary {
  background-color: var(--bg-sunken);
  color: var(--text-heading);
}

.es-btn--secondary:hover,
.es-btn--secondary:focus-visible {
  background-color: var(--border-subtle);
  color: var(--text-heading);
  transform: translateY(-2px);
}

.es-btn--secondary .arrow-icon {
  color: var(--brand-500);
}

/* Outline — the same pill as the rest of the family with the fill
   taken out and a hairline in its place.

   It is a new variant rather than a reuse: `--secondary` is named
   like an outline and is not one, it is a filled `--bg-sunken` pill,
   which on a light card ground reads as a grey slab and on a sunken
   band vanishes outright. This is declared here beside the others
   rather than inside one component's stylesheet, so the next page
   that needs an outlined control takes this one instead of drawing
   a second.

   `--btn-outline-edge` is what the border and the mark go to on
   hover. It falls back to the site accent, so the variant works
   anywhere with no setup; a panel with an accent of its own sets
   that property and the button follows it. */
.es-btn--outline {
  border: 1.5px solid var(--border-default);
  background-color: transparent;
  color: var(--text-heading);
}

.es-btn--outline:hover,
.es-btn--outline:focus-visible {
  border-color: var(--btn-outline-edge, var(--brand-500));
  color: var(--text-heading);
  transform: translateY(-2px);
}

.es-btn--outline .arrow-icon {
  color: var(--btn-outline-edge, var(--brand-500));
}

/* Dark — matches .com-new-btn-dark */
.es-btn--dark {
  background-color: var(--gray-900);
  color: var(--text-inverse);
}

.es-btn--dark:hover,
.es-btn--dark:focus-visible {
  background-color: var(--gray-800);
  color: var(--text-inverse);
  transform: translateY(-2px);
}

/* Staggered chevron sweep on hover — the site's signature CTA
   microinteraction. Keyframes arrowPulse1/2/3 live in style.css;
   redeclared here so the effect survives that file being dequeued. */
.es-btn:hover .arrow-icon path:nth-child(1) {
  animation: arrowPulse1 0.8s infinite;
}
.es-btn:hover .arrow-icon path:nth-child(2) {
  animation: arrowPulse2 0.8s infinite;
}
.es-btn:hover .arrow-icon path:nth-child(3) {
  animation: arrowPulse3 0.8s infinite;
}

@keyframes arrowPulse1 {
  0% {
    opacity: 0.6;
  }
  33% {
    opacity: 1;
  }
  66% {
    opacity: 0.8;
  }
  100% {
    opacity: 0.6;
  }
}
@keyframes arrowPulse2 {
  0% {
    opacity: 0.8;
  }
  33% {
    opacity: 0.6;
  }
  66% {
    opacity: 1;
  }
  100% {
    opacity: 0.8;
  }
}
@keyframes arrowPulse3 {
  0% {
    opacity: 1;
  }
  33% {
    opacity: 0.8;
  }
  66% {
    opacity: 0.6;
  }
  100% {
    opacity: 1;
  }
}

/* Size + width modifiers */
.es-btn--sm {
  padding: 11px 22px;
  font-size: var(--fs-small);
}
.es-btn--lg {
  padding: 18px 34px;
  font-size: var(--fs-lede);
}

@media (max-width: 480px) {
  .es-btn--block-mobile {
    width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .es-btn {
    transition: none;
  }
  .es-btn:hover {
    transform: none;
  }
  .es-btn:hover .arrow-icon path {
    animation: none;
  }
}

/* ── Rail nav arrows ───────────────────────────
   The prev/next pair that drives a horizontal card rail. Defined
   once, here, in the Recent Insights vocabulary, because more than
   one section shows it and utilities.css is the only sheet that
   loads on every page. A per-section copy is exactly how the three
   that existed drifted apart: 48px, 44px and 40px, one of them
   filling brand orange on hover and the other two not.

   Never define a second rail arrow. A section that needs the pair
   somewhere other than bottom-left sets that on `.es-ins__nav` in
   its own sheet and leaves the button alone.

   Ships `hidden` in the markup. es-sections.js unhides the pair
   only once it has measured that the track actually overflows, so
   a rail showing everything it has never offers two inert
   buttons. */
.es-ins__nav {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.es-ins__nav[hidden] {
  display: none;
}

.es-ins__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  color: var(--text-heading);
  cursor: pointer;
  transition:
    background-color var(--duration-normal) var(--ease-out),
    border-color var(--duration-normal) var(--ease-out),
    color var(--duration-normal) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}

.es-ins__btn svg {
  width: 20px;
  height: 20px;
}

/* One chevron, mirrored, rather than a second icon in inc/icons.php. */
.es-ins__btn--prev svg {
  transform: rotate(180deg);
}

.es-ins__btn:hover:not(:disabled) {
  border-color: var(--brand-500);
  color: var(--brand-600);
}

.es-ins__btn:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* Kept in the tab order and announced rather than removed, so the
   count of controls does not change under a screen reader
   mid-scroll. */
.es-ins__btn:disabled {
  opacity: 0.4;
  cursor: default;
}

@media (prefers-reduced-motion: reduce) {
  .es-ins__btn {
    transition: none;
  }
}

/* ── Section rhythm ─────────────────────────────────────────
   Fluid vertical padding: ~40px on mobile → 100px on desktop.
   Modifiers: --sm (compact), --lg (spacious), --flush-* (none). */
.es-section,
.rv-section {
  padding: var(--section-pad) 0;
  position: relative;
}

.es-section--sm {
  padding-top: var(--section-pad-sm);
  padding-bottom: var(--section-pad-sm);
}
.es-section--lg {
  padding-top: var(--section-pad-lg);
  padding-bottom: var(--section-pad-lg);
}
.es-section--flush-top {
  padding-top: 0;
}
.es-section--flush-bottom {
  padding-bottom: 0;
}

/* Section backgrounds */
.es-bg-white,
.rv-bg-white {
  background: var(--bg-surface);
}
.es-bg-gray,
.rv-bg-gray {
  background: var(--gray-50);
}
.es-bg-dark,
.rv-bg-dark {
  background: var(--gray-900);
}
.es-bg-tint {
  background: linear-gradient(
    180deg,
    var(--brand-50) 0%,
    var(--bg-surface) 100%
  );
}

/* ── Section head ───────────────────────────────────────────
   Eyebrow + title + optional lede. Fluid gap to the content
   below (~36px mobile → 56px desktop). */
.es-section-head,
.rv-section-head {
  max-width: 780px;
  margin-bottom: var(--section-head-gap);
}

.es-section-head.text-center,
.rv-section-head.text-center {
  margin-left: auto;
  margin-right: auto;
}

/* Head gap modifiers — replaces ad-hoc inline margin overrides */
.es-section-head--tight {
  margin-bottom: var(--gap-fluid-sm);
}
.es-section-head--flush {
  margin-bottom: 0;
}

.es-section-head h1,
.es-section-head h2,
.rv-section-head h1,
.rv-section-head h2 {
  font-size: var(--fs-section-title);
  font-weight: 800;
  line-height: var(--lh-heading);
  letter-spacing: -0.02em;
  color: var(--text-heading);
  margin: 0;
}

.es-lede,
.rv-lede {
  font-size: var(--fs-lede);
  line-height: var(--lh-body);
  color: var(--text-body);
}

.es-section-head .es-lede,
.es-section-head .rv-lede,
.rv-section-head .es-lede,
.rv-section-head .rv-lede {
  margin: 20px 0 0;
}

/* ── Eyebrow label ──────────────────────────────────────────
   The chip, site-wide, on the owner's instruction. It was bare
   brand-600 text on the section's own ground; it is now a rounded
   pill, which is the form the hero eyebrow and the About page's
   pillars already used. Changing it here rather than per section
   means every partial that renders `.es-eyebrow` picks it up, on
   every migrated page, with no template edits.

   ── This also closes the eyebrow contrast gap ───────────────
   The old pairing was brand-600 on whatever the section painted,
   which measured 4.47:1 against white and missed AA by 0.03 at
   12px. That single pairing was enough to hold Lighthouse
   accessibility off 100 site-wide. brand-700 (#A33A0D) on the
   brand-50 chip (#FFF3EE) measures 6.09:1, so the label now clears
   AA with room to spare regardless of what the section behind it
   is painted, because the chip brings its own ground.

   `width: fit-content` rather than `display: inline-flex`: the box
   has to shrink-wrap the label either way, and a block-level box
   can then be centred with `margin-inline: auto` below. An inline
   box would need `text-align: center` on the parent, which today
   comes from Bootstrap's `.text-center` and would break the day
   Bootstrap is dequeued. */
.es-eyebrow,
.rv-eyebrow {
  display: flex;
  width: fit-content;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-5);
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--brand-100);
  border-radius: var(--radius-full);
  background: var(--accent-subtle);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-bold);
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--brand-700);
}

/* The 40px hairline that used to trail every eyebrow is gone, on the owner's
   instruction. The `gap` above only applies between actual children, so
   removing the pseudo-element leaves no stray spacing behind. */

/* A shrink-wrapped block centres on its margins. `justify-content` no longer
   does anything here, since the box is only as wide as its content. */
.es-section-head.text-center .es-eyebrow,
.es-section-head.text-center .rv-eyebrow,
.rv-section-head.text-center .es-eyebrow,
.rv-section-head.text-center .rv-eyebrow {
  margin-inline: auto;
}

/* The (0N) index, where a section passes one. Inside the chip it takes the
   chip's own ink rather than --text-muted: gray-500 on brand-50 measures
   3.9:1 and would fail AA at 12px, reintroducing on the index exactly the
   problem the chip just fixed on the label. It is set apart by weight
   instead of by colour, which is also the rule about not carrying meaning
   in colour alone. */
.es-index,
.rv-index {
  color: inherit;
  font-weight: var(--font-medium);

  /* ── The marker is one token and must never be broken ──────
     The chip is a flex line, so the index is a flex item sitting beside the
     label. On a narrow screen a long label pushes the line past the
     container, both items shrink, and the marker was being split across two
     lines as "(05" then ")". Measured at 375px on the Theme Development
     page: the (04) marker rendered 24px wide and 38px tall, against 31x19
     for every marker on that page whose label was short enough to fit.

     `nowrap` removes the break opportunity inside the marker and
     `flex-shrink: 0` stops the flex line taking width off it in the first
     place, so the label absorbs the wrap on its own, which is where a wrap
     belongs. Both are needed: nowrap alone leaves the item squeezed and
     overflowing its own box.

     Not scoped to a breakpoint. A marker split in half is wrong at every
     width, and the bug only shows at narrow ones because that is where the
     line runs out of room. */
  flex-shrink: 0;
  white-space: nowrap;
}

/* Dark grounds. The chip is a light surface, so on a dark section it has to
   invert or it disappears against its own border. Kept as a translucent
   brand wash rather than a solid, so the section's artwork still reads
   through it the way it does behind the hero's eyebrow. */
.es-bg-dark .es-eyebrow,
.es-bg-dark .rv-eyebrow,
.rv-bg-dark .es-eyebrow,
.rv-bg-dark .rv-eyebrow {
  border-color: rgba(255, 157, 107, 0.28);
  background: rgba(232, 93, 32, 0.14);
  color: var(--brand-300);
}

/* Accent highlight inside headings */
.es-hl,
.rv-hl {
  color: var(--brand-500);
  font-style: italic;
}

/* ── Star rating ────────────────────────────────────────────
   The site's one star: assets/svg/star.svg, the owner's artwork of
   10 Sep 2026 (a 12 x 11.4 star). It replaces the text glyph that every
   rating row used to type. Drawn as a CSS mask, repeated
   --stars-count times across one strip, so the stars take their colour
   from CSS (currentColor) exactly as the glyph did. An <img> could not.

   Spacing: `mask-repeat: space` on a strip exactly N stars plus N-1
   gaps wide lays them out with a gap of one sixth of a star, which is
   the reference's 12px star and 2px gap.

   Partial ratings: --stars-fill (e.g. 90%) colours that much of the
   strip with currentColor and the rest with --stars-track; the mask cuts
   both to the star shapes.

   The knobs are read with fallbacks rather than declared here, so a
   parent can set them and they inherit. Put it inside an element that
   keeps the row's font size: that line box is what holds the row at the
   height the text glyphs gave it. */
.es-stars {
  position: relative;
  display: inline-block;
  vertical-align: middle;
  color: var(--brand-500);
  width: calc(
    var(--stars-size, 12px) * var(--stars-count, 5) + var(--stars-size, 12px) /
      6 * (var(--stars-count, 5) - 1)
  );
  height: calc(var(--stars-size, 12px) * 0.95);
  background: var(--stars-track, transparent);
  -webkit-mask: url("../svg/star.svg") 0 0 / var(--stars-size, 12px) auto space;
  mask: url("../svg/star.svg") 0 0 / var(--stars-size, 12px) auto space;
}

.es-stars::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--stars-fill, 100%);
  background: currentColor;
}

/* Dark-section variants (use on .es-bg-dark sections) */
.es-bg-dark .es-section-head h1,
.es-bg-dark .es-section-head h2,
.es-bg-dark .rv-section-head h1,
.es-bg-dark .rv-section-head h2,
.rv-bg-dark .es-section-head h1,
.rv-bg-dark .es-section-head h2,
.rv-bg-dark .rv-section-head h1,
.rv-bg-dark .rv-section-head h2 {
  color: var(--text-inverse);
}

/* ── Typography utilities ──────────────────────────────────
   For headings/copy outside a section head. */
.t-hero {
  font-size: var(--fs-hero);
  font-weight: 800;
  line-height: var(--lh-display);
  letter-spacing: -0.02em;
  color: var(--text-heading);
}
.t-section-title {
  font-size: var(--fs-section-title);
  font-weight: 800;
  line-height: var(--lh-heading);
  letter-spacing: -0.02em;
  color: var(--text-heading);
}
.t-title-md {
  font-size: var(--fs-title-md);
  font-weight: 800;
  line-height: var(--lh-heading);
  letter-spacing: -0.02em;
}
.t-title-sm {
  font-size: var(--fs-title-sm);
  font-weight: 700;
  line-height: var(--lh-subhead);
}
.t-card-title {
  font-size: var(--fs-card-title);
  font-weight: 800;
  line-height: var(--lh-subhead);
}
.t-lede-lg {
  font-size: var(--fs-lede-lg);
  line-height: var(--lh-body);
}
.t-lede {
  font-size: var(--fs-lede);
  line-height: var(--lh-body);
}
.t-body {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}
.t-small {
  font-size: var(--fs-small);
}
.t-caption {
  font-size: var(--fs-caption);
}

/* ── Spacing utilities ─────────────────────────────────────
   Token scale: 0=0  1=4px  2=8px  3=12px  4=16px
                5=24px  6=32px  7=48px  8=64px
   !important so they reliably win over component CSS —
   use them to replace inline style="" overrides. */
.u-mt-0 {
  margin-top: 0 !important;
}
.u-mt-1 {
  margin-top: var(--space-1) !important;
}
.u-mt-2 {
  margin-top: var(--space-2) !important;
}
.u-mt-3 {
  margin-top: var(--space-3) !important;
}
.u-mt-4 {
  margin-top: var(--space-4) !important;
}
.u-mt-5 {
  margin-top: var(--space-5) !important;
}
.u-mt-6 {
  margin-top: var(--space-6) !important;
}
.u-mt-7 {
  margin-top: var(--space-7) !important;
}
.u-mt-8 {
  margin-top: var(--space-8) !important;
}

.u-mb-0 {
  margin-bottom: 0 !important;
}
.u-mb-1 {
  margin-bottom: var(--space-1) !important;
}
.u-mb-2 {
  margin-bottom: var(--space-2) !important;
}
.u-mb-3 {
  margin-bottom: var(--space-3) !important;
}
.u-mb-4 {
  margin-bottom: var(--space-4) !important;
}
.u-mb-5 {
  margin-bottom: var(--space-5) !important;
}
.u-mb-6 {
  margin-bottom: var(--space-6) !important;
}
.u-mb-7 {
  margin-bottom: var(--space-7) !important;
}
.u-mb-8 {
  margin-bottom: var(--space-8) !important;
}

.u-pt-0 {
  padding-top: 0 !important;
}
.u-pt-1 {
  padding-top: var(--space-1) !important;
}
.u-pt-2 {
  padding-top: var(--space-2) !important;
}
.u-pt-3 {
  padding-top: var(--space-3) !important;
}
.u-pt-4 {
  padding-top: var(--space-4) !important;
}
.u-pt-5 {
  padding-top: var(--space-5) !important;
}
.u-pt-6 {
  padding-top: var(--space-6) !important;
}
.u-pt-7 {
  padding-top: var(--space-7) !important;
}
.u-pt-8 {
  padding-top: var(--space-8) !important;
}

.u-pb-0 {
  padding-bottom: 0 !important;
}
.u-pb-1 {
  padding-bottom: var(--space-1) !important;
}
.u-pb-2 {
  padding-bottom: var(--space-2) !important;
}
.u-pb-3 {
  padding-bottom: var(--space-3) !important;
}
.u-pb-4 {
  padding-bottom: var(--space-4) !important;
}
.u-pb-5 {
  padding-bottom: var(--space-5) !important;
}
.u-pb-6 {
  padding-bottom: var(--space-6) !important;
}
.u-pb-7 {
  padding-bottom: var(--space-7) !important;
}
.u-pb-8 {
  padding-bottom: var(--space-8) !important;
}

/* ── Scroll-reveal animation ────────────────────────────────
   Standard entrance used across templates. Add `.reveal-up`
   to any element; JS toggles `.is-visible` on scroll-in.
   Stagger with inline style="--rv-delay: .1s". Respects
   reduced-motion. */
.reveal-up {
  opacity: 0;
  transform: translateY(28px);
  transition:
    opacity 0.7s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: var(--rv-delay, 0s);
}

.reveal-up.is-visible {
  opacity: 1;
  transform: none;
}

/* Pop entrance. Same trigger and the same --rv-delay stagger as .reveal-up,
   but scaled up from small instead of lifted from below, with a short
   overshoot on the way in. For a chip or badge that belongs to the thing it
   sits on, arriving in place reads better than sliding in from underneath.
   Do not put this on anything that carries its own transform for placement:
   the entrance owns the transform property. */
.reveal-pop {
  opacity: 0;
  transform: scale(0.82);
  transition:
    opacity 0.45s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
  transition-delay: var(--rv-delay, 0s);
}

.reveal-pop.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal-up,
  .reveal-pop {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Fluid spacing utilities — scale with viewport */
.u-mt-fluid-xs {
  margin-top: var(--gap-fluid-xs) !important;
}
.u-mt-fluid-sm {
  margin-top: var(--gap-fluid-sm) !important;
}
.u-mt-fluid-md {
  margin-top: var(--gap-fluid-md) !important;
}
.u-mt-fluid-lg {
  margin-top: var(--gap-fluid-lg) !important;
}
.u-mt-fluid-xl {
  margin-top: var(--gap-fluid-xl) !important;
}

.u-mb-fluid-xs {
  margin-bottom: var(--gap-fluid-xs) !important;
}
.u-mb-fluid-sm {
  margin-bottom: var(--gap-fluid-sm) !important;
}
.u-mb-fluid-md {
  margin-bottom: var(--gap-fluid-md) !important;
}
.u-mb-fluid-lg {
  margin-bottom: var(--gap-fluid-lg) !important;
}
.u-mb-fluid-xl {
  margin-bottom: var(--gap-fluid-xl) !important;
}

/* ── Scrolling ──────────────────────────────────────────────
   style.css:3 already sets `scroll-behavior: smooth` on the
   root, site wide. Two things it is missing, added here rather
   than there because utilities.css loads last and legacy CSS is
   not edited without a reason to be in it.

   1. A landing offset. The header is `position: fixed`, so a
      jump to an in-page anchor puts the target under the bar:
      measured on the case study contents rail, the heading
      landed 96px above the top of its own text. `scroll-padding`
      on the scroll container is the property for that, and it
      fixes every anchor on the site at once rather than asking
      each target to carry its own `scroll-margin`.

   2. A reduced-motion escape. Smooth scrolling animates every
      jump, and for a visitor who has asked the system for less
      motion an animated jump is the thing they asked not to
      have. `auto` is instant, not broken. */
:root {
  scroll-padding-top: var(--sticky-offset);
}

@media (prefers-reduced-motion: reduce) {
  :root {
    scroll-behavior: auto;
  }
}
