/* =====================================================================
   SRVE Marketing site — layout & page-section styles.
   Built entirely on the design-system tokens (tokens.css) and reuses the
   component classes from Storybook (.srve-button, .srve-card, .srve-badge,
   .srve-chip, .srve-divider) plus the typography utilities.
   This file only adds marketing-page composition (nav, hero, sections,
   grids, plan cards, footer) — no new visual language.
   ===================================================================== */

/* Display face for hero headlines — marketing site only, so it lives here
   rather than in the design-system font sheet. Single weight (heavy,
   semi-condensed); nothing else on the site uses it. */
@font-face {
  font-family: 'Neue Reman Sans';
  src: url('assets/fonts/neue-reman-sans-heavy-semicond.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --site-max: 1200px;
  --site-gutter: var(--srve-space-4); /* 16px — outer page edge padding */
  /* Height of the fixed top bar. The mega panels and the mobile menu sheet
     open flush beneath it, so they read this rather than repeating it. */
  --site-header-h: 4rem; /* 64px */
  /* The platform page's section rail: a pill of a fixed height, floating a gap
     below the header. Read by the rail itself and by the scroll-margin that
     keeps an anchored section's heading out from under the two of them. */
  --subnav-pill-h: 2.5rem; /* 40px */
  --subnav-gap: var(--srve-space-4); /* 16px between the header and the pill */
  --section-pad: var(--srve-space-9); /* 96px */

  /* Marketing-only theme surfaces. The design-system tokens cover anything a
     component paints; these four are compositions this file invents (frosted
     bar, photo scrim, glass bezel, panel edge) and so have no token to
     inherit. Dark values are in the block at the foot of the file. */
  --site-header-glass: var(--srve-color-bg-page);
  --hero-film: rgb(11 31 22 / 0.32);

  /* Single ink colour every line-art illustration is drawn in. */
  --srve-illustration-ink: #f3f4f6;

  /* Olive — the closing CTA band's own fill, so it reads as a different beat
     from the brand green rather than a second helping of it. Not a design-
     system token: nothing else on the site paints with it. */
  --site-olive: #556b2f;
  --site-olive-deep: #3f4a26; /* type colour for white buttons on the olive */
}

/* Anchor jumps travel rather than cut. The platform page's section rail is the
   reason — six links into one long page, where a cut leaves you unsure whether
   you moved or the page changed — but it belongs on the root rather than that
   one page: every in-page anchor on the site is the same kind of move.
   Only the root scroller: the carousels set their own behaviour, and the rail's
   own sideways scroll passes it explicitly. */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  background: var(--srve-color-bg-page);
}

.site-container {
  width: 100%;
  max-width: var(--site-max);
  margin-inline: auto;
  padding-inline: var(--site-gutter);
}

/* ---- Header / nav ---------------------------------------------------- */
.site-header {
  /* Floats over the page — takes no layout space, so the hero
     extends to the very top of the viewport behind it. */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  background: none;
}
/* Solid page-coloured bar behind the logo, nav, and waitlist button — the
   scrolled state, matching the One platform band. Lives on ::before because a
   filter or backdrop-filter on the header itself would make it the containing
   block for the fixed dropdowns and the mobile menu sheet. No bottom border. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--site-header-glass);
  transition: opacity var(--srve-duration-base) var(--srve-ease-standard);
}
/* Over the hero photo the bar drops out entirely and the chrome reverses to
   white; site.js adds .site-header--over-hero while the hero is still in view
   and removes it once the page scrolls past. Pages with no hero never get the
   class, so they keep the page-coloured bar from the first pixel — and with it
   the theme's own chrome colours, which need no reversing. */
.site-header--over-hero::before { opacity: 0; }
.site-header--over-hero .site-logo { color: #ffffff; }
.site-header--over-hero .site-logo__img { filter: brightness(0) invert(1); }
.site-header--over-hero .nav-toggle__bar { background: #ffffff; }
/* Nav links only reverse on desktop, where they sit on the bar itself. Below
   1041px they live in the menu sheet — dark on white — so reversing them
   would be wrong.

   Both the desktop mega panel (position: fixed; top: var(--site-header-h)) and the mobile
   sheet (inset: var(--site-header-h) 0 0 0) open *beneath* the bar, never behind it. So an
   open dropdown changes nothing up here: the bar stays transparent and the
   links stay reversed. The --open/:focus-within selectors below re-assert
   white over the base `.nav-drop--open .nav-drop__trigger` active colour,
   which is dark and would otherwise win on source order. */
@media (min-width: 1041px) {
  .site-header--over-hero .site-nav a,
  .site-header--over-hero .site-nav a:hover,
  .site-header--over-hero .nav-drop__trigger,
  .site-header--over-hero .nav-drop--open .nav-drop__trigger,
  .site-header--over-hero .nav-drop:focus-within .nav-drop__trigger {
    color: #ffffff;
  }
}

/* An open dropdown pulls its opaque panel down beneath the bar, so the bar
   itself goes solid with matching chrome — even while it's floating over the
   hero (where it would otherwise be transparent with white text). It takes the
   *panel's* surface colour, not the page's, so bar and panel read as one
   uninterrupted sheet. In light mode both are white and nothing changes; in
   dark the panel sits a step above the page and the bar comes with it. */
.site-header:has(.nav-drop--open)::before {
  opacity: 1;
  background: var(--srve-color-bg-surface);
  /* No bottom hairline while open — it would draw a seam across the middle of
     that sheet. The panel's own bottom edge closes the shape instead. */
  border-bottom-color: transparent;
}
.site-header:has(.nav-drop--open) .site-logo { color: var(--srve-color-text-primary); }
.site-header:has(.nav-drop--open) .site-logo__img { filter: none; }
.site-header:has(.nav-drop--open) .nav-toggle__bar { background: var(--srve-color-text-primary); }
.site-header:has(.nav-drop--open) .theme-toggle { color: var(--srve-color-text-secondary); }
/* The bar going solid under an open panel is the one case that used to drop the
   links back to the secondary grey — the state they no longer have. They stay
   on the primary ink here as everywhere else; all this rule has left to do is
   undo the white the over-hero block reverses them to, now that the bar under
   them is opaque. It outranks that block on specificity — :has() carries its
   argument's — so it holds wherever the two meet. */
@media (min-width: 1041px) {
  .site-header:has(.nav-drop--open) .site-nav a,
  .site-header:has(.nav-drop--open) .nav-drop__trigger {
    color: var(--srve-color-text-primary);
  }
}
.site-header__inner {
  position: relative; /* anchors the centered nav pill */
  display: flex;
  align-items: center;
  gap: var(--srve-space-6);
  height: var(--site-header-h);
}
.site-logo {
  display: inline-flex;
  align-items: center;
  gap: var(--srve-space-2);
  font-family: var(--srve-font-family-ui);
  font-weight: var(--srve-font-weight-bold);
  font-size: var(--srve-font-size-heading-sm);
  letter-spacing: -0.02em;
  color: var(--srve-color-text-primary);
  text-decoration: none;
}
.site-logo__img {
  height: 26px;
  width: auto;
  display: block;
}
.site-nav {
  /* True-centered glass pill. The glass lives on ::before so the
     backdrop-filter never becomes a containing block for the
     fixed-position dropdown panels (cross-engine safe). */
  position: absolute;
  inset: 0;
  margin: auto;              /* centers without a transform, so the pill
                                never becomes the containing block for
                                the fixed-position dropdown panels */
  width: fit-content;
  height: fit-content;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: var(--srve-space-5);
  padding: 10px var(--srve-space-5);
}
/* (The nav's old glass pill was retired when the header became a full
   frosted bar — links now sit directly on the bar.) */
/* 600 and full-strength ink. On the bar the links are the page's own
   navigation, not secondary copy: at 500 in the secondary grey they sat back
   from the logo and the CTA either side of them and read as caption weight.
   The one colour holds wherever the bar is solid — white in the dark theme,
   near-black blue in the light one — so the only state that recolours them is
   the transparent-over-hero one further up, which reverses everything.

   Direct children only. Every card in the mega panel is an <a> inside .site-nav
   too, and the dim below would take a whole panel card with it. */
.site-nav > a {
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm);
  font-weight: var(--srve-font-weight-semibold);
  color: var(--srve-color-text-primary);
  text-decoration: none;
  white-space: nowrap;
  transition: opacity var(--srve-duration-fast) var(--srve-ease-standard);
}
/* Hover dims rather than recolours: there is no step up from the primary ink to
   move to, and a second colour here is the thing being taken out. */
.site-nav > a:hover {
  opacity: 0.7;
}
.site-header__actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--srve-space-4);
}

/* ---- Platform mega-dropdown ------------------------------------------ */
.nav-drop {
  position: relative;
  display: flex;
  align-items: center;
}
.nav-drop__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--srve-space-1);
  border: none;
  background: none;
  padding: 0;
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm);
  /* Matches .site-nav a — the trigger is a nav link that happens to be a
     button, and the two sit side by side on the bar. */
  font-weight: var(--srve-font-weight-semibold);
  color: var(--srve-color-text-primary);
  cursor: pointer;
  white-space: nowrap;
  transition: opacity var(--srve-duration-fast) var(--srve-ease-standard);
}
.nav-drop__trigger:hover { opacity: 0.7; }
/* Open and focused are already the full ink, so what marks them is the dim
   coming off rather than a colour arriving. */
.nav-drop--open .nav-drop__trigger,
.nav-drop:focus-within .nav-drop__trigger {
  color: var(--srve-color-text-primary);
  opacity: 1;
}
.nav-drop__caret {
  width: 12px;
  height: 12px;
  transition: transform var(--srve-duration-base) var(--srve-ease-standard);
}
.nav-drop--open .nav-drop__caret {
  transform: rotate(180deg);
}

.mega {
  /* Fixed + viewport-centered at every width: the header is sticky at the
     top, so top: var(--site-header-h) is always flush beneath it, and a definite width can
     never overflow the viewport. */
  position: fixed;
  top: var(--site-header-h);
  left: 50%;
  transform: translateX(-50%) translateY(8px);
  /* Span the site container: same width as the header content
     (--site-max minus the container gutters), clamped to the viewport. */
  width: min(calc(var(--site-max) - var(--site-gutter) * 2), calc(100vw - 2 * var(--site-gutter)));
  background: var(--srve-color-bg-surface);
  border-radius: 0;
  border-bottom: 1px solid var(--srve-color-border);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--srve-duration-base) var(--srve-ease-standard),
    transform var(--srve-duration-base) var(--srve-ease-standard),
    visibility 0s linear var(--srve-duration-base);
}
.nav-drop--open .mega {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition:
    opacity var(--srve-duration-base) var(--srve-ease-standard),
    transform var(--srve-duration-base) var(--srve-ease-standard);
}

/* Card-grid dropdown variant — a grid of bordered cards (icon + title +
   description) that replaces the old three-column Platform mega menu. The
   panel is full-bleed; its inner grid is aligned to the page container. */
.mega--grid {
  /* Full-bleed panel: pinned to the viewport edges and flush under the
     sticky header. The base .mega already sets position:fixed + top: var(--site-header-h). */
  left: 0;
  right: 0;
  width: 100vw;
  transform: none;
  /* No drop-shadow: on a full-bleed panel the base .mega shadow only shows
     as a gray hairline along the bottom edge. */
  box-shadow: none;
  /* No bottom edge: the panel ends against the page on its own. */
  border-bottom: none;
  /* Reveal by expanding downward from the top edge (clip-path) rather than
     fading. The panel itself stays opaque throughout the expand. */
  opacity: 1;
  clip-path: inset(0 0 100% 0);
  transition:
    clip-path var(--srve-duration-base) var(--srve-ease-standard),
    visibility 0s linear var(--srve-duration-base);
}
.nav-drop--open .mega--grid {
  transform: none;
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--srve-duration-base) var(--srve-ease-standard);
}
.mega--grid .mega__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--srve-space-3);
  max-width: var(--site-max);
  margin-inline: auto;
  /* a touch more breathing room along the bottom edge */
  padding: var(--srve-space-5) var(--site-gutter) var(--srve-space-6);
  /* Options fade in only after the panel has finished expanding. */
  opacity: 0;
  transition: opacity var(--srve-duration-fast) var(--srve-ease-standard);
}
.nav-drop--open .mega--grid .mega__grid {
  opacity: 1;
  transition: opacity var(--srve-duration-fast) var(--srve-ease-standard) var(--srve-duration-base);
}
@media (prefers-reduced-motion: reduce) {
  .mega--grid,
  .nav-drop--open .mega--grid,
  .mega--grid .mega__grid,
  .nav-drop--open .mega--grid .mega__grid {
    transition: none;
  }
}
/* Switching straight from one open menu to another: no expand/fade — the
   panels swap instantly. JS adds .nav-switching to <body> for that click. */
body.nav-switching .mega--grid,
body.nav-switching .mega--grid .mega__grid {
  transition: none;
}
/* The panel always paints its own surface, so card text stays at the theme's
   own contrast even when the header is in its reversed over-hero state
   (which otherwise forces nav links white). */
.site-header--over-hero .mega--grid .mega-card,
.site-header--over-hero .mega--grid .mega-card__title {
  color: var(--srve-color-text-primary);
}
.mega-card {
  position: relative; /* anchors the arrow */
  display: flex;
  flex-direction: column;
  gap: 2px; /* title and description read as one block, not two lines */
  /* Extra room on the right so the longest description can't run under the
     arrow sitting on that margin. */
  padding: var(--srve-space-4) var(--srve-space-7) var(--srve-space-4) var(--srve-space-5);
  border: 1px solid var(--srve-color-border);
  border-radius: 4px;
  text-decoration: none;
  white-space: normal;
  transition:
    background-color var(--srve-duration-fast) var(--srve-ease-standard),
    border-color var(--srve-duration-fast) var(--srve-ease-standard);
}
.mega-card:hover,
.mega-card:focus-visible {
  background: var(--srve-color-bg-subtle);
  border-color: var(--srve-color-text-primary);
}
.mega-card__head {
  display: flex;
  align-items: center;
  gap: var(--srve-space-3);
}
/* (.mega-card__icon is gone — the cards are title + description now.) */
/* Arrow parked on the card's right margin, centred against the two lines of
   copy. It nudges over on hover, so the card has a direction as well as a
   fill change. */
.mega-card__arrow {
  position: absolute;
  right: var(--srve-space-5);
  top: 50%;
  translate: 0 -50%;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--srve-color-text-secondary);
  transition:
    translate var(--srve-duration-fast) var(--srve-ease-standard),
    color var(--srve-duration-fast) var(--srve-ease-standard);
}
.mega-card:hover .mega-card__arrow,
.mega-card:focus-visible .mega-card__arrow {
  translate: 4px -50%;
  color: var(--srve-color-text-primary);
}
@media (prefers-reduced-motion: reduce) {
  .mega-card__arrow { transition: color var(--srve-duration-fast) var(--srve-ease-standard); }
  .mega-card:hover .mega-card__arrow,
  .mega-card:focus-visible .mega-card__arrow { translate: 0 -50%; }
}
.mega-card__title {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-md); /* 16px */
  /* bold, not semibold — the display face only ships the heavy weight. */
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
  line-height: var(--srve-line-height-snug);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.mega-card__desc {
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm); /* 14px */
  font-weight: var(--srve-font-weight-medium);
  color: var(--srve-color-text-secondary);
  line-height: var(--srve-line-height-snug);
}

/* ---- Section rail (platform page) -------------------------------------
   A second row of anchors under the primary nav, holding the six capability
   sections. It stands in for the jump grid near the top of the page once that
   grid has scrolled away — same six destinations, kept within reach for the
   long run of sections below it.

   A glass pill floating clear of the header at every width, not a bar ruled
   across the window: the rail is a small set of controls, and a full-bleed band
   with a hairline under it reads as a second piece of site chrome. What changes
   with the viewport is only how wide the pill is — shrink-wrapped to the six
   labels where they fit, inset to the page gutters where they don't and the row
   has to scroll inside it.

   Fixed rather than sticky: sticky would confine it to one parent's box, and
   this has to hold its place across six of them. It sits under the header's
   z-index 50, so the mobile menu sheet — a child of the header — covers it
   when open rather than being pierced by it.

   Hidden state is opacity plus visibility, not display: the transition needs
   something to interpolate, and `visibility` is what takes the links out of
   the tab order while they're off. */
.subnav {
  position: fixed;
  /* Clear of the bar above rather than hung off it — a floating pill wants air
     between itself and the chrome it sits under. */
  top: calc(var(--site-header-h) + var(--subnav-gap));
  left: 0;
  right: 0;
  z-index: 40;
  height: var(--subnav-pill-h);
  /* The band the pill floats in paints nothing and takes no clicks: a fixed box
     spanning the window would otherwise swallow every pointer event in a strip
     across the page. The pill itself takes them back. */
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  /* Drops in from behind the header rather than fading in place. */
  translate: 0 -8px;
  transition:
    opacity var(--srve-duration-base) var(--srve-ease-standard),
    translate var(--srve-duration-base) var(--srve-ease-standard),
    visibility var(--srve-duration-base);
}
.subnav.is-visible {
  opacity: 1;
  visibility: visible;
  translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
  .subnav { transition: none; translate: none; }
}
/* The pill. One row, scrolled sideways when it doesn't fit — the six labels run
   past a phone's width, so on a phone the pill is a fixed shape with the row
   moving inside it.
   Inset to the page gutters by default, which is what makes it read as floating
   on the narrow widths rather than as a bar with its ends rounded off. */
