/* ==========================================================================
   PAGE :: HOME
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: desktop node-id 2079-1484  |  mobile node-id 2106-2627

   Linked by index.html itself, after css/main.css:

     <link rel="stylesheet" href="/css/main.css" />
     <link rel="stylesheet" href="/css/pages/home.css" />

   It is NOT imported by the manifest. See block 7 of css/main.css for why.

   Every section on the homepage is a banner-delimited block in this file,
   in the order the sections appear on the page. One file per page, not one
   per section. Copy the shape from css/pages/_EXAMPLE-page.css for a new
   page.

   Every selector in a block starts with that SECTION's prefix, `hero_`,
   `problem_`, and so on, never `home_`. The prefix is what stops two
   sections in one file colliding, and it is what lets a section move to
   another page without a rename.
   ========================================================================== */

/* hero section css */

.hero_section {
  position: relative;
  padding-bottom: var(--hero-bottom);
  /* CLIPPED ON X ONLY. The decor items bleed past both side edges by design
     and have to stay clipped, or they widen the page. Nothing in here bleeds
     vertically, and .hero_brand deliberately sits ABOVE the section's top
     edge, so the Y axis is left visible. `clip` on one axis and `visible` on
     the other is a legal pair; with `hidden` the visible side would be
     forced to `auto` and the section would grow a scrollbar. */
  overflow-x: clip;
  overflow-y: visible;
}

/* --------------------------------------------------------------------------
   THE BRAND TAB
   --------------------------------------------------------------------------
   Frame 2777:20 draws the paper tab 243x64 at the top centre of the page,
   with the eyebrow at y 82 under it, and draws no links and no CTA. That is
   the resting state of this page: the bar is hidden (data-nav-hidden-at-rest
   in index.html) and this tab is the only lockup on screen.

   media/strike-while-lockup.svg is the whole cut-out, sheet and torn edge
   included, which is why this is one <img> and not a wrapper plus a mask.

   PLACED IN THE BAR'S BAND, not under it. The nav is sticky, so it holds its
   72px at the top of the page whether it is visible or not. Pulling this up
   by exactly that height lands it at page y 0, which is where the nav's own
   lockup sits (measured, both at 1440 and at 375), so revealing the bar on a
   scroll up does not shift the tab by a pixel, and the two never read as two
   different tabs. It also keeps the hero's own rhythm untouched: the eyebrow
   stays at y 82, exactly as the frame draws it.

   The bar carries --nav-z, so on the way in it passes over this rather than
   under it. No z-index needed here.
   -------------------------------------------------------------------------- */
.hero_brand {
  position: absolute;
  top: calc(var(--nav-height) * -1);
  left: 50%;
  transform: translateX(-50%);
  width: var(--nav-lockup-w);
  max-width: 100%;
  height: auto;
  /* Decorative, and the nav's copy is the real link. Nothing to click. */
  pointer-events: none;
}

.hero_container {
  position: relative;
}

.hero_rhythm {
  padding-top: var(--hero-padding-top);
}

.hero_layout {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.hero_eyebrow {
  color: var(--color-ink);
  opacity: var(--opacity-deemphasis);
}

.hero_heading {
  max-width: 52.5rem;
  margin-top: var(--hero-eyebrow-gap);
}

/* underline rides the phrase so it survives a reflow.
   left bottom, not bottom: the reveal grows the background's width from 0%,
   and a centred x-position would draw it outward from the middle instead of
   left to right. At the finished 100% width the two are identical. */
.hero_heading-mark {
  --line-mark-height: 0.3125rem;
  --line-mark-dur: 1200ms;
  display: inline-block;
  background-image: url("/media/home/hero-underline.svg");
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% var(--line-mark-height);
}

.hero_tagline {
  max-width: 50rem;
  opacity: var(--opacity-deemphasis);
}

.hero_form {
  width: 100%;
  max-width: 40rem;
  margin-top: var(--hero-form-gap);
}

.hero_decor-item {
  position: absolute;
  height: auto;
}

.hero_decor-item.is-topleft {
  top: 0;
  left: -14.6094%;
  width: 34.0920%;
}

.hero_decor-item.is-topright {
  top: 0;
  right: -8.6232%;
  width: 33.7013%;
}

.hero_decor-item.is-left {
  top: 27.7110rem;
  left: -12.1246%;
  width: 23.3312%;
}

.hero_decor-item.is-right {
  top: 28.4933rem;
  right: -2.1773%;
  width: 14.5649%;
}

.hero_decor-item.is-center {
  position: static;
  align-self: flex-start;
  margin-top: var(--hero-center-gap);
  margin-left: 35.2344%;
  width: 26.9531%;
}

/* intro section css */

.intro_section {
  position: relative;
  padding-bottom: var(--section-large);
}

.intro_layout {
  display: grid;
  grid-template-columns: 48.4375% 46.875%;
  justify-content: space-between;
  align-items: start;
}

.intro_copy {
  padding-top: var(--intro-copy-offset);
}

/* Nicola Display Regular 32/40, not the h4 class: that one is Medium */
.intro_para {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: var(--weight-regular);
  line-height: var(--lh-h4);
  letter-spacing: var(--ls-h4);
  color: var(--color-ink-copy);
}

.intro_para + .intro_para {
  margin-top: var(--intro-para-gap);
}

/* same trick as the hero, blue stroke here */
.intro_mark {
  --line-mark-height: 0.3rem;
  --line-mark-dur: 1500ms; /* draws left to right, unhurried */
  display: inline-block;
  background-image: url("/media/home/intro-underline.svg");
  background-repeat: no-repeat;
  background-position: left calc(100% + var(--space-tiny));
  background-size: 100% var(--line-mark-height);
}

.intro_cta {
  margin-top: var(--intro-cta-gap);
}

.intro_checks {
  display: flex;
  gap: var(--space-large);
  margin-top: var(--intro-checks-gap);
}

.intro_check {
  display: flex;
  align-items: center;
  gap: var(--space-xsmall);
}

.intro_check-icon {
  flex-shrink: 0;
}

.intro_media {
  display: block;
  width: 100%;
  height: auto;
}

/* client logo marquee section css */

.logos_section {
  position: relative;
  display: flex;
  align-items: center;
  height: var(--logos-bar-height);
}

.logos_section::before {
  content: "";
  position: absolute;
  top: calc((var(--logos-bar-height) - var(--logos-bar-art, 8.5rem)) / 2);
  right: 0;
  left: 0;
  height: var(--logos-bar-art, 8.5rem);
  background-image: url("/media/home/marquee-bar.png");
  background-repeat: repeat-x;
  background-position: left top;
  background-size: var(--logos-bar-tile, 1440px) var(--logos-bar-art, 8.5rem);
}
.logos_marquee {
  position: relative;
}

.home_marquee {
  margin-top: var(--space-xsmall);
}

.logos_logo {
  display: block;
  max-width: none;
}

.logos_logo[src*="sumoquote"]           { width: calc(226.498px * var(--logos-scale, 1)); height: calc(33.531px * var(--logos-scale, 1)); }  /* 2451:12 */
.logos_logo[src*="aspire"]              { width: calc(108.409px * var(--logos-scale, 1)); height: calc(52.0px * var(--logos-scale, 1)); }  /* 2451:24 */
.logos_logo[src*="truckbase"]           { width: calc(236.258px * var(--logos-scale, 1)); height: calc(28.059px * var(--logos-scale, 1)); }  /* 2451:28 */
.logos_logo[src*="smartmoving"]         { width: calc(224.697px * var(--logos-scale, 1)); height: calc(32.1px * var(--logos-scale, 1)); }  /* 2451:33 */
.logos_logo[src*="quipli"]              { width: calc(121.467px * var(--logos-scale, 1)); height: calc(34.096px * var(--logos-scale, 1)); }  /* 2451:49 */
.logos_logo[src*="servicetitan"]        { width: calc(222.85px * var(--logos-scale, 1)); height: calc(45.044px * var(--logos-scale, 1)); }  /* 2451:52 */
.logos_logo[src*="knowhow"]             { width: calc(169.023px * var(--logos-scale, 1)); height: calc(36.035px * var(--logos-scale, 1)); }  /* 2451:55 */
.logos_logo[src*="review-wave"]         { width: calc(156.741px * var(--logos-scale, 1)); height: calc(33.829px * var(--logos-scale, 1)); }  /* 2451:58 */
.logos_logo[src*="sphinxbio"]           { width: calc(190.901px * var(--logos-scale, 1)); height: calc(51.479px * var(--logos-scale, 1)); }  /* 2451:61 */
.logos_logo[src*="tymber"]              { width: calc(188.208px * var(--logos-scale, 1)); height: calc(48.905px * var(--logos-scale, 1)); }  /* 2451:64 */
.logos_logo[src*="pushpress"]           { width: calc(191.759px * var(--logos-scale, 1)); height: calc(48.169px * var(--logos-scale, 1)); }  /* 2451:69 */
.logos_logo[src*="papertrail"]          { width: calc(173.742px * var(--logos-scale, 1)); height: calc(30.836px * var(--logos-scale, 1)); }  /* 2451:72 */
.logos_logo[src*="credit-repair-cloud"] { width: calc(170.694px * var(--logos-scale, 1)); height: calc(36.035px * var(--logos-scale, 1)); }  /* 2451:73 */
.logos_logo[src*="spry"]                { width: calc(93.918px * var(--logos-scale, 1)); height: calc(33.406px * var(--logos-scale, 1)); }  /* 2451:103 */
.logos_logo[src*="deardoc"]             { width: calc(145.509px * var(--logos-scale, 1)); height: calc(34.748px * var(--logos-scale, 1)); }  /* 2451:105 */

/* proof / case-study marquee section css */

.proof_section {
  position: relative;
  padding-top: var(--space-huge);
  padding-bottom: var(--space-huge);
  overflow: clip;
}

/* .max-width-large's 52rem wraps this to 3 lines; 57rem is the narrowest
   width that still balances it into 2. */
.proof_heading {
  max-width: 57rem;
}

/* The generous gap above the cards is the section's own rhythm, not the
   scaffold's section padding, because it sits between two children of the
   same section rather than at its top or bottom edge. */
.proof_marquee {
  margin-top: var(--space-huge);
}

/* the-problem / testimonials + copy section css */

.problem_section {
  position: relative;
  padding-top: var(--space-huge);
  overflow: clip;
}

/* Narrow, left-aligned reading column, centred as a block. Shared by the
   heading group and the lower copy block so both wrap at the same width.
   45rem matches the reading column's authored width in the Figma export. */
.problem_wrapper {
  width: 100%;
  max-width: 45rem;
  margin-left: auto;
  margin-right: auto;
}

/* h1-h6 default to text-wrap:balance (see 02-reset.css), which rebalances
   this heading into "...expertise" starting its own line. A plain greedy
   wrap keeps "expertise" with the line above, matching the export. */
.problem_heading {
  text-wrap: wrap;
}

/* Trust 2A 24/32, the same face/size the export uses for both the intro
   line and the copy block below the cards (title-1 scale, regular weight,
   not the Figtree body-copy scale). */
.problem_intro {
  margin-top: var(--space-medium);
  font-family: var(--font-body);
  font-size: var(--fs-title-1);
  line-height: var(--lh-title-1);
  letter-spacing: var(--ls-title-1);
  font-weight: var(--weight-regular);
  color: var(--color-ink-copy);
}

/* Same trick as .hero_heading-mark / .intro_mark: a hand-drawn stroke riding
   under the phrase as a background image, so it survives a reflow. The
   three assets are all 6px tall raster strokes, stretched to the phrase's
   width exactly like the existing marks; nothing here invents a new idiom.

   display: inline (not inline-block) plus box-decoration-break: clone is
   what makes the stroke follow a wrapped phrase: each wrapped line becomes
   its own fragment box and gets its own copy of the background, sized and
   positioned against that fragment only, instead of one image stretched
   across the whole (invisible, multi-line) bounding box. */
.problem_mark {
  --line-mark-height: 0.375rem;
  --line-mark-dur: 1000ms; /* slower than --dur-slow: this reads as a drawn stroke */
  display: inline;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  background-repeat: no-repeat;
  background-position: left calc(100% + var(--space-xxsmall));
  background-size: 100% var(--line-mark-height);
}

/* Heavier than the blue strokes: this one sits under the section's largest
   type, where a 6px line reads as a hairline. The asset is solid ink across
   all six of its source rows, so scaling the height just thickens it. */
.problem_mark.is-red {
  --line-mark-height: 0.5rem;
  background-image: url("/media/home/problem/red-line.png");
}

/* Both blue strokes get a touch more weight than the 6px default too, just
   short of the red one, since they sit under smaller type. */
.problem_mark.is-blue-lg {
  --line-mark-height: 0.4375rem;
  background-image: url("/media/home/problem/blue-line-lg.png");
}

.problem_mark.is-blue-sm {
  --line-mark-height: 0.4375rem;
  background-image: url("/media/home/problem/blue-line-sm.png");
}

/* Scroll-in growth, driven by js/modules/reveal.js via [data-reveal-mark].
   Gated behind .reveal-ready so a blocked/failed script never leaves the
   line invisible: the base rule above (full width) is the fail-safe state.
   Only background-size (not opacity) animates, so the text underneath
   never fades or moves; each wrapped-line fragment grows independently but
   in sync, since box-decoration-break: clone gives each one its own size. */
.reveal-ready .problem_mark[data-reveal-mark],
.reveal-ready .hero_heading-mark[data-reveal-mark] {
  background-size: 0% var(--line-mark-height);
}

/* The intro stroke is the one long line on a quiet paragraph, so it draws
   noticeably slower than the problem marks and starts as soon as it is
   observed (no lead-in delay); the observer itself also fires it earlier,
   via data-reveal-mark="early" in the markup. */
.reveal-ready .intro_mark[data-reveal-mark] {
  --line-mark-delay: 0ms;
  background-size: 0% var(--line-mark-height);
}

.reveal-ready [data-reveal-mark].is-revealed {
  animation: line-mark-reveal var(--line-mark-dur, var(--dur-slow)) var(--ease-out)
    var(--line-mark-delay, var(--dur-base)) both;
}

@keyframes line-mark-reveal {
  from { background-size: 0% var(--line-mark-height); }
  to { background-size: 100% var(--line-mark-height); }
}

/* --------------------------------------------------------------------------
   TESTIMONIAL CALLOUTS
   --------------------------------------------------------------------------
   Three speech cards. A slight rotation and vertical stagger per card keeps
   them organic rather than a rigid three-up grid.
   -------------------------------------------------------------------------- */
.problem_cards {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-xlarge) var(--space-large);
  margin-top: var(--space-xlarge);
}

.problem_card {
  position: relative;
  isolation: isolate;
  flex: 0 1 20rem;
  max-width: 22rem;
  background-color: #e5e2e2;
  transform: translateY(var(--card-ty, 0rem)) rotate(var(--card-rotate, 0deg));
}

.problem_card.is-a { --card-rotate: -2deg; }
.problem_card.is-b { --card-rotate: 1deg; --card-ty: 1rem; flex-basis: 22rem; max-width: 24rem; }
.problem_card.is-c { --card-rotate: -1.5deg; }

/* Idle most of the cycle, then a quick, tiny nudge, like a notification
   tapping for attention. Base position always returns via the --card-*
   vars so this layers on top of each card's resting tilt/offset. */
@keyframes problem-card-nudge {
  0%, 74%, 100% {
    transform: translateY(var(--card-ty, 0rem)) rotate(var(--card-rotate, 0deg));
  }
  81% {
    transform: translateY(calc(var(--card-ty, 0rem) - 0.05rem)) rotate(calc(var(--card-rotate, 0deg) - 1deg));
  }
  88% {
    transform: translateY(var(--card-ty, 0rem)) rotate(calc(var(--card-rotate, 0deg) + 1deg));
  }
  95% {
    transform: translateY(var(--card-ty, 0rem)) rotate(calc(var(--card-rotate, 0deg) - 0.4deg));
  }
}

@media (prefers-reduced-motion: no-preference) {
  .problem_card {
    animation-name: problem-card-nudge;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
  }

  /* Slightly different durations/delays so the three never nudge in sync. */
  .problem_card.is-a { animation-duration: 6.2s; animation-delay: 0s; }
  .problem_card.is-b { animation-duration: 6.6s; animation-delay: 0.6s; }
  .problem_card.is-c { animation-duration: 7s; animation-delay: 1.2s; }
}

.problem_card-text {
  padding: var(--space-medium);
  font-family: var(--font-body);
  font-size: var(--fs-medium);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-normal);
  color: var(--color-ink-copy);
}

.problem_card-tail {
  position: absolute;
  left: var(--space-large);
  top: 92%;
  width: 20%;
  z-index: -1;
}

/* --------------------------------------------------------------------------
   LOWER COPY BLOCK
   -------------------------------------------------------------------------- */
.problem_copy p {
  font-family: var(--font-body);
  font-size: var(--fs-title-1);
  line-height: var(--lh-title-1);
  letter-spacing: var(--ls-title-1);
  font-weight: var(--weight-regular);
  color: var(--color-ink-copy);
}

.problem_copy {
  margin-top: var(--space-xxlarge);
}

/* the opening line carries the section's point, so it leads heavier */
.problem_copy p:first-child {
  font-weight: var(--weight-medium);
}

.problem_copy p + p {
  margin-top: var(--space-medium);
}

/* --------------------------------------------------------------------------
   MOUNTAIN ARTWORK
   --------------------------------------------------------------------------
   Bleeds off the bottom-right corner and partly over the divider below.
   Absolute to .problem_section, so it tracks the true viewport edge like
   the divider it sits on. The margin-right below caps that drift: it holds
   the asset to the right edge of a virtual 1840px canvas centred in the
   viewport once the screen gets wider than that, instead of letting the
   gap grow without limit on ultrawide monitors. max() clamps it to 0 below
   1840px, so the asset reads as flush right again down to mobile, no
   breakpoint needed. Negative z-index keeps it behind the copy/cards, since
   it is a background decoration, not content. */

/* Figma parks this asset off-canvas (2315:8058 at x=1502 in a 1440 frame), so
   the placement below is the build's own. Its foot lands inside the 3.2rem
   divider band that follows the section, 2rem shy of the band's bottom
   edge (the section's bottom edge IS the divider's bottom edge, since the
   divider is the section's last flow child). */
.problem_mountain {
  position: absolute;
  right: 0;
  bottom: 1.2rem;
  margin-right: max(0px, calc((100vw - 1840px) / 2));
  z-index: -1;
  height: 12.5rem; /* the asset is 511x190 at 1x; width follows naturally */
  width: auto;
}

/* --------------------------------------------------------------------------
   SECTION DIVIDER
   --------------------------------------------------------------------------
   Full-bleed, so it sits outside .padding-global like the marquees do. The
   shadow (::before) is the soft cast falling from this section, kept behind
   via z-index; the textured border (::after) is the rough tear at the seam
   itself, painted on top. Pseudo-elements instead of two extra img elements.
   -------------------------------------------------------------------------- */
.problem_divider {
  position: relative;
  width: 100%;
  height: 3.2rem;
  margin-top: var(--space-xhuge);
}

.problem_divider::before,
.problem_divider::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 100%;
  min-width: 91.75rem;
  transform: translateX(-50%);
  background-repeat: no-repeat;
}

.problem_divider::before {
  background-image: url("/media/home/problem/section-shadow.png");
  background-position: center top;
  background-size: 100% 100%;
  z-index: 0;
}

.problem_divider::after {
  background-image: url("/media/home/problem/grey-textured-border.png");
  background-position: center bottom;
  background-size: 100% 2.5rem;
  z-index: 2; /* highest of the three layers, always on top of the mountain */
}

@media screen and (max-width: 991px) {
  .problem_cards {
    gap: var(--space-large) var(--space-medium);
    margin-top: var(--space-large);
  }

  .problem_copy {
    margin-top: var(--space-large);
  }

  .problem_mountain {
    height: 10rem;
  }

  /* intro goes single column: the panel drops under the copy */
  .intro_layout {
    grid-template-columns: 1fr;
    justify-content: stretch;
    gap: var(--space-xlarge);
  }

  .intro_copy {
    padding-top: 0;
  }

  .intro_media {
    max-width: 37.5rem;
  }

  /* the side plates crowd the copy once the container stops shrinking */
  .hero_decor-item.is-left,
  .hero_decor-item.is-right {
    display: none;
  }

  .hero_heading,
  .hero_tagline {
    max-width: none;
  }

  /* The compact bar is shorter and its lockup is smaller, and the tab has to
     keep landing exactly on the bar's own lockup. Both numbers are the nav's
     own mobile tokens rather than new ones, so the two cannot drift. */
  .hero_brand {
    top: calc(var(--nav-height-mobile) * -1);
    width: var(--nav-lockup-w-mobile);
  }

  .proof_heading {
    max-width: 40rem;
  }
}

@media screen and (max-width: 767px) {
  .problem_cards {
    flex-direction: column;
    align-items: center;
    gap: var(--space-large) 0;
    margin-top: var(--space-large);
  }

  .problem_card,
  .problem_card.is-a,
  .problem_card.is-b,
  .problem_card.is-c {
    flex-basis: auto;
    width: 100%;
    max-width: 20rem;
  }

  .problem_copy {
    margin-top: var(--space-xlarge);
  }

  .problem_mountain {
    height: 8rem;
  }

  /* the field and its key stack, both full width. 2106:2682 */
  .form_inline.is-embedded {
    flex-direction: column;
    gap: var(--field-stack-gap);
    min-height: 0;
    padding-right: 0;
    background-color: transparent;
  }

  .form_inline.is-embedded .form_input {
    width: 100%;
    min-height: var(--btn-height);
    background-color: var(--color-surface-field);
    border-radius: var(--radius-field);
  }

  .form_inline.is-embedded .button {
    width: 100%;
  }

  .form_inline.is-embedded:focus-within {
    outline: none;
  }

  .hero_layout {
    text-align: center;
  }

  /* everything centres and the CTA goes full width. 2106:2694 */
  .intro_layout {
    text-align: center;
  }

  .intro_para,
  .intro_checks {
    text-align: center;
  }

  .intro_cta {
    display: flex;
    width: 100%;
  }

  .intro_checks {
    flex-direction: column;
    align-items: center;
    gap: var(--space-xsmall);
  }

  /* the cards run off the edge and swipe. 2106:2812 */
  .proof_marquee {
    margin-top: var(--space-xlarge);
  }

}



/* mobile, measured off the 440 frame 2106-2627 */
@media screen and (max-width: 479px) {
  /* the marked phrase wraps on mobile, so each line gets its own stroke. 2106:2658 / 2659 */
  .hero_heading-mark {
    display: inline;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }

  .intro_mark {
    display: inline;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }

  /* 20/28 -2%, not the h4 step. 2106:2689 */
  .intro_para {
    font-size: var(--fs-title-2);
    line-height: var(--lh-title-2);
    letter-spacing: var(--intro-para-ls);
  }

  /* copy to panel is 32 on mobile, checks stack at 12. 2106:2706 / 2701 */
  .intro_layout {
    gap: var(--space-medium);
  }

  .intro_checks {
    gap: var(--intro-check-row-gap);
  }

  /* only three plates survive on mobile: the roofer and the dentist are not
     drawn at 440 at all. positions are % of the 392 container. */
  .hero_decor-item.is-topleft {
    top: 2.3125rem;
    left: -41.33%;
    width: 68%;
  }

  .hero_decor-item.is-topright {
    top: 8%;
    right: -38%;
    width: 70.79%;
  }

  /* 440 crops the same art to a taller box, anchored bottom. 2106:2688 */
  .hero_decor-item.is-center {
    margin-left: 21.17%; /* 107px */
    width: 57.91%; /* 227px */
    object-fit: cover;
    object-position: bottom;
  }

  .intro_media {
    max-width: none;
  }

  /* 340 wide against the 392 container, 32/40 not the h2 step. 2106:2811 */
  .proof_heading {
    max-width: 21.25rem;
    font-size: var(--proof-heading-fs);
    line-height: var(--proof-heading-lh);
  }

  /* mobile rhythm is 80 between sections, 40 inside. 2106-2627 */
  .proof_section {
    padding-top: var(--space-xxlarge);
    padding-bottom: var(--section-rhythm-mobile);
  }

  .proof_marquee {
    margin-top: var(--section-rhythm-mobile);
  }

  .problem_section {
    padding-top: var(--section-rhythm-mobile);
  }

  .problem_wrapper {
    max-width: none;
    width: auto;
    margin-left: var(--space-small);
    margin-right: var(--space-small);
  }

  /* the bubbles stagger rather than centre. 2106:2848 / 2852 / 2856 */
  .problem_cards {
    align-items: stretch;
  }

  .problem_card,
  .problem_card.is-a,
  .problem_card.is-c {
    width: 52%;
    max-width: none;
    margin-right: auto;
  }

  .problem_card.is-b {
    width: 62%;
    max-width: none;
    margin-left: auto;
    margin-right: 0;
  }
  .problem_card.is-c{
    margin-top: 2rem;
  }
  .problem_card-text{
    padding: var(--space-small);
  }
  .problem_mountain {
    height: 6rem;
  }
  .hero_decor-item.is-center{
    margin-top: 3rem
  }

}

