/* Named curves, the stylesheet's half of the set page-transition.js
   registers with CustomEase. Same control points, same names: whatever
   asks for inoutMask gets the same gesture whether gsap, the Web
   Animations API or a CSS animation is driving it. */
:root {
  --ease-inout-mask: cubic-bezier(0.77, 0, 0.175, 1);  /* inOutQuart */
  --ease-qubic: cubic-bezier(0.65, 0.05, 0.36, 1);
}

/* Nav has to outrank both containers while they sit at
   z-index 1 and 2 inside .page_wrap */
.meganav_root {
  position: relative;
  z-index: 100;
}

/* No background on purpose: this file loads after Webflow's at the same
   specificity, so one here silently beats the Designer. */
.page_wrap {
  position: relative;
  z-index: 1;
}

/* Pinned on desktop only, the same breakpoint FOOTER_PIN uses in the JS:
   below it the footer is in flow and scrolls with the page, and the
   reserved space that reveals it is not written. Unscoped, this rule beat
   the Designer's own breakpoint — this file loads after Webflow's — and
   the two halves disagreed about whether the footer moves. */
@media (min-width: 992px) {
  .footer_wrap {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 0;
  }
}

/* Both containers go fixed mid-transition, collapsing the reveal gap, so
   the footer would flash through it. Unless it was on screen when the
   navigation started — then it travels in the outgoing layer. */
html.is-transitioning .footer_wrap:not(.is-transition-layer) {
  visibility: hidden;
}

/* On the root rather than on .page_wrap or the layers: clipping there
   makes them scrollports, and every sticky section then resolves against
   a 100vh box. The root is the scrollport either way. */
html.is-transitioning {
  cursor: wait;
  overflow: hidden;
}

/* Backing for the outgoing page while it is out of flow. Inherited, so it
   tracks .page_wrap rather than a variable that may not resolve. */
.page-transition__wrapper {
  background: inherit;
}

/* Fills the gap between the two pages mid-transition, independent of the
   page background. --transition-bg site-wide, data-transition-bg per
   template.  (docs: README ### Transition background) */