.subnav__inner {
  display: flex;
  align-items: center;
  gap: var(--srve-space-2);
  pointer-events: auto;
  width: auto;
  height: 100%;
  margin-inline: var(--site-gutter);
  /* 6px, not the page gutter .site-container brings: this is a band inside a
     pill, not a page margin. */
  padding-inline: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  border-radius: var(--srve-radius-full);
  /* Glass: a wash of the page colour thin enough to read what passes under it,
     with the blur doing the rest. The page token rather than a fixed white, so
     the pill reads as the page's own surface in either theme — a white-tinted
     glass would be invisible on the light one. */
  background: color-mix(in srgb, var(--srve-color-bg-page) 68%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border: 1px solid var(--srve-color-border);
  box-shadow: var(--srve-shadow-lg);
  /* Containing block for the moving highlight below. It's the scroll container
     too, which is what keeps the highlight travelling with the labels when the
     row is scrolled sideways rather than sliding off them. */
  position: relative;
}
.subnav__inner::-webkit-scrollbar { display: none; }
/* Wide enough for the whole set: the pill shrink-wraps the labels and centres
   on the page instead of stretching to the gutters, where it would be mostly
   empty glass with six words adrift in the middle.
   `safe` matters on a scroll container: plain `center` overflows equally at both
   ends, and the overflow past the *start* edge can't be scrolled back to. If a
   seventh label ever pushed the row past the pill, this falls back to flex-start
   instead of hiding the first one. */
@media (min-width: 1041px) {
  .subnav__inner {
    width: fit-content;
    max-width: calc(100% - 2 * var(--site-gutter));
    margin-inline: auto;
    justify-content: safe center;
  }
}
/* Narrow: the pill moves to the foot of the screen. Under the header it was the
   second bar down in a 375px-wide column, taking a strip off the top of every
   section as you read it; at the bottom it sits where a thumb already is and
   leaves the page's own top edge alone. Nothing about the rail changes but where
   it's pinned — same pill, same glass, same travelling highlight.

   Wider margins to go with it: floating over the content rather than filling a
   band under the chrome, it wants to read as an object on the page, and the page
   gutter left it looking like a bar that had merely been rounded off. */
@media (max-width: 1040px) {
  .subnav {
    top: auto;
    /* env() keeps it off the home indicator on the phones that have one, and
       falls back to 0 on the ones that don't. */
    bottom: calc(var(--srve-space-5) + env(safe-area-inset-bottom, 0px));
    /* Rises into place from below, since that's the edge it now belongs to. */
    translate: 0 8px;
  }
  .subnav__inner { margin-inline: var(--srve-space-6); }
}
.subnav__link {
  /* Each label keeps its own width and the rail scrolls. Without this the six
     of them divide the rail instead of overrunning it. */
  flex: 0 0 auto;
  padding: 6px var(--srve-space-3);
  border-radius: var(--srve-radius-full);
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm);
  font-weight: var(--srve-font-weight-medium);
  color: var(--srve-color-text-secondary);
  text-decoration: none;
  white-space: nowrap;
  transition:
    color var(--srve-duration-fast) var(--srve-ease-standard),
    background-color var(--srve-duration-fast) var(--srve-ease-standard);
}
.subnav__link:hover { color: var(--srve-color-text-primary); }
/* The section currently under the bar. A chip rather than a rule underneath:
   the rail already has a hairline along its lower edge, and a second line
   inside it reads as a second border.

   Reversed out: the ink fills the chip and the label takes the page colour, the
   way the tour strip marks its active tab. A tint the width of one word barely
   registered against the glass behind it — an inversion is the one thing that
   cannot be mistaken for the surface it sits on, in either theme.

   The fill itself is not here. It's one element that travels between the labels
   (below), so the chip slides from section to section instead of going out in
   one place and coming back in another; all the link carries is the reversed
   ink, which crossfades under the fill as it arrives. */
.subnav__link[aria-current="true"] {
  color: var(--srve-color-bg-page);
  font-weight: var(--srve-font-weight-semibold);
}
/* The travelling fill. Sized and placed off the active link by site.js, which
   publishes its box as three custom properties and lets the transition carry it
   there — animating width and a translate, not `left`, so the move runs on the
   compositor rather than relaying out the rail six times a second.

   Behind the labels, and inert: it's decoration for a state the aria-current
   attribute already carries. */
.subnav__marker {
  position: absolute;
  top: 50%;
  left: 0;
  z-index: 0;
  width: var(--marker-w, 0);
  height: var(--marker-h, 0);
  translate: var(--marker-x, 0) -50%;
  border-radius: var(--srve-radius-full);
  background: var(--srve-color-text-primary);
  pointer-events: none;
  /* Nothing to point at until a section is under the bar. */
  opacity: 0;
  transition:
    translate var(--srve-duration-base) var(--srve-ease-standard),
    width var(--srve-duration-base) var(--srve-ease-standard),
    opacity var(--srve-duration-fast) var(--srve-ease-standard);
}
.subnav__inner.has-marker .subnav__marker { opacity: 1; }
/* Over the fill. Only the marked one needs it, but a stacking order that
   depends on which link is marked would be a second thing to keep in step. */
.subnav__link { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) {
  .subnav__marker { transition: opacity var(--srve-duration-fast) linear; }
}
.subnav__link:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}

/* Buttons rendered as links — reuse .srve-button, keep link semantics. */
a.srve-button {
  text-decoration: none;
}

/* Site-wide stroke removal — overrides for reused design-system
   components (Storybook keeps the original bordered styles). */
.srve-button--secondary {
  border-color: transparent;
  background: var(--srve-color-neutral-100); /* #f3f4f6 fill instead of stroke */
}
.srve-button--secondary:hover {
  background: var(--srve-color-neutral-200);
}

/* ---- Generic section ------------------------------------------------- */
.section {
  padding-block: var(--section-pad);
}
/* Anchored sections (the Platform page's capability jump links) must clear the
   fixed header when scrolled to, or their heading lands underneath the bar. */
.section[id] {
  scroll-margin-top: calc(var(--site-header-h) + var(--srve-space-5));
}
/* On the platform page there are two bars to clear, not one: jumping to a
   section from the rail is the moment the rail is guaranteed to be showing, so
   its height belongs in the offset. */
/* …and on the platform page there are two to clear rather than one, wherever the
   section rail is overhead. Only at the width where it is: below 1041px the rail
   is pinned to the foot of the screen and the header is again the only thing an
   anchored section has to land under. */
@media (min-width: 1041px) {
  .page-platform .section[id] {
    scroll-margin-top: calc(
      var(--site-header-h) + var(--subnav-gap) + var(--subnav-pill-h) + var(--srve-space-4)
    );
  }
}
.section--tint {
  background: var(--srve-color-bg-subtle);
  /* Cards in here flip to the other surface, or they'd be the same colour as
     the band they sit on now that the hairline is gone. */
  --card-fill: var(--srve-color-bg-surface);
}
.section__head {
  max-width: 720px;
  margin-bottom: var(--srve-space-7);
}
.section__head--center {
  margin-inline: auto;
  text-align: center;
}
.section__title {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  /* 32px → 48px with the viewport. One size for every section headline on
     the site; the floor keeps it from crowding on phones. */
  font-size: clamp(
    var(--srve-font-size-heading-lg),
    5vw,
    var(--srve-font-size-display-lg)
  );
  font-weight: var(--srve-font-weight-bold);
  /* Same leading as .hero__title — all-caps has no descenders, so the 1.2
     tight token leaves a visible gap between lines. */
  line-height: 0.95;
  /* Looser than the -0.02em the UI font wants — see .hero__title. */
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--srve-color-text-primary);
}
.section__intro {
  margin-top: var(--srve-space-4);
  font-size: var(--srve-font-size-body-lg);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}

/* ---- "As seen in" logo strip ---------------------------------------- */
.logo-strip__label {
  text-align: center;
  margin-bottom: var(--srve-space-6);
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-size: var(--srve-font-size-caption); /* 12px */
  font-weight: var(--srve-font-weight-bold);
  line-height: var(--srve-line-height-snug);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--srve-color-text-secondary);
}
.logo-strip__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--srve-space-6) var(--srve-space-8);
}
.logo-strip__item {
  /* uniform optical box; the logo is contained + centered within it */
  display: flex;
  align-items: center;
  justify-content: center;
}
.logo-strip__logo {
  width: auto;
  max-width: 150px;
  /* muted, monochrome press-bar treatment; brightens on hover */
  opacity: 0.55;
  filter: grayscale(1);
  transition: opacity 0.2s ease, filter 0.2s ease;
}
.logo-strip__item:hover .logo-strip__logo {
  opacity: 1;
  filter: grayscale(0);
}
/* Per-logo heights tuned so wordmarks and the square Fox emblem read at
   the same optical weight despite their different aspect ratios. */
.logo-strip__logo--abc { height: 34px; }
.logo-strip__logo--bi { height: 26px; }
.logo-strip__logo--fox { height: 42px; }
.logo-strip__logo--spectrum { height: 30px; }
.logo-strip__logo--techstars { height: 22px; }

/* Reversed variant for the dark home hero: left-aligned to the copy, and the
   black artwork is inverted (not flattened to white) so the knocked-out
   letterforms inside the ABC roundel survive. */
.logo-strip--reversed {
  margin-top: var(--srve-space-7);
}
.logo-strip--reversed .logo-strip__label {
  text-align: left;
  margin-bottom: var(--srve-space-4);
  color: rgb(255 255 255 / 0.75);
}
/* One row at every width. The marks are allowed to shrink instead of
   wrapping: object-fit keeps each one's aspect ratio while the flex item
   narrows, so a tight viewport scales the row down rather than dropping the
   last logo onto a second line. */
.logo-strip--reversed .logo-strip__list {
  justify-content: flex-start;
  flex-wrap: nowrap;
  gap: var(--srve-space-7); /* 48px — the marks read as one clump any tighter */
}
.logo-strip--reversed .logo-strip__item {
  min-width: 0;
}
.logo-strip--reversed .logo-strip__logo {
  max-width: 100%;
  object-fit: contain;
  object-position: left center;
}
.logo-strip--reversed .logo-strip__logo {
  filter: invert(1);
  opacity: 0.8;
}
.logo-strip--reversed .logo-strip__item:hover .logo-strip__logo {
  filter: invert(1);
  opacity: 1;
}

@media (max-width: 640px) {
  /* Phone CTAs run the full width of whatever holds them — a 140px pill in a
     360px column reads as an afterthought, and the wide tap target is easier
     to hit one-handed. Scoped to <main> so the header's own controls (theme
     toggle, hamburger) keep their size. */
  main .srve-button { width: 100%; }
  /* The CTA pairs are flex rows; stack them so each one can take the width
     rather than splitting it. */
  .hero__cta,
  .cta-band__row {
    flex-direction: column;
    align-items: stretch;
  }
  .logo-strip__list { gap: var(--srve-space-5) var(--srve-space-6); }
  .logo-strip__logo--abc { height: 28px; }
  .logo-strip__logo--bi { height: 22px; }
  .logo-strip__logo--fox { height: 34px; }
  .logo-strip__logo--spectrum { height: 25px; }
  .logo-strip__logo--techstars { height: 18px; }
  /* Tighter gaps so the single row has as much width as possible to work
     with before the marks start scaling themselves down. */
  .logo-strip--reversed .logo-strip__list { gap: var(--srve-space-4); }
}

/* ---- Hero ------------------------------------------------------------ */
.hero {
  /* Extra breathing room above and below the hero on supporting pages
     (home uses .hero-lead and is unaffected). */
  padding-block: calc(var(--srve-space-9) + var(--srve-space-6)) var(--srve-space-9);
  background:
    radial-gradient(
      1200px 480px at 50% -10%,
      var(--srve-color-brand-primary-tint),
      transparent 70%
    );
}
/* Video-backed variant of the same hero. The footage sits behind the copy with
   a film over it, and the copy reverses to white — in BOTH themes. A dark
   video doesn't get lighter because the visitor is in light mode, so the
   section stops following the theme here and commits to the reversed
   treatment; only the surfaces around it keep flipping. */
.hero--video {
  position: relative;
  overflow: hidden;
  /* Flat dark ground under the video — what shows while it loads, if it fails,
     or when reduced motion drops it. Same value in both themes, so there's no
     white flash before the first frame paints for a light-mode visitor. */
  background: #0b0d0c;
  /* Its own film, and it still moves with the theme — heavier in dark so the
     hero settles into the page around it, lighter in light so the footage
     stays open. Both values are dark: the copy over them is white either way,
     so the question is only how much of the frame shows through. */
  --hero-film: rgb(6 16 12 / 0.38);
  /* The shot below the copy runs to the section's bottom edge — see the
     .hero--video .hero-shot rules further down. */
  padding-bottom: 0;
}
.hero__video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  pointer-events: none;
}
:root[data-theme="dark"] .hero--video { --hero-film: rgb(6 16 12 / 0.62); }
.hero--video::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--hero-film);
  pointer-events: none;
}
/* Copy and the shot below it ride above the film. */
.hero--video > .site-container { position: relative; z-index: 2; }

/* Short dither along the bottom edge of both video heroes — the home panel and
   this one. Enough to break the hard line where footage meets the page below
   it, not so much that it reads as a treatment over the video. Same
   checkerboard as the image placeholders, faded in on a smooth ramp so it
   grades rather than banding.

   The height is one number for both heroes: half the section. At that depth
   it stops being an edge treatment and becomes the bottom half of the frame
   dissolving into the page — which is why the ramp starts fully transparent
   and only reaches the checkerboard at the very bottom.

   Sits at the film's level (z-index 1) and, being an ::after, paints over it —
   but still under the copy and the shot, which are z-index 2. */
.hero-panel,
.hero--video { --hero-dither-h: 50%; }
.hero-panel::after,
.hero--video::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--hero-dither-h);
  z-index: 1;
  pointer-events: none;
  background: repeating-conic-gradient(#000 0% 25%, transparent 0% 50%) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000);
  mask-image: linear-gradient(to bottom, transparent, #000);
}

/* Reversed copy, theme-independent — matching the home hero over its own
   footage. The headline runs one colour: the emphasised clause stays white
   rather than picking up the brand green, which loses too much contrast
   against a moving frame. */
.hero--video .hero__title { color: #ffffff; }
.hero--video .hero__title em { color: inherit; }
.hero--video .hero__sub { color: rgb(255 255 255 / 0.92); }

/* The app shot starts hanging past the hero's bottom edge — the section clips
   it, so what's below the boundary is simply out of view — and rides up as the
   hero scrolls, stopping the moment it meets the edge. site.js drives
   --shot-rise from the hero's own progress and never takes it below 0, so the
   shot parks against the boundary rather than lifting off it and opening a gap
   underneath. The value here is the resting start, so a no-JS visitor still
   gets a sensible composition. */
/* The frame around the shot is an 8px band of glass: the padding is the band,
   and the translucent fill over a blur is what makes it read as glass rather
   than as a flat white rule. Outer radius is the inner 4px plus the 8px band,
   so the two curves stay concentric. */
.hero--video .hero-shot {
  margin-bottom: 0;
  transform: translateY(var(--shot-rise, 140px));
  will-change: transform;
  /* Open at the bottom: the glass band and its hairline wrap three sides, and
     the shot runs off the section's edge rather than closing against it. A
     bottom rule there would read as the frame ending mid-air once the shot
     parks on the boundary. */
  padding: 8px 8px 0;
  border-radius: 12px 12px 0 0;
  border: 1px solid rgb(255 255 255 / 0.22);
  border-bottom: 0;
  background: rgb(255 255 255 / 0.12);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
/* Same at the image's own corners — square where it meets the edge. */
.hero--video .hero-shot__img { border-radius: 4px 4px 0 0; }
/* The copy travels with it. Anchoring the two means the gap between headline
   and shot is fixed — they read as one object moving, not as a headline that
   happens to have something sliding underneath it. */
.hero--video .hero__inner {
  transform: translateY(var(--shot-rise, 140px));
  will-change: transform;
}
.hero--video > .site-container:last-child { padding-bottom: 0; }
/* Parallax is the kind of motion this setting turns off — both blocks sit
   still, the shot flush with the bottom edge. */
@media (prefers-reduced-motion: reduce) {
  .hero--video .hero-shot,
  .hero--video .hero__inner { transform: none; }
}
/* Motion in the background is what this setting is for — drop the footage and
   the section falls back to its flat ground. */
@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
}

.hero__inner {
  max-width: 860px;
  margin-inline: auto;
  text-align: center;
}
/* Names the solution above the headline, and the capability above a platform
   section heading. The UI font, not the display face — it sits directly over
   a display-face headline, and two cuts of the same voice stacked read as one
   broken line. Caps and 2px tracking in brand ink still mark it as a category
   rather than another line of copy. */
/* The mark that opens an anchored section, above its eyebrow. Same tinted tile
   the cards in the jump grid carry, and the same glyph — the grid, the rail,
   and the section itself all name a part with one mark, so arriving somewhere
   confirms the link you followed. */
.section__icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-bottom: var(--srve-space-4);
  border-radius: var(--srve-radius-md);
  background: var(--srve-color-brand-primary-tint);
  color: var(--srve-color-brand-text);
}
.section__icon svg { width: 22px; height: 22px; }
/* A centred head centres its mark with everything else in it. */
.section__head--center .section__icon { margin-inline: auto; }

.hero__eyebrow,
.section__eyebrow {
  margin-bottom: var(--srve-space-3);
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-caption); /* 12px */
  font-weight: var(--srve-font-weight-bold);
  line-height: var(--srve-line-height-snug);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--srve-color-brand-text);
}
.hero__title {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  /* 64px — the home hero's size, so every page opens at the same pitch. */
  font-size: 4rem;
  font-weight: var(--srve-font-weight-bold);
  /* Tighter than the 1.2 tight token — all-caps has no descenders, so the
     token leaves a visible gap between the two lines of the headline. */
  line-height: 0.95;
  /* Looser than the -0.03em the UI font wants — the display face is already
     semi-condensed, and the tighter value closes the counters up. */
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--srve-color-text-primary);
}
.hero__title em {
  font-style: normal;
  color: var(--srve-color-brand-text);
}
.hero__sub {
  margin-top: var(--srve-space-5);
  font-size: var(--srve-font-size-body-lg);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}
.hero__cta {
  margin-top: var(--srve-space-6);
  display: flex;
  gap: var(--srve-space-3);
  justify-content: center;
  flex-wrap: wrap;
}
.hero__note {
  margin-top: var(--srve-space-4);
  font-size: var(--srve-font-size-body-sm);
  color: var(--srve-color-text-secondary);
}

/* ---- Home "how it works" tabs ---------------------------------------
   A tab strip over one card that swaps copy + screenshot. Replaced a 450vh
   sticky scroll-driven tour, so there is no runway height to keep in sync
   with JS constants any more. */
.tour {
  /* The page colour, same as the Plans section — so the band no longer reads
     as a separate field and the pastel cards carry the contrast on their own.
     It follows the theme (white / #0d1210), which is why the chrome below is
     on theme tokens rather than the reversed white it used to use. */
  --tour-bg: var(--srve-color-bg-page);
  /* One card colour, not six: olive, the same fill the closing CTA band takes.
     Kept as six variables so a per-step palette can come back without touching
     the [data-step] rules below. */
  --tour-c0: var(--site-olive);
  --tour-c1: var(--site-olive);
  --tour-c2: var(--site-olive);
  --tour-c3: var(--site-olive);
  --tour-c4: var(--site-olive);
  --tour-c5: var(--site-olive);
  background: var(--tour-bg);
}
/* Outside the cards the type is just page type now — the title and intro
   inherit their own colours from .section__title / .section__intro. */