/* the reason (two fundamental questions) section css */
/* FIGMA: closed state 2227-1229, open state 2238-4740, mobile 2275-889.
   Both desktop states are standalone frames, not children of 2315-7821,
   so where this lands in the page was originally a judgement call rather
   than a measured position (the heading reads as a direct answer to
   .problem_section's closing line, "just another complicated SaaS product
   that their team will never adopt") - built ahead of Deals Dying on that
   basis, then moved to sit right after it instead, per direct request. */

/* Neither side is the --space-huge both used pre-reorder: this section's
   own padding is a judgement call (a standalone Figma frame with no fixed
   neighbour, see the comment above), but .process_section's 140px top pad
   IS a real, [LOCKED] measurement off its own frame (2227:529) - so on
   that boundary too, the fix belongs entirely on this side, not by
   shaving a verified figure that also carries that section's inset
   shadow. Top: Deals Dying's own content already carries a 96px bottom
   padding (.padding-section-medium), so this used to stack into ~192px of
   dead space with no colour break to justify it. Bottom: same idea one
   boundary later, 96 + process's own 140 read as too much once measured
   against the rest of the page's section-to-section rhythm. */
/* No background-color of its own any more - .dealsdying-reason_group
   supplies one shared backdrop for this section and .dealsdying_section
   together, so painting a second one here would just hide it again.
   padding-top trimmed a second time (48 -> 32px) after the shared
   backdrop made the boundary read as one continuous surface rather than
   two sections that need a visible gap between them at all. */
.reason_section {
  position: relative;
  padding-top: var(--space-medium);
  padding-bottom: var(--space-large);
}

/* Same reasoning as .dealsdying_content: needs to out-rank the shared
   .dealsdying_texture (z-index: 0) now that the two sit in the same
   .dealsdying-reason_group, or the texture - itself positioned - paints
   over this section's actual copy instead of staying behind it. */
.reason_content {
  position: relative;
  z-index: 1;
}

.reason_heading {
  max-width: var(--reason-heading-w);
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

.reason_cards {
  margin-top: var(--space-xlarge);
}

/* Specificity over .grid-2's own gap, not source order: 40px here, not the
   32px shared token .grid-2 defaults to. */
.reason_cards.grid-2 {
  gap: var(--reason-cards-gap);
}

/* Grid, not flex: both faces are stacked into the same cell below (see
   .reason_face), which makes the row size to whichever face is TALLER -
   closed or open - permanently, not just whichever one is currently
   showing. That is what stops the card (and its side-by-side neighbour,
   stretched to match via .grid-2's default align-items:stretch) from
   resizing every time a face toggles. */
.reason_card {
  display: grid;
  grid-template-areas: "face";
  min-height: var(--reason-card-min-h);
  background-color: var(--color-reason-card-bg);
}

/* Both faces share the same grid-area, so they overlap instead of
   stacking top-to-bottom - the grid's own equivalent of position:absolute
   but without opting out of the sizing calculation the way absolute
   positioning would. Grid's default align-items/justify-items: stretch
   fills the shared cell exactly like the old flex:1 did. */
.reason_face {
  position: relative;
  grid-area: face;
  display: flex;
  flex-direction: column;
  padding: var(--reason-card-pad);
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-inout);
}

/* NOT display:none. The whole point of the grid-area overlap above is
   that both faces keep contributing to the row's height at all times -
   display:none would pull the hidden one out of that calculation and
   bring back the resize this is meant to prevent. visibility:hidden gets
   the same "invisible and untouchable" result (also drops it from the a11y
   tree and tab order, like `hidden` normally does) while still reserving
   its space. */
.reason_face[hidden] {
  display: flex;
  visibility: hidden;
  pointer-events: none;
}

/* Crossfade, driven by js/modules/reason.js: the outgoing face gets this
   class and fades to 0 over --dur-base, THEN gets `hidden` once the
   timeout matching that duration elapses. The incoming face gets it
   applied a frame before `hidden` is lifted, so it starts the reveal at
   opacity 0 and fades up the same way, rather than popping straight to
   fully visible the instant the swap happens. */
.reason_face.is-fading {
  opacity: 0;
}

/* Whichever row carries the toggle button sits on the card's bottom edge
   regardless of how tall the illustration or the paragraph stack above it
   runs - closed state's question+toggle row, open state's toggle alone. */
.reason_face > *:last-child {
  margin-top: auto;
}

/* The open face's toggle is a bare last child, not nested in a .reason_row
   with a heading to push against - so, unlike the closed face's button,
   it needs its own horizontal alignment or it sits at the row's start
   (the left edge) instead of flush with the badge/heading row above it. */
.reason_face > .reason_toggle {
  align-self: flex-end;
}

.reason_media-row {
  display: flex;
  align-items: flex-start;
}

/* Below 992px (see the .reason_media-row rule in the tablet query further
   down) the image stays a normal flex item, sized by flex:1/height:auto.
   At desktop it is taken out of flow entirely - see the position:absolute
   override below - so these are dead there but still what renders it at
   tablet and mobile widths. */
.reason_media {
  display: block;
  flex: 1;
  min-width: 0;
  max-width: 100%;
  height: auto;
}

/* Desktop only: each illustration is individually placed in Figma, not
   derived from the badge/row it sits next to (see the token comment in
   css/01-tokens.css), so it is positioned absolutely against .reason_face
   rather than laid out in the .reason_media-row flex it shares with the
   badge - the badge stays in flow at its own 32/32 inset, unaffected. */
@media screen and (min-width: 992px) {
  .reason_media {
    position: absolute;
    top: var(--reason-media-top);
    left: var(--reason-media-left);
    width: var(--reason-media-w);
    height: var(--reason-media-h);
    max-width: none;
    object-fit: cover;
  }

  .reason_card:nth-child(1) {
    --reason-media-top: var(--reason-media-1-top);
    --reason-media-left: var(--reason-media-1-left);
    --reason-media-w: var(--reason-media-1-w);
    --reason-media-h: var(--reason-media-1-h);
  }

  .reason_card:nth-child(2) {
    --reason-media-top: var(--reason-media-2-top);
    --reason-media-left: var(--reason-media-2-left);
    --reason-media-w: var(--reason-media-2-w);
    --reason-media-h: var(--reason-media-2-h);
  }
}

/* .reason_row is reused for two different pairings, which need opposite
   packing: the closed face's heading+toggle want to sit at opposite ends
   of the row (space-between), but the open face's badge+heading want to
   sit snug together at the fixed --reason-row-gap - space-between there
   would spend the row's entire leftover width as the gap between just
   those two items, stranding the heading far right of its badge instead
   of right beside it. */
.reason_row {
  display: flex;
  align-items: center;
  gap: var(--reason-row-gap);
}

.reason_face.is-closed .reason_row {
  justify-content: space-between;
}

.reason_face.is-open .reason_row {
  justify-content: flex-start;
}

/* Only meaningful in that same open-face row: lets the heading claim the
   rest of the row's width (so a longer question still wraps at the full
   available width) without moving where it starts. */
.reason_face.is-open .reason_question {
  flex: 1;
}

.reason_question {
  color: var(--color-text);
}

.reason_badge {
  position: relative;
  isolation: isolate;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--reason-badge-size);
  height: var(--reason-badge-size);
  font-family: var(--font-body);
  font-size: var(--reason-badge-fs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--reason-badge-ls);
  color: var(--reason-badge-ink);
}

/* Fill painted by a pseudo-element so the deckle filter can rough up the
   square's edge without rippling the digit sitting on top of it - same
   reasoning as .accordion_trigger::before, same shared filter. 2227:1234 */
.reason_badge::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--color-brand);
  filter: url(#sw-deckle);
}

.reason_body {
  margin-top: var(--reason-body-gap);
  color: var(--color-ink-copy);
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
}

.reason_body > * + * {
  margin-top: var(--reason-body-p-gap);
}

.reason_list li {
  position: relative;
  padding-left: 1em;
}

.reason_list li + li {
  margin-top: var(--reason-body-p-gap);
}

/* Centred on the FIRST line specifically (half its line-height), not on
   the li's own total height - centring on the whole box instead floats
   the dot between the lines once a narrow card wraps an item to two,
   like the mobile card's third bullet. */
.reason_list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc((var(--case-text-lh) - var(--reason-list-dot)) / 2);
  width: var(--reason-list-dot);
  height: var(--reason-list-dot);
  border-radius: 50%;
  background-color: currentColor;
}

/* The icon is the exported Figma asset (circle backdrop + glyph baked
   together), swapped by which .reason_face is visible - never a filter or
   a rotate. See the token comment on --reason-toggle-size. */
.reason_toggle {
  flex-shrink: 0;
  display: block;
  width: var(--reason-toggle-size);
  height: var(--reason-toggle-size);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.reason_toggle img {
  display: block;
  width: 100%;
  height: 100%;
}

.reason_outro {
  max-width: 30rem;
  margin: var(--space-xlarge) auto 0;
  color: var(--color-ink-copy);
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  text-align: center;
}

.reason_outro p + p {
  margin-top: var(--reason-body-p-gap);
}

/* No mobile frame supplied for this section - interpolated, same as the
   note on --reason-card-min-h. This has to sit in its OWN media query
   here, after .reason_card's desktop rule above, rather than in the
   shared 767/479 blocks earlier in this file: those come BEFORE this
   component's desktop block in source order, so equal-specificity rules
   placed there would lose to the desktop rule regardless of viewport.

   991, not 767: .grid-2 itself only collapses the two cards to one
   column at 767 (css/04-layout.css), but each card's badge+illustration
   row runs out of room well before that - at 768-991 the cards are still
   side by side, so the side-by-side media row was left squeezing the
   illustration into a sliver next to the badge while --reason-card-min-h
   (a figure LOCKED off the 1440 frame) held the card at its full desktop
   height regardless, stranding a wall of empty grey underneath. Stacking
   badge-over-illustration - and dropping the fixed min-height - starts at
   the same 991 tablet collapse point the rest of the site uses, well
   before the two-card grid itself collapses. */
@media screen and (max-width: 991px) {
  /* Structural, not just smaller: the real 440 mobile frame (2275-889)
     stacks badge above illustration rather than sitting them side by
     side - there is no width left for both once the illustration needs
     to read at any size below desktop. The exact 440-anchor figures
     (padding, toggle size, a tighter gap here) are [LOCKED] and live in
     the 479px block below, not here - this block only owns the
     [INTERP] structural cut. */
  .reason_media-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-small);
  }

  .reason_media {
    width: 100%;
  }

  /* The 367px figure is [LOCKED] off the 1440 frame specifically, sized
     for that width's badge+illustration row - it does not scale down
     with the card, so below desktop it just strands empty grey under a
     shrunken illustration regardless of whether a neighbour is also
     stretching to match (see .reason_face's own grid-overlap comment for
     that mechanism, untouched here). Dropped so each card hugs the
     taller of its own two faces instead. */
  .reason_card {
    min-height: 0;
  }
}

/* Below 767, .grid-2 has already collapsed the two cards to one per row
   (css/04-layout.css) - each is now the only thing in its row, so
   nothing else is stretching to match it the way a side-by-side
   neighbour would at every wider tier. That removes the one reason
   .reason_face's grid-overlap reserves the TALLER of the closed/open
   face's height at all times (see the comment there): with no neighbour
   left to protect from an unwanted resize, a stacked card can instead
   just hug whichever face is actually showing, closed or open, so the
   closed state stops carrying dead space sized for a body paragraph it
   isn't displaying. The resulting height CHANGE when a card is toggled
   is then animated in js/modules/reason.js, timed to this transition. */
@media screen and (max-width: 767px) {
  .reason_card {
    overflow: hidden;
    transition: height var(--dur-base) var(--ease-inout);
  }

  .reason_face[hidden] {
    display: none;
  }
}

/* The 440 mobile anchor frame, 2275-889. Closed state only - no mobile
   "open" frame was supplied, so the open face's typography and layout are
   untouched here and stay on their desktop values at every width. */
@media screen and (max-width: 479px) {
  .reason_section {
    padding-top: var(--section-rhythm-mobile);
    padding-bottom: var(--section-rhythm-mobile);
  }

  .reason_heading {
    font-size: var(--reason-heading-fs);
    line-height: var(--reason-heading-lh);
    letter-spacing: var(--reason-heading-ls);
  }

  .reason_cards {
    margin-top: var(--reason-heading-gap);
  }

  /* Badge sits just above the illustration in 2275-889, overlapping its
     top edge by 14 of its own 48px - a page-coordinate detail not worth
     reproducing exactly (see DESIGN.md on carrying Figma coordinates into
     CSS). A small gap instead of a negative-margin overlap reads the same
     at a glance and does not risk the badge's edge and the image's edge
     fighting over which one shows on top. */
  .reason_media-row {
    gap: var(--space-xsmall);
  }

  /* Family switch off the desktop Nicola Display H5, same two-body-face
     split DESIGN.md documents elsewhere on this page - Trust 2A Medium
     here, 0 tracking rather than the H5 ladder's own. Colour is
     unchanged: --color-text (set on .reason_question at every width) is
     already this exact hex. */
  .reason_question {
    font-family: var(--font-body);
    font-size: var(--reason-question-fs);
    line-height: var(--reason-question-lh);
    font-weight: var(--weight-medium);
    letter-spacing: normal;
  }

  .reason_outro {
    max-width: var(--reason-outro-w);
    margin-top: var(--reason-outro-gap);
  }
}

/* deals dying (3 columns) section css */
/* FIGMA: node-id 2227-221 (content), 2227-220 (paper texture),
   2269-20 (mobile content, 440 frame) */

/* Shared backdrop for Deals Dying AND The Reason (2227:220, "image
   588256069" in Figma): one texture sampled once over --color-surface,
   not two separate copies each cropped/positioned independently - which
   is what actually caused the visible seam between the two sections
   earlier, not a colour mismatch. This wrapper owns the position/overflow
   .dealsdying_section used to carry alone, since the texture div now
   lives here instead of inside it. */
.dealsdying-reason_group {
  position: relative;
  overflow: hidden;
  background-color: var(--color-surface); /* #f5f5f5 */
}

.dealsdying_section {
  position: relative;
}

/* Torn-paper texture, sampled at 40% opacity in Figma. Absolutely positioned
   with a slight overflow rather than the literal Figma bleed coordinates,
   same idiom as `.hero_decor` in the section template.

   This is the base fold/crease layer only - checked directly against
   Figma's own rendered screenshot of this section (2227:221) and against
   the isolated layer render of 2227:220 itself, and neither matches a
   plain 40%-opacity blend of this file alone. The isolated render shows
   a fine speckled grain this file does not have and the two raw image
   fills behind the Figma layer (both downloaded and inspected) don't
   have either - so that grain is a Figma NOISE EFFECT on the layer, not
   part of either image fill, and needs its own overlay: see
   .dealsdying_grain below, which reuses --btn-grain (the same
   fractalNoise recipe already on the button hover, the process
   testimonial card, and the nav logo plate) rather than inventing a
   second noise recipe for one more speckled surface. */
.dealsdying_texture {
  position: absolute;
  inset: -1rem;
  background-image: url("/media/texture-paper.webp");
  background-size: cover;
  background-position: center;
  opacity: 0.4;
  pointer-events: none;
  z-index: 0;
}

.dealsdying_grain {
  position: absolute;
  inset: -1rem;
  background-image: var(--btn-grain);
  opacity: var(--btn-grain-opacity);
  pointer-events: none;
  z-index: 0;
}

.dealsdying_content {
  position: relative;
  z-index: 1;
}

/* Trims .padding-section-medium's shared 96px bottom pad down for this one
   boundary specifically, not the class itself (still used for this same
   element's own top edge, and potentially elsewhere). Cut a second time
   (48 -> 32px) alongside .reason_section's own top pad, now that the two
   sections share one backdrop (.dealsdying-reason_group) and read as a
   single surface rather than two that need a visible gap between them. */
.dealsdying_content {
  padding-bottom: var(--space-medium);
}

/* 607px in the 1440 frame, the exact width Figma wrapped the heading at
   ("Deals you could've closed are / quietly dying on your website."). Kept
   as a literal px->rem conversion, not a `ch` guess, so the two-line break
   matches exactly at the desktop anchor. 2227:222 */
.dealsdying_heading {
  max-width: 37.9375rem; /* 607px */
}

/* 24px, heading bottom to subheading top. Off-ladder (nearest tiers are 16
   and 32), carried as measured per DESIGN.md's rule for off-ladder section
   values. 2227:222 / 2227:223 */
.dealsdying_subheading {
  margin-top: 1.5rem; /* 24px */
  color: var(--color-ink-copy);
  /* This instance draws Trust 2A Regular 18/26, not the shared
     `.text-size-medium` class's Figtree 18/28 -1.2%. DESIGN.md flag #6:
     both body faces are genuinely in use across the site: this section's
     own Figma frame is Trust 2A, so it is scoped here rather than
     touching the shared class. No tracking utility on the source node,
     so 0 rather than the class's -1.2%. 2227:223 */
  font-family: var(--font-body);
  line-height: 1.625rem; /* 26px */
  letter-spacing: normal;
}

/* 72px, subheading bottom to the icon row. Off-ladder (nearest tiers are 64
   and 80). 2227:223 / 2227:228 */
.dealsdying_columns {
  margin-top: 4.5rem; /* 72px */
}

.dealsdying_item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.dealsdying_item-icon {
  display: block;
  margin-bottom: var(--space-medium); /* 32px, icon bottom to title top. On-ladder, exact match. 2227:228 / 2227:227 */
}

.dealsdying_item-title {
  margin-bottom: var(--space-small); /* 16px, title bottom to body top. On-ladder, exact match. 2227:227 / 2227:226 */
}

/* Same Trust 2A 18/26 divergence as .dealsdying_subheading, same source
   node type. 2227:226 */
.dealsdying_item-text {
  color: var(--color-ink-copy);
  font-family: var(--font-body);
  line-height: 1.625rem; /* 26px */
  letter-spacing: normal;
}

/* 850px wide, NOT centred: left-edge flush with the heading above it.
   60px above it (off-ladder), 12px/24px internal padding (both off-ladder).
   2227:239 / 2227:240 */
.dealsdying_callout {
  margin-top: 3.75rem; /* 60px */
  width: 100%;
  max-width: 53.125rem; /* 850px */
  padding: 0.75rem 1.5rem; /* 12px / 24px */
  background-color: var(--color-surface-sunken);
  text-align: center;
}

.dealsdying_callout-highlight {
  background-image: url("/media/problem-underline.svg");
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.1875rem;
  padding-bottom: 0.25rem;
}

/* At the 1440 anchor every item title is one line (Figma draws them
   `whitespace-nowrap`). "It puts your sales team on the defensive" measures
   ~406px in Nicola Display 500 at 24px/-1%, a hair over the 405.33px a
   3-column 1280 grid with a 32px gap actually gives it, so it wraps without
   this. The ~0.7px overflow lands in the empty column gutter, not on the
   next item. 2227:232 */
@media screen and (min-width: 1440px) {
  .dealsdying_item-title {
    white-space: nowrap;
  }
}

@media screen and (max-width: 991px) {
  .dealsdying_heading {
    max-width: 100%;
  }
}

/* The 440 frame is left-aligned throughout, not centred, and every gap
   below is measured off it - none of them are a simple half of the desktop
   value. 2269:20 */
@media screen and (max-width: 767px) {
  .dealsdying_subheading {
    margin-top: 0.75rem; /* 12px. 2269:22 / 2269:23 */
  }

  .dealsdying_columns {
    margin-top: 2.5rem; /* 40px, off-ladder. 2269:23 / 2269:24 */
  }

  .dealsdying_item-icon {
    /* 60px tall, matching the 440 frame. Width is left to the source PNG's
       own aspect ratio rather than a second cropped asset per icon: 60px
       tall gives 48px for the funnel and 60px for the other two, exactly
       the widths Figma draws. 2269:29 / 2269:35 / 2269:41 */
    height: 3.75rem;
    width: auto;
    margin-bottom: 0.75rem; /* 12px, icon bottom to title top. 2269:29/2269:28 */
  }

  .dealsdying_item-title {
    margin-bottom: 0.375rem; /* 6px, off-ladder. 2269:28 / 2269:27 */
  }

  .dealsdying_callout {
    margin-top: 2.5rem; /* 40px, off-ladder. 2269:39 / 2269:44 */
    max-width: 19.5rem; /* 312px: narrower than the content column, not
      full-bleed, sized to the wrapped two-line text plus padding. 2269:44 */
    padding-left: 0.75rem; /* 12px, half the desktop 24px. 2269:44/2269:46 */
    padding-right: 0.75rem;
    /* Desktop centres this line (2227:241 carries `text-center`); the 440
       frame does not - it's left-aligned like everything else in the
       section. 2269:46 */
    text-align: left;
    /* The 440 frame switches this line to Trust 2A Regular 20/28 -2%, not
       the heading-style-h4 face it uses at 1440. Scoped to this component
       rather than touching the shared class. 2269:46 */
    font-family: var(--font-body);
    font-size: 1.25rem; /* 20px */
    font-weight: var(--weight-regular);
    line-height: 1.75rem; /* 28px */
    letter-spacing: -0.02em; /* -0.4px at 20px */
    color: #575652; /* this instance only, not the shared ink token. 2269:46 */
  }
}

/* --------------------------------------------------------------------------
   PROCESS STEPS ("Lend us your buyer understanding")
   FIGMA: step 1 (Extraction) node 2227-527, step 2 (Translation) node
   2189-1094 / parent frame 2114-130. See the PROCESS STEPS block in
   01-tokens.css for every measurement and where it came from.

   Two states of ONE component: a step selector (left) drives which card +
   lower block shows (right). The heading sits above both, offset to align
   with the content column rather than the step column - see .process_intro.
   -------------------------------------------------------------------------- */

.process_section {
  position: relative;
  padding-top: var(--process-section-pad-top);
  padding-bottom: var(--process-section-pad-bottom);
  background-color: var(--color-process-bg);
  box-shadow: var(--process-section-shadow);
}

/* Fixed-width tracks, not fr: the 1020px content block is LEFT-ALIGNED
   inside the 1280 container and Figma leaves the remaining 260px of each
   row as clear dark ground on the right. fr columns would stretch to fill
   that space; px/rem tracks just stop, which is what the frame draws.

   The second track is minmax(0, card-w), not a bare card-w, and .process_
   panels below carries min-width:0: without both, this is the exact same
   auto-minimum trap .case-study_copy already documents for flexbox, just
   on grid instead - a grid track's automatic floor is its content's
   preferred size, not 0, so a bare fixed card-w track refused to shrink
   below 760px at ANY width, and the container-large scaffold already
   shrinks below 1020px+the step column well above the 767px mobile
   rewrite below, forcing the page into horizontal scroll everywhere from
   roughly 767px to 1130px. minmax(0, card-w) still resolves to the full
   760px the moment there is room for it (nothing below 1130px-ish changes
   on a real desktop), and only actually shrinks in that gap. */
.process_content {
  display: grid;
  grid-template-columns: var(--process-step-col-w) minmax(0, var(--process-card-w));
  column-gap: var(--process-columns-gap-x);
  row-gap: var(--process-columns-gap);
}

.process_intro {
  grid-column: 2;
  color: var(--color-process-heading-ink);
}

.process_heading {
  color: inherit;
}

/* Trust 2A Regular 18/26, not .text-size-medium (Figtree 18/28) - same
   two-body-face split already flagged elsewhere on this page. Reuses the
   case-study copy's own 18/26 tokens rather than inventing a duplicate
   pair for one more instance of the same measurement. */
.process_subheading {
  margin-top: var(--process-heading-gap);
  color: inherit;
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  letter-spacing: normal;
}

/* STICKY, driving the scrollspy in js/modules/process-steps.js: not in
   Figma (there's no scroll-driven state in the source frames at all), a
   decision same as .nav_component's own sticky call. align-self:start
   keeps this column at its own (short) content height rather than the
   grid's default stretch-to-row-height, which is what leaves it room to
   travel and re-pin as the much taller .process_panels column beside it
   (both panels now stacked and visible, see .process_panel+.process_panel
   below) scrolls past underneath.

   top is the navbar's OWN rendered height plus --process-steps-sticky-gap,
   not a made-up figure on its own - the gap keeps the pinned column off
   the bar's bottom edge instead of sitting flush against it. nav.css
   switches nav height at 992px, one breakpoint away from this component's
   own 767px cutover, so top needs the same split - see the 992px override
   further down. */
.process_steps {
  position: sticky;
  top: calc(var(--nav-height-mobile) + var(--process-steps-sticky-gap));
  grid-column: 1;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  gap: var(--process-step-gap);
  align-self: start;
}