.page-transition__backdrop {
  background: var(--transition-bg, #191915);
  pointer-events: none;
}


/* The cursor follower, created by servicesHover and parented to <body>.
   Only the box is here — it is the one part with a responsive dimension;
   the rows are styled inline, where they outrank the Designer. */
.services_follower {
  position: fixed;
  top: 0;
  left: 0;
  width: clamp(220px, 22vw, 420px);
  aspect-ratio: var(--services-follower-ratio, 1 / 1);
  z-index: 90;               /* under .meganav_root, which sits at 100 */
  pointer-events: none;
  will-change: transform;
}

/* Clips the incoming image while it grows, so it covers rather than
   spills past the frame. */
.services_follower__inner {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.services_follower__visual {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Filter checkboxes. filterSingle sets .filter-single and .is-checked on
   the label, so the idle state is one class on one element whichever piece
   of the Webflow checkbox is styled. Idle state only. */
.filter-single {
  --filter-idle-border: rgba(25, 25, 21, 0.1);
  --filter-idle-text: rgba(25, 25, 21, 0.6);
}

.filter-single:not(.is-checked) .insights_filter_check,
.filter-single:not(.is-checked) .w-checkbox-input {
  border-color: var(--filter-idle-border);
}

.filter-single:not(.is-checked) .w-form-label,
.filter-single:not(.is-checked) [data-filter-label] {
  color: var(--filter-idle-text);
}

/* Hover brings an idle chip up to full strength — the checked state it
   would take if pressed, minus the tick. Pointer devices only: on a
   touchscreen the hover state sticks after the tap and reads as a second
   kind of selected. */
.filter-single .insights_filter_check,
.filter-single .w-checkbox-input,
.filter-single .w-form-label,
.filter-single [data-filter-label] {
  transition: border-color 200ms ease, color 200ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .filter-single:not(.is-checked):hover .w-form-label,
  .filter-single:not(.is-checked):hover [data-filter-label] {
    color: var(--filter-hover-text, var(--_theme---typography--text-primary, #191915));
  }

  /* The box is drawn in currentColor by the section embed, so handing the
     border back to it is enough — one colour to change, not two. The idle
     rule pins border-color, so it has to be undone by name. */
  .filter-single:not(.is-checked):hover .insights_filter_check,
  .filter-single:not(.is-checked):hover .w-checkbox-input {
    border-color: currentColor;
    color: var(--filter-hover-text, var(--_theme---typography--text-primary, #191915));
  }
}

@media (prefers-reduced-motion: reduce) {
  .filter-single .insights_filter_check,
  .filter-single .w-checkbox-input,
  .filter-single .w-form-label,
  .filter-single [data-filter-label] {
    transition-duration: 0.01ms;
  }
}

/* Trims the label's line box to the letters, so the tick centres against
   the type rather than the leading. Progressive: without text-box the row
   is exactly as it was, and alignment stays in the Designer. */
@supports (text-box: trim-both cap alphabetic) {
  .filter-single .w-form-label,
  .filter-single [data-filter-label] {
    text-box: trim-both cap alphabetic;
  }
}

/* The box is a capital beside the label's capitals, as on the services
   rows: cap height on the label's own type, and centred on the trimmed
   label, so its top and bottom land on the cap line and the baseline.
   This replaces three fixed numbers that were only right at one width —
   a 0.75rem box against fluid type, the label's -1px nudge, and the
   item's -8px, which cancelled Webflow's clearfix. That clearfix is a
   flex item in this row, so it goes, and the 8px its ::after put between
   the chips comes back as a margin. The label's own u-text-style trim
   pseudos go too: text-box already trims it, and the two together
   trimmed it twice. */
.filter-single::before,
.filter-single::after,
.filter-single .w-form-label::before,
.filter-single .w-form-label::after {
  content: none;
}

.insights_filter_item.filter-single {
  margin-left: 0;
  margin-right: 0.5rem;
}

.filter-single .w-form-label {
  margin-top: 0;
}

.filter-single .w-checkbox-input {
  box-sizing: border-box;
  font-family: var(--_typography---font--secondary-family);
  font-size: var(--_typography---font-size--text-small);
  width: 1cap;
  height: 1cap;
}

/* Without the cap unit: Suisse Intl's cap height. Without text-box the
   label keeps its line box, whose middle is 0.054em below the caps'. */
@supports not (height: 1cap) {
  .filter-single .w-checkbox-input {
    width: 0.725em;
    height: 0.725em;
  }
}

@supports not (text-box: trim-both cap alphabetic) {
  .filter-single .w-checkbox-input {
    position: relative;
    top: -0.054em;
  }
}

/* ===== FORM CHECKBOXES — .form1_check_box — README ### form checkboxes ===== */

/* The form component's own embed styles a box that is a sibling of the
   input — input:checked ~ .form1_check_box, with an svg tick inside it
   to reveal. The markup here has neither: .form1_check_box is the input,
   so not one of those rules could ever match and the box arrived with no
   border, no fill and nothing to show for being checked.

   Styled as the input, then, which is simpler than the component it came
   from: :checked is the state, and the tick is a pseudo rather than a
   file. The tokens are this site's own — the embed reaches for
   --_theme---text-1 and --background-1, which are another project's
   names and unset here, so every colour it set resolved to nothing. */
.form1_check_wrap {
  /* Webflow floats the input and pulls it back by its own indent. The
     wrap is already a flex row with a gap, and the two arrangements were
     fighting: the box sat outside the row it belongs to. */
  padding-left: 0;
  /* The trimmed label below is shorter than its line; this keeps the
     rows as far apart as they were. */
  min-height: 1lh;
}

/* Webflow's clearfix on .w-checkbox, for the float layout this no longer
   uses. In a flex row each pseudo is an item, and the gap after the
   zero-width ::before pushed the box 8px in from the edge. */
.form1_check_wrap::before,
.form1_check_wrap::after {
  content: none;
}

/* Centring the box on the label's line box put it 0.26em below the
   middle of the letters, since the line box carries the descender
   space. Trimmed to cap height, centre is the letters' centre. */
.form1_check_wrap .form1_check_label {
  text-box: trim-both cap alphabetic;
}

@supports not (text-box: trim-both cap alphabetic) {
  .form1_check_wrap .form1_check_box {
    position: relative;
    top: -0.26em;
  }
}

.form1_check_wrap .form1_check_box {
  appearance: none;
  -webkit-appearance: none;
  float: none;
  margin: 0;
  flex: none;
  width: 1em;
  height: 1em;
  display: grid;
  place-content: center;
  border: 1px solid currentColor;
  background-color: transparent;
  color: inherit;
  cursor: pointer;
  transition:
    background-color var(--transition-duration-1, 200ms) var(--transition-ease, ease),
    border-color var(--transition-duration-1, 200ms) var(--transition-ease, ease);
}

/* Checked is a solid black box, no tick. Webflow's own classes get named
   here too: its script adds w--redirected-checked and w--redirected-focus
   in Safari, which bring a blue fill, a blue glow and a tick image of their
   own, and accent-color covers Safari tinting the native control. */
.form1_check_wrap .form1_check_box {
  accent-color: #000;
  background-image: none;
  box-shadow: none;
  -webkit-tap-highlight-color: transparent;
}

.form1_check_wrap .form1_check_box:not(.is-radio):not([type="radio"]):checked,
.form1_check_wrap .form1_check_box:not(.is-radio):not([type="radio"]).w--redirected-checked {
  background-color: #000 !important;
  border-color: #000 !important;
  background-image: none !important;
}

.form1_check_wrap .form1_check_box.w--redirected-focus {
  box-shadow: none !important;
}

/* A radio keeps the box empty and fills a dot instead, the way the
   component's own is-radio variant does. */
.form1_check_wrap .form1_check_box.is-radio,
.form1_check_wrap input[type="radio"].form1_check_box {
  border-radius: 50%;
}

/* The dot is a background rather than a ::before, which Safari is not to
   be relied on to draw on an input. It grows, since background-size
   animates and background-image does not. */
.form1_check_wrap .form1_check_box.is-radio,
.form1_check_wrap input[type="radio"].form1_check_box {
  background-image: radial-gradient(circle, currentColor 0 50%, transparent 50%);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0 0;
  transition: background-size var(--transition-duration-1, 200ms) var(--transition-ease, ease);
}

.form1_check_wrap .form1_check_box.is-radio:checked,
.form1_check_wrap input[type="radio"].form1_check_box:checked {
  background-color: transparent;
  border-color: currentColor;
  background-size: 56% 56%;
}

/* The only keyboard-reachable control on the page, so the ring is the
   site's rather than the browser's. */
.form1_check_wrap .form1_check_box:focus-visible {
  outline: var(--focus--width, 0.125rem) solid
    var(--_theme---typography--text-primary, currentColor);
  outline-offset: var(--focus--offset-outer, 2px);
}

[data-validate].is-error .form1_check_wrap .form1_check_box {
  border-color: var(--_swatch---error, currentColor);
}

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

/* ===== MEGANAV — README ## Meganav ===== */

/* The sheet is absolute and sized in viewport units, not fixed: it lives
   inside the nav, whose footer-hide transform would otherwise become its
   containing block. These rules beat the nav's own embed on specificity,
   not order — an embed renders after this file. */

.meganav_root .meganav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

/* The bar rides above the sheet: logo and Meny stay legible on black. */
.meganav_root .meganav_inner {
  position: relative;
  z-index: 2;
}

/* The logo is the only item in the bar that can give: everything beside
   it is flex-shrink 0, and its SVG is width:100% of whatever the anchor
   ends up being. On a phone that made the mark a different size per page,
   since the width left over depends on what sits opposite it. */
.meganav_root .meganav_logo {
  flex: 0 0 auto;
}

.meganav_root {
  --meganav-panel-bg: var(--_theme---background--background-primary, #191915);
  --meganav-panel-text: var(--_colour---color--color-paper, #f7f7f5);
}

.meganav_root .meganav_panel {


  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  right: 0;
  /* 100% of the fixed bar, not 100vw: that counts the scrollbar and
     puts a horizontal one on every page. */
  width: 100%;
  height: 100dvh;
  max-width: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--meganav-panel-bg);
  color: var(--meganav-panel-text);

  /* Closed. clip-path rather than opacity, so the module can swipe it
     down; visibility keeps it out of the tab order. */
  clip-path: inset(0% 0% 100% 0%);
  opacity: 1;
  visibility: hidden;
  pointer-events: none;
  transition: none;
}

.meganav_root .meganav_panel.is-open {
  visibility: visible;
  pointer-events: auto;
}

/* The embed fades and lifts this; GSAP staggers the rows instead. */
.meganav_root .meganav_panel_inner {
  opacity: 1;
  transform: none;
  transition: none;

  /* Vertical only: the sides belong to the container class the module
     moves onto this element. */
  padding-top: var(--nav--height, 5rem);
  padding-bottom: var(--meganav-bottom, 3rem);
}

/* The CTA sits on the floor of the panel rather than under its own
   sentence. Three things have to be true for that: the grid has to be as
   tall as the sheet (it was only as tall as its content, so there was no
   slack anywhere), the column has to stretch into it, and the text block
   has to take the leftover rather than the button.

   Desktop only, and display is set nowhere the Designer might be hiding
   something: the mobile sheet has a CTA of its own further down, and a
   display:flex here brought the desktop one back alongside it. The
   columns keep their own alignment for the same reason — stretching them
   moved text that was meant to sit at the top. */
/* The sheet is the flex container and the inner is its one child, so
   flex fills it exactly. min-height:100% did the same job badly: a
   percentage is of the sheet, the margin above is added on top of it, and
   the sum was 43px taller than the screen — a scrollbar for content that
   fits. min-height:0 lets it shrink back to what is actually there. */
.meganav_root .meganav_panel {
  display: flex;
  flex-direction: column;
}

.meganav_root .meganav_panel_inner {
  flex: 1 1 auto;
  min-height: 0;
}

@media screen and (min-width: 992px) {
  .meganav_root .meganav_feature_wrap {
    align-self: stretch;
    display: flex;
    flex-direction: column;
  }

  .meganav_root .meganav_feature_text_wrap {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
  }

  /* Under its own sentence, which is where it reads as belonging to the
     sentence. The column is still stretched — that is what holds the
     panel's height — so --meganav-cta-push: auto puts the button back on
     the floor of the panel for anyone who wants it there. */
  .meganav_root .meganav_feature_btn_wrap {
    margin-top: var(--meganav-cta-push, 0);
    align-self: var(--meganav-cta-align, start);
  }
}

/* The sheet has its own CTA below 992px — a different element, and the
   one actually on screen there. Same floor, reached through the column
   rather than the feature block. Display is not touched: whatever the
   Designer hides at a given width stays hidden. */
@media screen and (max-width: 991px) {
  .meganav_root .meganav_col {
    flex: 1 1 auto;
  }

  .meganav_root .meganav_content {
    display: flex;
    flex-direction: column;
    min-height: 100%;
  }

  /* flex:none first: the wrapper is handed flex-grow, so it swallowed the
     free space and held the button at the top of a 300px box — and an
     auto margin with no free space left to take is worth zero. */
  .meganav_root .meganav_mobile_btn_wrap {
    flex: 0 0 auto;
    margin-top: auto;
  }
}

/* Eased on the way back only: the sheet is what paints the bar, so
   opening should snap, while closing settles rather than flicks. */
.meganav_root .meganav,
.meganav_root .meganav .meganav_logo_svg,
.meganav_root .meganav .button_nav_text,
.meganav_root .meganav .button_link_text,
.meganav_root .meganav .footer_link_text,
.meganav_root .meganav .meganav_mobile_div {
  transition: background-color var(--meganav-restore, 0.28s) ease,
    color var(--meganav-restore, 0.28s) ease;
}

/* Open: the bar goes transparent and its type flips to the panel colour.
   Three classes deep to beat the embed's [data-transparent].is-open. */
.meganav_root .meganav.is-open,
.meganav_root .meganav.is-open[data-transparent="true"],
.meganav_root .meganav.is-open[data-transparent="false"] {
  background-color: transparent;
  color: var(--meganav-panel-text, #f7f7f5);
}

.meganav_root .meganav.is-open .meganav_logo_svg,
.meganav_root .meganav.is-open .button_link_wrap,
.meganav_root .meganav.is-open .button_link_text,
.meganav_root .meganav.is-open .button_link_arrow,
.meganav_root .meganav.is-open .footer_link_wrap,
.meganav_root .meganav.is-open .footer_link_text,
.meganav_root .meganav.is-open .meganav_mobile_div {
  color: var(--meganav-panel-text, #f7f7f5);
}

/* A link's underline is a real border with its own theme colour, so it
   stayed dark under a white label. [data-underline-link] is excluded: it
   holds that border transparent and draws the line in currentColor. */
.meganav_root .meganav.is-open .button_link_wrap:not([data-underline-link]),
.meganav_root .meganav.is-open .button_link_text:not([data-underline-link]),
.meganav_root .meganav.is-open .button_link_element:not([data-underline-link]),
.meganav_root .meganav.is-open .footer_link_wrap:not([data-underline-link]),
.meganav_root .meganav.is-open .footer_link_text:not([data-underline-link]),
.meganav_root .meganav.is-open [data-nav-border]:not([data-underline-link]) {
  border-color: var(--meganav-panel-text, #f7f7f5);
}

.meganav_root .meganav.is-open .meganav_mobile_div {
  background-color: var(--meganav-panel-text, #f7f7f5);
}

/* The burger crosses into an X: each bar travels half the distance
   between them, so they meet on the centre line before rotating. Set
   --burger-gap / --burger-thickness to match the Designer. */
.meganav_root .meganav_mobile_div {
  --burger-gap: 5px;
  /* Whole pixels: a 1.5px bar lands on a half pixel and renders as a
     lighter grey than its neighbour. */
  --burger-thickness: 2px;

  /* Both bars, both states, one colour: they follow the type rather than
     carrying a background of their own. */
  height: var(--burger-thickness);
  background-color: currentColor;
  opacity: 1;
  --burger-shift: calc((var(--burger-gap) + var(--burger-thickness)) / 2);

  transform-origin: center;
  transition:
    transform 0.45s cubic-bezier(0.05, 0.7, 0.1, 1),
    background-color 0.3s cubic-bezier(0.05, 0.7, 0.1, 1);
}

.meganav_root .meganav.is-open .meganav_mobile_div:nth-child(1) {
  transform: translateY(var(--burger-shift)) rotate(45deg);
}

.meganav_root .meganav.is-open .meganav_mobile_div:nth-child(2) {
  transform: translateY(calc(var(--burger-shift) * -1)) rotate(-45deg);
}

@media (prefers-reduced-motion: reduce) {
  .meganav_root .meganav_mobile_div {
    transition-duration: 0.01ms;
  }
}

/* The sheet is the mobile menu too. .meganav_mobile_wrap still holds
   another project's links — hidden here, delete it in the Designer. */
@media screen and (max-width: 991px) {
  /* The desktop sheet on a phone too, behind a transparent bar. */
  .meganav_root .meganav_panel {
    display: flex !important;
  }

  /* The embed paints the bar !important on mobile. */
  .meganav_root .meganav.is-open,
  .meganav_root .meganav.is-open[data-transparent="true"],
  .meganav_root .meganav.is-open[data-transparent="false"] {
    background-color: transparent !important;
    color: var(--meganav-panel-text) !important;
    transition: none !important;
  }

  .meganav_root .meganav.is-open .meganav_logo_svg,
  .meganav_root .meganav.is-open .button_nav_text,
  .meganav_root .meganav.is-open .button_link_text {
    color: var(--meganav-panel-text) !important;
  }

  .meganav_root .meganav.is-open .meganav_mobile_div {
    background-color: var(--meganav-panel-text) !important;
  }

  .meganav_root .meganav_mobile_wrap {
    display: none !important;
  }

  /* The Designer's two-column grid collapses the CTA's column at phone
     width, leaving the button zero wide. */
  .meganav_root .meganav_panel_inner {
    display: flex;
    flex-direction: column;
    gap: var(--meganav-mobile-gap, 2rem);
    padding-left: var(--meganav-inline-mobile, 1rem);
    padding-right: var(--meganav-inline-mobile, 1rem);
  }

  .meganav_root .meganav_panel_inner > * {
    width: 100%;
    max-width: none;
  }
}

/* The gutter is reserved always, so hiding the overflow does not widen
   the viewport and leave everything measured against it a fraction out
   when the scrollbar comes back. */
@media (hover: hover) and (pointer: fine) {
  html {
    scrollbar-gutter: stable;
  }
}

/* No double-tap zoom: it magnifies the visual viewport around whatever
   was tapped, which is easy to do by accident while scrolling.
   manipulation keeps panning and pinch. Listed per element as well as on
   the root, since Chrome resolves touch-action from the touched element
   up — and the accidental taps land on the video and the logo. */
html,
body,
[data-video="component"],
.meganav_logo,
.home_img_wrap {
  touch-action: manipulation;
}

html.is-menu-open {
  overflow: hidden;
}

/* ===== UNDERLINE LINKS — [data-underline-link] — README ## Underline links ===== */

/* The visible number or address is plain text under an overlay anchor,
   and iOS wraps text like that in a link of its own, underlined — a
   second line under this one. format-detection in the head stops most of
   it; this covers whatever iOS still wraps. */
a[x-apple-data-detectors],
[data-underline-link] a[href^="tel:"]:not(.g_clickable_wrap),
[data-underline-link] a[href^="mailto:"]:not(.g_clickable_wrap) {
  color: inherit !important;
  font: inherit !important;
  text-decoration: none !important;
  pointer-events: none;
}

/* Two lines, not one: the resting line wipes out right while a fresh one
   wipes in from the left, which reads as a replacement rather than a
   retreat and a return. Hover comes from the marked element, an ancestor
   link or [data-hover], or a Webflow overlay anchor covering the
   component — that last one is a SIBLING of the text, which is why
   :hover alone cannot do this. */

/* Doubled attribute: the static border is usually a shorthand on a
   class, which beats a single-attribute rule on a tie. Turned
   transparent, not removed, so the space it reserves stays. */
[data-underline-link]:not([data-underline-keep-border]),
[data-underline-link][data-underline-link]:not([data-underline-keep-border]) {
  border-bottom-color: transparent;
}

[data-underline-link] {
  --underline-thickness: max(1px, 0.0625em);
  --underline-offset: 0px;
  --underline-duration: 0.735s;
  --underline-delay: 0.3s;
  --underline-ease: cubic-bezier(0.625, 0.05, 0, 1);

  position: relative;
  text-decoration: none;
}

[data-underline-link]::before,
[data-underline-link]::after {
  content: "";
  position: absolute;
  bottom: calc(var(--underline-offset) * -1);
  left: 0;
  width: 100%;
  height: var(--underline-thickness);
  background-color: currentColor;
  pointer-events: none;
  /* The hairline rotation keeps the scaled edge on the compositor. */
  transform: scaleX(0) rotate(0.001deg);
  transition: transform var(--underline-duration) var(--underline-ease);
}

/* At rest ::before is the visible line and ::after waits off to the
   left. The delay is what makes the return wait for the exit. */
[data-underline-link]::before {
  transform-origin: left;
  transform: scaleX(1) rotate(0.001deg);
  transition-delay: var(--underline-delay);
}

[data-underline-link]::after {
  transform-origin: right;
  transition-delay: 0s;
}

/* The single-line version, for a link that should just draw itself in. */
[data-underline-link="simple"]::before {
  transform-origin: right;
  transform: scaleX(0) rotate(0.001deg);
  transition-delay: 0s;
}

[data-underline-link="simple"]::after {
  content: none;
}

@media (hover: hover) and (pointer: fine) {
  [data-underline-link]:hover::before,
  [data-underline-link]:focus-visible::before,
  [data-hover]:hover [data-underline-link]::before,
  a:hover [data-underline-link]::before,
  button:hover [data-underline-link]::before,
  *:has(> .g_clickable_wrap:hover) [data-underline-link]::before,
  *:has(> .g_clickable_wrap:focus-visible) [data-underline-link]::before {
    transform-origin: right;
    transform: scaleX(0) rotate(0.001deg);
    transition-delay: 0s;
  }

  [data-underline-link]:hover::after,
  [data-underline-link]:focus-visible::after,
  [data-hover]:hover [data-underline-link]::after,
  a:hover [data-underline-link]::after,
  button:hover [data-underline-link]::after,
  *:has(> .g_clickable_wrap:hover) [data-underline-link]::after,
  *:has(> .g_clickable_wrap:focus-visible) [data-underline-link]::after {
    transform-origin: left;
    transform: scaleX(1) rotate(0.001deg);
    transition-delay: var(--underline-delay);
  }

  /* The simple variant has one line, so it draws in from the left and
     retreats the way it came. */
  [data-underline-link="simple"]:hover::before,
  [data-hover]:hover [data-underline-link="simple"]::before,
  a:hover [data-underline-link="simple"]::before,
  *:has(> .g_clickable_wrap:hover) [data-underline-link="simple"]::before {
    transform-origin: left;
    transform: scaleX(1) rotate(0.001deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-underline-link]::before,
  [data-underline-link]::after {
    transition-duration: 0.01ms;
    transition-delay: 0s;
  }
}

/* The panel CTA. The button ships its palette per style variant, so on
   the sheet it rendered dark on dark; stated here against the sheet's own
   colours. Retarget with --meganav-cta-bg / --meganav-cta-text. */
.meganav_root .meganav_panel .button_main_wrap,
.meganav_root .meganav_panel .button_main_padding,
.meganav_root .meganav_panel .button_main_element {
  background-color: var(--meganav-cta-bg, var(--meganav-panel-text, #f7f7f5));
  color: var(--meganav-cta-text, var(--meganav-panel-bg, #191915));
}

.meganav_root .meganav_panel .button_main_text,
.meganav_root .meganav_panel .button_main_icon_wrap,
.meganav_root .meganav_panel .button_main_icon_arrow,
.meganav_root .meganav_panel .button_main_icon {
  color: var(--meganav-cta-text, var(--meganav-panel-bg, #191915));
}

/* ===== ARTICLE GRIDS — case and insight templates, tablet and down ===== */

/* Lifted out of the per-section embeds, where the same block was pasted
   into every template that needed it.

   Section spacing is not touched here at all. The Designer's spacer
   variants carry fluid section sizes that already scale, and every
   attempt to collapse them below 992px fought the values rather than the
   rhythm. What is left are the grids: a column gap is spending width
   nothing is using once the columns have stopped being columns.

   data-space-keep opts a section out — put it on the section, the
   container or a single element and nothing inside is touched. */
@media screen and (max-width: 991px) {
  /* Stacked, the column gap is spending width nothing is using and the
     row gap carries the whole rhythm. */
  .c_cases_row_grid:not([data-space-keep], [data-space-keep] *) {
    gap: var(--section-row-gap-mobile, 4rem);
  }

  :is(.c_collection_list, .post_contact_grid):not([data-space-keep], [data-space-keep] *) {
    row-gap: var(--section-row-gap-mobile, 4rem);
  }

  /* The share block is closer kin to the article than the cards are to
     each other, so it gets its own smaller number — and its Designer
     margin goes, or the two stack into a hole twice the size. */
  .insight_main_grid:not([data-space-keep], [data-space-keep] *) {
    row-gap: var(--insight-share-gap-mobile, 2rem);
  }

  .insight_main_share_wrap:not([data-space-keep], [data-space-keep] *) {
    margin-top: 0;
    margin-bottom: 0;
  }

  /* One share block at a time. The template carries two — the one in the
     grid above the article and .is-mobiie below it — and on a phone both
     were showing, the top one paying for a grid row it did not need. */
  .insight_main_share_wrap:not(.is-mobiie) {
    display: none;
  }
}

@media screen and (min-width: 992px) {
  .insight_main_share_wrap.is-mobiie {
    display: none;
  }
}

/* ===== FAQ — README ### faq ===== */

/* The answer is set in the large paragraph style, which on a phone is a
   lot of type for a panel that opens inside a list. It drops to the main
   style there — the design system's own tokens, so it follows the scale
   rather than pinning a size of its own.

   Three selectors deep on purpose: the Designer's variant class carries
   the large size at two, and which of the two files wins a tie is a
   question of load order. */
@media screen and (max-width: 767px) {
  .faq_wrap .faq_items_info .g_paragraph {
    font-size: var(--_typography---font-size--text-main);
    line-height: var(--_typography---line-height--1-35);
  }
}

/* ===== POST CONTACT — .post_contact_wrap ===== */

/* Grid and flex children default to min-width:auto and refuse to shrink
   below their content, so a wide image or an unbreakable string pushes
   the track past its container. */
.post_contact_grid > *,
.post_contact_inner_wrap,
.post_contact_content,
.post_contact_img_wrap,
.post_contact_details,
.post_contact_heading {
  min-width: 0;
}

/* Square, and taller only when the text beside it needs the room: the
   ratio sets the height asked for, min-height:100% lets it grow with a
   longer column. --post-contact-img-grow: 0 pins it square. */
.post_contact_img_wrap {
  /* Width first: given a ratio and a definite height, the box grows
     sideways to stay square and spills out of its column. */
  width: 100%;
  aspect-ratio: var(--post-contact-img-ratio, 1 / 1);
  height: auto;
  min-height: calc(100% * var(--post-contact-img-grow, 1));
}

.post_contact_img_wrap img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  object-fit: cover;
}

/* With growth off, the column should sit at the top of its row rather
   than being stretched by it. */
.post_contact_content:has(.post_contact_img_wrap) {
  align-self: var(--post-contact-img-align, stretch);

  /* The ratio belongs on the wrap: on the column, a tall row makes a
     square column ask for a matching width and the track grows. */
  aspect-ratio: auto;
  min-width: 0;
}

/* An address or phone number has no break opportunity, so it sets the
   minimum width of everything above it. */
.post_contact_details .button_link_text {
  overflow-wrap: anywhere;
}

/* ===== STAGGERED CHILDREN — [data-fade-children] — README ### data-fade ===== */

/* A marquee copies its list to loop, so anything the script writes on a
   logo before that happens is copied too — inline opacity and all — and
   two thirds of them never get a tween. A rule reaches the copies by
   itself, which is why this half is CSS: the module only adds .is-faded
   to the container when its cue arrives.

   The place in the order comes from nth-child, so a copied list staggers
   exactly like the original. Twenty is more logos than a row shows at
   once; past that they share the last slot.
 */
[data-fade-children]:not([data-fade-children="false"]) .is-fade-child,
[data-fade-children]:not([data-fade-children="false"]) [data-marquee-item] {
  opacity: 0;
  transition: opacity var(--fade-child-ms, 500ms) var(--ease-qubic);
  transition-delay: calc(var(--fade-child-i, 0) * var(--fade-child-step, 60ms));
}

[data-fade-children].is-faded .is-fade-child,
[data-fade-children].is-faded [data-marquee-item] {
  opacity: 1;
}

[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(1) { --fade-child-i: 0; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(2) { --fade-child-i: 1; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(3) { --fade-child-i: 2; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(4) { --fade-child-i: 3; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(5) { --fade-child-i: 4; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(6) { --fade-child-i: 5; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(7) { --fade-child-i: 6; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(8) { --fade-child-i: 7; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(9) { --fade-child-i: 8; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(10) { --fade-child-i: 9; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(11) { --fade-child-i: 10; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(12) { --fade-child-i: 11; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(13) { --fade-child-i: 12; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(14) { --fade-child-i: 13; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(15) { --fade-child-i: 14; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(16) { --fade-child-i: 15; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(17) { --fade-child-i: 16; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(18) { --fade-child-i: 17; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(19) { --fade-child-i: 18; }
[data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]):nth-child(n+20) { --fade-child-i: 19; }

/* Nothing to reveal if nothing can move. */
@media (prefers-reduced-motion: reduce) {
  [data-fade-children]:not([data-fade-children="false"]) :is(.is-fade-child, [data-marquee-item]) {
    opacity: 1;
    transition: none;
  }
}

/* data-fade-children="cms" — the Collection Items are the children.

   The generic rule needs a class or a marquee attribute on each child,
   and both mean editing a component that is placed in a dozen other
   spots to reach the one placement that wants the entrance. A CMS list
   is already a staggered set, so naming it on the container is enough.

   Its own value rather than the plain attribute, because a container
   that already fades marked children can hold a Collection List too —
   /kontakt's marquee holds thirty-three items — and those must keep
   staggering exactly as they do now. */
[data-fade-children="cms"] .w-dyn-item {
  opacity: 0;
  transition: opacity var(--fade-child-ms, 500ms) var(--ease-qubic);
  transition-delay: calc(var(--fade-child-i, 0) * var(--fade-child-step, 60ms));
}

[data-fade-children="cms"].is-faded .w-dyn-item {
  opacity: 1;
}

[data-fade-children="cms"] .w-dyn-item:nth-child(1) { --fade-child-i: 0; }
[data-fade-children="cms"] .w-dyn-item:nth-child(2) { --fade-child-i: 1; }
[data-fade-children="cms"] .w-dyn-item:nth-child(3) { --fade-child-i: 2; }
[data-fade-children="cms"] .w-dyn-item:nth-child(4) { --fade-child-i: 3; }
[data-fade-children="cms"] .w-dyn-item:nth-child(5) { --fade-child-i: 4; }
[data-fade-children="cms"] .w-dyn-item:nth-child(6) { --fade-child-i: 5; }
[data-fade-children="cms"] .w-dyn-item:nth-child(7) { --fade-child-i: 6; }
[data-fade-children="cms"] .w-dyn-item:nth-child(8) { --fade-child-i: 7; }
[data-fade-children="cms"] .w-dyn-item:nth-child(9) { --fade-child-i: 8; }
[data-fade-children="cms"] .w-dyn-item:nth-child(10) { --fade-child-i: 9; }
[data-fade-children="cms"] .w-dyn-item:nth-child(11) { --fade-child-i: 10; }
[data-fade-children="cms"] .w-dyn-item:nth-child(12) { --fade-child-i: 11; }
[data-fade-children="cms"] .w-dyn-item:nth-child(13) { --fade-child-i: 12; }
[data-fade-children="cms"] .w-dyn-item:nth-child(14) { --fade-child-i: 13; }
[data-fade-children="cms"] .w-dyn-item:nth-child(15) { --fade-child-i: 14; }
[data-fade-children="cms"] .w-dyn-item:nth-child(16) { --fade-child-i: 15; }
[data-fade-children="cms"] .w-dyn-item:nth-child(17) { --fade-child-i: 16; }
[data-fade-children="cms"] .w-dyn-item:nth-child(18) { --fade-child-i: 17; }
[data-fade-children="cms"] .w-dyn-item:nth-child(19) { --fade-child-i: 18; }
[data-fade-children="cms"] .w-dyn-item:nth-child(n+20) { --fade-child-i: 19; }

@media (prefers-reduced-motion: reduce) {
  [data-fade-children="cms"] .w-dyn-item {
    opacity: 1;
    transition: none;
  }
}

/* ===== BUTTON CHARACTERS — [data-button-animate-chars] — README ### buttonChars ===== */

/* Each character climbs out of the box while its own text-shadow, sitting
   one rise below it, climbs into the place it left. One transform per
   character and no second copy of the label in the markup.

   The delays are written per character by buttonChars; everything else is
   here, so a page whose script never lands still shows a button that
   reads and clicks. */
[data-button-animate-chars] {
  display: inline-block;
  position: relative;
  overflow: hidden;
}

[data-button-animate-chars] > span {
  display: inline-block;
  position: relative;
  /* The character that arrives. currentColor, so it follows the button
     through every theme and hover state it already has. */
  text-shadow: 0 var(--btn-chars-rise, 1.3em) currentColor;
  /* The hundredth of a degree keeps the glyph on the same rasterisation
     path through the move; without it the text reflows its antialiasing
     as the transform starts and the label appears to flicker. */
  transform: translateY(0) rotate(0.001deg);
  transition: transform var(--btn-chars-ms, 600ms)
    var(--btn-chars-ease, cubic-bezier(0.625, 0.05, 0, 1));
}

@media (hover: hover) and (pointer: fine) {
  /* .is-chars-hover is put on by buttonChars, on whichever element holds
     both the link and the label: the site's button covers itself with an
     absolutely positioned link and keeps the text in a sibling, so no
     selector starting at :hover can reach from the one to the other. The
     plain hovers stay for markup where the label is inside the link. */
  .is-chars-hover [data-button-animate-chars] > span,
  :is(a, button, .w-button):hover [data-button-animate-chars] > span,
  [data-button-animate-chars]:hover > span {
    transform: translateY(calc(var(--btn-chars-rise, 1.3em) * -1)) rotate(0.001deg);
  }
}

/* The label is the whole effect, so there is nothing to soften. */
@media (prefers-reduced-motion: reduce) {
  [data-button-animate-chars] > span {
    transition: none;
    transform: none;
  }
}

/* ===== SMOOTHLY ROW — .work_smoothly_wrap ===== */

/* The row has to clip: it is a rail of pictures wider than the screen.
   But `hidden` cuts an entrance dead — a picture that rises or tilts into
   place is travelling outside the box while it does it, and the bottom of
   it is sliced off against the rail's edge on the way.

   `clip` takes a margin where `hidden` takes none, so the rail still
   swallows the rail while the pictures paint past its top and bottom.
   The margin applies on every side, which costs nothing here: the row is
   full bleed, so its left and right edges are the viewport's, and
   .page_wrap clips what falls outside.

   The leading `html` outranks the section embed's own `overflow: hidden`
   without needing !important — that rule is a single class, and it sits
   later in the document, so a plain class here would tie and lose. */
html .work_smoothly_wrap {
  overflow: clip;
  overflow-clip-margin: var(--smoothly-spill, 120px);
}

/* ===== HOVER SCALE — [data-scale] — README ### data-scale ===== */

/* The picture leans in under the pointer. Mark the wrapper or the
   picture itself; either way the transform lands on the picture and the
   wrapper does the clipping.

   Nothing here takes the pointer: no overlay, no pseudo, no
   pointer-events. A transform moves pixels, not hit-testing, so a link
   or a card under this keeps every click it had.

   No will-change either. A permanent compositing layer on every marked
   picture is what put the slider's art over its own colour panel in
   Safari, and a hover is not worth paying that on load. */
@media (hover: hover) and (pointer: fine) {
  /* Whatever is scaled has to be clipped by something, or the picture
     spills over what sits beside it. clip, not hidden — a hidden
     overflow makes the element a scrollport, which is what broke the
     sticky sections. */
  [data-scale]:not(img):not(video),
  :has(> img[data-scale]),
  :has(> video[data-scale]) {
    overflow: clip;
  }

  /* Read where the attribute is, inherited by the picture. attr() only
     ever sees the element it runs on, so a number written on the wrapper
     is invisible to a rule that targets the image inside it — which is
     how a data-scale written on a wrapper quietly came out at the
     fallback instead.

     attr() with a type is Chrome 133+; everywhere else takes the
     fallback, which is the number we would have written anyway. */
  [data-scale] {
    --scale-to: attr(data-scale type(<number>), 1.04);
  }

  [data-scale] :is(img, video),
  img[data-scale],
  video[data-scale] {
    transform: scale(1);
    /* Softer than the site's own curve: a lean this small wants to ease
       in and out of itself rather than snap away and coast back. */
    transition: transform var(--scale-ms, 500ms) ease-in-out;
  }

  [data-scale]:hover :is(img, video),
  img[data-scale]:hover,
  video[data-scale]:hover {
    transform: scale(var(--scale-to, 1.04));
  }
}

/* The lean is the whole effect, so there is nothing to soften: it goes. */
@media (prefers-reduced-motion: reduce) {
  [data-scale] :is(img, video),
  img[data-scale],
  video[data-scale] {
    transition: none;
    transform: none;
  }
}

/* ===== TESTIMONIAL COLOURS — README ### testimonialColours ===== */

/* The hex lives on the element as data-bg / data-text, and this reads it
   straight into the variables the component already uses — no script in
   the way, so the colour is there at first paint rather than a frame
   later.

   attr() with a type is Chrome 133+, and testimonialColours writes the
   same variables inline for everything else. Inline wins, so the two
   never disagree; where both work the CSS has simply already done it.

   The fallbacks are deliberately loud rather than neutral: a hex the CMS
   got wrong shows as an uncoloured section, which is a thing you notice,
   instead of silently inheriting something that looks almost right. */
.c_testimonial_content_wrap[data-bg]:not([data-bg=""]) {
  --section-bg: attr(data-bg type(<color>), transparent);
}

.c_testimonial_content_wrap[data-text]:not([data-text=""]) {
  --section-text: attr(data-text type(<color>), currentColor);
}

.c_testimonial_content_wrap[data-text-secondary]:not([data-text-secondary=""]) {
  --section-text-secondary: attr(data-text-secondary type(<color>), currentColor);
}

/* ===== RULE REVEAL — [data-rule] — README ### ruleReveal ===== */

/* The line the module draws over the element's own border. It only
   exists once the module has stamped .is-rule, so a page the script
   never reaches keeps the real border instead of losing the line.

   Pulled up by its own height, since an absolute child resolves against
   the padding box and the border sits outside that. */
.is-rule {
  position: relative;
}

.is-rule::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--rule-h, 1px) * -1);
  height: var(--rule-h, 1px);
  background-color: var(--rule-color, currentColor);
  transform: scaleX(var(--rule-scale, 1));
  transform-origin: left center;
  pointer-events: none;
}

.is-rule[data-rule="bottom"]::before {
  top: auto;
  bottom: calc(var(--rule-h, 1px) * -1);
}

/* ===== INFO SECTION — .c_info_wrap ===== */

/* The text style trims the cell's box to the ink — ::before and ::after
   as zero-height tables pulling -0.21em off the top and -0.36em off the
   bottom — so the box hugs the caps and the baseline and the text spills
   past its own wrapper.

   That makes the grid's gap measure ink to ink while every other gap on
   the page measures box to box, and the same spacing variable then reads
   0.57em short here and nowhere else: var(--_sizes---space--7-32) drew
   32px of box gap between the paragraphs everywhere, and ~23 here.

   Cut, so the cell's box is its line box again and the gap is the number
   the Designer shows. The cells are already display: flow-root, so the
   pseudos are not holding a margin collapse open either. */
.c_info_grid_wrap > .c_info_grid_cell::before,
.c_info_grid_wrap > .c_info_grid_cell::after {
  display: none;
}

/* ===== SLIDER SECTION — .c_slider_wrap — README ### slider ===== */

/* Lifted out of the section embed, where truncated declarations made the
   parser skip whatever followed until it resynced — the mobile inset
   among them. Recovered variable names carry a fallback. The track is
   full-bleed and clipped, with the first slide offset into line with the
   page container by the module. */

.c_slider_wrap {
  /* clip, not hidden, so this does not become the scrollport a sticky
     descendant resolves against. It absorbs the few px the full-bleed
     rule below overshoots by. */
  overflow-x: clip;
  overflow-y: visible;
}

/* Full bleed: 50% is half the parent and 50vw half the viewport, so the
   difference is exactly the inset to cancel, however deeply the section
   is nested. Margins, never a transform — that would make this the
   containing block for fixed descendants. */
.c_slider_offset {
  width: auto;
  max-width: none;
  margin-inline: calc(50% - 50vw);
}

/* clip, not hidden: a hidden overflow makes this the scrollport that any
   position:sticky descendant resolves against. */
.c_slider_swiper {
  width: 100%;
  overflow: clip;
}

/* Slides, whichever element is the slide. */
.c_slider_swiper_wrap > * {
  min-width: 0;
  height: auto;
}

/* Old structure only: several cards inside one slide. */
.c_slider_swiper_slide {
  display: flex;
  gap: 1rem;
}

.c_slider_swiper_slide .c_slider_card {
  width: 100%;
  min-width: 0;
}

/* Swiper writes each slide's width inline and lays the track out from
   that number, so a Designer max-width clamps the rendered slide without
   Swiper knowing: the track is built for one width and drawn at another,
   and every snap after the first lands short of the page margin.
   !important because the breakpoint rules that set it come later, and on
   any slide, since the same clamp on another slider does the same. */
.c_slider_swiper .swiper-slide {
  max-width: none !important;
  min-width: 0 !important;
  height: auto;

  /* Slides stay stretched, which is what gives a row of cards one
     height when their quotes are different lengths.

     The ones with an aspect-ratio of their own cannot: a stretched flex
     item takes its cross size from the line, which outranks the ratio —
     Chrome resolves the ratio first and those cards came out square,
     WebKit stretched and they did not. The module takes those out of
     stretch, one slide at a time, since no media query can ask whether
     an element has a ratio. See `slider` in the README. */
}

.c_slider_swiper .card_hover_wrap {
  border: 0;
  overflow: clip;
  transform: translateZ(0);
}

.card_hover_bg_hover {
  transform: scale(1.01);
  transform-origin: center;
}

/* Overspilling rather than inset: a pixel in, the card's own background
   showed as a rim as the colour layer faded. The wrap clips. */
.card_hover_img_wrap {
  inset: -1px;
}

.c_slider_card_image_wrap,
.card_hover_img_wrap {
  overflow: clip;
}

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

/* The card art must NOT be its own compositing layer. Promoted — by
   will-change: transform, or backface-visibility: hidden — it is ordered
   by the compositor rather than by the card's stacking context, and the
   colour panel that hides it is an ordinary painted element. Swiper moves
   the track with translate3d, WebKit pulls the subtree into a 3D
   rendering context, and every promoted image is drawn over its own
   panel: every card in the slider shows its photo the moment you press
   next. Chrome resolves the same tree the other way, so it only ever
   appears in Safari.

   Higher specificity than the section embed's own rule, so it wins from
   here without !important and without the embed having to be edited.
   The 1.004 overscale on the image stays where it is — that is what
   answers the fractional-scale seam these were reaching for. */
.c_slider_swiper .card_hover_img_wrap img {
  will-change: auto;
  backface-visibility: visible;
}

/* The same lean the rest of the site has on [data-scale], on the card
   art — 1.04 on an ease-in-out, one gesture with the colour panel
   lifting off it.

   The section embed reaches for this already and cannot land it: its
   rest scale is on .card_hover_img (0,1,0) while its own later rule sets
   transform on .card_hover_img_wrap img (0,1,1), which is more specific
   and wins. The image sat at 1.004 and hovered to 1 — a shrink of four
   thousandths, which is nothing to see. Both rules here are more
   specific again, so the pair reads as written without touching the
   embed.

   No will-change and no backface-visibility, deliberately: promoting
   this image is what put the photo over its own colour panel in Safari,
   and a transform transition does not need the layer. */
@media (hover: hover) and (pointer: fine) {
  .c_slider_swiper .card_hover_img_wrap img {
    transform: scale(1);
    transition: transform var(--scale-ms, 500ms) ease-in-out;
  }

  .c_slider_swiper .card_hover_wrap:hover .card_hover_img_wrap img {
    transform: scale(var(--card-img-lean, 1.04));
  }
}

@media (prefers-reduced-motion: reduce) {
  .c_slider_swiper .card_hover_img_wrap img {
    transition: none;
  }
}

/* Hover tint. A pseudo inside the image wrap, not a layer of its own, so
   it is clipped by the same rounded corners and rides the image's scale.
   Always on, never animated: the colour panel above it is what reveals
   the picture, and a tint that fades in on the same timer shows the
   image bare for the first frames. Tinted underneath, the photo arrives
   already dark. Pointer-fine only: on touch the panel never lifts. */
@media (hover: hover) and (pointer: fine) {
  .card_hover_img_wrap::after {
    content: "";
    position: absolute;
    inset: 0;
    background-color: #000;
    opacity: var(--card-tint, 0.3);
    pointer-events: none;
  }
}

/* No pointer, no reveal. The section embed's own rule is a plain
   :hover, and a touch browser hands out :hover on tap and keeps it until
   something else is tapped — so the colour panel lifted and the photo
   underneath sat there on a phone, on whichever card was touched last.

   Held shut from here rather than in the embed: the panel is the card on
   touch, and the picture is only ever a hover reward. Specific enough to
   beat the embed's :hover without !important, and matched on the rest
   state too, so a card that is mid-fade when a finger lands still closes.

   (hover: none) and nothing else. any-pointer: coarse was here to catch
   the finger on a laptop with a touchscreen, and it caught the laptop:
   a machine with a screen you can poke answers true whatever it is being
   driven with, so the lean went off on every desktop Chrome that had
   one. hover reads the pointer actually in charge, which is the mouse —
   and where a mouse is in charge the sticky :hover this guards against
   cannot happen, because the hover is real. */
@media (hover: none) {
  .c_slider_swiper .card_hover_wrap .card_hover_bg_hover,
  .c_slider_swiper .card_hover_wrap:hover .card_hover_bg_hover,
  .c_slider_swiper .card_hover_wrap:focus-within .card_hover_bg_hover {
    opacity: 1;
    transition: none;
  }

  /* The art stays put under it: with the panel down there is nothing to
     see, and a lean that cannot be seen is a transition nobody asked the
     phone to run. */
  .c_slider_swiper .card_hover_img_wrap img,
  .c_slider_swiper .card_hover_wrap:hover .card_hover_img_wrap img {
    transform: none;
    transition: none;
  }
}

/* The card logos are drawn at whatever size their file happens to be —
   30px tall to 101px across the set — and the row they sit in is 4rem.
   The tall ones overflowed it and ate the margin under the row, which is
   why the gap under a logo looked different card to card.

   Capped, not stretched: a logo's own size is a decision somebody made
   in the file, and forcing them all to one height made the light ones
   loom and the heavy ones shrink. Only the ones that do not fit are
   touched, and they keep their ratio on the way down.

   The embed between the row and the svg has to be allowed to shrink, or
   it holds the old width open under a narrower picture. */
/* Sat on the floor of the row rather than centred in it. The logos are
   different heights by design now — the same ink, not the same height —
   and centring spread that difference across both sides, so the gap
   under a short logo was bigger than under a tall one and no two cards
   agreed. Aligned to the bottom, that gap is the row's margin every
   time and the difference goes above, where the card has air anyway.

   Both levels: the embed is the row's flex item, the svg is the embed's
   if a rule made that one a flex container too. */
.card_hover_wrap .card_hover_logo_wrap {
  align-items: flex-end;
}

.card_hover_wrap .card_hover_logo_wrap .card_hover_logo_svg {
  min-width: 0;
  align-self: flex-end;
}

/* Two caps, not one. Height alone cannot balance a set like this: a
   stacked lockup and a horizontal wordmark at the same height read
   nothing alike, and the wide ones came out looming. Capped both ways, a
   wordmark is stopped by the width and ends up shorter, a square mark by
   the height — which is how a wall of logos is usually evened out.

   Nothing is scaled up. A logo smaller than both caps is left at the
   size its file asks for, since that size was somebody's decision. */
.card_hover_wrap .card_hover_logo_wrap .card_hover_logo_svg svg {
  display: block;
  width: auto;
  height: auto;
  max-height: var(--card-logo-max-h, 3rem);
  max-width: var(--card-logo-max-w, 11rem);

  /* Not stretch: a stretched flex item is sized by its line rather than
     by itself, and with a ratio to keep that came out as width — a
     207x30 logo drawn 442 wide to fill a 4rem row. Any other value
     leaves it at its own size, and the bottom is the one that lines the
     row up. Harmless where the wrapper is not a flex container. */
  align-self: flex-end;
}

/* The card already animates its colour, so this follows it with the
   alpha baked in rather than running a timer of its own. */
.c_slider_swiper .card_hover_wrap .card_hover_details_name,
.c_slider_swiper .card_hover_wrap .u-color-secondary {
  color: color-mix(in srgb, currentColor 60%, transparent) !important;
  opacity: 1 !important;
  transition: none !important;
}

.c_slider_swiper .card_hover_wrap .card_hover_details_position {
  transition: none !important;
}

/* Arrows */
.c_slider_button_prev,
.c_slider_button_next {
  background-color: var(--_theme---swiper--bg);
  border: 1px solid var(--_theme---swiper--border, currentColor);
  cursor: pointer;
  transition: background-color 200ms, border-color 200ms;
}

.c_slider_button_prev svg,
.c_slider_button_next svg {
  color: var(--_theme---swiper--icon);
  transition: color 200ms;
}

@media (hover: hover) {
  .c_slider_button_prev:not(.is-inactive):hover,
  .c_slider_button_next:not(.is-inactive):hover {
    background-color: var(--_theme---swiper--bg-hover);
    border-color: var(--_theme---swiper--border-hover);
  }

  .c_slider_button_prev:not(.is-inactive):hover svg,
  .c_slider_button_next:not(.is-inactive):hover svg {
    color: var(--_theme---swiper--icon-hover, var(--_theme---swiper--icon));
  }
}

.c_slider_button_prev.is-inactive,
.c_slider_button_next.is-inactive {
  background-color: var(--_theme---swiper--bg-inactive);
  border-color: var(--_theme---swiper--border-inactive, var(--_theme---swiper--border, currentColor));
  cursor: not-allowed;
  pointer-events: all;
}

.c_slider_button_prev.is-inactive svg,
.c_slider_button_next.is-inactive svg {
  color: var(--_theme---swiper--icon-inactive, color-mix(in srgb, currentColor 40%, transparent));
}

/* Designer preview, where Swiper is not running. */
.wf-design-mode .c_slider_swiper_wrap {
  display: flex;
  gap: 120px;
}

.wf-design-mode .c_slider_swiper_wrap > * {
  width: calc((100% - 120px) / 1.5);
  flex-shrink: 0;
}

/* Opt-in mobile inset, zero by default: an inset here is what stops the
   rail reaching the edges, and the first card is aligned by
   slidesOffsetBefore instead. */
@media (max-width: 767px) {
  .c_slider_offset {
    padding-left: var(--slider-inset-mobile, 0px);
    padding-right: var(--slider-inset-mobile, 0px);
  }
}

/* ===== TABS — stacked below 992px — README ### tabs ===== */

/* The module moves each visual beside its own text and adds .is-stacked;
   these undo the desktop presentation, where the visuals are a pile of
   absolutely positioned panels. */

/* One column: on desktop the wrapper is two, and the stack would sit in
   half the width beside the emptied visual column. */
[data-tabs="wrapper"].is-stacked {
  display: block;
}

[data-tabs="wrapper"].is-stacked .is-tabs-visuals-empty {
  display: none;
}

[data-tabs="wrapper"].is-stacked [data-tabs="content-item"] {
  width: 100%;
  max-width: none;
}

[data-tabs="wrapper"].is-stacked [data-tabs="visual-item"] {
  position: static;
  inset: auto;
  width: 100%;
  opacity: 1;
  visibility: visible;
  transform: none;
}

[data-tabs="wrapper"].is-stacked [data-tabs="item-details"] {
  height: auto;
  overflow: visible;
}

[data-tabs="wrapper"].is-stacked [data-tabs="item-progress"] {
  display: none;
}

/* Nothing is selectable, so nothing should look it. The divider goes
   too: it is the item's border-top, and stacked, the visual is the
   item's first child, so the line sat on top of every picture. */
[data-tabs="wrapper"].is-stacked [data-tabs="content-item"] {
  cursor: auto;
  border-top: 0;
}

/* ===== SERVICES — slider below 992px — README ### servicesHover ===== */

/* The module wraps the rows in a Swiper and adds .is-slider to the list
   and .is-services-slider to the section. These undo the Designer's
   tablet rows — a 100vh row with the image as its background under a
   gradient — into a card: image, then the square and the name, then the
   list, dark type on the section's colour. The Designer preview is a
   separate html.wf-design-mode embed in the section. */

@media screen and (max-width: 991px) {
  .services_wrap.is-services-slider {
    background-color: var(--services-slider-bg, var(--_colour---color--color-neon));
    color: var(--services-slider-text, #191915);
  }

  .services_wrap.is-services-slider[data-services-color="green"] {
    --services-slider-text: var(--_colour---color--color-paper, #f7f7f5);
  }

  /* The square may be a filled div or an SVG; both routes are covered. */
  .is-services-slider[data-services-color="green"] .services_hover_icon {
    background-color: var(--services-slider-text);
    color: var(--services-slider-text);
  }

  .is-services-slider[data-services-color="green"] .services_hover_icon * {
    fill: currentColor;
  }

  .is-services-slider .services_contain,
  .is-services-slider .g_heading {
    color: inherit;
  }

  /* Swiper's offsets carry the inset, so the next card runs off the
     screen's edge rather than stopping at the container's margin. */
  .services_hover_items.is-slider {
    height: auto;
    width: auto;
    max-width: none;
    margin-top: var(--_sizes---space--9-64);
    margin-left: 0;
    margin-right: 0;
    padding-left: 0;
    padding-right: 0;
  }

  .services_hover_items.is-slider .services_hover_item {
    display: flex;
    flex-direction: column;
    height: auto;
    border: 0;
    background: none;
  }

  .services_hover_items.is-slider .services_hover_img_wrap {
    order: -1;
    position: relative;
    inset: auto;
    width: 100%;
    max-width: none;
    height: auto;
    aspect-ratio: 1;
    overflow: hidden;
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  .services_hover_items.is-slider .services_hover_img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .services_hover_items.is-slider .services_hover_img_overlay {
    display: none;
  }

  /* Flex, not the Designer's grid: its cells are placed by ID, which no
     class rule can move, and its tablet style runs them as a column. The
     square and the name share the first line, the list takes the next. */
  .services_hover_items.is-slider .services_hover_inner {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    column-gap: 0.75rem;
    row-gap: var(--_sizes---space--7-32);
    height: auto;
    margin-top: var(--_sizes---space--8-48);
    padding: 0;
    color: inherit;
  }

  .services_hover_items.is-slider .services_hover_right {
    flex: none;
    width: auto;
    justify-content: flex-start;
  }

  .services_hover_items.is-slider .services_hover_icon_wrap {
    display: block;
  }

  .services_hover_items.is-slider .services_hover_icon {
    transform: scale(1);
  }

  .services_hover_items.is-slider .services_hover_service {
    flex: 1 1 0;
    min-width: 0;
  }

  /* The name is h3 in the slider, h4 on desktop. The utility classes set
     their size variables on the element itself, so those are redefined
     there too; the square beside it reads the same size for its 1cap. */
  .services_hover_items.is-slider :is(
    .services_hover_right,
    .services_hover_service,
    .services_hover_service :is(.g_heading, .u-text-style-h4, .u-text-style-h4-secondary)
  ) {
    --_heading-primary---font-size: var(--_typography---font-size--h3-primary);
    --_heading-secondary---font-size: var(--_typography---font-size--h3-secondary);
    --_heading-primary---line-height: var(--_typography---line-height--1-3);
    --_heading-secondary---line-height: var(--_typography---line-height--1-3);
    --_heading-primary---letter-spacing: var(--_typography---letter-spacing--negative-l);
    --_heading-secondary---letter-spacing: var(--_typography---letter-spacing--negative-l);
    font-size: var(--_typography---font-size--h3-secondary);
    line-height: var(--_typography---line-height--1-3);
  }

  .services_hover_items.is-slider .services_hover_types {
    flex: 0 0 100%;
  }

  /* The arrows sit space 9 below the cards. The spacer that came with
     the copied arrow block would add its own on top. */
  .is-services-slider [data-services-nav] {
    margin-top: var(--_sizes---space--9-64);
  }

  .is-services-slider [data-services-nav] > .g_section_space {
    display: none;
  }

  .is-services-slider .is-inactive {
    opacity: 0.4;
    pointer-events: none;
  }
}

/* The arrows are the slider's; on desktop there is no slider. Put
   data-services-nav on their wrapper to hide it with them. */
@media screen and (min-width: 992px) {
  .services_wrap [data-services-nav],
  .services_wrap [data-services-prev],
  .services_wrap [data-services-next] {
    display: none;
  }
}

/* Mobile portrait: the heading gives up whatever width cap it carries and
   takes the screen. A Designer width that reads as restraint at 430px is
   two words a line at 390. */
@media screen and (max-width: 479px) {
  .services_wrap .services_heading_wrap,
  .services_wrap .services_heading_wrap .g_heading {
    width: 100%;
    max-width: none;
  }
}

/* The type lists are Webflow rich text. Leading is set here rather than
   inherited from the wrapper: the wrapper carries a heading style, and a
   heading's leading on a list of body lines is the tight look. */
.services_hover_item_text > * {
  line-height: var(--services-types-leading, 1.5);
}

/* Tablet and down the entries stack on a gap instead, so the margin that
   set their spacing is the one thing that goes. */
@media screen and (max-width: 991px) {
  .services_hover_item_text {
    display: flex;
    flex-direction: column;
    gap: var(--services-types-gap, 8px);
  }

  .services_hover_item_text > * {
    margin-top: 0;
    margin-bottom: 0;
  }
}

/* The row icon grows out of its middle, keeping its square either way: a
   transform never reflows, so the row does not jump as the pointer
   arrives. Sized from a variable rather than an em, which would resolve
   against the wrapper — the type style sits on the text element. */
.services_hover_item .services_hover_icon {
  --services-icon-size: 1.25rem;
  --services-icon-ease: cubic-bezier(0.625, 0.05, 0, 1);

  flex: none;
  width: var(--services-icon-size);
  height: var(--services-icon-size);
  aspect-ratio: 1;
  transform: scale(0);
  transform-origin: center;
  transition: transform 0.45s var(--services-icon-ease);
}

/* The icon has a column of its own beside the heading, top-aligned in the
   row, so its top met the line's top and its middle sat (line - icon) / 2
   above the text's. One heading line tall and centred instead. The type
   variables are the h4 heading's; change them with the heading's style. */
.services_hover_right {
  font-family: var(--_typography---font--secondary-family);
  font-size: var(--_typography---font-size--h4-primary);
  line-height: var(--_typography---line-height--1-25);
  height: 1lh;
  align-items: center;
}

/* A capital beside the capitals: cap height tall, and lifted off the
   line box's middle onto theirs. Suisse Intl's content area (ascent 1.174,
   descent 0.556 of the em) puts the middle of the caps 0.054em above the
   line box's, so the square's top and bottom land on the cap line and the
   baseline. From the font file, whose metrics are the same on every
   platform; 0.725em is its cap height, for anything without the unit. */
.services_hover_right .services_hover_icon {
  --services-icon-size: 1cap;
  position: relative;
  top: -0.054em;
}

@supports not (height: 1cap) {
  .services_hover_right .services_hover_icon {
    --services-icon-size: 0.725em;
  }
}

@media (hover: hover) and (min-width: 992px) {
  .services_hover_item:hover .services_hover_icon,
  .services_hover_item:focus-within .services_hover_icon {
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .services_hover_item .services_hover_icon {
    transition-duration: 0.01ms;
  }
}

/* Text swap: one grid cell, so the statements stack and the tallest still
   sets the height. Absolute children would collapse the wrapper. */
[data-swap].is-swapping {
  display: grid;
}

/* Stack mode — HERO_VIDEO.textMode. The statements are a column rather
   than a pile in one cell: they arrive together and leave together, so
   there is nothing to swap and they need a gap instead of a shared
   grid area. The token first, then the number it holds today, so the
   spacing follows the system where the system is readable. */
[data-swap].is-stacked {
  display: grid;
  gap: var(--_sizes---space--7, 2rem);
}

/* One row each. The Designer places both statements in row 1 — they
   were a pile to swap between, and its placement is written against each
   child's node id, which outranks any class. !important is the only
   thing that outranks an id, and the alternative is asking somebody to
   remember to clear the grid position on two elements in Webflow. */
[data-swap].is-stacked > * {
  grid-area: auto !important;
  grid-column: 1 / -1 !important;
}

/* One explicit track, so the JS placing statements at 1 / -1 has an end
   line to reach: implicit columns resolve -1 back to line 1. */
@media (max-width: 767px) {
  [data-swap].is-swapping {
    grid-template-columns: 1fr;
  }
}

/* Video poster handoff (docs: README ### videoPoster). display is pinned
   !important because video-min.js sets display:none inline the moment it
   plays, and an element out of flow cannot be faded — this makes its
   toggle a no-op and leaves opacity to decide. */
[data-video="component"] [data-video="poster"] {
  display: block !important;
  opacity: 1;
  transition: opacity 400ms ease;
  pointer-events: none;
}

[data-video="component"].is-video-playing [data-video="poster"] {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  [data-video="component"] [data-video="poster"] {
    transition-duration: 0.01ms;
  }
}

/* Share menu (docs: README ### share). Hidden here rather than inline, so
   the Designer can restyle it without the JS having an opinion. */
[data-share] {
  position: relative;
}

/* data-share-swap: trigger, menu and confirmation take turns in ONE grid
   cell, so the menu opens where the trigger was and nothing on the page
   moves. Opt-in, and the three must be DIRECT children — grid placement
   does not reach through a div. */
[data-share][data-share-swap] {
  display: grid;
  justify-items: start;
  align-items: start;
}

[data-share][data-share-swap] > [data-share-open],
[data-share][data-share-swap] > [data-share-menu],
[data-share][data-share-swap] > [data-share-copied] {
  grid-area: 1 / 1;
}

[data-share][data-share-swap] > [data-share-open] {
  transition: opacity 200ms ease;
}

/* Held back through the confirmation as well as the menu, so the three
   states come one at a time. */
[data-share][data-share-swap].is-share-open > [data-share-open],
[data-share][data-share-swap].is-share-copied > [data-share-open] {
  opacity: 0;
  pointer-events: none;
}

[data-share-menu] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 200ms ease, visibility 200ms;
}

/* Secondary to the thing that opened them: the trigger is the only one
   that reads at full strength. */
[data-share].is-share-open [data-share-menu] {
  opacity: 0.5;
  visibility: visible;
  pointer-events: auto;
}

/* One line at every width: two words that wrap read as a layout accident,
   and on desktop the swap holds the trigger's cell, which a second line
   would grow. */
[data-share-copied] {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity 200ms ease, visibility 200ms;
}

[data-share].is-share-copied [data-share-copied] {
  opacity: 1;
  visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
  [data-share-menu],
  [data-share-copied],
  [data-share][data-share-swap] > [data-share-open] {
    transition-duration: 0.01ms;
  }
}

/* Tablet and down the menu runs across, not down: stacked, it is a tall
   panel over a short trigger, and there is width to spare on a phone.
   Wrapping so a long label cannot push an action off the screen.

   The confirmation takes the same row: it stands in for the menu, so it
   should read at the same height rather than a line below where the
   actions just were. Its own text never wraps at any width — see the base
   rule. */
@media screen and (max-width: 991px) {
  [data-share-menu],
  [data-share-copied] {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--share-menu-gap, 1rem);
  }

  [data-share-copied] {
    flex-wrap: nowrap;
  }
}

/* Mobile: the menu and the confirmation leave the flow, so a wrapper is
   only ever as tall as its trigger. Hidden is not weightless —
   visibility:hidden still reserves every item's height, and a second
   share block on a phone pays for that twice. */
@media (max-width: 767px) {
  [data-share] > [data-share-menu],
  [data-share] > [data-share-copied] {
    position: absolute;
    top: 0;
    left: 0;
  }
}

/* ===== TEXT HOLD — README ### textAnim ===== */

/* Marked text is hidden by textAnim at mount, which is a frame or two
   after the first paint — long enough to see every heading in the hero
   drawn and then snatched away. This holds it from the stylesheet, which
   the browser has before it paints anything.

   from/to and no fill, the same shape the video's hold uses: the
   animation expires by itself, so a page the bundle never reaches shows
   its text a few seconds late rather than never. textAnim drops it at
   mount and takes over the hiding with visibility.

   A Webflow component carries its attribute everywhere it is placed and
   arrives switched off as "false", which marks nothing — those are left
   alone, or they would be held for three seconds and then appear.

   html:not(.wf-design-mode) so the Designer canvas keeps its text: an
   element parked at opacity 0 is invisible to design against. */
@keyframes text-anim-hold {
  from, to { opacity: 0; }
}

html:not(.wf-design-mode) :is(
  [data-text-anim-heading]:not([data-text-anim-heading="false"]),
  [data-text-anim-solo]:not([data-text-anim-solo="false"]),
  [data-text-anim-body]:not([data-text-anim-body="false"]),
  [data-text-anim-list]:not([data-text-anim-list="false"])
) {
  animation: text-anim-hold var(--text-anim-hold, 3s) linear;
}

/* Nothing is hidden and nothing is revealed under reduced motion, so
   there is nothing to hold. */
@media (prefers-reduced-motion: reduce) {
  html :is(
    [data-text-anim-heading],
    [data-text-anim-solo],
    [data-text-anim-body],
    [data-text-anim-list]
  ) {
    animation: none;
  }
}

/* ===== HERO VIDEO — README ### heroVideo ===== */

/* Hidden from first paint: the module's pre-hide is JS, so between the
   paint and the bundle landing the video showed at full opacity.

   A watchdog, not a state, and deliberately without a fill — the hold
   ends by itself, so a page that never gets the script shows the video
   rather than an empty cell. heroVideo drops the animation at mount,
   which is what lets the inline tween write opacity at all. */
@keyframes hero-video-hold {
  from, to { opacity: 0; }
}

html:not(.wf-design-mode) .home_wrap [data-video="component"] {
  animation: hero-video-hold var(--hero-video-hold, 3s) linear;
}

@media (prefers-reduced-motion: reduce) {
  html:not(.wf-design-mode) .home_wrap [data-video="component"] {
    animation: none;
  }
}

/* In the grid until heroVideo mounts, and for good if it never does: the
   component ships u-cover-absolute, which contributes no height, so the
   cell collapsed around it. --hero-video-ratio sets the shape. */
.home_wrap [data-video="component"] {
  position: relative;
  inset: auto;
  width: 100%;
  height: auto;
  aspect-ratio: var(--hero-video-ratio, 16 / 9);
}

/* In the section it grows into, the component is the video's own shape
   just covering it, centred; heroVideo writes that box, and a transform
   and a crop that take it from the hero cell to the whole section. */
[data-video="component"].is-staged {
  position: absolute;
  inset: auto;
  z-index: 1;
  transform-origin: 50% 50%;
  will-change: transform, clip-path;
  backface-visibility: hidden;
}

[data-video="component"].is-staged .g_visual_video,
[data-video="component"].is-staged [data-video="poster"] {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Reduced motion: no growth, the video simply is the section. */
[data-video="component"].is-hero-video-static {
  position: absolute;
  inset: 0;
}

/* ===== INLINE HEADING PICTURES — .hero-h1__img ===== */

/* A picture set into a line of type is a box the heading laid out, and
   the file inside it almost never has that exact ratio. Nothing was
   fitting it or clipping it, so the excess painted past the wrapper —
   the strip of image hanging off the last one, which is only the last
   one because that is where the leftover falls.

   cover, not contain: the box is the design, and a letterboxed picture
   in the middle of a word reads as a mistake. clip rather than hidden,
   for the reason it always is here — hidden makes the element a
   scrollport. */
.hero-h1__img {
  overflow: clip;
}

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

/* ===== HERO ENTRANCE — [data-hero-in] — README ### hero entrance ===== */

/* The home hero's arrival, lifted out of its embed so any hero can wear
   it: the picture opens from a line at its middle while growing out of
   nothing. Two animations rather than one, because they run on their own
   clocks — the clip finishes a beat after the scale, which is what stops
   the edge arriving with the picture already at full size.

   Put [data-hero-in] on the picture, or on the wrapper around it.
   [data-hero-in-group] on their parent orders them: each child takes the
   next slot, lead + i * step, the same shape SEQUENCE uses in the JS.

   html:not(.wf-design-mode) so the Designer canvas is left alone — an
   element parked at opacity 0 is invisible to design against. */

@keyframes hero-in {
  from { opacity: 0; clip-path: inset(50% 50% 50% 50%); }
  to   { opacity: 1; clip-path: inset(0% 0% 0% 0%); }
}

@keyframes hero-scale {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}

html:not(.wf-design-mode) [data-hero-in],
html:not(.wf-design-mode) [data-hero-in] > img,
html:not(.wf-design-mode) [data-hero-in] > video {
  transform-origin: center;
}

html:not(.wf-design-mode) [data-hero-in] {
  animation:
    hero-in var(--hero-in-open, 1s) var(--ease-inout-mask)
      var(--hero-in-delay, var(--hero-in-lead, 0.25s)) both,
    hero-scale var(--hero-in-grow, 0.8s) var(--ease-inout-mask)
      var(--hero-in-delay, var(--hero-in-lead, 0.25s)) both;
}

/* Slots, written out rather than counted: sibling-index() would do this
   in one line and is too new to rely on. Eight is more pictures than a
   hero has; a ninth simply shares the eighth's cue. */
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(1) { --hero-in-i: 0; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(2) { --hero-in-i: 1; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(3) { --hero-in-i: 2; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(4) { --hero-in-i: 3; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(5) { --hero-in-i: 4; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(6) { --hero-in-i: 5; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(7) { --hero-in-i: 6; }
html:not(.wf-design-mode) [data-hero-in-group] > *:nth-child(n+8) { --hero-in-i: 7; }

html:not(.wf-design-mode) [data-hero-in-group] [data-hero-in],
html:not(.wf-design-mode) [data-hero-in-group] > [data-hero-in] {
  --hero-in-delay: calc(
    var(--hero-in-lead, 0.25s) + var(--hero-in-i, 0) * var(--hero-in-step, 0.15s)
  );
}

@media (prefers-reduced-motion: reduce) {
  html [data-hero-in] {
    animation: none;
    opacity: 1;
  }
}

/* ===== HERO ENTRANCE ORDER ===== */

/* The entrance itself is a @keyframes in the .home_wrap embed; only the
   delays are taken over here, so the sequence lives next to the video's —
   the one cell GSAP drives, which cannot read a number out of the
   Designer.

   Uniform spacing, scattered order:  1->2  2->4  3->0  4->3  5->1  6->5
   so the six read as arriving rather than counting.

   The video takes the LAST slot by necessity: its fade is a GSAP tween
   off the intro queue, so its delay starts from a later zero by however
   long the boot took, and that drift only ever pushes it later.

   The leading `html` wins the tie against the section's own embed, and
   `nth-child(N of .home_img_wrap)` counts cells rather than siblings, so
   a heading in the grid cannot shift the order by one. */

/* The hero grid overlaps its own cells at some widths — the picture
   cells are placed over the heading's rows, and which of the two paints
   last was only ever DOM order. Pinned here so the words are never the
   thing underneath.

   The cells take an explicit 0 rather than the heading alone taking a 1:
   .home_img_wrap is transformed by the parallax, which makes it a
   stacking context, and a transformed cell later in the grid outpaints
   an untransformed heading on its own.

   pointer-events: none, because the heading's box spans cells it does
   not visually cover, and on top it would eat the hover the image bump
   binds to. Links inside it are handed back. */
.home_wrap .home_img_wrap {
  position: relative;
  z-index: 0;
}

.home_wrap h1 {
  position: relative;
  z-index: 2;
}

.home_wrap :is(h1, .home_heading_wrap) {
  pointer-events: none;
}

.home_wrap :is(h1, .home_heading_wrap) a {
  pointer-events: auto;
}

.home_wrap {
  --hero-in-lead: 0.15s;   /* before the first cell moves */
  --hero-in-step: 0.1s;    /* between one cell and the next */
  --hero-in-video-slot: 5; /* read by heroVideo for its own delay */
}

html .home_wrap .home_img_wrap:nth-child(1 of .home_img_wrap) img {
  animation-delay: calc(var(--hero-in-lead) + 2 * var(--hero-in-step));
}

html .home_wrap .home_img_wrap:nth-child(2 of .home_img_wrap) img {
  animation-delay: calc(var(--hero-in-lead) + 4 * var(--hero-in-step));
}

html .home_wrap .home_img_wrap:nth-child(3 of .home_img_wrap) img {
  animation-delay: var(--hero-in-lead);
}

html .home_wrap .home_img_wrap:nth-child(4 of .home_img_wrap) img {
  animation-delay: calc(var(--hero-in-lead) + 3 * var(--hero-in-step));
}

html .home_wrap .home_img_wrap:nth-child(5 of .home_img_wrap) img {
  animation-delay: calc(var(--hero-in-lead) + 1 * var(--hero-in-step));
}

/* The video's cell: nothing animates here, but a seventh cell — or a
   sixth that is not the video — still wants a place in the sequence. */
html .home_wrap .home_img_wrap:nth-child(6 of .home_img_wrap) img,
html .home_wrap .home_img_wrap:nth-child(n+7 of .home_img_wrap) img {
  animation-delay: calc(var(--hero-in-lead) + var(--hero-in-video-slot) * var(--hero-in-step));
}

/* ===== CTA — README ### ctaReveal ===== */

/* The section's embed sets height: 200vh !important; the pin length
   belongs with the motion it paces, so it comes from --cta-scroll.
   !important answers the embed's, and the leading html wins the tie. */

html .cta_wrap {
  height: var(--cta-scroll, 200vh) !important;
  /* The tint is absolute against this, so the section is the containing
     block rather than the sticky screen inside it. */
  position: relative;
}

/* A layer rather than the section's background: one element cannot
   cross-fade between two backgrounds, so both colours stay named in the
   Designer. --cta-tint retargets it. */
.cta_bg_tint {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  background: var(--cta-tint, var(--_colour---color--color-neon));
  pointer-events: none;
}

/* Positioned paints over unpositioned, so without this the wash covers
   the content it belongs behind. */
.cta_contain > .cta_content_wrap {
  position: relative;
  z-index: 1;
}

/* Black under the last sticky card — see stickyStack. */
.sticky_scrim {
  position: absolute;
  inset: 0;
  z-index: 10;
  opacity: 0;
  background: var(--sticky-scrim, #000);
  pointer-events: none;
}

/* How long each card holds before the next reaches it — the gap sits
   behind the stuck card, so it never shows.

   ABOVE the next element, never below the sticky one: a sticky box is
   constrained by its containing block inset by its own margins, so a
   margin-bottom shortens the range it can stay stuck for. */
@media (min-width: 992px) {
  html .design_sticky_track > .design_sticky_item + * {
    margin-top: var(--sticky-hold, 55vh);
  }

  /* Sticky makes a stacking context, so the cards paint in the
     positioned layer and a static section below them stays underneath.
     This is what puts the work section over the card it climbs. */
  html .design_sticky_track > .work_wrap {
    position: relative;
    z-index: 1;
  }
}

/* No hover icon on touch: a tap fires :hover and leaves it stuck, and
   where the effect is an IX2 interaction there is nothing to override —
   it writes inline. The icon carries no meaning of its own. */
@media (hover: none), (max-width: 991px) {
  .meganav_root .footer_link_hover_icon,
  .meganav_root .button_link_icon {
    display: none !important;
  }
}

/* Footer and open menu: every link at 70% of the paper colour, the
   current page and the one under the pointer at full. Set on the wrap,
   not the text, so the square icon — drawn in currentColor — dims with
   it. A fixed colour rather than --_theme---navlinks--text-inactive: in
   the dark theme that variable is the full paper colour, so nothing
   stepped back and the current page stopped standing out, and in the
   menu it resolved to the light theme's dark ink. w--current lands on the
   invisible overlay anchor, a sibling of the text, so the row is matched
   on containing it. */
.footer_wrap .footer_link_wrap,
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap {
  --link-on: #f7f7f5;
  color: color-mix(in srgb, var(--link-on) 70%, transparent);
  transition: color 0.3s ease;
}

.footer_wrap .footer_link_text,
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_text {
  color: inherit;
}

.footer_wrap .footer_link_wrap:has(a.is-current),
.footer_wrap .footer_link_wrap:has(a.w--current),
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap:has(a.is-current),
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap:has(a.w--current) {
  color: var(--link-on);
}

/* A pointer lights the link it is on. A touchscreen gets the tap
   instead: :hover sticks after a tap, and would leave a second link lit
   beside the current one. */
@media (hover: hover) {
  .footer_wrap .footer_link_wrap:hover,
  .meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap:hover {
    color: var(--link-on);
  }
}

.footer_wrap .footer_link_wrap:active,
.footer_wrap .footer_link_wrap:has(a:focus-visible),
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap:active,
.meganav_root .meganav.is-open .meganav_links_wrap .footer_link_wrap:has(a:focus-visible) {
  color: var(--link-on);
}

/* The current page wears its icon permanently, in the footer and the
   open menu, at the same end state the hover animates to. The menu hides
   its icons below 992px and on touch, and that rule still wins there.
   Values mirrored from the component's own rule. */
.footer_wrap .footer_link_wrap:has(a.is-current) .footer_link_hover_icon,
.footer_wrap .footer_link_wrap:has(a.w--current) .footer_link_hover_icon,
.meganav_root .meganav_links_wrap .footer_link_wrap:has(a.is-current) .footer_link_hover_icon,
.meganav_root .meganav_links_wrap .footer_link_wrap:has(a.w--current) .footer_link_hover_icon {
  width: var(--icon-size);
  margin-right: var(--icon-gap);
}

.footer_wrap .footer_link_wrap:has(a.is-current) .footer_link_hover_icon::before,
.footer_wrap .footer_link_wrap:has(a.w--current) .footer_link_hover_icon::before,
.meganav_root .meganav_links_wrap .footer_link_wrap:has(a.is-current) .footer_link_hover_icon::before,
.meganav_root .meganav_links_wrap .footer_link_wrap:has(a.w--current) .footer_link_hover_icon::before {
  transform: translateX(0);
  opacity: 1;
}

/* Corporate hero, mobile images: the home hero's entrance borrowed —
   same 0.6 scale, staggered down the block. The scroll drift is the
   corporateHero module. */
/* Below 992 these pictures leave the heading and stack as blocks of
   their own, and they wear the shared hero entrance — same two keyframes
   the home hero uses, so there is one definition of what arriving looks
   like. Above the breakpoint they are inline in the heading instead,
   where textAnim scales them with the line they ride. */

@media screen and (max-width: 991px) {
  /* The inline images leave with the desktop heading — lifted out of the
     section embed so the breakpoint is one number in one place. */
  .corporate_wrap .hero-h1__img,
  .corporate_heading.is-hero {
    display: none;
  }

  html:not(.wf-design-mode) .corporate_images_mobile_img_wrap {
    animation:
      hero-in var(--hero-in-open, 1s) var(--ease-inout-mask)
        var(--corp-hero-lead, 0.15s) both,
      hero-scale var(--corp-hero-duration, 0.8s) var(--ease-inout-mask)
        var(--corp-hero-lead, 0.15s) both;
    transform-origin: center center;
  }

  html .corporate_images_mobile_img_wrap:nth-child(2) {
    animation-delay: calc(var(--corp-hero-lead, 0.15s) + var(--corp-hero-step, 0.1s));
  }

  html .corporate_images_mobile_img_wrap:nth-child(3) {
    animation-delay: calc(var(--corp-hero-lead, 0.15s) + 2 * var(--corp-hero-step, 0.1s));
  }
}

@media (prefers-reduced-motion: reduce) {
  .corporate_images_mobile_img_wrap {
    animation: none;
    opacity: 1;
  }
}

/* Eyebrow icon: eyebrowIcon sizes the square from the text beside it
   through --icon-size / --icon-gap. The fallbacks cover a dead script. */
.icon_eyebrow_wrap,
.design_sticky_eyebrow,
.stats_eyebrow_wrap,
.subheading_eyebrow_wrap,
.c_title_text_eyebrow_wrap {
  display: flex;
  align-items: center;
  gap: var(--icon-gap, 0.5em);
}

.icon_eyebrow_item,
.design_sticky_blcvk,
.stats_eyebrow_icon,
.subheading_block {
  flex: none;
  width: var(--icon-size, 0.72em);
  height: var(--icon-size, 0.72em);
}

/* The measured cap height, where the rest approximate it at 0.72. */
.c_title_text_eyebrow_color {
  flex: none;
  width: var(--icon-size, 0.72em);
  height: var(--icon-size, 0.72em);
}

/* An unvariant square takes the text's colour. Scoped away from the
   variants, which set background-color directly and lose to this file's
   position in the cascade. */
.icon_eyebrow_item:not([class*="w-variant"]),
.design_sticky_blcvk:not([class*="w-variant"]) {
  background-color: var(--icon-color, currentColor);
}

/* This one's variants are combo classes rather than Webflow variants, so
   .is-purple and its siblings are what has to be scoped away. */
.c_title_text_eyebrow_color:not([class*="w-variant"]):not([class*="is-"]) {
  background-color: var(--icon-color, currentColor);
}

/* ===== CONSENT — Finsweet's internal component ===== */

/* An empty holder Finsweet keeps for its own use, sitting in flow after
   the footer: at opacity 0 it still left a blank strip under the footer,
   taller wherever its placeholder was. The banner is not in it. */
[fs-consent-element="internal-component"] {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}