.tour .section__title {
  max-width: 32ch;
  margin-inline: auto;
}

/* Tab strip */
.tour__tabs {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--srve-space-3);
  margin-top: var(--srve-space-7);
}
.tour__tab {
  appearance: none;
  /* Anchors the countdown sweep, clips it to the tab's own corner, and gives it
     a stacking context to sit behind the label in. */
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: var(--srve-space-3) var(--srve-space-5);
  border: 1px solid var(--srve-color-border);
  border-radius: var(--srve-radius-md);
  background: none;
  cursor: pointer;
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-md);
  font-weight: var(--srve-font-weight-medium);
  color: var(--srve-color-text-secondary);
  white-space: nowrap;
  transition:
    color var(--srve-duration-base) var(--srve-ease-standard),
    border-color var(--srve-duration-base) var(--srve-ease-standard),
    background var(--srve-duration-base) var(--srve-ease-standard);
}
.tour__tab:hover {
  color: var(--srve-color-text-primary);
  border-color: var(--srve-color-text-primary);
}
/* The active tab inverts against the page rather than against a dark band —
   except it arrives at that inversion rather than starting there. It opens on
   the same ink at low alpha, a soft chip a shade off the page, and the solid
   fill is what the countdown below brings across it. Ink for the label, which
   is what that faint ground is light enough to carry; the sweep hands it over
   to the reversed copy as it passes. */
.tour__tab[aria-current="true"] {
  color: var(--srve-color-text-primary);
  background: color-mix(in srgb, var(--srve-color-text-primary) 18%, transparent);
  border-color: var(--srve-color-text-primary);
  font-weight: var(--srve-font-weight-semibold);
}
.tour__tab:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}

/* Auto-advance, counted down on the tab itself: the solid fill sweeps across the
   active tab over five seconds and the carousel moves on when it lands, so the
   strip doubles as the progress indicator rather than carrying a separate bar.

   Both values are the same page ink the active tab has always inverted to — the
   tab rests on it at 18% and the sweep brings it at full strength — which is
   what makes it read correctly either way round without a second colour to keep
   in step. In light mode a pale grey chip filling to near-black, in dark mode a
   dark grey one filling to off-white.

   Filling to solid means the ground under the label changes as the sweep
   passes, so the word is drawn twice: once by the button in ink, once by
   ::after in the reversed colour and clipped to the same sweep. It flips letter
   by letter as the fill arrives under it, and neither copy is ever the same
   colour as the ground it is standing on. A pseudo-element rather than a second
   span — the button's own label stays the only text in the accessibility tree.
   Both halves run one linear five seconds, started by the same attribute
   change, which is what keeps the fill and the flip on the same edge.

   The sweep sits behind the button's own label: `isolation` above makes the tab
   a stacking context, so a negative index puts it after the tab's ground and
   before its text. ::after is positioned and so lands above both.

   The advance hangs off `animationend` rather than a matching timer in the
   script. One clock instead of two: pausing the sweep pauses the carousel with
   it, and nothing can drift out of step with the bar the reader is watching. */
.tour__tab[aria-current="true"]::before,
.tour__tab[aria-current="true"]::after {
  position: absolute;
  inset: 0;
  animation-duration: 5s;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  pointer-events: none;
}
.tour__tab[aria-current="true"]::before {
  content: '';
  z-index: -1;
  background: var(--srve-color-text-primary);
  transform: scaleX(0);
  transform-origin: left center;
  animation-name: tour-tab-fill;
}
.tour__tab[aria-current="true"]::after {
  content: attr(data-label);
  /* Centred in the same box the button centres its own label in, so the two
     copies sit exactly on top of each other. The type comes from the button by
     inheritance, semibold weight included. */
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--tour-bg);
  clip-path: inset(0 100% 0 0);
  animation-name: tour-tab-wipe;
}
@keyframes tour-tab-fill {
  to { transform: scaleX(1); }
}
@keyframes tour-tab-wipe {
  to { clip-path: inset(0 0 0 0); }
}
/* Held only while the page is in a background tab, which the script marks: a CSS
   animation keeps running there and the reader would come back several slides
   along. Pausing the sweep is what pauses the advance.

   Nothing a pointer does stops it. Neither hover nor focus is here: the section
   is a full-bleed band most of the way down the page, so a pointer resting
   anywhere in it stopped the carousel dead, and clicking a tab leaves the focus
   on it afterwards, which held the next card indefinitely — a mouse user would
   read both as the countdown having simply died. */
.tour.is-held .tour__tab[aria-current="true"]::before,
.tour.is-held .tour__tab[aria-current="true"]::after {
  animation-play-state: paused;
}
/* No countdown, and so no advance: the carousel stays where it is put. Content
   that moves on by itself is the thing this preference is asking not to happen,
   and the strip is just as usable driven by hand. The tab goes straight to the
   filled end of the sweep, which is what the active tab looked like before any
   of this. */
@media (prefers-reduced-motion: reduce) {
  .tour__tab[aria-current="true"]::before {
    animation: none;
    transform: scaleX(1);
  }
  .tour__tab[aria-current="true"]::after {
    animation: none;
    clip-path: none;
  }
}

/* Panels — a horizontal snap track. The active card is centred with a slice
   of the previous and next cards showing on either side.

   The inline padding does double duty: it creates the leading/trailing space
   that lets the first and last cards centre, and it shrinks the content box
   to exactly the card width — which is why the card below is flex-basis
   100% rather than a calc(). */
.tour__panels {
  /* The peek is the container's own side margin, which makes the card exactly
     content-container wide: centred in the full-bleed rail, its edges land on
     the same lines as every other block on the page, and the neighbours show
     in the margins either side. */
  --tour-peek: max(
    var(--site-gutter),
    calc((100vw - var(--site-max)) / 2 + var(--site-gutter))
  );
  /* Full bleed: the rail runs to both viewport edges so the cards either side
     of the active one carry on past them, rather than stopping short at a
     container margin. (Was capped at 1800px and centred.) */
  width: 100%;
  margin: var(--srve-space-7) 0 0;
  display: flex;
  gap: var(--srve-space-5);
  padding-inline: var(--tour-peek);
  scroll-padding-inline: var(--tour-peek);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.tour__panels::-webkit-scrollbar { display: none; }
.tour__panels:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 4px;
}
.tour__panel {
  position: relative; /* anchors the background illustration */
  flex: 0 0 100%; /* = rail minus both peeks; see .tour__panels padding */
  scroll-snap-align: center;
  display: grid;
  /* Split down the middle: the olive fill is one half of the card and the
     picture surface is the other, meeting on a straight edge. */
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 0;
  /* Fixed, not min-height: the five screenshots range from 3024x1964 to
     3024x3422, so auto-height cards would be ragged along the track. */
  height: min(76vh, 680px);
  /* No inset on the card itself — it would show as a band of olive down the
     picture's outer edges. The copy half carries its own padding instead. */
  padding: 0;
  border-radius: var(--srve-radius-md); /* 8px */
  overflow: hidden;
}
.tour__panel[data-step="0"] { background: var(--tour-c0); }
.tour__panel[data-step="1"] { background: var(--tour-c1); }
.tour__panel[data-step="2"] { background: var(--tour-c2); }
.tour__panel[data-step="3"] { background: var(--tour-c3); }
.tour__panel[data-step="4"] { background: var(--tour-c4); }
.tour__panel[data-step="5"] { background: var(--tour-c5); }

/* The card's picture half — the other side of the split from the olive fill.
   Its own element rather than one of the shots: those are a composition that
   floats over this, and reusing one of them as the half would mean the card had
   no surface of its own to fall back on. Empty it renders as the site's
   standard placeholder grey; drop an <img> in and .img-placeholder covers the
   box with it.

   Placed in the same grid cell as the media column rather than pinned to half
   the panel, so the two always describe the same rectangle: side by side on the
   desktop split, stacked below the copy on a phone. Pinned to a fixed half it
   tracked the card instead of the column, and once the copy ran past that half
   the picture and the card floating on it were centred on different boxes.

   It stays a layer below the copy and the media (z-index 2), which is what lets
   the dither between them run across both halves as one wash rather than
   stopping at the seam. */
.tour__art {
  grid-area: 1 / 2;
  align-self: stretch;
  z-index: 0;
  /* Containing block for the photo. .img-placeholder covers its box with an
     absolutely positioned <img>, which without this would resolve against the
     panel and cover the olive half along with the picture one. */
  position: relative;
  /* The card already clips to its own 8px corner; this keeps the photo inside
     the column's rectangle rather than relying on that. */
  overflow: hidden;
}
/* Placed explicitly, all three: with the art occupying a cell, auto-placement
   would push the media out of it and onto a row of its own. */
.tour__copy { grid-area: 1 / 1; }
.tour__media { grid-area: 1 / 2; }

/* The card thins out into the page along its bottom edge rather than ending on
   a hard line — the same two-layer treatment the photos use, but in the page
   colour instead of black, so it mirrors the field it is dissolving into: white
   in light, #0d1210 in dark. --srve-color-bg-page is the same token .tour sets
   its own background from, so the two can't drift apart.

   ::before is the smooth ramp carrying most of the fade; ::after is the 4px
   checkerboard of the same colour, masked in on a continuous ramp so the dots
   come up evenly rather than switching on in bands.

   The transparent end of each gradient is that same colour at zero alpha, not
   the `transparent` keyword — that keyword is transparent *black*, and
   interpolating to it drags a grey bloom through the middle of the ramp in
   light mode.

   Layer 1 of three: above the olive fill *and* the picture half, so the fade is
   one continuous wash across the whole card instead of stopping where the two
   meet; below the copy and the shots, which the rule after this lifts to 2. */
.tour__panel::before,
.tour__panel::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.tour__panel::before {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--srve-color-bg-page) 0%, transparent) 55%,
    var(--srve-color-bg-page)
  );
}
.tour__panel::after {
  background: repeating-conic-gradient(
      var(--srve-color-bg-page) 0% 25%,
      color-mix(in srgb, var(--srve-color-bg-page) 0%, transparent) 0% 50%
    ) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 50%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 50%, #000 100%);
}

/* Layer 3, over the dither. `relative` is load-bearing besides: the media
   column is the containing block every .tour__shot is positioned against. */
.tour__copy,
.tour__media {
  position: relative;
  z-index: 2;
}

.tour__copy {
  /* The card's inset lives here now rather than on the panel, so it applies to
     the olive half only and the picture half can meet the card's edge. */
  padding: var(--srve-space-8);
  /* Top-aligned, not centred with the rest of the row: the panel got taller,
     and centred copy just drifted into the middle of all that space. The
     screen beside it stays centred. */
  align-self: start;
  max-width: 44ch;
  /* Reversed and fixed: the cards are the same olive in both themes, so the
     ink is fixed with them rather than following the theme tokens. Both lines
     are full white — the body text at 82% measured 4.4:1 on the olive, under
     the 4.5 AA floor. */
  --tour-ink: #ffffff;
  --tour-ink-muted: #ffffff;
}
/* The step CTA is a green primary button, which muddies against the olive —
   invert it, same as the featured plan card and the closing band. */
.tour__copy .srve-button--primary {
  background: #ffffff;
  color: var(--site-olive-deep);
  border-color: transparent;
  box-shadow: none;
}
.tour__copy .srve-button--primary:hover {
  background: color-mix(in srgb, #fff 90%, #000);
}
/* The section's own icon, above the title — the same glyph the platform page
   gives that section, so a slide and the page it links to are recognisably the
   same thing. The block keeps .feature__icon's size and radius and takes new
   colours: that class fills with the brand tint over page ink, which is read on
   the page's own background, and these cards are a fixed olive in both themes.
   Reversed and fixed for the same reason --tour-ink is — a translucent white
   block, matching the glass on the shots beside it rather than inventing a
   third treatment.

   Scoped through .tour__copy to outrank .feature__icon, which sits ~750 lines
   further down: one class each is a specificity tie, and the later rule would
   otherwise keep the tint. */
.tour__copy .tour__icon {
  background: rgb(255 255 255 / 0.14);
  border: 1px solid rgb(255 255 255 / 0.22);
  color: var(--tour-ink);
  margin-bottom: var(--srve-space-3);
}
.tour__title {
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-heading-lg);
  font-weight: var(--srve-font-weight-bold);
  line-height: var(--srve-line-height-tight);
  letter-spacing: -0.02em;
  color: var(--tour-ink);
}
.tour__body {
  margin-top: var(--srve-space-4);
  /* 16px, not the 18 the page's other lead-ins take — it's supporting copy in
     a card, sitting under a 32px title, not a section intro. */
  font-size: var(--srve-font-size-body-md);
  line-height: var(--srve-line-height-relaxed);
  color: var(--tour-ink-muted);
}
.tour__copy .srve-button { margin-top: var(--srve-space-6); }

/* The screenshot reads as a desktop screen: a landscape window centred in the
   card, clear of its top and bottom edges, still running off the right edge so
   the card keeps its sideways momentum.

   The 16/10 box is what does the work. The sources are all over the place —
   3024x1964 through 3024x3838 — and stretching them to the card's full height
   cropped the tall ones down to a narrow vertical slice of app. Fixing the
   frame's ratio instead of the image's means every panel presents the same
   screen shape whatever was captured. */
.tour__media {
  align-self: center; /* not stretch — the frame sets its own height now */
  aspect-ratio: 16 / 10;
  min-height: 0;
}
.tour__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top; /* keep each screenshot's header in frame */
  display: block;
  border-radius: var(--srve-radius-md);
}

/* Cards off-position dim back so the in-view one reads as the subject. */
.tour__panel {
  transition: opacity var(--srve-duration-base) var(--srve-ease-standard);
}
.tour__panel:not(.is-active) { opacity: 0.55; }

@media (prefers-reduced-motion: reduce) {
  .tour__panel { transition: none; }
  .tour__panels { scroll-behavior: auto; }
}

/* Small screens: copy above the screenshot, image no longer bleeds */
@media (max-width: 1023px) {
  .tour__tabs {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    /* let the strip run to the viewport edges while the section keeps its
       gutters, so the last tab doesn't look clipped mid-scroll */
    margin-inline: calc(-1 * var(--site-gutter));
    padding-inline: var(--site-gutter);
    scrollbar-width: none;
  }
  .tour__tabs::-webkit-scrollbar { display: none; }
  /* Each tab keeps its own width and the strip scrolls. Without this the six of
     them stay flex items with the default shrink of 1, so instead of overflowing
     the rail they divide it — every tab squeezed to 50px against labels wanting
     91–146, and .tour__tab's own overflow:hidden (there for the countdown sweep)
     swallowing the rest, so "Storefront" read as "Sto". The scroll container
     above only helps once the items are allowed to overrun it. */
  .tour__tab { flex: 0 0 auto; }
  /* The rail is full-bleed now, so the leading pad is the page gutter rather
     than zero — that's what keeps the first card on the same margin as every
     other block. The trailing pad stays a peek wide so the last card can still
     reach the middle. */
  .tour__panels {
    --tour-peek: 44px;
    padding-inline: var(--site-gutter) var(--tour-peek);
    scroll-padding-inline: var(--site-gutter);
  }
  .tour__panel {
    /* content box is already the rail minus the trailing peek — take the
       leading one off here so the card can sit centred between two slices. */
    flex-basis: calc(100% - var(--tour-peek));
    scroll-snap-align: center;
    grid-template-columns: 1fr;
    gap: var(--srve-space-6);
    /* height:auto, not min-height:0 — the desktop rule sets a fixed height,
       which would clip the stacked layout. */
    height: auto;
    /* Stays off the card, as on the desktop split: an inset here would show as
       a band of olive around the picture half, which is the one thing the split
       is meant not to do. The children carry it instead — 24px rather than the
       desktop 48, since the card is only ~286px wide on a phone and the larger
       inset was spending a third of it on margins. */
    padding: 0;
    gap: 0;
  }
  /* Only the first card opens flush with the gutter; every other one centres.
     Snapping it to `start` rather than `center` is what keeps it there — with
     no leading pad there is nothing to centre it against anyway. */
  .tour__panel:first-child { scroll-snap-align: start; }
  /* Stacked, the split turns from side-by-side to top-and-bottom: the olive
     takes the upper half with the copy on it, the picture half the lower. Same
     card, same seam, rotated a quarter turn. */
  .tour__copy { max-width: none; padding: var(--srve-space-5); grid-area: 1 / 1; }
  /* Even inset on all four sides, not open at the top: the shot is centred in
     this box and the art behind it is not inset at all, so an uneven one put
     the two rectangles' middles 12px apart. */
  .tour__media { padding: var(--srve-space-5); grid-area: 2 / 1; }
  /* The seam turns horizontal: the olive takes the upper row with the copy on
     it, the picture half the lower. Same cell as the media column here too, so
     the grey and the card floating on it stay centred on the same box. */
  .tour__art { grid-area: 2 / 1; }
}

/* Same frame used mid-page, between a section's copy and its feature grid.
   Only the spacing differs — it has a heading above it rather than a hero. */
.section__shot {
  margin-top: var(--srve-space-6);
  margin-bottom: var(--srve-space-7);
  /* Anchors the pull-out below. The frame clips its own corners, so the
     overhang has to stay inside the box. */
  position: relative;
}

/* A UI pull-out floating over the photo, centred on it both ways. The offset
   is half the card's own size rather than a fixed inset, so a card of any
   height lands on the photo's centre without the value being retuned. */
.section__float {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(300px, 42%);
  aspect-ratio: 16 / 10;
  border-radius: var(--srve-radius-md);
  background: var(--srve-color-bg-surface);
  border: 1px solid var(--srve-color-border-surface);
  box-shadow: var(--srve-shadow-lg);
}
/* Most pull-outs are upright cards of roughly one ratio, which is what the
   width above is set for. A wide one — a table rather than a card — reaches
   that cap while still short, and its type ends up smaller than its
   neighbours'. The extra width buys the type back rather than making the
   card louder. */
.section__float--wide { width: min(480px, 62%); }
/* A quarter wider again, for the widest artwork of the set. The calendar card
   is 921x357 — nearly 2.6:1 — so at the --wide cap its rows came out too small
   to read as a calendar at all. Its own step rather than a bump to --wide,
   which two other pull-outs share and are sized correctly at. */