/* --------------------------------------------------------------------------
   THE STEP PILL
   --------------------------------------------------------------------------
   Scalloped "postage stamp" edge, with a paper grain over the fill.
   Figma's own export for this shape (node 2227:563, "Comet 2026-08-05
   16.48.59") came back a fully transparent PNG - a flattened-filter
   casualty, the same failure DESIGN.md documents for the nav logo plate
   and the accordion card - so there is no cutout to fall back on.

   This was reverted once already: an in-Figma screenshot briefly showed a
   plain flat rectangle, which read as the real answer overriding this
   reconstruction, but a second, sharper screenshot from the same file
   (Pratham, 2026-09-04) shows the scalloped edge and the grain clearly,
   matching a fresh pull from Figma's own render API - so the FIRST
   screenshot was the anomaly (almost certainly an unloaded image fill in
   the Figma client), not this one. Scalloping restored.

   The edge itself is rebuilt as a TILEABLE SVG mask, one period wide
   (media/home/process/step-pill-mask.svg: a single period-width strip with
   one circular bite top-centre and one bottom-centre), repeated with
   `mask-repeat: repeat-x` and `mask-size: auto 100%` rather than stretched
   to fill a fixed box. That is what lets the same file serve the 1440
   frame's fixed 209px pills AND the 440 frame's fit-content ones (174px
   and 181px for the two different labels, see the MOBILE block below)
   without distortion - `auto 100%` scales the tile's width in proportion
   to its height, so the bite stays circular at either pill height instead
   of stretching into an ellipse. The trade against the source: real
   scalloping only runs along the top and bottom edges now, and the left
   and right ends are a plain flat cut rather than Figma's own rounded
   corner bite - a deliberate simplification, not a further reconstruction
   error. Period and radius are read off the rendered screenshot rather
   than the (missing) source file.

   THE HOLES MUST BE REAL TRANSPARENCY, NOT BLACK PAINT. The first version
   drew a white rect with opaque BLACK circles on top, which is how an SVG
   `<mask>` element (luminance: black = hidden) would read it - but
   `mask-image: url("file.svg")` pointing at a bare SVG *document* is not
   that: Chromium masks by ALPHA there, not luminance, and a black circle at
   full opacity has alpha 1 same as the white rect around it, so nothing was
   actually cut - the whole box just rendered as a plain flat rectangle,
   which is exactly the false alarm that first read as "Figma must draw this
   flat after all". Confirmed by testing the identical markup as a plain
   <img> (renders the scallop correctly - proving the file itself was fine)
   against the same markup as a mask-image (renders flat - proving the
   mode, not the artwork, was the bug). Fixed by punching the circles as
   real holes: one <path fill-rule="evenodd"> tracing the rect and every
   circle as sub-paths, so the "hidden" pixels have no paint at all rather
   than black paint. The grain reuses --btn-grain, the same fractalNoise
   recipe already used on the testimonial card below and the nav logo
   plate, rather than a second texture asset for one more speckled surface.

   INACTIVE IS OPACITY, NOT A SECOND COLOUR. Figma draws both pills the same
   fill and drops the inactive one to 40% over the dark section background
   (--process-step-inactive-opacity) - confirmed, not assumed, in
   01-tokens.css's comment on --color-process-step-bg. So the state here is
   one opacity swap, not a colour swap.
   -------------------------------------------------------------------------- */
.process_step {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--process-step-w);
  height: var(--process-step-h);
  padding: 0 var(--space-medium);
  border: 0;
  background-color: var(--color-process-step-bg);
  -webkit-mask-image: url("/media/home/process/step-pill-mask.svg");
  mask-image: url("/media/home/process/step-pill-mask.svg");
  -webkit-mask-size: auto 100%;
  mask-size: auto 100%;
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  font-family: var(--font-body);
  font-size: var(--fs-large);
  line-height: var(--lh-large);
  color: var(--color-process-step-ink);
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  opacity: var(--process-step-inactive-opacity);
  transition: opacity var(--dur-base) var(--ease-inout);
}

.process_step::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--btn-grain);
  opacity: var(--btn-grain-opacity);
  pointer-events: none;
}

.process_step.is-active {
  opacity: 1;
  cursor: default;
}

/* Purely decorative, and drawn identically in both Figma states - it never
   moves to track progress, it just marks the gap between the two pills.
   A background-image on an empty span, not an <img>: the mobile frame
   draws a genuinely different asset (a long horizontal dash rather than a
   short vertical tick, not the same artwork rotated), so the source has to
   swap per breakpoint - trivial for a background-image, would otherwise
   need two <img> elements toggled by display. */
.process_step-connector {
  position: absolute;
  left: 0.6875rem; /* 11px */
  top: 2.9375rem;  /* 47px: 5px above the gap so the art overlaps into both pills, matching the frame */
  width: 0.5rem;
  height: 2.125rem; /* 34px */
  background-image: url("/media/home/process/step-connector.svg");
  background-size: 100% 100%;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* Nav switches from its compact bar to the full 992px+ bar here (see
   nav.css's own BREAKPOINT comment) - .process_steps's sticky offset has
   to follow the same line or it either pins with a gap under the taller
   bar, or tucks a few px under the shorter one. */
@media screen and (min-width: 992px) {
  .process_steps {
    top: calc(var(--nav-height) + var(--process-steps-sticky-gap));
  }
}

/* Both panels stay in the flow and stacked at once from here up - see the
   comment on .process_steps above. js/modules/process-steps.js stops
   toggling `hidden` per click at this same width and starts a scroll-driven
   "which panel is under the sticky nav right now" check instead, so
   scrolling this section reads as moving between steps rather than
   dropping straight into "Who We Serve" the moment step 1 clears the
   viewport. --process-columns-gap is reused rather than a new gap invented
   for it - same rhythm as the heading-to-row gap right above it, and nothing
   in Figma specifies this distance since the stacked state doesn't exist
   there at all. */
@media screen and (min-width: 768px) {
  .process_panel + .process_panel {
    margin-top: var(--process-columns-gap);
  }
}

.process_panels {
  grid-column: 2;
  grid-row: 2;
  min-width: 0; /* lets the minmax(0, card-w) track above actually shrink - see the comment on .process_content */
}

/* --------------------------------------------------------------------------
   THE CARD
   --------------------------------------------------------------------------
   Shared by both states - same torn-paper treatment as the accordion
   (#sw-deckle filter on a pseudo-element background, see partials/svg-defs).
   Confirmed against the render at 4x zoom: this card's corners are ragged,
   the testimonial card below it is not - two different textures, not one
   applied inconsistently. See .process_testimonial for the plain-grain one.
   -------------------------------------------------------------------------- */
.process_card {
  position: relative;
  isolation: isolate;
  padding: var(--process-card-pad);
}

.process_card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--color-process-card-bg);
  filter: url(#sw-deckle);
}

.process_card-title {
  margin-bottom: var(--process-card-title-gap);
}

.process_checklist {
  display: flex;
  flex-direction: column;
  gap: var(--process-check-gap);
}

.process_checklist-item {
  display: flex;
  align-items: center;
  gap: var(--process-check-icon-gap);
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  color: var(--color-ink-copy);
}

.process_checklist-icon {
  flex-shrink: 0;
  width: var(--process-check-icon-size);
  height: var(--process-check-icon-size);
  object-fit: contain;
}

.process_card-text {
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  color: var(--color-ink-copy);
}

.process_card-text + .process_card-text {
  margin-top: var(--process-para-gap);
}

/* Hand-drawn stroke riding under a phrase, same idiom as .hero_heading-mark
   and .problem_mark elsewhere on this page - a raster/vector stroke as a
   background-image so it survives a reflow instead of a border under a
   span. Brand-mark red per the exported asset (#E7412E, already a token).

   FLUSH BOTTOM, not the +space-tiny hang those other marks use: this
   stroke is only 2px tall, and Figma draws it flush against the line's own
   bottom edge (2114:173/174 sit exactly at the line box's foot), not
   hanging below it. Reusing the +offset formula here pushed the entire
   2px image past the box's bottom edge - background-clip:border-box then
   clips 100% of it, not just part, which is invisible rather than merely
   off-register. A thicker mark can absorb a few px of offset; this one
   cannot. */
.process_mark {
  display: inline-block;
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 100% 0.125rem;
}

.process_mark.is-long {
  background-image: url("/media/home/process/underline-long.svg");
}

.process_mark.is-short {
  background-image: url("/media/home/process/underline-short.svg");
}

.process_testimonial,
.process_media {
  margin-top: var(--process-block-gap);
}

/* --------------------------------------------------------------------------
   TESTIMONIAL (step 1)
   --------------------------------------------------------------------------
   Straight corners, not torn - confirmed against the render, unlike
   .process_card above. The fill is flat #4f4f4f (--color-ink, reused) with
   the same fine grain the button hover already carries (--btn-grain),
   permanently visible here rather than fading in, so it is its own
   pseudo-element rather than reusing the button's fade-in transition.
   -------------------------------------------------------------------------- */
.process_testimonial {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: var(--process-testimonial-avatar) 1fr;
  column-gap: var(--process-testimonial-gap);
  padding: var(--process-testimonial-pad);
}

.process_testimonial::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background-color: var(--color-process-testimonial-bg);
}

.process_testimonial::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--btn-grain);
  opacity: var(--btn-grain-opacity);
  pointer-events: none;
}

.process_testimonial-avatar {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.process_testimonial-quote {
  margin: 0;
  color: var(--color-process-heading-ink);
  font-family: var(--font-body);
  font-size: var(--fs-large);
  line-height: var(--lh-large);
  font-style: normal;
}

.process_testimonial-author {
  display: block;
  margin-top: var(--process-testimonial-name-gap);
  font-style: normal;
}

/* Same face/weight pairing as .case-study_quote-name / -role elsewhere on
   this page for an author byline: Trust 2A 700 for the name (a REAL loaded
   weight - Figtree only ships 400, so the Figma layer's "Figtree SemiBold"
   would synthesise a fake bold here) and Trust 2A Regular for the role. */
.process_testimonial-name {
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  font-weight: var(--weight-bold);
  color: var(--color-process-heading-ink);
  opacity: 0.9;
}

.process_testimonial-role {
  margin-left: 0.25em;
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  font-weight: var(--weight-regular);
  color: var(--color-process-heading-ink);
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   MEDIA (step 2): backdrop photo + floating browser screenshot.
   --------------------------------------------------------------------------
   Fluid, not the fixed 760 x 420 the frame draws: max-width caps it at the
   real measurement on a wide screen, but width:100% plus aspect-ratio let
   it shrink with its column instead of forcing horizontal scroll the
   moment .process_panels (above) has less than 760px to give it - the
   same reasoning as the minmax() grid track. The screenshot and its
   shadow are positioned in PERCENTAGES of this box rather than fixed px
   tokens, so they scale down with it instead of spilling past its edges. */
.process_media {
  position: relative;
  width: 100%;
  max-width: var(--process-media-w);
  aspect-ratio: 760 / 420; /* [LOCKED] the frame's own box, 2238:4742 */
  overflow: hidden;
}

.process_media-backdrop {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.process_media-shadow {
  position: absolute;
  left: 1.5%;    /* 11.4 / 760 [LOCKED, derived] */
  top: 19.52%;   /* 82 / 420 */
  width: 50.1%;  /* 380.771 / 760 */
  height: 80.9%; /* 339.779 / 420 */
  pointer-events: none;
}

.process_media-screenshot {
  position: absolute;
  left: 9.08%;    /* 69 / 760 [LOCKED, derived] */
  top: 12.38%;    /* 52 / 420 */
  width: 81.84%;  /* 622 / 760 */
  height: 74.29%; /* 312 / 420 */
  object-fit: cover;
}

.process_cta {
  margin-top: var(--process-media-gap);
}

/* --------------------------------------------------------------------------
   PROCESS STEPS :: MOBILE (767px and below)
   --------------------------------------------------------------------------
   Real anchor: frame 2275-924, 440 wide, Step 1 (Extraction) only - see the
   PROCESS STEPS :: MOBILE block in 01-tokens.css for what's measured
   [LOCKED] versus extrapolated [INTERP]. One breakpoint, not a 767-then-479
   pair: this is a full structural rebuild (single column, step selector
   moves below the card, avatar moves above the quote), not a fluid shrink,
   so there is no useful middle "tablet" state to hold the 1440 numbers at -
   the 440-anchor design is what this section looks like at every width
   from 767 down.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 767px) {
  .process_section {
    padding-top: var(--process-section-pad-mobile);
    padding-bottom: var(--process-section-pad-mobile);
  }

  .process_content {
    grid-template-columns: 1fr;
    row-gap: 0;
  }

  /* min-width:0 on every direct grid item: a grid item's automatic minimum
     width is its content's preferred size, not 0, so without this the
     .process_heading child's own 376px max-width (below) held the WHOLE
     single 1fr column open at 376px regardless of the viewport, pushing
     the section past a 375px phone rather than letting the heading wrap
     into the space actually available. Same gotcha .case-study_copy
     already documents for flex, here on grid instead. */
  .process_intro,
  .process_panels,
  .process_steps {
    min-width: 0;
  }

  .process_intro {
    grid-column: 1;
    grid-row: 1;
    text-align: center;
  }

  /* 32/40 -1.5%, NOT the shared H2 mobile ladder (24/32 at this tier) - a
     real, distinct measurement off 2269:437, not an oversight. Same idiom
     as .problem_callout's own mobile face override elsewhere on this page:
     scoped here rather than bent into the shared token. */
  .process_heading {
    max-width: 23.5rem; /* 376px [LOCKED] 2269:437 */
    margin-inline: auto;
    font-size: 2rem;
    line-height: 2.5rem;
    letter-spacing: -0.015em;
  }

  .process_subheading {
    max-width: 20rem; /* 320px [LOCKED] 2269:438 */
    margin-inline: auto;
    margin-top: var(--process-heading-gap-mobile);
    color: var(--color-process-subheading-mobile);
    /* Trust 2A Regular 16/24 - equals --fs-regular/--lh-regular exactly,
       reused rather than repeated as literals. */
    font-size: var(--fs-regular);
    line-height: var(--lh-regular);
  }

  .process_panels {
    grid-column: 1;
    grid-row: 2;
    margin-top: var(--process-columns-gap-mobile);
  }

  /* The step selector drops from a left COLUMN to a ROW below the card -
     grid-row alone moves it there, no DOM reorder needed. */
  /* Centred as a PAIR with a fixed gap between them, not spread across the
     full card width. space-between was tried first because it happens to
     reproduce the 440 anchor exactly - at that one width the two pills
     plus a 37px gap sum to precisely the card's own 392px, so a fixed gap
     and "stretch to fill" are visually identical there. They stop
     agreeing at any other width: a phone wider than 392px (or the fluid
     desktop layout above 767px, which reuses this same row) stretched the
     gap open instead of the pills staying together, exactly what a fixed
     gap does not do. */
  .process_steps {
    /* Undoes the desktop sticky pin: stacked single-column here, the step
       row would otherwise stick to the viewport top and float over
       whatever scrolls beneath it - the scrollspy this drives is a
       desktop-only behaviour (see js/modules/process-steps.js), so the
       column needs to go back to sitting in normal flow at this width.
       relative, NOT static: .process_step-connector below is positioned
       absolute against this element - static would knock it out of that
       positioning context entirely and send it looking for the next
       positioned ancestor up the tree instead. */
    position: relative;
    top: auto;
    grid-column: 1;
    grid-row: 3;
    margin-top: var(--process-block-gap);
    flex-direction: row;
    justify-content: center;
    gap: var(--process-step-gap-mobile);
  }

  /* Fit-content, not the desktop's shared fixed width - Figma draws the two
     mobile pills at their own two different widths (174px and 181px), see
     01-tokens.css. The tiling mask handles either width without
     distortion; see the comment on .process_step above. */
  .process_step {
    /* min-width:0 + white-space:normal: the 440 anchor fits both labels on
       one nowrap line each with room to spare, but a narrower phone (375,
       320) cannot - flexbox's default min-width:auto held both pills at
       their full nowrap width regardless, pushing "Step 2: Translation"
       past the viewport. Letting the label wrap to two lines below the
       anchor width is the graceful failure, not a second breakpoint. */
    min-width: 0;
    width: auto;
    height: auto;
    min-height: 3rem; /* 48px [LOCKED] */
    padding: 0.5rem 1.5rem; /* 24px side [LOCKED]; vertical is new, only needed once wrapping is possible */
    white-space: normal;
    /* Figtree, not Trust 2A - another instance of the sitewide two-body-face
       split (DESIGN.md flag 6), built as drawn per 2269:428. */
    font-family: var(--font-para);
    font-size: 1rem;     /* 16px [LOCKED] */
    line-height: 1.5rem; /* 24px [LOCKED] */
  }

  /* A different asset, not the desktop tick rotated - see the comment on
     .process_step-connector above. Centred in the gap between the two
     pills rather than the desktop's fixed left/top, because the gap itself
     is no longer fixed once the pills are fit-content.

     ROTATED, NOT STRETCHED. Figma draws this asset portrait (8 wide x 41
     tall, per 2269:433's own pre-rotation box) and rotates the layer -90deg
     to lay it flat - the same construction as the desktop tick. The first
     version instead set the box to its POST-rotation footprint (41 x 2.35)
     directly and stretched the source into it with background-size:100%
     100%, which is a ~5x horizontal stretch against a ~17x VERTICAL
     squash - enough to crush the little square marker down to nothing.
     Keeping the box at the source's own portrait aspect and rotating the
     element itself (matching Figma's own layer transform) preserves the
     marker instead of destroying it. */
  .process_step-connector {
    left: 50%;
    top: 50%;
    width: 0.5rem;     /* 8px [LOCKED] pre-rotation, matches the source SVG */
    height: 2.5625rem; /* 41px [LOCKED] pre-rotation, 2269:433 */
    transform: translate(-50%, -50%) rotate(-90deg);
    background-image: url("/media/home/process/step-connector-mobile.svg");
  }

  .process_card {
    padding: var(--process-card-pad-mobile);
  }

  /* H5 (24/32 -1%), not H4: swaps the size the shared utility class set at
     desktop for the one the 440 frame actually draws. --fs-h5 carries no
     breakpoint override anywhere in 01-tokens.css, so this is the same
     real value at every width, not a second measurement to keep in sync. */
  .process_card-title {
    margin-bottom: var(--process-card-title-gap-mobile);
    font-size: var(--fs-h5);
    line-height: var(--lh-h5);
    letter-spacing: var(--ls-h5);
  }

  .process_checklist-item {
    gap: var(--process-check-icon-gap-mobile);
    /* Trust 2A Regular 16/24 - equals --fs-regular/--lh-regular exactly,
       same reuse as the subheading above. */
    font-size: var(--fs-regular);
    line-height: var(--lh-regular);
  }

  .process_checklist-icon {
    width: var(--process-check-icon-size-mobile);
    height: var(--process-check-icon-size-mobile);
  }

  /* [INTERP] no step-2 mobile frame exists yet (DESIGN.md flag 27) - same
     16/24 Trust 2A step as the checklist above, for consistency with it
     rather than a second guess. */
  .process_card-text {
    font-size: var(--fs-regular);
    line-height: var(--lh-regular);
  }

  /* Avatar moves from a left column to ABOVE the quote - flex-direction
     alone does it, since both children are already siblings in source
     order. */
  .process_testimonial {
    display: flex;
    flex-direction: column;
  }

  .process_testimonial-avatar {
    width: var(--process-testimonial-avatar-mobile);
    height: var(--process-testimonial-avatar-mobile);
  }

  /* Figtree, not Trust 2A, and a third distinct ink colour
     (--color-process-quote-mobile) - built as drawn, per 2269:460. */
  .process_testimonial-quote {
    margin-top: var(--process-testimonial-avatar-gap-mobile);
    color: var(--color-process-quote-mobile);
    font-family: var(--font-para);
    font-size: 1rem;        /* 16px [LOCKED] */
    line-height: 1.375rem;  /* 22px [LOCKED] */
    letter-spacing: -0.012em; /* -1.2% [LOCKED] 2269:460 */
  }

  .process_testimonial-author {
    margin-top: var(--process-testimonial-name-gap-mobile);
  }

  /* Figtree Regular for BOTH, not "SemiBold" for the name: Figtree only
     ships weight 400 on this site (00-fonts.css), so asking for 600 here
     would synthesise a fake bold, the same trap already documented on the
     desktop testimonial name. The name/role distinction rides on opacity
     alone (90%/70%, inherited from the desktop rule below) rather than a
     weight this build cannot honestly draw. */
  .process_testimonial-name,
  .process_testimonial-role {
    font-family: var(--font-para);
    font-size: 1rem;       /* 16px [LOCKED] */
    line-height: 1.375rem; /* 22px [LOCKED] */
    font-weight: var(--weight-regular);
  }

  /* STEP 2 MEDIA needs no override here: it's already fluid at the base
     rule (percentage insets on an aspect-ratio box, not fixed px), which
     is what makes it correct at this width too and not just at 1440. No
     mobile frame exists for Step 2 (Translation) at all - DESIGN.md flag
     27 - so this is this build's own extrapolation of the Step 1
     structural pattern onto Step 2's desktop content, not a measurement. */
}

/* case studies + video testimonial section css */
/* FIGMA: node-id 2238-4978, part of the Homepage-updated frame (2315-7821).
   README's roadmap table lists this as 2315-8267. Desktop only: no mobile
   frame supplied, so nothing here has a breakpoint override yet. */

.case-studies_section {
  position: relative;
}

.case-studies_heading {
  max-width: var(--case-heading-w);
}

.case-studies_list {
  display: flex;
  flex-direction: column;
  gap: var(--case-row-gap);
  margin-top: var(--case-heading-gap);
}

.case-study {
  position: relative;
  background-image: url("/media/home/case-studies/paper-texture.png");
  background-size: cover;
  background-position: center bottom;
}

/* Decorative "staple" strip along the row's top edge. One shared SVG (nine
   staples, three clusters) rather than eighteen positioned elements per row:
   all six rows draw the exact same strip. 2238:4908 onward. */
.case-study_notches {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1.5rem;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   THE FLIP (front content <-> testimonial)
   --------------------------------------------------------------------------
   Every row has two faces stacked in the same spot: the case content
   (front, .case-study_face-front) and a testimonial (back,
   .case-study_face-back/.is-quote). Only one real testimonial exists so
   far - it is reused as a placeholder back face on every row until real
   ones are supplied per client, not a bug.

   Modelled as a spiral desk calendar, hinged along the top edge to match
   .case-study_notches (the staple/binding art already sitting there):
   clicking "View Testimonial" lifts the front page up and back over that
   hinge, the way a bound calendar page turns, rather than a coin-flip on a
   vertical axis.

   .is-flipped is a real toggle set by js/modules/case-study-flip.js on
   click, not a :hover pseudo-class - a click has no equivalent of "mouse
   leaves" to undo it automatically, so something has to hold the state.
   The trigger (.case-study_tag) and the thing it moves
   (.case-study_flip-inner) are siblings, not ancestor/descendant, hence
   toggling the class on their shared parent (.case-study) rather than on
   either one directly. The same click also swaps the trigger's own label
   between "View Testimonial" and "View Statistics" to match whichever
   face is now showing. */
/* perspective-origin MUST match .case-study_flip-inner's transform-origin
   (top center, below) - perspective-origin defaults to the box's own
   center, and a row this tall (476px) hinged at its TOP instead sweeps
   the far (bottom) edge through a wildly different distance from the
   camera than a centre-hinged flip would, ballooning it to several times
   its own size mid-rotation. Anchoring the vanishing point at the same
   point the rotation pivots around keeps the far edge's distance from the
   camera sane throughout the turn. */
.case-study_flip {
  position: relative;
  perspective: 250rem;
  perspective-origin: top center;
}

/* Grid, not the flex .case-study_content already uses one level in: both
   faces share this one grid area, so the row's height is always whichever
   face is taller (the front's 476 vs the back's 420) - same "reserve
   space for both states so toggling never resizes the box" technique
   already used for the reason cards elsewhere on this page. */
.case-study_flip-inner {
  position: relative;
  height: var(--case-row-h);
  transform-style: preserve-3d;
  transform-origin: top center;
  transition: transform 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}

/* .case-study_content.case-study_face, not .case-study_face alone: both
   classes are always paired in the markup, but .case-study_content's own
   position:relative (below) is the same specificity and comes later in
   the cascade, so a single-class selector here loses that fight and the
   face silently stayed position:relative - stacking the faces top of each
   other instead of overlapping, which is what let one face's height push
   the other one down rather than both sharing this box. The compound
   selector out-specifies it regardless of source order. */
.case-study_content.case-study_face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* Pre-rotated 180deg so it lands right-side up once .case-study_flip-inner
   itself rotates - the standard "pre-twisted so the net rotation reads
   normal" trick every CSS flip card uses.

   transform-origin MUST repeat .case-study_flip-inner's (top center, not
   this element's own default centre) - without it the parent pivots the
   whole 3D space around its top edge while this element's own 180deg
   spins around its OWN centre partway down that already-rotated space,
   two different pivot points fighting each other. The visible result
   wasn't a clean turn, it was the far edge swinging through a completely
   different arc and ballooning to several times its size mid-flip.
   Matching the origin collapses both rotations back to one consistent
   pivot, which is what makes rotateX(parent) + rotateX(180) compose into
   the plain single-angle turn the math assumes. */
.case-study_face-back {
  transform: rotateX(180deg);
  transform-origin: top center;
}

.case-study.is-flipped .case-study_flip-inner {
  transform: rotateX(-180deg);
}

/* min-height, not height: the 476px figure is what the padding and the
   372px-tall media box add up to at the 1440 anchor, not a hard cap. Below
   1440 the copy and media columns are allowed to shrink (see below), so a
   wrapped paragraph can need more room than 476px holds - min-height lets
   the row grow for that instead of clipping or spilling into the next one.
   A fixed height plus percentage-height children would also silently stop
   working the moment this became min-height, since an auto-height parent
   doesn't give percentage heights anything definite to resolve against;
   putting the height concern directly on this element sidesteps that. */
.case-study_content {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-large);
  min-height: var(--case-row-h);
  padding: var(--case-content-pad-top) var(--case-inset-x) var(--case-content-pad-bottom);
}

/* The testimonial row's video panel sits in its own 48/32 top/bottom
   inset (2238:4842), not the 64/40 every standard row shares, and the row
   itself is shorter (420 vs 476) since that panel is shorter than the
   other rows' media box. Same background asset, cropped tighter from the
   bottom rather than swapped for a second texture. Scoped to the face
   itself now, not an ancestor .is-quote article - every row carries both
   faces, so the article can no longer say which one it "is". */
.case-study_content.is-quote {
  min-height: var(--case-row-h-quote);
  padding-top: 3rem;
  padding-bottom: 2rem;
}

/* Text column hugs its own content width and never stretches to fill the
   row: the image column is what pins to the row's right edge, so the gap
   between them is whatever space-between leaves over. Matches the frame,
   where every row draws a different gap because every paragraph is a
   different length. 2238:4896 vs 2238:4904 */
.case-study_copy {
  display: flex;
  flex-direction: column;
  /* Without this, flex's default align-items:stretch forces every child to
     the column's full 552px width. Block children (h3, p) already look
     full-width regardless, so this was invisible everywhere except the one
     logo sized by height + auto width (Acorn): stretch overrode that auto
     and squashed its aspect ratio flat. */
  align-items: flex-start;
  /* Shrinkable, not fixed: between the 1440 and 440 anchors (no frame
     covers that range) this is what lets the copy column narrow instead
     of forcing the row wider than its container. min-width:0 overrides
     flexbox's own min-width:auto, which would otherwise refuse to shrink
     past the longest word's width - the same gotcha .button documents. */
  flex: 1 1 var(--case-copy-max-w);
  min-width: 0;
  max-width: var(--case-copy-max-w);
}

.case-study_logo {
  display: block;
  height: var(--case-logo-h);
  width: auto;
}

/* Four of the five client marks are drawn in Figma as a solid-ink shape
   masked from the client's own (multi-colour) logo file, not as a plain
   image: 2238:4954, 4754, 4759 and 4781 all pair a bg-colour div with a
   mask-image, discarding the source colours so the row reads as one
   uniform grey line-up rather than five different brand palettes. Acorn is
   the one exception in the frame (2238:4767, a plain <img>) because its
   source is already a flat grey vector. */
.case-study_logo.is-mask {
  background-color: var(--color-ink);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: left center;
  mask-position: left center;
  -webkit-mask-size: contain;
  mask-size: contain;
}

.case-study_logo.is-reviewwave {
  width: 11.5rem; /* 184px [LOCKED] 2238:4956 */
  -webkit-mask-image: url("/media/home/case-studies/logo-reviewwave.png");
  mask-image: url("/media/home/case-studies/logo-reviewwave.png");
}

.case-study_logo.is-creditrepair {
  width: 11.875rem; /* 190px [LOCKED] 2238:4754 */
  -webkit-mask-image: url("/media/home/case-studies/logo-creditrepair.png");
  mask-image: url("/media/home/case-studies/logo-creditrepair.png");
}

.case-study_logo.is-deardoc {
  width: 10.4375rem; /* 167px [LOCKED] 2238:4759 */
  -webkit-mask-image: url("/media/home/case-studies/logo-deardoc.png");
  mask-image: url("/media/home/case-studies/logo-deardoc.png");
}

.case-study_logo.is-sphinxbio {
  width: 11.25rem; /* 180px [LOCKED] 2238:4781 */
  -webkit-mask-image: url("/media/home/case-studies/logo-sphinxbio.png");
  mask-image: url("/media/home/case-studies/logo-sphinxbio.png");
}

/* The one plain-<img> logo (already a flat grey vector, no mask needed) is
   also the one drawn a touch taller than the rest: 44px, not 40. 2238:4767 */
.case-study_logo.is-acorn {
  height: 2.75rem;
}

.case-study_title {
  margin-top: var(--case-title-gap);
}

.case-study_text {
  margin-top: var(--case-text-gap);
  font-family: var(--font-body);
  font-size: var(--case-text-fs);
  line-height: var(--case-text-lh);
  color: var(--color-ink-copy);
}

.case-study_text strong {
  font-weight: var(--weight-medium);
  color: var(--color-ink);
}

/* Shrinkable too, same reasoning as .case-study_copy above: fixed at 548px
   this would force the row wider than its container anywhere below the
   1440 anchor. object-fit:cover on the <img> means a narrower box just
   shows a tighter crop rather than squashing the image. */
.case-study_media {
  flex: 1 1 var(--case-media-w);
  min-width: 12rem;
  height: var(--case-media-h);
  overflow: hidden;
}

.case-study_media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   THE "VIEW TESTIMONIAL" TAG
   --------------------------------------------------------------------------
   A torn washi-tape flag. Built as a CSS clip-path rather than the exported
   SVG: all three tape instances in the frame share one identical path
   (2238:5288, 5289, 5290 all trace "M1 1H235L211 57H25L1 1Z"), so the shape
   is a real, reusable component rather than per-row artwork, and the two
   colours are the existing brand tokens rather than a second flat asset.

   Rest state carries no underline; hover/focus adds one. That pairing is
   the only interaction state the frame draws (2238:4950 rest vs 2238:4792
   hover, both real Figma instances, not a guess).
   -------------------------------------------------------------------------- */
/* A <button> now (it drives the flip, not a navigation target), so the
   usual button-element resets apply on top of the visual rule below:
   appearance/border to strip the native chrome a plain <a> never carried,
   cursor since a button's own default is already "pointer" everywhere
   that matters but Safari's isn't guaranteed, and text-align because a
   button centres its content by default where an <a> just followed its
   parent's flex layout. */
.case-study_tag {
  position: absolute;
  top: 100%;
  left: var(--case-tag-inset);
  z-index: 2;
  display: flex;
  align-items: center;
  width: var(--case-tag-w);
  height: var(--case-tag-h);
  padding-left: 2.5rem;
  border: 0;
  appearance: none;
  text-align: left;
  cursor: pointer;
  clip-path: var(--case-tag-clip);
  filter: drop-shadow(var(--case-tag-shadow));
  background-color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: var(--fs-regular);
  font-weight: var(--weight-semibold);
  line-height: var(--lh-regular);
  letter-spacing: var(--ls-regular);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--case-tag-ink-blue);
  transition: color var(--dur-fast) var(--ease-out);
}

.case-study_tag:hover,
.case-study_tag:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.case-study_tag.is-red {
  background-color: var(--color-brand-mark);
  color: var(--case-tag-ink-red);
}

/* --------------------------------------------------------------------------
   THE VIDEO TESTIMONIAL ROW
   -------------------------------------------------------------------------- */
.case-study_quote {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0; /* same shrink-below-content-width fix as .case-study_copy */
  max-width: 49.625rem; /* 794px [LOCKED] 2238:4798 */
  /* blockquote/cite carry a user-agent italic; the frame draws this upright. */
  font-style: normal;
}

.case-study_quote-text {
  margin: 0;
  font-family: var(--font-body);
  font-weight: var(--weight-regular);
  font-size: 2rem;      /* 32px [LOCKED] 2238:4798 */
  line-height: 2.75rem; /*  44px [LOCKED] */
  letter-spacing: -0.015em; /* -0.48px at 32px [LOCKED] */
  color: var(--color-ink-copy);
}

.case-study_quote-text strong {
  font-weight: var(--weight-medium);
  color: var(--color-ink);
}

.case-study_quote-footer {
  display: flex;
  align-items: center;
  gap: var(--space-small);
  margin-top: var(--space-xlarge); /* 64px [LOCKED] 2238:4799 y minus quote bottom */
}

.case-study_quote-author {
  font-style: normal;
}

.case-study_quote-name,
.case-study_quote-role {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-large); /* 20px, matches Body 1 exactly */
  line-height: var(--lh-large);
}

.case-study_quote-name {
  font-weight: var(--weight-bold); /* real usage of 700, confirmed by 2238:4801 */
  letter-spacing: -0.005em; /* -0.1px at 20px [LOCKED] */
  color: var(--color-ink);
}

.case-study_quote-role {
  font-weight: var(--weight-regular);
  color: var(--color-ink-copy);
}

/* Pinned to the far right of the footer row rather than the row itself: its
   right edge lines up exactly with the quote text's own right edge
   (both land on row-relative x1220), so it rides with the text column
   instead of the fixed-width video column beside it. 2238:4803 */
.case-study_badge {
  margin-left: auto;
  flex-shrink: 0;
}

.case-study_video {
  position: relative;
  /* Shrinkable, same reasoning as .case-study_media: fixed at 334px this
     would force the testimonial row wider than its container below 1440.
     min-width is higher than .case-study_media's matching 12rem, though:
     this panel also has to hold the centred 252px Play pill, and shrinking
     past that pushes the pill wider than the panel - with overflow:hidden
     here, the overflow doesn't spill, it gets silently clipped, cutting
     text and icon off both edges instead of just looking cropped. */
  flex: 1 1 20.875rem; /* 334px [LOCKED] 2238:4842 */
  min-width: 19rem;
  height: 21.25rem;     /* 340px [LOCKED] */
  overflow: hidden;
}

.case-study_video-bg,
.case-study_video-portrait {
  position: absolute;
  object-fit: cover;
}

.case-study_video-bg {
  inset: 0;
  width: 100%;
  height: 100%;
  transform: rotate(180deg);
}

/* Percentage, not the 292px the frame draws: that figure only holds at
   the panel's own 334x340 box. Below 1440 the panel is shrinkable, and
   below 767 it is stacked full-width and sized by aspect-ratio instead of
   a fixed height (see the 767 breakpoint), so its own size changes with
   the viewport. A fixed-size portrait couldn't track that: at any width
   much past the 1440 anchor it would stay pinned at 292px in the middle
   of a much bigger panel, stranding the Play label below it in bare
   "Comet" backdrop instead of over the photo. 292/334 and 292/340 - the
   portrait's real proportion of its box at the anchor - keeps it filling
   the same fraction of the panel at every size instead. */
.case-study_video-portrait {
  top: 50%;
  left: 50%;
  width: 87.43%;  /* 292/334 [LOCKED, derived] 2238:4846 vs 2238:4842 */
  height: 85.88%; /* 292/340 [LOCKED, derived] */
  transform: translate(-50%, -50%);
}

/* A static label, not a control: nothing on the page plays this video, so
   it must not look or behave like a button - no pointer cursor, and built
   from a <div> rather than a <button> in the markup. */
/* Horizontally centred, not left-anchored at a fixed 44px: that figure
   (2238:4847) is only 3px off centre in a 334px-wide panel (41px would be
   exact), which reads as the designer centring it, not placing it at a
   deliberate offset. A fixed left stayed unnoticed while the panel itself
   was a fixed 334px, but once it became shrinkable (see .case-study_video
   above) that same 44px no longer means "centred" at any other width - it
   just pins the pill to the left edge with a growing gap on the right as
   the panel widens. */
.case-study_play {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 16rem;     /* 256px [LOCKED] */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 15.75rem; /* 252px [LOCKED] */
  max-width: calc(100% - 1.5rem); /* safety margin, keeps this inside .case-study_video's clip at any width */
  height: 2.5rem;  /*  40px [LOCKED] */
  padding: 0 1rem;
  border-radius: var(--radius-pill);
  background-color: #f3f2f2; /* [LOCKED] 2238:4847, not on the surface scale */
  box-shadow: var(--shadow-btn); /* same moulded-key lip every button uses */
  font-family: var(--font-mono);
  font-weight: var(--weight-semibold);
  font-size: var(--fs-regular);
  line-height: var(--lh-regular);
  color: var(--color-ink);
}

.case-study_play-icon {
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--color-brand-mark);
}

/* --------------------------------------------------------------------------
   TABLET / NARROW DESKTOP (1280 down to 768)
   --------------------------------------------------------------------------
   No frame covers this range - DESIGN-SPEC's interpolation rule applies.
   .case-study_copy and .case-study_media are already flex-shrinkable (see
   their base rules above), so the ROW does not need a breakpoint here: it
   narrows in proportion all the way down from 1440 to the 767 stacking
   point below.

   The quote TEXT is the one exception, and only from 991 (this project's
   tablet tier) down. font-size does not shrink with its column the way
   width-based layout does, so the column narrows while 32px stays fixed -
   at a real tablet width that wraps the quote to five-plus lines next to
   the video panel, taller and heavier than the rest of the row was ever
   drawn to look. Stepped down here rather than at 1280: 1280-991 is still
   closer to a desktop reading width, where 32px keeps working. */
@media screen and (max-width: 991px) {
  .case-study_quote-text {
    font-size: 1.5rem;  /* 24px, this build's own step - no tablet frame exists */
    line-height: 2rem;  /* 32px */
  }
}
/* -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   STACKED (767 and below)
   --------------------------------------------------------------------------
   FIGMA: node-id 2269-18, a real 440 anchor - but only for widths at or
   below 479, where it is applied exactly (see that block further down).
   767 is the site's mobile-landscape tier, chosen as where a fluidly
   shrunk two-column row stops being legible (a 40px heading wrapping into
   a ~300px column) rather than where anything overflows: the shrink alone
   already prevents overflow above this point. Between 767 and 479 there is
   again no frame, so this block deliberately keeps every SIZE token at its
   1440 value and changes only the STRUCTURE - row, then media, then copy -
   rather than inventing tablet-specific numbers. The 479 block below layers
   the real mobile measurements on top of this same structure.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 767px) {
  .case-study_content {
    flex-direction: column;
    align-items: stretch;
  }

  /* The desktop-only 48/32 pair belongs to the side-by-side layout's fixed
     video panel. Stacked, the testimonial row's content is measured the
     same as every other row (2275:1082 top/bottom offsets match 2275:1024
     exactly), so the quote variant rejoins the shared padding tokens here
     instead of keeping its wider-only override. */
  .case-study_content.is-quote {
    padding-top: var(--case-content-pad-top);
    padding-bottom: var(--case-content-pad-bottom);
  }

  /* Image first, copy second, regardless of source order: the two case-study
     row variants disagree (copy-then-media markup vs media-then-quote), so
     order is set explicitly rather than relied on. */
  .case-study_media {
    order: 1;
  }

  .case-study_copy {
    /* flex-basis is axis-relative, not a fixed width: the base rule's
       "flex: 1 1 var(--case-copy-max-w)" means 552px on the row layout's
       horizontal main axis, but stacked here the main axis is vertical, so
       that same 552 was being read as a forced HEIGHT - every card was
       exactly 552px of copy regardless of how little text it held. Reset
       to flex:none so height goes back to following the content. */
    flex: none;
    order: 2;
    width: 100%;
    max-width: none;
  }

  /* aspect-ratio, not a fixed height: stacked full-width, these need to
     scale smoothly with whatever width the row ends up at, and a fixed
     height (right at 767, or at 479, or anywhere between) is exactly the
     kind of single-width fix this tier is meant to avoid. 548x372 and
     334x340 are each box's own ratio at the 1440 anchor. */
  .case-study_media {
    flex: none;
    width: 100%;
    height: auto;
    aspect-ratio: 548 / 372;
  }

  .case-study_video {
    flex: none;
    width: 100%;
    height: auto;
    aspect-ratio: 334 / 340;
  }

  .case-study_quote {
    max-width: none;
  }

  /* Centred once the row is a full-width stack, rather than left-inset
     against a column that no longer exists beside it - matches the real
     440 anchor (2275:1076: tag centre sits exactly on the frame's own
     centre). translateX keys off the tag's own width, so this holds
     whether that width is the 1440 figure or the 479 override below. */
  .case-study_tag {
    left: 50%;
    transform: translateX(-50%);
  }

  /* top:16rem on the base rule assumes the video's fixed 340px desktop
     height. Stacked, that height is aspect-ratio-driven and changes with
     the row's width, so top no longer tracks the video's actual bottom
     edge - anchor from the bottom instead, which does. Horizontal centring
     is already handled by the base rule's left:50%/translateX and needs no
     override here. */
  .case-study_play {
    top: auto;
    bottom: 2.5rem;
  }
}

/* --------------------------------------------------------------------------
   MOBILE (479 and below)
   --------------------------------------------------------------------------
   FIGMA: node-id 2269-18, applied exactly: rows go from side-by-side to
   stacked (image, then copy - already true from 767px up, see above),
   author name/role stack instead of sitting inline, and the tag and logos
   both step down in size. Everything here is read straight off the real
   440 anchor, same as the desktop tokens above; it only has to add the
   VALUES the 767 block above left at their 1440 defaults.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 479px) {
  .case-study_content {
    gap: var(--case-content-gap);
    padding: var(--case-content-pad-top) var(--case-inset-x) var(--case-content-pad-bottom);
  }

  /* The 767 block's 334/340 ratio is the desktop box's own shape, reused
     as a stand-in above this width. The real 440 anchor draws this panel
     square (2275:1145, 344x344), and the play button's position below is
     measured against that square, so it takes over here specifically. */
  .case-study_video {
    aspect-ratio: 1 / 1;
  }

  .case-study_logo {
    height: var(--case-logo-h);
  }

  .case-study_logo.is-reviewwave  { width: 5.75rem; }  /*  92px [LOCKED] half the desktop mark */
  .case-study_logo.is-creditrepair { width: 5.9125rem; } /* 94.6px [LOCKED] */
  .case-study_logo.is-deardoc     { width: 5.2rem; }    /* 83.3px [LOCKED] */
  .case-study_logo.is-sphinxbio   { width: 5.625rem; }  /*  90px [LOCKED] */
  /* Width left auto, not the 138.9px the mobile frame's bounding box
     reports: that box includes clear space the glyph doesn't use, and
     pairing it with this height would skew the logo off its 259.57:44
     aspect ratio the same way the missing align-items did above. */
  .case-study_logo.is-acorn       { height: 1.25rem; }

  .case-study_title {
    /* Was guessed at --space-medium (32px) without measuring; the mask
       group bottom (2275:1029, y=8036 h=20 -> 8056) to the title top
       (2275:1033, y=8072) is actually 16px. */
    margin-top: var(--space-small);
  }

  .case-study_text {
    margin-top: var(--space-small); /* 16px, 2275:1034 */
  }

  /* left/centering already set at 767px and unchanged here - only the
     real 440-anchor size differs from what that tier inherits. */
  .case-study_tag {
    width: var(--case-tag-w);
    height: var(--case-tag-h);
  }

  .case-study_quote-text {
    font-size: var(--fs-large);   /* 20px, no dedicated mobile measurement past this box width */
    line-height: var(--lh-large);
  }

  .case-study_quote-footer {
    margin-top: var(--space-medium);
  }

  .case-study_badge {
    width: var(--case-badge-size);
    height: var(--case-badge-size);
  }

  /* left/bottom already set at 767px; only the real 440-anchor width
     differs (252px there vs 264px here). */
  .case-study_play {
    width: 16.5rem; /* 264px [LOCKED] 2275:1156 */
  }
}

/* who we serve section css */
/* FIGMA: desktop node-id 2777-1365 (refreshed 2026-09-10; previously
   2626-3882), mobile node-id 2795-127 (previously 2669-77). The current
   frames are full section backgrounds, not just content sized to their own
   box like the earlier ones were - hence .serve_section's own background
   and shadow below, which the section never carried before this refresh.

   Nine industry photo cards. Desktop lays them out as a staggered brick:
   row 1 holds five, row 2 holds four, offset half a card right and 32px
   down. Both frames draw every card at the same 243 x 325, which is what
   lets the stagger be built as ten equal grid columns (each card spans
   two, so half a card is one column) instead of a fixed-pixel offset - the
   grid stays fluid at any width and the offset falls out of the maths.

   Below 767 the nine cards run in a single row that overflows the 440
   frame instead of staying in the brick, so they swap to this project's
   standard carousel, Swiper - see js/modules/slider.js. */

/* Same treatment as .process_section immediately below it on the page -
   see --serve-section-bg/--serve-section-shadow in 01-tokens.css for why
   this reuses that section's exact fill and inset shadow rather than two
   coincidentally-identical values. */
.serve_section {
  position: relative;
  background-color: var(--serve-section-bg);
  box-shadow: var(--serve-section-shadow);
}

.serve_heading {
  margin-left: auto;
  margin-right: auto;
  max-width: var(--container-medium);
  color: var(--serve-heading-ink);
}

.serve_grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  column-gap: var(--space-small);
  row-gap: var(--space-medium);
  margin-top: var(--serve-heading-gap);
}

/* Row 1: five cards on the odd half-columns. */
.serve_grid > :nth-child(1) { grid-column: 1 / span 2; grid-row: 1; }
.serve_grid > :nth-child(2) { grid-column: 3 / span 2; grid-row: 1; }
.serve_grid > :nth-child(3) { grid-column: 5 / span 2; grid-row: 1; }
.serve_grid > :nth-child(4) { grid-column: 7 / span 2; grid-row: 1; }
.serve_grid > :nth-child(5) { grid-column: 9 / span 2; grid-row: 1; }
/* Row 2: four cards, offset one half-column = half a card. */
.serve_grid > :nth-child(6) { grid-column: 2 / span 2; grid-row: 2; }
.serve_grid > :nth-child(7) { grid-column: 4 / span 2; grid-row: 2; }
.serve_grid > :nth-child(8) { grid-column: 6 / span 2; grid-row: 2; }
.serve_grid > :nth-child(9) { grid-column: 8 / span 2; grid-row: 2; }

.serve_card {
  position: relative;
  aspect-ratio: 243 / 325; /* [LOCKED] every card, both frames */
  overflow: hidden;
}

/* Each source photo is bigger than its card and offset - left/top/width/
   height as a share of the card, set inline per card straight from
   Figma's own "Fill" box for that layer (2777:1365, refreshed 2026-09-10).
   Percent rather than px so the crop holds at any card size, not just
   243 x 325.

   object-fit:cover is required, not decorative: the box's own aspect
   ratio never matches the source photo's, by design - that mismatch is
   exactly what makes it a crop instead of a stretch. Figma resolves that
   mismatch per photo (sometimes as a plain object-fit:cover export,
   sometimes as its own manually-computed inner crop achieving the same
   result) - letting the browser's real cover algorithm redo it against
   the actual downloaded pixels reproduces either export path from just
   the one outer box, without having to know which path Figma took for a
   given photo. */
.serve_card-image {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  height: var(--h);
  max-width: none;
  object-fit: cover;
}

/* Top scrim, so the white label reads over any photo underneath it. 107 of
   325px in Figma (2626:3889), carried as a share of the card height so it
   holds at any size rather than a fixed px band.

   z-index:1 is load-bearing, not decoration: a plain sibling after the
   image in DOM order paints on top of one before it at the shared auto
   stacking level, so without this every card was showing its raw photo
   straight behind the label - which is what first read as a hazy,
   low-contrast "blur" rather than a dark gradient.

   Plain two-stop linear fade, 0.8 to 0 top to bottom - matching Figma
   exactly rather than a plateau'd version tried first to hold contrast
   under a two-line label. That version fixed the contrast but cost the
   thing Figma actually draws: in the comp this blends smoothly, with no
   visible edge at all, and a plateau blurred by 20px still showed a faint
   seam where the flat section ended and the fade began. A plain linear
   fade has no seam to begin with, and the 20px blur below carries enough
   of its own darkness past the box's nominal bottom edge to keep a
   two-line label readable without one.

   backdrop-filter + filter, not a downloaded image: 2626:3889 is a
   Figma "background blur" layer, which samples whatever sits behind it
   live - there is no flat asset to export. Isolated, with no photo behind
   it, Figma's own export has nothing to sample but the frame's page-grey
   fill, which is exactly what came back testing this: a plain grey-to-dark
   gradient, no transparency, no photo. Baking that one export in across
   nine different photos would look right on none of them. backdrop-filter
   is the faithful version: like Figma, it blurs whatever photo is actually
   behind it, per card, live.

   TWO NESTED ELEMENTS: kept even though the outer no longer carries a
   filter (below), because `filter` and `backdrop-filter` on the SAME
   element is unreliable in Safari - it can drop the backdrop-filter
   entirely once `filter` is also present. Splitting them keeps that door
   closed if the outer element ever needs a filter of its own again.
   `.serve_card-scrim` (outer) is layout/positioning only; `.serve_card-
   scrim-inner` (inner) carries the actual backdrop-filter + gradient.

   NO filter:blur() ON THE OUTER ELEMENT. Figma's own layer tree pairs a
   "Layer blur" on the outer frame with a "Background blur" on the inner
   one, so the outer originally carried `filter: blur(20px)` to match that
   1:1. Measured against Figma pixel-for-pixel (2026-09-10), that 20px
   blur was the actual bug, not a faithful translation: blurring a band
   this short (107px) smears its own hard top edge across a big share of
   its own height, which drags the strong 0.8-alpha opacity at the top
   down to roughly a third of its intended strength and leaves the card
   visibly LESS scrimmed than Figma renders it - confirmed by sampling
   pixels with and without the outer blur, not eyeballed. Dropping it
   (while keeping the inner element's own 2.05px backdrop-filter, which is
   too small relative to the band to cause the same smear) reproduces
   Figma's actual contrast; CSS filter and Figma's "Layer blur" are
   evidently not the same operation at this width. */