.section__float--xwide { width: min(600px, 78%); }
/* Once a card fills the slot the placeholder's flat surface gives way to the
   same 8px band of glass the video hero and the tour tiles wear: the padding is
   the band, and the translucent fill over a blur is what makes it read as glass
   rather than a flat white rule. Outer radius is the inner 4px plus the 8px
   band, so the two curves stay concentric. The 16/10 above is a shape for the
   empty state to hold — a card brings its own, and cropping to that ratio would
   cut the copy the card exists to show. */
.section__float:has(> img) {
  aspect-ratio: auto;
  padding: 8px;
  border-radius: 12px;
  border: 1px solid rgb(255 255 255 / 0.22);
  background: rgb(255 255 255 / 0.12);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
/* The band is the padding the flowed image sits inside, so all the image owes
   it is the inner corner. */
.section__float > img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}
/* Narrow: the photo is small enough that a 42% card leaves nothing around it,
   so the pull-out takes a larger share of a smaller frame. Centring is
   inherited — only the width changes. */
@media (max-width: 700px) {
  /* The frame stops clipping here. On a wide screen the pull-out is a small card
     sitting well inside a 16/9 photo; on a phone the same card, sized as a share
     of a much shorter frame, comes out *taller* than the photo — the widest of
     them by ~30px — and overflow:hidden was cutting its top and bottom rows off.
     Letting it hang is the right answer at this width: the card is the content,
     the photo is the ground it stands on, and a card breaking past both edges
     reads as an object in front rather than a panel laid on. Nothing is lost by
     dropping the clip — .hero-shot__img does its own rounding and cropping. */
  .hero-shot.section__shot {
    /* Both classes: .hero-shot sets the clip, and it is written further down the
       file, so a single-class selector here would lose the tie. */
    overflow: visible;
    /* Room under the frame for the overhang. The pull-out is centred on the
       photo, so a card 30px taller than it drops ~15 below the lower edge; the
       48px this used to carry is what separates the *photo* from the grid below,
       and the overhang was eating a third of it. */
    margin-bottom: var(--srve-space-8);
  }
  .section__float {
    width: 56%;
    /* Over the feature grid that follows as well as the photo. The grid's cards
       come later in the DOM, so a positioned card at z-index 2 only stays in
       front of them while .section__shot keeps its own stacking order — and the
       overhang now reaches into their space. Lifted here rather than at the base
       rule, which is already correct for a card that stays inside its frame. */
    z-index: 3;
  }
  /* After the rule above, not before it: both selectors are a single class, so
     the modifier only outranks the base by coming later. */
  .section__float--wide { width: 78%; }
  /* Held level with --wide rather than stepped up again: 78% of a phone-width
     frame is already most of the photo, and a quarter more would leave the
     glass band running edge to edge. --xwide needs a rule of its own here all
     the same — without one it would fall back to the 56% base and end up
     narrower on a phone than the card it was widened past. */
  .section__float--xwide { width: 78%; }
}

/* Scroll-linked reveal. site.js writes --float-reveal (0 → 1) from the shot's
   position in the viewport on every frame, so the card's fade and rise are tied
   to the scroll position rather than played as a one-shot animation on entry —
   it tracks the wheel both ways and runs backwards on the way up.

   The value is published on the shot, not the card, so a pull-out moves with
   the photo it sits on rather than to a clock of its own.

   Two deliberate choices here:
   - `translate`, not `transform`: the centring rule above already spends
     `transform` on the -50%/-50%. They're separate properties that compose, so
     the rise is written here without restating the centring — and without
     fighting it the way animating `top` would.
   - no transition: the scroll position *is* the timeline. A duration on top of
     it would only put lag between the wheel and the card.

   Gated on html.js, and defaulting to 1 on both properties, so a no-JS visitor
   gets the cards in place rather than permanently invisible. */
.js .section__shot .section__float {
  opacity: var(--float-reveal, 1);
  translate: 0 calc((1 - var(--float-reveal, 1)) * 28px);
}
@media (prefers-reduced-motion: reduce) {
  /* site.js stops publishing --float-reveal here too; this is the belt to that
     braces, so a card can never sit at a stale offset. */
  .js .section__shot .section__float {
    opacity: 1;
    translate: none;
  }
}

/* Plain-hero variant (platform pages): centered, no frame overlap */
.hero-shot--page {
  max-width: 908px;
  margin-inline: auto;
  margin-top: var(--srve-space-7); /* 48px below the hero copy */
}

/* Desktop product-preview placeholder below the hero CTAs.
   Swap .hero-shot__img's contents for a real screenshot <img>. */
.hero-shot {
  margin-top: var(--srve-space-8); /* 64px */
  /* No bezel: the 6px frosted frame was for a shot floating over the home
     hero's photo, and these sit on a plain page. Square corners too. */
  border-radius: 0;
  overflow: hidden;
  text-align: left;
}
.hero-shot__img {
  border-radius: 0;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--srve-space-3);
  /* Flat black while it's a placeholder — a real screenshot covers it. */
  background: #000000;
  color: rgb(255 255 255 / 0.6);
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm);
  font-weight: var(--srve-font-weight-semibold);
}
.hero-shot__img svg {
  width: 48px;
  height: 48px;
}
/* When a real screenshot or video lands, it fills the frame edge-to-edge. */
.hero-shot__img img,
.hero-shot__img video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}
/* A photo isn't a screenshot: the subject is in the middle of the frame, not
   at the top, so a 16/9 crop of one has to hold the centre. */
.hero-shot__img--photo img {
  object-position: center;
}
/* The shading runs the whole height of the frame here rather than starting a
   third of the way down. A 16/9 hero is short enough that a ramp beginning at
   30% reads as a band across the bottom; over the full box the same fade reads
   as one graded wash. */
/* Both selectors carry the .hero-shot ancestor so they outrank
   .img-placeholder--dither's own ::before/::after, which are defined later in
   the file and would otherwise win the tie. */
.hero-shot .hero-shot__img--photo::before {
  background: linear-gradient(to bottom, transparent, rgb(0 0 0 / 0.5));
}
.hero-shot .hero-shot__img--photo::after {
  -webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.12), #000);
  mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.12), #000);
}

/* ---- Homepage hero panel --------------------------------------------
   Edge-to-edge panel (no side margins or corner radius, unlike the tour
   below) that extends up behind the floating header. Full-bleed chef photo,
   no scrim — the copy is reversed to white. Hero copy and CTAs at the top, a
   glass-bezel app screenshot filling the rest and bleeding off the panel's
   bottom edge. */
.hero-lead {
  /* No bottom padding — the showcase below butts straight up against the
     photo's bottom edge. */
  padding-bottom: 0;
}
.hero-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start; /* copy on the left */
  justify-content: flex-end; /* ...and anchored to the bottom */
  width: 100%;
  /* Full-viewport hero at every width. min-height rather than height so a
     short window grows the panel instead of clipping the copy against
     overflow: hidden. (The old 118vh made room for the device mockup that
     used to bleed off the bottom edge.) */
  min-height: 100vh;
  min-height: 100svh; /* svh = stable under mobile browser chrome */
  overflow: hidden;
  /* Flat dark ground under the video. It used to be a still of the same
     footage, standing in while the file loaded; the colour holds that job now
     and there's no second image to keep in sync with the clip. */
  background-color: #14231a;
}
/* Background loop, cropped to fill the panel.
   The three layers are z-indexed explicitly rather than left to DOM order —
   ::before generates as the panel's first child, so without this the video
   would paint over the film meant to sit on top of it. */
.hero-panel__video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  pointer-events: none;
}
/* Even dark film over the footage — just enough to settle the white chef
   shirts the headline crosses, without reading as a wash. */
.hero-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--hero-film);
  pointer-events: none;
}
/* Motion in the background is the kind of thing this setting is for — drop
   the video and the panel falls back to its flat ground. */
@media (prefers-reduced-motion: reduce) {
  .hero-panel__video { display: none; }
}
.hero-panel__content {
  position: relative; /* above the film */
  /* Sits 64px below centre. An offset rather than a margin: the auto margins
     below do the centring, and a margin-top would fight them for the free
     space instead of shifting the result. `top` also leaves the press bar's
     position alone, since a relative offset doesn't affect layout. */
  top: var(--srve-space-8); /* 64px */
  z-index: 2;
  flex: 0 0 auto;
  /* Centres the copy in the space above the press bar. Auto margins absorb the
     panel's free space before justify-content:flex-end gets to it, so the copy
     floats to the middle while the press bar stays parked on the bottom edge —
     without either one needing to know the other's height. */
  margin-block: auto;
  /* content-box so the 760px is the copy column itself. Under the global
     border-box the left inset below (which grows with the viewport) was
     eaten out of the 760, so the wider the screen the narrower the copy —
     at 1920 the headline was down to a 360px column. */
  box-sizing: content-box;
  max-width: 760px;
  /* The left inset tracks the right edge of .site-container so the copy lines
     up with the page grid rather than the raw viewport edge. The bottom
     clearance is the press bar's job now (.hero-panel__press), which sits
     below this block on the panel's bottom edge. */
  padding: 0 var(--srve-space-5) var(--srve-space-7);
  padding-left: max(
    var(--site-gutter),
    calc((100vw - var(--site-max)) / 2 + var(--site-gutter))
  );
  text-align: left;
}
.hero-panel__content .hero__cta { justify-content: flex-start; }
/* Reversed copy for the photo background — flat white, no shadow. The size
   that used to live here is the shared one now, so only the colour differs. */
.hero-panel__content .hero__title {
  color: #ffffff;
}
/* Headline runs a single colour — the emphasised clause is white with the
   rest of the line rather than picking up the brand green. */
.hero-panel__content .hero__title em { color: inherit; }
.hero-panel__content .hero__sub {
  color: rgb(255 255 255 / 0.92);
  /* Pulled in from the shared 24px — the all-caps headline has no descenders,
     so the default gap reads larger than it measures. */
  margin-top: var(--srve-space-3);
}
/* Staggered entrance: each block fades up from below, headline first, then the
   supporting line and the buttons. The travel is deliberately larger than the
   ~24px that reads as "polish" — at this size, over a moving video, a short
   rise is finished before the eye has settled on the text at all. */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: none; }
}
.hero-panel__content > * {
  animation: heroRise 0.9s var(--srve-ease-standard) both;
}
.hero-panel__content .hero__title { animation-delay: 0.15s; }
.hero-panel__content .hero__sub { animation-delay: 0.45s; }
.hero-panel__content .hero__cta { animation-delay: 0.55s; }
.hero-panel__content .logo-strip { animation-delay: 0.7s; }

@media (prefers-reduced-motion: reduce) {
  .hero-panel__content > * { animation: none; }
}

/* (No mobile height override — the panel is viewport-height at every width,
   and the centred copy clears the header on its own.) */

/* ---- Image + video showcase -------------------------------------------
   Photo bleeds off the left viewport edge; the video card is absolutely
   positioned so it straddles the photo's right edge, centred on the photo's
   midline, with its right edge on the site container's gutter. */
.showcase {
  position: relative;
  /* Flush at both ends: the hero sits directly above, and the navy "how it
     works" band starts at this section's content edge. */
  padding-block: 0;
}
.showcase__image {
  width: 57%;
  /* Roughly square: the source is a portrait frame, so a squarer block keeps
     the chef's head and the cutting board in shot under object-fit: cover. */
  aspect-ratio: 1 / 1;
  /* Inset off the section's top, left and bottom edges — the photo no longer
     runs straight into the viewport corner. Nothing on the right, where it
     meets the video card and the illustration. */
  padding: 2rem 0 2rem 2rem;
  margin: 0; /* it's a <figure> now, and UA styles give those margins */
  position: relative; /* anchor for the credit */
}
/* Chef credit, sitting on the photo's lower-left corner. No scrim or shadow:
   that corner of the frame averages out dark enough to carry white type on
   its own, and the site dropped reversed-type shadows elsewhere. */
.showcase__credit {
  position: absolute;
  left: calc(2rem + var(--srve-space-5));
  bottom: calc(2rem + var(--srve-space-5));
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  /* 24px → 32px. Fixed at 32 the name ran past the photo's right edge once
     the frame narrowed on a phone. */
  font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: var(--srve-font-weight-bold);
  line-height: var(--srve-line-height-snug);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #ffffff;
}
.showcase__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: var(--srve-radius-md);
}
.showcase__video {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  /* line up with the right edge of .site-container at any viewport width */
  right: max(
    var(--site-gutter),
    calc((100vw - var(--site-max)) / 2 + var(--site-gutter))
  );
  width: min(46%, 620px);
  /* Holds the card's box while the video is lifted into the lightbox, so the
     close animation has a real rect to fly back to. */
  aspect-ratio: 16 / 9;
}
.showcase__video video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: #14231a; /* letterbox fill until the first frame paints */
  border-radius: var(--srve-radius-md);
}

/* Decorative fish filling the white column between the photo and the right
   gutter, with the video card floating over it. The white ground and the ink
   colour are handled by .illustration (see the bottom of this file). */
.showcase__fish {
  position: absolute;
  /* No z-index: a positioned element with one — even 0 — opens a stacking
     context, and the image inside would then blend against this box's own
     empty backdrop instead of the page. In dark mode that showed as a film:
     the inverted drawing's black square composited as a black square rather
     than screening away. Paint order still works out, the video card is
     positioned and comes later in the markup. */
  /* Fills the white column the photo leaves behind: from the photo's right
     edge (it is 57% wide) out to the viewport edge, top to bottom. Bleeding
     off the right mirrors the photo bleeding off the left; the video card
     still stops at the container gutter. */
  top: 0;
  bottom: 0;
  left: 57%;
  right: 0;
  pointer-events: none;
}
/* The <img> is wrapped rather than positioned directly: an absolutely
   positioned replaced element resolves `width: auto` to its intrinsic size
   and ignores the opposite inset, so the 1024px drawing hung outside the
   section. A plain block honours all four insets, and the image then fills it.

   cover, not contain: the column is taller than it is wide while the drawing
   is square, so containing it would leave the box short of its own height.
   Covering fills both axes and crops the plate's left and right edges, which
   stays inside the box — nothing escapes the section either way. */
.showcase__fish img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scroll-linked rise. site.js writes --scroll-rise from the card's position
   in the viewport on every frame, so the card's travel is tied to the scroll
   position rather than played as a one-shot animation on entry.

   Two deliberate choices here:
   - `transform`, not `translate`: the rule above already spends `translate`
     on the -50% vertical centring. They're separate properties that compose,
     so the card stays centred while being offset.
   - No transition. The value is already updated per frame; easing it too
     would make the card lag the scroll instead of tracking it.

   The fallback of 0px means no JS (or reduced motion, where site.js clears
   the property) leaves the card exactly where the layout puts it. */
.showcase__video {
  transform: translateY(var(--scroll-rise, 0px));
}

/* Play pill — replaces the native controls until the video is started.
   site.js unhides it and strips `controls`, so no-JS visitors keep the
   native player. */
.video-play {
  /* inset + margin:auto centres it while leaving the *full* card width as
     available space — with left:50% the shrink-to-fit width is measured
     against the right half only, which wraps the label to three lines. */
  position: absolute;
  inset: 0;
  margin: auto;
  width: max-content;
  height: max-content;
  display: inline-flex;
  align-items: center;
  gap: var(--srve-space-5);
  max-width: calc(100% - 2 * var(--srve-space-5));
  padding: 10px 10px 10px var(--srve-space-6);
  border: 0;
  border-radius: 999px;
  background: rgb(38 44 41 / 0.55);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  color: #ffffff;
  text-align: left;
  cursor: pointer;
  transition:
    background var(--srve-duration-base) var(--srve-ease-standard),
    opacity var(--srve-duration-base) var(--srve-ease-standard);
}
.video-play:hover { background: rgb(38 44 41 / 0.72); }
.video-play:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 3px;
}
.video-play__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.video-play__title {
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-lg);
  font-weight: var(--srve-font-weight-semibold);
  line-height: var(--srve-line-height-tight);
}
.video-play__meta {
  font-size: var(--srve-font-size-body-sm);
  color: rgb(255 255 255 / 0.72);
}
.video-play__meta:empty { display: none; }
.video-play__icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: #ffffff;
  /* Pinned, not --srve-color-text-primary: the disc is white in both themes,
     so a theme-following glyph would be near-white on white in dark mode. */
  color: #111827;
}
.video-play__icon svg {
  width: 24px;
  height: 24px;
  /* optical centring — the triangle's mass sits left of its bounding box */
  margin-left: 2px;
}

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

/* Video lightbox ------------------------------------------------------- */
.video-modal {
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  overflow: hidden;
}
.video-modal[open] {
  display: flex;
  align-items: center;
  justify-content: center;
}
.video-modal::backdrop {
  background: rgb(11 31 22 / 0.88);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* The stage itself is animated from the card's rect in site.js; the backdrop
   can only be reached from CSS, so it fades on its own. */
@keyframes videoModalBackdropIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.video-modal[open]::backdrop {
  animation: videoModalBackdropIn 0.28s var(--srve-ease-standard) both;
}
/* No letterboxing: the stage takes the video's own aspect ratio (site.js
   overwrites --video-ar from the file's real dimensions), and the height
   budget is folded into the *width* calculation rather than applied as a
   max-height. A max-height would squash the box off-ratio and object-fit
   would fill the difference with bars. */
.video-modal__stage {
  --video-ar: 1.7778; /* 16:9 until metadata lands */
  aspect-ratio: var(--video-ar);
  width: min(
    1280px,
    calc(100vw - 2 * var(--srve-space-6)),
    calc((100dvh - 2 * var(--srve-space-8)) * var(--video-ar))
  );
}
/* Beats the card's `.showcase__video video` rule while the element is
   parented here. The stage already matches the video's ratio, so contain
   fills it edge to edge with nothing left over. */
.video-modal .video-modal__stage video {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: contain;
  background: none;
  border-radius: var(--srve-radius-md);
  box-shadow: none;
}
.video-modal__close {
  position: absolute;
  top: var(--srve-space-5);
  right: var(--srve-space-5);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(255 255 255 / 0.14);
  color: #ffffff;
  cursor: pointer;
  transition: background var(--srve-duration-base) var(--srve-ease-standard);
}
.video-modal__close:hover { background: rgb(255 255 255 / 0.28); }
.video-modal__close:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 3px;
}
.video-modal__close svg { width: 22px; height: 22px; }

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