.serve_card-scrim {
  position: absolute;
  z-index: 1;
  top: 0;
  left: -4.53%;  /* -11 of 243px [LOCKED] */
  width: 121.8%; /* 296 of 243px [LOCKED] */
  height: 33%;
  pointer-events: none;
}

.serve_card-scrim-inner {
  position: absolute;
  inset: 0;
  -webkit-backdrop-filter: blur(2.05px); /* [LOCKED] the inner layer */
  backdrop-filter: blur(2.05px);
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
}

.serve_card-label {
  position: absolute;
  z-index: 2;
  left: var(--serve-card-pad);
  right: var(--serve-card-pad);
  top: var(--serve-card-pad);
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: 1.25rem;   /* 20px [LOCKED] 2626:3890 */
  line-height: 1.75rem; /* 28px [LOCKED] */
  color: var(--color-white);
}

/* --------------------------------------------------------------------------
   MOBILE SLIDER
   Same nine cards, Swiper-driven. Hidden until the grid above no longer
   reads as a grid - see the 767 block below, the same breakpoint the
   shared hide/show-mobile-landscape utilities target.
   -------------------------------------------------------------------------- */
.serve_slider {
  display: none;
  margin-top: var(--serve-heading-gap);
}

/* The card gets its width here, on the SLIDE itself, not on .serve_card.
   data-slider-preset="peekFixed" sets slidesPerView:'auto', which sizes
   each .swiper-slide off its own CSS width rather than a fraction of the
   container - if the fixed width instead sat on .serve_card, one level
   further in, Swiper would still divide the container by a decimal
   slidesPerView it doesn't have, size the SLIDE arbitrarily wide, and
   leave dead space between the card's true right edge and the next
   slide. flex-shrink:0 keeps Swiper's own flex track from squeezing it
   back down below that width. */
.serve_slider .swiper-slide {
  width: 15.1875rem; /* 243px [LOCKED], the drawn card */
  flex-shrink: 0;
}

/* Pagination dots, 2269:490. Drawn as one rotated SVG rather than real
   Swiper markup: a 12 x 136 vertical strip, rotated -90deg to lay flat
   under the slider, so a 40-tall pill becomes a 40-WIDE one and the four
   12px circles keep their size either way. The one thing easy to miss
   reading it flat is that EVERY shape - the pill included - shares the
   exact same fill and opacity, #848484 at 0.4 alpha. Active is a shape
   change only, not the shared component's grey-to-brand colour swap, so
   this overrides that default rather than reusing it.

   Gap is 0.5rem by choice, not measurement: the SVG draws a uniform 12px
   edge-to-edge, but nine real bullets read as tighter/cleaner at 8px -
   --space-xsmall, back on the ladder - than at the drawn spacing. */
.serve_slider .slider_pagination {
  gap: var(--space-xsmall); /* 8px, off the SVG's drawn 12px by choice */
}

.serve_slider .slider_pagination .swiper-pagination-bullet {
  width: 0.75rem;  /* 12px [LOCKED] */
  height: 0.75rem; /* 12px [LOCKED] */
  background-color: rgba(132, 132, 132, 0.4); /* #848484 @ 0.4 alpha [LOCKED] */
}

.serve_slider .slider_pagination .swiper-pagination-bullet-active {
  width: 2.5rem; /* 40px [LOCKED] */
  background-color: rgba(132, 132, 132, 0.4); /* same fill - shape is the only change [LOCKED] */
}

/* No tablet frame exists for this section (or the page - DESIGN.md flags
   the gap sitewide), so this tier is this build's own call, same as the
   767/479 tiers below it are for other sections without a measured
   anchor. The desktop brick only works at exactly nine cards across two
   rows of five and four: it is ten grid columns with each card fixed to
   a span-2 slot, so shrinking the container just shrinks every card in
   place rather than reflowing them - a label like "Restaurant and food
   service platforms" was wrapping to four lines before it ran out of
   width entirely. Below 991, the brick's exact column assignments are
   dropped back to auto and the grid is handed a plain minmax track
   instead, so it wraps like an ordinary card grid: three or four per
   row depending on width, however many actually fit, down to the 767
   handoff to the slider below. */
@media screen and (max-width: 991px) {
  .serve_grid {
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  }

  /* :nth-child(n) matches every child, same as the wildcard tried first,
     but it carries the same specificity as the .serve_grid > :nth-child(1)
     etc. rules above rather than less of it - a wildcard was losing that
     fight and leaving every card pinned to its brick-row column/span,
     which is what turned the auto-fill track into wreckage instead of a
     wrap. */
  .serve_grid > :nth-child(n) {
    grid-column: auto;
    grid-row: auto;
  }
}

@media screen and (max-width: 767px) {
  .serve_grid {
    display: none;
  }

  .serve_slider {
    display: block;
  }
}

@media screen and (max-width: 479px) {
  /* Nicola Display Medium 28/36 -1%, 2269:468 - off the shared H3 ladder
     (26/34 at this tier), so it gets its own pair instead of bending
     heading-style-h3 for one section. Left-aligned here too: the desktop
     centring is this build's own call for the wide line, and the 440
     frame draws it flush left instead. */
  .serve_heading {
    max-width: none;
    text-align: left;
    font-size: var(--serve-heading-fs);
    line-height: var(--serve-heading-lh);
    letter-spacing: var(--serve-heading-ls);
  }
}


/* ==========================================================================
   SECTION :: THE OFFERS   (homepage, between CASE STUDIES and CONVERSION)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2777-6354 (desktop 1440). No mobile frame supplied, so
   every breakpoint block at the end of this section is [UNVERIFIED].

   This block sits above THE JOIN banner below rather than in page order
   next to CONVERSION, and deliberately: the join is a provenance line, not
   a running order, and moving one block across it would make the sentence
   it carries false. The file already runs out of page order in this half
   (CASE STUDIES above WHO WE SERVE), so the section's position on the page
   is stated here instead.

   Three offer cards on the page grey. Each card is a sheet with a
   file-folder tab, and the cards pin into a deck as you scroll past them.

   THE SHEET IS FOUR THINGS, THREE OF THEM ALREADY TOKENS
     flat fill      --offers-card-bg-dark / -light, both existing colours
     inset shadow   --offers-card-shadow, the same 0/19/27.7 figure
                    --serve-section-shadow and --conversion-bg-shadow carry
     paper speckle  --btn-grain, the fractalNoise recipe the nav plate, the
                    testimonial card and .process_step all already use
     torn edge      the only part with no equivalent, so the only part that
                    ships as artwork: media/home/offers/card-edge-*.svg,
                    2.2KB each, used as a MASK. See 01-tokens.css for what
                    the exported grounds were and why they are not here.

   Three edge files rather than one, differing only in their feTurbulence
   seed, because a tear repeated verbatim three times down one column reads
   as a pattern - which is a tiling artefact, not a design.

   MASKS EVERYWHERE, AND WHY. The drawn hairline, the who-for outline and
   the three figure icons are all single-colour artwork that has to appear
   in two inks (light on the dark card, dark on the light ones). Shipped
   once each and tinted with background-color through a mask, the same
   idiom .case-study_logo already uses for the client logos. mask-image on
   a bare SVG document masks by ALPHA, not luminance - see the long note on
   .process_step before changing any of these files.
   -------------------------------------------------------------------------- */
.offers_intro {
  text-align: center;
}

.offers_heading {
  max-width: var(--offers-heading-w);
  margin-inline: auto;
}

.offers_lede {
  max-width: var(--offers-lede-w);
  margin: var(--offers-lede-gap) auto 0;
  font-family: var(--font-body);
  font-size: var(--fs-large);
  line-height: var(--lh-large);
  letter-spacing: var(--ls-normal);
  color: var(--color-ink-copy);
}

/* The second sentence is a different style, not a <strong>: Figma draws it
   Trust 2A MEDIUM at -1.2% while the first stays Regular at 0. It carries
   no extra semantic weight, so the span is presentational and the element
   stays a span rather than <strong>. */
.offers_lede-strong {
  font-weight: var(--weight-medium);
  letter-spacing: var(--ls-medium);
}

.offers_stack {
  margin-top: var(--offers-intro-gap);
}

/* --------------------------------------------------------------------------
   ONE CARD
   --------------------------------------------------------------------------
   Every ink on a card is a custom property set here and read by the parts
   below, so the dark variant is one block of overrides rather than a
   parallel set of `.is-dark .offers_x` rules for every child.
   -------------------------------------------------------------------------- */
.offers_card {
  --offers-card-bg:        var(--offers-card-bg-light);
  --offers-card-ink:       var(--color-ink-copy);
  --offers-card-ink-strong: var(--color-ink);
  --offers-card-ink-label: var(--color-ink);
  --offers-card-lead-ink:  var(--offers-ink-light-lead);
  --offers-card-rule-ink:  var(--offers-rule-dark);
  --offers-card-tab-ink:   var(--offers-ink-tab);
  /* No inner shadow: the frame draws the light sheets flat. Only the dark
     one is lit from above - see --offers-card-shadow in 01-tokens.css. */
  --offers-card-sheet-shadow: none;
  --offers-card-sheet-paper: var(--offers-card-paper);
  --offers-card-grain:     0;
  --offers-card-icon-h:    var(--offers-term-icon);
  --offers-card-coin-h:    var(--offers-term-coin);
  /* The light cards draw their ticks a shade stronger than the dark one. */
  --offers-card-check-opacity: 0.8;

  position: relative;
}

.offers_card.is-dark {
  --offers-card-bg:        var(--offers-card-bg-dark);
  --offers-card-ink:       var(--offers-ink-dark);
  --offers-card-ink-strong: var(--offers-ink-dark);
  --offers-card-ink-label: var(--offers-ink-dark-label);
  --offers-card-lead-ink:  var(--offers-ink-dark);
  --offers-card-rule-ink:  var(--offers-ink-dark-label);
  --offers-card-tab-ink:   var(--color-white);
  --offers-card-sheet-shadow: var(--offers-card-shadow);
  /* No image fill on this one in Figma: its ground is the flat colour plus
     the noise pass its own vector export carries. */
  --offers-card-sheet-paper: none;
  --offers-card-grain:     var(--btn-grain-opacity);
  --offers-card-icon-h:    var(--offers-term-icon-dark);
  --offers-card-coin-h:    var(--offers-term-coin-dark);
  --offers-card-check-opacity: var(--offers-check-opacity);
}

.offers_card + .offers_card {
  margin-top: var(--offers-stack-gap);
}

/* One tear per card. nth-of-type, not a class: which tear a card gets is
   not a design decision anyone needs to express in the markup. */
.offers_card:nth-of-type(1) .offers_sheet {
  --offers-card-edge: url("/media/home/offers/card-edge-1.svg");
}

.offers_card:nth-of-type(2) .offers_sheet {
  --offers-card-edge: url("/media/home/offers/card-edge-2.svg");
}

.offers_card:nth-of-type(3) .offers_sheet {
  --offers-card-edge: url("/media/home/offers/card-edge-3.svg");
  /* A different phase of the same sheet of paper than the card above, which
     is what the frame does with its two crops. */
  background-position: left bottom;
}

/* --------------------------------------------------------------------------
   THE TAB
   --------------------------------------------------------------------------
   Same trapezoid family as .case-study_tag and built the same way, in
   clip-path rather than the exported artwork. Figma traces
   "M4 1L248 1L220.564 57H31.4359L4 1Z" and flips it vertically, so what
   renders is narrow at the top and wide where it meets the sheet.

   Width is content-driven, not the frame's 244 / 298 / 275: those three
   numbers are one 45px inset either side of three different labels, so the
   padding is the real decision and the widths fall out of it.

   The bottom --offers-tab-overlap sits BEHIND the sheet, which is what the
   negative margin and the z-index pair below arrange. Without the overlap
   the tab would read as a separate chip floating above the card.
   -------------------------------------------------------------------------- */
.offers_tab {
  position: relative;
  z-index: 0;
  display: inline-flex;
  align-items: flex-start;
  height: var(--offers-tab-h);
  margin-bottom: calc(var(--offers-tab-overlap) * -1);
  padding: var(--offers-tab-pad-top) var(--offers-tab-pad-x) 0;
  clip-path: polygon(
    var(--offers-tab-slope) 0,
    calc(100% - var(--offers-tab-slope)) 0,
    100% 100%,
    0 100%
  );
  background-color: var(--offers-card-bg);
  font-family: var(--font-mono);
  font-size: var(--fs-regular);
  font-weight: var(--weight-semibold);
  line-height: var(--lh-regular);
  letter-spacing: var(--ls-regular);
  text-transform: uppercase;
  color: var(--offers-card-tab-ink);
}

/* THE FLAG, on the website card only. Same trapezoid, in the accent blue,
   sitting far enough right that the tab paints over its left end. It is
   marked up BEFORE the tab so that overlap needs no z-index of its own. */
.offers_badge {
  position: absolute;
  top: var(--offers-badge-top);
  left: var(--offers-badge-left);
  z-index: 0;
  display: flex;
  align-items: center;
  width: var(--offers-badge-w);
  height: var(--offers-badge-h);
  padding-left: var(--offers-badge-pad);
  clip-path: polygon(
    var(--offers-tab-slope) 0,
    calc(100% - var(--offers-tab-slope)) 0,
    100% 100%,
    0 100%
  );
  background-color: var(--color-accent);
  font-family: var(--font-body);
  font-size: var(--offers-badge-fs);
  line-height: var(--offers-badge-lh);
  color: var(--color-white);
}

/* --------------------------------------------------------------------------
   THE SHEET
   -------------------------------------------------------------------------- */
.offers_sheet {
  position: relative;
  /* Above the tab, so the tab's wide end disappears behind it, and its own
     stacking context so the grain and the stack scrim can sit either side
     of the content with z-index -1 / 1. */
  z-index: 1;
  isolation: isolate;
  padding: var(--offers-card-pad) var(--offers-card-pad) var(--offers-card-pad-end);
  /* Colour behind the paper, not instead of it: it is what shows if the
     image never arrives, and it is the ground the dark card paints with. */
  background-color: var(--offers-card-bg);
  background-image: var(--offers-card-sheet-paper);
  /* cover, which is what the frame does with it: the sheet is 725 wide and
     the card 1280, so one scaled-up slice of the middle of the paper fills
     it. Tiling it at natural size was tried and measured instead - it is
     sharper, but it puts the whole sheet on screen, and the parts of it the
     frame never shows are visibly flatter than the parts it does. */
  background-size: cover;
  background-position: center;
  box-shadow: var(--offers-card-sheet-shadow);
  -webkit-mask-image: var(--offers-card-edge);
  mask-image: var(--offers-card-edge);
  /* 100% 100%, not cover: the file is 1286x402 with preserveAspectRatio
     "none", so it is meant to be stretched to whatever the card measures.
     cover would crop the tear off two of the four edges. */
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.offers_sheet::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--btn-grain);
  opacity: var(--offers-card-grain);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   BODY: WHO IS THIS FOR  |  PITCH + CHECKLIST
   -------------------------------------------------------------------------- */
.offers_body {
  display: grid;
  grid-template-columns: var(--offers-who-w) minmax(0, 1fr);
  column-gap: var(--offers-body-gap);
}

.offers_who-label,
.offers_or-label {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  /* The "Overline" named style is the hero kicker's face, size and tracking
     on a 20px line instead of 24, so only the line height is new. */
  line-height: var(--offers-overline-lh);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
}

/* The drawn outline runs to the bottom of the row, not to the bottom of
   its own copy. Figma draws all three boxes deeper than the words inside
   them - 194 / 195 / 172 against copy of 156 / 120 / 96 - and in every one
   the bottom edge lands level with the end of the checklist beside it. That
   is a box sized by the row, so it is built as one: the column is a flex
   column and the box takes the leftover. Left to hug its text it reads as a
   caption that ran short, which is what it looked like before. */
.offers_who {
  display: flex;
  flex-direction: column;
}

.offers_who-label {
  position: relative;
  z-index: 1;
  color: var(--offers-card-ink-label);
}

/* The label's line box crosses the outline's top edge, and the drawn
   outline carries a notch there rather than running behind the words - so
   the box is pulled up under the label rather than the label being pushed
   into the box. */
.offers_who-box {
  position: relative;
  isolation: isolate;
  flex: 1;
  margin-top: calc(var(--offers-who-label-sink) * -1);
  padding: var(--offers-who-pad-top) var(--offers-who-pad) var(--offers-who-pad);
}

.offers_who-box::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--offers-card-ink-label);
  opacity: var(--offers-who-opacity);
  -webkit-mask-image: url("/media/home/offers/whofor-box.svg");
  mask-image: url("/media/home/offers/whofor-box.svg");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  pointer-events: none;
}

.offers_who-copy,
.offers_item {
  font-family: var(--font-body);
  font-size: var(--fs-regular);
  line-height: var(--lh-regular);
  /* Trust 2A at 0, not Body 3's Figtree at -0.4%. Same size, different
     style; --ls-regular would tighten it by a quarter pixel per glyph. */
  letter-spacing: var(--ls-normal);
  color: var(--offers-card-ink);
}

.offers_who-copy + .offers_who-copy {
  margin-top: var(--offers-who-para-gap);
}

.offers_lead {
  font-family: var(--font-body);
  font-size: var(--fs-large);
  line-height: var(--lh-large);
  letter-spacing: var(--ls-normal);
  color: var(--offers-card-lead-ink);
}

/* THREE COLUMNS, FILLED COLUMN BY COLUMN. grid-auto-flow: column with both
   tracks stated: eight items land 3 / 3 / 2 exactly as the frame draws
   them, and the markup order is then also the reading order, so no
   reordering happens between DOM and screen. */
.offers_list {
  margin-top: var(--offers-lead-gap);
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(3, auto);
  column-gap: var(--offers-list-col-gap);
  row-gap: var(--offers-list-row-gap);
}

.offers_item {
  display: grid;
  grid-template-columns: var(--offers-check-col) minmax(0, 1fr);
}

.offers_check {
  /* 18px on a 24px line would centre the glyph in the line box; the frame
     drops it 4px so it sits on the item's first line instead. */
  padding-top: var(--offers-check-drop);
  font-size: var(--fs-medium);
  line-height: 1;
  opacity: var(--offers-card-check-opacity);
}

/* --------------------------------------------------------------------------
   THE DRAWN HAIRLINE
   --------------------------------------------------------------------------
   2px tall, not 1: the export is a 1200x1 box with the drawn line placed at
   -1px, so the wobble needs the second pixel to exist at all. Height stays
   in px for the same reason every other hairline on this project does - it
   must not grow with the root font size.
   -------------------------------------------------------------------------- */
.offers_rule,
.offers_or::before,
.offers_or::after {
  height: 2px;
  background-color: var(--offers-card-rule-ink);
  opacity: var(--offers-rule-opacity);
  -webkit-mask-image: url("/media/home/offers/rule.svg");
  mask-image: url("/media/home/offers/rule.svg");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.offers_rule {
  margin-top: var(--offers-rule-gap);
}

/* --------------------------------------------------------------------------
   TERMS + CTA
   -------------------------------------------------------------------------- */
.offers_foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-medium);
  margin-top: var(--offers-terms-gap);
}

.offers_terms {
  display: flex;
  align-items: center;
  gap: var(--offers-terms-col-gap);
}

/* Two priced tiers. A grid rather than two flex rows, so the figures line
   up column by column across both tiers - "5 pages" and "10 pages" start
   at the same x in the frame, and a flex row would let the wider second
   tier push its neighbours right. */
.offers_terms.is-tiered {
  display: grid;
  grid-template-columns: repeat(3, max-content);
  column-gap: var(--offers-terms-col-gap);
  row-gap: var(--offers-terms-row-gap);
  align-items: center;
}

.offers_term {
  display: flex;
  align-items: center;
  gap: var(--offers-term-gap);
  font-family: var(--font-body);
  font-weight: var(--weight-medium);
  font-size: var(--offers-price-fs);
  line-height: var(--offers-price-lh);
  letter-spacing: var(--offers-price-ls);
  color: var(--offers-card-ink-strong);
  white-space: nowrap;
}

.offers_card.is-dark .offers_term {
  font-size: var(--offers-price-fs-dark);
  line-height: var(--offers-price-lh-dark);
}

/* Each icon is sized off ONE height token per card and the artwork's own
   aspect ratio, so the dark card's larger set is a single override rather
   than six width/height pairs. The ratios are the trimmed bounding boxes of
   the three files, not eyeballed.

   TWO OF THE THREE ARE PAINTED, ONE IS TINTED, and the split is Figma's,
   not a preference. The clock and the pages are pencil drawings: a light
   fill with darker linework inside it, which is what makes them read as
   drawn on both grounds. Flattening either into a mask throws that interior
   away - every pixel the drawing covers is opaque, so a tint fills the
   whole silhouette and the clock comes out a solid blob. Only the coins are
   drawn in Figma as a mask over a colour fill (2777:6380), and only that one
   is a mask here, which is also what lets it take the card's ink. */
.offers_term-icon {
  flex: none;
  height: var(--offers-card-icon-h);
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

.offers_term-icon.is-price {
  height: var(--offers-card-coin-h);
  width: calc(var(--offers-card-coin-h) * 1.9);
  background-color: currentColor;
  -webkit-mask-image: url("/media/home/offers/icon-price.png");
  mask-image: url("/media/home/offers/icon-price.png");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}

.offers_term-icon.is-pages {
  width: calc(var(--offers-card-icon-h) * 1.06);
  background-image: url("/media/home/offers/icon-pages.png");
}

.offers_term-icon.is-time {
  width: var(--offers-card-icon-h);
  background-image: url("/media/home/offers/icon-clock.png");
}

.offers_or {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--space-small);
}

.offers_or::before,
.offers_or::after {
  content: "";
  flex: 1;
}

.offers_or-label {
  opacity: var(--offers-or-opacity);
  color: var(--offers-card-ink-label);
}

.offers_cta {
  flex: none;
  width: var(--offers-cta-w);
}

/* The filled dark key the two light cards carry. Not a global button
   variant yet; see --offers-cta-ink-bg in 01-tokens.css. */
.offers_cta.is-ink {
  background-color: var(--offers-cta-ink-bg);
  color: var(--offers-cta-ink-label);
}

/* --------------------------------------------------------------------------
   THE STACK
   --------------------------------------------------------------------------
   MIN-WIDTH, against this file's desktop-first grain, and deliberately: the
   deck is an enhancement on top of a plain vertical run of three cards, so
   the plain run is the base rule and the stack is what gets opted into.

   Three conditions have to hold before a card pins:
     992px wide   below it the body is one column and the cards are tall
     700px tall   a card taller than the viewport pins its top off-screen
                  and hides its own CTA, which is worse than not pinning
     no-preference  reduced motion gets the vertical run, not a quieter
                  version of the deck

   Each card sticks one --offers-stack-step lower than the one above, so the
   card behind keeps a strip of itself and its tab visible. The scale and
   the scrim are driven by --stack-progress, a 0-1 value that
   js/modules/card-stack.js writes per card; with no JS both resolve to 0
   and the cards simply stack flat, which is a perfectly good degraded
   state. transform-origin is the top edge so a shrinking card stays pinned
   where it is instead of sliding up out of its own pin.
   -------------------------------------------------------------------------- */
@media screen and (min-width: 992px) and (min-height: 700px) and (prefers-reduced-motion: no-preference) {
  .offers_card {
    position: sticky;
    top: var(--offers-stack-top);
    /* Equal sticky boxes: empty space below the sheet brings every card's
       BORDER BOX - which is what sticky measures itself against - up to the
       height of the tallest. Without it the three unpin in height order
       rather than document order, and the middle card rides up over the top
       one at the bottom of the stack, tabs crossing.

       The space is given back on the NEXT card's top margin below, never on
       this card's bottom margin: sticky insets its constraint rectangle by
       the element's own margins, so a negative bottom margin here would
       hand back the exact height the padding added and change nothing.
       js/modules/card-stack.js publishes both numbers. */
    padding-bottom: var(--stack-fill, 0px);
    transform: scale(calc(1 - var(--stack-progress, 0) * var(--offers-stack-scale)));
    transform-origin: top center;
  }

  .offers_card + .offers_card {
    margin-top: calc(var(--offers-stack-gap) - var(--stack-lead, 0px));
  }

  /* The last card's fill has no next card to give it back to, so the stack
     itself returns it. On the container, never on the card: see the note in
     js/modules/card-stack.js. */
  .offers_stack {
    margin-bottom: calc(-1 * var(--stack-tail, 0px));
  }

  /* The tab goes with the strip it lives in. Full strength for the whole
     approach and for the assembled deck; it only fades as the deck closes
     up, where the next card's narrower tab would otherwise leave the tail
     of this one's label poking out past its edge.

     Twice the rate, so the tab is gone by the time half the strip is: the
     label sits in the top 34px of a 40px strip, so a tab still half-lit at
     half a strip is a label with its bottom sliced off. opacity clamps its
     own input, which is what keeps the assembled deck at a flat 1. */
  .offers_tab,
  .offers_badge {
    opacity: calc(var(--stack-exposure, 1) * 2 - 1);
  }

  .offers_card:nth-of-type(2) {
    top: calc(var(--offers-stack-top) + var(--offers-stack-step));
  }

  .offers_card:nth-of-type(3) {
    top: calc(var(--offers-stack-top) + var(--offers-stack-step) * 2);
  }

  /* Over the content, under nothing: this is what keeps the card on top the
     readable one. Masked along with the rest of the sheet, so it stops at
     the torn edge rather than painting a hard rectangle over it. */
  .offers_sheet::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background-color: var(--offers-stack-scrim);
    opacity: calc(var(--stack-progress, 0) * var(--offers-stack-dim));
    pointer-events: none;
  }
}

/* --------------------------------------------------------------------------
   BREAKPOINTS.  All [UNVERIFIED]: the frame is desktop only.
   -------------------------------------------------------------------------- */
/* The checklist's third column is the first thing to run out of room: at
   1200 the three tracks are under 180px and two-word lines start breaking
   mid-phrase. Four rows of two hold the same eight items. */
@media screen and (max-width: 1199px) {
  .offers_list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(4, auto);
  }
}

@media screen and (max-width: 991px) {
  .offers_card {
    --offers-card-pad: var(--space-medium);
  }

  /* The who-for box stops being a column and becomes the block that opens
     the card. */
  .offers_body {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--offers-body-gap);
  }

  .offers_foot {
    flex-direction: column;
    align-items: stretch;
    gap: var(--offers-terms-gap);
  }

  .offers_cta {
    width: 100%;
  }
}

@media screen and (max-width: 767px) {
  .offers_list {
    grid-auto-flow: row;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    row-gap: var(--space-small);
  }

  /* Three figures at 28px will not sit on one line at this width. Back to a
     wrapping flex row: the column alignment between the two tiers is worth
     less than keeping each figure on one line, and the drawn "or" still
     spans the full width to separate them. */
  .offers_terms.is-tiered {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--offers-terms-col-gap);
    row-gap: var(--space-small);
  }

  .offers_or {
    width: 100%;
  }

  .offers_card .offers_term,
  .offers_card.is-dark .offers_term {
    font-size: var(--fs-title-1);
    line-height: var(--lh-title-1);
  }

  .offers_card {
    --offers-card-icon-h: var(--space-small);
    --offers-card-coin-h: var(--space-small);
  }
}

@media screen and (max-width: 479px) {
  .offers_card {
    --offers-card-pad: var(--space-small);
    --offers-card-pad-end: var(--space-medium);
    --offers-tab-pad-x: var(--space-medium);
  }

  /* The flag no longer fits beside the tab, so it moves under it and the
     two stop overlapping. */
  .offers_badge {
    position: static;
    width: auto;
    max-width: 100%;
    margin-bottom: calc(var(--offers-tab-overlap) * -1);
    padding-right: var(--offers-badge-pad);
  }
}


/* ==========================================================================
   SECTION :: ASSESSMENT   (homepage, sits directly above CONVERSION)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2777-6553 (desktop 1440). No mobile frame supplied.

   Section #12 in the README ownership table, "Assessment prompt" - the
   README's recorded node id (2315-8842) predates a reorganisation in the
   Figma file; this is that same content, re-pulled. Built 2026-09-11.

   A centred, narrow card: heading, subheading, body copy, then the SAME
   URL-plus-accent-button form the hero uses (form.css already names this
   section as the second of the two instances, in the comment on
   .form_inline.is-embedded), then a short testimonial. Two hand-drawn
   illustrations flank the card and bleed past the container, same idiom
   as .hero_decor-item.

   BOTH DECOR EXPORTS CAME BACK NARROWER THAN THEIR FIGMA BOUNDING BOX -
   the exact trap DESIGN.md documents for the hero decor: a frame clips the
   node's own export, not just what a section wrapper shows on the page.
   Both illustrations are cut flush to the HOMEPAGE FRAME'S OWN edges (x 0
   and x 1440), not to some edge nearer the card - a first pass here
   assumed the latter for the mountain and positioned it 132.8% out from
   the container, floating alone with a huge gap. A fresh 1440-wide render
   of the node (get_screenshot, not the node export, which carries its own
   full untrimmed bounding box and says nothing about what is actually cut)
   put the visible cluster at x 1144-1439, flush against 1440 same as the
   carpenter is flush against 0. Both therefore resolve to the SAME
   -6.25% off the container edge, left on one side and right on the other -
   not a coincidence, the frame's 80px gutter is what both are measuring.

   NO MOBILE FRAME. Both illustrations are hidden below 1280 rather than
   interpolated to a guessed size, same call the FAQ float image already
   makes for the same reason.
   ========================================================================== */

.assessment_section {
  position: relative;
  /* Same reasoning as .hero_section: the decor bleeds past both side edges
     on purpose and must stay clipped so it cannot widen the page, while Y
     is left `visible` on purpose - setting only one axis forces the other
     to `auto`, which would clip nothing here but is worth stating rather
     than relying on the default silently doing the right thing. */
  overflow-x: clip;
  overflow-y: visible;
}

.assessment_container {
  position: relative;
}

.assessment_card {
  /* Both decor images paint above normal-flow content by default - an
     absolutely positioned element always stacks over static content in the
     same context, regardless of DOM order (CSS2.1 §9.9). Each PNG's "empty"
     side is a flat fill close to the page grey, not true transparency, so
     without this the tail of the carpenter illustration silently covers
     the first few characters of the body copy and the quote. Same fix
     05-utilities.css already names for exactly this: .content-layer /
     .decor-layer, applied locally since only one side of this pairing is
     section-scoped. */
  position: relative;
  z-index: 1;
  max-width: var(--assessment-card-w);
  margin-inline: auto;
  text-align: center;
}

.assessment_subheading {
  margin-top: var(--space-small);
}

/* Body/B3 as drawn is Trust 2A, not the shared .text-size-regular class
   (Figtree) - same two-body-face split already flagged sitewide (DESIGN.md
   flag 6), built as drawn rather than silently switching the face. */
.assessment_body {
  margin-top: var(--space-small);
  max-width: 31.875rem; /* 510px [LOCKED] 2777:6559 */
  margin-inline: auto;
  font-family: var(--font-body);
  font-size: var(--fs-regular);
  line-height: var(--lh-regular);
  color: var(--color-assessment-ink);
}

.assessment_form {
  margin-top: var(--assessment-cta-gap);
}

.assessment_quote-block {
  margin-top: var(--assessment-cta-gap);
}

/* Body/B1 size (20/28) but the section's own darker ink, not
   --color-ink-copy - same "built as drawn" call as the body copy above. */
.assessment_quote {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-large);
  line-height: var(--lh-large);
  font-style: normal;
  color: var(--color-assessment-ink);
}

.assessment_quote p {
  margin: 0;
}

.assessment_quote strong {
  font-weight: var(--weight-medium);
}

.assessment_quote-author {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--assessment-avatar-gap);
  margin-top: var(--assessment-quote-gap);
  font-style: normal;
}

/* SQUARE, not circular. Both layers Figma composites here (2777:6572) are
   typed as plain rectangles in the metadata, and neither carries a rounded-
   corner class through get_design_context - confirmed by rendering the
   node itself rather than assuming the usual avatar convention. What reads
   as a "rounded" corner at a glance is the hand-painted brush edge of the
   yellow backdrop bleeding unevenly to the frame, not a border-radius. A
   circle crop was cutting that texture away on all four sides. */
.assessment_quote-avatar {
  flex-shrink: 0;
  width: 3.125rem; /* 50px [LOCKED] 2777:6572 */
  height: 3.125rem;
  object-fit: cover;
}

.assessment_quote-byline {
  display: flex;
  flex-direction: column;
  text-align: left;
}

/* Same face/weight pairing as .process_testimonial-name / -role and
   .case-study_quote-name / -role: Trust 2A 700 for the name, a real loaded
   weight, Trust 2A Regular at 70% opacity for the role. */
.assessment_quote-name,
.assessment_quote-role {
  font-family: var(--font-body);
  font-size: var(--fs-regular);
  line-height: var(--lh-regular);
  color: var(--color-assessment-ink);
}

.assessment_quote-name {
  font-weight: var(--weight-bold);
}

.assessment_quote-role {
  font-weight: var(--weight-regular);
  opacity: 0.7;
}

.assessment_decor {
  position: absolute;
  height: auto;
  pointer-events: none;
}

.assessment_decor.is-left {
  top: var(--assessment-decor-left-top);
  left: var(--assessment-decor-left-left);
  width: var(--assessment-decor-left-w);
}

.assessment_decor.is-right {
  top: var(--assessment-decor-right-top);
  right: var(--assessment-decor-right-right);
  width: var(--assessment-decor-right-w);
}

/* Hidden above 1280 by default; the media query below turns it on. Declared
   BEFORE that query, not after - same specificity either way, so source
   order is what decides, and a later plain rule would beat an earlier
   media-queried one at any width the query matches. */
.assessment_decor-mobile {
  display: none;
}

/* Below 1280 the flanking pair has nowhere to bleed to, so both hide and
   the in-flow illustration above takes over - all the way from tablet
   down through mobile, not just below 479. There is no real tablet frame
   for this section (or a tablet twin of 2795-476), so the same single
   mobile image is asked to cover both tiers rather than leaving tablet
   with no illustration at all. */
@media screen and (max-width: 1280px) {
  .assessment_decor {
    display: none;
  }

  .assessment_decor-mobile {
    display: block;
    width: var(--assessment-mobile-decor-w);
    max-width: 100%;
    height: auto;
    margin: 0 auto var(--space-large);
  }
}

@media screen and (max-width: 479px) {
  .assessment_body {
    max-width: none;
    /* [LOCKED] 2269:654. Reads lighter than the section's own darker
       --color-assessment-ink at this breakpoint - built as drawn, same
       "two different greys for two instances" call this file already
       makes elsewhere rather than forcing them to match. */
    color: var(--color-ink-copy);
  }

  /* Nicola Display on desktop (heading-style-h5), Trust 2A Medium here -
     same family switch the intro paragraph already makes across this
     breakpoint. Size and tracking both still land exactly on --fs-large /
     --lh-large / --ls-h5, so only family and weight actually change. */
  .assessment_subheading {
    font-family: var(--font-body);
    font-weight: var(--weight-medium);
    font-size: var(--fs-large);
    line-height: var(--lh-large);
    letter-spacing: var(--ls-h5);
  }

  .assessment_form {
    margin-top: var(--space-medium); /* 32px [LOCKED, derived] 2269:654 bottom to 2795:527 top */
  }

  .assessment_quote-block {
    margin-top: var(--space-large); /* 48px [LOCKED, derived] 2795:527 bottom to 2377:2711 top */
  }

  .assessment_quote-author {
    gap: var(--space-small); /* 16px [LOCKED, derived] avatar right edge to byline, wider than desktop's 12px */
    margin-top: var(--assessment-mobile-byline-gap);
  }

  /* Circular here, where the desktop layer is a plain rectangle - the
     opposite of the usual assumption, confirmed by rendering the mobile
     node rather than carrying the desktop shape over. */
  .assessment_quote-avatar {
    width: var(--assessment-mobile-avatar-size);
    height: var(--assessment-mobile-avatar-size);
    border-radius: var(--radius-pill);
  }

  .assessment_quote-name,
  .assessment_quote-role {
    font-size: var(--fs-small);
    line-height: var(--lh-small);
  }
}

/* ==========================================================================
   THE JOIN, 2026-09-10
   --------------------------------------------------------------------------
   Everything above this line is Himanshi's and Gayatri's work, taken from
   main unchanged. Everything below is Pratham's five sections, in page
   order, each a banner-delimited block that carries its own breakpoints.

   The two halves are CONCATENATED rather than interleaved, and that is not
   laziness: her sections span the file rather than sitting in blocks, hero
   reaching from line 27 to 1581 and process from 720 to 2677, so there is no
   contiguous run of hers to slice around. Ours contains no rule for any of
   her prefixes and hers none for ours, checked prefix by prefix at merge
   time, so nothing here can shadow anything there.

   If this file is ever reorganised into strict page order, move whole
   banner-delimited blocks and re-check that list. Do not split one.

   ASSESSMENT, ABOVE, IS NEITHER HALF. It was unclaimed in both the original
   join and the README ownership table (#12, never started), added after
   both authors' blocks rather than slotted into either - so the accounting
   above is still accurate for the two halves it describes, and this is a
   third, later addition on top of it.
   ========================================================================== */
/* ==========================================================================
   SECTION :: CONVERSION   (homepage, the first section under the nav)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2533-651 (desktop 1440). No mobile frame supplied.

   A dark full-bleed block with an isometric cube grid over it, a light strip
   torn along the top, and one paper card carrying the 90-day guarantee.

   TWO NODES IN THE FRAME ARE NOT BUILT, both deliberately:

     2533:683  a vertical "Strike While" repeat down the folded tab. It is
               HIDDEN. Not faint, hidden: the tab renders as bare paper grain
               when that strip of the frame is pulled up at six times
               contrast, and a 13-unit difference against the tab would have
               been obvious at that gain. Building it would have put text on
               the page that nobody can see.

     2533:659  a second copy of the body paragraph, sitting at frame level
               behind the card and completely covered by it. A leftover.

   THE CARD CANNOT LEAK. Its width, its padding and its artwork all come off
   the same box, so the copy is inside a normally padded element at every
   width rather than positioned over a picture and hoping. See the max-width
   note below for the half of that which is not obvious.
   ========================================================================== */

.conversion_section {
  position: relative;
  /* CLIPS ON PURPOSE. The torn strip is drawn taller than it shows and hangs
     over the top edge, and this is what cuts it back. `clip` and not `hidden`
     so this never becomes a scroll container, matching .page-wrapper. There
     is no sticky anything in this section, so the warning in the SHOWCASE
     banner does not apply here. */
  overflow: clip;
  background-color: var(--color-conversion-bg);
  background-image: url("/media/conversion-grid-tile.png");
  background-repeat: repeat;
  background-size: var(--conversion-grid-tile);
  /* Inset along the top edge, which is what makes the dark block read as
     recessed under the section above rather than stuck on top of it. */
  box-shadow: var(--conversion-bg-shadow);
}

/* The light strip along the top edge, ragged along its lower side.

   #sw-torn-edge and NOT one of the two deckles. Both of those build a drop
   shadow into the shape, which rules a hard dark line under the strip that
   the frame does not have: the frame's edge darkens the ground beneath it by
   about 6, and the deckles darken it by 23. The filter banner in
   partials/svg-defs.html carries the measurements.

   Drawn taller than it shows and pulled up by the overshoot, so the section
   clips the filter's wander off the TOP of the strip. Without that it moves
   both edges and the dark ground shows through as nicks along the boundary
   with whatever sits above, which is the one edge that has to stay straight. */
.conversion_section::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: calc(var(--conversion-edge-over) * -1);
  right: 0;
  left: 0;
  height: calc(var(--conversion-edge-h) + var(--conversion-edge-over));
  background-color: var(--color-conversion-edge);
  filter: url(#sw-torn-edge);
}

/* --------------------------------------------------------------------------
   THE CARD
   --------------------------------------------------------------------------
   The card is a normal padded box. The artwork is painted BEHIND it on a
   pseudo-element that hangs off both sides by the amount the picture is
   wider than the card. That ordering is the whole trick: the copy is laid
   out by padding rather than positioned over an image, so it cannot escape
   the paper at any width, and the artwork follows the box instead of the box
   having to follow the artwork.
   -------------------------------------------------------------------------- */
.conversion_card {
  position: relative;
  isolation: isolate; /* keeps the artwork's negative z-index inside the card */
  width: 100%;

  /* THE SECOND TERM IS THE OVERFLOW GUARD, and it is why this section needs
     no media query to stay inside the viewport.

     The artwork is wider than the card by --conversion-art-over on the right,
     and the card is centred, so the container has to hold the card plus that
     overhang on BOTH sides before the fold starts getting clipped. Solving
     card + 2 x over x card <= container gives the divisor below. At 1440 the
     860px cap wins; from about 1024 down the guard does, and the card gives
     up exactly as much width as the fold needs.

     Change --conversion-art-over and this recalculates itself. That is the
     reason it is a bare ratio in the token file rather than a percentage. */
  max-width: min(
    var(--conversion-card-w),
    calc(100% / (1 + 2 * var(--conversion-art-over)))
  );
  margin-inline: auto;
  /* No bottom margin, and no bleed. The section's bottom padding is zero, so
     the foot of the card lands on the section boundary by itself. See the
     note on --conversion-card-pad-b. */
  padding: var(--conversion-card-pad-y) var(--conversion-card-pad-x)
    var(--conversion-card-pad-b);
  /* The fallback surface, for the moment before the artwork arrives and for
     the case where it never does. Painted on the card itself and not on the
     pseudo-element, because the pseudo-element is wider than the card and a
     light fill on it would show as a pale band where the dark fold belongs.
     A negative z-index child paints above its parent's background, so the
     artwork covers this cleanly once it loads. */
  background-color: var(--color-conversion-card);
  color: var(--color-ink-copy);
}

.conversion_card::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 0;
  bottom: 0;
  left: calc(var(--conversion-art-under) * -100%);
  right: calc(var(--conversion-art-over) * -100%);
  /* Plain url() first as the fallback, then image-set() for anything that
     understands it. This is the first raster BACKGROUND on the site, so it
     cannot use the <picture> pattern in README: <picture> is markup and this
     is CSS. Same idea, same order, AVIF first.

     Do not collapse these into one declaration. The first line is what a
     browser without image-set() keeps. */
  background-image: url("/media/content-tv-bg.png");
  background-image: image-set(
    url("/media/content-tv-bg.avif") type("image/avif"),
    url("/media/content-tv-bg.webp") type("image/webp"),
    url("/media/content-tv-bg.png") type("image/png")
  );
  background-repeat: no-repeat;
  /* STRETCHES, and that is the considered choice rather than an oversight.
     The card's height is set by its copy, so the artwork has to meet it
     somewhere. Everything in this picture takes a vertical stretch without
     complaint: the paper is grain with a standard deviation of 3, the torn
     edges just read as a coarser tear, and the fold is a flat shape whose
     diagonals sit at fixed percentages of the height either way. Checked at
     a phone-shaped card, 3.6x taller than the export's own proportion, and
     it still reads as torn paper with a folded tab. `cover` was the
     alternative and it is worse: it would crop the fold off entirely. */
  background-size: 100% 100%;
}

/* --------------------------------------------------------------------------
   THE COPY
   --------------------------------------------------------------------------
   Vertical rhythm is margins between siblings rather than a flex column with
   one gap, because six different gaps are measured off the frame here and a
   single gap would flatten all six into an average of them.
   -------------------------------------------------------------------------- */
.conversion_heading {
  margin: 0;
  color: var(--color-ink);
  /* 48/56 at -0.025em is .heading-style-h2 exactly, which the markup carries,
     so no type is set here. -1.2px in the frame is -0.025em at 48px. */
}

/* Body/B2, 18/26 Trust 2A. There is no global class for it: .text-size-large
   is B1 at 20/28 and .text-size-medium is 18/28 in --font-para, so both are
   wrong by something. Set locally rather than overriding two properties of a
   class that does not mean this. If a third section needs B2, promote it to
   03-typography.css instead of copying this.

   The line-height is UNITLESS so it survives the size tiers: --fs-medium
   steps 18 -> 16 -> 14 down the breakpoints and a hard 26px would go from
   comfortable to cramped on the way. */
.conversion_body,
.conversion_list,
.conversion_closer {
  font-family: var(--font-body);
  font-size: var(--fs-medium);
  line-height: 1.4444; /* 26 / 18 */
  letter-spacing: 0;
  color: var(--color-ink-copy);
}

.conversion_body {
  max-width: var(--conversion-body-w);
  margin: var(--conversion-gap-body) 0 0;
}

/* 20/28 at letter-spacing 0 is exactly .text-size-large, which the markup
   carries. Only the spacing is set here. */
.conversion_fine {
  margin: var(--conversion-gap-fine) 0 0;
}

.conversion_list {
  margin: var(--conversion-gap-list) 0 0;
  padding: 0;
  list-style: none;
}

.conversion_list > li {
  position: relative;
  padding-left: var(--conversion-check-gap);
}

.conversion_list > li + li {
  margin-top: var(--conversion-list-gap);
}

/* The tick is a ::before rather than a character in the markup. It is pure
   decoration, the sentence beside it carries the whole meaning, and a literal
   U+2713 in the list item gets read out as "check mark" before every line.
   Generated content keeps it out of the accessibility tree in the browsers
   that matter and out of a copied selection everywhere.

   Frame puts the tick at x350 and its sentence at x381, so the 31px is the
   indent and not a gap between two boxes. */
.conversion_list > li::before {
  content: "\2713";
  position: absolute;
  top: 0;
  left: 0;
  /* Inherits the row's line-height, which centres the two on the same optical
     line. Figma nudges the tick 4px down off a leading-none box to land in
     the same place. */
}

.conversion_closer {
  margin: var(--conversion-gap-closer) 0 0;
}

.conversion_closer > p {
  margin: 0;
}

.conversion_closer > p + p {
  margin-top: var(--conversion-closer-gap);
}

.conversion_cta {
  margin-top: var(--conversion-gap-cta);
}

/* --------------------------------------------------------------------------
   991 AND BELOW
   --------------------------------------------------------------------------
   Pratham 2026-09-02. No frame for any of this.

   Nothing rearranges: the card is already a single column and it only has to
   get narrower. The card's own max-width handles that on its own, so all
   that is left is to take the padding in, which stops 60px gutters eating a
   375px screen, and to let the body run the full width once the card is
   narrower than the 560px measure.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 991px) {
  .conversion_card {
    padding: var(--space-medium) var(--space-medium) var(--space-xhuge);
  }

  .conversion_body {
    max-width: none;
  }
}

@media screen and (max-width: 767px) {
  .conversion_card {
    padding: var(--space-medium) var(--space-medium-tight) var(--space-xxlarge);
  }

  .conversion_list > li {
    padding-left: var(--space-medium);
  }

  /* Only the spacing changes here. The label is 34 characters of uppercase
     monospace, about 375px, which no phone-width card fits on one line, and
     that is handled by `is-wrap` in the markup rather than by a breakpoint:
     the button then wraps at whatever width it actually runs out of room,
     which measures as 480 and not 767. */
  .conversion_cta {
    margin-top: var(--space-medium);
  }
}

@media screen and (max-width: 479px) {
  .conversion_card {
    padding: var(--space-small) var(--space-small) var(--space-xlarge);
  }

  .conversion_closer,
  .conversion_list {
    margin-top: var(--space-small);
  }
}


/* ==========================================================================
   SECTION :: FIXES   (homepage, directly above TABS)
   --------------------------------------------------------------------------
   FIGMA: node-id 2522-204 (desktop 1440). No mobile frame supplied.

   A heading, three "why the usual fix fails" columns, and a dark CTA bar.

   NO BOTTOM PADDING, on purpose and at every width. The markup carries
   `padding-section-large padding-bottom-none`. TABS sits directly underneath
   with its own top padding, and the two share one ground colour, so any
   bottom padding here would double the gap and split one surface into two.
   `.padding-bottom-none` already existed in 04-layout.css for exactly this:
   "half-rhythm modifiers, for two sections that visually belong together".
   ========================================================================== */

.fixes_section {
  position: relative;
  background-color: var(--color-surface-light);
}

.fixes_heading {
  max-width: var(--fixes-heading-w);
  /* 40/48 at -2% is .heading-style-h3 exactly, so no type is set here. */
}

/* --------------------------------------------------------------------------
   THE THREE COLUMNS
   --------------------------------------------------------------------------
   Equal thirds. 3 x 400 + 2 x 40 is exactly 1280, the container, so the gap
   is not a free number: it is what makes the columns land on 400.
   -------------------------------------------------------------------------- */
.fixes_points {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--fixes-col-gap);
  margin-top: var(--space-xxlarge);
}

/* The icon is a fixed BOX with the artwork contained inside it, rather than
   three hand-tuned crops. The three glyphs have different proportions, about
   1.47, 1.05 and 0.97, and object-fit keeps each one honest while the shared
   box keeps their bottom edges on one line. */
.fixes_icon {
  display: block;
  width: var(--fixes-icon-w);
  height: var(--fixes-icon-h);
  object-fit: contain;
  object-position: left bottom;
}