/* Stacked below the two-column breakpoint: photo full-bleed, video pulled up
   over its lower edge so the overlap survives. */
@media (max-width: 900px) {
  /* Stacked, the video card is the last thing in the section — a little
     clearance so its rounded corners don't butt straight into the navy.
     (Desktop needs none: the photo bleeds to the boundary on purpose and the
     video card sits well clear of it.) */
  .showcase { padding-block: 0 var(--srve-space-6); }
  /* Keeps the square crop from the desktop rule — a landscape crop of this
     portrait source cuts the chef's head off. Padding goes symmetric too:
     the desktop rule leaves the right edge flush because the video card and
     the illustration meet it there, and stacked there is nothing to meet. */
  .showcase__image {
    width: 100%;
    /* A rem wider each side than the video's inset below, so the photo sits
       slightly narrower than the card and the two read as a stack rather than
       one block. */
    padding-inline: 3rem;
    /* No bottom inset — the video meets it directly. */
    padding-bottom: 0;
  }
  /* The name centres on the photo here. Left-aligned it sat off-axis against a
     stacked column that is otherwise centred, and the narrower frame left it
     little room to run. */
  .showcase__credit {
    /* Matches the photo's own inset above, so the name can't run past its
       edges — it's positioned against the figure, not the image. */
    left: 3rem;
    right: 3rem;
    bottom: var(--srve-space-5);
    text-align: center;
  }
  /* Stacked, there is no white column left for it to sit in. */
  .showcase__fish { display: none; }
  .showcase__video {
    /* relative, not static — the play pill is absolutely positioned and must
       stay anchored to the card rather than the section. */
    position: relative;
    inset: auto;
    translate: none;
    /* A rem wider each side than the photo above it. */
    width: calc(100% - 4rem);
    /* Flush against the photo — no gap at all. The desktop overlap was there
       to make the card read as floating on the photo, which only works
       alongside the parallax; stacked, it just crops the chef. */
    margin: 0 auto;
    /* And no parallax: site.js keeps publishing --scroll-rise, but a card in
       the flow that slides against its own photo reads as a glitch rather
       than depth. */
    transform: none;
  }
}

/* ---- Grids ----------------------------------------------------------- */
.grid {
  display: grid;
  gap: var(--srve-space-5);
}
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }

/* Centres a short last row instead of leaving it hanging on the left — five
   advisors in a three-across grid read as lopsided otherwise.

   Six columns with every card spanning two: the card width works out
   identical to a three-column track (2·(W−5g)/6 + g == (W−2g)/3), so nothing
   resizes — it just buys half-column positions for the orphan row. Scoped
   above the 900px collapse, where .grid--3 is a single column and the spans
   would fight it. */
@media (min-width: 901px) {
  .grid--3.grid--balanced { grid-template-columns: repeat(6, 1fr); }
  .grid--3.grid--balanced > * { grid-column: span 2; }
  /* a trailing pair — indent by one column and the two centre themselves */
  .grid--3.grid--balanced > :nth-child(3n+1):nth-last-child(2) { grid-column: 2 / span 2; }
  /* a single trailing card — dead centre */
  .grid--3.grid--balanced > :nth-child(3n+1):last-child { grid-column: 3 / span 2; }
}

/* ---- Feature / selector cards --------------------------------------- */
.feature {
  padding: var(--srve-space-6);
  /* With the hairline gone the fill is the only thing making this read as a
     card, so it takes whichever of the two surfaces its section isn't:
     the tint on a plain band, the surface colour inside .section--tint. */
  background: var(--card-fill, var(--srve-color-bg-subtle));
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  gap: var(--srve-space-3);
  height: 100%;
}
.feature__icon {
  width: 44px;
  height: 44px;
  border-radius: var(--srve-radius-md);
  background: var(--srve-color-brand-primary-tint);
  color: var(--srve-color-brand-text);
  display: grid;
  place-items: center;
  margin-bottom: var(--srve-space-2);
}
.feature__icon svg { width: 22px; height: 22px; }
/* Card titles run in the UI face, not the display one. The site carries far
   too many of them — six-up and eight-up grids on every solution page — and at
   that count the heavy semi-condensed display face reads as texture rather
   than as headings. The display face is left to the section titles and heroes,
   where it has room to do its job. Both tiers are listed because the jump-nav
   and solution cards are .selector__label, not .feature__title, and sit at the
   same level in the hierarchy. */
.feature__title,
.selector__label {
  font-family: var(--srve-font-family-ui);
  letter-spacing: -0.01em;
  /* 18px, not the 20px heading step. Every card title on the site was already
     carrying an inline override down to this size; it lives here now so the
     scale is set in one place and the compact grids below can step it down
     without fighting inline styles. */
  font-size: var(--srve-font-size-body-lg);
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
}
.feature__body {
  font-size: var(--srve-font-size-body-md);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}

/* Self-identification selector (links styled as cards) */
.selector {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--srve-space-4);
  padding: var(--srve-space-5) var(--srve-space-6);
  background: var(--card-fill, var(--srve-color-bg-subtle)); /* see .feature */
  border-radius: 12px;
  text-decoration: none;
  transition:
    background-color var(--srve-duration-base) var(--srve-ease-standard),
    transform var(--srve-duration-base) var(--srve-ease-standard);
}
/* The lift came from a deepening shadow, then from the border; with the
   border gone the fill deepens instead, keeping the same 2px rise. */
.selector:hover {
  background: color-mix(
    in srgb,
    var(--card-fill, var(--srve-color-bg-subtle)) 85%,
    var(--srve-color-text-primary)
  );
  transform: translateY(-2px);
}
/* Type for .selector__label is set alongside .feature__title above — the two
   are the same tier and stay in step. */

/* Cursor trace — opt-in per section via .pointer-trace on the <section>.
   Hover already deepens the whole fill toward the text colour (85%); this
   pools a little further in the same direction under the pointer, so the
   highlight and the hover cue move the same way rather than fighting. Both
   mixes read from --card-fill, so it stays right in either theme and in any
   section that overrides the fill.

   --pointer-x/y are written in px by site.js; the 50% fallback keeps the
   card centred (and invisible, since opacity is 0) before the first move. */
.pointer-trace .selector {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.pointer-trace .selector::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Behind the label, above the card's own background. */
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--srve-duration-base) var(--srve-ease-standard);
  background: radial-gradient(
    180px circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
    color-mix(
      in srgb,
      var(--card-fill, var(--srve-color-bg-subtle)) 76%,
      var(--srve-color-text-primary)
    ),
    transparent 72%
  );
}
.pointer-trace .selector:hover::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  /* Nothing to track — the plain hover fill carries the state. */
  .pointer-trace .selector::after { display: none; }
}

/* ---- Launch / operate / grow ---------------------------------------- */
.stage {
  --stage-fill: var(--srve-color-bg-subtle);
  position: relative; /* anchors the numeral behind the copy */
  padding: var(--srve-space-6);
  /* room for the numeral above the title — the title crosses its lower half */
  padding-top: 3.1rem;
  border-radius: 12px;
  background: var(--stage-fill);
  height: 100%;
  overflow: hidden; /* the numeral overhangs the padding box */
}
/* The number is scenery, not a badge: an oversized numeral tucked into the
   top-left corner with the title sitting across it, the way the step blocks
   read in print. Tonal rather than brand green — it carries the sequence
   without competing with the title for the eye. */
.stage__num {
  position: absolute;
  left: calc(var(--srve-space-6) - 1.4rem); /* optical: numerals carry side bearing */
  top: -0.14em;
  z-index: 0;
  font-family: var(--srve-font-family-ui);
  font-size: 7rem;
  line-height: 1;
  font-weight: var(--srve-font-weight-bold);
  letter-spacing: -0.04em;
  color: color-mix(in srgb, var(--srve-color-text-primary) 18%, var(--stage-fill));
  pointer-events: none;
  user-select: none;
}
/* …and it dissolves downward, same idea as the hero and photo dithers: a 4px
   checkerboard painted in the card's own fill, faded in by a mask, so the
   bottom of the numeral breaks into pixels instead of ending on a hard edge.
   Punching the fill through works in both themes — the pattern is the card
   colour, whatever that colour is. */
.stage__num::after {
  content: "";
  position: absolute;
  inset: -2px;
  background: repeating-conic-gradient(
      var(--stage-fill) 0% 25%,
      transparent 0% 50%
    ) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 30%, #000 92%);
  mask-image: linear-gradient(to bottom, transparent 30%, #000 92%);
}
.stage__title {
  position: relative; /* over the numeral */
  z-index: 1;
  /* UI face, same as .feature__title — these boxes sit in a grid between two
     card grids on every solution page, so they read as the same tier. Size
     stays a step up: three cards, one word each, carrying the numbered
     sequence. */
  font-family: var(--srve-font-family-ui);
  letter-spacing: -0.01em;
  font-size: var(--srve-font-size-heading-sm);
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
  margin-bottom: var(--srve-space-3);
}
/* padding:0 matters as much as list-style:none — the base reset zeroes margin
   but leaves the UA's 40px padding-inline-start, which was pushing the bullets
   out past the stage number and title above them. Same fix as .plan__list. */
.stage__list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--srve-space-2); }
.stage__list li {
  position: relative;
  padding-left: var(--srve-space-4);
  font-size: var(--srve-font-size-body-md);
  color: var(--srve-color-text-secondary);
  line-height: var(--srve-line-height-normal);
}
/* A small dot in the type colour rather than brand green — white on the dark
   theme, near-black on the light one, so it stays a marker and not an accent. */
.stage__list li::before {
  content: "";
  position: absolute;
  left: 2px; top: 0.62em;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--srve-color-text-primary);
}

/* ---- Operational loop (alternating rows) ---------------------------
   The section tint runs edge to edge; the rows are held to the site
   container. Each row is a two-column grid — a rounded photo one side, the
   copy the other — with a UI pull-out floating over the photo's inner
   corner. Rows alternate sides via :nth-child(even). */
.loop-rows {
  /* contained, like every other section's .site-container */
  width: 100%;
  max-width: var(--site-max);
  margin-inline: auto;
  padding-inline: var(--site-gutter);
  display: flex;
  flex-direction: column;
  gap: clamp(var(--srve-space-8), 8vw, 112px);
}
.loop-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: clamp(var(--srve-space-6), 5vw, var(--srve-space-9));
  min-height: min(64vh, 520px);
}
/* odd row: photo left / copy right */
.loop-row__media { order: -1; }
.loop-row__text { max-width: 46ch; }
/* even row: photo right / copy left */
.loop-row:nth-child(even) .loop-row__media { order: 0; }
.loop-row:nth-child(even) .loop-row__text { justify-self: end; }

.loop-row__media {
  position: relative;
  align-self: stretch;
  min-height: min(64vh, 520px);
}
/* Rounding lives on the photo, not the media box — the float is a sibling
   that overhangs the media edge and must not be clipped. */
.loop-row__photo {
  position: absolute;
  inset: 0;
  border-radius: var(--srve-radius-md);
  overflow: hidden;
  background: var(--srve-color-bg-subtle); /* shown only while loading */
}
.loop-row__photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Floating card hovering over the photo's lower edge, centred on it. */
.loop-row__float {
  position: absolute;
  z-index: 2;
  width: min(340px, 78%);
  left: 50%;
  /* Hangs a little past the photo's lower edge rather than sitting inside it —
     the overhang is what makes the card read as a separate object in front of
     the picture instead of a panel laid onto it. The row gap below is 48px at
     its tightest, so the drop never reaches the next row. */
  bottom: calc(var(--srve-space-6) * -1);
  transform: translateX(-50%);
  /* 14, not the 8 of --srve-radius-md: the service cards are artwork that draws
     its own corner at 12 against a 266px canvas, which lands near 15 once the
     card renders at 340 — the tighter clip was cutting a second curve inside the
     drawn one. Literal for the same reason the tour's glass frames are: there is
     no token at this step. */
  border-radius: 14px;
  overflow: hidden;
  background: var(--srve-color-bg-surface);
}
/* Standing in for artwork that hasn't landed. Fills whatever box holds it, so
   one class covers the full-bleed photo and the small floating card.

   --placeholder-fill/-ink are the knobs: the two nest inside each other here,
   so they have to differ or the floating card disappears into the photo spot
   behind it. */
.img-placeholder {
  position: relative; /* the absolute image below anchors here, not on the page */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--srve-space-3);
  width: 100%;
  height: 100%;
  /* Mid grey from the palette — loud enough to spot as a gap, quiet enough
     not to read as a designed block. Both tokens flip with the theme, so the
     label stays legible on either value of the fill. */
  background: var(--placeholder-fill, var(--srve-color-neutral-300));
  color: var(--placeholder-ink, var(--srve-color-text-primary));
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-sm);
  font-weight: var(--srve-font-weight-semibold);
  text-align: center;
}
.img-placeholder svg { width: 40px; height: 40px; }
/* A real image dropped in fills the frame and the shading plays over it —
   the placeholder becomes a frame rather than a stand-in. */
.img-placeholder > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
/* The chip on top flips to the light surface, so the two blocks read as two
   separate images rather than one. */
.loop-row__float .img-placeholder {
  --placeholder-fill: var(--srve-color-bg-surface);
  --placeholder-ink: var(--srve-color-text-primary);
}
/* …and the photo's own label sits in its upper half, clear of the chip that
   covers the middle. Centred, it was hidden behind it. */
.loop-row__photo .img-placeholder { padding-bottom: 42%; }
.img-placeholder--dither { position: relative; /* anchors the layers below */ }
/* Three layers, bottom up: the image, the shading, then the label. Excluding
   the image here matters — caught in the same rule it would lift above the
   shading and the gradient would do nothing. */
.img-placeholder--dither > :not(img) {
  position: relative;
  z-index: 2;
}
/* Darkening toward the bottom, in two parts.

   ::before is the smooth ramp — a plain gradient carrying most of the shade.
   ::after is the dither: a 4px checkerboard of the same dark, faded in by a
   mask. The mask used to be cut into fixed-alpha bands, which made the dots
   switch on in visible steps — legible as horizontal blocks across the frame
   rather than as texture. It's a continuous ramp now, so the checkerboard
   comes up evenly and the two layers together read as one graded shadow that
   breaks into pixels as it deepens. */
.img-placeholder--dither::before,
.img-placeholder--dither::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}
.img-placeholder--dither::before {
  background: linear-gradient(to bottom, transparent 35%, rgb(0 0 0 / 0.5));
}
.img-placeholder--dither::after {
  background: repeating-conic-gradient(#000 0% 25%, transparent 0% 50%) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 30%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 30%, #000 100%);
}
/* The floating card has no height of its own — the photo behind it is the one
   filling the media box — so the placeholder inside it needs a shape. */
.loop-row__float .img-placeholder { aspect-ratio: 16 / 10; height: auto; }
.loop-row__float img { width: 100%; height: auto; display: block; }

.loop-row__title {
  font-size: var(--srve-font-size-heading-lg);
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
  line-height: var(--srve-line-height-tight);
  letter-spacing: -0.02em;
}
.loop-row__body {
  margin-top: var(--srve-space-4);
  font-size: var(--srve-font-size-body-lg);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}
.loop-row__text .srve-button { margin-top: var(--srve-space-6); }

/* ---- Membership plans ----------------------------------------------- */
.plan {
  position: relative; /* anchors .plan__tag on any plan card */
  display: flex;
  flex-direction: column;
  padding: var(--srve-space-6);
  border-radius: 16px;
  /* No hairline — the fill carries the card, so it takes whichever surface its
     section isn't (same as .feature). Pinned to bg-surface it was white on a
     white page, which is what the border was covering for. */
  background: var(--card-fill, var(--srve-color-bg-subtle));
  height: 100%;
}
/* The featured plan stood out by casting a deeper shadow than its siblings,
   then by a brand-coloured edge; now it's filled with the brand green and
   everything inside it reverses. */
.plan--featured {
  background: var(--srve-color-brand-primary);
  position: relative;
}
.plan--featured .plan__name,
.plan--featured .plan__price-amount,
.plan--featured .plan__list li { color: #ffffff; }
/* The muted greys have no equivalent on a green fill — a white at reduced
   opacity keeps the same hierarchy without going grey-on-green. */
.plan--featured .plan__for,
.plan--featured .plan__price-period,
.plan--featured .plan__price-annual { color: rgb(255 255 255 / 0.82); }
/* The ticks were brand-green on white; on green they'd vanish. */
.plan--featured .plan__list svg { color: #ffffff; }
/* Same inversion the CTA band gives its buttons — the card's CTA is a primary
   green one, which would disappear into the fill otherwise. */
.plan--featured .srve-button--primary,
.plan--featured .srve-button--secondary {
  background: #ffffff;
  color: var(--srve-color-brand-primary-active);
  border-color: transparent;
  box-shadow: none;
}
.plan--featured .srve-button--primary:hover,
.plan--featured .srve-button--secondary:hover {
  background: color-mix(in srgb, #fff 90%, #000);
}
/* Starter and Business thin out into the page along their lower edge rather than
   ending on a hard line — the same two-layer treatment the tour cards take, in
   the page colour, so each mirrors the field it is dissolving into: white in
   light, #0d1210 in dark. Not the featured plan, which is filled with the brand
   green and is meant to hold its edge against the two either side of it.

   ::before is the smooth ramp carrying most of the fade; ::after is the 4px
   checkerboard of the same colour, masked in on a continuous ramp so the dots
   come up evenly rather than switching on in bands.

   The transparent end of each gradient is that same colour at zero alpha, not
   the `transparent` keyword — that keyword is transparent *black*, and
   interpolating to it drags a grey bloom through the middle of the ramp in
   light mode.

   Both sit over the card's fill and under everything in it: it is the surface
   that goes, while the copy and the CTA standing on it stay at full strength. */
.plan:not(.plan--featured)::before,
.plan:not(.plan--featured)::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  /* The card's own 16px corner — square, the layers would show past it. */
  border-radius: inherit;
  pointer-events: none;
}
.plan:not(.plan--featured)::before {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--srve-color-bg-page) 0%, transparent) 55%,
    var(--srve-color-bg-page)
  );
}
.plan:not(.plan--featured)::after {
  background: repeating-conic-gradient(
      var(--srve-color-bg-page) 0% 25%,
      color-mix(in srgb, var(--srve-color-bg-page) 0%, transparent) 0% 50%
    ) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 50%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 50%, #000 100%);
}
/* Lifted clear of the two layers. The tag is positioned already and needs only
   the index; the rest of the card is in flow and needs both. */