.fixes_point-copy {
  margin-top: var(--fixes-icon-gap);
  color: var(--color-ink-copy);
  /* 20/28 at letter-spacing 0 is .text-size-large exactly. */
}

/* The opening phrase of each column is a different face, Figtree Medium
   against Trust 2A Regular, at the same size and line height. Only the
   tracking differs.

   Figtree is NOT in the repo yet, so --font-para falls back to system-ui and
   this currently renders as a slightly heavier sans beside Trust 2A rather
   than as Figtree. That is the same known gap css/00-fonts.css already logs
   for Geist Mono on button labels, and the emphasis still reads because the
   weight change survives the fallback. */
.fixes_lead {
  font-family: var(--font-para);
  font-weight: var(--weight-medium);
  letter-spacing: var(--fixes-lead-ls);
  color: var(--color-ink);
}

/* --------------------------------------------------------------------------
   THE CTA BAR
   --------------------------------------------------------------------------
   Dark bar, copy left, button right. min-height rather than height: if the
   copy wraps on a narrow desktop the bar grows instead of the text spilling
   out of it.
   -------------------------------------------------------------------------- */
.fixes_cta {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-medium);
  min-height: var(--fixes-bar-h);
  margin-top: var(--space-xxlarge);
  padding: var(--fixes-bar-pad-y) var(--fixes-bar-pad-end)
    var(--fixes-bar-pad-y) var(--fixes-bar-pad-start);
  /* No background here. It is painted by the pseudo-element below so the
     tear can roughen the bar's edge without rippling the copy and the button
     sitting on top of it. */
}

/* THE BAR IS TORN PAPER, like an accordion card and the tabs pane.
   get_design_context reports 2522:605 as a flat #3a3a37 with no effects, and
   that is simply incomplete: rendering the node and measuring it shows grain
   across the surface, a standard deviation of 3.4 in the red channel where a
   flat fill would be 0, plus 3px of bleed past the node box and a ragged
   edge. The frame data said flat; the pixels said torn. Trust the pixels.

   #sw-deckle and not the section variant: at 1280 x 112 this is a card, not
   a section edge, and it should match the cards. The 3px shadow rides along
   inside the filter. */
.fixes_cta::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--color-fixes-bar);
  filter: url(#sw-deckle-dark);
}

/* Figma's named "Heading Typo effect": two inner shadows and a grain over
   the glyphs. It is on every Heading style in the file, but on the light page
   ground it is invisible; here, light type on a dark bar, it reads. See
   #sw-letterpress in partials/svg-defs.html for the translation.

   Applied to the TEXT, not to .fixes_cta, so it grains the letters and
   leaves the bar's own texture to the filter above. */
.fixes_cta-text {
  margin: 0;
  color: var(--color-fixes-bar-ink);
  /* 32/40 at -1.5% is .heading-style-h4 exactly. */
}

/* The letterpress goes on the INK, never on the paragraph. A filter applies
   to everything inside the element it is set on, including background
   images, so filtering the paragraph also ran the rule under the marked
   phrase through it: measured, the rule came out rgb(159,89,81) where the
   frame keeps it at its source rgb(231,65,46). Wrapping the glyphs in their
   own spans keeps the effect on the type and off the artwork. */
.fixes_cta-ink {
  filter: url(#sw-letterpress);
}

/* The rule under "measurable results." is painted on the PHRASE, not drawn as
   a separate 266px bar. Figma has it as a fixed vector under the tail of the
   line, which is right at 1440 and lands under the wrong words the moment
   the copy wraps. Same technique as the TABS heading, with the exported
   brush as the background image.

   It is #e7412e, the logo red, which is neither --color-brand nor the
   showcase rule red. The asset carries the colour, so nothing here sets it. */
.fixes_cta-mark {
  background-image: url("/media/fixes-rule.svg");
  background-repeat: no-repeat;
  background-size: 100% var(--fixes-rule-h);
  background-position: 0 100%;
}

.fixes_cta-button {
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   991 AND BELOW
   --------------------------------------------------------------------------
   No mobile frame exists for this section, so everything below is reasoned
   from the desktop frame and the project's ladder, not measured.

   The bar stacks here because it cannot do otherwise: the copy alone wants
   about 625px and the button another 260, which stops fitting well before
   the columns do.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 991px) {
  .fixes_heading {
    max-width: none;
  }

  .fixes_points {
    gap: var(--space-medium);
    margin-top: var(--space-xlarge);
  }

  .fixes_cta {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-medium);
    margin-top: var(--space-xlarge);
    padding: var(--fixes-bar-pad-y) var(--fixes-bar-pad-end);
  }
}

/* --------------------------------------------------------------------------
   767 AND BELOW :: THE THREE POINTS STACK
   -------------------------------------------------------------------------- */
@media screen and (max-width: 767px) {
  .fixes_points {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-large);
    margin-top: var(--space-large);
  }

  .fixes_cta {
    margin-top: var(--space-large);
    padding: var(--space-medium);
  }
}

@media screen and (max-width: 479px) {
  .fixes_points {
    gap: var(--space-medium);
  }

  .fixes_cta {
    gap: var(--space-small);
    padding: var(--space-small);
  }
}


/* ==========================================================================
   SECTION :: TABS   (homepage, directly above FAQ)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2510-174 (layout, tab 3 active)
          node-id 2315-9417 (all seven tab states and their copy)
   No mobile frame supplied.

   Seven tab links on the left, one pane on the right. Only the pane's COPY
   changes between tabs: the card and the artwork are identical in all seven
   states, so there is one artwork in the markup and not seven.

   Behaviour is js/modules/tabs.js, driven by the ARIA tabs pattern. The CSS
   here styles states off aria-selected and never off a class the JS invents,
   so what a screen reader is told and what is painted cannot drift.
   ========================================================================== */

.tabs_section {
  position: relative;
  background-color: var(--color-tabs-bg);
}

/* --------------------------------------------------------------------------
   HEADING AND ITS RULE
   --------------------------------------------------------------------------
   Both centred. The rule is the exported hand-drawn vector, not a border: it
   is a brush line with ragged ends, and a 3px border would read as a ruled
   underline in a design where nothing else is straight.
   -------------------------------------------------------------------------- */
/* The measure lives on a WRAPPER, so the heading itself is free to be styled
   and the width that controls where it breaks is stated in one place. */
.tabs_heading-wrap {
  max-width: var(--tabs-heading-w);
  margin-inline: auto;
}

.tabs_heading {
  text-align: center;
  /* 48/56 at -2.5% comes from .heading-style-h2, an exact match for the
     frame, so no type is overridden here.

     text-wrap MUST be reset. 02-reset.css sets `text-wrap: balance` on
     headings, which equalises line lengths, and that is usually what you
     want. Here it fought the frame: line one wants to run to its natural
     767px and break after "stories.", and balance instead broke it after
     "horror" and pushed a LONGER second line, which is not what 2510:101
     draws. A max-width alone cannot fix that, because balancing happens
     inside whatever width it is given. */
  text-wrap: wrap;
}

/* --------------------------------------------------------------------------
   THE BRUSH RULE, ON THE CLOSING PHRASE ONLY
   --------------------------------------------------------------------------
   Figma draws this as a separate centred 580px vector (2510:102) sitting
   under the whole heading. That is right at 1440 and wrong everywhere else:
   as soon as the heading rewraps, a fixed centred bar sits under whatever
   words happen to land above it.

   So it is painted on the phrase instead, the same way the showcase marks
   "Not a feature list." The difference is that this rule is a hand-drawn
   brush stroke rather than a flat line, so the background is the exported
   SVG rather than a gradient.

   The numbers happen to line up almost exactly: the phrase measures 579px at
   desktop and the asset is 580 wide, so background-size: 100% renders the
   brush at very close to its native scale rather than stretching it.
   -------------------------------------------------------------------------- */
/* The phrase gets its OWN LINE down to 498px, per Pratham 2026-09-02.

   display: block is what forces the break, and it forces it regardless of
   how wide the measure happens to be, so the line always lands where the
   frame puts it rather than depending on where the text would have wrapped.

   width: fit-content matters as much as the block does. A plain block would
   stretch to the full measure and the underline, being a background, would
   stretch with it and run out past the words on both sides. Shrinking to the
   content keeps the rule exactly as long as the phrase, and the auto margins
   then centre it under the line above. */
.tabs_heading-mark {
  display: block;
  width: fit-content;
  margin-inline: auto;
  background-image: url("/media/tabs-rule.svg");
  background-repeat: no-repeat;
  background-size: 100% var(--tabs-rule-h);
  background-position: 0 100%;
}

/* Below 498 it rejoins the sentence and wraps like ordinary text. 498 is a
   one-off and NOT one of the project's six breakpoints, so it is written out
   here rather than added to the ladder: it is a single decision about one
   heading, not a tier the rest of the site steps down at. The query is
   max-width 497 because the phrase keeps its own line AT 498. */
@media screen and (max-width: 497px) {
  .tabs_heading-mark {
    display: inline;
    width: auto;
    margin-inline: 0;
  }
}

/* --------------------------------------------------------------------------
   LAYOUT
   --------------------------------------------------------------------------
   540 + 140 + 600 is exactly 1280, the container, so unlike the showcase
   there is NO slack here for a gap to absorb. Below a 1280 container
   everything has to shrink, and the only question is how.

   fr columns plus a percentage gap shrink all three together and keep the
   540:600 ratio at every width. At the full 1280 container the maths lands
   exactly on the frame: gap 10.9375% of 1280 is 140, and the remaining 1140
   splits 540:600 as 540 and 600.

   The obvious alternative, minmax(0, 540px) and minmax(0, 600px) with
   space-between, was tried and is wrong here: with no slack the grid shrinks
   both tracks EQUALLY rather than proportionally, so at a 1280 viewport both
   columns came out 536px and the pane lost its extra width over the links.
   -------------------------------------------------------------------------- */
.tabs_layout {
  display: grid;
  grid-template-columns: var(--tabs-links-fr) var(--tabs-pane-fr);
  column-gap: var(--tabs-col-gap);
  align-items: start;
  margin-top: var(--tabs-layout-gap);
}

/* --------------------------------------------------------------------------
   TAB LINKS
   -------------------------------------------------------------------------- */
.tabs_links {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

/* A real <button>, so Enter, Space and focus come free. The dot column is
   reserved on EVERY row, active or not, with grid rather than by indenting
   the active one: indenting only the selected row would shift its label
   sideways on every tab change, and the eye reads that as the list jumping.
   The label starts in the same place all the way down; only the dot appears. */
.tabs_link {
  display: grid;
  grid-template-columns: var(--tabs-link-indent) minmax(0, 1fr) auto;
  align-items: center;
  gap: 0;
  width: 100%;
  padding-block: var(--tabs-link-pad-top) var(--tabs-link-pad-bottom);
  text-align: left;
  color: var(--color-ink);
  /* The hand-drawn separator, as a background rather than a border, for the
     same reason the rule is not a border: it is a brush line. Pinned to the
     bottom edge and stretched to the row width. */
  background-image: url("/media/tabs-separator.svg");
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
}

/* First row sits flush with the top of the group, last row carries no
   separator. 0 + 32 + 18, then five rows of 66, then 16 + 32 + 0 = 428. */
.tabs_link:first-child {
  padding-top: 0;
}

.tabs_link:last-child {
  padding-bottom: 0;
  background-image: none;
}

/* Same family and size in both states. ONLY the weight changes, per Pratham
   on 2026-09-02, so selecting a tab cannot reflow the list. 24/32 at -1% is
   exactly --fs-h5, which is why those tokens are reused rather than new ones
   invented. */
.tabs_label {
  grid-column: 2;
  /* --font-display, which is Nicola Display. There is no --font-heading in
     this project and an undefined var() here would fall back to the browser
     serif without erroring. */
  font-family: var(--font-display);
  font-size: var(--fs-h5);
  font-weight: var(--weight-regular);
  line-height: var(--lh-h5);
  letter-spacing: var(--ls-h5);
}

.tabs_link[aria-selected="true"] .tabs_label,
.tabs_link[aria-expanded="true"] .tabs_label {
  font-weight: var(--weight-medium);
}

/* The dot lives in column 1 and is simply invisible until selected, so it
   occupies its space at all times. Hidden with opacity and not with display,
   because display:none would let the grid column collapse. */
.tabs_dot {
  grid-column: 1;
  width: var(--tabs-dot-size);
  height: var(--tabs-dot-size);
  border-radius: var(--radius-pill);
  background-color: var(--color-tabs-accent);
  /* The frame gives the dot an inner top highlight, the same trick the button
     keycap uses at the opposite edge. */
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.tabs_link[aria-selected="true"] .tabs_dot,
.tabs_link[aria-expanded="true"] .tabs_dot {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   PLUS AND MINUS
   --------------------------------------------------------------------------
   Two files, not one rotated. The accordion turns a plus into a cross, which
   works because a cross IS a rotated plus. Here the selected state is a
   MINUS, which is the plus with its vertical bar gone, and no rotation
   produces that.

   These are not the accordion's icon either: same notch construction, but the
   strokes are #4F4F4F rather than black and the notch is knocked out in
   #F0F0F0 to match this section's ground. Re-export 2510:105 and 2510:136 if
   they need to change.
   -------------------------------------------------------------------------- */
.tabs_icon {
  grid-column: 3;
  flex-shrink: 0;
  width: var(--tabs-icon-size);
  height: var(--tabs-icon-size);
}

.tabs_link[aria-selected="true"] .tabs_icon-plus,
.tabs_link[aria-selected="false"] .tabs_icon-minus,
.tabs_link[aria-expanded="true"] .tabs_icon-plus,
.tabs_link[aria-expanded="false"] .tabs_icon-minus {
  display: none;
}

/* --------------------------------------------------------------------------
   THE PANE
   --------------------------------------------------------------------------
   One card, one artwork, seven paragraphs of which one is shown. The card is
   NOT clipped: the artwork overhangs its top edge by 21px in the frame, and
   its own top rows are transparent so the overhang lands on the section
   ground without showing a seam.
   -------------------------------------------------------------------------- */
/* The card is torn paper, exactly like an accordion card. Same #sw-deckle
   filter, same construction: the fill goes on a PSEUDO-ELEMENT so the tear
   roughens the card edge without rippling the artwork and the copy sitting on
   top of it.

   #sw-deckle and not #sw-deckle-section. The section variant exists because a
   1440px-wide edge needed more amplitude to read; this card is 600px, much
   closer to a 391px accordion card, and the point here is that it matches
   those cards rather than the FAQ's section edge.

   The 3px shadow comes free: it is baked into the filter, torn by the same
   noise as the card, so its edge follows the ragged edge instead of drawing a
   clean rectangle behind it. That is the shadow visible in the frame. */
.tabs_pane {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  aspect-ratio: var(--tabs-pane-ratio);
  padding: var(--tabs-pane-pad);
  /* So the copy can size itself against THE CARD rather than the viewport.
     See --tabs-copy-fs: the card shrinks with the container while fixed type
     does not, which is what walked the artwork's ink into the copy below
     1440. inline-size containment only, so the artwork still overhangs the
     card's top edge: that needs paint containment to clip, which this is
     not. Asserted in the suite. */
  container-type: inline-size;
}

.tabs_pane::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--color-surface-sunken);
  filter: url(#sw-deckle);
}

/* <picture> is inline by default, which would drop the artwork into the flex
   flow instead of leaving it positioned. */
.tabs_pane picture {
  display: block;
}

.tabs_art {
  position: absolute;
  right: var(--tabs-art-right);
  top: var(--tabs-art-top);
  width: var(--tabs-art-w);
  height: auto;
  aspect-ratio: var(--tabs-art-ratio);
  object-fit: contain;
  pointer-events: none;
}

/* PER CARD ARTWORK GEOMETRY, frame 2777:6579, one open state per tab.

   Every illustration is its own size and sits at its own height, so the three
   numbers that vary are set on the PANEL and inherited by the image inside
   it. The panel is the right place for them rather than the image: below 768
   the panel is what moves into the list, and it takes the values with it.

   All values are [LOCKED], read off a 600 x 428 card:
     --tabs-art-w          the drawn width over 600
     --tabs-art-ratio      the drawn box, so height needs no second number
     --tabs-art-top        the offset from the card's top edge over 428,
                           negative where the illustration overhangs it
     --tabs-art-stacked-w  the same drawn width in rem, for accordion mode

   The horizontal anchor is shared and lives in css/01-tokens.css as
   --tabs-art-right. Do not add a `left` here: these are right-aligned.

   If a picture is ever swapped, all four numbers come from the new node. The
   crop is BAKED INTO the asset (see the note on the markup), so nothing here
   needs to know about it. */
.tabs_panel.is-art-exclusive {
  --tabs-art-w: 43.0531%;                  /* 258.3187 / 600 */
  --tabs-art-ratio: 258.3187 / 232.8242;
  --tabs-art-top: -4.8655%;                /* -20.824 / 428 */
  --tabs-art-stacked-w: 16.145rem;         /* 258.32px */
}

.tabs_panel.is-art-evidence {
  --tabs-art-w: 53.9515%;                  /* 323.7093 / 600 */
  --tabs-art-ratio: 323.7093 / 240;
  --tabs-art-top: -5.5874%;                /* -23.914 / 428 */
  --tabs-art-stacked-w: 20.232rem;         /* 323.71px */
}

.tabs_panel.is-art-ownership {
  --tabs-art-w: 47.8591%;                  /* 287.1547 / 600 */
  --tabs-art-ratio: 287.1547 / 180;
  --tabs-art-top: 2.7312%;                 /* +11.689 / 428, sits inside */
  --tabs-art-stacked-w: 17.947rem;         /* 287.15px */
}

.tabs_panel.is-art-pricing {
  --tabs-art-w: 33.0247%;                  /* 198.1481 / 600 */
  --tabs-art-ratio: 198.1481 / 250;
  --tabs-art-top: -5.6075%;                /* -24 / 428 */
  --tabs-art-stacked-w: 12.384rem;         /* 198.15px */
}

.tabs_panel.is-art-personas {
  --tabs-art-w: 36.7585%;                  /* 220.5511 / 600 */
  --tabs-art-ratio: 220.5511 / 250;
  --tabs-art-top: -8.9223%;                /* -38.188 / 428, the deepest */
  --tabs-art-stacked-w: 13.784rem;         /* 220.55px */
}

.tabs_panel.is-art-promises {
  --tabs-art-w: 37.8306%;                  /* 226.9833 / 600 */
  --tabs-art-ratio: 226.9833 / 220;
  --tabs-art-top: 9.3458%;                 /* +40 / 428, sits well inside */
  --tabs-art-stacked-w: 14.186rem;         /* 226.98px */
}

.tabs_panel.is-art-end-to-end {
  --tabs-art-w: 46.4375%;                  /* 278.625 / 600 */
  --tabs-art-ratio: 278.625 / 230;
  --tabs-art-top: -4.2056%;                /* -18 / 428 */
  --tabs-art-stacked-w: 17.414rem;         /* 278.63px */
}

/* THE PANEL is three boxes, and each one has a job. The shape is copied from
   the FAQ accordion because below 768 this animates exactly the way that one
   does, and the reasons the FAQ gives apply here unchanged:

     .tabs_panel        the box whose height animates, and the box the paper
                        card is painted on. The fill has to live here rather
                        than on the inner, because the inner clips and a
                        filtered fill inside it would have its torn edge and
                        its 3px shadow sheared straight off.
     .tabs_panel-inner  the thing that clips, with no margin of its own or
                        the collapsed state leaks pixels.
     .tabs_panel-body   the padding. On the inner it would leak too: overflow
                        clips CONTENT, not the element's own padding box, so
                        a closed panel would still stand 80px tall.

   Above 768 none of that is doing anything: the panel is a plain flex child
   of the pane, switched with the hidden attribute, and only the paper card
   the pane already draws shows. The three boxes cost nothing there.

   justify-content: flex-end on the pane pins the panel to the bottom, so the
   artwork stays put at the top and the slack sits between them. Pinning the
   copy to a fixed top offset instead would leave the short tabs, number 6 is
   one sentence, floating oddly in the middle of the card. */
.tabs_copy {
  position: relative;
  max-width: var(--tabs-copy-w);
  /* 2777:6584 draws this at 24/34, off the type ladder, so the SECTION owns
     the size and the markup no longer carries .text-size-large. It used to:
     20/28 at letter-spacing 0 was that utility exactly. Family, tracking and
     colour are the same values it was providing, so only the size changed. */
  font-family: var(--font-body);
  font-size: var(--tabs-copy-fs);
  line-height: var(--tabs-copy-lh);
  letter-spacing: var(--ls-large);
  color: var(--color-ink-copy);
}

/* Panels are switched with the hidden attribute, which the JS owns. Belt and
   braces: [hidden] is display:none in every browser, but a flex child with a
   display of its own can override it, and the panel is inside a flex column. */
.tabs_panel[hidden] {
  display: none;
}

/* --------------------------------------------------------------------------
   991 AND BELOW :: STACKED, CENTRED, LINKS IN TWO COLUMNS
   --------------------------------------------------------------------------
   Pratham 2026-09-02. No frame for any of this.

   "Centred aligned" is applied to the two BLOCKS, not to the text inside a
   row: a tab row is a dot, a label and an icon spread across a width, and
   centring that text would pull the label away from its own dot. So the
   links group and the pane are centred as boxes and the rows are untouched.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 991px) {
  .tabs_layout {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: var(--tabs-stack-gap);
  }

  .tabs_links,
  .tabs_pane {
    width: 100%;
  }

  /* The 24/34 the frame draws is a desktop measurement: 2777:6579 is a 1440
     frame and nothing below it exists for this section. From here down the
     copy goes back to the type ladder's large rung, 18/26 at this tier,
     which is exactly what shipped before and what every other body copy on
     the page steps to at the same width. */
  .tabs_copy {
    font-size: var(--fs-large);
    line-height: var(--lh-large);
  }

  /* TWO INDEPENDENT COLUMNS, via multi-column and NOT a two-column grid.
     This was a 2-column grid first and it had a real bug: a grid stretches
     every item in a row to the height of the tallest one, so a one-line
     label sitting beside a two-line label got stretched too, and its
     separator, being pinned to the bottom of the padding box, ended up about
     32px below its own text. Measured: rows 3 and 4 were both 98px when only
     row 4 needed it. On screen that reads as the short row having lost its
     line and the next row having gained a stray one.

     Multi-column lets each button keep its natural height, so every separator
     hugs the label it belongs to. It also puts the DOM order down one column
     and then the other, which happens to be what the arrow keys already do,
     so keyboard order now matches what the eye follows. */
  .tabs_links {
    display: block;
    column-count: 2;
    column-gap: var(--space-xlarge);
    /* Every row keeps its 16px top padding below, including the first, so the
       two columns start at the same text baseline. The block is then pulled
       up by that same 16px so the group as a whole still sits flush. CSS
       cannot target "first item in a column", and this sidesteps needing to. */
    margin-top: calc(-1 * var(--tabs-link-pad-top));
  }

  .tabs_link {
    /* A row must never be split across the column break. */
    break-inside: avoid;
  }

  .tabs_link:first-child {
    padding-top: var(--tabs-link-pad-top);
  }

  .tabs_link:last-child {
    padding-bottom: var(--tabs-link-pad-bottom);
    background-image: url("/media/tabs-separator.svg");
  }
}

/* --------------------------------------------------------------------------
   767 AND BELOW :: THE TABS BECOME AN ACCORDION
   --------------------------------------------------------------------------
   Pratham 2026-09-04. No frame for this, so the styling is derived from THIS
   SECTION'S own desktop design and deliberately NOT from the shared accordion
   component: no #c5c5c5 question card, no separate #e7e7e7 answer card, no
   40px indent. A row here stays what it is above 768, a dot and a label and a
   plus that becomes a minus over a brush separator, and the panel that opens
   under it is the section's own pane, same #dbdbdb paper and same #sw-deckle
   tear.

   THE PANELS ARE MOVED IN THE DOM, by js/modules/tabs.js, from the pane into
   the links list so each one follows its own trigger. CSS cannot do that: the
   panels live in the other grid column above 768 and there is no way to move a
   node between containers with style alone. The module also swaps the ARIA,
   because a stack of disclosures is not a tablist. See its banner.

   767 IS WRITTEN TWICE, here and in data-tabs-accordion-below on the group in
   index.html, because matchMedia cannot read a custom property. Change both.

   .tabs_links is display:block with column-count here, from the 991 block, so
   the moved panels land as ordinary blocks in flow order: row, panel, row,
   panel. Nothing else is needed to lay them out.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 767px) {
  .tabs_layout {
    gap: var(--tabs-stack-gap-sm);
  }

  /* Back to one column, and the negative pull-up goes with it, so the
     desktop first-and-last rules apply again exactly as they do above 992. */
  .tabs_links {
    column-count: 1;
    column-gap: 0;
    margin-top: 0;
  }

  .tabs_link:first-child {
    padding-top: 0;
  }

  /* :last-of-type, NOT :last-child. Once the panels move in, the last child of
     the list is a panel, so :last-child stops matching any row and the bottom
     row keeps a separator it should not have. Panels are <div> and rows are
     <button>, so :last-of-type still finds the bottom row. Above 768 the
     panels are elsewhere and the two selectors mean the same thing. */
  .tabs_link:last-of-type {
    padding-bottom: 0;
    background-image: none;
  }

  /* THE SEPARATOR MOVES BELOW THE OPEN CARD. Pratham 2026-09-04: a rule
     between a row and its own panel reads as cutting the two apart, when they
     are one thing. So the open row gives its separator up and the panel takes
     it, which puts the line where the group actually ends.

     The last row is the exception twice over: it never had a separator, and
     its panel must not grow one, or the list gains a bottom edge it does not
     have when closed. */
  [data-tabs-mode="accordion"] .tabs_link[aria-expanded="true"] {
    background-image: none;
  }

  [data-tabs-mode="accordion"] .tabs_link[aria-expanded="true"] + .tabs_panel {
    background-image: url("/media/tabs-separator.svg");
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 100% 1px;
  }

  [data-tabs-mode="accordion"] .tabs_link:last-of-type + .tabs_panel {
    background-image: none;
  }

  /* THE OPEN PANEL, animating exactly the way the FAQ accordion does, because
     that is the feel Pratham asked for and a hidden/shown panel cannot have
     it: display:none is not animatable, so the card used to pop.

     So below 768 the JS stops setting hidden and the row's aria-expanded
     drives the height instead, 0fr to 1fr on the grid row. The margin has to
     open WITH it: as a plain margin it would apply while closed too and space
     the closed rows apart by a gap that belongs to an open card. */
  [data-tabs-mode="accordion"] .tabs_panel {
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-rows: 0fr;
    margin-block: 0;
    transition:
      grid-template-rows var(--dur-base) var(--ease-out),
      margin-block var(--dur-base) var(--ease-out);
  }

  [data-tabs-mode="accordion"] .tabs_link[aria-expanded="true"] + .tabs_panel {
    grid-template-rows: 1fr;
    margin-block: var(--tabs-acc-gap);
  }

  /* The paper. On the animating box and NOT on the inner, so the deckle's torn
     edge and its 3px shadow are not clipped away. An absolutely positioned
     ::before is not a grid item, so it does not disturb the 0fr/1fr row. */
  [data-tabs-mode="accordion"] .tabs_panel::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-color: var(--color-surface-sunken);
    filter: url(#sw-deckle);
  }

  [data-tabs-mode="accordion"] .tabs_panel-inner {
    overflow: hidden;
  }

  [data-tabs-mode="accordion"] .tabs_panel-body {
    padding: var(--tabs-acc-pad);
  }

  /* VISUAL AND COPY TOGETHER IN THE CARD, per Pratham 2026-09-04. The artwork
     used to sit in its own card above the whole list, which was wrong for a
     section whose visuals are going to differ per tab: it would have shown one
     tab's picture over all seven. It is inside the panel now, so it changes
     with the copy.

     It also stops being absolutely positioned. Above 768 it is placed against
     the pane and deliberately overhangs its top edge; in a card that hugs its
     own content there is nothing to overhang, and absolute would take it out
     of the flow the collapse measures. */
  [data-tabs-mode="accordion"] .tabs_art {
    position: static;
    width: 100%;
    max-width: var(--tabs-art-stacked-w);
    height: auto;
    margin-inline: auto;
    margin-bottom: var(--tabs-acc-gap);
  }

  /* The pane is only the desktop card now. With every panel moved into the
     list there is nothing left in it to show. */
  [data-tabs-mode="accordion"] .tabs_pane {
    display: none;
  }

  [data-tabs-mode="accordion"] .tabs_copy {
    /* The 520px desktop measure caps a card inside a 600px column. Here the
       card is the width of the screen and the cap would leave the copy
       hanging off to one side. */
    max-width: none;
  }
}

@media screen and (max-width: 479px) {
  .tabs_layout {
    gap: var(--tabs-stack-gap-xs);
  }
}


/* ==========================================================================
   SECTION :: FAQ   (homepage, directly above SHOWCASE)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2315-8189 (desktop 1440). No mobile frame supplied.

   Heading and a decorative cluster on the left, the accordion on the right,
   and a torn strip along the bottom edge.

   THE ACCORDION IS NOT STYLED HERE. It is the shared component in
   css/components/accordion.css, dropped in as-is. Frame 2470-29 is a detail
   view of this very accordion, which is why its numbers already agree with
   this frame: 604 column, 40 indent, 20 gaps. If an accordion value looks
   wrong on this page, fix the component, not this file.

   No background either. body already paints --color-page-bg, and repainting
   it here would give one colour two sources of truth.
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE SECTION IS A TORN SHEET
   --------------------------------------------------------------------------
   Not Figma's approach, on purpose. The frame draws a flat #777876 bar across
   the bottom (2315:8266) and that bar is gone. Instead the section paints its
   own fill through the same #sw-deckle filter the accordion cards run, so its
   edges are genuinely torn, and it overlaps its neighbours so the section
   beyond shows THROUGH the rag rather than a third colour being laid on top.

   Three parts, all three required:

   1. The fill moves to a PSEUDO-ELEMENT. Running filter: url() on the section
      itself would ripple the heading, the illustration and every accordion
      card along with the background, and would also make the section a
      containing block for the absolutely positioned floaters.
   2. NEGATIVE MARGINS pull the section over its neighbours by the tear depth.
      Without the overlap the tear reveals body, which is the same colour, so
      there would be nothing to see.
   3. z-index LIFTS it above those neighbours. Later siblings paint on top by
      default, so without this the showcase would simply cover the torn edge.

   The interior gains the filter's fine grain as a side effect. That is the
   card material applied to a whole sheet, which is the point, but it is worth
   knowing it is not in the frame.
   -------------------------------------------------------------------------- */
.faq_section {
  position: relative;
  z-index: 1;
  margin-block: calc(-1 * var(--faq-tear-depth));
  /* Give the padding back, or the overlap would eat into the section's own
     vertical rhythm at both ends. */
  padding-block: var(--faq-tear-depth);
}

/* #sw-deckle-section, NOT #sw-deckle. Same filter, same four steps, but the
   displacement is scaled up: sw-deckle moves pixels about 2px, which is a
   convincing tear on a 48px card and measures as a straight line across a
   1440px edge. The section variant rags by about 9px with a much longer
   wavelength. Amplitude has to follow the element or the technique stops
   reading at this size. Both live in partials/svg-defs.html.

   Bled sideways by the tear depth so the LEFT and RIGHT edges, and the hard
   shadow that travels with them, fall outside the viewport. Only the
   horizontal edges are meant to be seen torn. */
.faq_section::before {
  content: "";
  position: absolute;
  inset: 0 calc(-1 * var(--faq-tear-depth));
  z-index: -1;
  background-color: var(--color-page-bg);
  filter: url(#sw-deckle-section);
}

/* No column gap, deliberately. Figma has no gutter between the two columns:
   the left one is 606 wide and the accordion starts immediately at 686. The
   air between the heading and the questions is the heading's own 487px cap
   inside a wider column, not a gap. Adding one here pushes the accordion
   right and closes that slack from the wrong side.

   The right track carries the accordion PLUS its 50px inset, so the badge can
   sit flush at the container edge the way the frame draws it. */
.faq_layout {
  display: grid;
  grid-template-columns:
    minmax(0, 1fr)
    minmax(0, calc(var(--faq-accordion-w) + var(--faq-accordion-inset)));
  gap: 0;
  align-items: start;
}

.faq_heading {
  max-width: var(--faq-heading-w);
  /* 40/48 at -2% and #4f4f4f all come from .heading-style-h3 plus the default
     ink, both of which already match Heading/H3 in this frame exactly.
     Nothing is overridden. */
}

/* --------------------------------------------------------------------------
   THE DECORATIVE CLUSTER
   --------------------------------------------------------------------------
   ONE flattened image: the thinker and all three question marks in a single
   export. It began as four separately positioned images matching the frame's
   four nodes, and Pratham replaced them with one flattened 2x PNG on
   2026-09-02. That deleted four absolute-position rules and three of the four
   HTTP requests, so do not split it apart again.

   The aspect-ratio comes from the export itself, 806 x 842, not from the old
   derived bounding box. Reserving the ratio here rather than letting the
   image size itself is what stops the column reflowing as it decodes, which
   matters on this page because the accordion beside it is measured.
   -------------------------------------------------------------------------- */
/* <picture> is an INLINE wrapper by default. Left inline it would put the
   block-level image inside an anonymous box and the auto margins that centre
   it below 992 would have nothing predictable to centre against. Every
   <picture> in this section is a block. */
.faq_col-intro picture,
.faq_col-accordion picture {
  display: block;
}

.faq_cluster {
  display: block;
  width: 100%;
  max-width: var(--faq-cluster-w);
  aspect-ratio: var(--faq-cluster-ratio);
  height: auto;
  margin-top: var(--faq-cluster-gap);
  object-fit: contain;
}

/* --------------------------------------------------------------------------
   RIGHT-HAND FLOATERS
   --------------------------------------------------------------------------
   A brand badge and one more question mark, both sitting OUTSIDE the
   accordion column in the frame. They are anchored to that column rather than
   to the page, so they travel with it instead of needing a page coordinate.

   Both are hidden below 1280. They overhang the container by design, and
   there is no room for that overhang once the layout starts compressing;
   keeping them would either push the page wide or land them on top of the
   accordion. Decoration is the right thing to drop first.
   -------------------------------------------------------------------------- */
.faq_col-accordion {
  position: relative;
  padding-right: var(--faq-accordion-inset);
}

.faq_float {
  position: absolute;
  top: var(--faq-float-top);
  right: var(--faq-float-right);
  width: var(--faq-float-w);
  aspect-ratio: var(--faq-float-ratio);
  object-fit: contain;
  /* The frame leans this glyph and we were drawing it upright. See
     --faq-float-rotate for where the angle comes from and how it was checked.
     Rotating about the centre leaves top/right addressing the same box, so
     the glyph turns in place rather than walking off its anchor. */
  transform: rotate(var(--faq-float-rotate));
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   THE ACTIVE-CARD INDICATOR
   --------------------------------------------------------------------------
   The brand disc that marks WHICH question is open. One per item, shown on
   the open one.

   THIS REACHES INTO accordion_ CLASSES, WHICH THE BANNER IN index.html TELLS
   YOU NOT TO DO. It is deliberate and it is Pratham's call, 2026-09-02: the
   indicator is wanted on this page and not necessarily on any other, so the
   component stays exactly as it is and the skin lives here.

   What keeps that honest is the .faq_accordion hook on the wrapper. Every
   selector below is anchored to a faq_ class, so none of it can reach an
   accordion on another page, and deleting the hook from the markup removes
   the whole feature. If a second page ever wants this, move it into the
   component rather than copying these rules.

   A pseudo-element rather than markup in all seven items: it is decoration
   that repeats, it must not reach the accessibility tree, and the state it
   reads already exists. .accordion_item::before is taken by the merged-card
   fill below 767, ::after is free.

   NOT inside .accordion_panel, which was the first idea. The panel animates
   its height and .accordion_panel-inner is overflow:hidden, so a disc hung
   over the panel's edge would be clipped for the whole animation and then
   pop. On the item it is outside everything that clips. */
.faq_accordion .accordion_item {
  position: relative;
}

.faq_accordion .accordion_item::after {
  content: "";
  position: absolute;
  /* Hangs back out of the item by the column inset, which lands its right
     edge on the container edge exactly as the frame draws it, and centred on
     the item's bottom edge. Anchoring to the item rather than to a measured
     offset is what makes it follow whichever card is open. */
  right: calc(var(--faq-accordion-inset) * -1);
  bottom: calc(var(--faq-badge-size) / -2);
  width: var(--faq-badge-size);
  height: var(--faq-badge-size);
  border-radius: var(--radius-pill);
  /* Same keycap as .button: brand fill, pill radius, the identical 3px lip.
     Not a button variant, because it is not a control and nothing about it
     is clickable. */
  background-color: var(--color-brand);
  background-image: url("/media/faq-badge-mark.svg");
  background-repeat: no-repeat;
  background-position: center calc(50% - 1px);
  background-size: var(--faq-badge-mark-w) var(--faq-badge-mark-h);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.25);
  /* Figma applies scaleY(-1) then rotate(180deg) to the mark, which composes
     to a plain horizontal flip. Flipping the whole disc rather than just the
     mark is safe and saves a second element: the circle and a shadow offset
     only on Y are both symmetric about the vertical axis, so the mark is the
     only thing this changes. */
  transform: scaleX(-1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}

/* Driven off aria-expanded, not off .is-open, so the painted state cannot
   disagree with the announced one. Same reasoning as the component's own
   open-state rule, and the same selector shape. */
.faq_accordion .accordion_item:has(.accordion_trigger[aria-expanded="true"])::after {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   1279 AND BELOW :: DROP THE OVERHANGING FLOATERS
   -------------------------------------------------------------------------- */
@media screen and (max-width: 1279px) {
  /* Only the floating question mark goes here. It overhangs the CONTAINER by
     28px and there is no room for that once the layout starts compressing.

     The indicator survives this breakpoint. It is not decoration any more, it
     tells you which card is open, and unlike the glyph it sits inside the
     container rather than past it. It goes at 767, for its own reasons: see
     the note down there. */
  .faq_float {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   991 AND BELOW :: STACK
   --------------------------------------------------------------------------
   No mobile frame exists for this section, so the order below is a judgement
   call: heading, then the cluster, then the questions. That keeps the
   desktop reading order, where the cluster belongs to the heading and both
   sit ahead of the accordion.

   The cluster is capped rather than left at its full width. At 991 it would
   otherwise run to 545px and push the questions most of a screen down, and
   the questions are what people came for.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 991px) {
  .faq_layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xlarge);
  }

  /* The 50px inset exists only to park the indicator beside the cards, and
     50px is a lot of width to hold back from a one-column layout. It goes,
     and the indicator moves ONTO the answer card's bottom-right corner
     instead, which costs no layout width at all.

     This is the 768 to 991 treatment only. Below 767 the indicator is hidden
     outright, so nothing here needs to survive that far down. */
  .faq_col-accordion {
    padding-right: 0;
  }

  .faq_accordion .accordion_item::after {
    right: 0;
  }

  /* Centred from here down, per Pratham 2026-09-02. Two separate mechanisms
     and both are needed: text-align centres the heading's LINES inside its
     box, and the auto margins centre the image's BOX inside the column. One
     without the other leaves the pair looking misaligned. */
  .faq_col-intro {
    text-align: center;
  }

  .faq_heading {
    max-width: none;
  }

  .faq_cluster {
    max-width: 24rem; /* 384px */
    margin-top: var(--space-large);
    margin-inline: auto;
  }
}

@media screen and (max-width: 767px) {
  .faq_layout {
    gap: var(--space-large);
  }

  .faq_cluster {
    max-width: 18rem; /* 288px */
    margin-top: var(--space-medium);
  }

  /* The indicator goes. Pratham 2026-09-03.

     767 is also where the component merges the question and answer into one
     card, so the corner it was stamping stops being an answer card's corner
     and becomes the whole item's. On a phone that puts a 36px disc on top of
     the copy at the exact width where the copy has least room, and the open
     card is already unmistakable there: it is the merged one, and its icon is
     a minus. The disc was reinforcing something obvious at the cost of the
     only space that matters. */
  .faq_accordion .accordion_item::after {
    display: none;
  }
}

@media screen and (max-width: 479px) {
  .faq_cluster {
    max-width: 14rem; /* 224px */
  }
}


/* ==========================================================================
   SECTION :: SHOWCASE   (homepage, directly above the footer)
   --------------------------------------------------------------------------
   OWNER: unassigned
   FIGMA: node-id 2467-2 (desktop 1440). No mobile frame supplied.

   A sticky text column on the left, a tall scrolling stack of visuals on the
   right. Below 992 the two stack vertically and the visuals become a Swiper
   slider with pagination dots.

   THE ONE RULE THAT MATTERS IN THIS FILE
   Nothing between .showcase_sticky and the scroll root may set `overflow`
   on the block axis. `position: sticky` fails SILENTLY inside an
   overflow:hidden ancestor: no error, no warning, the element just scrolls
   away like a static one. The section template this file was copied from
   (_EXAMPLE-hero.css) sets `overflow: hidden` on its section wrapper, and
   that line was deliberately NOT carried over. If a future change needs to
   clip something here, clip the inline axis only with `overflow-x` and never
   `overflow` or `overflow-y`.

   The second sticky requirement is that .showcase_col-text stays as tall as
   the grid row, which is what gives the sticky child somewhere to travel.
   That is why this grid never sets `align-items: start`.
   ========================================================================== */

.showcase_section {
  position: relative;
  background-color: var(--color-showcase-bg);
  box-shadow: var(--showcase-bg-shadow);
  /* NO overflow property here. See the banner. */
}

/* Two fixed columns pushed to the outer edges, not a fractional split. Figma
   draws 460 and 600 inside a 1240 group, leaving 180 between them; letting
   the leftover fall into the gap keeps both columns at their measured widths
   and stays fluid as the container narrows. minmax(0, x) rather than a bare
   width so the columns can still shrink before the breakpoint rather than
   overflowing. */
.showcase_layout {
  display: grid;
  grid-template-columns:
    minmax(0, var(--showcase-text-w))
    minmax(0, var(--showcase-visual-w));
  justify-content: space-between;
  gap: var(--space-xxlarge);
  /* align-items is left at `stretch` ON PURPOSE. `start` would collapse the
     text column to its own content height and kill the sticky travel. */
}

/* --------------------------------------------------------------------------
   LEFT :: THE STICKY TEXT COLUMN
   --------------------------------------------------------------------------
   `top` is published by js/modules/sticky-center.js as --sticky-center-top,
   because the centred value is (viewportHeight - elementHeight) / 2 and CSS
   cannot see the element's own height. The 0px fallback in the var() is the
   no-JS state: a normal top-pinned sticky column, which is a perfectly
   reasonable degradation rather than a broken one.
   -------------------------------------------------------------------------- */
.showcase_sticky {
  position: sticky;
  top: var(--sticky-center-top, 0px);
}

.showcase_heading {
  max-width: var(--showcase-text-w);
  color: var(--color-showcase-ink);
  /* Size, weight, line-height and tracking all come from .heading-style-h2 in
     the markup, which already measures 48/56 at -0.025em. That is an exact
     match for Heading/H2 in this frame, so nothing is overridden here. */
}

/* --------------------------------------------------------------------------
   THE RED RULE UNDER THE CLOSING PHRASE
   --------------------------------------------------------------------------
   Figma draws a fixed 340px vector at 2467:10. That width is right for
   "Not a feature list." at 1440 and wrong at every other width: the heading
   rewraps, and a bar of fixed length slides onto whatever words happen to
   end up above it. So the rule is painted on the PHRASE instead and tracks
   those four words everywhere.

   A background gradient rather than text-decoration, for two reasons: the
   thickness is exactly the 3px Figma specifies, where underline thickness is
   the font's business, and the colour is independent of the text colour.

   Because it is a background on an INLINE element, it repeats per line box,
   so the rule still lands correctly if the phrase itself ever wraps.
   -------------------------------------------------------------------------- */
.showcase_heading-mark {
  background-image: linear-gradient(
    var(--color-showcase-rule),
    var(--color-showcase-rule)
  );
  background-repeat: no-repeat;
  background-size: 100% var(--showcase-rule-h);
  background-position: 0 100%;
}

.showcase_copy {
  max-width: var(--showcase-copy-w);
  margin-top: var(--showcase-copy-gap);
  color: var(--color-showcase-ink);
  /* 20/28 at letter-spacing 0 comes from .text-size-large, which is already
     an exact match for Body/B1. */
}

.showcase_copy > * + * {
  margin-top: var(--space-medium);
}

.showcase_cta {
  margin-top: var(--showcase-cta-gap);
}

/* --------------------------------------------------------------------------
   RIGHT :: THE VISUAL COLUMN
   --------------------------------------------------------------------------
   Above 992 this is a plain vertical stack that scrolls with the page while
   the text sticks. Below 992 the very same markup is handed to Swiper and
   becomes a horizontal slider, mounted and destroyed by viewport in
   js/pages/home.js.

   The wrapper carries BOTH .showcase_visuals and Swiper's .swiper-wrapper.
   Swiper's stylesheet loads after css/main.css in the document head, so at
   equal specificity the vendor rule would win. The two-class descendant
   selector below outranks the bare .swiper-wrapper without reaching for
   !important.
   -------------------------------------------------------------------------- */
/* A little space under the last visual, nothing more. Keep it small: this was
   briefly a viewport-relative tail sized to bring the last visual to the
   centre of the screen, and while the arithmetic was right the result read as
   broken, because it left hundreds of pixels of dead space and kept the
   sticky text pinned long after the visuals had run out. */
.showcase_col-visual {
  position: relative;
  padding-bottom: var(--showcase-visual-tail);
}

.showcase_col-visual .showcase_visuals {
  display: flex;
  flex-direction: column;
  gap: var(--showcase-visual-gap);
}

.showcase_visual {
  background-color: var(--color-showcase-card);
  aspect-ratio: var(--showcase-visual-ratio);
}

/* <picture> is inline by default, so it would collapse to the image's own
   height and the object-fit: cover below would have nothing to fill. It has
   to be a block that fills the slide before the image can cover it. */
.showcase_visual picture {
  display: block;
  width: 100%;
  height: 100%;
}

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

/* No dots above 992, and this has to be CSS rather than trust in the vendor.
   Swiper's destroy(true, true) cleans up its transforms and its wrapper
   classes but NOT the pagination bullets it generated, so resizing from
   slider range back to desktop leaves three orphaned dots sitting under the
   last visual. A fresh desktop load looks fine, which is exactly what makes
   it easy to miss.

   Two classes deep on purpose: Swiper stamps its own swiper-pagination-*
   classes on this element, and its stylesheet loads after css/main.css, so a
   single-class selector here would lose on the cascade. */
.showcase_col-visual .showcase_pagination {
  display: none;
}

/* The stack dissolves into the section at both ends instead of being cut off.
   Pseudo-elements rather than markup, and pointer-events:none so they never
   swallow a click on a visual.

   ANGLE NOTE, worth reading before "fixing" this. Figma reports 2467:27 and
   2467:28 as a HORIZONTAL gradient (to-left). On a 724x100 strip lying across
   the top and bottom of a vertically scrolling column that would be visually
   meaningless, so it is implemented VERTICAL here. Flag raised with the
   designer; if the horizontal reading turns out to be intended, this is the
   only place to change. */
.showcase_col-visual::before,
.showcase_col-visual::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: var(--showcase-fade-h);
  pointer-events: none;
  z-index: 1;
}

.showcase_col-visual::before {
  top: 0;
  background: linear-gradient(
    to bottom,
    var(--color-showcase-bg),
    rgba(61, 61, 58, 0)
  );
}

/* Offset by the tail so the fade keeps hugging the last visual rather than
   sitting below it in the padding. */
.showcase_col-visual::after {
  bottom: var(--showcase-visual-tail);
  background: linear-gradient(
    to top,
    var(--color-showcase-bg),
    rgba(61, 61, 58, 0)
  );
}

/* --------------------------------------------------------------------------
   991 AND BELOW :: STACKED, AND THE VISUALS BECOME A SLIDER
   --------------------------------------------------------------------------
   No mobile frame exists for this section, so the rules below follow the
   brief rather than a measurement. They are the reasoned step down, not
   [LOCKED] values.
   -------------------------------------------------------------------------- */
@media screen and (max-width: 991px) {
  .showcase_layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--showcase-stack-gap);
  }

  /* The tail is breathing room for a scrolling column. A slider does not
     scroll, so it would just be a dead gap above the footer. */
  .showcase_col-visual {
    padding-bottom: 0;
  }

  /* Sticky off. The module already stops publishing --sticky-center-top
     below 992, but position must be reset too or the column would pin at the
     0px fallback. Both halves are needed. */
  .showcase_sticky {
    position: static;
  }

  .showcase_heading,
  .showcase_copy {
    max-width: none;
  }

  /* Hand the row back to Swiper: it lays slides out horizontally and drives
     the transform itself. gap must go to 0, because Swiper spaces slides
     with its own spaceBetween rather than a flex gap, and a surviving 24px
     gap would desynchronise every slide position from its transform. */
  .showcase_col-visual .showcase_visuals {
    flex-direction: row;
    gap: 0;
  }

  /* No fades over a slider. There is no scroll to dissolve, and they would
     sit on top of the first and last slide. */
  .showcase_col-visual::before,
  .showcase_col-visual::after {
    content: none;
  }

  /* Dots back on, at the same specificity as the desktop hide above. */
  .showcase_col-visual .showcase_pagination {
    display: flex;
    margin-top: var(--space-small);
  }
}

/* The stack gap tightens twice more on the way down. Three values, three
   tiers, all three from the brief rather than a frame. */
@media screen and (max-width: 767px) {
  .showcase_layout {
    gap: var(--showcase-stack-gap-sm);
  }
}

@media screen and (max-width: 479px) {
  .showcase_layout {
    gap: var(--showcase-stack-gap-xs);
  }
}