.plan:not(.plan--featured) > *:not(.plan__tag) { position: relative; }
.plan:not(.plan--featured) > * { z-index: 1; }

.plan__tag { position: absolute; top: var(--srve-space-5); right: var(--srve-space-5); }
.plan__name {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  /* 36px — 16 up from the heading-sm this used to take. No token sits at 36,
     so it's written out rather than rounded to heading-lg's 32. */
  font-size: 2.25rem;
  font-weight: var(--srve-font-weight-bold);
  line-height: var(--srve-line-height-tight);
  color: var(--srve-color-text-primary);
}
.plan__for {
  margin-top: var(--srve-space-2);
  font-size: var(--srve-font-size-body-md);
  color: var(--srve-color-text-secondary);
  line-height: var(--srve-line-height-relaxed);
  min-height: 66px;
}
.plan__price {
  display: flex;
  align-items: baseline;
  gap: var(--srve-space-2);
  margin-top: var(--srve-space-4);
}
.plan__price-amount {
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-heading-lg); /* 32px */
  font-weight: var(--srve-font-weight-bold);
  letter-spacing: -0.02em;
  color: var(--srve-color-text-primary);
}
.plan__price-period {
  font-size: var(--srve-font-size-body-sm);
  font-weight: var(--srve-font-weight-medium);
  color: var(--srve-color-text-secondary);
}
.plan__price-annual {
  margin-top: var(--srve-space-1);
  font-size: var(--srve-font-size-caption);
  font-weight: var(--srve-font-weight-medium);
  color: var(--srve-color-text-secondary);
}
/* padding:0 matters as much as list-style:none here — the base reset zeroes
   margin on everything but leaves the UA's 40px padding-inline-start on lists,
   which was indenting the ticks away from the plan name above them. */
.plan__list { list-style: none; margin: var(--srve-space-5) 0; padding: 0; display: flex; flex-direction: column; gap: var(--srve-space-3); flex: 1; }
.plan__list li {
  display: flex;
  gap: var(--srve-space-2);
  font-size: var(--srve-font-size-body-md);
  color: var(--srve-color-text-primary);
  line-height: var(--srve-line-height-normal);
}
.plan__list svg { width: 18px; height: 18px; color: var(--srve-color-brand-text); flex-shrink: 0; margin-top: 2px; }

/* ---- Pricing comparison table ----------------------------------------
   Base styling comes from the design-system table component
   (src/components/table/table.css). These overrides only add the
   feature-column emphasis and the Pro-column highlight. */
.compare tbody td:first-child {
  font-weight: var(--srve-font-weight-semibold);
  color: var(--srve-color-text-primary);
}
.compare thead th.is-pro {
  color: var(--srve-color-brand-text);
}
.compare tbody td.is-pro {
  color: var(--srve-color-text-primary);
  font-weight: var(--srve-font-weight-medium);
}

/* ---- Proof / stories ------------------------------------------------- */
.quote {
  padding: var(--srve-space-6);
  border-radius: 0;
  /* Takes whichever surface its section isn't — see .feature. It was pinned to
     bg-surface, which is white on the white page these sit on, so the card had
     no edge at all in light mode. */
  background: var(--card-fill, var(--srve-color-bg-subtle));
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--srve-space-4);
}
.quote__text {
  font-size: var(--srve-font-size-body-lg);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-primary);
}
.quote__image {
  display: block;
  width: calc(100% + 2 * var(--srve-space-6));
  max-width: none; /* escape the global img max-width so it can full-bleed */
  height: 240px;
  margin: calc(-1 * var(--srve-space-6)) calc(-1 * var(--srve-space-6)) 0;
  border-radius: var(--srve-radius-md);
  background: var(--srve-color-neutral-200);
  object-fit: cover;
  object-position: 50% 32%; /* portraits — keep faces in the strip */
}
.quote__who { display: flex; align-items: center; gap: var(--srve-space-3); margin-top: auto; }
.quote__name {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
  font-size: var(--srve-font-size-body-sm);
}
.quote__role { color: var(--srve-color-text-secondary); font-size: var(--srve-font-size-caption); }

/* ---- Final CTA band -------------------------------------------------- */
.cta-band {
  /* The footer's surface, so the closing band and the footer under it read as
     one landing. Theme-driven rather than a fixed fill, which is also what
     lets the type inside stay on the ordinary ink tokens. */
  background: var(--srve-color-bg-subtle);
  border-radius: 24px;
  padding: var(--srve-space-8);
  text-align: center;
  color: var(--srve-color-text-primary);
  /* Anchors the dither below and clips it to the 24px corner. */
  position: relative;
  overflow: hidden;
}
/* The same 4px checkerboard the video heroes and the image frames wear, turned
   over: theirs is anchored to the bottom edge and takes the block *into* the
   page below it, and this one is anchored to the top so the band comes *out of*
   the page above. Its lower edge is the one thing here that shouldn't dissolve —
   the band and the footer under it are meant to read as one landing, and a
   treatment along that seam would draw the line the shared surface exists to
   hide. So the mask runs to the top instead: solid checkerboard at the band's
   upper edge, clear by halfway down.

   Half the band's height, as on the heroes — at that depth it stops reading as
   an edge treatment and becomes the top of the block breaking into pixels. */
.cta-band::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 50%;
  z-index: 0;
  pointer-events: none;
  /* Checkerboard in the page colour, not the flat #000 the heroes use. Theirs
     sits over photography, where black is what a picture dissolves into; this
     one sits on a surface a step off the page, and what it has to dissolve into
     is the page. Black did read correctly in the dark theme — the page is nearly
     black there — but in the light one it painted the top third of the band and
     the headline in it. The token is right in both. */
  background: repeating-conic-gradient(var(--srve-color-bg-page) 0% 25%, transparent 0% 50%) 0 0 / 4px 4px;
  -webkit-mask-image: linear-gradient(to top, transparent, #000);
  mask-image: linear-gradient(to top, transparent, #000);
}
/* Type and buttons over the texture, not in it. */
.cta-band > * { position: relative; z-index: 1; }
.cta-band__title {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-size: var(--srve-font-size-display-lg);
  font-weight: var(--srve-font-weight-bold);
  line-height: 0.95; /* see .hero__title */
  letter-spacing: -0.01em;
  text-transform: uppercase;
}
.cta-band__sub {
  margin-top: var(--srve-space-4);
  font-size: var(--srve-font-size-body-lg);
  color: var(--srve-color-text-secondary);
}
.cta-band__row {
  margin-top: var(--srve-space-6);
  display: flex;
  gap: var(--srve-space-3);
  justify-content: center;
  flex-wrap: wrap;
}
/* (No button overrides: the band is an ordinary surface now, so the green
   primary and outlined secondary read the same here as anywhere else. The
   inverted white-on-colour pair the olive/green fills needed is gone.) */

/* ---- Footer ---------------------------------------------------------- */
.site-footer {
  padding-block: var(--srve-space-8) var(--srve-space-6);
  background: var(--srve-color-bg-subtle);
}
.footer-grid {
  display: grid;
  /* Brand column plus one per link list — three of those since Resources came
     out. A stale repeat(4) left an empty track hanging off the right. */
  grid-template-columns: 1.6fr repeat(3, 1fr);
  gap: var(--srve-space-6);
}
.footer-col h4 {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-size: var(--srve-font-size-caption);
  text-transform: uppercase;
  letter-spacing: 2px; /* matches the other display-face labels */
  /* Full-strength ink, so the column heads sit above their links rather than
     alongside them: near-black on light, near-white on dark. */
  color: var(--srve-color-text-primary);
  margin-bottom: var(--srve-space-4);
  font-weight: var(--srve-font-weight-bold);
}
/* 8px, not 12: the columns run to nine links, and the looser rhythm read as
   nine separate things rather than one list. */
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--srve-space-2); }
.footer-col li { margin: 0; }
.footer-col a {
  font-size: var(--srve-font-size-body-sm);
  color: var(--srve-color-text-secondary);
  text-decoration: none;
}
.footer-col a:hover { color: var(--srve-color-text-primary); }
.footer-about { max-width: 280px; color: var(--srve-color-text-secondary); font-size: var(--srve-font-size-body-sm); line-height: var(--srve-line-height-relaxed); }
/* Social row under the logo and blurb. A row, so it overrides the column
   direction the sibling link lists use. */
.footer-col .footer-social {
  flex-direction: row;
  gap: var(--srve-space-4);
  margin-top: var(--srve-space-5);
}
.footer-social a {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--srve-radius-full);
  color: var(--srve-color-text-secondary);
  transition:
    background-color var(--srve-duration-fast) var(--srve-ease-standard),
    color var(--srve-duration-fast) var(--srve-ease-standard);
}
.footer-social a:hover {
  background: var(--srve-color-neutral-100);
  color: var(--srve-color-text-primary);
}
.footer-social a:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}
.footer-social svg { width: 20px; height: 20px; }
/* ---- Social glyphs -----------------------------------------------------
   The shipped files in /assets/social are single-colour white artwork, so
   they're drawn as masks rather than images: the glyph takes currentColor
   and keeps following the link through theme changes and hover. */
.social-icon {
  display: block;
  width: 20px;
  height: 20px;
  background: currentColor;
  -webkit-mask: var(--social-icon) center / contain no-repeat;
  mask: var(--social-icon) center / contain no-repeat;
}
.social-icon--linkedin { --social-icon: url('/assets/social/linkedin.png'); }
.social-icon--threads { --social-icon: url('/assets/social/threads.svg'); }
.footer-bottom {
  margin-top: var(--srve-space-7);
  padding-top: var(--srve-space-5);
  display: flex;
  justify-content: space-between;
  gap: var(--srve-space-4);
  flex-wrap: wrap;
  color: var(--srve-color-text-secondary);
  font-size: var(--srve-font-size-caption);
}

/* ---- Responsive ------------------------------------------------------ */
@media (max-width: 1040px) {
  .site-nav { display: none; }
  /* Desktop only: below this width the bar is logo + theme toggle + hamburger,
     and the CTA belongs in the menu sheet's own flow rather than the bar. */
  .site-header__cta { display: none; }
}
@media (max-width: 900px) {
  :root { --section-pad: var(--srve-space-8); }
  .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; }
  /* Exception: card sets short enough to hold their own at half width — a
     title and a sentence, or just a label. Stacking six or eight of those
     one-up turns a glanceable index into a long scroll, so they go two-up
     instead: 2x2, 2x3, or 2x4 depending on the count. Marked with
     .grid--compact; every grid of .selector links qualifies by definition,
     since those are a label and an optional one-liner. Grids whose cards
     carry a full paragraph are left stacked — see the list at .grid--compact
     use sites. */
  .grid--compact,
  .grid:has(> .selector) {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--srve-space-4);
  }
  /* Half-width cards need the type to come down a step with them. One rule
     for every compact grid, so a 2x2 of features and a 2x4 of selectors read
     at the same size. */
  .grid--compact .feature,
  .grid--compact .selector,
  .grid:has(> .selector) .selector { padding: var(--srve-space-5); }
  .grid--compact .feature__title,
  .grid--compact .selector__label,
  .grid:has(> .selector) .selector__label {
    font-size: var(--srve-font-size-body-md);
  }
  .grid--compact .feature__body,
  .grid:has(> .selector) .feature__body {
    font-size: var(--srve-font-size-body-sm);
  }
  /* Stacked: photo on top, copy below — both already inside .loop-rows'
     container padding, so no extra edge insets needed. */
  .loop-row {
    grid-template-columns: 1fr;
    gap: var(--srve-space-6);
    min-height: 0;
  }
  .loop-row__media,
  .loop-row:nth-child(even) .loop-row__media { order: -1; }
  .loop-row__media { min-height: 56vw; }
  .loop-row__text,
  .loop-row:nth-child(even) .loop-row__text {
    max-width: none;
    justify-self: stretch;
  }
  /* The card stays on the photo here too — it's a 16/10 chip now, not the tall
     document that used to have to flow inline to clear the copy. Narrower and
     tighter to the edge so it leaves the shorter mobile photo some air. */
  .loop-row__media { display: flex; flex-direction: column; }
  .loop-row__photo { position: relative; inset: auto; aspect-ratio: 3 / 2; }
  .loop-row__float,
  .loop-row:nth-child(even) .loop-row__float {
    width: min(260px, calc(100% - 4 * var(--site-gutter)));
    /* Same 16px overhang past the photo, scaled to the shorter photo and the
       tighter card — but measured against the media box's padding edge, which
       the rule below has moved 16px below the picture. So: bottom:0 here, not
       the negative inset the desktop uses. */
    bottom: 0;
    /* In front of the copy that follows as well as the photo it sits on. The
       row is one column here, so the text block is a later sibling directly
       under the card's overhang rather than beside it. */
    z-index: 3;
  }
  /* The overhang needs somewhere to hang. The row gap is 32px at this width and
     the card used to drop 16 straight into it, which left the title reading as
     though it were labelling the card rather than the section. The media box
     carries the clearance as padding — inside itself, so the gap between the
     copy and the next row is untouched — and the card, positioned against that
     padding edge, keeps its overhang over the picture. */
  .loop-row__media { padding-bottom: var(--srve-space-4); }
  /* Held at 48px rather than following the desktop rule up to 64 — a 64px
     headline wraps to four or five lines on a phone and pushes the CTAs off
     the panel. Shared by every hero now, the home one included. */
  .hero__title { font-size: 3rem; /* 48px */ }
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 560px) {
  .footer-grid { grid-template-columns: 1fr; }
  .cta-band__title { font-size: var(--srve-font-size-heading-lg); }
}

/* ---- Mobile header & navigation --------------------------------------
   ≤1040px the desktop nav pill is hidden; site.js injects a hamburger
   that opens the same .site-nav markup as a full-screen sheet with the
   dropdown groups as accordions. The floating header also gains a glass
   bar so the logo stays legible over scrolled content. */
.nav-toggle { display: none; }

@media (max-width: 1040px) {
  /* (Glass bar now lives on the base .site-header::before rule.) */
  .nav-toggle {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
  }
  .nav-toggle__bar {
    width: 22px;
    height: 2px;
    border-radius: 1px;
    background: var(--srve-color-text-primary);
    transition:
      transform var(--srve-duration-base) var(--srve-ease-standard),
      opacity var(--srve-duration-base) var(--srve-ease-standard);
  }
  .nav-open .nav-toggle__bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  .nav-open .nav-toggle__bar:nth-child(2) { opacity: 0; }
  .nav-open .nav-toggle__bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

  /* The menu sheet — same markup, restyled from the hidden desktop pill.
     It covers the viewport edge to edge, top included, and the top padding
     keeps the first link clear of the header row.

     The header's own z-index:50 is no help here: .site-nav is a *child* of
     .site-header, so it sits inside that stacking context and its z-index:40
     is compared against its siblings, not against the header. Without the
     rule below the sheet paints over the logo and — worse — over the close
     button, leaving no way to shut the menu. */
  .nav-open .site-nav {
    display: flex;
    position: fixed;
    inset: 0;
    margin: 0;
    width: auto;
    height: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: calc(var(--site-header-h) + var(--srve-space-2)) var(--site-gutter) var(--srve-space-8);
    overflow-y: auto;
    background: var(--srve-color-bg-page);
    z-index: 40;
  }
  .nav-open .site-nav::before { display: none; } /* no pill glass */
  /* Lift the header row's own contents back above the sheet (see above). */
  .nav-open .site-logo,
  .nav-open .site-header__actions {
    position: relative;
    z-index: 41;
  }

  /* The bar is now floating on the sheet rather than on page content, so its
     glass and bottom edge drop out and the two read as one surface. */
  .nav-open .site-header::before {
    opacity: 0;
    border-bottom-color: transparent;
  }
  /* ...and the reversed-on-photo chrome un-reverses: the hero it was reading
     against is covered by the sheet, so white-on-white would disappear. */
  .nav-open .site-header .site-logo { color: var(--srve-color-text-primary); }
  .nav-open .site-header .site-logo__img { filter: none; }
  .nav-open .site-header .nav-toggle__bar { background: var(--srve-color-text-primary); }
  .nav-open .site-header .theme-toggle { color: var(--srve-color-text-secondary); }
  /* The logo mark is flat dark artwork, so in dark mode it still has to be
     reversed — this outranks the `filter: none` above. */
  :root[data-theme="dark"] .nav-open .site-header .site-logo__img {
    filter: brightness(0) invert(1);
  }
  .nav-open .site-nav .nav-drop {
    display: block;
    position: static;
  }
  .nav-open .site-nav > a,
  .nav-open .site-nav .nav-drop__trigger {
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: space-between;
    padding: var(--srve-space-4) 0;
    font-size: var(--srve-font-size-heading-sm);
    font-weight: var(--srve-font-weight-semibold);
    color: var(--srve-color-text-primary);
    border-bottom: 1px solid var(--srve-color-neutral-100);
  }
  .nav-open .site-nav .nav-drop__caret {
    width: 16px;
    height: 16px;
  }
  .nav-open .nav-drop--m-open .nav-drop__caret { transform: rotate(180deg); }

  /* Dropdown panels become inline accordions */
  .nav-open .site-nav .mega {
    position: static;
    transform: none;
    width: auto;
    box-shadow: none;
    border-radius: 0;
    opacity: 1;
    visibility: visible;
    transition: none;
    display: none;
    background: none;
  }
  .nav-open .site-nav .nav-drop--m-open .mega { display: block; }
  /* The desktop panel opens by unclipping itself and fading its grid in, both
     keyed to .nav-drop--open. The accordion is keyed to .nav-drop--m-open, so
     neither ever fired here: the panel stayed clipped to zero height with a
     fully transparent grid inside it, and the nine solution links never
     appeared. Reset both — the accordion does its own showing via display. */
  .nav-open .site-nav .mega--grid {
    transform: none;
    clip-path: none;
  }
  .nav-open .site-nav .mega--grid .mega__grid {
    grid-template-columns: 1fr;
    gap: var(--srve-space-3);
    padding: var(--srve-space-4) 0;
    opacity: 1;
  }
  /* Unboxed in the sheet. On the desktop panel the border is what separates
     nine cards laid out in three columns over a floating surface; stacked in a
     full-screen menu they're a list, and nine boxes down a phone read as nine
     buttons competing with the three nav items above them. The rows keep their
     own rhythm from the grid gap instead.
     Indented rather than flush: with no box around them, the step in from the
     left margin is the only thing left saying these nine sit *under*
     "Solutions" rather than beside "Platform" and "Pricing". */
  .nav-open .site-nav .mega-card {
    border: 0;
    border-radius: 0;
    background: none;
    padding: var(--srve-space-2) var(--srve-space-6) var(--srve-space-2) var(--srve-space-5);
  }
  /* The fill was the box lighting up; with no box it shows as a grey band down
     the sheet. Focus keeps its ring — that comes from the ordinary
     :focus-visible outline, not from this. */
  .nav-open .site-nav .mega-card:hover,
  .nav-open .site-nav .mega-card:focus-visible {
    background: none;
  }
  /* The arrow sat on the card's inner right margin. That margin was the box's;
     without it the glyph belongs on the sheet's own edge. */
  .nav-open .site-nav .mega-card__arrow { right: 0; }
  /* Titles onto Manrope, as the card titles across the rest of the site already
     are: the heavy semi-condensed display face reads as texture rather than as
     headings once there are nine of them in a column. The uppercase and the
     open tracking go with it — both are settings for that face, and Manrope in
     caps at 16px would read as a row of labels rather than links. */
  .nav-open .site-nav .mega-card__title {
    font-family: var(--srve-font-family-ui);
    font-weight: var(--srve-font-weight-bold);
    letter-spacing: -0.01em;
    text-transform: none;
  }
}

/* ---- Theme toggle ----------------------------------------------------
   Injected into .site-header__actions by site.js. Two glyphs share one
   button; CSS picks which is visible from the <html data-theme> value, so
   the icon can never drift out of step with the theme it reports. */
.theme-toggle {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--srve-radius-full);
  background: none;
  color: var(--srve-color-text-secondary);
  cursor: pointer;
  transition:
    background-color var(--srve-duration-fast) var(--srve-ease-standard),
    color var(--srve-duration-fast) var(--srve-ease-standard);
}
.theme-toggle:hover {
  background: var(--srve-color-neutral-100);
  color: var(--srve-color-text-primary);
}
.theme-toggle:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}
.theme-toggle svg { width: 20px; height: 20px; }
/* Show the glyph for the theme you'd switch *to*. */
.theme-toggle__moon { display: none; }
:root[data-theme="dark"] .theme-toggle__moon { display: block; }
:root[data-theme="dark"] .theme-toggle__sun { display: none; }
/* Over the hero photo the chrome reverses, same as the logo and hamburger. */
.site-header--over-hero .theme-toggle { color: #ffffff; }
.site-header--over-hero .theme-toggle:hover { background: rgb(255 255 255 / 0.16); }
.site-header--over-hero:has(.nav-drop--open) .theme-toggle {
  color: var(--srve-color-text-secondary);
}
.site-header--over-hero:has(.nav-drop--open) .theme-toggle:hover {
  background: var(--srve-color-neutral-100);
  color: var(--srve-color-text-primary);
}
@media (prefers-reduced-motion: reduce) {
  .theme-toggle { transition: none; }
}

/* ---- Dark theme, marketing surfaces ----------------------------------
   Everything else follows from the design-system tokens; these are the four
   compositions this file paints itself (see the :root block at the top).
   The photo sections (hero, USPCA band) and the navy tour are dark-on-dark
   already and carry over unchanged. */
:root[data-theme="dark"] {
  /* A heavier film: the hero's reversed white copy needs more separation
     from the photo once the surrounding page is dark too. */
  --hero-film: rgb(6 16 12 / 0.48);
  --srve-illustration-ink: #303836;
}
/* The logo mark is flat #111827 artwork, so it has to be reversed rather than
   recoloured. The second selector re-reverses it for the one case light mode
   explicitly *un*-reverses — an open dropdown, which turns the bar solid — and
   outranks that rule on specificity. */
:root[data-theme="dark"] .site-logo__img,
:root[data-theme="dark"] .site-header--over-hero:has(.nav-drop--open) .site-logo__img {
  filter: brightness(0) invert(1);
}



/* ---- Integrations ----------------------------------------------------
   Centred copy with the tiles scattered around it. Every tile is pinned to
   the middle of the stage and pushed out by its own --dx/--dy, so the same
   two numbers give both the resting position and the vector it travels on
   the way out — nothing to keep in sync between layout and motion.

   --dx is in cqw, a share of the stage's own width — and the stage is a
   .site-container, so the scatter is bounded by the same gutters as the copy
   instead of running to the viewport edges. --dy is in px, so the stage's
   height stays predictable no matter how wide the window is. */
.integrations__stage {
  position: relative;
  /* Room for the outermost --dy (±238px) plus a tile half-height, so the
     scatter never spills into the sections above and below. */
  min-height: 620px;
  display: grid;
  place-items: center;
  /* What cqw resolves against. */
  container-type: inline-size;
}
.integrations__intro {
  position: relative;
  z-index: 1;
  max-width: 900px;
  text-align: center;
}
/* (No font-size override here any more — the clamp this section used to own
   is now the shared .section__title rule.) */
.integrations__intro .section__intro {
  max-width: 52ch;
  margin-inline: auto;
}
.integrations__intro .srve-button { margin-top: var(--srve-space-6); }

.integrations__grid {
  --tile: 72px;
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  inset: 0;
}
/* Pinned to the centre of the stage; --dx/--dy carry it out from there. The
   margin (not a translate) does the centring, which leaves the transform
   free for the throw.

   Two elements, two jobs: the <li> owns the throw (a long, one-shot
   transition), the inner tile owns the parallax drift (retargeted every
   frame). Sharing one transform would make the drift inherit the throw's
   1.2s easing and smear behind the scroll. */
.integration {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--tile);
  height: var(--tile);
  margin: calc(var(--tile) / -2);
  transform: translate(var(--dx), var(--dy)) scale(var(--s, 1));
}
/* --scroll is set on the list by site.js: a signed pixel offset for the
   section's progress through the viewport. --p is the tile's rate — negative
   values drift against the scroll, so the field spreads and gathers rather
   than sliding as one sheet. */
.integration__tile {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  border-radius: var(--srve-radius-lg);
  /* --tile-bg is set per tile in the markup to the mark's own background, so
     an icon that carries a coloured square (Yelp red, Linktree green) doesn't
     sit in a white frame. Marks with a transparent background fall back to
     white in both themes — they're drawn for a light surface, and a dark chip
     behind them reads as a mistake. */
  background: var(--tile-bg, #fff);
  border: 1px solid var(--tile-border, var(--srve-color-border-surface));
  box-shadow: var(--srve-shadow-sm);
  transform: translateY(calc(var(--scroll, 0px) * var(--p, 1)));
  will-change: transform;
}
/* Transparent marks sit inset on the chip… */
.integration__tile img {
  width: 62%;
  height: 62%;
  object-fit: contain;
}
/* …while a mark that is its own square runs to the edges, with the hairline
   dropped so nothing outlines the colour. */
.integration__tile--bleed {
  overflow: hidden;
  --tile-border: transparent;
  /* Matches the rx=32 on the 128-wide marks, so the tile's corner sits exactly
     under the icon's own. A fixed radius left a crescent of --tile-bg showing
     past each corner — white against Instagram's pink edge, for instance. */
  border-radius: 25%;
}
.integration__tile--bleed img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scroll reveal — the tiles are thrown out from the centre as the section
   arrives, each on its own clock so the group scatters loose rather than as
   one block. The pacing is in CSS (a duration + delay per tile); site.js only
   flips .is-revealed once, so re-timing never means touching the script.

   Gated on html.js like the showcase card, so a no-JS visitor gets the tiles
   in place rather than permanently invisible. */
.js .integrations__grid .integration {
  opacity: 0;
  /* Start stacked in the middle, small — the resting rule above is what they
     transition to. */
  transform: translate(0, 0) scale(0.35);
  transition:
    opacity var(--rise-dur) var(--rise-ease) var(--rise-delay),
    transform var(--rise-dur) var(--rise-ease) var(--rise-delay);
  /* Long, and decelerating hard at the end — the tiles coast into place. */
  --rise-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --rise-dur: 1.2s;
  --rise-delay: 0s;
}
/* Different paces: no two tiles share both a duration and a delay, and the
   order is deliberately not clockwise or column-by-column. */
.js .integrations__grid .integration:nth-child(1) { --rise-dur: 1.1s;  --rise-delay: 0.12s; }
.js .integrations__grid .integration:nth-child(2) { --rise-dur: 1.6s;  --rise-delay: 0s;    }
.js .integrations__grid .integration:nth-child(3) { --rise-dur: 1.3s;  --rise-delay: 0.28s; }
.js .integrations__grid .integration:nth-child(4) { --rise-dur: 1.8s;  --rise-delay: 0.08s; }
.js .integrations__grid .integration:nth-child(5) { --rise-dur: 1.45s; --rise-delay: 0.2s;  }
.js .integrations__grid .integration:nth-child(6) { --rise-dur: 1.25s; --rise-delay: 0.34s; }

.js .integrations__grid .integration.is-revealed {
  opacity: 1;
  transform: translate(var(--dx), var(--dy)) scale(var(--s, 1));
}
@media (prefers-reduced-motion: reduce) {
  .js .integrations__grid .integration,
  .js .integrations__grid .integration.is-revealed {
    opacity: 1;
    transform: translate(var(--dx), var(--dy)) scale(var(--s, 1));
    transition: none;
  }
  /* site.js stops publishing --scroll here too; this is the belt to that
     braces, so a tile can never sit at a stale offset. */
  .integration__tile { transform: none; }
}

/* Narrow: there is no room to scatter around the copy without landing on top
   of it, so the field collapses into a pile beneath it — the tiles overlapping
   in one leaning heap rather than a tidy wrapped row. The pile keeps the
   section's "a stack of the places you show up" read at a width where the
   scatter can't survive, and it fits six marks in the space a row fits four. */
@media (max-width: 900px) {
  /* Stacked, the pile drops past where the section's bottom padding would hold
     it and runs up to the edge of the tint, stopping --edge-clear short of it.
     The padding comes back as a margin, outside the band, so the gap to the
     next section is the same as it ever was. Narrow widths only — the desktop
     scatter keeps its ordinary padding. */
  .integrations {
    --edge-clear: 14px;
    padding-bottom: 0;
    margin-bottom: var(--section-pad);
  }
  .integrations__stage {
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  /* The list comes first in the markup so it can be painted around the copy on
     desktop; stacked, it belongs underneath it. */
  .integrations__grid {
    order: 1;
    position: static;
    /* Smaller chips than the desktop scatter — six of them overlap into one
       object, so each needs less room to read. */
    --tile: 68px;
    /* How much of each tile the next one covers. Held at a third: enough to
       read as one heap, little enough that every mark is still identifiable. */
    --overlap: 0.3;
    display: flex;
    justify-content: center;
    gap: 0;
    /* Nudged down another 32px off the CTA, so the pile sits away from the copy
       and closer to the band's edge. */
    margin-top: calc(var(--srve-space-7) + 32px);
    /* The pile is the last thing in the stage and the band has no bottom
       padding here, so this is the whole of the space beneath it. The rotated
       corners hang a few px past the flat box, hence the extra. */
    margin-bottom: calc(var(--edge-clear) + 8px);
  }
  .integrations__grid .integration,
  .js .integrations__grid .integration,
  .js .integrations__grid .integration.is-revealed {
    position: static;
    opacity: 1;
    margin: 0 0 0 calc(var(--tile) * var(--overlap) * -1);
    /* --tilt/--lift are set per tile below; --s rides along from the markup so
       the pile keeps the same size variance the scatter has. */
    transform: rotate(var(--tilt, 0deg)) translateY(var(--lift, 0px)) scale(var(--s, 1));
    transition: none;
  }
  .integrations__grid .integration:first-child { margin-left: 0; }
  /* Alternating lean and a little lift, so the heap looks dropped rather than
     dealt. The z-order rises to the middle and falls away again — that's what
     makes it read as a pile with a top rather than a shingled row. */
  .integrations__grid .integration:nth-child(1) { --tilt: -9deg;  --lift: 4px;  z-index: 1; }
  .integrations__grid .integration:nth-child(2) { --tilt: 5deg;   --lift: -3px; z-index: 2; }
  .integrations__grid .integration:nth-child(3) { --tilt: -4deg;  --lift: 6px;  z-index: 3; }
  .integrations__grid .integration:nth-child(4) { --tilt: 8deg;   --lift: -5px; z-index: 6; }
  .integrations__grid .integration:nth-child(5) { --tilt: -6deg;  --lift: 2px;  z-index: 5; }
  .integrations__grid .integration:nth-child(6) { --tilt: 3deg;   --lift: -6px; z-index: 4; }
  /* Overlapping chips need an edge to separate them; the desktop hairline is
     transparent on the bleed tiles, so the shadow does that work. */
  .integration__tile {
    /* A pile has nothing to parallax against — the drift would only shear the
       tiles out of their overlap. */
    transform: none;
    box-shadow: var(--srve-shadow-md, var(--srve-shadow-sm));
  }
}
/* Small phones: the pile leans in further so it clears the gutters. */
@media (max-width: 360px) {
  .integrations__grid {
    --tile: 58px;
    --overlap: 0.38;
  }
}

/* The bar used to float lower over the hero (24px of padding-top on the inner)
   and ease back up as the glass bar arrived. That offset is gone: the contents
   sit on the same line in both states, so crossing from the transparent bar to
   the fixed one changes only the surface behind them — nothing moves. Only the
   flex children took the padding anyway; the nav pill centres off the padding
   box and stayed put, which is what split the two apart mid-transition. */

/* ---- Hero press bar ---------------------------------------------------
   Last child of .hero-panel (not of the copy column), so it spans the panel
   and centres on the panel's own axis rather than the copy's. The panel is a
   column flexbox with justify-content:flex-end, which parks it on the bottom
   edge; align-self:stretch overrides the panel's align-items:flex-start so it
   gets the full width to centre within. */
.hero-panel__press {
  position: relative; /* above the film, same as the copy */
  z-index: 2;
  align-self: stretch;
  margin-top: 0; /* .logo-strip--reversed's top margin was for the copy stack */
  padding: 0 var(--site-gutter) var(--srve-space-7);
  /* Matches the copy's entrance, one beat after the CTAs. */
  animation: heroRise 0.7s var(--srve-ease-standard) 0.65s both;
}
.hero-panel__press .logo-strip__label { text-align: center; }
.hero-panel__press .logo-strip__list {
  justify-content: center;
  /* The marks are centred as a row now, so they no longer hug a left edge. */
  margin-inline: auto;
}
.hero-panel__press .logo-strip__logo { object-position: center; }

@media (prefers-reduced-motion: reduce) {
  .hero-panel__press { animation: none; }
}

/* ---- Illustrations ----------------------------------------------------
   Line-art SVGs are authored as dark drawings on an opaque white ground.
   This class throws that ground away and redraws the artwork in a single
   themed ink, so one asset works on both themes and no file needs editing
   before it goes in. The filter itself is injected by site.js — see the note
   there for how the luminance mask works.

   Usage: <img class="illustration" src="assets/illustrations/whatever.svg" alt=""> */
.illustration {
  filter: url(#illustration-ink);
}
/* The flood colour lives in CSS rather than on the SVG so it follows the
   theme; the primitive inherits the custom property from :root. */
#illustration-ink-flood {
  flood-color: var(--srve-illustration-ink);
}

/* ---- About page -------------------------------------------------------
   Copy beside a portrait-ish image, and the team row. Both are page-local
   compositions — nothing else on the site needs them. */
.about-split {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--srve-space-8);
  align-items: start;
}
/* The paragraphs stack as .section__intro, which carries its own top margin;
   the head's bottom margin would double it. */
.about-split__copy .section__intro + .section__intro { margin-top: var(--srve-space-5); }
.about-split__shot {
  aspect-ratio: 4 / 5;
  border-radius: var(--srve-radius-md);
  overflow: hidden;
}
@media (max-width: 900px) {
  .about-split { grid-template-columns: 1fr; gap: var(--srve-space-6); }
  /* Landscape once it's full width — a 4/5 frame that wide is a wall. */
  .about-split__shot { aspect-ratio: 3 / 2; }
}

.team-card__shot {
  aspect-ratio: 1 / 1;
  border-radius: 0; /* square corners on portraits */
  overflow: hidden;
  margin-bottom: var(--srve-space-4);
}
.team-card__name {
  font-family: 'Neue Reman Sans', var(--srve-font-family-ui);
  font-size: var(--srve-font-size-heading-sm);
  font-weight: var(--srve-font-weight-bold);
  color: var(--srve-color-text-primary);
}
.team-card__role {
  margin-top: var(--srve-space-1);
  font-size: var(--srve-font-size-body-sm);
  color: var(--srve-color-text-secondary);
}
/* Name and role sit left, the LinkedIn link is pushed out to the card's right
   margin and centred against the two lines it belongs to. */
.team-card__lockup {
  display: flex;
  align-items: center;
  gap: var(--srve-space-3);
}
.team-card__linkedin {
  margin-left: auto;
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--srve-radius-full);
  color: var(--srve-color-text-secondary);
  transition:
    background-color var(--srve-duration-fast) var(--srve-ease-standard),
    color var(--srve-duration-fast) var(--srve-ease-standard);
}
.team-card__linkedin:hover {
  background: var(--srve-color-neutral-100);
  color: var(--srve-color-text-primary);
}
.team-card__linkedin:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}
.team-card__linkedin svg { width: 20px; height: 20px; }

/* ---- FAQ ---------------------------------------------------------------
   Native <details>, so every answer is readable (and findable with the
   browser's own find-in-page) before any script runs. */
.faq-group {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: var(--srve-space-8);
  align-items: start;
}
/* The category title stays beside its questions on the way down the page. */
.faq-group__title { position: sticky; top: calc(var(--site-header-h) + var(--srve-space-6)); }
.faq-item {
  border-bottom: 1px solid var(--srve-color-border-surface);
}
.faq-item:first-child { border-top: 1px solid var(--srve-color-border-surface); }
.faq-item__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--srve-space-5);
  padding: var(--srve-space-5) 0;
  cursor: pointer;
  list-style: none; /* Firefox */
  font-family: var(--srve-font-family-ui);
  font-size: var(--srve-font-size-body-lg);
  font-weight: var(--srve-font-weight-semibold);
  line-height: var(--srve-line-height-snug);
  color: var(--srve-color-text-primary);
}
/* Safari draws its own triangle without this. */
.faq-item__q::-webkit-details-marker { display: none; }
.faq-item__q:focus-visible {
  outline: 2px solid var(--srve-color-brand-text);
  outline-offset: 2px;
}
.faq-item__caret {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--srve-color-text-secondary);
  transition: transform var(--srve-duration-base) var(--srve-ease-standard);
}
.faq-item[open] .faq-item__caret { transform: rotate(180deg); }
.faq-item__a {
  padding-bottom: var(--srve-space-5);
  max-width: 68ch;
  font-size: var(--srve-font-size-body-md);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}
@media (max-width: 900px) {
  .faq-group { grid-template-columns: 1fr; gap: var(--srve-space-5); }
  /* Nothing to stick to once it's stacked above its own list. */
  .faq-group__title { position: static; }
}
@media (prefers-reduced-motion: reduce) {
  .faq-item__caret { transition: none; }
}
.team-card__bio {
  margin-top: var(--srve-space-3);
  font-size: var(--srve-font-size-body-md);
  line-height: var(--srve-line-height-relaxed);
  color: var(--srve-color-text-secondary);
}

/* ---- Phone type scale --------------------------------------------------
   The display headings step down on a phone — the hero from 64 to 48, the
   section titles from 48 to 32 — but the Manrope beside them was pinned at
   its desktop size, so card titles and body copy crept up on the headings
   they're meant to sit under. These bring the UI font down by the same sort
   of ratio, which restores the gap between the two. */
@media (max-width: 640px) {
  /* Card titles: 32 → 24, so they stay a step below a 32px section title
     rather than matching it. */
  .tour__title,
  .loop-row__title { font-size: var(--srve-font-size-heading-md); }
  /* Lead-in and body copy: 18 → 16. */
  .hero__sub,
  .section__intro,
  .tour__body,
  .loop-row__body,
  .quote__text,
  .cta-band__sub,
  .faq-item__q { font-size: var(--srve-font-size-body-md); }
}

/* ---- Tour card lock-ups ------------------------------------------------
   The copy holds the top-left of every card; the placeholder art sits in the
   open space beside it. Four arrangements cycle down the six cards so the
   track doesn't read as the same slide over and over.

   Each is one large plate with one or two smaller tiles overlapping it and
   breaking past its edges — the offset is the point, so the tiles are placed
   against the media box rather than flowed, and the plate is inset to leave
   them somewhere to hang. */
.tour__media {
  position: relative;
  aspect-ratio: auto;
  align-self: stretch;
  min-height: 0;
}
.tour__shot {
  position: absolute;
  /* The tiles sit over the plate. */
  z-index: 1;
  /* .img-placeholder pins width and height to 100%, which on an absolutely
     positioned box beats the opposing inset — a plate set to left:26%; right:0
     took the media box's full width and hung 26% off its right edge instead of
     fitting between the two. The slots below are drawn with insets, so sizing
     goes back to them; the ones that want an explicit width or height say so. */
  width: auto;
  height: auto;
}
.tour__shot--main {
  z-index: 0;
  /* Only the plate is labelled — the tiles are silent, as in the mockups. */
}
/* A filled slot is sized by its picture, not the other way round. The shared
   .img-placeholder rule stretches an image over the box and crops the overflow
   away with object-fit; here the image flows instead, so the slot's height
   follows the artwork and nothing is cut. The arrangements below give each slot
   a width and a single vertical anchor — a main also carries a bottom inset,
   for the empty placeholder state, which has nothing left to do once a picture
   is setting the height. */
.tour__shot > img {
  position: static;
  width: 100%;
  height: auto;
  display: block;
}
/* Scoped through .tour__media only to outrank the arrangement rules further
   down, which set an explicit height on the tiles and a bottom on the plates. */
.tour__media .tour__shot:has(> img) { height: auto; }
.tour__media .tour__shot--main:has(> img) { bottom: auto; }
/* The same 8px band of glass the video hero wraps around its app shot: the
   padding is the band, and the translucent fill over a blur is what makes it
   read as glass rather than a flat white rule. Closed on all four sides here —
   these plates sit inside the card instead of running off its edge, so there's
   no boundary for the frame to open against. Outer radius is the inner 4px plus
   the 8px band, so the two curves stay concentric. */
.tour__shot--glass {
  padding: 8px;
  border-radius: 12px;
  border: 1px solid rgb(255 255 255 / 0.22);
  background: rgb(255 255 255 / 0.12);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
/* The band is the padding the flowed image sits inside, so all the image owes
   it is the inner corner. */
.tour__shot--glass > img { border-radius: 4px; }
.tour__shot--2,
.tour__shot--3 {
  /* A step darker than the plate, so the overlap reads as two surfaces. */
  --placeholder-fill: var(--srve-color-neutral-400);
  --placeholder-ink: var(--srve-color-bg-page);
}
/* Once a real photo fills a tile the fill above stops doing its job — a bright
   shot over a bright plate loses the seam. A lift does the separating instead,
   so the tile still reads as a card hanging off the plate. */
.tour__shot--glass.tour__shot--2,
.tour__shot--glass.tour__shot--3 { box-shadow: var(--srve-shadow-lg); }

/* A tile with no band at all. The glass frame is right for a screenshot, which
   is a picture of an interface and wants an edge drawn around it; on a portrait
   it read as a border ruled around someone's face. The lift and the corner the
   band used to give the photo move onto the tile itself, so it still sits as a
   card over the plate rather than as a hole cut in it — the seam the fill above
   talks about still has to come from somewhere. */
.tour__shot--plain {
  border-radius: 8px;
  overflow: hidden;
  box-shadow: var(--srve-shadow-lg);
}

/* Some slots are filled by artwork that is already a card — a UI screen exported
   as SVG, carrying its own white ground, radius, and hairline. Both the
   placeholder fill and the glass band would double what the file already draws,
   so the slot gives way to the art instead: no fill and no band. The lift is a
   drop-shadow rather than a box-shadow so it traces the card's own rounded
   corners instead of the square div behind them. */
.tour__shot--card { background: none; }
.tour__shot--card > img { filter: drop-shadow(0 10px 28px rgb(0 0 0 / 0.28)); }

/* A bare photo plate takes the card's own 8px corner, so a picture never meets
   the panel with a square edge. Only the plates that carry nothing else: a
   glass one already rounds to 12 around its band, and a card one is artwork
   that draws its own corner — rounding either here would cut a second curve
   inside the first. `overflow` is what does the cutting, since the picture is
   the thing being rounded, not the box. */
.tour__shot--main:not(.tour__shot--glass):not(.tour__shot--card) {
  border-radius: 8px;
  overflow: hidden;
}

/* …except where a card slot is wanted framed after all, in the same 8px band of
   glass the plates and the platform pull-outs wear. Written against both
   classes so it outranks the no-band rule above whatever the order, and the
   image rule set after that one so the drop-shadow can be cleared: inside a
   band a shadow on the artwork reads as the card floating in its own frame.
   The lift moves to the tile, where it traces the band instead. */
.tour__shot--card.tour__shot--framed {
  padding: 8px;
  border-radius: 12px;
  border: 1px solid rgb(255 255 255 / 0.22);
  background: rgb(255 255 255 / 0.12);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: var(--srve-shadow-lg);
}
.tour__shot--framed > img {
  border-radius: 4px;
  filter: none;
}

/* a — wide plate, one tile off its lower-left */
.tour__media--a .tour__shot--main { top: 4%; right: 0; left: 14%; bottom: 18%; }
/* Open on the right, the way the video hero's frame opens at the bottom: the
   band and its hairline wrap three sides and the screen runs off the card's
   edge rather than closing against it. The inset above already puts the plate
   at the edge — it was the 8px band and its 1px rule that held the picture
   short of it. Corners follow: square where the screen meets the edge. */
.tour__media--a .tour__shot--main.tour__shot--glass {
  padding-right: 0;
  border-right: 0;
  border-radius: 12px 0 0 12px;
}
.tour__media--a .tour__shot--main.tour__shot--glass > img {
  border-radius: 4px 0 0 4px;
}
.tour__media--a .tour__shot--2 { left: 0; bottom: 4%; width: 26%; height: 46%; }

/* b — full-width plate with a single card centred across its lower edge. The
   card is pulled back by half its own width rather than given equal left and
   right insets, so it stays centred at whatever width the artwork asks for.
   `transform` is free on a tile — only the plate spends it, on the centring
   above — and `translate` is left alone for the reveal. */
.tour__media--b .tour__shot--main { top: 0; left: 22%; right: 22%; bottom: 6%; }
.tour__media--b .tour__shot--2 {
  left: 50%;
  transform: translateX(-50%);
  top: 58%;
  width: 62%;
}

/* c — plate to the right, one large tile over its left edge */
.tour__media--c .tour__shot--main { top: 2%; right: 0; left: 22%; bottom: 6%; }
.tour__media--c .tour__shot--2 { left: 0; top: 22%; width: 52%; height: 52%; }

/* d — full-width plate with a card centred across its lower edge. The plate ran
   at 60% when it carried two small tiles in opposite corners; the card that
   replaced them is 78% wide, so the plate had to grow past it — an overlay
   narrower than the thing it sits on is the point of the arrangement.

   This plate keeps both vertical insets, unlike the others. Everywhere else the
   artwork sets the height and the box follows it; here the photo is a 2:3
   upright, and at the column's full width it would stand half again as tall as
   the panel and run out past the bottom edge. So the box is pinned to the
   column instead and the picture is fitted into it — which also means the plate
   is centred in the panel by construction, being exactly as tall as it.

   The :has() selector is repeated because the rule that clears `bottom` for a
   filled plate carries it too, and would otherwise outrank a plain pair of
   class names. */
.tour__media--d .tour__shot--main,
.tour__media--d .tour__shot--main:has(> img) {
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
/* Back to cover-and-crop, which is what .img-placeholder does for a photo and
   what .tour__shot > img turns off so artwork can set its own height. Anchored
   above centre: the hand at the peppers is in the upper third, so the crop is
   spent on the basket rather than on the subject. */
.tour__media--d .tour__shot--main > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 38%;
}
/* The card sits centred on the plate both ways. Written out here rather than
   borrowed from --footer: that modifier means "across the lower edge", which is
   still what the slide beside this one wants. */
.tour__media--d .tour__shot--2 {
  left: 50%;
  right: auto;
  top: 50%;
  bottom: auto;
  transform: translate(-50%, -50%);
  width: 78%;
  height: auto;
}

/* e — plate to the right, one tile centred against its left edge. The plate is
   inset the same top and bottom so its midline and the media box's are the
   same, and the tile reads as centred on the picture either way you measure it.
   translateY here, not translate: the entrance animation owns that property. */
.tour__media--e .tour__shot--main { top: 6%; right: 0; left: 26%; bottom: 6%; }
.tour__media--e .tour__shot--2 {
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  /* The widest of the tile slots, and the same share the review card opposite
     takes. 46% was sized for a tile hung off a plate; this column holds nothing
     but the dish picker now, and at that share it read as a thumbnail of a
     screen rather than the screen itself. */
  width: 62%;
  height: 58%;
}

/* f — two cards, the smaller one hung off the larger's lower-left. Neither gets
   a bottom inset: these are .tour__shot--card slots, so the artwork sets the
   height and an inset would fight it. */
.tour__media--f .tour__shot--main { top: 16%; left: 12%; right: 0; }
.tour__media--f .tour__shot--2 { left: 0; bottom: 12%; width: 56%; }

/* Full-bleed plate: the photo runs the whole width of the media column instead
   of being inset to leave a margin for the tiles beside it, and sits centred in
   the column's height rather than hanging from one edge. The tiles keep their
   positions and simply overlap a wider plate.

   A modifier rather than an edit to the arrangement: --a is worn by two slides,
   and only one of them bleeds. It has to sit after the arrangements above —
   same specificity, so source order is what carries it.

   Centred with `transform`, not `translate`: the reveal further down already
   spends `translate` on the plate's travel, and it animates to `0 0`, which
   would wipe a centring written there the moment the slide became active.
   Anchoring at 50% and pulling back half the plate's own height is what keeps
   this true whatever height the artwork turns out to be. */
.tour__media--bleed .tour__shot--main {
  left: 0;
  right: 0;
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
}

/* A card centred across the plate's lower edge. This lives on the tile rather
   than in an arrangement because the slide that wants it wears --a, which is
   shared with a slide that wants its tile in the lower-left corner instead.
   Scoped through .tour__media and set after the arrangements for that reason:
   one class each is a specificity tie against them, so order is what carries
   it. `transform` is free on a tile — the plate is the only thing spending it,
   on its own centring — and `translate` is left to the reveal. */
.tour__media .tour__shot--footer {
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  top: auto;
  bottom: 5%;
  /* A quarter wider than the tile slots elsewhere — the same step the platform
     pull-outs take. The card is a wide details panel, and at the narrower share
     its three columns were reading small against the title beside them. */
  width: 78%;
  height: auto;
}

/* A column left holding one tile centres it, both ways. The arrangements above
   place a tile against a plate — an edge to hang off, a corner to sit in — and
   with the plate gone there is nothing left for them to be relative to; what is
   left is a card alone in half a card, which belongs in the middle of it.

   Keyed on being the only shot rather than listed per slide, so a column goes
   back to its arrangement the moment a second one joins it. `:only-child` is a
   third class's worth of specificity, which is what carries this past the
   two-class arrangement rules whatever their order. Widths are left alone —
   each card still asks for its own. */
.tour__media > .tour__shot:only-child:not(.tour__shot--main) {
  top: 50%;
  left: 50%;
  right: auto;
  bottom: auto;
  transform: translate(-50%, -50%);
}
/* A plate left on its own is the other case, and it does not want the rule
   above. A tile with nothing to hang off has lost its whole reason for sitting
   where it sits; a plate still has its own — the arrangements run it off the
   card's right edge, which is what keeps the slide moving sideways. So it keeps
   its horizontal placement and only takes the vertical centring, the same way
   --bleed does. `transform` for the pull-back, not `translate`: the entrance
   animation owns that property and animates it to 0. */
.tour__media > .tour__shot--main:only-child {
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
}

@media (max-width: 1023px) {
  /* One column, and a short one. The tiles used to be dropped here, back when
     every column had a plate underneath and a tile was the second thing in it —
     on a ~271px card the smaller ones came out too small to read. Four columns
     hold nothing but a tile now, so dropping them empties those slides; they
     stay, and the rule below flows them instead.

     A column that still has a plate keeps its overlap, which is the one case
     this used to avoid. Those two tiles are a portrait and a card, both large
     enough at this width to carry it. */

  /* No ratio, and no intrinsic floor. This column used to hold a 16/10 frame
     here — a shape for an empty placeholder to stand in. It cannot stay: a grid
     item carrying an aspect ratio with an auto width is not stretched to its
     track, its width is *transferred from its height*, and the height here comes
     from the row, which the panel's flex height stretches. On the four columns
     with no plate to clear the ratio that worked out to 318–424px inside a 271px
     card, and min-width:auto floored the track with it, so the copy overflowed
     too — both halves clipped against .tour__panel's overflow:hidden, and the
     art and the shots over it stopped describing the same rectangle. The column
     takes its height from the row now, as the filled ones already did. */
  .tour__media {
    aspect-ratio: auto;
    min-width: 0;
    /* Centres whatever is flowed in it — the plate on a column that has one,
       the lone card on the four that don't — on the same box .tour__art draws,
       which is the whole point of the two sharing a grid cell. */
    display: grid;
    place-items: center;
  }
  .tour__shot--main { inset: 0 !important; }
  .tour__media .tour__shot--main:has(> img) {
    position: static;
    inset: auto !important;
  }
  /* A plate left on its own flows like any other lone card: full width, and the
     vertical centring it takes on the desktop split cleared — pulled up by half
     its own height it would sit over the copy. Written with the same four parts
     as the rule setting that centring, plus the filled test, so it lands after
     it whatever the order. */
  .tour__media > .tour__shot--main:only-child:has(> img) {
    width: 100%;
    transform: none;
  }
  /* A column holding nothing but a card flows it, the same way a filled plate
     flows above. Absolute, it was still sized by its desktop arrangement — 46%,
     62%, 78% of a column that is half a card wide there and the whole of it
     here, which came out at 125–223px, small enough that the menu screenshot was
     unreadable. Flowed it fills the column and the centring above places it.

     Written against the same four parts as the rule it is undoing, plates
     excluded and all: one class short, the `:not()` over there would outrank
     this and the card would stay absolute, pinned half off the card's left
     edge. Plates are covered by the rule above instead. */
  .tour__media > .tour__shot:only-child:not(.tour__shot--main) {
    position: static;
    inset: auto;
    width: 100%;
    transform: none;
  }
  /* The inset resets above can't reach a transform, and a flowed plate pulled
     up by half its height would sit over the copy. Same specificity as the
     centring rule, so being later in the file is what undoes it. */
  .tour__media--bleed .tour__shot--main { transform: none; }
  /* Every tile centres on the column here. The arrangements hang them off a
     corner of the plate, which is a composition for a wide column — stacked and
     narrow there is no room for it, and a tile pinned to one edge just reads as
     off-centre. The lone tiles are already centred by the :only-child rule
     outside this block; this is what reaches the two columns that still have a
     plate. */
  .tour__media .tour__shot--2 {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
  }
}


/* Lock-up entrance. The plate fades up on its own; the tiles stacked over it
   (the z-index:1 ones) come in from below, a beat later, so the overlap builds
   rather than arriving all at once.

   Keyed to .is-active, which the carousel already sets on the card in view —
   so the boxes assemble as you land on each card, not once on page load.

   Gated on .js: without the script nothing would ever add .is-active and the
   boxes would sit at opacity 0 forever. */
.js .tour__shot {
  opacity: 0;
  transition:
    opacity 520ms var(--srve-ease-standard),
    translate 520ms var(--srve-ease-standard);
}
.js .tour__shot--main {
  translate: 0 12px;
  transition-delay: 40ms;
}
/* Further from the plate = further to travel, and later. */
.js .tour__shot--2 {
  translate: 0 32px;
  transition-delay: 200ms;
}
.js .tour__shot--3 {
  translate: 0 32px;
  transition-delay: 320ms;
}
.js .tour__panel.is-active .tour__shot {
  opacity: 1;
  translate: 0 0;
}
@media (prefers-reduced-motion: reduce) {
  .js .tour__shot,
  .js .tour__shot--main,
  .js .tour__shot--2,
  .js .tour__shot--3 {
    opacity: 1;
    translate: 0 0;
    transition: none;
  }
}
