/* --- The type scale -------------------------------------------------------
   The design system's, restated here because the theme's is a different scale and the
   two had drifted badly enough that components were being given sizes by hand:

                    theme was    tokens say
     display          64            80
     h1               50            44
     h2               28            38
     h3               26 / lh 1.4   32 / lh 1.5
     h4               20            20   (tracking was 0, should be -1%)
     h5               22            22   (regular, should be semibold)
     h6               18            18
     body             15            16
     small            14            14

   h2 at 28 against h3 at 26 is the one that made everything look muddled: two steps
   two pixels apart is not a hierarchy, so every section that needed a real heading
   reached past the scale for a hand-written size. This is that fixed at the root.

   Held as custom properties so a component that the frame specifies as "h2" can say
   `var(--fs-h2)` rather than repeat a number that then goes stale. Every hand-written
   size in this file has been replaced by one of these.

   Sizes scale between 1024 and 1440, which is the band the theme already used. The
   floors are ours — the frames are drawn at one width only — and they keep the steps in
   order at every size, which the old ones did not: h2 bottomed out at 22 and h3 at 24,
   so on a phone h3 was bigger than h2.

   Tracking is a percentage of the size, as the tokens specify it: -2% on display, -1% on
   h1 to h4, none on h5 and h6, and +1% on body copy.                                   */

:root {
  --fs-d: clamp(36px, 36px + 52 * (100vw - 1024px) / 416, 88px);
  --fs-h1: clamp(32px, 32px + 12 * (100vw - 1024px) / 416, 44px);
  --fs-h2: clamp(28px, 28px + 10 * (100vw - 1024px) / 416, 38px);
  --fs-h3: clamp(24px, 24px + 8 * (100vw - 1024px) / 416, 32px);
  /*
   * h4 was 18 to 20, which put it BELOW h5 at every width above the floor and made the
   * step down from h3 a cliff: 32 to 20 is a 1.6 drop where every step above it is 1.15.
   * A scale whose fourth step is smaller than its fifth is not a scale, it is a list.
   *
   * 20 to 26 puts it back in order — 32, 26, 22, 18 at the top end — and gives the same
   * ratio between every heading step. It is the only number in this block that moved.
   */
  --fs-h4: clamp(20px, 20px + 6 * (100vw - 1024px) / 416, 26px);
  --fs-h5: clamp(18px, 18px + 4 * (100vw - 1024px) / 416, 22px);
  --fs-h6: clamp(16px, 16px + 2 * (100vw - 1024px) / 416, 18px);
  --fs-xl: 22px;
  --fs-l: 18px;
  --fs-r: 16px;
  --fs-sm: 14px;
  --fs-xs: 12px;
}

.title-huge,
.text-size-display,
.text-size-d {
  font-size: var(--fs-d);
  line-height: 1.3;
  letter-spacing: -0.02em;
}

h1, .h1, .has-h-1-font-size {
  font-size: var(--fs-h1);
  line-height: 1.3;
  letter-spacing: -0.01em;
}

h2, .h2, .has-h-2-font-size {
  font-size: var(--fs-h2);
  line-height: 1.3;
  letter-spacing: -0.01em;
}

h3, .h3, .has-h-3-font-size {
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

h4, .h4, .has-h-4-font-size {
  font-size: var(--fs-h4);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

/* The one step the tokens set in semibold rather than regular. */
h5, .h5, .has-h-5-font-size {
  font-size: var(--fs-h5);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0;
}

h6, .h6, .has-h-6-font-size {
  font-size: var(--fs-h6);
  line-height: 1.5;
  letter-spacing: 0;
}

/* Body copy was 15 and is 16. It is the most-set size on the site, so it was also the
   most visible half-step out. */
.text-size-r {
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
}

/* The two body steps between `r` and the headings. They are in the token list and were
   the only two with no class here, so a section that wanted 22 or 18 wrote the number. */
.text-size-xl {
  font-size: var(--fs-xl);
  line-height: 1.4;
  letter-spacing: 0.01em;
}

.text-size-l {
  font-size: var(--fs-l);
  line-height: 1.5;
  letter-spacing: 0.01em;
}

.text-size-sm {
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
}

/* The only step the tokens set in medium. */
.text-size-xs {
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0;
}

/* --- The card surface -----------------------------------------------------
   The client logos, the testimonial cards, the stat rows and the footer cards are all
   the same object in the frames: a filled box, no border. Every one of them was built
   here as a transparent box with a 1px ink/30 border, which is why they all read as
   outlines against a design full of solids.

   One property so the four cannot drift apart again.                                   */

:root {
  --surface-card: #0f0f0f;
}

/*
 * A Phosphor icon.
 *
 * The drawing is a mask and the colour is the element's own, so an icon beside text is
 * always exactly the colour of that text — including when the text changes colour on
 * hover, which an <img> could never follow.
 */
.ph {
  display: inline-block;
  flex: 0 0 auto;
  width: var(--ph-size, 24px);
  height: var(--ph-size, 24px);
  background: currentColor;
  -webkit-mask: var(--ph) center / contain no-repeat;
  mask: var(--ph) center / contain no-repeat;
}

/*
 * The small radius.
 *
 * Six things were drawn at 8 — the page-header cards, the footer cards, the row cards, the
 * work tiles, the form controls and the text links. They are 4 now. A token rather than six
 * numbers, so the next change to it is one line and they cannot end up at three different
 * values again.
 *
 * The 16 on the filled cards stays: that is a different shape doing a different job.
 */
:root {
  --radius-s: 4px;
}

/*
 * The spacing scale.
 *
 * Taken from the design file's own variables, not inferred from the stylesheet. That
 * distinction cost something to learn: the first version of this block was reverse
 * engineered by counting which gap values appeared most often, and it produced a tidy
 * doubling scale that was WRONG in both directions. It invented 12, 32, 64 and 96, and it
 * excluded 20 and 40 — both of which are real, named tokens (`spacing/r-5`, `spacing/xxl`)
 * used all over the frames.
 *
 * The grid overlay was about to start flagging genuine design tokens as mistakes, which is
 * the worst thing an instrument can do. The lesson is small and general: a design system is
 * something you read from the source, not something you infer from the output.
 *
 * Names follow the file so a value can be traced back to it. The four marked below are the
 * ones the stylesheet leans on that the sampled node did not evidence; they are kept
 * because the site plainly uses them, and they should be checked against the file.
 */
:root {
  --sp-none: 0;
  --sp-xs: 4px;
  --sp-s: 8px;
  --sp-m: 12px;   /* unconfirmed */
  --sp-r: 16px;
  --sp-r5: 20px;
  --sp-l: 24px;
  --sp-xl: 32px;  /* unconfirmed */
  --sp-xxl: 40px;
  --sp-xxxl: 48px;
  --sp-4xl: 64px; /* unconfirmed */
  --sp-5xl: 96px; /* unconfirmed */
}

/* --- The container ladder -------------------------------------------------
   A stepped set of fixed max-widths, one per breakpoint, each equal to the breakpoint
   itself, centred, with the gutter carried inside. No clamp() anywhere — the width steps,
   it does not slide, which is what keeps the grid honest at every size.

   The cap is 1536. Wider than that and a full-width row of cards stops reading as a row
   and starts reading as a scatter.

   Worth knowing, because it is a real conflict: the Figma frames are drawn on a 1280
   page with 32px margins, which is 1216 of content — exactly what this file replaced.
   Above 1280 every section is now wider than it was drawn. That is what replicating
   their ladder means, and it is why the numbers below are the only fixed widths here:
   everything else is a percentage or a vw, so it comes with us.

   Overrides `.grid-container` from custom.css, which is a single fixed 1280 + 32.      */

.grid-container {
  width: 100%;
  max-width: none;
  margin-inline: auto;
  /* 16 up to md, 20 from md — their `mx-4` → `md:mx-5`. */
  padding-inline: 16px;
}

@media (min-width: 640px) {
  .grid-container { max-width: 640px; }
}

@media (min-width: 768px) {
  .grid-container { max-width: 768px; padding-inline: 20px; }
}

@media (min-width: 1024px) {
  .grid-container { max-width: 1024px; }
}

@media (min-width: 1280px) {
  .grid-container { max-width: 1280px; }
}

@media (min-width: 1440px) {
  .grid-container { max-width: 1440px; }
}

/* The cap. Nothing is wider than this. */
@media (min-width: 1536px) {
  .grid-container { max-width: 1536px; }
}

/*
 * ink/40. The rest of the scale is defined in custom.css; this one step is not, and the
 * process cards' border is drawn at it.
 */
:root {
  --color-natural-40: rgba(255, 255, 255, 0.4);

  /* The gap between one button and the next, anywhere on the site. */
  --btn-gap: 8px;

  /* And between one card and the next. */
  /*
   * THE CARD GAP, AND WHEN IT IS NOT THE GUTTER.
   *
   * A row of cards that spans the container to fill it — the footer's contact cards, the
   * stat rows, the logo grid, the notes — is not sitting on columns and should not be
   * spaced like it is. The cards ARE the row; they cross the column lines by design, and
   * putting a 48 gutter between them opens holes in something that is meant to read as one
   * band. Sixteen.
   *
   * The gutter is for things that occupy columns: a five-column card beside a seven-column
   * one, a heading against a body. Those land on lines and the space between them is the
   * grid's own space. These do not, and it is not.
   */
  --card-gap: 16px;
}

/* --- Swiza Light ----------------------------------------------------------
   Registered as weight 300 of the existing family rather than a separate family, so
   `font-family: "Swiza"` keeps working everywhere and the weight does the choosing.
   That also stops the browser synthesising a fake light from the regular if a face is
   ever missing — it falls back to the real regular instead, which is the better failure.

   woff2 first, woff behind it for anything that can't take woff2. The .eot supplied
   alongside them is for IE8-11 and isn't shipped.                                     */

@font-face {
  font-family: 'Swiza';
  src:
    url('../fonts/Swiza-Light.woff2') format('woff2'),
    url('../fonts/Swiza-Light.woff') format('woff');
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/*
 * Every heading, and the two display steps, in Regular.
 *
 * Light was tried and it went too far — at these sizes it thinned the type past where
 * the brand sits. Back to 400. The Light face stays registered above, so asking for it
 * anywhere is one property, and nothing is synthesised.
 *
 * Set on the elements rather than on the `Swiza` family default, because body copy is
 * Inter and shouldn't be dragged along, and because a heading that opts out later only
 * has to change one property.
 */
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6,
.title-huge,
.text-size-d,
.has-h-1-font-size,
.has-h-2-font-size,
.has-h-3-font-size,
.has-h-4-font-size,
.has-h-5-font-size,
.has-h-6-font-size {
  font-weight: 400;
}

/*
 * Except h5, which the tokens set in semibold — the only step in the scale that is not
 * regular. Restated after the rule above rather than in the scale itself, because that
 * rule comes later in the file and would win.
 */
h5, .h5, .has-h-5-font-size {
  font-weight: 600;
}

/* --- Type scale ----------------------------------------------------------
   Overrides of the theme's heading sizes, from the Figma variables.

   These live here rather than in custom.css for the usual reason: that file is compiled
   WordPress output kept so the original can be re-extracted. site.css loads after it, so
   the same selector list wins without editing it.

   The theme's own formula is kept — clamp(min, min + delta * (100vw - 1024px) / 416, max)
   — which scales between a 1024px and a 1440px viewport rather than jumping at a
   breakpoint. Matching it means these headings behave like every other size on the site.

     h1   40px desktop / 26px mobile   (was 50 / 34 — both came down)
     h2   28px desktop / 22px mobile   (unchanged; the file matches what was already here)

   Tracking is left as the theme had it. The Figma specifies it as a percentage of the
   size, and changing both at once would make it impossible to tell which caused what.
   Worth revisiting deliberately.
   --------------------------------------------------------------------------- */

/* h1 was restated here at 40. It is 44 in the tokens and is set in the type scale. */

/* ---------------------------------------------------------------------------
   Styles for sections built after the WordPress migration.

   custom.css and foundation.css are compiled output from the old WP theme. They're
   kept exactly as they were served so they can be re-extracted if the original ever
   needs consulting again — nothing new goes in them. This file loads after both, so
   it can lean on the theme's own tokens and type scale rather than restating them:

     colour     --color-main, --color-eyebrow, --bg-color, --border-default
     type       .h1–.h6, .text-size-l, .text-size-r, .text-size-sm
     spacing    the flx-section--pt-* / --pb-* padding scale
     motion     [data-scroll] plus .ease-btm / .ease-top / .ease-left / .ease-right

   Anything defined here that the theme could have provided is a mistake worth
   questioning.
   --------------------------------------------------------------------------- */

/* One step below the theme's smallest size, for secondary attribution lines.
   The theme's scale stops at 14px (.text-size-sm); the design system uses 12px for
   the line under a name. */
.text-size-xs {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
}

/* The frame carries no eyebrow now — the mark and the size say what this is. The rule
   stays for a page that wants one anyway, at the ordinary distance rather than the 128
   the centred version needed to hold the two apart. */
.flx-testimonial-central .eyebrow {
  margin-bottom: 56px;
}

/* --- The single quote -----------------------------------------------------
 * Figma 3664:50206.
 *
 * No longer centred. The opening mark sits out on its own at the far left and the quote
 * takes the right-hand seven columns — the same 5/7 the rest of the site's two-column
 * sections use, so a quote between two of them lines up with both.
 *
 * The centred version put a 584px measure in the middle of a 1216 container, which read
 * as a poster rather than as part of the page.
 */
.flx-testimonial-central__inner {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 48px;
  align-items: start;
}

.flx-testimonial-central__mark {
  grid-column: 1 / span 2;
  font-family: 'Swiza', sans-serif;
  /* Twice the quote's step. The frame's glyph went from 24 tall to 48 — it is a mark on
     the page now rather than punctuation, which is why it can be this quiet and still be
     seen. Stated against `--fs-h3` rather than as 64px so it scales with the quote it
     belongs to. */
  font-size: calc(var(--fs-h3) * 2);
  /* 1, not the 1.5 the quote is set in: this is one glyph and the line box around it
     would push it half a line below the first line of the quote it opens. */
  line-height: 1;
  /* ink/30. */
  color: rgba(255, 255, 255, 0.3);
}

.flx-testimonial-central__body {
  grid-column: 6 / span 7;
  display: flex;
  flex-direction: column;
  /* The frame's 56 between the quote and whoever said it. */
  gap: 56px;
}

.flx-testimonial-central__quote {
  /* Foundation styles every <blockquote> with a left rule, a left indent and 70% white
     text — sensible for a pull quote in body copy, wrong for one at heading size. Undo
     all three; `.h3` supplies the family, size, line-height and tracking. */
  margin: 0;
  padding: 0;
  border: 0;
  /* ink/100. Stated, not inherited: the quote is the loudest thing in the section. */
  color: var(--color-natural-100, #fff);
}

.flx-testimonial-central__by {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.flx-testimonial-central__by p {
  margin: 0;
  color: var(--color-natural-100, #fff);
}

.flx-testimonial-central__by p + p {
  /* The role sits back from the name — ink/70, and a touch heavier at the smaller step so
     it holds its own against the line above it. */
  color: rgba(255, 255, 255, 0.7);
  font-weight: 500;
}

@media only screen and (width <= 899px) {
  .flx-testimonial-central .eyebrow {
    margin-bottom: 40px;
  }

  /* Stacked, with the mark above the quote rather than beside it — at this width there
     is no room beside it worth having. */
  .flx-testimonial-central__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
  }

  .flx-testimonial-central__mark,
  .flx-testimonial-central__body {
    grid-column: 1 / -1;
  }

  .flx-testimonial-central__body {
    gap: 40px;
  }
}

/* --- Header: message, pill navigation, mode toggle ------------------------
   Figma: "Header" (2021:23177) and "header-right" (3555:38443).                */

/* The availability strip beside the logo. 12 between light and words — the halo ring
   needs air, and at 8 the pulse read as touching the text. */
.header-message {
  display: flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  color: inherit;
}

.header-message--link:hover .header-message__text {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.header-message__dot {
  flex: 0 0 auto;
  /* 10px, up from 8 - the light is the only signal in the bar, so it may as well be
     seen. The halo went from 28% to 42% for the same reason. */
  width: 10px;
  height: 10px;
  border-radius: 50%;
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 42%, transparent);
}

.header-message__text {
  letter-spacing: 0.14px;
  white-space: nowrap;
}

.header-message__text strong {
  font-weight: 600;
}

/* --- Navigation, collapsed behind a plus --------------------------------
   Figma: closed (3578:98675), open (3578:98539).

   The links come in from the right, one after another, nearest the button first.

   Nothing about the row's width animates, and that is the point. It used to: a one-column
   grid going `0fr` to `1fr`, clipped from the right. It opened to exactly the width of the
   links with nothing measuring them, which was elegant — and it read backwards. Growing
   that track changes the width of the whole header row, so every link slid *rightwards*
   into place while the reveal edge travelled left, and what you saw was a menu unfolding
   left to right. Two attempts at flipping the travel could not beat the reflow underneath
   it.

   So the row now always occupies its own width, and only the links move: each starts to
   the right of its place and settles left, hidden until it does. Nothing reflows, the
   button never shifts, and the direction is unambiguous. The empty space this reserves
   when the menu is shut is exactly that — empty space on a dark bar.                    */

/*
 * Reset what the old slide-out menu left behind.
 *
 * `.header-nav` is the theme's own class — it was the nav inside the hamburger panel,
 * which is gone. It brought `padding: 4rem 0`, `flex-direction: row-reverse`,
 * `justify-content: space-between` and a `gap`, plus a mobile variant that made it a
 * full-height column. Cleared explicitly rather than one property at a time.
 */
.header-nav {
  display: flex;
  /*
   * `row`, explicitly. The theme's own `.header-nav` rule sets `row-reverse`, which put
   * the toggle to the left of the links and had them unfold the wrong way out of it —
   * invisible until this element became a flex container with more than one child.
   */
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 0;
  height: auto;
}

.header-nav__panel {
  /* No width animation. The row is always its own width; the links are what move. */
  display: block;
}

.header-nav__clip {
  /*
   * Hidden once shut, so nothing in there can be focused or read out. Delayed by the
   * length of the animation on the way out, and instant on the way in — a plain
   * `visibility` transition can't be eased, only timed, which is exactly what's wanted.
   */
  visibility: hidden;
  transition: visibility 0s linear 0.55s;
}

.header-nav__panel.is-open .header-nav__clip {
  visibility: visible;
  transition-delay: 0s;
}

.header-nav__list {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
  /* Never squashed by the track while it's narrow — it's clipped instead. */
  width: max-content;
}

/*
 * Each link starts to the right of where it belongs and travels left into place, nearest
 * the button first. Far enough to be read as a direction rather than a settle — 18px was
 * too small to see against everything else that was moving.
 *
 * The per-item delay is set in the component, because the order reverses on the way out.
 */
.header-nav__item {
  margin: 0;
  opacity: 0;
  transform: translateX(40px);
  transition:
    opacity 0.42s ease-out,
    transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.header-nav__panel.is-open .header-nav__item {
  opacity: 1;
  transform: translateX(0);
}

/*
 * 40px, everywhere.
 *
 * The site had two button heights — 40 and 36 — and no rule about which went where. The
 * services row and the people row were 40; the page headers, the sticky bar, the header
 * itself and the hero pills were 36. Nothing chose that: it is what each frame happened
 * to be drawn at, carried across one section at a time.
 *
 * 40 is the size in the current frames and the one that survives a thumb. 36 is inside
 * the 44px target every touch guideline asks for, and the difference is most visible in
 * the header, where the smallest buttons on the site sit at the top of every page.
 *
 * Only the height moves. Padding, radius, type size and gap are untouched, so each button
 * keeps its own proportions and gains four pixels of vertical room — the label sits where
 * it did, with more air above and below it.
 */
.header-nav__link {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 8px 16px;
  border: 1px solid var(--border-default);
  /* Pills, to sit with the round toggle and the round button. */
  border-radius: 999px;
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.25s ease, background-color 0.25s ease;
}

.header-nav__link:hover,
.header-nav__link:focus-visible {
  border-color: var(--border-hover);
}

/* The page you're on, marked without a moving indicator — there's nothing for one to
   travel between while the row is shut. */
.header-nav__link.is-current {
  border-color: transparent;
  background: color-mix(in srgb, currentColor 12%, transparent);
}

/*
 * The header button: filled white, with the arrow it carries in both Figma states.
 *
 * It spent a while as an outline with a travelling highlight around it. `HoverBorder` is
 * still in the tree and is now used nowhere — left there rather than deleted, because it
 * is a whole effect and reinstating it is one component swap.
 */
.header-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid var(--color-natural-100, #fff);
  border-radius: 99999px;
  background: var(--color-natural-100, #fff);
  color: var(--bg-color, #000);
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color 0.3s ease, border-color 0.3s ease;
}

.header-cta:hover,
.header-cta:focus-visible {
  background: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  border-color: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  color: var(--bg-color, #000);
}

.header-cta svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.header-cta:hover svg {
  transform: translateX(3px);
}

/* --- The plus ------------------------------------------------------------ */

.header-nav__toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-default);
  /* A circle now, as drawn. */
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.25s ease;
}

/*
 * The icon turns, not the button.
 *
 * Rotating the whole control was pointless once it became a circle — a circle turning
 * looks like nothing at all. Turning the icon inside a still ring is the movement you
 * can actually see. Half a rotation rather than a quarter: at 90° the plus lands back on
 * itself. The slight overshoot is what stops it feeling mechanical.
 */
.header-nav__toggle.is-open .header-nav__icon {
  transform: rotate(180deg);
}

/* Foundation fills every <button> on hover. Neutralise it for every state rather than
   only the resting one, and raise the specificity so the theme's own rules don't win. */
.header .header-nav__toggle,
.header .header-nav__toggle:hover,
.header .header-nav__toggle:focus,
.header .header-nav__toggle:focus-visible,
.header .header-nav__toggle:active {
  background: transparent;
  color: inherit;
}

.header-nav__toggle:hover,
.header-nav__toggle:focus-visible {
  border-color: var(--border-hover);
}

.header-nav__icon {
  position: relative;
  display: block;
  width: 16px;
  height: 16px;
  transition: transform 0.45s cubic-bezier(0.34, 1.3, 0.5, 1);
}

.header-nav__bar {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12.5px;
  height: 1px;
  background: currentColor;
  transition: transform 0.45s cubic-bezier(0.34, 1.3, 0.5, 1), opacity 0.25s ease;
}

.header-nav__bar--h {
  transform: translate(-50%, -50%);
}

/* The upright bar. It turns onto the flat one and fades as it goes, which is what makes
   the plus become a minus rather than one icon being swapped for another. */
.header-nav__bar--v {
  transform: translate(-50%, -50%) rotate(90deg);
}

.header-nav__toggle.is-open .header-nav__bar--v {
  transform: translate(-50%, -50%) rotate(180deg);
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .header-nav__panel,
  .header-nav__item,
  .header-nav__icon,
  .header-nav__bar {
    transition-duration: 0.01ms;
  }
  .header-nav__item {
    transition-delay: 0ms !important;
  }
}

/* --- The height of the bar ------------------------------------------------
   The row inside the header is 40px — the button, the toggle and the links are all 40,
   and the logo is 28 and centred in it. So the bar's height is that 40 plus whatever
   padding is set here, and the two numbers have to be written together: the theme puts
   the header in `position: absolute` inside a `.header-wrap` of a stated height, and the
   wrap is what actually reserves the space above the page. Change one without the other
   and you get a gap under the header that looks like padding and isn't, or a header that
   overhangs the first section.

   24 top and bottom, so 24 + 40 + 24 = 88.

   This deliberately sits ABOVE the narrow-screen rules below. Media queries carry no
   extra weight — a later rule of the same specificity wins whatever query it is in — so
   putting these at the end of the file would silently override the phone's own padding
   and the phone's `height: auto`. They go here, before both.                            */

.menu-grid-container {
  padding: 24px 0;
}

.header-wrap {
  height: 88px;
}

/* --- Narrow screens ------------------------------------------------------
   The Figma frame is desktop only.

   Above 1024 the header is the logo, the availability line and three controls. Below it
   the availability line drops — it's the least load-bearing thing in there — and below
   768 the links open downwards from the toggle instead of sideways, because there is no
   sideways left. Everything else about the interaction is the same: the same stagger,
   the same direction of travel, the same button turning over.                          */

/* Eight, as drawn. */
.menu-grid-container__right {
  gap: 8px;
}

@media only screen and (width <= 1024px) {
  .header-message {
    display: none;
  }
}

@media only screen and (width <= 768px) {
  .menu-grid-container {
    padding: 20px 0;
  }
  .menu-grid-container__left {
    padding-right: 0;
    gap: 1rem;
  }
  .menu-grid-container__right {
    min-width: 0;
  }

  /*
   * A panel hanging off the toggle rather than a row beside it.
   *
   * Three links, a mode toggle and a button will not share a line with the logo on a
   * phone, and the sideways reveal has nowhere to go. Anchoring the panel under the
   * toggle keeps the header one row at every width and keeps the menu one tap away.
   */
  .header-nav {
    position: relative;
  }
  .header-nav__panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 20;
    /*
     * `max-content`, not `1fr`. The panel's containing block is the 36px toggle wrapper,
     * so a fractional track resolves to 36px and clips every label to its first letter.
     * Only the height animates down here, so the width can size to the content.
     */
    width: max-content;
    grid-template-columns: max-content;
    grid-template-rows: 0fr;
    opacity: 0;
    transition:
      grid-template-rows 0.38s cubic-bezier(0.22, 1, 0.36, 1),
      opacity 0.2s ease;
  }
  .header-nav__panel.is-open {
    grid-template-rows: 1fr;
    opacity: 1;
  }
  .header-nav__clip {
    /* Nothing to clip from the left down here — the panel is as wide as it needs to be. */
    direction: ltr;
    border-radius: var(--radius-s);
    background: var(--bg-color);
    /* An inset ring rather than a border: a border on a box whose height animates from
       zero shows up as a hairline before there's anything inside it. */
    box-shadow: inset 0 0 0 1px var(--border-default);
  }
  .header-nav__list {
    flex-direction: column;
    align-items: stretch;
    width: auto;
    min-width: 180px;
    padding: 8px;
  }
  .header-nav__link {
    justify-content: flex-start;
    border-color: transparent;
  }
}

/*
 * Let the header reserve its own height on mobile.
 *
 * The theme's `.header-wrap` is a fixed 80px on a phone and the `.header` inside it is
 * absolutely positioned, so the wrap reserves 80px no matter how tall the header really
 * is. That was true when the header was one row. The pill row makes it two — 164px — and
 * the extra 84px was painting straight over the first section of every page, headline
 * included.
 *
 * Taking the header out of absolute positioning here lets the wrap size to its content,
 * so the reserved space and the actual header agree again. The wrap stays sticky, so the
 * show/hide-on-scroll behaviour is untouched.
 */
@media only screen and (width <= 768px) {
  .header-wrap {
    height: auto;
  }
  .header {
    position: relative;
  }
}

/* The header button sits next to a scrolling row on mobile; letting it wrap turns
   "Get in touch" into three lines. */
.menu-grid-container__right .button {
  white-space: nowrap;
}

/* --- Page header with link cards ----------------------------------------
   Figma: "Masthead 1" (3555:38298).                                          */

/*
 * One step above the theme's h1. The design calls it heading-size-d — 48px on desktop,
 * scaling down the way every other size in this theme does, with the file's -2% tracking
 * convention. Named for the scale rather than the block, because a display size is a
 * type-scale decision, not a section's private business.
 */
/*
 * `.title-huge` is the display step under an older name: the theme developer called it
 * "huge", the design system calls it display, and they are one rule so the two names
 * cannot drift apart.
 *
 * The size used to be restated here at 48. It is 80 in the tokens, and it is set once in
 * the type scale at the top of this file — this rule now only says which family it is in.
 */
.text-size-d,
.title-huge {
  font-family: 'Swiza', sans-serif;
}

/* The theme gives .title-huge a bottom margin; keep it there and off the new class,
   which is used inside its own flex layout. */
.text-size-d {
  margin-bottom: 0;
}

.flx-page-header-cards .grid-container {
  display: flex;
  flex-direction: column;
  gap: 64px;
}

.flx-page-header__content {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 700px;
}

.flx-page-header__sub {
  margin: 0;
  color: var(--color-natural-70);
}

.flx-page-header__cards {
  display: flex;
  gap: 16px;
}

.flx-page-header__card {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 16px;
  padding: 24px 32px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
  color: inherit;
  text-decoration: none;
  /* Own stacking context, so the glow layers can't escape the card. */
  position: relative;
  isolation: isolate;
  transition: border-color 0.3s ease;
}

.flx-page-header__card-title {
  margin: 0;
  line-height: 1.5;
  letter-spacing: -0.2px;
}

.flx-page-header__card-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  line-height: 1;
  color: var(--color-grey);
  transition: color 0.3s ease;
}

.flx-page-header__card:hover .flx-page-header__card-link {
  color: inherit;
}

/* --- Edge glow -----------------------------------------------------------
   The glowing hover, white only. Ask for it by name.

   To put it on a new group of cards: wrap them in `<Glow className="…">`, which adds
   `glow-group`, and give each card `glow-card` alongside its own class. The card needs
   `position: relative` and a radius; the glow inherits the radius and draws on the
   border. That's the whole contract — nothing here is tied to a particular section.

   Two layers, both driven by --glow-x / --glow-y, which Glow.tsx sets from the pointer:

     ::before  the lit edge — a radial gradient clipped to a 1px ring by masking the
               padding box out of the border box, which is what makes the light appear
               to travel around the border rather than sit on top of the card
     ::after   a soft wash inside, so the card reads as lit rather than outlined

   Both start at zero opacity and only appear once the pointer is inside the row, so a
   card is never mysteriously glowing at a cursor that has gone.                        */

.glow-card::before,
.glow-card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

.glow-card::before {
  padding: 1px;
  background: radial-gradient(
    180px circle at var(--glow-x, 50%) var(--glow-y, 50%),
    rgba(255, 255, 255, 0.85),
    rgba(255, 255, 255, 0.15) 45%,
    transparent 70%
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

.glow-card::after {
  background: radial-gradient(
    220px circle at var(--glow-x, 50%) var(--glow-y, 50%),
    rgba(255, 255, 255, 0.07),
    transparent 70%
  );
  z-index: -1;
}

/* Lit only while the pointer is in the group. Any group, not one named section. */
.glow-group:hover .glow-card::before,
.glow-group:hover .glow-card::after,
.glow-card:focus-visible::before {
  opacity: 1;
}

/* In the light theme a white glow is invisible; the same effect in ink reads the same
   way round. */
.theme-light .glow-card::before {
  background: radial-gradient(
    180px circle at var(--glow-x, 50%) var(--glow-y, 50%),
    rgba(0, 0, 0, 0.75),
    rgba(0, 0, 0, 0.12) 45%,
    transparent 70%
  );
}

.theme-light .glow-card::after {
  background: radial-gradient(
    220px circle at var(--glow-x, 50%) var(--glow-y, 50%),
    rgba(0, 0, 0, 0.07),
    transparent 70%
  );
}

@media (prefers-reduced-motion: reduce) {
  .glow-card::before,
  .glow-card::after {
    transition: none;
  }
}

@media only screen and (width <= 1024px) {
  .flx-page-header-cards .grid-container {
    gap: 48px;
  }
  .flx-page-header__cards {
    flex-wrap: wrap;
  }
  .flx-page-header__card {
    flex: 1 1 calc(50% - 8px);
  }
}

@media only screen and (width <= 640px) {
  .flx-page-header__card {
    flex: 1 1 100%;
  }
}

/* --- Masthead with a rotating word ---------------------------------------
   Figma: "Masthead 1" (3556:38637).                                          */

.flx-masthead-rotating .grid-container {
  display: flex;
  flex-direction: column;
  gap: 56px;
}

.flx-masthead-rot__top {
  display: flex;
  flex-direction: column;
  gap: 40px;
  align-items: flex-start;
}

.flx-masthead-rot__content {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 795px;
}

/*
 * 80px in the frame. That's above every step in the type scale — the largest token is the
 * 48px display — so it's written here rather than pretending to be a scale value. Same
 * -2% tracking as the rest of the headings, and the theme's clamp shape so it scales
 * between 1024px and 1440px like everything else.
 */
.flx-masthead-rot__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: clamp(40px, 40px + 40 * (100vw - 1024px) / 416, 80px);
  line-height: 1.3;
  letter-spacing: -0.02em;
}

.flx-masthead-rot__sub {
  margin: 0;
  color: var(--color-natural-70);
}

/* --- The rotating word ---------------------------------------------------
   Read off a reference headline rather than guessed at. The numbers, exactly:

     in    y 80% -> 0     400ms  cubic-bezier(0.16, 1, 0.3, 1)   30ms apart per letter
     out   y 0 -> -80%    220ms  cubic-bezier(0.4, 0, 1, 0.6)    25ms apart per letter
                          opacity 1 -> 0 on the way out only

   There is no 3D in it. No perspective, no rotateX, no scale, no blur — none of those
   words appear anywhere in their page. What looks like letters folding away is each
   letter being clipped by its own little box as it travels: you watch a letter get
   shorter, which the eye reads as a fold rather than a slide.

   Two details carry it:

   Each character has its own `overflow: hidden` window, with a tenth of an em of padding
   and the same again in negative margin so accents and descenders aren't shaved off while
   the letter sits still.

   The travel is 100% of the letter's height, not the 80% the motion is usually written
   with. 80% clears the window only when the headline is set tight; ours is set at 1.3, so
   the window is taller than the glyph by a wide margin and 80% left the tops of the
   waiting letters showing under the line. 100% is the same motion translated to our
   leading — the glyph still clears long before the box does.

   The two halves don't overlap. The outgoing ripple finishes before the incoming one
   starts, which is `--lead`: set on the wrapper, in the component, from the length of the
   word that's leaving. Longer word, longer wait.

   The width is the part this design needs and a fixed headline doesn't: a headline that
   never changes word never resizes its box. Ours has five words, so a hidden copy of the
   longest holds the line open and nothing ever moves.                                                         */

.rotating-word {
  display: inline-grid;
  /* One cell. The sizer and every word are placed into it below. */
  grid-template: 1fr / auto;
  vertical-align: baseline;
  text-align: left;

  --in-dur: 400ms;
  --in-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --in-stagger: 30ms;
  --out-dur: 220ms;
  --out-ease: cubic-bezier(0.4, 0, 1, 0.6);
  --out-stagger: 25ms;
  --travel: 100%;
  /* Overwritten per changeover by the component. */
  --lead: 0ms;
}

.rotating-word__sizer,
.rotating-word__word {
  grid-area: 1 / 1;
}

/* Holds the box open at the width of the longest word, and is never seen. */
.rotating-word__sizer {
  visibility: hidden;
  pointer-events: none;
  white-space: nowrap;
}

.rotating-word__word {
  display: inline-flex;
  white-space: nowrap;
  /* Words with no part to play are taken out entirely rather than parked below the line —
     five words' worth of waiting letters otherwise show their tops through the padding. */
  visibility: hidden;
}

.rotating-word__word.is-current,
.rotating-word__word.is-leaving {
  visibility: visible;
}

/* One clipping window per letter. The padding gives accents and descenders room; the
   matching negative margin takes that room back out of the line so the baseline holds. */
.rotating-word__mask {
  display: inline-block;
  overflow: hidden;
  padding: 0.1em 0;
  margin: -0.1em 0;
}

/*
 * A letter waiting its turn, sitting below its window.
 *
 * No transition on purpose. A letter that has finished leaving has to get back down here
 * to be used again, and it should do that instantly rather than travelling back through
 * the window. CSS takes the transition from the state being moved *to*, so the two states
 * below still animate normally.
 */
.rotating-word__ch {
  display: inline-block;
  transform: translateY(var(--travel));
  opacity: 1;
  transition: none;
}

/* Arriving: opaque the whole way, so it rises into the window rather than fading in. */
.is-current > .rotating-word__mask > .rotating-word__ch {
  transform: translateY(0);
  transition: transform var(--in-dur) var(--in-ease)
    calc(var(--lead) + var(--i) * var(--in-stagger));
}

/* Leaving: up and out, fading as it goes. */
.is-leaving > .rotating-word__mask > .rotating-word__ch {
  transform: translateY(calc(var(--travel) * -1));
  opacity: 0;
  transition:
    transform var(--out-dur) var(--out-ease) calc(var(--i) * var(--out-stagger)),
    opacity var(--out-dur) var(--out-ease) calc(var(--i) * var(--out-stagger));
}

/*
 * Reduced motion: the first word, still.
 *
 * The component also declines to start its timer, so this is belt and braces — but it
 * matters, because it means the static state is correct even in the frame before the
 * JavaScript has run.
 */
@media (prefers-reduced-motion: reduce) {
  .rotating-word__ch {
    transform: none;
    transition: none;
    opacity: 1;
  }
  .rotating-word__word:not(.is-current) {
    /* Out of the flow entirely, so it can't be selected or found by ctrl-F. */
    visibility: hidden;
  }
}

/* --- Buttons and USP row -------------------------------------------------- */

.flx-masthead-rot__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.flx-masthead-rot__buttons .button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.flx-masthead-rot__usps {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.flx-masthead-rot__usp {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  line-height: 1;
  color: var(--color-natural-70);
}

.flx-masthead-rot__usp svg {
  flex: 0 0 auto;
}

@media only screen and (width <= 768px) {
  .flx-masthead-rotating .grid-container {
    gap: 40px;
  }
  .flx-masthead-rot__top {
    gap: 32px;
  }
  /* Stacked rather than wrapped: three points wrapping to two lines splits one of them
     awkwardly, and a short vertical list reads faster on a phone anyway. */
  .flx-masthead-rot__usps {
    flex-direction: column;
    gap: 12px;
  }
  .flx-masthead-rot__buttons .button {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* --- Concept: display masthead -------------------------------------------
   Figma: "Masthead 10" (3566:37966).

   Everything here is scoped to the block's own section class. A concept has to be
   judged on a real page, with the real header and the real type, and that is only
   safe if it cannot reach anything else — so there is not one unscoped selector
   below, and no existing rule is touched.

   The frame is 1512 wide with no site header, headline at 140px. On a narrower
   desktop that would run off the side, so it scales on the same 1024→1440 ramp the
   rest of the type uses and settles at the drawn size.                             */

.flx-masthead-display {
  background: var(--md-bg);
  color: var(--md-fg);
}

/* Cream — the concept as drawn. */
.flx-masthead-display--cream {
  --md-bg: #f6f3e7;
  --md-fg: #000;
  --md-btn-bg: #000;
  --md-btn-fg: #fff;
  --md-line: #ccc;
  --md-pill-bg: #fff;
  --md-pill-line: transparent;
  --md-muted: rgb(0 0 0 / 0.8);
}

.flx-masthead-display--paper {
  --md-bg: #fff;
  --md-fg: #000;
  --md-btn-bg: #000;
  --md-btn-fg: #fff;
  --md-line: #ddd;
  --md-pill-bg: #fff;
  /* On white the pill has no ground of its own, so it needs an edge to exist at all. */
  --md-pill-line: #e5e5e5;
  --md-muted: rgb(0 0 0 / 0.8);
}

.flx-masthead-display--ink {
  --md-bg: #000;
  --md-fg: #fff;
  --md-btn-bg: #fff;
  --md-btn-fg: #000;
  --md-line: rgb(255 255 255 / 0.25);
  --md-pill-bg: rgb(255 255 255 / 0.08);
  --md-pill-line: rgb(255 255 255 / 0.16);
  --md-muted: rgb(255 255 255 / 0.7);
}

/*
 * The ground runs the full page, not just the section.
 *
 * The header is absolutely positioned over the top of the first section and painted in
 * the theme's background colour, so without this the concept starts under a bar of a
 * different colour. `:has()` keeps it to pages that actually carry the block.
 */
body:has(.flx-masthead-display--cream) {
  background: #f6f3e7;
}

body:has(.flx-masthead-display--paper) {
  background: #fff;
}

body:has(.flx-masthead-display--cream) .header,
body:has(.flx-masthead-display--cream) .header-wrap,
body:has(.flx-masthead-display--paper) .header,
body:has(.flx-masthead-display--paper) .header-wrap {
  background: transparent;
}

/*
 * The header, over a light concept.
 *
 * The cream masthead used to flip the whole page to `theme-light` and the header came
 * along with it. Light mode is off now, so it doesn't — and white type on cream is
 * nothing. Dressing the header as light on exactly the pages that have a light masthead
 * gets the concept back without giving the site a light mode again.
 */
body:has(.flx-masthead-display--cream) .header-wrap,
body:has(.flx-masthead-display--paper) .header-wrap {
  color: #000;
}

/* The logo is an inline SVG with its own fill on the paths, so `currentColor` on the
   <svg> alone doesn't reach it. */
body:has(.flx-masthead-display--cream) .header-wrap .logo svg,
body:has(.flx-masthead-display--cream) .header-wrap .logo svg path,
body:has(.flx-masthead-display--paper) .header-wrap .logo svg,
body:has(.flx-masthead-display--paper) .header-wrap .logo svg path {
  fill: #000;
}

.flx-masthead-display .grid-container {
  display: flex;
  flex-direction: column;
  gap: 80px;
}

.flx-masthead-display__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 36px;
}

/* --- The availability pill ----------------------------------------------- */

.flx-masthead-display__notice {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid var(--md-pill-line);
  border-radius: 999px;
  background: var(--md-pill-bg);
  color: var(--md-fg);
  font-size: 14px;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
}

a.flx-masthead-display__notice:hover {
  color: var(--md-fg);
}

a.flx-masthead-display__notice:hover span:not(.flx-masthead-display__light) {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.flx-masthead-display__light {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  /* A soft halo, so it reads as a status light rather than a bullet. */
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 12%, transparent);
}

/* --- The headline --------------------------------------------------------- */

/*
 * 140px in the frame — nearly three times the largest step in the type scale. Written at
 * the size drawn, because arguing a concept into an existing scale defeats the point of
 * drawing it. It is not a scale value and shouldn't quietly become one.
 */
.flx-masthead-display__title {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: clamp(56px, 56px + 84 * (100vw - 1024px) / 416, 140px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--md-fg);
}

.flx-masthead-display__line {
  display: block;
}

/* --- Copy, buttons and proof ---------------------------------------------- */

.flx-masthead-display__foot {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.flx-masthead-display__sub {
  margin: 0;
  max-width: 689px;
  color: var(--md-fg);
}

.flx-masthead-display__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

.flx-masthead-display__buttons {
  display: flex;
  gap: 16px;
}

/*
 * Fully rounded, unlike the site's 8px buttons. That difference is one of the things
 * being tested, so these are their own thing rather than a modifier on `.button` — a
 * concept that edits the live button is not a concept.
 */
.flx-masthead-display__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 14px;
  line-height: 1;
  text-decoration: none;
  transition: opacity 150ms ease;
}

.flx-masthead-display__btn:hover {
  opacity: 0.85;
}

.flx-masthead-display__btn--primary {
  background: var(--md-btn-bg);
  color: var(--md-btn-fg);
}

.flx-masthead-display__btn--primary:hover {
  color: var(--md-btn-fg);
}

.flx-masthead-display__btn--outline {
  border-color: var(--md-line);
  color: var(--md-fg);
}

.flx-masthead-display__btn--outline:hover {
  color: var(--md-fg);
}

.flx-masthead-display__proof {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--md-muted);
}

.flx-masthead-display__proof-item {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0;
  white-space: nowrap;
  letter-spacing: 0.14px;
}

/* The 4px separator, drawn on the item rather than added as markup. */
.flx-masthead-display__proof-item + .flx-masthead-display__proof-item::before {
  content: '';
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}

@media only screen and (width <= 1024px) {
  .flx-masthead-display .grid-container {
    gap: 48px;
  }
  .flx-masthead-display__head {
    gap: 24px;
  }
  .flx-masthead-display__row {
    /* The proof row can't share a line with the buttons once the line gets short. */
    flex-direction: column;
    align-items: flex-start;
    gap: 24px;
  }
  .flx-masthead-display__proof {
    flex-wrap: wrap;
    row-gap: 8px;
  }
}

@media only screen and (width <= 640px) {
  .flx-masthead-display__title {
    font-size: clamp(40px, 11vw, 56px);
  }
  .flx-masthead-display__proof {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .flx-masthead-display__proof-item + .flx-masthead-display__proof-item::before {
    display: none;
  }
  .flx-masthead-display__buttons .flx-masthead-display__btn {
    flex: 1 1 auto;
  }
}

/* --- Masthead 1, middle text rotation ------------------------------------
   Figma: "Masthead 1" (2031:32217).

   Their headline animation, read out of their bundle rather than guessed at:

     from { y: "110%" }  to { y: 0 }   duration 1s   ease power3.out   stagger 0.1s

   Each line has its own clipping window and travels 110% of its own height. Three
   details in that are worth spelling out.

   **110%, not 100%.** The window is one line tall and descenders hang below the
   baseline; at 100% the tail of a `p` or a `y` is still showing along the bottom edge.
   The extra tenth clears it.

   **The quartic ease-out** — `1 - (1-t)^4`. The usual name for it is one behind the maths,
   and the shorthand everyone reaches for, `cubic-bezier(0.165, 0.84, 0.44, 1)`, is 4%
   away from it at the tenth of the way in — visible on type this size. The curve below
   is fitted to the real function and is within half a percent across the whole range.

   **The change is one movement, not two.** The line leaving and the line arriving
   travel together at the same speed, so what you see is a single strip passing the
   window rather than two lines crossing in it.                                        */

.flx-masthead-mid .grid-container {
  display: flex;
  flex-direction: column;
  /* The frame keeps the headline and the media in two page-width blocks a 96px root gap
     apart; here they're one container, so the gap moves onto it. */
  gap: 96px;
}

.flx-masthead-mid__content {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

/* --- The headline -------------------------------------------------------- */

.rot-line {
  margin: 0;
  /*
   * A column, not a block. The rows carry the leading as margin, and between two block
   * siblings those margins collapse into one — which quietly shortened the headline by
   * two thirds of a line. Flex containers don't collapse margins.
   */
  display: flex;
  flex-direction: column;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /*
   * No size here.
   *
   * It was written at 80 on its own ramp, which is close enough to the display step to
   * look right and far enough to never match it — and it meant the block's Title Tag
   * Style setting had nothing to change. The class the CMS puts on this element carries
   * the size now, which is why the setting exists.
   */

  /* Fitted to the quartic ease-out. */
  --ease-out-quart: cubic-bezier(0.26, 1, 0.48, 1);
  --change: 1000ms;
  --stagger: 100ms;
  /*
   * Ascender to descender, near enough. Big enough that nothing is shaved off a `p` or
   * a `d` while it sits still, small enough that a travelling line is gone before it
   * reaches the line above. 1.06 turned out to shave the bottom of Swiza's descenders
   * after all — 1.14 clears them, and the margin formula below gives the extra height
   * back out of the leading, so the 1.3 rhythm is untouched.
   */
  --band: 1.14em;
}

/*
 * The window. One line tall, and nothing leaves it.
 *
 * `clip` rather than `hidden`: `hidden` makes the box a scroll container, which means a
 * browser can scroll it to bring a focused thing inside into view — and it would be
 * scrolling a line of a headline. `clip` cuts without that.
 */
/*
 * The window is the glyph, not the line box.
 *
 * This is where I had it wrong. Clipping to the full 1.3em line box is correct in the
 * box model and wrong on screen: at 1.3 the glyph fills about 0.8em of its line and the
 * rest is leading, so a line travelling upward is still painting inside its own window
 * when it has already reached the glyphs of the line above. "We make" was being crossed
 * out by the line beneath it.
 *
 * A headline set at 0.9 doesn't have this problem — its window is the type. Ours is 1.3
 * in the frame and should stay 1.3, so instead the
 * window shrinks to the band the glyphs actually occupy and the leading goes back as
 * margin. Same rhythm, same overall height, nothing able to travel into the gap.
 */
.rot-line__row {
  display: grid;
  grid-template: auto / auto;
  overflow: clip;

  /* The band the type sits in… */
  height: var(--band);
  line-height: var(--band);
  /* …and the leading, split evenly above and below, so the 1.3 rhythm is unchanged. */
  margin: calc((1.3em - var(--band)) / 2) 0;
}

.rot-line__slide {
  grid-area: 1 / 1;
  display: block;
  line-height: var(--band);
  transform: translateY(110%);
  transition: transform var(--change) var(--ease-out-quart);
  /* Each line follows the one above it in. */
  transition-delay: calc(var(--n, 0) * var(--stagger));
}

/* The two fixed lines arrive on load and never move again. */
.rot-line.is-revealed .rot-line__slide:not(.rot-line__slide--stacked) {
  transform: translateY(0);
}

/*
 * The changing line fades as well as travelling.
 *
 * A headline set at 0.9 doesn't need this — the three lines are one solid block and a
 * line sliding through it reads as part of that block. Ours is set
 * at 1.3, which leaves an open gap between the lines, and a line arriving at full
 * strength into that gap reads as a collision: for a moment "We make" looks crossed out
 * by the line beneath it.
 *
 * So the changing line is only at full strength in the middle of its travel. It is
 * already invisible by the time it reaches either edge of its window, which is what
 * stops it appearing to touch the lines it sits between. The two fixed lines don't fade
 * at all — the entrance is unchanged.
 *
 * A stacked line waiting its turn, with no transition on purpose.
 *
 * A line that has finished leaving has to get back below the window to be used again,
 * and it must do that instantly rather than travelling back down through it. CSS takes
 * the transition from the state being moved *to*, so the two states below still animate.
 *
 * This is also what keeps the other lines out of sight during the reveal: only the one
 * marked current is told to come up.
 */
.rot-line__slide--stacked {
  opacity: 0;
  transition: none;
}

.rot-line.is-revealed .rot-line__slide--stacked.is-current {
  transform: translateY(0);
  opacity: 1;
  transition:
    transform var(--change) var(--ease-out-quart),
    /* Opaque by the time it's clear of the edge, and it stays that way. */
    opacity calc(var(--change) * 0.4) linear;
  transition-delay: calc(var(--n, 0) * var(--stagger));
}

/*
 * Once the cycle is running the middle line answers immediately. The stagger was a
 * one-off for the entrance; carrying it into every change adds a tenth of a second of
 * nothing before each one.
 */
.rot-line.is-started .rot-line__slide--stacked.is-current {
  transition-delay: 0s;
}

.rot-line__slide--stacked.is-leaving {
  transform: translateY(-110%);
  opacity: 0;
  transition:
    transform var(--change) var(--ease-out-quart),
    /* Gone before it gets near the line above. */
    opacity calc(var(--change) * 0.35) linear;
}

/* Reduced motion: the sentence, still, from the first frame. */
.rot-line.is-still .rot-line__slide {
  transform: none;
  transition: none;
}

.rot-line.is-still .rot-line__slide--stacked.is-current {
  opacity: 1;
}

.rot-line.is-still .rot-line__slide--stacked:not(.is-current) {
  /* Out of the flow entirely, so it can't be selected or found by ctrl-F. */
  visibility: hidden;
}

.flx-masthead-mid__sub {
  margin: 0;
}

/*
 * The strapline, 20% up on a phone.
 *
 * It carries `text-size-r`, which is 16 everywhere and is right everywhere else. Under the
 * masthead on a phone it is the only line of copy on the screen and it read as a caption
 * under the headline rather than as the sentence that explains it, so it gets 19.2 there
 * and nothing changes above 899. The size is written on this block alone, so the body step
 * is untouched on every other page.
 */
@media only screen and (width <= 899px) {
  .flx-masthead-mid__sub {
    font-size: 19.2px;
    line-height: 1.5;
  }
}

/* --- The media frame ------------------------------------------------------ */

/*
 * Edge to edge in a hairline frame, with a small radius.
 *
 * It used to be inset by 56px on each side so the ground showed down both edges. The
 * headline dropping to two lines came with a wider picture, so the inset goes and the
 * image fills the frame.
 */
.flx-masthead-mid__frame {
  border: 0;
  border-radius: var(--radius-s);
  overflow: clip;
}

.flx-masthead-mid__media {
  /*
   * 16:9, the same ratio as every other picture on the site.
   *
   * It was a fixed 700px, which is the one number that made this picture disagree with
   * all of them: at the 1496 the container settles on it came out at better than 2:1,
   * so the homepage masthead — the first picture anyone sees — was the only wide crop
   * left. The reason it was fixed was that the container used to be a ladder of
   * breakpoints and a ratio grew the picture at every step; that is no longer an
   * argument against it, because 16:9 of the row is exactly what the galleries and the
   * featured media block do, and this now matches them.
   *
   * The phone rule further down takes it to 4:3, and it takes effect now — with a fixed
   * height set here it never could.
   */
  aspect-ratio: 16 / 9;
  background: #d9d9d9;
  /* The cycling images stack inside this box, so it has to be the box they measure
     against. Without it they escape to whatever is positioned further up and the frame
     collapses to a grey band. */
  position: relative;
}

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

@media only screen and (width <= 1024px) {
  .flx-masthead-mid .grid-container {
    gap: 64px;
  }

}

@media only screen and (width <= 640px) {
  .flx-masthead-mid .grid-container {
    gap: 48px;
  }
  .flx-masthead-mid__content {
    gap: 24px;
  }

  .flx-masthead-mid__media {
    /* Taller on a phone, so a wide crop doesn't become a letterbox slot. */
    aspect-ratio: 4 / 3;
  }
}

/* --- Footer --------------------------------------------------------------
   Figma: "Footer Update" (3594:62380).

   Written fresh rather than as overrides of the theme's `.footer`, because almost
   nothing survived: the old one is a Foundation grid of blocks, this is a statement
   with the logo, two link rows and two rows of cards. Sharing the class names would
   have meant fighting the old rules on every property.

   The cards carry the edge glow. They only need `glow-card` and a radius — see
   Glow.tsx.                                                                          */

/*
 * NO FILL. The footer is the page, not a panel on it.
 *
 * This carried `background: var(--bg-color)` — the same colour as the page behind it, so
 * it looked like nothing and did something: an opaque layer across the full width, which
 * the pixel field cannot show through. The pattern ran down the page and stopped dead at
 * the footer's top edge, which reads as the page ending early rather than as a footer.
 *
 * A colour identical to its own background is the easiest kind of rule to leave in and
 * the hardest to see. Now that the animation is on site-wide, it is the one that mattered.
 *
 * The text colour stays: the footer is white type whatever it is sitting on.
 */
.footer-new {
  color: var(--color-natural-100, #fff);
}

.footer-new .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
  /*
   * A third of what it was.
   *
   * There was about 88px of nothing under the copyright line: 40 of padding here and a
   * stray 48 of margin on the last block inside the details, which is the theme's default
   * paragraph spacing doing a job nobody asked it to do at the very bottom of the page. A
   * gap that size at the end of a document reads as a page that has not finished loading.
   */
  padding-bottom: 12px;
}

/* The last thing on the page owes nothing to whatever would have come after it. */
.footer-new .grid-container > *:last-child,
.footer-new__details > *:last-child,
.footer-new__legal,
.footer-new__legal > *:last-child {
  margin-bottom: 0;
}

/* --- Statement, logo, links ----------------------------------------------- */

.footer-new__top {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.footer-new__statement {
  display: grid;
  /* Eight columns of twelve for the sentence, four for the logo — the frame's grid. */
  grid-template-columns: 2fr 1fr;
  gap: 48px;
  align-items: center;
  padding-top: 64px;
  border-top: 1px solid var(--border-default);
}

.footer-new__statement p {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-size: 32px;
  line-height: 1.5;
  letter-spacing: -0.01em;
}

.footer-new__logo {
  display: flex;
  justify-content: flex-end;
}

.footer-new__logo svg,
.footer-new__logo img {
  width: 123px;
  height: auto;
}

.footer-new__links {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
}

.footer-new__menu,
.footer-new__socials {
  display: flex;
  align-items: center;
  gap: 32px;
  flex-wrap: wrap;
}

/*
 * Set in the heading face, not the body face — these are the size of an h4 and read as
 * type rather than as navigation.
 *
 * The resting state is 70% and the hover is white. That's the "Work" in the frame: it's
 * the hover state drawn in place, not a current-page marker.
 */
.footer-new__menu a,
.footer-new__socials a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: 'Swiza', sans-serif;
  font-size: 20px;
  line-height: 1.5;
  letter-spacing: -0.01em;
  white-space: nowrap;
  text-decoration: none;
  color: var(--color-natural-70);
  transition: color 0.2s ease;
}

.footer-new__menu a:hover,
.footer-new__menu a:focus-visible,
.footer-new__socials a:hover,
.footer-new__socials a:focus-visible {
  color: var(--color-natural-100, #fff);
}

/* --- The cards ------------------------------------------------------------ */

/* The cards and the small print, 32px apart — one group in the frame. */
.footer-new__details {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.footer-new__cards-wrap {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.footer-new__cards {
  display: flex;
  gap: 12px;
}

.footer-card {
  /* The glow draws on this element's own border box, so it has to be positioned and
     carry the radius. */
  position: relative;
  isolation: isolate;

  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 16px;
  padding: 24px 32px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
  text-decoration: none;
  color: inherit;
}

.footer-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.footer-card__title {
  font-family: 'Swiza', sans-serif;
  font-size: 20px;
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

/*
 * Its own size rather than the theme's `.text-size-sm`, which carries 1.5 leading. At
 * 14px that's seven pixels of nothing under every value, and it made every card in the
 * row eight pixels taller than the frame.
 */
.footer-card__value {
  font-size: 14px;
  line-height: 1;
  color: var(--color-natural-70);
}

/* The award wordmark, when there's no logo uploaded. Letterspaced caps rather than an
   approximation of somebody else's logotype. */
.footer-card__award-text {
  font-size: 14px;
  /* Same as a card value. Left on the inherited 1.5 it was seven pixels taller, and
     because the row stretches every card to the tallest, that made all four wrong. */
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-natural-70);
  white-space: nowrap;
}

/*
 * The award mark: left-aligned, and taller than the line it sits on.
 *
 * `align-self` because the card is a flex column — without it the image is stretched to
 * the card's width and the mark ends up in the middle of it.
 *
 * The height is the interesting part. Every card in the row is as tall as the tallest, so
 * growing this one grows all four. The other three carry a 21px value line (14px at
 * 1.5), so the mark is drawn at 32 and the eleven pixels of difference are handed back as
 * negative margin: its box still contributes 21px to the column, and the row does not
 * move.
 */
.footer-card__award {
  align-self: flex-start;
  /*
   * 14px, and the ratio is the logo's own.
   *
   * `height` with `width: auto` is what keeps the proportions: the picture is scaled to
   * the height and takes whatever width that implies, so a wordmark and a roundel both
   * come out right without either being told a width. `max-height` would leave a small
   * logo at its natural size — this is a cap AND a target, which is what a row of marks
   * needs to sit level.
   *
   * It was 32. The value line beside it is 21px (14 at 1.5), so a 32px mark was half as
   * tall again as the type it sits under and read as the loudest thing in the footer.
   * At 14 it matches the type's own size and reads as a credit rather than a banner.
   */
  height: 14px;
  width: auto;
  /* The mark is now shorter than the 21px value line it replaces, so the negative margin
     that pulled a taller box back into the row would push this one out of it. The row
     keeps its height on its own. */
  margin-block: calc((21px - 14px) / 2);
}

/* --- The avatar stack ----------------------------------------------------- */

.footer-avatars {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.footer-avatars__globe {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  margin-right: -4px;
  border-radius: 50%;
  background: var(--color-natural-10);
  color: var(--color-natural-70);
}

/*
 * The faces, lapped 4px and ringed in the card colour.
 *
 * The ring is a spread-only `box-shadow` rather than a border: a border would eat into
 * the 24px circle and shrink the face inside it, and the frame draws the ring outside.
 * It is the card's own colour, so the laps read as one strip cut out of the card rather
 * than as circles with outlines.
 *
 * This replaced a pair of flat PNGs — one ringed black, one ringed white, swapped by
 * theme. Neither ever rendered on the live site, and a baked strip goes stale the moment
 * the team changes. Real people from the library, ringed in CSS, do neither.
 */
.footer-avatars__face {
  position: relative;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  margin-right: -4px;
  border-radius: 50%;
  background: var(--color-natural-10, rgba(255, 255, 255, 0.1));
  box-shadow: 0 0 0 2px var(--surface-card, #0f0f0f);
}

.footer-avatars__face:last-child {
  margin-right: 0;
}

.footer-avatars__face img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}

/* --- Small print ---------------------------------------------------------- */

/*
 * One row: the company on the left, the legal links on the right.
 *
 * They were stacked, which on a full-width footer meant two short lines against the left
 * edge and an empty half. They are the same kind of thing — small print — and reading as
 * one line across the bottom is what makes them look deliberate rather than left over.
 *
 * `wrap` with a gap rather than a breakpoint: on a narrow screen they fall onto two lines
 * on their own, which is the same result the old rule gave and one rule fewer.
 */
.footer-new__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 32px;
  /* The grid button is fixed to the bottom-left corner, so at the very end of the page it
     lands on top of whatever the last line is. This is the air that keeps the small print
     above it. Belongs here rather than on the button, because the button's job is to be in
     the same place on every page.
     
     Margin rather than padding, and not for the usual reasons: padding is part of the
     element's box, so the x-ray outlined a 48px empty rectangle under the copyright and it
     read as a stray element. Space that exists to keep something else away belongs outside
     the box, which is what margin is for. */
  margin-bottom: 48px;
}

.footer-new__copy,
.footer-new__copy p,
.footer-new__legal-links a {
  margin: 0;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--color-natural-50, rgb(255 255 255 / 0.5));
}

/* The company line in full white - it is the one line in the row that is us. */
.footer-new__copy,
.footer-new__copy p {
  color: #fff;
}

.footer-new__legal-links {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.footer-new__legal-links a {
  text-decoration: none;
  transition: color 0.2s ease;
}

.footer-new__legal-links a:hover,
.footer-new__legal-links a:focus-visible {
  color: var(--color-natural-100, #fff);
}

/* The 4px separator, drawn rather than added as markup. */
.footer-new__legal-links a + a::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-right: 8px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
}

@media only screen and (width <= 1024px) {
  .footer-new .grid-container {
    gap: 64px;
  }
  .footer-new__statement {
    grid-template-columns: 1fr;
    gap: 32px;
    padding-top: 48px;
  }
  .footer-new__logo {
    justify-content: flex-start;
  }
  .footer-new__cards {
    flex-wrap: wrap;
  }
  .footer-card {
    /* Two up, so each takes half the row less half the card gap. Restated at the bottom
       of this file off the token, where the gap itself is set. */
    flex: 1 1 calc(50% - 6px);
  }
}

@media only screen and (width <= 640px) {
  .footer-new .grid-container {
    gap: 48px;
  }
  .footer-new__statement p {
    font-size: 24px;
  }
  .footer-new__menu,
  .footer-new__socials {
    gap: 20px;
  }
  .footer-card {
    flex: 1 1 100%;
  }
}

/* --- The availability light ----------------------------------------------
   Figma: "Header Message" (3578:98680). The dot sits in a 16px box, so there is
   already room around it for the ring to travel into without moving anything.

   One ring, expanding and fading. Not a second one on a delay — two overlapping
   rings is a notification badge demanding attention, and this is a status light
   saying everything is fine.

   The dot itself never moves. Only the ring behind it does, and it's drawn with a
   box-shadow spread rather than a scaled element so it can't affect layout.       */

.header-message__dot {
  position: relative;
}

.header-message__dot::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* Inherits the dot's colour, so it follows the status rather than being green
     forever. */
  background: inherit;
  animation: gd-status-pulse 2.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  pointer-events: none;
}

@keyframes gd-status-pulse {
  0% {
    transform: scale(1);
    /* Nearly full strength at the start of the beat - the old 0.5 faded before anyone
       caught it. */
    opacity: 0.9;
  }
  /* Most of the cycle is the rest between beats. A pulse that pulses constantly is
     a strobe. */
  70%,
  100% {
    transform: scale(3.2);
    opacity: 0;
  }
}

/* --- Hover border gradient -----------------------------------------------
   The travelling border on hover, in white.

   A highlight travels around the edge and the whole edge lights on hover. Their four
   positions are lopsided on purpose — wider on the top and bottom than the sides —
   which keeps the highlight the same apparent size all the way round a shape that
   isn't square:

     top     20.7% 50%   at 50% 0%
     right   16.2% 41.2% at 100% 50%
     bottom  20.7% 50%   at 50% 100%
     left    16.6% 43.1% at 0% 50%

   ## Why this isn't four keyframes

   It was, and it was wrong. The state swaps on a one-second interval, which looks like
   it should be four discrete steps — but the swap is eased linearly and the numbers
   inside the gradient are tweened, not the string. So the highlight *slides* from one
   edge to the next. Stepping
   between the four positions gets you the four positions and none of the travel,
   which is exactly the difference you can see.

   Interpolating a gradient in plain CSS needs the numbers to be properties rather
   than part of a string, and a custom property only interpolates once it has been
   registered with a type. Hence the four `@property` declarations: with those, the
   position and size animate, and the gradient is rebuilt from them every frame.

   Order is clockwise from the top, which is their default.                         */

@property --hb-x {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 50%;
}

@property --hb-y {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}

@property --hb-w {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 20.7%;
}

@property --hb-h {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 50%;
}

.hover-border {
  position: relative;
  display: inline-flex;
  /* The border is this padding — the two layers show through it and nowhere else. */
  padding: 1px;
  border: 0;
  border-radius: 999px;
  /* The resting edge, under the travelling highlight. */
  background: var(--border-default);
  isolation: isolate;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}

.hover-border__travel,
.hover-border__glow {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;
}

.hover-border__travel {
  background: radial-gradient(
    var(--hb-w) var(--hb-h) at var(--hb-x) var(--hb-y),
    #fff 0%,
    rgba(255, 255, 255, 0) 100%
  );
  /* Linear. Any easing here would make the highlight hesitate at each
     corner, which is the one thing a travelling light shouldn't do. */
  animation: gd-border-travel 4s linear infinite;
}

.hover-border__glow {
  background: radial-gradient(
    75% 181.16% at 50% 50%,
    #fff 0%,
    rgba(255, 255, 255, 0) 100%
  );
  opacity: 0;
  transition: opacity 1s linear;
}

.hover-border:hover .hover-border__glow,
.hover-border:focus-visible .hover-border__glow {
  opacity: 1;
}

/* The label sits on its own ground, which is what leaves only the 1px ring showing. */
.hover-border__inner {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 34px;
  padding: 0 15px;
  border-radius: inherit;
  background: var(--bg-color);
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  color: var(--color-natural-100, #fff);
}

@keyframes gd-border-travel {
  /* top */
  0% {
    --hb-x: 50%;
    --hb-y: 0%;
    --hb-w: 20.7%;
    --hb-h: 50%;
  }
  /* right */
  25% {
    --hb-x: 100%;
    --hb-y: 50%;
    --hb-w: 16.2%;
    --hb-h: 41.2%;
  }
  /* bottom */
  50% {
    --hb-x: 50%;
    --hb-y: 100%;
    --hb-w: 20.7%;
    --hb-h: 50%;
  }
  /* left */
  75% {
    --hb-x: 0%;
    --hb-y: 50%;
    --hb-w: 16.6%;
    --hb-h: 43.1%;
  }
  /* back to the top, so the loop closes without a jump */
  100% {
    --hb-x: 50%;
    --hb-y: 0%;
    --hb-w: 20.7%;
    --hb-h: 50%;
  }
}

/*
 * Both are decoration. Under reduced motion the edge stops travelling and the button
 * keeps its resting border; hovering still lights it, because that's feedback rather
 * than movement.
 */
@media (prefers-reduced-motion: reduce) {
  .header-message__dot::after {
    animation: none;
    opacity: 0;
  }
  .hover-border__travel {
    animation: none;
  }
}

/* --- Partners logo grid ---------------------------------------------------
   The edge glow, same as the footer cards. The theme already gives .logo-item
   `position: relative` and a 4px radius, so the only thing missing was a stacking
   context: the soft inner wash sits at z-index -1, and without `isolation` it would
   fall through the card and paint behind the section instead of inside it.

   The theme's own border-colour hover stays — it's the same movement the glow makes,
   half a second slower, and the two read as one gesture.                            */

.logo-item.glow-card {
  isolation: isolate;
}

/* --- Statement -----------------------------------------------------------
   Figma: "Masthead 1" (3591:44124).

   One sentence at the display step, container width, and nothing else in the band.

   The size is the comp's, not the site's. `.text-size-d` tops out at 48px because the
   display step was brought down across the site earlier; this frame asks for 80px with
   -2% tracking. Rather than quietly move the global step — which would resize the page
   headers that were already signed off — the statement carries its own size, on the same
   1024→1440 clamp formula the rest of the scale uses. If the display step is meant to be
   80 everywhere, this rule is the one to delete.                                       */

/*
 * The design system's display step — heading-size-d, 80px.
 *
 * `.text-size-d` is 48 because the heading sizes were brought down across the site after
 * it was written, and moving it now would resize page headers that are already signed
 * off. So the 80 the frames actually draw lives here under its own name. Two sections use
 * it, and any new one that wants a display line should too.
 *
 * If the display step is meant to be 80 everywhere, this rule and `.text-size-d` should
 * become one — that is a decision about the scale, not about these sections.
 */
.text-size-display,
.flx-statement__text {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /* The display step. See the type scale at the top of this file. */
  font-size: var(--fs-d);
  line-height: 1.3;
  letter-spacing: -0.02em;
}

/* --- Work carousel -------------------------------------------------------
   Figma: "Card Carousel" (3603:47683).

   Cards 689 wide with a 48 gutter on a 1512 frame — so a card is 45.57vw and roughly
   one and three quarters of them are on screen at once, with the next one cut by the
   right edge. Holding that as a vw rather than a fixed width keeps the same framing at
   every size instead of stranding one enormous card on a laptop.

   The scrub itself is in WorkScrub.tsx. Everything here is layout, plus the two states
   the component toggles:

     .is-scrubbing   wide screen, motion allowed — the track is transformed
     (neither)       phone, or reduced motion — the track is an ordinary scroller       */

/*
 * The row is cut at the window, not at the container — so a card leaving the screen runs
 * off it rather than vanishing at the margin. `clip` and not `hidden`: `hidden` would
 * make this a scroll container and swallow the row's own horizontal scrolling.
 */
.flx-work {
  overflow-x: clip;
}

.work-row {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

/*
 * The theme gives every eyebrow 56px of margin under it. Every section that spaces its
 * children with a flex gap has to zero that or it pays twice — here it was 96 of gap plus
 * 56 of margin, so the heading sat 152 below the rule instead of 96.
 */
.flx-work .eyebrow {
  margin-bottom: 0;
}

.flx-work__head {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

.flx-work__title-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 48px;
}

/* The "See all work" link and the two arrows share the right-hand end, bottom-aligned
   with the heading beside them. */
.flx-work__actions {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 12px;
}

.flx-work__title {
  margin: 0;
  max-width: 60%;
}

.flx-work__all {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 10px 20px;
  height: 40px;
  border: 1px solid var(--border-default);
  border-radius: 99999px;
  color: inherit;
  text-decoration: none;
  font-size: 14px;
  white-space: nowrap;
  transition: border-color 0.3s ease;
}

.flx-work__all:hover,
.flx-work__all:focus-visible {
  border-color: var(--border-hover);
  color: inherit;
}

.flx-work__all svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.flx-work__all:hover svg {
  transform: translateX(3px);
}

/*
 * The row itself.
 *
 * It was a pinned scrub: a spacer as tall as the row was wide, a sticky stage inside it,
 * and a transform written from the scroll position. It is a row you drag now — the same
 * engine as the quotes row, which is `useDragScroll.ts` — and it starts flush with the
 * container rather than indented.
 *
 * Everything the scrub needed and this does not is gone with it: the spacer, the sticky
 * stage, the separate measuring box, the trailing spacer element, the fallback for
 * windows too short to pin in, and the 240px indent. What is left is a scroller.
 *
 * `.drag-row` supplies the drag, the momentum, the cursor and the bleed to the right-hand
 * edge of the window; there is nothing to add here but the gap.
 */
/*
 * 24 between one card and the next, which is the Work page's column gap.
 *
 * It was 48. The row and the Work grid draw the same cards from the same pictures, and
 * two different gaps is what made them read as two different components.
 */
.work-row__track {
  gap: 24px;
}

.work-card {
  flex: 0 0 auto;
  /*
   * 689 on a 1512 frame is 45.57vw, which keeps roughly one and three quarters of a card
   * on screen at every width instead of stranding one enormous card on a laptop.
   *
   * On a window too short to hold the whole section, WorkScrub does not pin it at all —
   * see there. Shrinking the cards to fit was the other option and it is what caused the
   * cut edges: once a card is not exactly a fraction of the row, part of a third one is
   * always showing.
   */
  /*
   * A Work page column, exactly.
   *
   * The card was 689 wide and its picture was 3:2 landscape, while the same case study
   * on the Work page is a 389:500 portrait in a 3-up grid — the same featured image,
   * cropped two different ways at two different sizes. Now the row's card *is* a Work
   * page tile: the width below resolves to the same number the grid's column does at
   * every breakpoint, and it steps at the same widths.
   *
   *   ≥1024   four columns, which is what three across a twelve column grid is.
   *   ≤1023   two across:   (viewport − 40 padding − 24 of gap) / 2.
   *   ≤639    the grid goes to one full-width column, which in a *scroller* would mean
   *           nothing of the next card showing, so the row keeps a card and a bit.
   *
   * ## The arithmetic that was wrong, because it is worth not repeating
   *
   * This was `min(483px, calc((100vw - 88px) / 3))` — three across the window, less the
   * 40 of page padding and 48 of gap. Three cards in a row have TWO gaps between them,
   * not one, so it was 16px per card too wide and every card after the first sat further
   * off the line than the one before it. By the fourth the row was 48 out, which is a
   * whole gutter, and the drift is exactly what the overlay was drawing.
   *
   * The deeper fault was measuring against the window at all. The window is not the grid;
   * the container is, and it stops growing at points the window does not. Stated as
   * columns of the container the number cannot drift, cannot be wrong at one width and
   * right at another, and does not need a cap bolted on to stop it running away.
   */
  width: calc((100% - 11 * 48px) / 12 * 4 + 3 * 48px);
  display: flex;
  flex-direction: column;
  gap: 24px;
  color: inherit;
  text-decoration: none;
}

/*
 * No stroke on a picture.
 *
 * Every picture frame on the site was drawn as a hairline box around the image. On a
 * photograph that already has its own edge the line adds nothing and reads as a border
 * left on by mistake — the picture is the object, not a thing inside a box.
 *
 * The background stays: it is what fills the frame while the image is still loading, and
 * without it the space is a hole rather than a placeholder.
 */
.work-card__frame {
  display: block;
  position: relative;
  /* 389:500 — the Work page tile's ratio. See the note on the width above. */
  aspect-ratio: 389 / 500;
  border: 0;
  border-radius: var(--radius-s);
  overflow: clip;
  background: rgba(255, 255, 255, 0.04);
}

.work-card__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* A slow, small push in on hover. The frame clips it, so the card itself never moves
     and the row stays on its grid. */
  transform: scale(1);
  transition: transform 0.6s cubic-bezier(0.26, 1, 0.48, 1);
}

.work-card:hover .work-card__img,
.work-card:focus-visible .work-card__img {
  transform: scale(1.03);
}

/* The picture already grows under the pointer — see `.work-card__img` above. A stroke
   lighting up as well was the same message said twice. */
.work-card:focus-visible .work-card__frame {
  outline: 2px solid var(--color-natural-40);
  outline-offset: 2px;
}

/* The two steps the Work grid takes, taken here at the same widths. */
@media only screen and (width <= 1023px) {
  .work-card {
    width: calc((100vw - 64px) / 2);
  }
}

@media only screen and (width <= 639px) {
  .work-card {
    /* Not the grid's full-width column: in a scroller that leaves nothing of the next
       card on screen and the row stops looking like one. */
    width: min(320px, 78vw);
  }
}

.work-card__info {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-bottom: 24px;
}

.work-card__head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.work-card__title {
  flex: 1 1 auto;
  margin: 0;
  line-height: 1.5;
  letter-spacing: -0.01em;
}

/* The arrow leans out on hover — the same gesture the link makes. */
.work-card__head svg {
  flex: 0 0 auto;
  transition: transform 0.4s cubic-bezier(0.26, 1, 0.48, 1);
}

.work-card:hover .work-card__head svg,
.work-card:focus-visible .work-card__head svg {
  transform: translate(2px, -2px);
}

.work-card__text {
  margin: 0;
  /*
   * ink/70, the same as every other line of supporting copy on the site — the subhead
   * under a page header, the sentence under a section heading.
   *
   * It was `--color-grey`, which is a fixed hex rather than a step on the ink scale, and
   * on this dark ground it resolves to #4d4d4d: barely above the black it sits on, and
   * visibly dimmer than the identical kind of line anywhere else. Two lines doing the same
   * job in the same typeface should not be two different greys.
   */
  color: var(--color-natural-70);
}

/*
 * No pin: phones, and anyone who asked for less movement.
 *
 * The track becomes a plain scroller. It keeps the right-edge bleed so the next card is
 * still cut by the window — that cut is what says "there is more", and it does the job
 * whether the movement is a scrub or a thumb.
 */
@media only screen and (width <= 899px), (prefers-reduced-motion: reduce) {
  .work-row,
  .flx-work__head {
    gap: 48px;
  }

  .work-row__track {
    gap: 16px;
  }

  .flx-work__title-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 24px;
  }

  .flx-work__title {
    max-width: none;
  }
}

/* --- Header, hiding on scroll --------------------------------------------

   The full-width bar, unchanged in every respect except that it gets out of the way when
   the page is being scrolled down and comes back when it is scrolled up. No pill, no
   narrower container, no change of background — the header that is there at the top of
   the page is the same header the whole way down.

   Two states, both set by FloatingHeader.tsx:

     .header--scrolled   riding with the page rather than sitting at the top of it
     .header--out        gone through the top, because the page is going down

   Named after the block, not `.is-hidden`: Foundation already has that name, and it means
   `display: none !important`. Borrowing it took the header out of the flow instead of
   sliding it — the document lost the header's height, the browser clamped the scroll to
   match, the clamp read as an upward scroll, and the header came back. It hid and showed
   several times a second and never settled.

   `position: sticky`, not `fixed`, and this is the whole trick. The theme gives
   `.header-wrap` a fixed 108px of height in the flow. Taking it out of the flow — which
   `fixed` does — shortens the document by 108px, the browser clamps the scroll position
   to match, and that clamp arrives as an upward scroll: the header decides it is being
   scrolled up and reappears the instant it hides. Sticky keeps the 108px exactly where
   it was, so nothing moves and nothing lies about which way the page went.

   Only `transform` animates, so it stays a compositor job and the page underneath is
   never relaid out.                                                                    */

.header-wrap.header--scrolled {
  position: sticky;
  top: 0;
  z-index: 5000;
  transform: translateY(0);
  transition: transform 0.25s ease;
}

.header-wrap.header--scrolled.header--out {
  /* Its own height, so it clears the top of the window whatever the header ends up
     being — 108px on a desktop, 80px on a phone. */
  transform: translateY(-100%);
}

/* Nothing off-screen should be clickable. */
.header-wrap.header--scrolled.header--out .menu-grid-container {
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .header-wrap.header--scrolled {
    transition: none;
  }
}

/* --- Services list -------------------------------------------------------
   Figma: "Services list" (3595:51855).

   Three columns on a 1216 container: the six names at 373, then the sentence and the
   list of what the service covers, 373.5 each, with 48 between all of them.

   The markup is button, panel, button, panel — see ServicesList.tsx. Here that becomes a
   grid where the buttons run down column one and every panel is pinned to row one of
   column two, stacked; below 900px the same markup is a plain column, so each panel is
   already under its own button and the section is an accordion without moving anything. */

.flx-services .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

/* The theme gives every eyebrow 5rem underneath it. Here the column's own 96px gap is
   the spacing, and the two together were putting the first name a third of the way down
   the section. */
.flx-services .eyebrow {
  margin-bottom: 0;
}

/* --- Scroll-driven services -------------------------------------------------
   On a desktop the list pins and scrolling walks the six chapters; the wrapper's
   height is the runway (40vh a step, plus the viewport the pinned list occupies).
   Phones keep the accordion, small viewports keep the plain list, and a
   reduced-motion setting keeps everything exactly where it is put — the wrapper
   collapses to its content and `sticky` never engages because there is nothing
   to travel inside. ServicesList.tsx mirrors these same conditions in script. */
/* Eyebrow, list and foot stack inside the pin wrapper in every mode. The 96 matches
   the flex gap the section's container gave these elements when they were its direct
   children, so nothing moves where the section is not scroll-driven. */
.svc-pin {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

@media only screen and (width >= 900px) and (height >= 780px) and (prefers-reduced-motion: no-preference) {
  .svc-scroll {
    height: calc(100vh + var(--steps, 6) * 40vh);
  }

  /* The pinned element is the whole chapter — eyebrow, the six names, and the foot —
     a viewport tall and centring its content, so the block holds in the middle of the
     screen rather than hanging from the top. */
  .svc-scroll .svc-pin {
    position: sticky;
    top: 0;
    height: 100vh;
    justify-content: center;
    gap: 80px;
  }
}

.svc {
  display: grid;
  /*
   * Three equal tracks, not `373px 1fr 1fr`.
   *
   * 373 was a measurement lifted off the Figma frame, and a fixed pixel width can only
   * land on a column line at the one width it was drawn at. Everywhere else this section
   * quietly kept its own rhythm while every section above and below it kept the grid's,
   * which is exactly the sort of thing nobody can name and everybody can feel.
   *
   * With a 48 gutter, three equal tracks ARE four columns each: (100% - 96) / 3 works out
   * to 4 columns plus the three gutters inside them, every time, at every breakpoint. No
   * calc, no magic number, and provably on the grid rather than approximately on it.
   */
  grid-template-columns: repeat(3, 1fr);
  /* One row per name. Stated rather than implicit so a panel can span all of them. */
  grid-template-rows: repeat(var(--rows, 6), auto);
  column-gap: 48px;
  row-gap: 12px;
  align-items: start;
}

.svc__name {
  grid-column: 1;
  grid-row: calc(var(--n) + 1);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  /* Where the stacked layout anchors a tapped name to (see ServicesList.tsx) — clear
     of the floating header rather than underneath it. */
  scroll-margin-top: 96px;
  /* White. The theme's body colour is ink/70 and everything inherits it — the names and
     their numbers are ink/100 in the frame, and the dimming is the opacity below, not a
     second colour. */
  color: var(--color-natural-100, #fff);
  text-align: left;
  cursor: pointer;
  /* Both the number and the name dim together, which is what the frame does. Doing it
     with opacity rather than two colours also keeps it right on any ground. */
  opacity: 0.3;
  transition: opacity 0.3s ease;
}

/*
 * Foundation styles bare `button` with a white background, and `button:hover` beats a
 * single class — so without restating it here the name you are pointing at turns into a
 * white block. Stated on every interactive state rather than just hover, because :focus
 * carries the same rule and a keyboard is how this control is meant to be driven.
 */
.svc__name:hover,
.svc__name:focus,
.svc__name:active {
  background: none;
  color: var(--color-natural-100, #fff);
}

.svc__name:hover {
  opacity: 0.6;
}

/* The theme turns outlines off site-wide. This control moves the panel with the arrow
   keys, so it has to show where the focus is. */
.svc__name:focus-visible {
  outline: 1px solid var(--border-hover);
  outline-offset: 8px;
  border-radius: 2px;
  opacity: 1;
}

.svc__name.is-open,
.svc__name.is-open:hover {
  opacity: 1;
}

.svc__num {
  flex: 0 0 28px;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h6);
  line-height: 1.5;
}

.svc__title {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /* h1. This used to restate the step at 40 because that is what the theme's h1 was; the
     scale is the tokens' now and h1 is the 44 the frame asks for. */
  font-size: var(--fs-h1);
  line-height: 1.3;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.svc__panel {
  grid-column: 2 / -1;
  /*
   * Every row, not just the first.
   *
   * In row one alone a 285px panel makes row one 285px tall, and the six names stop being
   * a list and become a paragraph with holes in it. Spanning the grid means the panel is
   * measured against all six rows together, which it is shorter than — so it changes
   * nothing — and a longer one lengthens the section instead of one row of it.
   */
  grid-row: 1 / -1;
  align-self: start;
}

/*
 * Two cards, 12 apart. Not one card split down the middle.
 *
 * It was one panel holding two columns with a hairline between them, which is a
 * reasonable reading of the earlier frame but not what 3595:49126 draws: two separate
 * surfaces, and the gap between them is the divider. The rule on the covers list goes
 * with it — a border needs something to sit against, and there is nothing there now.
 */
.svc__panel-inner {
  display: grid;
  /*
   * Eight columns, because that is what the panel spans.
   *
   * It was `1fr 1fr` with a 96 gap, chosen by eye to separate the sentence from the list.
   * It read well and it was off the grid: two equal halves of an eight-column box with a
   * 96 gap put the left card 24 short of its column line and the right one 24 past it, and
   * the overlay caught both.
   *
   * The air was the right instinct and the gap was the wrong lever. The sentence takes four
   * columns, the list takes three, and the empty column between them gives 167px of
   * separation rather than 96 — more air than the number it replaces, and every edge on a
   * line. The gap is the frame's own 48 again, doing what a gutter does instead of standing
   * in for a margin.
   */
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 48px;
  /* Both cards the height of the taller one, as drawn. */
  align-items: stretch;
}

/* The same filled card as the logos, the quotes and the stat rows, at 16 radius with no
   border — but 40 of padding rather than the 28 those carry. */
/*
 * NOT A CARD ANY MORE, despite the name.
 *
 * These two columns were drawn as filled, rounded, 40px-padded boxes. The frame has no
 * box: the sentence and the list sit directly on the black, in line with the numbers
 * beside them, and the only thing separating the three columns is the gap between them.
 *
 * The boxes were doing real damage, not just adding decoration. Forty pixels of padding
 * pushed the sentence out of alignment with the service name it belongs to, so the eye
 * had to re-find the baseline on every click. Without them the three columns share one
 * top edge and the section reads as one row of information rather than a heading and two
 * containers.
 *
 * The class keeps its name because the component sets `--i` on it for the entrance
 * stagger, and renaming it would be a change to two files for no gain.
 */
.svc__card {
  background: none;
  border-radius: 0;
  padding: 0;
  min-width: 0;
}

/* Four columns of prose, a column of air, three of list. */
.svc__card:first-child { grid-column: 1 / 5; }
.svc__card:last-child { grid-column: 6 / 9; }

/*
 * A fade-and-rise as an animation rather than a transition, because a panel arrives from
 * `display: none` — and a transition on an element that was not being drawn a frame ago
 * has nothing to transition from.
 *
 * The whole chapter reads left to right: the name lights first (its 0.3s opacity
 * transition starts on the click), the prose card follows at 140ms — as the name is
 * mostly lit — and the covers list at 300ms. The step comes off `--i`, which the
 * component sets per card, so adding a third card later needs nothing here.
 */
.svc__panel.is-open .svc__card {
  animation: gd-svc-in 0.45s cubic-bezier(0.26, 1, 0.48, 1) both;
  animation-delay: calc(140ms + var(--i, 0) * 160ms);
}

/* Someone who has asked for less movement gets the cards, not the entrance. */
@media (prefers-reduced-motion: reduce) {
  .svc__panel.is-open .svc__card {
    animation: none;
  }
}

@keyframes gd-svc-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/*
 * The panel on its way out.
 *
 * On a wide screen all six panels share one grid cell, so a click is a crossfade and not
 * an open and a close: the outgoing panel and the incoming one are drawn on top of each
 * other for a fifth of a second. That is why this is a fade and a small lift rather than
 * a collapse — there is no height to collapse, and an earlier attempt to animate one
 * proved it by doing nothing at all.
 *
 * Shorter than the entrance on purpose. The thing arriving is what somebody asked for and
 * is allowed to take its time; the thing leaving is in the way.
 */
.svc__panel.is-leaving {
  animation: gd-svc-out 0.2s ease-in both;
  /* It is still on screen, so it can still be clicked. It should not be. */
  pointer-events: none;
}

@keyframes gd-svc-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-6px);
  }
}

/*
 * Two cases where the outgoing panel is taken away rather than faded.
 *
 * Reduced motion: with no animation it would simply sit there at full opacity, two panels
 * open at once, which is worse than the instant switch it replaced.
 *
 * Under 900 this is an accordion in a flex column, not a stack — the outgoing panel is in
 * the flow above the incoming one, so fading it would push the whole section down and let
 * it snap back. `display: none` takes it out of the flow on the same frame; the entrance
 * animation still runs and the section grows once, in one direction.
 */
@media (prefers-reduced-motion: reduce) {
  .svc__panel.is-leaving {
    display: none;
  }
}

@media only screen and (width <= 899px) {
  .svc__panel.is-leaving {
    display: none;
  }
}

.svc__text {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  /* h6 in the frame. */
  font-size: var(--fs-h6);
  line-height: 1.5;
  /* The open item's opening line is ink/100. Inherited it was the body's ink/70. */
  color: var(--color-natural-100, #fff);
}

.svc__covers {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  /* 33 between one line and the next: 21 of text and 12 of gap. */
  gap: 12px;
}

.svc__covers li {
  margin: 0;
  /* ink/100. The frame sets the whole panel white — this was the one part of it still
     inheriting the body's ink/70. */
  color: var(--color-natural-100, #fff);
}

.flx-services__foot {
  display: flex;
  align-items: center;
  gap: 28px;
  flex-wrap: wrap;
}


.flx-services__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 8px 20px;
  border: 1px solid var(--border-default);
  border-radius: 99999px;
  font-size: 14px;
  line-height: 1;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
  /*
   * The same 0.25s ease the card around it uses.
   *
   * The pill moved on 0.3s and the connect card on 0.25s, so hovering one row started two
   * animations a twentieth of a second apart, finishing a twentieth apart. Small enough
   * that nobody can name it and large enough that the pair reads as one thing dragging
   * behind the other rather than one object responding.
   *
   * The arrow inside is a different property doing a different job and keeps its own curve,
   * but it starts at the same instant. That is the distinction worth keeping: things that
   * are the same gesture share a duration, things that are separate gestures do not.
   */
  transition: border-color 0.25s ease, background-color 0.25s ease, color 0.25s ease;
}

.flx-services__btn:hover,
.flx-services__btn:focus-visible {
  border-color: var(--border-hover);
  background: rgba(255, 255, 255, 0.06);
  color: inherit;
}

.flx-services__btn svg {
  /* Starts with the rest and settles a fraction later, which is what makes the arrow read
     as being carried by the card rather than animated alongside it. */
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

.flx-services__btn:hover svg {
  transform: translateX(3px);
}

.flx-services__note {
  margin: 0;
}

/* An accordion. Same markup, one column, and each panel is already where it needs to be. */
@media only screen and (width <= 899px) {
  .flx-services .grid-container {
    gap: 48px;
  }

  .svc {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  .svc__name {
    padding: 12px 0;
  }

  .svc__title {
    white-space: normal;
  }

  /* One column of two cards on a phone, still 12 apart. The padding that used to sit on
     the panel belongs to the cards now. */
  .svc__panel-inner {
    grid-template-columns: 1fr;
    gap: 12px;
    margin: 0 0 24px;
  }

  /* The template collapsed but the cards' own column spans did not: `1 / 5` and `6 / 9`
     invent implicit columns and the pair sat squashed side by side in them. On one
     column every card is simply the next row. */
  .svc__card:first-child,
  .svc__card:last-child {
    grid-column: auto;
  }

}


/* --- The heading-then-indented-column shape -------------------------------
   Stats (3595:54179), People (3603:44658) and Connect (3603:45098) are the same layout,
   and it is not a two-column grid — that was the first reading and it was wrong. The
   heading runs the full width of the container; what follows is indented to a 689px
   column held against the right-hand margin, one gap of 96 below it.

   689 of 1216 is 56.66%, held as a percentage so it stops being a fixed column on a
   laptop. Below 900px it goes full width.

   The class is on `.grid-container` itself, not an ancestor — which is why these are
   written per section rather than as one `.two-col` wrapper.                           */

.flx-stats .grid-container,
.flx-people .grid-container,
.flx-connect .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

/* The theme gives every eyebrow 5rem underneath it; here the column's own gap is the
   spacing, and the two together push the heading a third of the way down the section. */
.flx-stats .eyebrow,
.flx-people .eyebrow,
.flx-connect .eyebrow {
  margin-bottom: 0;
}

.col-head {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.col-head__title,
.col-head p {
  margin: 0;
}

.col-right {
  /*
   * Seven columns, not 56.66%.
   *
   * 689/1216 was measured off the frame, and a percentage of a container is only the same
   * thing as a column span at the one width the frame was drawn at. Everywhere else it
   * landed a few pixels off the line, which is exactly the sort of miss nobody can name and
   * everybody can feel. The overlay called it at +5.
   */
  width: calc((100% - 11 * 48px) / 12 * 7 + 6 * 48px);
  margin-left: auto;
}

/* --- Stats --------------------------------------------------------------- */

/* .stat-rows is a two-column grid now — see the Impact rules further down. */

.stat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

/*
 * The rule between rows, and the 48 either side of it, as one rule.
 *
 * A border on every row plus a margin would put a line above the first one; this way the
 * spacing and the line arrive together and only between two rows, which is the only place
 * either belongs.
 */
.stat-row + .stat-row {
  margin-top: 48px;
  padding-top: 48px;
  border-top: 1px solid var(--border-default);
}

.stat-row__value {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /* The number is the section. White, not the body's inherited ink/70. */
  color: var(--color-natural-100, #fff);
  /* The display step — the number is the point of the section. */
  font-size: var(--fs-d);
  line-height: 1.3;
  letter-spacing: -0.02em;
}

.stat-row__meta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-align: right;
}

.stat-row__emoji {
  font-size: 24px;
  line-height: 1;
}

.stat-row__label {
  color: inherit;
}

/* --- People -------------------------------------------------------------- */

/*
 * The heading and the faces are one bottom-aligned row: the heading takes the space and
 * the faces sit against the right-hand edge. They used to sit under the last line of the
 * heading — that was the earlier drawing of this section.
 */
.flx-people__head {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: 48px;
  width: 100%;
}

.flx-people__title {
  margin: 0;
  flex: 1 1 0;
  min-width: 0;
}

.flx-people__faces {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 20px;
  /* Dropped 20 so the discs rest on the heading's baseline rather than its line box. */
  padding-bottom: 20px;
}

.face-stack {
  display: inline-flex;
}

.face-stack__face {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: clip;
  background: rgba(255, 255, 255, 0.12);
  /* 28px of pitch on a 40px face — they overlap by twelve. */
  margin-left: -12px;
  /* A hairline in the page's own background, so the faces read as separate discs rather
     than one strip of texture. */
  box-shadow: 0 0 0 2px var(--bg-color);
}

.face-stack__face:first-child {
  margin-left: 0;
}

.face-stack__face img,
.face-stack__face svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* h5 — the one step the tokens set in semibold. */
.flx-people__more {
  font-family: 'Swiza', sans-serif;
  font-weight: 600;
  font-size: var(--fs-h5);
  line-height: 1.5;
}

/* 689 wide and against the left edge — the frame stops it there rather than indenting
   it on the right, which is where it used to sit. */
.flx-people__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 40px;
  max-width: 689px;
}

.flx-people__text {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-100, #fff);
}

.flx-people__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 8px 20px;
  border: 1px solid var(--border-default);
  border-radius: 99999px;
  color: inherit;
  text-decoration: none;
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  transition: border-color 0.3s ease;
}

.flx-people__btn:hover,
.flx-people__btn:focus-visible {
  border-color: var(--border-hover);
  color: inherit;
}

.flx-people__btn svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.flx-people__btn:hover svg {
  transform: translateX(3px);
}

.flx-people__body p {
  margin: 0;
}

/* --- Connect ------------------------------------------------------------- */

/*
 * 3603:45097.
 *
 * These were headings with a rule between each pair. They are cards now — the identical
 * object to the stat rows, which is why the measurements below are the same measurements:
 * filled, 16 radius, 28 top and bottom, 32 either side, 12 apart. The fill and the hover
 * ring come from the card rules at the end of this file, so they are not repeated here.
 *
 * The rules between them are gone with the change: two filled cards 12 apart already
 * separate themselves, and a rule as well is the divider drawn twice.
 */
/*
 * Six hundred, centred, and the type still starts at the left.
 *
 * These rows ran the full width of the container, which put an emoji and three words at one
 * end of a 1496px band and a button at the other, with a metre of nothing between them. A
 * row that wide stops reading as a row and becomes two unrelated things at opposite edges of
 * the screen.
 *
 * Centred as a column, not centred as text: the block sits in the middle of the page and
 * every line inside it still starts on the same left edge, which is the only arrangement
 * that stays readable when one of these labels is longer than the others.
 */
.connect-rows {
  max-width: 600px;
  margin-inline: auto;
  text-align: left;
}

.connect-rows {
  display: flex;
  flex-direction: column;
  gap: var(--card-gap, 16px);
}

.connect-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 28px 32px;
  border-radius: var(--radius-s);
  /*
   * The button's transition, not its own.
   *
   * The row moved on `box-shadow 0.3s ease` while the pill inside it moved on
   * `0.25s ease` across colour and background. Two things responding to the same pointer
   * at two different speeds reads as one of them lagging, which is exactly what it was.
   * Same duration, same curve, same properties — they arrive together now.
   */
  transition: border-color 0.25s ease, background-color 0.25s ease, color 0.25s ease,
    box-shadow 0.25s ease;
}

/*
 * THE WHOLE ROW IS THE LINK.
 *
 * The row already lit up on hover — the stroke appears when anything in it is hovered —
 * and then only the pill at the far end could be clicked. A card that visibly responds to
 * the cursor and does nothing when clicked is worse than one that never responded: the
 * hover made a promise the row did not keep.
 *
 * This is the stretched-link pattern rather than wrapping the row in an anchor. There is
 * still exactly ONE link in the markup, so a screen reader announces one destination
 * rather than two, and the heading beside it stays a heading instead of becoming link
 * text. The pill grows an invisible layer over the whole row; clicks anywhere land on it.
 *
 * `z-index` is left alone deliberately — the layer sits above the row's background and
 * below nothing else, because nothing else in the row is interactive.
 */
.connect-row .flx-services__btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* And the cursor says so across the whole row, not just over the pill. */
.connect-row:has(a) {
  cursor: pointer;
}

/* The emoji and the heading travel together, 20 apart, and the button takes the rest. */
.connect-row__label {
  display: flex;
  align-items: center;
  gap: 20px;
  min-width: 0;
}

/*
 * 22px, which is the h5 step — the size the frame sets it at, not the heading's size.
 * `line-height: 1` would clip the taller glyphs; 1.5 matches the heading beside it so the
 * two sit on the same centre line whatever the row's height turns out to be.
 */
.connect-row__emoji {
  flex: 0 0 auto;
  font-size: 22px;
  line-height: 1.5;
}

/* h3, at the scale — it was its own clamp topping out at 40, and the frame sets 32. */
.connect-row__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

/* --- Testimonials carousel ----------------------------------------------- */

/*
 * The cut is on the section, not the container — the same correction the work row needed.
 *
 * `.grid-container` carries 32px of padding, and `overflow` clips at the padding box, so
 * clipping there stopped the row 32px past the container instead of at the window: a card
 * sliced off with the rest of the gutter left empty beside it. The section is full width,
 * so cutting there is cutting at the glass, which is what bleeding off means.
 */
.flx-quotes {
  overflow-x: clip;
}

.flx-quotes .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
  /* Deliberately not clipped here. See above. */
}

.flx-quotes .eyebrow {
  margin-bottom: 0;
}

.flx-quotes__title {
  margin: 0;
}

/*
 * The row itself is a scroller. See DragRow.tsx — dragging is added on top of scrolling
 * rather than instead of it, so a trackpad, a thumb and the Tab key all work already.
 */
.drag-row {
  display: flex;
  align-items: stretch;
  gap: 48px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /*
   * No snapping.
   *
   * The reference has the machinery for it and switches it off on any device with a real
   * pointer — a row of cards wider than the screen is a thing you push, and snapping
   * overrules where you put it. Momentum is what replaces it; see DragRow.tsx.
   */
  /*
   * Out to both edges of the screen.
   *
   * `100%` is the container's content width, so half the difference to the window is this
   * side's gutter. The negative margin pulls the scroller out to the glass and the padding
   * puts the gutter back on the inside, so the first and last cards come to rest on the
   * margin rather than against the edge — but a card scrolled past stays visible all the
   * way out instead of being cut at the container.
   *
   * The right side was already doing this. The left was not, so pushing the row along
   * sliced the outgoing card off 32px inside the window and it read as a bug rather than
   * as a row continuing past the edge.
   */
  margin-right: calc((100% - 100vw) / 2);
  padding-right: calc((100vw - 100%) / 2);
  margin-left: calc((100% - 100vw) / 2);
  padding-left: calc((100vw - 100%) / 2);
}

.drag-row::-webkit-scrollbar {
  display: none;
}

/* The cursor is the affordance, so it only appears when there is somewhere to go. */
.drag-row.is-draggable {
  cursor: grab;
}

.drag-row.is-dragging {
  cursor: grabbing;
  /* Nothing gets selected while dragging — a drag across type otherwise highlights it. */
  user-select: none;
}

/*
 * AND THE CARDS SAY IT TOO.
 *
 * `cursor` on the row was being overruled by the cards inside it. Every card is an `<a>`,
 * and a browser gives a link `cursor: pointer` of its own accord — a value set on the
 * child, which beats anything the parent says. So the grab hand only ever appeared in the
 * gaps BETWEEN cards, and over the cards themselves — which is the whole row, visually —
 * the pointer said "click me" and nothing said "drag me".
 *
 * The dragging worked the entire time. It was unfindable, which for an affordance is the
 * same as broken: nobody drags a thing that looks like a button.
 *
 * `inherit` rather than restating grab/grabbing, so the cards follow the row through both
 * states without either value being written twice.
 *
 * Scoped to `.is-draggable`, which the script only adds when the row actually overflows.
 * A row whose cards all fit has nowhere to go, and its links go back to being links.
 */
.drag-row.is-draggable a,
.drag-row.is-draggable button,
.drag-row.is-draggable .work-card,
.drag-row.is-draggable .work-card * {
  cursor: inherit;
}

/*
 * And the browser's own picture-drag is turned off inside the row.
 *
 * Press on a photograph and a browser starts its native drag-and-drop — the ghost image
 * that follows the cursor, ready to be dropped into another window. That gesture and this
 * one begin identically, so the browser's wins on the element it started on and the row
 * stops following the pointer halfway through a swipe.
 *
 * It is also what the performance panel was complaining about on `img.work-card__img`:
 * the native drag competing with the pointer handlers on the same press.
 */
.drag-row img,
/*
 * And the cards themselves, which are anchors.
 *
 * The `dragstart` handler in useDragScroll already cancels the operation, and this says
 * the same thing to the engine that draws the ghost rather than to the event. Belt and
 * braces on the one bug that makes a carousel feel broken: a translucent copy of the
 * link, URL and all, trailing the pointer across the row.
 */
.drag-row a,
.drag-row a * {
  -webkit-user-drag: none;
  user-select: none;
}

.drag-row:focus-visible {
  outline: 1px solid var(--border-hover);
  outline-offset: 8px;
}

.quote-card {
  flex: 0 0 auto;
  /* Below the desktop frame the card keeps a peek of the next one, which is what tells you
     the row scrolls. That affordance matters more than alignment on a phone. */
  width: min(480px, 78vw);
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 56px;
  min-height: 200px;
}

.quote-card__text {
  margin: 0;
  padding: 0;
  border: 0;
  /* h6 in the frame. */
  font-size: var(--fs-h6);
  line-height: 1.5;
  /* The quote itself is ink/100; only the attribution under it is dimmed. */
  color: var(--color-natural-100, #fff);
  /* The quote mark rides at the end of the first line in the frame, level with its cap
     height — an inline element after the text, floated to the right of it. */
  position: relative;
}

.quote-card__mark {
  float: right;
  margin-left: 12px;
  font-family: 'Swiza', sans-serif;
  font-size: 24px;
  /* Sits level with the cap height of the first line rather than on its baseline. */
  line-height: 1;
}

/* The name and the role stack; that job belongs to `__who` now, because the row they sit
   in also holds the arrow. Left here as a comment rather than a rule so the next person
   looking for where the attribution is laid out finds the answer. */
.quote-card__role {
  color: var(--color-grey);
}

/* --- Sticky CTA ----------------------------------------------------------
   Figma "Banner" (3595:54153).

   Comes up from the bottom of the window half way down the page and can be dismissed for
   the visit. The behaviour is in StickyCta.tsx; this is the bar.

   Only `transform` animates, so it is a compositor job and the page under it is never
   relaid out.                                                                          */

.sticky-cta {
  position: fixed;
  inset-inline: 0;
  z-index: 4000;
  /*
   * The accent, with the original green as the fallback.
   *
   * The variable is set by StickyCta.tsx once it has read the visitor's stored colour,
   * and deliberately not before — so the first paint, and any browser where the script
   * does not run, gets the green this bar has always been rather than a bar with no
   * background at all. Every colour it can be sits at 10:1 or better against the black
   * type; see ACCENTS in that file for the measurements.
   */
  background: var(--cta-accent, #36ffae);
  color: #000;
  padding: 28px 0;
  /*
   * Two movements, two properties, so neither eases the other.
   *
   * `--cta-lift` is how far the footer has come up past the bottom of the window — set
   * on every scroll frame by StickyCta.tsx, 0 until the footer appears. It rides on
   * `bottom`, deliberately untransitioned: eased, the bar would lag a frame behind the
   * footer edge it is supposed to be sitting on.
   *
   * `transform` is the only thing that eases, and it does one job — up when it arrives,
   * down when it is dismissed. It clears its own height *plus* the lift, so a bar that
   * is parked 400px up the footer still goes all the way off the bottom when closed
   * rather than dropping to 400px and stopping there in plain sight.
   */
  --cta-lift: 0px;
  bottom: var(--cta-lift);
  transform: translateY(calc(100% + var(--cta-lift)));
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.sticky-cta.is-up {
  transform: translateY(0);
}

/*
 * Text, then everything else against the right-hand margin, 8px apart.
 *
 * `space-between` used to spread three children — text, button, dismiss — which put the
 * dismiss on the margin and the button somewhere in the middle. The gap is 8 and the
 * actions take the free space instead, so the button and the dismiss stay together.
 */
.sticky-cta__inner {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sticky-cta__text {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

/*
 * The one heading on the site set in black on a bright ground, and it needs a hair more
 * weight than the rest to look like the rest.
 *
 * It is already the same face at the same weight as every other heading — Swiza 400,
 * checked on the live page rather than assumed. What is different is the ground. White
 * type on black bleeds outward: the light spreads into the dark and the strokes read
 * thicker than they are drawn. Black type on a bright accent does the opposite — the
 * ground eats into the letterform and the same weight reads thinner. Set them side by
 * side and it is obvious, which is exactly how this was noticed.
 *
 * There is no Medium face to reach for; Swiza ships Light and Regular here and nothing
 * between. Asking for 500 would make the browser SYNTHESISE a bold — it smears the
 * outline sideways and the result is uneven at this size, which is worse than the problem.
 *
 * A tenth of a percent of stroke on the outline is the smallest honest lever: it thickens
 * the drawn edge itself, it is centred on the outline so the metrics do not move and
 * nothing reflows, and at 40px it is half a pixel. Rendered against 0.018em to check it
 * was not going too far — that one starts to read as bold, this one reads as the same
 * weight as the headings above it.
 */
.sticky-cta__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  -webkit-text-stroke: 0.01em currentColor;
  font-size: clamp(22px, 22px + 18 * (100vw - 1024px) / 416, 40px);
  line-height: 1.3;
  letter-spacing: -0.01em;
}

.sticky-cta__sub {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

.sticky-cta__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  margin-left: auto;
}

.sticky-cta__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 8px 20px;
  border-radius: 99999px;
  background: #000;
  color: #fff;
  font-size: 14px;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.25s ease;
}

/* The label and arrow take the banner's own accent, so the button answers whichever
   colour the session dealt — every accent is light enough to read on the black pill.
   The old hover was a 0.85 opacity fade, which on a bright banner read as nothing. */
.sticky-cta__btn:hover,
.sticky-cta__btn:focus-visible {
  color: var(--cta-accent, #36ffae);
}

/* The arrow leans out on hover — the same gesture every other button on the site makes. */
.sticky-cta__btn svg {
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

.sticky-cta__btn:hover svg,
.sticky-cta__btn:focus-visible svg {
  transform: translateX(3px);
}

.sticky-cta__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  /* No stroke — the mark alone. */
  border: 0;
  border-radius: 50%;
  background: none;
  color: #000;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

/* Foundation styles bare `button` with a white ground, and `button:hover` beats one
   class — without this the dismiss turns into a white disc on the green. */
.sticky-cta__close:hover,
.sticky-cta__close:focus,
.sticky-cta__close:active {
  background: rgba(0, 0, 0, 0.08);
  color: #000;
}

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

@media only screen and (width <= 767px) {
  .sticky-cta {
    padding: 16px 0;
  }

  .sticky-cta__inner {
    gap: 16px;
  }

  .sticky-cta__sub {
    display: none;
  }
}

/* --- The three two-column sections and the quotes, in one column ---------- */

@media only screen and (width <= 899px) {
  .flx-stats .grid-container,
  .flx-people .grid-container,
  .flx-connect .grid-container,
  .flx-quotes .grid-container {
    gap: 48px;
  }

  .col-right,
  /* Two classes beat one: without naming the card-row here its 58.33% desktop
     column held on phones and the copy wrapped at half the screen. */
  .flx-card-row .col-right {
    width: 100%;
    margin-left: 0;
  }

  .flx-people__head {
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
  }

  .flx-people__title {
    max-width: none;
  }

  .stat-row,
  .connect-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }

  /* Still a card, one step in from the desktop 28/32. */
  .connect-row {
    padding: 24px;
  }

  .stat-row__meta {
    text-align: left;
  }

  .drag-row {
    gap: 24px;
  }

  .quote-card {
    gap: 32px;
  }
}

/* --- Cycling media --------------------------------------------------------
   Every image is in the page, stacked in the same box, and only the opacity moves. No
   layout is redone on a change and nothing has to load part way through — see
   MediaCycle.tsx.

   The box these sit in must be positioned — they are absolute, and an unpositioned parent
   sends them somewhere else on the page entirely.                                      */

.media-cycle__item {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.6s ease;
}

.media-cycle__item.is-on {
  opacity: 1;
}

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

.flx-chips-card__text {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--color-natural-70);
  max-width: 46ch;
}

/* --- The statement, brightening a word at a time --------------------------
   See WordScrub.tsx. The container carries one custom property that the scroll writes —
   `--p`, 0 to 1 — and every word works out its own opacity from that and its index, so
   there is a single style write per frame however long the statement is.

       --w  =  progress × (words + spread)  −  this word's index
       opacity: clamp(dim, --w, 1)

   Before the wave reaches a word, --w is negative and the clamp holds it at --dim; as the
   wave passes, --w runs 0 → 1 and the clamp lets it through; after, it is above 1 and the
   clamp holds it at white. No keyframes, no per-word writes, no library.                */

.flx-statement__text.is-scrubbed .ws__w {
  /* Inline so words still wrap and justify as text, and so a `<br>` still breaks. */
  display: inline-block;
  opacity: clamp(var(--dim, 0.3), calc(var(--p, 0) * (var(--n, 1) + var(--spread, 6)) - var(--i, 0)), 1);
  /* Takes the edge off the step between two hundredths of scroll without smearing the
     wave — a tenth of a second is under two frames of a normal scroll. */
  transition: opacity 0.1s linear;
  will-change: opacity;
}

/*
 * Untouched without the script, and untouched for anyone who has asked for less movement:
 * WordScrub does not wrap anything in either case, so there is no dim state to be stuck
 * in. The statement is readable before this file runs and stays that way if it never does.
 */
@media (prefers-reduced-motion: reduce) {
  .flx-statement__text.is-scrubbed .ws__w {
    opacity: 1;
    transition: none;
  }
}

/* --- New site animations --------------------------------------------------
   Every rule is under `.new-anim`, which NewAnimations.tsx puts
   on <html> only when the toggle is on, the window is wide enough and nobody has asked
   for less movement. Untick the toggle and none of this applies.

   Their eases, as CSS:
     power1.out  cubic-bezier(0.25, 0.46, 0.45, 0.94)
     power3.out  cubic-bezier(0.215, 0.61, 0.355, 1)                                    */

/* Their FadeInUp: 2.5rem and a long 1.5s. */
.new-anim .na-rise {
  opacity: 0;
  transform: translate3d(0, 2.5rem, 0);
  transition:
    opacity 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    transform 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  will-change: opacity, transform;
}

.new-anim .na-rise.na-in {
  opacity: 1;
  transform: none;
}

/*
 * Their AnimatedGrid: a shorter rise, 1.5rem, and the children go one after another in
 * DOM order — which on every row on this site is left to right.
 */
.new-anim .na-stagger > * {
  opacity: 0;
  transform: translate3d(0, 1.5rem, 0);
  transition:
    opacity 1s cubic-bezier(0.25, 0.46, 0.45, 0.94),
    transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  transition-delay: var(--na-delay, 0ms);
  will-change: opacity, transform;
}

.new-anim .na-stagger.na-in > * {
  opacity: 1;
  transform: none;
}

/*
 * The work row scrolls itself, so its cards fade rather than translate — a transform on
 * a card inside a scroller fights the scroller.
 */
.new-anim .work-row__track.na-stagger > * {
  transform: none;
}

/*
 * Releasing the layer is right. Releasing it HERE was not.
 *
 * `.na-in` is added at the instant the animation starts, so this dropped the compositing
 * hint on the first frame of a 1.5-second transition — exactly when it was needed. A big
 * photograph then faded and travelled 2.5rem with no layer of its own, repainting every
 * frame, which reads as flashing while it moves and as the section lagging behind the
 * scroll rather than arriving with it. Most visible on the full-width image sections,
 * because they are the largest thing on the page to repaint.
 *
 * The hint is now dropped when the transition actually ENDS, from NewAnimations.tsx —
 * `transitionend` is the only accurate signal for that, and CSS has no way to express it.
 * The card-per-row concern the old rule was written for still holds; it just needs to
 * happen a second and a half later than it did.
 */

@media (prefers-reduced-motion: reduce) {
  .new-anim .na-rise,
  .new-anim .na-stagger > * {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- The between-pages wipe -----------------------------------------------
   A cream panel that rises to cover the page, then carries on out of the top once the
   next one has arrived. See PageWipe.tsx — the cover is driven from the click, the exit
   from the route actually changing.

   `transform` only, so it is one composited layer and never touches layout.            */

.page-wipe {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: #f6f3e7;
  transform: translate3d(0, 100%, 0);
  will-change: transform;
}

/* Rising to cover. Quick off the mark, long settle — the page behind it is loading. */
.page-wipe--covering {
  transform: translate3d(0, 0, 0);
  transition: transform 0.52s cubic-bezier(0.77, 0, 0.175, 1);
}

/* And away, out of the top. */
.page-wipe--clearing {
  transform: translate3d(0, -100%, 0);
  transition: transform 0.62s cubic-bezier(0.77, 0, 0.175, 1);
}

/* At rest it is parked below the fold and does nothing at all. */
.page-wipe--idle {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .page-wipe {
    display: none;
  }
}

/* --- Running to the edge --------------------------------------------------
   The container holds the reading measure, not the pictures. Big blocks sit outside it
   and run to 20px of the window; only text columns are held in. Measured on a 1728
   screen, content used to start at 116px — 96 of that being the container hitting its
   1536 cap and centring, not the gutter, which is a lot of nothing down each side.

   So the gutter becomes a variable and two things stop being held in the container: the
   site chrome, and the picture bands. Type stays in it, because a line of body copy 1690
   pixels wide is not readable, whatever it looks like in a screenshot.

   `--edge` is the one number. Change it here and everything that touches the edge moves
   together.                                                                             */

:root {
  --edge: 20px;
}

@media only screen and (width <= 767px) {
  :root {
    --edge: 16px;
  }
}

/*
 * The header and footer were released from the cap here and have been put back: at 1728
 * the logo sat at 20 and the whole row read as too wide. They stay in the container with
 * everything else, and only the picture bands reach the edge.
 */

/*
 * A block inside the container that should reach the window instead.
 *
 * `100%` in a margin resolves against the containing block, which here is the container's
 * content width — so half the difference between that and the window, less the gutter we
 * want to keep, is exactly how far each side has to be pulled out.
 *
 * On a platform with overlay scrollbars (every Mac) `100vw` is the visible width and this
 * is exact. Where a scrollbar takes up space it is out by half of one, which is why the
 * sections these sit in clip.
 */
.bleed-edge,
.pic-band {
  margin-inline: calc((100% - 100vw) / 2 + var(--edge));
}

.flx-m30,
.flx-people {
  overflow-x: clip;
}

/* --- Masthead 2 -----------------------------------------------------------
   Figma "Services — Masthead 2" (3608:48732). Measured, not approximated: 128 above and
   208 below, the copy capped at 795, 40 down to the pills, 16 inside the heading group,
   and the pills a fixed 36 high with 8 between them.                                   */

.m2__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 40px;
  /* The claim breaks where it was drawn to break rather than running the width of a
     1496 container. */
  max-width: 795px;
}

.m2__head {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

/*
 * No size here, deliberately.
 *
 * This heading is rendered by the Title component, which puts the CMS's "Title Tag Style"
 * on it as a class — `title-huge`, `h1`, `h2`. A rule like this one is two classes deep
 * and outranks that single class, so whatever an editor picked was drawn over and the
 * setting looked broken. It was: they set Display on the contact heading and nothing
 * moved.
 *
 * So the section rule says everything except the size, and the size comes from the CMS.
 * The default is set where defaults belong — on the block, and in the seed that fills it.
 */
.m2__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
}

/* White. It sits inside an ink/100 group in the frame and nothing overrides it — this is
   the opening line of the page, not a caption under a heading. */
.m2__text {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-100, #fff);
}

.m2__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/*
 * These are buttons.
 *
 * They were a pill shape with one fixed outline, which is why picking a style did nothing
 * — there was nothing to pick. Same measurements as every other small button on the site,
 * and the three shared `btn-*` modifiers at the end of this file do the colouring.
 */
.m2__pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 8px 16px;
  border: 1px solid var(--border-default);
  border-radius: 99999px;
  font-size: 14px;
  line-height: 1;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color 0.3s ease, background-color 0.3s ease;
}

.m2__pill .ph {
  flex: 0 0 auto;
}

a.m2__pill:hover,
a.m2__pill:focus-visible {
  border-color: var(--border-hover);
  color: inherit;
}

/*
 * The pills. 3608:50597.
 *
 * An icon and a fact, three across, and they are cards — filled at the card surface, 4
 * radius, 20 of padding and 20 between the icon and the line. The frame outlines them;
 * every other card on this site is filled, so these are too, and they get the card system's
 * fill by joining that list at the end of the file rather than declaring it again here.
 *
 * They wrap rather than shrink: three facts squeezed onto one line on a laptop is three
 * things nobody reads.
 */
.m2__notes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--card-gap, 16px);
  width: 100%;
  /* 64 under the copy block, which is what the frame puts between `content` and `Cards`. */
  margin-top: 64px;
}

.m2__note {
  flex: 1 1 220px;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 20px;
  border-radius: var(--radius-s);
}

.m2__note .ph {
  color: var(--color-natural-100, #fff);
}

.m2__note-text {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.14px;
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
}

@media only screen and (width <= 899px) {
  .m2__note {
    flex: 1 1 100%;
  }
}

@media only screen and (width <= 899px) {
  .m2__copy {
    gap: 32px;
  }
}

/* --- Masthead 30 ----------------------------------------------------------
   Figma "Services — Masthead 30" (3608:51967). The opening of a single-service page.  */

.flx-m30 .grid-container {
  display: flex;
  flex-direction: column;
  /* 96 between the copy and the picture band — spacing/6xl. */
  gap: 96px;
}

/*
 * The eyebrow is a word with a line under it, not a word with a line beside it.
 *
 * Every other section on the site draws the theme's eyebrow: the label, then a hairline
 * running to the right-hand margin. The single-service header (3608:51855) hides that
 * divider and underlines the label instead — 8px either side of the word, so the rule is
 * a little wider than the letters rather than stopping dead against them.
 *
 * The underline is a border on the padded box for exactly that reason. `text-decoration`
 * would hug the word and lose the 8.
 */
.flx-m30 .eyebrow__divider,
.flx-case-text .eyebrow__divider {
  display: none;
}

/*
 * The eyebrow on a single service page. 3608:51855.
 *
 * It was underlined. The redraw drops the underline and puts Phosphor's
 * ArrowElbowDownRight in front of the word instead — the ↳ that says this page sits under
 * something, which is exactly what a single service page does.
 *
 * The glyph is a mask rather than an <img>, so it takes its colour from the text and the
 * two can never disagree. That also keeps it out of the markup: `Eyebrow` is shared by
 * forty blocks and none of the others want an arrow.
 *
 * 12 of indent on the row, 8 between the arrow and the word, both from the frame.
 */
.flx-m30 .eyebrow,
.flx-case-text .eyebrow {
  padding-left: 12px;
  gap: 8px;
}

.flx-m30 .eyebrow__title,
.flx-case-text .eyebrow__title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border-bottom: 0;
  /*
   * No colour set. The frame says ink/80 and the theme's eyebrow token is ink/70 — a
   * tenth of an alpha, against every other eyebrow on the site matching each other. The
   * consistency is worth more than the tenth, so this one inherits like the rest and the
   * arrow, drawn in currentColor, follows it.
   */
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.14px;
}

.flx-m30 .eyebrow__title::before,
.flx-case-text .eyebrow__title::before {
  content: '';
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M229.66%2C157.66l-48%2C48a8%2C8%2C0%2C0%2C1-11.32-11.32L204.69%2C160H80a8%2C8%2C0%2C0%2C1-8-8V48a8%2C8%2C0%2C0%2C1%2C16%2C0v96H204.69l-34.35-34.34a8%2C8%2C0%2C0%2C1%2C11.32-11.32l48%2C48A8%2C8%2C0%2C0%2C1%2C229.66%2C157.66Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M229.66%2C157.66l-48%2C48a8%2C8%2C0%2C0%2C1-11.32-11.32L204.69%2C160H80a8%2C8%2C0%2C0%2C1-8-8V48a8%2C8%2C0%2C0%2C1%2C16%2C0v96H204.69l-34.35-34.34a8%2C8%2C0%2C0%2C1%2C11.32-11.32l48%2C48A8%2C8%2C0%2C0%2C1%2C229.66%2C157.66Z%22%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;
}

/* The heading group carries its own 16, so the theme's 56 under every eyebrow would pay
   twice. */
.flx-m30 .eyebrow {
  margin-bottom: 0;
}

/*
 * The measure, widened from the frame's 795.
 *
 * 795 was drawn around a shorter headline. At the display step — which is 88 now, not the
 * 80 the frame used — the longest of the four service headlines still took three lines at
 * 56.66%, and a three-line page header reads as a paragraph rather than a statement. 72%
 * is what actually lands them all on two.
 *
 * A percentage rather than a fixed number, so it keeps its proportion as the container
 * grows instead of being right at one width and wrong above it. The 1100 ceiling stops
 * the line getting too long to read at the top of the ladder.
 */
.m30__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 40 between the words and the buttons — spacing/xxl. */
  gap: 40px;
/*
 * Snapped up to the next whole column.
 *
 * The measure was picked by eye and lands between two column lines, which is the one place
 * a text block can sit where nothing on the page agrees with it. Rounding UP rather than
 * down keeps the line length the design chose rather than shortening it, and the block now
 * ends on a line every other section also ends on.
 */
  max-width: calc((100% - 11 * 48px) / 12 * 9 + 8 * 48px);
}

@media only screen and (width <= 1023px) {
  .m30__copy {
    max-width: none;
  }
}

/* Eyebrow, line, subtitle: 16 apart, which is spacing/r. */
.m30__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  width: 100%;
}

/* Size from the CMS — see the note on `.m2__title`. */
.m30__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
}

/* White. The frame has this at ink/100; it was inheriting the body's ink/70. */
/* ink/70. It was ink/100, which put the supporting line at the same weight as the
   headline above it and left nothing to tell them apart but size. */
.m30__text {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-70);
}

.m30__btns {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.m30__btn,
.ovw__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding: 8px 16px;
  border: 1px solid var(--border-default);
  border-radius: 99999px;
  color: inherit;
  text-decoration: none;
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  transition: border-color 0.3s ease, background-color 0.3s ease, color 0.3s ease;
}

.m30__btn:hover,
.m30__btn:focus-visible,
.ovw__btn:hover,
.ovw__btn:focus-visible {
  border-color: var(--border-hover);
  color: inherit;
}

/*
 * The first button is the frame's primary: a solid fill against an outline.
 *
 * The frame is drawn on white, so its primary is a near-black fill with white type. This
 * site is the other way up, so the fill is the page's foreground and the type is its
 * ground — the same contrast, inverted, which is what "primary" means here.
 */
.m30__btn--primary {
  background: var(--color-natural-100, #fff);
  border-color: var(--color-natural-100, #fff);
  color: var(--bg-color, #000);
}

.m30__btn--primary:hover,
.m30__btn--primary:focus-visible {
  background: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  border-color: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  color: var(--bg-color, #000);
}

.m30__btn svg,
.ovw__btn svg {
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.m30__btn:hover svg {
  transform: translateX(3px);
}

/* The overview's arrow points up and to the right, so it leaves that way rather than
   sideways. */
.ovw__btn:hover svg,
.ovw__btn:focus-visible svg {
  transform: translate(2px, -2px);
}

/*
 * The picture band, shared by Masthead 30 and People.
 *
 * 596 · 286 · 286 with 24 between them — 1216 on the frame, held as proportions so the
 * band keeps its rhythm on every rung of the container ladder.
 */
.pic-band {
  display: flex;
  align-items: stretch;
  /* The card gap, like every other row that spans the width to fill it. It was 24, which is
     not a number this site uses for anything else — the photographs sat looser than the
     cards above and below them for no reason anybody chose. */
  gap: var(--card-gap, 16px);
  height: clamp(320px, 39.7vw, 600px);
}

.pic-band__cell {
  position: relative;
  overflow: clip;
  border: 0;
  border-radius: var(--radius-s);
  background: rgba(255, 255, 255, 0.04);
}

.pic-band__cell--wide {
  flex: 596 1 0;
}

.pic-band__cell--tall {
  flex: 286 1 0;
}

.pic-band__stack {
  flex: 286 1 0;
  display: flex;
  flex-direction: column;
  /* The same gap stacked as beside, so the two photographs in this column sit exactly as
     far apart as they do from the one next to them. */
  gap: var(--card-gap, 16px);
}

.pic-band__stack .pic-band__cell {
  flex: 1 1 0;
}

.pic-band__cell img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media only screen and (width <= 899px) {
  .flx-m30 .grid-container {
    gap: 48px;
  }

  /* The band becomes the wide one over the pair; the tall one goes, because a fourth
     picture at phone width is a scroll, not a composition. */
  .pic-band {
    flex-direction: column;
    height: auto;
  }

  .pic-band__cell--wide {
    aspect-ratio: 596 / 600;
  }

  .pic-band__cell--tall {
    display: none;
  }

  .pic-band__stack {
    flex-direction: row;
  }

  .pic-band__stack .pic-band__cell {
    aspect-ratio: 1;
  }
}

/* --- FAQs -----------------------------------------------------------------
   Figma "Services — FAQs" (3608:55400). Heading left, questions right, five columns of
   twelve against seven. Built on <details>, so the browser owns the behaviour.        */

.flx-faqs .grid-container {
  display: flex;
  flex-direction: column;
  gap: 64px;
}

.flx-faqs .eyebrow {
  margin-bottom: 0;
}

.faqs__inner {
  display: flex;
  align-items: flex-start;
  gap: 48px;
}

.faqs__head {
  /* Five columns, not five parts of the leftover space. `flex: 5` and `flex: 7` split what
     is left AFTER the gap, so the boundary landed 8px off the line at every width. Stated
     as a column measure the head is exactly five and the list takes exactly seven. */
  flex: 0 0 calc((100% - 11 * 48px) / 12 * 5 + 4 * 48px);
  /* The heading stays with the questions on a long list rather than scrolling away from
     them at the top. */
  position: sticky;
  top: 120px;
}

.faqs__title {
  margin: 0;
}

.faqs__list {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.faq {
  border-top: 1px solid var(--border-default);
}

.faq:last-child {
  border-bottom: 1px solid var(--border-default);
}

/* The heading is the row; the button fills it. Both are stripped back to nothing so the
   question looks exactly as it did when it was a <summary>. */
.faq__row {
  margin: 0;
  font: inherit;
}

/*
 * The theme paints every <button> on hover and focus — `button:hover, button:focus {
 * color: var(--color-main); background-color: var(--btn-bg-hover) }`. This one is not a
 * button in the "thing you press that looks like a pill" sense, it is a row of text, so
 * every state is restated as nothing. Without this the question turns into a white bar
 * the moment it is clicked.
 */
.faq__q,
.faq__q:hover,
.faq__q:focus,
.faq__q:focus-visible,
.faq__q:active {
  background: none;
  background-color: transparent;
  color: inherit;
}

/* Taken away above, so put a real one back — this is the only thing that shows a keyboard
   user where they are. */
.faq__q:focus-visible {
  outline: 2px solid var(--color-natural-40);
  outline-offset: -2px;
  border-radius: var(--radius-s);
}

.faq__q {
  appearance: none;
  width: 100%;
  margin: 0;
  border: 0;
  text-align: left;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  padding: 24px 0;
  cursor: pointer;
  font-family: 'Swiza', sans-serif;
  /* h4 in the frame. */
  font-size: var(--fs-h4);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

.faq__caret {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 30px;
  color: var(--color-natural-70);
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.faq.is-open .faq__caret {
  transform: rotate(180deg);
}

/*
 * The open and close.
 *
 * One grid row going from 0fr to 1fr. That transitions to whatever height the answer
 * actually is, with nothing measured in JavaScript — so it stays right when the copy
 * changes, when the column narrows, and when the text reflows to a different number of
 * lines. `min-height: 0` on the inner is what lets the row shrink below its content;
 * without it the grid refuses and nothing moves.
 */
.faq__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.faq.is-open .faq__panel {
  grid-template-rows: 1fr;
}

.faq__panel-inner {
  min-height: 0;
  overflow: hidden;
  /* The answer fades slightly behind the height, so it arrives rather than being revealed
     a line at a time by the edge of the box. */
  opacity: 0;
  transition: opacity 0.25s ease;
}

.faq.is-open .faq__panel-inner {
  opacity: 1;
  transition: opacity 0.3s ease 0.12s;
}

/*
 * The answer runs the width of the question above it.
 *
 * It was capped at 62ch, which is a good measure for a column of prose and the wrong one
 * here: the question is a full-width line across the list, so an answer stopping two
 * thirds of the way along reads as a column that has gone wrong rather than as careful
 * typesetting. A question and its answer are one block; they should share an edge.
 *
 * The 48 on the right stays. That is not measure, it is the caret's gutter — without it a
 * long line runs under the chevron.
 */
.faq__a {
  margin: 0;
  padding: 0 48px 24px 0;
  color: var(--color-natural-70);
}

/* Asked for less movement: it still opens and closes, it just does it at once. */
@media (prefers-reduced-motion: reduce) {
  .faq__panel,
  .faq__panel-inner,
  .faq__caret {
    transition: none;
  }
}

@media only screen and (width <= 899px) {
  .flx-faqs .grid-container {
    gap: 48px;
  }

  .faqs__inner {
    flex-direction: column;
    gap: 32px;
  }

  .faqs__head {
    position: static;
  }

  .faq__q {
    font-size: 18px;
  }

  .faq__a {
    padding-right: 0;
  }
}

/* --- Process carousel -----------------------------------------------------
   Figma "Services — Process Carousel" (3608:48738). A DragRow with two arrows over it;
   see ProcessRow.tsx. The cut is on the section, at the window — the same correction the
   work row and the quotes row needed, for the same reason.                             */

.flx-process {
  overflow-x: clip;
}

.flx-process .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
}

/* 600 x 336 on the 1512 frame; 48 of padding, not 40. */

.flx-process .eyebrow {
  margin-bottom: 0;
}

.flx-process__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 48px;
}

.flx-process__title {
  margin: 0;
}

/* Figma component 2021:25211 — 36px circles, 12 apart, bottom-aligned. */
.row-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 0 0 auto;
}

.row-nav__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.3s ease, opacity 0.3s ease;
}

/* Foundation styles a bare `button` white, and one class does not beat `button:hover`.
   These three exist to cancel that; they deliberately do NOT brighten the ring, because
   a click leaves focus behind and a button that keeps its hover look after the pointer
   has gone reads as stuck. */
.row-nav__btn:hover,
.row-nav__btn:focus,
.row-nav__btn:active {
  background: none;
  color: inherit;
  border-color: var(--border-default);
}

/* The brighter ring: the pointer actually over it, the press itself, or keyboard focus.
   Mouse focus left behind by a click gets the resting ring above. */
.row-nav__btn:hover,
.row-nav__btn:active,
.row-nav__btn:focus-visible {
  border-color: var(--border-hover);
}

/*
 * Disabled arrows: the same outline control, drawn darker. Not a grey fill — the
 * site-wide disabled state is a solid pill made for buttons with labels, and on a
 * bare outline circle it read as a pressed button rather than an absent one. And not
 * an opacity fade, for the same reason the submit buttons abandoned one: a ghost of
 * a control looks like a rendering fault. A dimmer ring and a dimmer arrow say
 * "nothing further this way" and still survive being looked at.
 */
/* Every pseudo-state spelled out, because Foundation has `button[disabled]:hover`
   filling the control white at (0,2,1) — one class plus [disabled] loses to it, and a
   dead button lit up like a hover is exactly what it looked like. Disabled means
   nothing happens, including to the paint. */
.row-nav__btn[disabled],
.row-nav__btn[disabled]:hover,
.row-nav__btn[disabled]:focus,
.row-nav__btn[disabled]:active,
.swiper-nav .swiper-button[aria-disabled='true'],
.swiper-nav .swiper-button[aria-disabled='true']:hover,
.swiper-nav .swiper-button[aria-disabled='true']:focus {
  opacity: 1;
  background: none;
  border-color: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.28);
  cursor: default;
}

/* The stages themselves. 598 x 340 on the 1512 frame, held as a vw below it so two and a
   bit are on screen at every width rather than one enormous card on a laptop. */
.process-row {
  gap: 24px;
}

.stage-card {
  flex: 0 0 auto;
  width: clamp(280px, 39.68vw, 600px);
  min-height: 336px;
  display: flex;
  flex-direction: column;
  /* The name at the top and the writing at the bottom, so the descriptions line up across
     the row whatever length any one of them runs to. */
  justify-content: space-between;
  gap: 48px;
  padding: 48px;
  border: 1px solid var(--color-natural-40);
  border-radius: var(--radius-s);
}

.stage-card__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 2px 0;
}

/*
 * 14px. 3608:55012.
 *
 * White and in the heading face, sharing a baseline box with the name beside it, so the
 * two read as one line — "01 Discovery". It was 18; the redraw takes it down to the small
 * step, which is what stops it competing with the name it is numbering.
 */
.stage-card__n {
  flex: 0 0 28px;
  width: 28px;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.14px;
  color: var(--color-natural-100, #fff);
}

/*
 * h3 — 32 at full size. 3608:49490.
 *
 * It was h2, which the frame did set when this card was drawn. The card has since been
 * redrawn a step down: 32 at 1.5, not 38 at 1.3. One rule, so every process card on every
 * page moves together.
 */
.stage-card__title {
  margin: 0;
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

/* The mark in the card's top-right corner is gone with the redraw — 3608:55012. */

.stage-card__text {
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.6;
  color: var(--color-natural-70);
}

@media only screen and (width <= 899px) {
  .flx-process .grid-container {
    gap: 48px;
  }

  .flx-process__head {
    align-items: flex-start;
    gap: 24px;
  }

  /* The arrows go on a phone — the row is plainly a scroller there, and two 40px targets
     next to a heading is a lot of chrome for something a thumb already does. */
  .row-nav {
    display: none;
  }

  .stage-card {
    min-height: 0;
    gap: 32px;
    padding: 24px;
  }
}

/* --- Card row -------------------------------------------------------------
   Figma "Services — Masthead 1" (3608:50216). The claim indented on the right, the
   reasons for it across the page underneath. See CardRowSection in blocks.tsx.        */

.flx-card-row .grid-container {
  display: flex;
  flex-direction: column;
  /* 64 between the eyebrow, the heading column and the row of cards — spacing/4xl. */
  gap: 64px;
}

.flx-card-row .eyebrow {
  margin-bottom: 0;
}

/* This one left the offset grid column behind: the heading and its line sit at the
   left edge, over the cards, wherever the card row is used. The measure is capped so
   the supporting sentence doesn't run the full container at desktop widths. */
.flx-card-row .col-right {
  width: 100%;
  max-width: 58.33%;
  margin-left: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.flx-card-row .col-right p {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-70);
}

.flx-card-row__title {
  margin: 0;
}

/*
 * Three cards and two signs between them: 364 + 58 + 364 + 58 + 364 on the 1512 frame,
 * which is the 1216 container exactly. The cards take equal shares of whatever is left
 * once the signs have theirs, so the same row holds four without being redrawn.
 */
.card-row {
  display: flex;
  align-items: stretch;
  gap: 16px;
}

.row-card {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* The number sits at the top of the card and the writing at the bottom of it, however
     tall the tallest card in the row turns out to be. */
  justify-content: center;
  gap: 48px;
  padding: 36px 32px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
}

/*
 * White, not ink/70.
 *
 * The frame sets every card number at ink/100 — it is a label, not a footnote, and at
 * 14px in a dimmed grey it disappeared against the border.
 */
.row-card__n {
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-100, #fff);
}

/* h4 in the frame. */
.row-card__title {
  margin: 0 0 12px;
  font-size: var(--fs-h4);
  line-height: 1.5;
  letter-spacing: -0.01em;
}

.row-card__text {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-70);
}

.card-row__x {
  flex: 0 0 auto;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  color: var(--border-default);
  font-size: 22px;
  line-height: 1;
}

@media only screen and (width <= 899px) {
  .flx-card-row .grid-container {
    gap: 48px;
  }

  /* One under another, and the signs go — they are a way of reading three things across
     a page, and there is no across on a phone. */
  .card-row {
    flex-direction: column;
    gap: 16px;
  }

  .card-row__x {
    display: none;
  }

  .row-card {
    min-height: 0;
    gap: 32px;
    padding: 24px;
  }
}

/* --- The four sections, brought to their frames ---------------------------
   3588:42086 logos · 3595:52154 testimonials · 3595:54179 impact · 3594:62380 footer.
   All four draw the filled card above; the hover effects were additions and are gone.  */

/* Client logo boxes — filled, 16 radius, 28 padding, 12 apart. */
.flx-text-logos .logo-item {
  background: var(--surface-card);
  border: 0;
  border-radius: var(--radius-s);
  padding: 28px;
  transition: box-shadow 0.3s ease;
}

/*
 * A logo stops growing at the size it was drawn at.
 *
 * The imported theme sizes these against the viewport, so on a 4K screen every mark keeps
 * getting bigger with nothing to stop it. Most of them do not show it: a wide, short logo
 * runs out of card width long before it runs out of height, so `max-width` catches it and
 * the growth is invisible. A compact one has no such limit, which is why Upmind was twice
 * the size of the row it sits in — the same rule applied to every logo, and only the
 * squarest one able to obey it.
 *
 * A ceiling rather than a fixed height: these are supplied artwork at a dozen different
 * aspect ratios, and one height for all of them would make a wordmark tiny and a monogram
 * enormous. The mark scales with the page up to the width the design was drawn at and then
 * holds, which is what the container does too.
 */
.flx-text-logos .logo-item img {
  max-height: clamp(22px, 2.2vw, 34px);
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

.flx-text-logos__grid {
  gap: var(--card-gap, 16px);
}

.flx-text-logos .eyebrow {
  margin-bottom: 96px;
}

/* Testimonial cards — filled, 16 radius, 28 padding, a fixed 280 tall, 12 apart. The
   rules that used to sit between them go: two filled cards 12 apart separate themselves. */
.quote-card {
  background: var(--surface-card);
  border-radius: var(--radius-s);
  padding: 28px;
  height: 280px;
  min-height: 0;
  gap: 20px;
}

.flx-quotes .drag-row {
  gap: 12px;
}

/*
 * On the desktop frame the cards are four columns wide at the frame's own gutter.
 *
 * 480 and a 12 gap were drawn by eye, and neither is a column measure, so no card after the
 * first landed on a line. Four columns is 429.6 at 1440 and it holds at every width, which
 * is the point of expressing it this way rather than as another number.
 *
 * Only from 1024 up. Below that the card keeps its 78vw so a slice of the next one shows,
 * and that peek is what tells somebody the row scrolls at all.
 */
@media only screen and (width >= 1024px) {
  .flx-quotes .drag-row {
    gap: 48px;
  }

  .flx-quotes .quote-card {
    width: calc((100% - 11 * 48px) / 12 * 4 + 3 * 48px);
  }
}

/* Swiza, not the body face — the frame sets the quote in the heading family. */
.quote-card__text {
  font-family: 'Swiza', sans-serif;
}

.quote-card__name {
  color: var(--color-natural-100, #fff);
}

/*
 * ink/70, not `--color-grey`.
 *
 * That token resolves to #4d4d4d under `theme-dark` — a near-black grey, on a card that
 * is now also near-black. It was close to unreadable before the card was filled.
 */
.quote-card__role {
  color: var(--color-natural-70);
}

.quote-card__mark {
  margin-left: 20px;
}

/* Impact — the rows are filled cards, not rules. */
.stat-row {
  background: var(--surface-card);
  border-radius: var(--radius-s);
  /* 28 top and bottom, 32 either side. */
  padding: 28px 32px;
  gap: 24px;
}

/*
 * Two columns of three, 12 apart in both directions.
 *
 * `grid-auto-flow: column` with a stated number of rows is what fills downwards before
 * across — the frame reads one to three on the left and four to six on the right, and
 * the default row-first flow would deal them alternately instead.
 */
.stat-rows {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: repeat(var(--rows, 3), auto);
  grid-auto-flow: column;
  /* The card gap. These are two columns of cards filling the band, not two column measures
     sitting on lines — see the note on `--card-gap`. */
  gap: var(--card-gap, 16px);
}

.stat-row + .stat-row {
  margin-top: 0;
  padding-top: 28px;
  border-top: 0;
}

/* h1, not the display step — the frame sets the number at 44. */
.stat-row__value {
  font-size: var(--fs-h1);
  letter-spacing: -0.01em;
}

.stat-row__label {
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
}

/* Footer cards — filled, and the frame keeps these at the small radius rather than 16. */
.footer-card {
  background: var(--surface-card);
  border: 0;
  border-radius: var(--radius-s);
}

@media only screen and (width <= 899px) {
  .flx-quotes .drag-row {
    gap: 12px;
  }

  .quote-card {
    height: auto;
    min-height: 240px;
    padding: 24px;
  }

  /* One column on a phone. Two cards of a 44px number side by side is not a layout. */
  .stat-rows {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-auto-flow: row;
  }

  .stat-row,
  .flx-text-logos .logo-item {
    padding: 24px;
  }
}

/* --- Featured media -------------------------------------------------------
   Figma "Breakpoint=Featured Media" (2042:11826): 1216 x 700 — which is 1.74:1, near
   enough 16:9 that the frame was drawing one.

   So it is 16:9, the same ratio and therefore the same height as every picture in a
   gallery. A run of pictures down a case study reads as one band whether it is made of
   featured media or gallery rows, which is the whole point of picking a single ratio.

   It was a stated 700px, which held at the width the frame was drawn at and nowhere
   else: on a wider container the picture stayed 700 tall and grew only sideways, ending
   up at better than 2:1. The theme's own 64% is 25:16, a ratio nothing arrives in.      */

.flx-featured-media__image {
  height: auto;
  padding-bottom: 56.25%;
}

/* --- Work grid ------------------------------------------------------------
   Figma "Case Studies - Desktop" (3642:62573). Three across, filters over the top.     */

.flx-work-grid .grid-container {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.flx-work-grid .eyebrow {
  margin-bottom: 0;
}

/*
 * FILTERS ARE NOT BUTTONS, and they had stopped saying so.
 *
 * These were pills — bordered, 40 high, filled when chosen — which is exactly what every
 * button on the site looks like. So a row of them read as five things to press, when it
 * is really one control with five positions, and only one of them true at a time.
 *
 * They are closer to tabs than to buttons: one grid underneath, and these choose what it
 * shows. So they are set as text, and the chosen one is marked the way a tab is — it goes
 * to full white and takes a rule beneath it. Nothing is drawn around any of them.
 *
 * TWO STATES, TWO SIGNALS, deliberately not the same one twice. Hover brightens the
 * label. Chosen brightens it AND draws the rule. If hover also drew a rule, every filter
 * would look chosen the moment the cursor crossed it, which is the failure this pattern
 * usually has.
 *
 * The gap goes from 8 to 28. Eight was right between bordered pills, where the borders
 * did the separating; between bare words it reads as one broken sentence.
 */
.work-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  margin-bottom: 48px;
}

.work-filter {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The 40 stays even with the border gone — it is the touch target, not the pill. */
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.3s ease;
}

/*
 * The rule is always in the markup and always the full width of its label — it is scaled
 * to nothing until the filter is chosen. Animating a transform rather than a width keeps
 * it on the compositor, and means it grows from the left rather than out of the middle,
 * which is the direction the eye is already reading.
 */
.work-filter::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 6px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

/* Foundation styles a bare `button` white, and one class does not beat `button:hover` —
   so every interactive state still has to say otherwise, border or no border. */
.work-filter:hover,
.work-filter:focus,
.work-filter:active {
  background: none;
  border: 0;
  color: var(--color-natural-100, #fff);
}

.work-filter.is-on,
.work-filter.is-on:hover,
.work-filter.is-on:focus {
  background: none;
  border: 0;
  color: var(--color-natural-100, #fff);
}

.work-filter.is-on::after {
  transform: scaleX(1);
}

@media (prefers-reduced-motion: reduce) {
  .work-filter::after {
    transition: none;
  }
}

.work-filter:focus-visible {
  outline: 1px solid var(--border-hover);
  outline-offset: 4px;
}

/* Three columns, 24 between them and 40 down. */
.work-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: 24px;
  row-gap: 40px;
}

.work-tile {
  display: flex;
  flex-direction: column;
  gap: 24px;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.work-tile__frame {
  display: block;
  position: relative;
  /* 500 on the frame. Held as a ratio of the column so a wider container does not leave
     a 500px band of picture under a much wider card. */
  aspect-ratio: 389 / 500;
  overflow: clip;
  border: 0;
  border-radius: var(--radius-s);
  background: var(--surface-card);
}

/* Nothing on hover: the picture is the object and it has no box to light up now. */

.work-tile__frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s cubic-bezier(0.26, 1, 0.48, 1);
}

/* The frame clips it, so the card itself never moves and the grid stays on its rows. */
.work-tile:hover .work-tile__frame img {
  transform: scale(1.03);
}

.work-tile__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-bottom: 28px;
}

.work-tile__head {
  display: flex;
  align-items: center;
  gap: 12px;
}

.work-tile__title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h6);
  line-height: 1.5;
  color: var(--color-natural-100, #fff);
}

.work-tile__head svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.work-tile:hover .work-tile__head svg {
  transform: translate(2px, -2px);
}

.work-tile__text {
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-70);
}

@media only screen and (width <= 1023px) {
  .work-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media only screen and (width <= 639px) {
  .work-grid {
    grid-template-columns: 1fr;
    row-gap: 32px;
  }

  .work-filters {
    margin-bottom: 32px;
  }
}

/* --- Pixel field ---------------------------------------------------------
   The pixel field. Two placements, two switches — see PixelField.tsx for when
   either is allowed to run at all.

   `pointer-events` stays on, because the ripple is the point: a click has to reach the
   canvas. What sits above it is dealt with at the end of this block.                   */

.pixel-field {
  z-index: 0;
  pointer-events: auto;
}

.pixel-field .pixel-blast-container,
.pixel-field canvas {
  width: 100%;
  height: 100%;
  display: block;
}

/* Behind the masthead, no taller than a screen, and it scrolls away with the section
   because it is inside it. */
.pixel-field--header {
  position: absolute;
  inset: 0;
  max-height: 100vh;
}

/*
 * Any header that can carry the field needs the same two things: a stacking context of
 * its own so the field cannot escape it, and its content lifted above the field.
 *
 * These were written for Masthead middle alone, which is why the switch only ever worked
 * there — and why, on a page whose header is one of the others, the animation appeared at
 * whatever Masthead middle happened to be further down instead.
 */
.flx-masthead-mid,
.flx-masthead-rotating,
.flx-m2,
.flx-m30,
.flx-mastheads {
  position: relative;
  isolation: isolate;
}

.flx-masthead-mid > .grid-container,
.flx-masthead-rotating > .grid-container,
.flx-m2 > .grid-container,
.flx-m30 > .grid-container,
.flx-mastheads > .grid-container {
  position: relative;
  z-index: 1;
}

/*
 * Behind the whole page, and travelling with it.
 *
 * Pinned to the window and exactly one screen tall. The travelling is done in the shader,
 * which offsets the pattern by the scroll position — see the note in PixelField.tsx. It
 * used to be a canvas as tall as the document, which is the obvious way to do it and the
 * one that stops working on a long page: past the GPU's texture limit the layer had to
 * give up and hold still, so the background scrolled on short pages and was nailed to the
 * window on long ones.
 */
.pixel-field--page {
  position: fixed;
  inset: 0;
  /*
   * THE CLICKS COME HERE. Do not turn this off again.
   *
   * It was off, and turning it off is what stopped the ripple working — the canvas
   * inherits from this element, so with `none` here no pointer event could ever reach
   * the thing the whole effect is listening on. The field was running and animating the
   * entire time; it simply could not be touched.
   *
   * It was turned off for a reason that turned out not to hold. The worry was that a
   * layer fixed to the window covers every pixel of the screen at all times, so with
   * pointer events on it would sit between the reader and the page. It does not, because
   * it is underneath all of it: measured on the live site, the field is z-index 0 and
   * every sibling that draws anything is above it — main and the footer at 1, the sticky
   * bar at 4000, the menu at 4600, the header at 5000. Checked with the field clickable:
   * a click on the Hire us button still lands on the button, a click on the headline
   * still lands on the headline, and a click on the empty left margin lands on the field.
   * Which is the whole of what was wanted.
   *
   * What makes the gaps reachable is the block at the end of this file: inside `main` the
   * layout boxes stop taking pointer events and everything that draws something takes
   * them back. That is what a click in the space between the heading and the picture
   * falls through.
   */
  pointer-events: auto;
}

/*
 * Everything the page draws goes above the field. A stacking context per region rather
 * than one on the body, which would trap the header and the sticky bar in it too.
 *
 * NEITHER THE HEADER NOR THE STICKY BAR BELONGS IN THIS LIST, and the header being in it
 * is what stopped the navigation coming back on the way up — site-wide, because the
 * page-wide field runs on every page.
 *
 * `position: relative` was the whole of it. The header is sticky once you have scrolled
 * past the top (`.header-wrap.header--scrolled`, 0-2-0), and this selector is 0-2-1 — a
 * `:has()` takes the specificity of what is inside it — so it out-ranked the sticky and
 * won. The class still went on and came off exactly as it should; the bar it was on was
 * simply in the flow, three thousand pixels up the document. Which looks precisely like
 * a broken script and is a stylesheet.
 *
 * Both of them already sit above the field on their own z-index below. Nothing needs to
 * be added to them here.
 */
body:has(.pixel-field--page) main,
body:has(.pixel-field--page) .footer-new {
  position: relative;
  z-index: 1;
}

body:has(.pixel-field--page) .sticky-cta {
  z-index: 4000;
}

body:has(.pixel-field--page) .header-wrap {
  z-index: 5000;
  /* It is the page colour, so over the field it reads as a band with the pattern missing
     from it. The header hides on the way down and returns on the way up, so it is rarely
     over anything long enough to need a ground of its own. */
  background: transparent;
}

/* --- Letting the clicks through -------------------------------------------
   The ripple needs the pointer and the pointer was never reaching it. A section and its
   container are full-width boxes that cover the field completely, and an empty box still
   swallows a click — between the masthead heading and the picture there is nothing but
   container, which is exactly where you would try to click.

   So the boxes stop taking pointer events and everything that draws something takes them
   back. Layout is transparent to the pointer; content is not. Text stays selectable,
   links stay clickable, and the gaps between them reach the canvas.

   Only where a field is running. With both switches off nothing here applies and the
   page behaves exactly as it did.                                                      */

/*
 * `.pixel-field canvas`, not `.pixel-field`.
 *
 * The wrapper is always in the page; the canvas inside it is not. It is absent under a
 * reduced-motion request, without WebGL2, on anything under 900px for the page-wide
 * layer, and on a tab that has been switched away from. Keyed on the wrapper, all of
 * those cases took the pointer off every layout box in `main` with nothing underneath to
 * receive it — a phone in particular ran the whole site this way. Keyed on the canvas,
 * the rules exist only when there is something for a click to reach, and every one of
 * those cases gets the page's ordinary pointer behaviour, untouched.
 */
body:has(.pixel-field canvas) main,
body:has(.pixel-field canvas) main > section,
body:has(.pixel-field canvas) main .grid-container,
/*
 * AND THE FOOTER, which was left out and is a third of some pages.
 *
 * The footer sits above the field on its own z-index with pointer events on, so every
 * click landing in the space around the contact cards — which is most of the footer — hit
 * a box with nothing in it and went nowhere. The ripple worked everywhere in the body and
 * stopped dead at the last section, which reads as the effect being broken rather than as
 * a boundary anybody drew.
 *
 * Same treatment: the layout boxes stop taking the pointer and everything that draws
 * something takes it back, which is the rule immediately below.
 */
body:has(.pixel-field canvas) .footer-new,
body:has(.pixel-field canvas) .footer-new .grid-container,
body:has(.pixel-field canvas) .footer-new__inner,
body:has(.pixel-field canvas) .footer-new__details,
body:has(.pixel-field canvas) .footer-new__cards-wrap,
body:has(.pixel-field canvas) .footer-new__cards,
body:has(.pixel-field canvas) .flx-masthead-mid__content,
body:has(.pixel-field canvas) .m30__copy,
body:has(.pixel-field canvas) .m30__head,
body:has(.pixel-field canvas) .m2__copy,
body:has(.pixel-field canvas) .m2__head {
  pointer-events: none;
}

/*
 * Anything that draws, anything interactive, and anything a person might want to select.
 * `:where` so this carries no weight of its own and a component can still say otherwise
 * with a single class.
 */
body:has(.pixel-field canvas) main :where(
  a, button, input, textarea, select, label, summary, details,
  img, svg, picture, video, canvas, iframe,
  p, h1, h2, h3, h4, h5, h6, li, span, figure, blockquote, figcaption,
  [tabindex], [role], [contenteditable],
  /* The draggable rows are bare divs and would otherwise be dead between their cards. */
  .drag-row, .work-row, .card-row, .swiper, .swiper-wrapper,
  /*
   * AND EVERY RAIL. This is the bug this rule caused.
   *
   * Anything that scrolls itself has to be able to receive a finger, and none of these were
   * on the list: the credits groups are a `dl`, the work filters and the contact pills are
   * bare `div`s, and `dt` and `dd` are not among the text elements above. So on any page
   * carrying the pixel field — which is most of them — those rails were `pointer-events:
   * none` from the wrapper down, and a touch went straight through them to the canvas
   * behind.
   *
   * It looked completely correct, which is why it survived: the rail rendered at its right
   * width with the next column peeking, and the only symptom was that dragging it did
   * nothing. The buttons inside the filters still worked, because `button` was on the list,
   * so even the broken thing was half working.
   *
   * The general rule this teaches: a rule that takes the pointer away from a whole subtree
   * has to name everything that scrolls, not just everything that is clicked.
   */
  dl, dt, dd,
  .credits__groups, .credits__group,
  .work-filters, .cform__opts, .m30__btns, .flx-services__list
),
/* The footer's own contents, on the same terms. The cards are the interactive part and
   everything in them stays clickable; the space around them reaches the field. */
body:has(.pixel-field canvas) .footer-new :where(
  a, button, input, textarea, select, label,
  img, svg, picture, canvas,
  p, h1, h2, h3, h4, h5, h6, li, span,
  .footer-card, .stat-row, [tabindex], [role]
) {
  pointer-events: auto;
}

/* ── Contact form ───────────────────────────────────────────────────────────
   Figma "Contact Form" (3621:58632).

   The frame is a 1216 content row split 479 / 48 / 689 — so the right column is the
   fixed 689 and the left takes the remainder. Everything down the right column is one
   48px rhythm: pills, intro, fields, submit. Inside the field group the rhythm is 28.

   Every rule is prefixed with the section class. Not for tidiness: the foundation
   stylesheet styles bare `button` white and `button:hover` beats a single class, so a
   pill styled on `.cform__opt` alone loses its own hover. Two classes wins.
   ────────────────────────────────────────────────────────────────────────── */
/* 80 under the eyebrow's rule, which is where the frame starts the columns. The theme
   gives every eyebrow 56; this section is one of the ones that says otherwise. */
.flx-contact-form .eyebrow {
  margin-bottom: 80px;
}

/* No heading: the form keeps its column against the right-hand margin and the left is
   simply empty, rather than the form sliding across to fill the row. */
/*
 * Two cards, side by side. Figma 3650:67318.
 *
 * It was a two-column grid with the eyebrow above both halves. It is now a row of two
 * bordered cards, each with its own eyebrow inside it, because they are alternatives rather
 * than two halves of one thing: one asks you to fill in a form, the other offers to write
 * you a document, and a visitor is choosing between them.
 *
 * ## Five columns and seven, not 479 and 689
 *
 * The frame draws the pitch at 479 and the form at 689 with a 48 between them. Those are
 * not two arbitrary widths: on the frame's own 1216 content box they are exactly five
 * columns and seven, because (1216 - 11 x 48) / 12 = 57.33 and 7 x 57.33 + 6 x 48 = 689.
 *
 * Taking the pixels rather than the columns was the mistake, and it survived because it
 * looks right at precisely one window width. The container runs to 1280, then 1440, then
 * 1536, while 689 stays 689 — so above 1280 the form sat still and the pitch beside it
 * swallowed every pixel the page grew by. At a 1536 window the split was 758 / 689 when
 * the grid wanted 595 / 853, and neither card had an edge on a column line.
 *
 * So the pitch is stated as five columns of whatever the container currently is, and the
 * form simply takes the rest. The rest is seven columns exactly, by arithmetic rather than
 * by a second number that could disagree with the first: five columns, plus the gutter
 * between them, plus what is left, is twelve columns and eleven gutters, which is the
 * container. One calc, and no width in this file that only works at one screen size.
 */
.flx-contact-form .cform__inner {
  display: flex;
  align-items: flex-start;
  gap: 48px;
  width: 100%;
  min-width: 0;
}

.flx-contact-form .cform__left {
  flex: 0 0 calc((100% - 11 * 48px) / 12 * 5 + 4 * 48px);
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.flx-contact-form .cform__right {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
  padding: 48px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: var(--radius-s);
}

/* Under 1100 five columns is no longer a readable column of prose, so they stack and both
   cards give up their share of the row. */
@media only screen and (width <= 1100px) {
  .flx-contact-form .cform__inner {
    flex-direction: column;
  }

  .flx-contact-form .cform__left,
  .flx-contact-form .cform__right {
    flex: 1 1 auto;
    width: 100%;
  }

  /* Stacked, the form goes first. Somebody on a phone who chose Contact wants the
     form; the brief writer is the counter-offer and reads fine second. */
  .flx-contact-form .cform__right {
    order: -1;
  }
}

@media only screen and (width <= 899px) {
  .flx-contact-form .cform__right {
    padding: 24px;
  }
}

/* 40 below the card's eyebrow, on top of the card's own 32, which is how the frame draws
   it: the eyebrow belongs to the card and the panel starts a clear distance under it. */
.flx-contact-form .cform__panel {
  display: flex;
  flex-direction: column;
  gap: 48px;
  padding-top: 40px;
}

/* -- the enquiry pills -- */
.flx-contact-form .cform__opts,
.flx-brief .cform__opts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.flx-contact-form .cform__opt,
.flx-brief .cform__opt {
  display: inline-flex;
  align-items: center;
  height: 40px;
  padding: 0 16px;
  border: 1px solid #333;
  border-radius: 9999px;
  background: transparent;
  color: #fff;
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.flx-contact-form .cform__opt:hover,
.flx-brief .cform__opt:hover,
.flx-contact-form .cform__opt:focus-visible,
.flx-brief .cform__opt:focus-visible {
  background: transparent;
  border-color: var(--color-natural-40, rgba(255, 255, 255, 0.4));
  color: #fff;
}

.flx-contact-form .cform__opt[aria-pressed='true'],
.flx-brief .cform__opt[aria-pressed='true'],
.flx-contact-form .cform__opt[aria-pressed='true']:hover,
.flx-brief .cform__opt[aria-pressed='true']:hover,
.flx-contact-form .cform__opt[aria-pressed='true']:focus-visible,
.flx-brief .cform__opt[aria-pressed='true']:focus-visible {
  background: #f4f4f4;
  border-color: #f4f4f4;
  color: #000;
}

/* -- the copy that changes with the pill -- */
.flx-contact-form .cform__intro {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.7);
}

.flx-contact-form .cform__email,
.flx-contact-form .cform__email:hover {
  color: #fff;
  font-weight: 600;
  text-decoration: none;
}

.flx-contact-form .cform__email:hover { text-decoration: underline; }

/* -- the form -- */
.flx-contact-form .cform__form,
.flx-brief .cform__form {
  display: flex;
  flex-direction: column;
  gap: 48px;
}

.flx-contact-form .cform__fields,
.flx-brief .cform__fields {
  display: flex;
  flex-direction: column;
  gap: 28px;
}

/* The label sits in a notch in the border. It is not a floating placeholder — it stays
   put whether the control is empty or not, which is how the frame draws it. */
.flx-contact-form .cform__field,
.flx-brief .cform__field { position: relative; }

.flx-contact-form .cform__control,
.flx-brief .cform__control {
  width: 100%;
  height: 48px;
  padding: 0 16px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: var(--radius-s);
  background: transparent;
  color: #fff;
  font-family: inherit;
  font-size: var(--fs-r);
  line-height: 1.5;
  appearance: none;
  outline: none;
  transition: border-color 0.2s ease;
}

.flx-contact-form .cform__control--area,
.flx-brief .cform__control--area {
  height: 133px;
  padding: 12px 16px;
  resize: vertical;
}

.flx-contact-form .cform__control--select,
.flx-brief .cform__control--select { cursor: pointer; }
.flx-contact-form .cform__control--select option,
.flx-brief .cform__control--select option { color: #000; }

/* Dimmed until something is chosen. A select shows its first option at full strength,
   which made "Choose one" read as an answer rather than as a prompt. `:has` asks whether
   the empty option is still the selected one, so no script is involved. */
.flx-contact-form .cform__control--select:has(option[value='']:checked),
.flx-brief .cform__control--select:has(option[value='']:checked) {
  /* 0.58, up from 0.4: still clearly the prompt rather than an answer, but 5.3:1
     against the black - 0.4 measured 3.65:1, under the 4.5 accessibility asks for. */
  color: rgba(255, 255, 255, 0.58);
}

.flx-contact-form .cform__control:hover,
.flx-brief .cform__control:hover { border-color: rgba(255, 255, 255, 0.45); }
.flx-contact-form .cform__control:focus,
.flx-brief .cform__control:focus { border-color: #fff; }

.flx-contact-form .cform__control::placeholder,
.flx-brief .cform__control::placeholder { color: rgba(255, 255, 255, 0.4); }

/* Autofill repaints the box white in Chrome. A long inset shadow is the only way back. */
.flx-contact-form .cform__control:-webkit-autofill,
.flx-brief .cform__control:-webkit-autofill,
.flx-contact-form .cform__control:-webkit-autofill:hover,
.flx-brief .cform__control:-webkit-autofill:hover,
.flx-contact-form .cform__control:-webkit-autofill:focus,
.flx-brief .cform__control:-webkit-autofill:focus {
  -webkit-text-fill-color: #fff;
  -webkit-box-shadow: 0 0 0 1000px #000 inset;
  caret-color: #fff;
  transition: background-color 5000s ease-in-out 0s;
}

.flx-contact-form .cform__label,
.flx-brief .cform__label {
  position: absolute;
  left: 9px;
  top: -12px;
  padding: 0 4px;
  background: #000;
  color: rgba(255, 255, 255, 0.7);
  font-size: var(--fs-sm);
  line-height: 21px;
  pointer-events: none;
}

.flx-contact-form .cform__submit,
.flx-brief .cform__submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 52px;
  padding: 8px 24px;
  border: 1px solid transparent;
  border-radius: 9999px;
  background: #fff;
  color: #000;
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.flx-contact-form .cform__submit:hover,
.flx-brief .cform__submit:hover,
.flx-contact-form .cform__submit:focus-visible,
.flx-brief .cform__submit:focus-visible {
  background: rgba(255, 255, 255, 0.85);
  color: #000;
}

/*
 * Not ready yet.
 *
 * A darker grey, not a faded white.
 *
 * It was `opacity: 0.25` on the white pill, which is the frame's own drawing of it, and it
 * had the problem the frame could not show: opacity fades the LABEL as well as the fill, so
 * black text at a quarter strength on a quarter-strength white sits at roughly 1.5:1 against
 * the page. Unreadable, and unreadable in a way that looks like a rendering fault rather
 * than a state.
 *
 * A solid grey fill with a legible label says the same thing — this is not ready — and can
 * still be read by somebody working out why. Stated once here and applied to every disabled
 * button on the site by the selector below, so there is one disabled state rather than one
 * per component.
 */
:where(button, [type='submit'], [type='button'], .button, .btn):disabled,
:where(button, [type='submit'], [type='button'], .button, .btn)[aria-disabled='true'] {
  opacity: 1;
  background: #2a2a2a;
  border-color: #2a2a2a;
  color: rgba(255, 255, 255, 0.45);
  cursor: default;
  box-shadow: none;
}

.flx-contact-form .cform__submit:disabled,
.flx-brief .cform__submit:disabled {
  opacity: 1;
  background: #2a2a2a;
  border-color: #2a2a2a;
  color: rgba(255, 255, 255, 0.45);
  cursor: default;
}

/* The arrow travels with the label, so it dims with it rather than staying bright on a
   button that is doing nothing. */
.flx-contact-form .cform__submit:disabled svg,
.flx-brief .cform__submit:disabled svg,
:where(button, .button, .btn):disabled svg {
  color: inherit;
}

.flx-contact-form .cform__submit svg,
.flx-brief .cform__submit svg {
  transition: transform 0.25s ease;
}

.flx-contact-form .cform__submit:not(:disabled):hover svg,
.flx-brief .cform__submit:not(:disabled):hover svg {
  transform: translateX(3px);
}

.flx-contact-form .cform__note,
.flx-brief .cform__note,
.flx-contact-form .cform__error,
.flx-brief .cform__error,
.flx-contact-form .cform__done,
.flx-brief .cform__done {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
}

.flx-contact-form .cform__done,
.flx-brief .cform__done { color: #fff; }
.flx-contact-form .cform__error,
.flx-brief .cform__error { color: #ff8b7d; }

.flx-contact-form .cform__hp,
.flx-brief .cform__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

@media (max-width: 1279px) {
  .flx-contact-form .cform__inner { grid-template-columns: 1fr 1fr; }
}


@media (max-width: 991px) {
  .flx-contact-form .cform__inner {
    grid-template-columns: 1fr;
    gap: 40px;
  }
  /* One column, so there is no second one to sit in. */
    .flx-contact-form .eyebrow {
    margin-bottom: 48px;
  }
  /* padding-top: 0 lived here from the borderless era of this card. With a border
     and the eyebrow inside, zero top padding pressed the eyebrow against the frame -
     the card keeps its even padding now. */
  .flx-contact-form .cform__panel { gap: 32px; }
  .flx-contact-form .cform__form { gap: 32px; }
}


/* --- One gap between buttons ----------------------------------------------
   8px, everywhere, stated once.

   It had drifted: 16 in the theme's own `.btn-wrap`, 12 on the work row's actions and
   again on the carousel arrows, 8 on the newer sections. Nothing was wrong with any one
   of them; they were just written at different times against different frames, and a
   button pair that measures differently on two sections of the same page reads as a
   mistake even when nobody can say which one is wrong.

   Listed rather than applied to a shared class because these rows do not share one — the
   theme's markup predates the newer sections and renaming its hooks would touch every
   template. A list is honest about that and takes ten seconds to extend.

   Not on this list: the services footer, which spaces a button from a line of text rather
   than from another button, and the contact form's enquiry pills, which the frame draws
   at 8 already.                                                                        */
.btn-wrap,
.flx-work__actions,
.row-nav,
.m30__btns,
.m2__pills,
.sticky-cta__actions,
.flx-masthead__btns,
.flx-masthead-mid__btns,
.flx-cta .btn-wrap,
.hero-404 .btn-wrap {
  gap: var(--btn-gap, 8px);
}

/* --- The card -------------------------------------------------------------
   Figma 3649:66037 and the frames around it.

   Every card on the site is the same object: a filled surface, no outline, and a
   hairline at ink/20 that appears on hover. Half of them were drawn the other way round
   — a transparent box with a 1px ink/30 border — which is why they read as outlines
   against a design made of solids. The logo grid was fixed first; this is the same fix
   applied to the rest of them at once, so they cannot drift apart again.

   ## The hover only means something if there is somewhere to go

   A hairline that lights up under the pointer is a promise that clicking does something.
   On a card that is just words it is a lie, and after the second one nobody trusts any of
   them. So the stroke is conditional: the card either is a link or contains one.

   `:has(a)` asks that question in CSS, which means no renderer has to be told — a card
   that gains a link later starts responding on its own, and one that loses it stops.
   `:focus-within` is the keyboard's version of the same thing.

   ## Why an inset shadow and not a border

   A border added on hover moves the card's content by a pixel; a border kept transparent
   at rest costs two pixels of inside width on every card whether it is hovered or not.
   An inset ring is drawn inside the box and changes no measurement at all.            */

.row-card,
.stage-card,
.card-item,
.link-item,
.flx-page-header__card,
.footer-card,
.quote-card,
.stat-row,
.connect-row,
.m2__note,
.flx-text-logos .logo-item {
  background: var(--surface-card);
  border: 0;
  transition: box-shadow 0.3s ease;
}

/*
 * A card with room says where it goes with an arrow, not a stroke.
 *
 * The hairline said "this does something" without saying what, and on a page of filled
 * cards it read as the card changing shape. Phosphor's ArrowUpRight in the corner is the
 * same promise, made specifically, and it is the glyph the frames already use for exactly
 * this.
 *
 * It only appears on a card that is a link or contains one — `:has(a)` asks that in CSS,
 * so no renderer has to be told.
 */
.row-card,
.stage-card,
.card-item,
.link-item,
.flx-page-header__card,
.footer-card {
  position: relative;
}

.row-card:has(a)::after,
.stage-card:has(a)::after,
.card-item:has(a)::after,
.link-item:has(a)::after,
.flx-page-header__card:has(a)::after,
a.footer-card::after,
.footer-card:has(a)::after {
  content: '';
  position: absolute;
  top: 20px;
  right: 20px;
  width: 16px;
  height: 16px;
  background: currentColor;
  -webkit-mask: url('/icons/arrow-up-right.svg') center / contain no-repeat;
  mask: url('/icons/arrow-up-right.svg') center / contain no-repeat;
  opacity: 0;
  /* Down and to the left at rest, so it arrives along its own diagonal rather than
     appearing where it will end up. */
  transform: translate(-4px, 4px);
  transition:
    opacity 0.25s ease,
    transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
  pointer-events: none;
}

.row-card:has(a):hover::after,
.row-card:focus-within::after,
.stage-card:has(a):hover::after,
.stage-card:focus-within::after,
.card-item:has(a):hover::after,
.card-item:focus-within::after,
.link-item:has(a):hover::after,
.link-item:focus-within::after,
.flx-page-header__card:has(a):hover::after,
.flx-page-header__card:focus-within::after,
a.footer-card:hover::after,
a.footer-card:focus-visible::after,
.footer-card:has(a):hover::after,
.footer-card:focus-within::after {
  opacity: 1;
  transform: none;
}

/*
 * The rows keep the stroke.
 *
 * A stat row and a Connect row are full-width and already carry something at the right
 * edge — a figure, a button. There is no corner free to put an arrow in, so those keep
 * the hairline they had. The logo cards keep it too: a logo is not a place.
 */
.stat-row:has(a):hover,
.stat-row:focus-within,
.connect-row:has(a):hover,
.connect-row:focus-within,
.flx-text-logos .logo-item:has(a):hover,
.flx-text-logos .logo-item:focus-within {
  box-shadow: inset 0 0 0 1px var(--color-natural-20);
}

/* Asked for less movement: the arrow is still the answer, it just does not travel. */
@media (prefers-reduced-motion: reduce) {
  .row-card:has(a)::after,
  .stage-card:has(a)::after,
  .card-item:has(a)::after,
  .link-item:has(a)::after,
  .flx-page-header__card:has(a)::after,
  a.footer-card::after,
  .footer-card:has(a)::after {
    transform: none;
    transition: opacity 0.2s ease;
  }
}

/* The theme colours this one on hover and the border is gone, so it has nothing to
   colour. Restated as none rather than left to fail quietly. */
.link-item:hover {
  border-color: transparent;
}

/*
 * The quote card's arrow. Figma 3603:41526.
 *
 * A 36px target in the bottom-right corner, and nothing else on the card is a link — the
 * row it sits in is dragged sideways, and a link stretched over the whole card would
 * fight the gesture it lives inside. Press the card and it moves; press the arrow and it
 * goes to the project.
 *
 * That is also why the card does not take the shared hover hairline: a card that lights
 * up under the pointer is claiming to be clickable, and this one is not.
 */
.quote-card__by {
  display: flex;
  /* Stated. The theme laid this out as a column, and a column is what put the arrow under
     the name rather than beside it. */
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  text-align: left;
}

.quote-card__who {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.quote-card__go {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--border-default);
  border-radius: 50%;
  color: inherit;
  text-decoration: none;
  transition: border-color 0.25s ease, background-color 0.25s ease, color 0.25s ease;
}

.quote-card__go:hover,
.quote-card__go:focus-visible {
  border-color: #f4f4f4;
  background: #f4f4f4;
  color: #000;
}

.quote-card__go svg {
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.quote-card__go:hover svg {
  transform: translateX(2px);
}

/* --- A service ------------------------------------------------------------
   Figma 2031:32545.

   Two columns on the frame's 1216 row: the card is 478.67 and the pictures 689.33, 48
   apart. Held as percentages for the same reason every other two-column section here is —
   the container runs to 1536 and a fixed column stops being right the moment it does.

   Inside the bento everything is 12 apart, and the tall picture is exactly half the
   right-hand column less half that gap.                                                */

.flx-service-bento .sb__inner {
  display: grid;
  grid-template-columns: 1fr 56.6667%;
  gap: 48px;
  align-items: stretch;
}

/*
 * NOT A CARD. The name stays; the box does not.
 *
 * This column was a filled, rounded, 40px-padded card sitting beside the pictures. The
 * frame (2031:32474) has none of that: the heading, the line under it, the list and the
 * buttons sit directly on the page, and the only thing marking the column is where its
 * text starts.
 *
 * The padding was the real cost. Forty pixels of it pushed the heading down and in, so
 * the service name sat lower than the top of the photographs beside it and further right
 * than every other heading on the page. Removing it puts this column back on the same top
 * line and the same left margin as everything above and below.
 *
 * `space-between` and `align-items: stretch` stay, and now do the job the frame actually
 * asks of them: the buttons sit against the bottom of the pictures, however long the list
 * above them runs.
 */
.flx-service-bento .sb__card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 40px;
}

.flx-service-bento .sb__head {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.flx-service-bento .sb__title {
  margin: 0;
  color: var(--color-natural-100, #fff);
}

.flx-service-bento .sb__text {
  margin: 0;
  color: var(--color-natural-100, #fff);
}

/* 36 between one line and the next: 24 of text and 12 of gap. */
.flx-service-bento .sb__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-size: var(--fs-r);
  line-height: 1.5;
  /* ink/60 in the frame, and a step on the scale rather than a hard-coded rgba — the
     list is quieter than the line above it, which is what separates them now that there
     is no box holding them together. */
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
}

.flx-service-bento .sb__btns {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--btn-gap, 8px);
}

.sb__btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid #333;
  border-radius: 9999px;
  background: transparent;
  color: #fff;
  font-size: var(--fs-sm);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}

.sb__btn svg {
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.sb__btn:hover svg,
.sb__btn:focus-visible svg {
  transform: translateX(3px);
}

/* Colour comes from the shared button modifiers at the end of this file — the section
   sets the shape, the CMS sets the style. */

/* -- the bento -- */
/*
 * A FIXED HEIGHT, NOT A FLOOR.
 *
 * This was `min-height: 600px`, which sets the shortest the bento can be and says nothing
 * about the tallest. The row then sized itself to its contents — and its contents are
 * pictures whose `height: 100%` resolves against a row that has no definite height, so it
 * falls back to the image's own. Drop in one tall portrait and the whole section grows to
 * the natural height of that photograph, which is what happened here.
 *
 * A stated height fixes both halves at once: the section stops being at the mercy of
 * whatever gets uploaded, and the row becomes definite so `height: 100%` on the pictures
 * finally means what it says.
 */
.flx-service-bento .sb__pics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  height: 448px;
}

.flx-service-bento .sb__stack {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 12px;
  min-height: 0;
}

/* One picture takes the whole width rather than leaving half the row empty; two split it
   and the second one is not stacked against anything. */
.flx-service-bento .sb__pics--1 {
  grid-template-columns: 1fr;
}

.flx-service-bento .sb__pic {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-s);
  background: var(--surface-card);
  min-height: 0;
}

/*
 * Absolutely positioned, so the picture can never size the box that holds it.
 *
 * `width/height: 100%` is only a rule about the parent when the parent has a size to be a
 * percentage of; where it does not, the image quietly falls back to its intrinsic
 * dimensions and pushes the layout around from the inside. Taking it out of flow removes
 * that possibility entirely rather than relying on the height above staying put — the
 * height above is the kind of number that gets adjusted later by somebody who has not
 * read this comment.
 */
.flx-service-bento .sb__pic img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media only screen and (width <= 1023px) {
  .flx-service-bento .sb__inner {
    grid-template-columns: 1fr;
    gap: 24px;
  }

  .flx-service-bento .sb__pics {
    min-height: 420px;
  }
}

@media only screen and (width <= 640px) {
  /* The bento becomes one picture over two on a phone; three side by side at 390 wide is
     three thumbnails. */
  .flx-service-bento .sb__pics {
    grid-template-columns: 1fr;
    min-height: 0;
  }

  .flx-service-bento .sb__pic--tall {
    aspect-ratio: 4 / 3;
  }

  .flx-service-bento .sb__stack {
    grid-template-rows: none;
    grid-template-columns: 1fr 1fr;
  }

  .flx-service-bento .sb__stack .sb__pic {
    aspect-ratio: 1;
  }
}

/* --- Text + bullets -------------------------------------------------------
   Figma 3608:53662.

   The prose on the right moved into a filled card, like the services panel and the
   service sections above it. The two columns are the frame's — 39.36% and 56.66%, 48
   apart — rather than the theme's 38/57, so this section lines up with every other
   two-column section on the page.                                                      */

.flx-text-bullets .flx-text-bullets__inner {
  display: grid;
  grid-template-columns: 1fr 56.6667%;
  gap: 48px;
  align-items: start;
}

.flx-text-bullets .flx-text-bullets__left,
.flx-text-bullets .flx-text-bullets__right {
  flex: none;
  min-width: 0;
}

.flx-text-bullets .flx-text-bullets__card {
  background: var(--surface-card);
  border-radius: var(--radius-s);
  padding: 40px;
}

.flx-text-bullets .flx-text-bullets__card > * {
  margin: 0;
}

.flx-text-bullets .flx-text-bullets__card > * + * {
  margin-top: 12px;
}

/*
 * The opening paragraph carries the point, so the frame sets it a step larger and in
 * white while the rest stays at body size in ink/70. Taken from the first child rather
 * than a class, because it is rich text — there is no hook to hang a class on, and the
 * first paragraph is always the one that opens.
 */
.flx-text-bullets .flx-text-bullets__card > :first-child {
  font-size: var(--fs-l);
  line-height: 1.5;
  color: var(--color-natural-100, #fff);
  margin-bottom: 24px;
}

@media only screen and (width <= 1023px) {
  .flx-text-bullets .flx-text-bullets__inner {
    grid-template-columns: 1fr;
    gap: 24px;
  }
}

@media only screen and (width <= 640px) {
  .flx-text-bullets .flx-text-bullets__card {
    padding: 28px;
  }
}

/* --- The project bento ----------------------------------------------------
   Figma 2031:32533 — the right-hand column of the "What we do" rows.

   Two pictures stacked on the left and one running the full height on the right, then
   the project's name with an arrow and a line about it. It replaced a single wide
   picture with the same caption underneath.

   The mirror of the service section's bento (2031:32545), which puts the tall one on the
   left. That is what the frames draw; the two are on different pages and never appear
   together, so neither has to give way.                                                */

.flx-chips-card .post-bento {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.flx-chips-card .post-bento__link {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: var(--radius-s);
}

.flx-chips-card .post-bento__pics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  aspect-ratio: 689 / 500;
}

.flx-chips-card .post-bento__stack {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 12px;
  min-height: 0;
}

/* One picture takes the whole width rather than leaving half the row empty. */
.flx-chips-card .post-bento__pics--1 {
  grid-template-columns: 1fr;
}

.flx-chips-card .post-bento__pic {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-s);
  background: var(--surface-card);
  min-height: 0;
}

.flx-chips-card .post-bento__pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.6s cubic-bezier(0.26, 1, 0.48, 1);
}

/* The whole thing is one link, so the pictures answer to a hover anywhere on it. */
.flx-chips-card .post-bento:has(.post-bento__link):hover .post-bento__pic img {
  transform: scale(1.03);
}

.flx-chips-card .post-bento__info {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The name on the left, the arrow against the right-hand edge — not tucked in beside the
   last word, which is where an inline arrow ends up on a short title. */
.flx-chips-card .post-bento__title {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  color: var(--color-natural-100, #fff);
}

.flx-chips-card .post-bento__title svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.flx-chips-card .post-bento:has(.post-bento__link):hover .post-bento__title svg {
  transform: translate(3px, -3px);
}

.flx-chips-card .post-bento__text {
  margin: 0;
}

@media only screen and (width <= 640px) {
  .flx-chips-card .post-bento__pics {
    grid-template-columns: 1fr;
    aspect-ratio: auto;
  }

  .flx-chips-card .post-bento__pic--tall {
    aspect-ratio: 4 / 3;
  }

  .flx-chips-card .post-bento__stack {
    grid-template-rows: none;
    grid-template-columns: 1fr 1fr;
  }

  .flx-chips-card .post-bento__stack .post-bento__pic {
    aspect-ratio: 1;
  }
}


/* --- One gap between cards ------------------------------------------------
   12px, everywhere, stated once — the same argument as the button gap above.

   It had drifted worse than the buttons had: 24 on the contact tiles and the gallery, 24
   across and 40 down on the work grid, 16 on the page-header cards and the card row, 20
   between the quote cards, 32 on the footer group, 48 between related projects. Every one
   of them was right against the frame it was drawn from and wrong next to the section
   above it.

   Card grids only. The gap between a section's heading and its cards is a different
   measurement and is left alone; so is the 96 between one section and the next.        
   THE SERVICES PANEL IS NOT IN THIS LIST, and was until now. Two cards side by side get
   12 because they are two objects of the same kind. That panel is a sentence beside the
   list of what it covers — a different relationship, needing a different gap — and being
   in here meant the 96 set on it a few thousand lines above was silently overridden back
   to 12. Measured in a browser: computed gap 12px, with the rule above plainly saying 96.
*/
.flx-page-header__cards,
.flx-text-links__grid,
.flx-text-cards__grid,
.flx-text-logos__grid,
.flx-gallery__grid,
.flx-similar-posts__grid,
.flx-quotes__row,
.work-grid,
.card-row,
.footer-new__cards,
.footer-new__cards-wrap,
.stat-rows,
.flx-service-bento .sb__pics,
.flx-service-bento .sb__stack,
.flx-chips-card .post-bento__pics,
.flx-chips-card .post-bento__stack {
  gap: var(--card-gap, 16px);
}

/* The work grid states its two separately, and both are this. */
.work-grid {
  column-gap: var(--card-gap, 16px);
  row-gap: var(--card-gap, 16px);
}

/*
 * ONE PER ROW ON A PHONE, not two.
 *
 * This rule used to say two up, and being the last word on `.footer-card` in the file it
 * beat the `flex: 1 1 100%` set for phones a few thousand lines above — media queries
 * carry no specificity of their own, so the later rule simply won. The result was two
 * cards in 390 pixels, each about 110 wide inside its own 32px padding, and
 * `careers@griddesign.co.uk` does not fit in 110 pixels. It did not wrap; it pushed. That
 * was the seventeen pixels the whole site could be scrolled sideways by — measured on
 * every page, because the footer is on every page.
 *
 * Two up is kept for the tablet band above, where there is room for it.
 */
@media only screen and (width <= 767px) {
  .footer-card {
    flex: 1 1 100%;
    /* Half the desktop side padding. The Toronto address is the longest line any card
       holds, and at full padding it wrapped its postcode onto a second line on phones.
       Measured: 16px sides puts it on one line at 390 and up. */
    padding: 24px 16px;
  }
}

/* The smallest phones (375 and below) still cannot hold that address at 14px even with
   the halved padding — only its line steps down, one pixel, and only down here. */
@media only screen and (width <= 389px) {
  .footer-card__value {
    font-size: 13px;
  }
}

/*
 * And a value can never push its card open again.
 *
 * An email address is one unbroken word to a browser, so a narrow card gives it nowhere
 * to break and it overflows rather than wraps. `anywhere` lets it break mid-word when
 * there is no other option, which is the correct trade for an address: an address split
 * across two lines is still readable, and a page that scrolls sideways is not.
 */
.footer-card__value,
.footer-card__title {
  overflow-wrap: anywhere;
}

/* --- The full-screen menu -------------------------------------------------
   Figma "Fullscreen Nav" (3651:67681).

   A project on the left — three pictures, its name, a line about it — and a narrow column
   on the right holding the four links and the two ways to reach the studio. Content
   starts 128 from the top and runs 633.

   Dark only. There is no light variant of this, and the tokens it uses are the dark ones
   directly rather than pretending otherwise with variables that only ever hold one value.
   ────────────────────────────────────────────────────────────────────────── */

/* -- the trigger: a plus that becomes a cross -- */
.menu-trigger {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.25s ease;
}

/* Foundation fills every <button> on hover. Every state, at a specificity the theme's own
   rules cannot beat. */
.header .menu-trigger,
.header .menu-trigger:hover,
.header .menu-trigger:focus,
.header .menu-trigger:focus-visible,
.header .menu-trigger:active {
  background: transparent;
  color: inherit;
}

.menu-trigger:hover,
.menu-trigger:focus-visible {
  border-color: var(--border-hover);
}

/*
 * One shape, rotated.
 *
 * Two bars crossed at right angles are a plus; turn one of them 90° and the pair is a
 * cross. Swapping one icon for another is the obvious way to do this and it always
 * flickers — there is a frame where neither is drawn.
 */
/*
 * A hamburger that becomes a cross.
 *
 * Three bars 4 apart. Opening brings the outer two to the middle and turns them, and the
 * middle one goes out where it stands rather than travelling with them — it has nowhere to
 * go, and sliding it anywhere makes it look like a fourth thing happening.
 *
 * The travel and the turn are one transform each, in one transition, so the bars arc
 * rather than moving and then rotating.
 */
.menu-trigger__bar {
  position: absolute;
  width: 14px;
  height: 1px;
  background: currentColor;
  transition:
    transform 0.4s cubic-bezier(0.26, 1, 0.48, 1),
    opacity 0.2s ease;
}

.menu-trigger__bar--top {
  transform: translateY(-4px);
}

.menu-trigger__bar--bot {
  transform: translateY(4px);
}

.menu-trigger.is-open .menu-trigger__bar--top {
  transform: translateY(0) rotate(45deg);
}

.menu-trigger.is-open .menu-trigger__bar--bot {
  transform: translateY(0) rotate(-45deg);
}

.menu-trigger.is-open .menu-trigger__bar--mid {
  opacity: 0;
}

/*
 * The header stays on top of the panel.
 *
 * At the top of a page the header is not sticky yet and sits at z-index 11 — under the
 * panel, which is why the logo and the close button vanished the moment the menu opened.
 * While the menu is open it is lifted above, so the two controls you need are the two you
 * can still see.
 */
body:has(.site-menu.is-open) .header-wrap {
  position: sticky;
  top: 0;
  z-index: 4700;
  transform: none;
  background: transparent;
}

/*
 * The "Hire us" button stays put while the menu is open.
 *
 * It used to fade out, on the reasoning that a button in the header competes with the
 * menu's own links. That reasoning was wrong about where people are: somebody who has just
 * opened the navigation is deciding what to do next, which is the single moment the
 * primary call to action is most worth having in front of them. Taking it away at exactly
 * that point to keep the layout tidy is tidiness winning an argument it should lose.
 */

/* -- the panel -- */
.site-menu {
  position: fixed;
  inset: 0;
  /* Under the header, which stays put: the logo and the trigger have to remain visible
     and clickable, and putting them inside the panel would mean drawing them twice. */
  z-index: 4600;
  background: #000;
  overflow-y: auto;
  /*
   * Hidden by opacity, not by `display`.
   *
   * The panel is measured and painted at all times so the entrance has something to
   * animate from. `visibility` is what actually takes it out of the picture — it is
   * animatable in steps, so it flips at the end of the exit rather than at the start.
   */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-1.5%);
  transition: opacity 0.4s ease, transform 0.5s cubic-bezier(0.26, 1, 0.48, 1), visibility 0s linear 0.5s;
}

.site-menu.is-open {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 0.35s ease, transform 0.55s cubic-bezier(0.26, 1, 0.48, 1), visibility 0s;
}

/*
 * THE PICTURE TAKES ITS OWN WIDTH; THE MENU TAKES THE REST.
 *
 * This was `56.6667% 30.7%` with `space-between`, which was measured off the frame back
 * when the picture filled a landscape box. It does not any more — it is a 389:500
 * portrait capped by the window height — so the left column stayed at 56% of the screen
 * while the picture inside it shrank, and the leftover became a dead strip several
 * hundred pixels wide between the project and the links.
 *
 * `max-content` sizes the left column to the picture rather than to a percentage of a
 * window nobody measured. The right column then takes what is left, which lands close to
 * the two thirds asked for on a wide screen and stays sensible on a narrow one — a fixed
 * 2/3 would have re-created exactly the problem being fixed, one column later.
 */
.site-menu__inner {
  display: grid;
  /*
   * The left column is STATED, not measured.
   *
   * `max-content` was the obvious answer and it is wrong for the same reason it was wrong
   * on the picture itself: the only thing inside with an intrinsic width is the caption,
   * because the photograph is absolutely positioned inside its frame. So the column sized
   * itself to the words "Air Landline" and the picture — asking for 100% of it — shrank
   * to match. Measured at 180px wide in a browser.
   *
   * So the column is given the width the picture wants: whatever a 389:500 portrait can
   * be in the height available, held between a sensible floor and ceiling. The picture
   * then fills it, the caption wraps inside it, and nothing is sizing itself off anything
   * that is sizing itself off it.
   */
  grid-template-columns: clamp(320px, calc((100vh - 320px) * 389 / 500), 560px) minmax(0, 1fr);
  /* 48, the layout gutter, rather than 96 — the extra 48 was coming straight out of the
     menu column, which is the one asked to be wider. */
  gap: 48px;
  align-items: stretch;
  padding-top: 128px;
  padding-bottom: 80px;
}

/* -- the project -- */
.menu-work {
  display: flex;
  flex-direction: column;
  /* 500 of pictures, 24, then the name and the line. */
  gap: 24px;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

.menu-work__pics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--card-gap, 16px);
  aspect-ratio: 689 / 500;
}

.menu-work__stack {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: var(--card-gap, 16px);
  min-height: 0;
}

/* One picture takes the whole width rather than leaving half the row empty. */
.menu-work__pics--1 {
  grid-template-columns: 1fr;
}

.menu-work__pic {
  display: block;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-s);
  background: var(--surface-card);
  min-height: 0;
}

.menu-work__pic img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.7s cubic-bezier(0.26, 1, 0.48, 1);
}

.menu-work:hover .menu-work__pic img,
.menu-work:focus-visible .menu-work__pic img {
  transform: scale(1.03);
}

.menu-work__foot {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* The name on the left, the arrow against the right-hand edge — not tucked in beside the
   last letter, which is where an inline arrow lands on a short title. */
.menu-work__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.menu-work__title {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: #fff;
}

.menu-work__head svg {
  flex: 0 0 auto;
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1);
}

.menu-work:hover .menu-work__head svg {
  transform: translate(3px, -3px);
}

.menu-work__goal {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.7);
}

/* -- the links and the details -- */

/* The links at the top, the two addresses at the bottom of the same column — 561 down a
   633 column on the frame, which `space-between` gives without a number. */
.site-menu__side {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 64px;
  min-width: 0;
}

.menu-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  /* 80 between one row's top and the next, on rows 48 tall. */
  gap: 32px;
}

/*
 * Each row arrives after the one above it.
 *
 * The delay is `--n` multiplied, set on the item in the markup, so the list can grow in
 * the CMS without anything here needing to know how long it is. Only on the way in: a
 * staggered exit means the last row is still leaving after the panel has gone.
 */
.site-menu.is-ready .menu-list__item {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.4s ease, transform 0.5s cubic-bezier(0.26, 1, 0.48, 1);
}

.site-menu.is-ready.is-open .menu-list__item {
  opacity: 1;
  transform: none;
  transition-delay: calc(120ms + var(--n, 0) * 60ms);
}

.menu-list__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /* 15% over the h3 step, and scoped to this list alone.
     `--fs-h3` is used across the whole site, so the size lives here as its own ramp rather
     than by moving a token every other section reads. */
  font-size: clamp(27.6px, 27.6px + 9.2 * (100vw - 1024px) / 416, 36.8px);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: #fff;
  text-decoration: none;
  transition: color 0.3s ease;
}

.menu-list__link svg {
  flex: 0 0 auto;
  color: rgba(255, 255, 255, 0.4);
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1), color 0.3s ease;
}

/*
 * Sub-links. 3651:67886.
 *
 * The four single-service pages under Services, indented 20 and set at h4 — a step down
 * from the h3 above them, which is what makes the indent read as "these belong to that"
 * rather than as four short items in the same list.
 *
 * 24 between the parent and the first child, 16 between the children, against the 32 the
 * top-level rows carry. Closer together than the list they sit in, so the group holds
 * itself apart from the row underneath.
 */
.menu-sublist {
  margin: 24px 0 0;
  padding: 0 0 0 20px;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Menu text is UI, not a heading. It was borrowing the h4 step, which is how a change to
   the heading scale ends up resizing the navigation: two systems, one token. */
.menu-sublist__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-xl);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: #fff;
  text-decoration: none;
  transition: color 0.3s ease;
}

.menu-sublist__link svg {
  flex: 0 0 auto;
  color: rgba(255, 255, 255, 0.4);
  transition: transform 0.35s cubic-bezier(0.26, 1, 0.48, 1), color 0.3s ease;
}

.menu-sublist__link:hover svg,
.menu-sublist__link:focus-visible svg,
.menu-sublist__link.is-current svg {
  color: #fff;
  transform: translate(3px, -3px);
}

.menu-list__link:hover svg,
.menu-list__link:focus-visible svg,
.menu-list__link.is-current svg {
  color: #fff;
  transform: translate(3px, -3px);
}

.site-menu__details {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

.site-menu__detail {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-xl);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: #fff;
  /* Underlined from the start, as drawn — these are addresses, and an address that only
     looks like a link once you find it is a link nobody finds. */
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-color: rgba(255, 255, 255, 0.4);
  transition: text-decoration-color 0.25s ease;
}

.site-menu__detail:hover,
.site-menu__detail:focus-visible {
  color: #fff;
  text-decoration-color: #fff;
}

@media only screen and (width <= 1023px) {
  .site-menu__inner {
    grid-template-columns: 1fr;
    gap: 64px;
    padding-top: 120px;
    padding-bottom: 48px;
  }

  /* The links first. They are what the menu is for; the project is a bonus, and a bonus
     does not go above the thing itself. The DOM order is the reading order and this is
     the only place it is overridden. */
  .site-menu__side {
    order: -1;
    gap: 48px;
  }

  .menu-list {
    gap: 20px;
  }
}

@media only screen and (width <= 640px) {
  .menu-work__pics {
    grid-template-columns: 1fr;
    aspect-ratio: auto;
  }

  /* The picture keeps its own shape. A fixed 4/3 with `cover` on it cropped every
       image that was not 4/3, which on the one big picture in the menu is the whole
       composition. Let the image set the height and nothing is thrown away. */
    .menu-work__pic--tall {
      aspect-ratio: auto;
    }

    .menu-work__pic--tall img {
      height: auto;
      object-fit: contain;
    }

  .menu-work__stack {
    grid-template-rows: none;
    grid-template-columns: 1fr 1fr;
  }

  .menu-work__stack .menu-work__pic {
    aspect-ratio: 1;
  }
}

/* Someone who has asked for less movement gets the menu, not the entrance. */
@media (prefers-reduced-motion: reduce) {
  .site-menu,
  .site-menu.is-open,
  .site-menu.is-ready .menu-list__item,
  .site-menu.is-ready.is-open .menu-list__item,
  .menu-trigger__bar {
    transition: none;
    transform: none;
  }

  /* No travel, but the shape still has to change — otherwise the three bars stay three
     bars while the menu is open and the button stops saying anything. */
  .menu-trigger__bar--top {
    transform: translateY(-4px);
  }

  .menu-trigger__bar--bot {
    transform: translateY(4px);
  }

  .menu-trigger.is-open .menu-trigger__bar--top {
    transform: rotate(45deg);
  }

  .menu-trigger.is-open .menu-trigger__bar--bot {
    transform: rotate(-45deg);
  }

  .menu-trigger.is-open .menu-trigger__bar--turn {
    transform: rotate(-45deg);
  }
}

/* --- The gap the sticky bar parks in --------------------------------------
   The bar rides up onto the footer as it appears, which used to mean it sat over the last
   section of the page. A bar cannot sit on top of content politely, so a gap its own
   height is reserved above the footer and the bar comes to rest inside it.

   `--cta-h` is measured by StickyCta.tsx — the bar is two lines on a phone and one on a
   desktop, so the number has to be whatever it actually is. Zero until it is measured,
   and zero again once the bar is dismissed.                                             */
.footer-new {
  margin-top: var(--cta-h, 0px);
}

/* --- Ways to get in touch -------------------------------------------------
   Four cards, two by two: New Business and Careers on the top row, Phone and the social
   account under them. The theme laid these out as a wrapping flex row at 50% each, which
   gives a 2x2 only while there are exactly four of them and silently becomes something
   else at three or five. A grid says two columns and means it.                          */

.flx-text-links .flx-text-links__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--card-gap, 16px);
  align-items: stretch;
}

.flx-text-links .link-item {
  /* The theme sizes these as flex items. In a grid the track does the sizing, and a flex
     basis left behind fights it. */
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 28px 32px;
  border-radius: var(--radius-s);
}

@media only screen and (width <= 640px) {
  .flx-text-links .flx-text-links__grid {
    grid-template-columns: 1fr;
  }
}

/* --- Button styles --------------------------------------------------------
   Primary, outline, ghost — chosen per placement in the CMS rather than decided by where
   the button happens to sit in the markup.

   One set of three classes, worn on top of whatever the section already calls its button:
   `.flx-work__all btn-outline`, `.m30__btn btn-primary`. The alternative was a variant of
   every base class — `.m30__btn--primary`, `.sb__btn--primary`, `.flx-work__all--primary`
   and so on — which is the same three rules written seven times and six of them left
   behind the next time one changes.

   `:where()` on the base keeps the selector at one class of weight so a section that
   really does need its own answer can still say so with a plain class.               */

:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-primary {
  background: var(--color-natural-100, #fff);
  border: 1px solid var(--color-natural-100, #fff);
  color: var(--bg-color, #000);
}

:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-primary:hover,
:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-primary:focus-visible {
  background: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  border-color: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  color: var(--bg-color, #000);
}

/* The arrow on a filled button is black like the label, not the white it inherits from
   the section around it. */
:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn).btn-primary svg path {
  stroke: currentColor;
}

:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-outline {
  background: transparent;
  border: 1px solid var(--border-default);
  color: var(--color-natural-100, #fff);
}

:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-outline:hover,
:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-outline:focus-visible {
  background: transparent;
  border-color: var(--border-hover);
  color: var(--color-natural-100, #fff);
}

/*
 * HOVERING THE ROW LIGHTS THE BUTTON.
 *
 * The row is one target now, so it should look like one: point anywhere on it and the
 * stroke appears AND the pill takes its hover state, rather than the pill sitting there
 * inert until the cursor happens to cross it. Two halves of one control responding
 * together is what makes it read as one control.
 *
 * Each variant is restated rather than handled generically, because "hover" means
 * something different for each: filled goes brighter, outline brightens its border, ghost
 * gains its label colour. One shared rule would flatten three deliberate behaviours into
 * whichever happened to be written last.
 *
 * Keyboard is covered by the existing `:focus-visible` on the button itself — the row
 * cannot take focus, and should not.
 */
.connect-row:hover .flx-services__btn.btn-primary {
  background: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  border-color: var(--color-natural-90, rgba(255, 255, 255, 0.9));
  color: var(--bg-color, #000);
}

/*
 * An outline button on a hovered row FILLS, rather than brightening its edge.
 *
 * Brightening the border was the literal translation of the button's own hover, and it is
 * too quiet here: the row is already showing a stroke, so a second, slightly lighter
 * stroke inside it is two hairlines saying the same thing. Filling it makes the button
 * the brightest object on the row, which is the point — the row is the target and this is
 * where the click resolves.
 */
.connect-row:hover .flx-services__btn.btn-outline {
  background: var(--color-natural-100, #fff);
  border-color: var(--color-natural-100, #fff);
  color: var(--bg-color, #000);
}

/* The arrow is part of the label, so it turns with it. */
.connect-row:hover .flx-services__btn.btn-outline svg path {
  stroke: currentColor;
}

.connect-row:hover .flx-services__btn.btn-ghost {
  color: var(--color-natural-100, #fff);
}

/* The arrow travels on row hover too, the same 2px it does on its own. */
.connect-row:hover .flx-services__btn svg {
  transform: translate(2px, -2px);
}

@media (prefers-reduced-motion: reduce) {
  .connect-row:hover .flx-services__btn svg {
    transform: none;
  }
}

/* The header pill's arrow moves like the rest of them. */
.m2__pill svg {
  flex: 0 0 auto;
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

.m2__pill:hover svg,
.m2__pill:focus-visible svg {
  transform: translate(2px, -2px);
}

@media (prefers-reduced-motion: reduce) {
  .m2__pill svg,
  .m2__pill:hover svg {
    transition: none;
    transform: none;
  }
}

/* Ghost: the label and nothing else until you point at it. The border is transparent
   rather than absent, so a row holding one of each does not step. */
:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-ghost {
  background: transparent;
  border: 1px solid transparent;
  color: var(--color-natural-70, rgba(255, 255, 255, 0.7));
  padding-inline: 8px;
}

:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-ghost:hover,
:where(.m30__btn, .sb__btn, .flx-work__all, .flx-services__btn, .flx-people__btn, .m2__pill, .button).btn-ghost:focus-visible {
  background: transparent;
  border-color: transparent;
  color: var(--color-natural-100, #fff);
}

/* --- Section padding stops growing at the design's numbers -------------------
   The theme sets `html { font-size: 1.0582vw }`, so every rem — including these paddings
   — is a proportion of the window. At 1512 that lands exactly on the drawn values, which
   is why it looked right. Above 1512 it keeps going: 13rem is 208px on the frame and 330
   on a 2400 monitor, and the gaps between sections quietly become canyons.

   `min()` keeps the scaling down — a section on a laptop still tightens up, which is
   wanted — and stops it at the number the design actually specifies. Nothing else on the
   site is touched; capping the root font size would have re-scaled the whole theme.

   24 · 32 · 40 · 56 · 80 · 128 · 208.                                                  */

.flx-section--pt-small {
  padding-top: min(1.5rem, 24px);
}

.flx-section--pb-small {
  padding-bottom: min(1.5rem, 24px);
}

.flx-section--pt-medium {
  padding-top: min(2rem, 32px);
}

.flx-section--pb-medium {
  padding-bottom: min(2rem, 32px);
}

.flx-section--pt-xmedium {
  padding-top: min(2.5rem, 40px);
}

.flx-section--pb-xmedium {
  padding-bottom: min(2.5rem, 40px);
}

.flx-section--pt-large {
  padding-top: min(3.5rem, 56px);
}

.flx-section--pb-large {
  padding-bottom: min(3.5rem, 56px);
}

.flx-section--pt-xlarge {
  padding-top: min(5rem, 80px);
}

.flx-section--pb-xlarge {
  padding-bottom: min(5rem, 80px);
}

.flx-section--pt-xxlarge {
  padding-top: min(8rem, 128px);
}

.flx-section--pb-xxlarge {
  padding-bottom: min(8rem, 128px);
}

.flx-section--pt-huge {
  padding-top: min(13rem, 208px);
}

.flx-section--pb-huge {
  padding-bottom: min(13rem, 208px);
}

/* --- Two image grids in a row ------------------------------------------------
   A run of picture sections is one thing, not several. Left to their own padding they sit
   208 apart, which reads as three separate sections that happen to all be pictures rather
   than as a sequence.

   So a grid with another grid directly after it closes up, and the one after starts
   flush. The LAST grid in the run matches nothing here and keeps whatever it is set to,
   which is what separates the run from whatever comes next.

   The distance it closes up to is `--card-gap` — the same gap that sits between two
   pictures side by side. A run of pictures is a grid, and a grid whose rows are further
   apart than its columns reads as rows of pictures rather than as a field of them.

   `:has(+ …)` asks "is the next section a grid too?" in CSS, so nothing has to be set in
   the CMS and moving a section changes the spacing on its own.                          */

/*
 * BOTH picture blocks, in every combination.
 *
 * This was written when a run of pictures could only be galleries, and it named only
 * `.flx-gallery`. It is not true any more: a single full-width picture is a
 * `featuredMedia`, which is what the reshape builds and what a person reaches for by
 * hand, so a run is now a mix of the two.
 *
 * The effect was that Luc Vincent — built out of 1-across galleries — closed up
 * correctly at 12, while every study using featuredMedia matched none of these rules
 * and paid both sections' padding at every join. Same page, same design, two different
 * rhythms, and nothing in the CMS to explain it.
 *
 * Four selectors each way rather than a clever one, because there are exactly two block
 * types and `:has()` needs the real ones named.
 */
.flx-gallery:has(+ .flx-gallery),
.flx-gallery:has(+ .flx-featured-media),
.flx-featured-media:has(+ .flx-gallery),
.flx-featured-media:has(+ .flx-featured-media) {
  padding-bottom: var(--card-gap, 16px);
}

.flx-gallery + .flx-gallery,
.flx-gallery + .flx-featured-media,
.flx-featured-media + .flx-gallery,
.flx-featured-media + .flx-featured-media {
  padding-top: 0;
}

/* --- Who owns the gap, on a case study ----------------------------------------
 * A case study alternates copy and pictures, and each section carries its own padding.
 * Left alone, a text section closing on 128 above a picture opening on 128 puts 256
 * between them — twice the gap the frame draws, and no single setting is wrong.
 *
 * So the copy sets the rhythm and the pictures fall in behind it: a picture section
 * directly after copy loses its top padding, and one directly before copy loses its
 * bottom. Both edges are stated here rather than one, because a run of pictures can sit
 * either side of a paragraph and only the edge that touches the copy should give way.
 *
 * ONLY THE FACING EDGE. A picture section's other side still answers to Section Options,
 * so the settings are not decoration — the picture that opens a page and the one that
 * closes it are still yours to space. That is the whole reason this is four narrow rules
 * and not "pictures have no padding on a case study".
 *
 * Two text sections in a row are left alone deliberately: they would pay 256 as well, but
 * that is two blocks of copy and 256 may well be right. If it isn't, it is a setting.
 *
 * Scoped to `.case-study`, so the same blocks on an ordinary page keep their own spacing.
 */
.case-study .flx-case-text + .flx-gallery,
.case-study .flx-case-text + .flx-featured-media {
  padding-top: 0;
}

.case-study .flx-gallery:has(+ .flx-case-text),
.case-study .flx-featured-media:has(+ .flx-case-text) {
  padding-bottom: 0;
}

/* --- Credits: the services on a project, or the people -----------------------
   A pair on the left and named groups on the right, two across.

   The reference sets this in a much larger face; here the group headings are h4 and the
   lines are the body size, because on a black page at the reference's weight the list
   stops being a list and starts competing with the section above it.                   */

.credits__inner {
  display: flex;
  align-items: flex-start;
  gap: 48px;
}

/* Left column at the same 5/7 the rest of the site's two-column sections use. */
.credits__lead {
  flex: 5 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.credits__body {
  flex: 7 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The small word above each column — the same step and colour as an eyebrow, because it
   is doing an eyebrow's job. */
.credits__label {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.14px;
  color: var(--color-natural-70);
}

/* The project's line, at h3. It is the largest thing in the section and the one the
   column is about, so it takes the heading step and the full ink rather than the
   three-quarter grey the supporting lines are set in. */
.credits__lead-value {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

.credits__groups {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* 48 down, 48 across: the groups have to read as four things, not one block of text. */
  gap: 48px;
}

.credits__group {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Strategy, Website, Branding — the names of the groups, and the only white in the
   right-hand column. What is under them is the detail, and it stays grey. */
.credits__group-title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h4);
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

/* `dd` carries a browser indent by default, which would put the lines out of line with
   the heading above them. */
.credits__list {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.credits__item {
  font-size: var(--fs-r);
  line-height: 1.6;
  color: var(--color-natural-70);
}

@media only screen and (width <= 899px) {
  .credits__inner {
    flex-direction: column;
    gap: 32px;
  }

  .credits__groups {
    grid-template-columns: minmax(0, 1fr);
    gap: 32px;
  }
}

/* --- Folded copy -------------------------------------------------------------
   Closed, the text is clamped to a number of LINES — the unit an editor can actually
   decide in, and the only one that survives the copy reflowing at a different width.

   Open, the clamp is lifted and a grid row grows to the copy's own height. Line-clamp
   cannot be transitioned; a grid row going to 1fr can, and it lands on the real height
   with nothing measured in JavaScript.                                                */

.fold {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
}

/*
 * The height is animated, and it is set in pixels by FoldedText for the length of the
 * movement only — see the note there. `overflow: hidden` is what makes that height mean
 * anything; without it the copy simply spills out of the box being animated.
 */
.fold__body {
  width: 100%;
  overflow: hidden;
  transition: height 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

/*
 * The last two lines fade out, so the copy reads as continuing rather than as stopping.
 *
 * A mask, not a gradient laid over the top. An overlay has to be painted in the page's
 * background colour to hide anything, and this text sits over the pixel field on a case
 * study — a black band across the last two lines would cover the animation with a
 * rectangle you can see the edges of. A mask fades the *letters* to nothing and leaves
 * whatever is behind them alone.
 *
 * `3em` is two lines at the 1.5 line height the copy is set in. Registering it as a
 * length is what lets it be animated: a custom property is a string to the browser
 * otherwise, and a string cannot be interpolated, so the fade would snap off the moment
 * the panel opened rather than lifting with it. Where `@property` is not supported the
 * value still applies — it just changes in one step, which is the old behaviour.
 */
@property --fold-fade {
  syntax: '<length>';
  inherits: true;
  initial-value: 0px;
}

.fold__body > p {
  margin: 0;
  min-height: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--fold-lines, 6);
  line-clamp: var(--fold-lines, 6);
  --fold-fade: 3em;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fold-fade)), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--fold-fade)), transparent 100%);
  transition: --fold-fade 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Open: no clamp, no fade, and the row grows to whatever the copy needs. */
.fold.is-open .fold__body > p {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  --fold-fade: 0px;
}

/* Phosphor's caret, turning over when it opens. */
.fold__caret {
  --ph-size: 16px;
  /* Phosphor CaretDown at the THIN weight, inlined.
     The generated set in /icons is the light weight — that is the set an editor picks
     from, and it is right for a pill or a connect row. The icons inside buttons are thin,
     and rather than ship a second copy of 1,512 files for the handful of them that are,
     the few that the chrome uses are written in where they are used. */
  --ph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M210.83%2C98.83l-80%2C80a4%2C4%2C0%2C0%2C1-5.66%2C0l-80-80a4%2C4%2C0%2C0%2C1%2C5.66-5.66L128%2C170.34l77.17-77.17a4%2C4%2C0%2C1%2C1%2C5.66%2C5.66Z%22%2F%3E%3C%2Fsvg%3E");
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.fold.is-open .fold__caret {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .fold__body,
  .fold__body > p,
  .fold__caret {
    transition: none;
  }
}

/* The jump lands the section under the header rather than behind it. */
.case-study .flx-section[id] {
  scroll-margin-top: 120px;
}

/*
 * Smooth scrolling, the browser's own — a fallback for anchor links before the script has
 * mounted, and the whole of the behaviour for anyone it does not run for.
 */
html {
  scroll-behavior: smooth;
}

/*
 * Off once the eased scrolling is running. Every frame of that is a `scrollTo`, and the
 * browser smoothing each one of those on top of the easing does not read as smoother —
 * it reads as a page that will not keep up.
 */
html.has-eased-scroll {
  scroll-behavior: auto;
}

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


/* --- No stroke on a picture, anywhere --------------------------------------
 *
 * Every picture frame the theme ships was drawn as a hairline box around the image, and
 * several of them brighten that line on hover. On a photograph that already has its own
 * edge the line adds nothing and reads as a border left on by mistake — the picture is
 * the object, not a thing inside a box.
 *
 * `.work-card__frame` was done at the time it was written; this is the rest of them, in
 * one place, because they come from custom.css and that file is not ours to edit.
 *
 * The backgrounds stay: they are what fill a frame while its image is still loading, and
 * without them the space is a hole rather than a placeholder. The radius stays too — that
 * is the shape, not the stroke.
 */
.post-item__image,
.post-item:hover .post-item__image,
.post-featured__image,
.post-featured__image:hover,
.flx-cta__image,
.flx-featured-media__image,
.gallery-image,
.gallery-item,
.card-sl-item__image,
.card-sl-item:hover .card-sl-item__image,
.featured-hover-item__images,
.flx-text-images__gallery .image-horizontal,
.team-item__photo,
.timeline-item__image,
.work-card__frame,
.stage-card__frame,
.card-item__image,
.svc__card-img {
  border: 0;
}


/* --- Project Overview — "The Challenge" ------------------------------------
   Figma 2138:28545.

   A 12-column grid at the frame's 48 gutter: the facts hold five columns, the story
   holds seven, and the two never meet in the middle. The old rule was two flex items at
   36% and 57% of whatever was left, which drifted a few pixels at every width and put
   the right-hand column somewhere different on every screen.                           */

.flx-project-overview .eyebrow {
  /* The frame's 80 between the rule and the grid, against the theme's usual 56. */
  margin-bottom: 80px;
}

.ovw__inner {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 48px;
}

.ovw__facts {
  grid-column: 1 / span 5;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
  /* The frame gutters the column rather than the grid here, so a long value breaks before
     it reaches the story rather than sitting against it. */
  padding-right: 48px;
}

.ovw__body {
  grid-column: 6 / span 7;
}

/* One fact: the name of the thing, then the thing. */
.ovw__fact {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.ovw__label {
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.7);
}

.ovw__value {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  /*
   * ink/100. Every colour in this section is stated, never inherited.
   *
   * The frame is explicit about which of three greys each line takes — ink/100 for the
   * things you are meant to read, ink/70 for the things around them — and `inherit` picks
   * up whatever the theme hands down instead, which here was ink/70 for the lot. The
   * whole hierarchy of the section is those two greys against each other, so losing one
   * of them loses the design.
   */
  color: var(--color-natural-100, #fff);
}

/*
 * The story.
 *
 * Everything in here comes out of one rich text field, so the spacing is stated as
 * relationships between elements rather than as a layout: sixteen between anything and
 * the next thing, and forty-eight before a heading, which is what opens the second half
 * of the column in the frame without an editor having to leave a blank line for it.
 */
.ovw__body .wysiwyg-wrap > * {
  margin: 0;
}

.ovw__body .wysiwyg-wrap > * + * {
  margin-top: 16px;
}

.ovw__body .wysiwyg-wrap > * + h2,
.ovw__body .wysiwyg-wrap > * + h3,
.ovw__body .wysiwyg-wrap > * + h4 {
  margin-top: 48px;
}

.ovw__body .wysiwyg-wrap h2,
.ovw__body .wysiwyg-wrap h3,
.ovw__body .wysiwyg-wrap h4 {
  font-size: var(--fs-h3);
  line-height: 1.5;
  letter-spacing: -0.01em;
  /* ink/100 — the opening claim and "Key challenges:" are the two lines in this column
     that are not ink/70, and that difference is the column's structure. */
  color: var(--color-natural-100, #fff);
}

.ovw__body .wysiwyg-wrap p {
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.7);
}

.ovw__body .wysiwyg-wrap ul,
.ovw__body .wysiwyg-wrap ol {
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/*
 * A bullet, positioned rather than laid out.
 *
 * `display: flex` on the row would centre the dot against the whole item, which is right
 * for one line and wrong for two — on a wrapped bullet the dot slides to the middle of
 * the paragraph. Absolute against the first line keeps it where a reader expects it at
 * any length. `0.75em` is half of the 1.5 line height; the 3px takes off half the dot.
 */
.ovw__body .wysiwyg-wrap li {
  /* The theme spaces list items with a margin. The list is a flex column with a gap now,
     and paying both puts the bullets twice as far apart as the frame draws them. */
  margin: 0;
  position: relative;
  padding-left: 18px;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.7);
}

.ovw__body .wysiwyg-wrap li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(0.75em - 3px);
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* Stacked on anything narrow, and the facts lose their gutter — there is nothing beside
   them to be kept away from. */
@media only screen and (width <= 899px) {
  .flx-project-overview .eyebrow {
    margin-bottom: 40px;
  }

  .ovw__inner {
    grid-template-columns: 1fr;
    gap: 40px;
  }

  .ovw__facts,
  .ovw__body {
    grid-column: 1 / -1;
  }

  .ovw__facts {
    padding-right: 0;
  }

  .ovw__body .wysiwyg-wrap > * + h2,
  .ovw__body .wysiwyg-wrap > * + h3,
  .ovw__body .wysiwyg-wrap > * + h4 {
    margin-top: 32px;
  }
}


/* --- The picture grid ---------------------------------------------------------
 * The theme sizes each picture as `calc(33.33% - 16px)` — a third of the row, less its
 * share of a 24px gap — and restates that number at three breakpoints for a 10px gap.
 * The card gap is 12 now, one value for every grid on the site, and none of those six
 * hard-coded numbers agree with it: at some widths a row came up short of the right
 * margin and at others it ran past it, so a 2-up row and a 3-up row ended in different
 * places. Which is what you see as pictures that do not line up with the row above them.
 *
 * So the same maths, written against the gap instead of a number that has to be kept in
 * step with it by hand. Change `--card-gap` and the pictures resize themselves.
 *
 * STILL FLEX, deliberately. Grid would be the tidier way to say this and it is the wrong
 * one: a percentage padding resolves against the containing block, and for a flex item
 * that is the row, while for a grid item it is the item's own column. The theme sets
 * every picture's height as `padding-bottom: 50%` and similar, so moving to grid silently
 * rescales every picture on the site. The heights are the design; only the widths were
 * ever wrong.
 *
 * The aspect ratios and the breakpoint at which they change stay in custom.css, untouched
 * — these rules set nothing but `flex`.
 */
/*
 * Every picture in a gallery is the same height: 16:9 of the row.
 *
 * The full-width one is a true 16:9 — the theme drew it at 64% of the row, which is 25:16,
 * a ratio no camera, screen recording or screenshot tool produces, so every picture on the
 * site was being cropped to fit a shape nothing arrives in.
 *
 * The two-up and three-up rows then take that same *height* rather than their own ratio.
 * A percentage padding on a flex item resolves against the row, not the item, so 56.25%
 * is 16:9 of the container whether one picture is sitting in it or three — which is the
 * whole point: a run of galleries down a case study now reads as one band of pictures at
 * one height, instead of a full-width shot, then a pair of squares, then three portraits.
 * The pictures inside crop to fill, as they already did.
 *
 * One number, one place. Change 56.25% and every row follows.
 */
.flx-gallery__grid .gallery-item {
  padding-bottom: 56.25%;
}

.flx-gallery__grid .gallery-item--2 {
  flex: 0 1 calc((100% - var(--card-gap, 16px)) / 2);
}

.flx-gallery__grid .gallery-item--3 {
  flex: 0 1 calc((100% - 2 * var(--card-gap, 16px)) / 3);
}

/* The theme's own steps down, kept: three become two below 768, everything becomes one
   below 641. Only the width moves — the heights at each step are the theme's. */
@media only screen and (width <= 768px) {
  .flx-gallery__grid .gallery-item--3 {
    flex: 0 1 calc((100% - var(--card-gap, 16px)) / 2);
  }
}

@media only screen and (width <= 641px) {
  .flx-gallery__grid .gallery-item--2,
  .flx-gallery__grid .gallery-item--3 {
    flex: 0 1 100%;
  }
}


/* --- 4px corners, everywhere but the buttons ---------------------------------
 * The site's corner is 4. The theme's is 8, and in a few places 10, 12, 16 or 20.
 *
 * Those all live in custom.css, which is compiled and not ours to edit, so they are
 * restated here — the whole list, so there is one place to look rather than a number
 * hiding in a stylesheet nobody opens.
 *
 * Buttons are the exception, and stay as they are: `.button--with-arrow`, the theme
 * toggle, the menu icon, and every pill on the site. A button is a shape, not a corner.
 */
body .select2-container .select2-selection,
.footer__block-link,
.card-item,
.chip-item,
.link-item,
.flx-cta__inner,
.flx-cta__image,
.flx-featured-media__image,
.flx-text-images__gallery .image-horizontal,
.gallery-image,
.gallery-item,
.post-item__image,
.post-featured__image,
.team-item,
.experts,
.carousel-item,
.card-sl-item__image,
.featured-hover-item__images {
  border-radius: var(--radius-s);
}


/* --- The eyebrow on a case study text column ----------------------------------
   The same one the page header wears — the ↳ in front of the word, no underline — so a
   section of copy reads as sitting under the page rather than starting a new one. The
   arrow and the type come from the `.flx-m30` rules above; only the space beneath it is
   this block's own, because the header's heading group carries its own 16 and a column
   of copy does not.                                                                    */
.flx-case-text .eyebrow {
  margin-bottom: 24px;
}


/* --- Returns typed into a plain text field ------------------------------------
 * The copy on a case study is a `textarea`, and a return typed into one arrives as a
 * newline character. HTML collapses newlines to spaces, so two paragraphs written as two
 * paragraphs came out as one — the break was in the field and never on the page.
 *
 * `pre-line` is the fix rather than turning the newlines into `<br>` on the way out:
 * nothing is rewritten, the field keeps exactly what was typed, and the paragraph is
 * still one element, which is what `-webkit-line-clamp` needs to fold it. Runs of spaces
 * still collapse and the copy still wraps normally — only the returns survive.
 */
.flx-case-text__content > p,
.flx-case-text .fold__body > p,
/*
 * And the story column on a Text + Images section, which is the same job in a different
 * block: a textarea holding several paragraphs. It was missing here, so the returns an
 * editor typed on the People page arrived as one unbroken slab.
 *
 * Both the folded and unfolded states, because the fold replaces the paragraph with its
 * own and would otherwise lose the breaks the moment somebody ticked the box — a bug that
 * only appears when a setting is changed is the kind nobody connects to the setting.
 */
.flx-text-images__content > p,
.flx-text-images .fold__body > p,
/*
 * And the story column on the Team block, which is the one on /people that actually
 * holds the long copy. It was missed because the section above it — three photographs
 * and a paragraph — is a Text + Images block, and I fixed that one instead. Two blocks
 * that look alike on the page and are not the same block.
 */
.flx-text-team__content > p,
.flx-text-team .fold__body > p {
  white-space: pre-line;
}


/* --- The client's logo on a case study header ----------------------------------
 * The Partners grid's card, one of them, at the top of the header: the same filled
 * ground, the same 4px corner, the same 28 of padding. Stated against the same tokens
 * rather than copied numbers, so the two stay the same card.
 *
 * A fixed height and an automatic width. A logo set is never one shape — a wordmark is
 * long and low, a monogram is square — and sizing by width makes the tall ones tiny and
 * the wide ones enormous. Height is the thing the eye reads as "the same size".
 */
.m30__logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: max-content;
  padding: 20px 28px;
  margin-bottom: 40px;
  background: var(--surface-card);
  border-radius: var(--radius-s);
}

.m30__logo img,
.m30__logo svg {
  display: block;
  height: 40px;
  width: auto;
  max-width: 220px;
  object-fit: contain;
}

@media only screen and (width <= 899px) {
  .m30__logo {
    padding: 16px 20px;
    margin-bottom: 32px;
  }

  .m30__logo img,
  .m30__logo svg {
    height: 32px;
  }
}


/* --- An empty picture slot ------------------------------------------------------
 * The filled card ground, on every picture frame. Two jobs, one rule.
 *
 * While a page is being built a slot with nothing in it was invisible — a transparent
 * box the exact size of the missing picture, which tells you nothing about where the
 * pictures go or how many are missing. Now it is a grey box at the right size and ratio.
 *
 * Once a picture is in, the same ground is what fills the frame while the file loads,
 * so the space is a placeholder rather than a hole. `.work-card__frame` has done this
 * since it was written; this is the rest of them.
 */
.gallery-item,
.flx-featured-media__image {
  background: var(--surface-card);
}


/* --- People — the core team ---------------------------------------------------
   Figma 3690:70765.

   WHAT CHANGED, AND WHY EACH RULE IS HERE

   The section was two columns: prose on the left at 38%, a stack of wide cards on the
   right at 57%, each card carrying a 7rem portrait. Twelve people in that arrangement is
   a column of cards two and a half screens long, read one at a time, while half the page
   sits empty beside it. The design puts the prose across the top and the people in a
   three-up grid underneath, which is the shape the content actually is: a short argument,
   then a list you scan rather than read.

   Every rule below overrides one in custom.css. That file is minified and belongs to the
   migrated theme, so nothing is edited there — this sheet loads last and says what it
   means. The old rules are left intact and inert.                                       */

/* Was a row with `justify-content: space-between`. Now a column: prose, then grid. */
.flx-text-team__inner {
  display: flex;
  flex-direction: column;
  gap: 48px;
}

/* Was `flex: 0 1 38%` of a row that no longer exists. 584px is the design's measure, and
   it is a max rather than a width so a narrow window shortens it instead of overflowing. */
.flx-text-team__content {
  flex: 0 0 auto;
  /*
   * Five columns wide, and the words go all the way to the edge of it.
   *
   * The box was already five columns. Inside it, a `padding-right: 48px` meant the heading
   * and the paragraph stopped 48 short, so the container was on the grid and every line of
   * type in it ended 33 past a line. The padding was standing in for the gutter, which the
   * flex `gap` beside it already provides, so it was the gutter counted twice.
   */
  max-width: calc((100% - 11 * 48px) / 12 * 5 + 4 * 48px);
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/*
 * The grid. Three columns, counted rather than fitted.
 *
 * `repeat(auto-fill, minmax(320px, 1fr))` was the first version of this and it was wrong,
 * which is worth writing down because it looks more correct than it is. It gives three
 * columns at the width the design was drawn to and four on any container wider than about
 * 1330px — and this site's container goes to 1536. So the section quietly became a
 * four-up on large displays, which is not what anybody chose; it is just what the
 * arithmetic did. Counting the columns and stepping them down by hand says the thing that
 * was actually decided.
 */
.flx-text-team__list {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--card-gap, 16px);
}

@media (max-width: 1023px) {
  .flx-text-team__list {
    grid-template-columns: repeat(2, 1fr);
  }
}

/*
 * The card.
 *
 * `margin-bottom` is zeroed because the grid's gap is now doing that job; leaving it
 * would add a sixteenth row of space to every card and put the columns out of step.
 */
.flx-text-team .team-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  width: auto;
  padding: 20px 32px;
  margin-bottom: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
}

.flx-text-team .team-item__desc {
  min-width: 0;
}

/* The design's 20px heading and 16px line under it, 6px apart. */
.flx-text-team .team-item h3 {
  margin-bottom: 6px;
  font-size: 20px;
  line-height: 1.5;
  letter-spacing: -0.2px;
}

.flx-text-team .team-item__desc p {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--color-natural-70);
}

/*
 * The portrait.
 *
 * Was 7rem. Both dimensions are set rather than one plus an aspect ratio, because a
 * flex child with only a height set will be squashed by a long name before it will push
 * the row wider — the circle would become an ellipse on exactly the cards that are
 * hardest to notice. `flex: 0 0 auto` is the other half of that; without it the same
 * squash happens through the flex basis instead.
 */
.flx-text-team .team-item__photo {
  flex: 0 0 auto;
  /* The portrait inside is absolutely positioned by `.img-obj-fit`, so this has to be
     its containing block. custom.css already says so; it is repeated here because a
     rule that only works because of a minified file is a rule waiting to break. */
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid var(--color-natural-40);
}

/*
 * The roster tile.
 *
 * It now wears `.team-item` as well, so it inherits everything above; these rules only
 * undo what `.experts` in custom.css sets on top — a different padding, a column
 * direction, and a left-aligned group.
 */
.flx-text-team .experts {
  flex-direction: row;
  align-items: center;
  padding: 20px 32px;
}

.flx-text-team .experts__group {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0;
}

/*
 * The faces on it overlap, the way a stack of avatars does.
 *
 * `:not(:last-child)` rather than a margin on every one, so the row still ends flush
 * against the card's padding — a trailing negative margin would pull the last face
 * sixteen pixels into the gutter and read as a rendering fault.
 */
.flx-text-team .experts__group img,
.flx-text-team .experts__group svg {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--color-natural-40);
}

.flx-text-team .experts__group img:not(:last-child),
.flx-text-team .experts__group svg:not(:last-child) {
  margin-right: -16px;
}

/* One column on a phone, and the prose gives back the gutter it was holding for a
   second column that is no longer beside it. */
@media (max-width: 767px) {
  .flx-text-team__inner {
    gap: 32px;
  }

  .flx-text-team__content {
    max-width: none;
    padding-right: 0;
  }

  .flx-text-team__list {
    grid-template-columns: 1fr;
  }

  .flx-text-team .team-item,
  .flx-text-team .experts {
    padding: 16px 20px;
    gap: 20px;
  }
}


/* --- The timeline — two things that were inherited rather than chosen -----------

   1. THE ROWS WERE INSET AND THE EYEBROW WAS NOT.

   `.flx-timeline__inner` carries `padding: 6rem 3rem`. The 6rem is the space between one
   entry and the next and is doing a job. The 3rem either side is not: it pushes every
   card 48px inside the container while the eyebrow above it — and every other section on
   the site — starts at the container edge. One section indented from the rest of the page
   reads as a mistake even when nobody can say what is wrong with it.

   The vertical value is restated rather than overridden with `padding-inline`, because
   `padding-inline: 0` would leave two rules deciding one box between them and the next
   person to change the spacing would find only half of it here.

   2. THE LINE STARTED A THIRD OF THE WAY DOWN.

   `top: 14.625rem` — 234px — was the theme's way of starting the line level with the
   bottom of a card's image, back when the image had a fixed height of exactly that. The
   image is a percentage now, so the number means nothing and just holds the line down.

   Nothing in TracingLine.tsx needs to change: it reads `wrap.offsetTop` precisely so the
   stylesheet gets to decide this, and the height it sets already accounts for the run
   between the start and the first card's centre. Set to 0, that run is drawn immediately
   and the line is there from the top, with the dot riding down it from the first entry. */
/*
 * Scoped to the theme's own desktop breakpoint, and that is not fussiness.
 *
 * Both of these have a mobile counterpart in custom.css — the rows get `0 16px 20px 36px`
 * and the line moves to `left: 16px` — and a media query adds no specificity. An
 * unscoped rule in this sheet would therefore beat the mobile one at every width, put
 * 6rem of air between phone rows and take away the 36px the line sits in. The value is
 * the theme's, spelled the way the theme spells it, so the two stay in step.
 */
@media print, screen and (min-width: 64.0625em) {
  .flx-timeline__inner {
    padding: 6rem 0;
  }

  .tracing-line-wrap {
    top: 0;
  }
}


/* --- Legal documents ----------------------------------------------------------
   Terms, privacy, GDPR, accessibility.

   These pages were `<main class="container">`, and `.container` is not a class this
   theme has — the site's is `.grid-container`. So they rendered with no max width and no
   gutter, and on a wide screen the copy started off the left edge of the window. That is
   the bug in the screenshot; everything below is the layout that replaces it.

   The shape is a contents rail that stays put and a column of copy that scrolls past it.
   A privacy notice is not read, it is searched — somebody arrives wanting one answer —
   and a rail that is always on screen turns twelve headings of scrolling into one click. */

.legal {
  padding-top: var(--pad-xxlarge, 96px);
  padding-bottom: var(--pad-huge, 160px);
}

.legal__head {
  padding-bottom: 48px;
  border-bottom: 1px solid var(--border-default);
  margin-bottom: 64px;
}

.legal__title {
  font-family: 'Swiza', serif;
  font-size: clamp(40px, 6vw, 72px);
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin: 0;
}

.legal__revised {
  margin: 16px 0 0;
  font-size: 14px;
  color: var(--color-natural-70);
}

/*
 * Three columns of contents, nine of document, one gutter between them.
 *
 * It was `300px` and a 96 gap: a rail width picked by eye and a gap that is two gutters
 * pretending to be one. Neither is a column measure, so the contents ended 38 short of a
 * line and the document started 10 past one — the two errors are not even the same size,
 * which is what happens when a layout is described in two units that know nothing about
 * each other.
 */
.legal__layout {
  display: grid;
  grid-template-columns: calc((100% - 11 * 48px) / 12 * 3 + 2 * 48px) minmax(0, 1fr);
  gap: 48px;
  align-items: start;
}

/*
 * The rail.
 *
 * `position: sticky` on the nav itself rather than on a wrapper, with its own max height
 * and its own scrollbar. A long contents list on a short window is the failure case here:
 * without `overflow-y: auto` the last few entries sit below the fold with no way to reach
 * them, which is the one thing a contents list must never do.
 */
.legal__toc {
  position: sticky;
  /* Clear of the fixed header, which is why this is not simply 0. */
  top: 120px;
  max-height: calc(100vh - 160px);
  overflow-y: auto;
  /* The scrollbar, when it appears, should not shove the type sideways. */
  scrollbar-width: thin;
}

.legal__toc-label {
  margin: 0 0 24px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-natural-70);
}

.legal__toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/*
 * The marker is a pseudo-element in a fixed-width pad, not a bullet and not a border.
 *
 * A `list-style` dot would indent the text away from the label above it, and a left
 * border would move every line by a pixel as it turned on. This reserves the space
 * whether or not the dot is there, so the type never shifts — the only thing that changes
 * when you scroll into a section is that a dot fades in and the words go white.
 */
.legal__toc a {
  position: relative;
  display: block;
  padding: 6px 0 6px 20px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--color-natural-70);
  text-decoration: none;
  transition: color 0.2s ease;
}

.legal__toc a::before {
  content: '';
  position: absolute;
  left: 4px;
  top: 50%;
  width: 4px;
  height: 4px;
  margin-top: -2px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.legal__toc a:hover,
.legal__toc a:focus-visible {
  color: var(--color-natural-100, #fff);
}

.legal__toc a.is-on {
  color: var(--color-natural-100, #fff);
}

.legal__toc a.is-on::before {
  opacity: 1;
}

/*
 * The copy.
 *
 * 68 characters, give or take. Long-form legal text at the full width of a 1536px
 * container is unreadable in a way that has nothing to do with the words — the eye loses
 * the line on the way back to the left margin.
 */
.legal__body {
  max-width: 68ch;
}

.legal__body h2 {
  font-size: 28px;
  line-height: 1.3;
  letter-spacing: -0.01em;
  margin: 64px 0 20px;
  /*
   * The heading a jump link lands on should not sit against the top of the window with
   * the fixed header over it. This is the whole reason `scroll-margin-top` exists, and
   * without it every entry in the rail appears to overshoot.
   */
  scroll-margin-top: 120px;
}

.legal__body h2:first-child {
  margin-top: 0;
}

.legal__body h3 {
  font-size: 20px;
  line-height: 1.4;
  margin: 40px 0 12px;
  scroll-margin-top: 120px;
}

.legal__body p,
.legal__body li {
  font-size: 16px;
  line-height: 1.7;
  color: var(--color-natural-70);
}

.legal__body p {
  margin: 0 0 20px;
}

.legal__body ul,
.legal__body ol {
  margin: 0 0 20px;
  padding-left: 20px;
}

.legal__body li {
  margin-bottom: 8px;
}

.legal__body a {
  color: var(--color-natural-100, #fff);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.legal__body strong {
  color: var(--color-natural-100, #fff);
  font-weight: 500;
}

/* --- The company facts, at the foot of every one ---------------------------- */
.legal__company {
  margin-top: 80px;
  padding-top: 48px;
  border-top: 1px solid var(--border-default);
}

.legal__company h2 {
  margin-top: 0;
}

/*
 * A description list, because that is what it is: a set of terms and their values. Read
 * aloud, "Company number, 14577642" is the sentence somebody needs; a two-column table of
 * anonymous cells is not.
 */
.legal__facts {
  margin: 0;
  display: grid;
  gap: 0;
}

.legal__facts > div {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: 24px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border-default);
}

.legal__facts dt {
  margin: 0;
  font-size: 15px;
  color: var(--color-natural-70);
}

.legal__facts dd {
  margin: 0;
  font-size: 15px;
  color: var(--color-natural-100, #fff);
}

.legal__facts a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* --- Narrow ----------------------------------------------------------------- */
@media (max-width: 1023px) {
  .legal__layout {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  /*
   * The rail stops sticking and becomes a block of links above the document.
   *
   * A sticky contents list on a phone is a bad trade — it eats a third of a small screen
   * for the whole read. Above the text it does the same job on arrival, which is when it
   * is wanted, and then gets out of the way.
   */
  .legal__toc {
    position: static;
    max-height: none;
    overflow: visible;
    padding-bottom: 32px;
    border-bottom: 1px solid var(--border-default);
  }

  .legal__body {
    max-width: none;
  }

  .legal__facts > div {
    grid-template-columns: 1fr;
    gap: 4px;
  }
}

@media (max-width: 767px) {
  .legal__head {
    padding-bottom: 32px;
    margin-bottom: 40px;
  }

  .legal__body h2 {
    font-size: 24px;
    margin-top: 48px;
  }
}


/* --- The project in the menu ---------------------------------------------------
   One picture, at the ratio the rest of the site crops a featured image to.

   The bento was drawn for three pictures — two stacked left, one tall right — inside a
   689:500 box. With one picture it filled that box on its own, so a featured image cropped
   everywhere else at 389:500 was being squeezed into a landscape frame: a portrait shot
   lost its top and bottom, and the same project looked like a different photograph in the
   menu than on the work page.

   So the one-picture case takes the featured image's own ratio. Height is what runs out
   first at that ratio in a half-width column, so the cap is on height and the width
   follows — `margin-inline: auto` then centres what is left. Capping the width instead
   would let a tall window push the picture past the bottom of the screen, which is the
   version of this that looks broken rather than merely small.                          */
/*
 * THE WIDTH IS STATED. It cannot be left to work itself out, and the first version of
 * this rule collapsed the picture to nothing by trying.
 *
 * That version was `width: auto; max-height: …; margin-inline: auto`, which reads as
 * "stretch, but no taller than this, and centre what is left". It is wrong twice over.
 * An auto inline margin cancels the stretch a flex column would otherwise give the item,
 * so the width falls back to the content — and the content is a grid whose only child
 * holds an absolutely positioned image, which has no intrinsic width at all. Zero width,
 * and `aspect-ratio` faithfully turned that into zero height. The menu showed a title
 * with nothing above it.
 *
 * So the width is computed from the height it is allowed to have, and the ratio does the
 * rest: as wide as the column, or as wide as a 389:500 picture can be in the vertical
 * space available, whichever is smaller. Nothing depends on a stretch that an auto margin
 * then takes away.
 */
/* The column above is already the right width, so the picture simply fills it. */
.menu-work__pics--1 {
  aspect-ratio: 389 / 500;
  width: 100%;
}

/*
 * Only the project on screen is drawn.
 *
 * The others are real links carrying `hidden`, which already takes them out of the tab
 * order and off the screen — but `.menu-work` sets `display: flex`, and a display value
 * in the stylesheet beats the `hidden` attribute's own `display: none`. Without this the
 * menu shows ten projects stacked on top of each other, which is a genuinely alarming
 * thing to see and takes a moment to diagnose.
 */
.menu-work[hidden] {
  display: none;
}


/* --- Pictures that change under the pointer -------------------------------------
   The story row on the People page, and any other "Text + Images" gallery.

   Two pictures in the same box, the second on top at zero opacity. A cross-fade rather
   than a swap: at 0.45s the two faces read as one person moving, where an instant
   change reads as a broken image reloading.

   THE SECOND PICTURE IS NOT LAZY-LOADED-AWAY. It is in the markup with `loading="lazy"`
   like every other image on the site, and because its container is painted rather than
   `display: none`, the browser fetches it with the page. That is deliberate: an image
   that only starts downloading on hover arrives after the pointer has left.            */
.gallery-image--swap {
  position: relative;
  overflow: hidden;
}

.gallery-image__hover {
  position: absolute;
  inset: 0;
  opacity: 0;
  /*
   * Out slower than in. The picture under the pointer should feel like it answers
   * immediately and lets go reluctantly; matched timings feel mechanical.
   */
  transition: opacity 0.6s ease;
  /* The pointer must reach the picture underneath, not this. */
  pointer-events: none;
}

.gallery-image--swap:hover .gallery-image__hover,
.gallery-image--swap:focus-within .gallery-image__hover {
  opacity: 1;
  transition: opacity 0.45s ease;
}

/*
 * A touch on a phone is a hover that never ends — the second picture appears and stays
 * there until something else is tapped, which is not a hover effect, it is the wrong
 * picture. `hover: hover` keeps the whole thing to devices with a pointer.
 */
@media (hover: none) {
  .gallery-image__hover {
    display: none;
  }
}

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


/* --- A gallery does not butt into what comes next -------------------------------
   The quote under the pictures on a case study, and anything else in that position.

   THE PROBLEM WITH LEAVING THIS TO THE PADDING FIELD

   Every section carries its own top padding, chosen in the CMS, and most of the time that
   is right — it is the section deciding how much air it wants. It stops being right after
   a full-width block of pictures. A gallery ends on a hard edge across the whole page, so
   a section set to "None" underneath it does not read as tight, it reads as broken: the
   quote mark sits on the photograph's bottom edge and the two look like one element that
   has gone wrong.

   That is not a value somebody should have to remember to set on every case study. It is
   a property of what comes before, which is exactly the kind of thing the layout should
   know and the editor should not have to.

   So this raises a floor rather than setting a value. Only the steps BELOW 128 are listed;
   a section already set to xxlarge or huge is left alone, because it asked for more and
   should get it. The gap is never smaller than 128 after pictures and never larger than
   what was chosen.                                                                     */
@media print, screen and (min-width: 64.0625em) {
  /*
   * NOT WHEN THE NEXT THING IS ALSO A PICTURE.
   *
   * A run of galleries and full-width images is meant to sit tight — that is the
   * collapse a few hundred lines above, `padding-top: 0` between any two picture
   * blocks. This rule was written at the same weight and later in the file, so it beat
   * that collapse and put 128px back between every pair of pictures on every case study.
   * A rule meant to stop a quote touching a photograph pulled apart every image run on
   * the site instead.
   *
   * The exclusion is what makes the two rules agree about who is in charge: pictures
   * after pictures stay closed up, everything else gets the floor.
   */
  .flx-gallery + .flx-section--pt-none:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--pt-small:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--pt-medium:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--pt-xmedium:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--pt-large:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--pt-xlarge:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-none:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-small:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-medium:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-xmedium:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-large:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--pt-xlarge:not(.flx-gallery):not(.flx-featured-media) {
    padding-top: min(8rem, 128px);
  }
}

/*
 * And a smaller floor on a phone, against the mobile classes rather than the desktop
 * ones. 128 is a third of a phone screen; 64 is the same idea at the size the rest of the
 * mobile spacing works at.
 *
 * Written at the same two-class weight as the rule above deliberately: the mobile padding
 * classes are single-class selectors, so a rule of this strength would otherwise beat
 * them at every width and take the phone spacing with it.
 */
@media print, screen and (max-width: 64.06125em) {
  .flx-gallery + .flx-section--mob-pt-none:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--mob-pt-small:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--mob-pt-medium:not(.flx-gallery):not(.flx-featured-media),
  .flx-gallery + .flx-section--mob-pt-xmedium:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--mob-pt-none:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--mob-pt-small:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--mob-pt-medium:not(.flx-gallery):not(.flx-featured-media),
  .flx-featured-media + .flx-section--mob-pt-xmedium:not(.flx-gallery):not(.flx-featured-media) {
    padding-top: 64px;
  }
}


/* --- The timeline, tightened --------------------------------------------------
   Figma 3694:71397.

   The rows were as tall as their contents plus 96 above and 96 below, so two entries sat
   nearly two hundred pixels apart and the section ran on for screens at a time. The design
   does something different and better: each row is a FIXED 430 tall and the card inside it
   is 662, centred — so the cards overlap the rows above and below them and the whole thing
   interlocks instead of queueing.

   THE CARDS CANNOT COLLIDE, and it is worth writing down why rather than trusting it.
   They alternate sides, so the nearest card on the same side is two rows away — 860px
   between centres. Two 662-tall cards meeting in the middle need 662. There is 198px of
   clearance, and a card would have to grow past 860 before anything touched.             */
@media print, screen and (min-width: 64.0625em) {
  .flx-timeline__inner {
    height: 430px;
    padding: 0;
    /*
     * `align-items: center` centres the card in the row VERTICALLY, which is the whole
     * point — the card is taller than the row and hangs equally above and below it.
     *
     * THERE IS NO `justify-content` HERE, AND THERE MUST NOT BE. The first version of
     * this rule said `center` and it destroyed the timeline: the card is half the row
     * wide, so centring it horizontally parked every entry in the middle of the page,
     * one on top of the next, and the left-right alternation vanished. That alternation
     * is not a property of the card — it comes from `flex-direction: row-reverse` on the
     * even rows in custom.css, which only has an effect while the card is packed against
     * one end. Centre it and there are no ends left to pack against.
     */
    align-items: center;
  }

  /*
   * `overflow: hidden` on the frame would clip a card that is taller than its row — which
   * is now every card — so the whole effect would be shaved off at the row boundary.
   *
   * `overflow-x: clip` is the fix and not `overflow-x: hidden`: `hidden` on one axis
   * forces the other to `auto`, which would bring back a vertical scrollbar inside the
   * section. `clip` has no such rule, so the sideways overflow the original was guarding
   * against is still contained and the vertical overflow is free.
   */
  .flx-timeline {
    overflow: visible;
    overflow-x: clip;
  }
}


/* --- The closing tile on the work grid ------------------------------------------
   The last card, for when there is not enough published work to fill the row.

   It is a `.work-tile` like the rest, so it inherits the grid's sizing and the filter's
   behaviour without either needing to know it exists — no `data-types` means it shows
   under "All" and disappears the moment somebody filters, which is right: a "more to
   come" card inside a filtered view answers a question nobody asked.                   */
.work-tile--more {
  /* Not a link when it has nowhere to go, so it must not pretend to be one. */
  cursor: default;
}

.work-tile--more[href] {
  cursor: pointer;
}

/*
 * A card, not an empty frame.
 *
 * It was a hairline box with the page showing through and the words underneath it, which
 * is the shape of a project card whose photograph has failed. The same box, filled, with
 * the words inside it, is a card that never wanted a picture. Same surface as the client
 * logos and the footer cards, because that is what this site already does with a card
 * that has no image in it.
 *
 * The ratio is the projects' 389:500, so it sits in the row at exactly their height and
 * nothing in the grid has to know it is different.
 */
.work-tile__frame--card {
  display: flex;
  align-items: flex-end;
  position: relative;
  aspect-ratio: 389 / 500;
  padding: 28px;
  border-radius: var(--radius-s);
  background: var(--surface-card);
  transition: box-shadow 0.3s ease;
}

/* The same ring the other cards take on hover, so it answers a pointer the way they do.
   Only when it is a link — a card with nowhere to go should not offer. */
.work-tile--more[href]:hover .work-tile__frame--card,
.work-tile--more[href]:focus-visible .work-tile__frame--card {
  box-shadow: inset 0 0 0 1px var(--color-natural-20);
}

/* Bottom-left, where a project's title sits under its picture — so the eye finds the
   words on this card in the same place it finds them on every other one. */
.work-tile__more {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.work-tile--more .work-tile__title {
  color: var(--color-natural-100, #fff);
}

.work-tile--more .work-tile__text {
  color: var(--color-natural-70);
}

.work-tile__more-link {
  margin-top: 4px;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--color-natural-100, #fff);
  text-decoration: underline;
  text-underline-offset: 3px;
}


/* --- The menu dims what you are not pointing at --------------------------------
   Hover one link and it stays white while its neighbours drop to ink/70.

   `:has()` on the list, not a rule on the links themselves. A link cannot know that a
   sibling is being hovered — only their common parent can — so the state lives on the
   `<ul>` and the effect is applied to `:not(:hover)`. That is the whole trick, and it is
   why this needs no JavaScript and no class-toggling on mouseover.

   Focus is included, so a keyboard gets the same signal a pointer does. `:focus-visible`
   rather than `:focus`, so it does not fire on a mouse click as well as the hover.       */
.menu-list:has(.menu-list__link:hover) .menu-list__link:not(:hover),
.menu-list:has(.menu-list__link:focus-visible) .menu-list__link:not(:focus-visible) {
  color: var(--color-natural-70, rgba(255, 255, 255, 0.7));
}

/*
 * The arrows go with their words.
 *
 * They already sit at ink/40 and brighten on their own hover. Without this they would
 * stay put while the word beside them dimmed, which reads as two elements rather than
 * one link — and the arrow is the part that looks broken when it disagrees.
 */
.menu-list:has(.menu-list__link:hover) .menu-list__link:not(:hover) svg,
.menu-list:has(.menu-list__link:focus-visible) .menu-list__link:not(:focus-visible) svg {
  color: rgba(255, 255, 255, 0.25);
}

/*
 * A transition on the colour, so the ones that dim fade rather than snap.
 *
 * It is on the link at rest rather than inside the `:has()` rules, so it covers the way
 * back as well. A transition declared only in the hover state animates in and cuts out.
 */
.menu-list__link {
  transition: color 0.3s ease;
}


/* --- The social links in the full-screen menu ----------------------------------
   A rule, then a row of marks, under the email and the phone number.

   The rule is a border on this element rather than an `<hr>` in the markup: it is a
   boundary between two groups, not a thing in its own right, and a screen reader has no
   use for being told a horizontal line was drawn.                                       */
/* The rule and the space now belong to the row, not to the icons, because the row is the
   thing that sits at the foot of the panel. */
.site-menu__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px 32px;
  flex-wrap: wrap;
  margin-top: 32px;
  padding-top: 32px;
  border-top: 1px solid var(--border-default);
}

.site-menu__socials {
  display: flex;
  align-items: center;
  gap: 24px;
}

/*
 * The tap target is 44px; the mark inside it is 20.
 *
 * Icon-only links are the ones most often left at the size of their drawing, which on a
 * phone is a 20px target — half what it should be. The padding does the work and a
 * negative margin takes it back out of the layout, so the row still looks like the marks
 * are 24 apart while actually being comfortable to hit.
 */
.site-menu__social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -12px;
  color: var(--color-natural-70, rgba(255, 255, 255, 0.7));
  text-decoration: none;
  transition: color 0.3s ease;
}

.site-menu__social:hover,
.site-menu__social:focus-visible {
  color: var(--color-natural-100, #fff);
}

/* The fallback when no icon matched the label — see the note in MenuOverlay. */
.site-menu__social-word {
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
}

/*
 * The row dims with the rest of the menu.
 *
 * The main links already fade their siblings on hover. Without this the social row would
 * sit at full strength while the thing above it dimmed, which reads as the row being
 * hovered rather than as the rest stepping back.
 */
.site-menu__side:has(.menu-list__link:hover) .site-menu__social,
.site-menu__side:has(.menu-list__link:focus-visible) .site-menu__social {
  color: rgba(255, 255, 255, 0.4);
}

/* --- Mobile section spacing, a quarter tighter -----------------------------
   Every section's top and bottom padding on mobile, reduced by 25%.

   The scale itself is not changed and nothing in the CMS moves: a section set to Huge is
   still Huge, and still the biggest gap there is. The whole ladder simply sits three
   quarters of where it did, so the relationships between the steps — which are what make
   the page read as designed rather than as a list — are exactly as they were.

   WHY THIS IS HERE AND NOT IN THE THEME'S OWN BLOCK. The scale is defined in custom.css,
   which loads before this file. These are the same specificity, so the later file wins,
   and that is the whole mechanism. Nothing needs `!important`.

   The breakpoint is the theme's own mobile query — 64.06125em, or 1025px — which is the
   same one that switches the CMS's "Padding Mobile" values on, so the two agree about
   what "mobile" means. Below it the root font size is a fixed 16px, so these px values
   and the rem values they replace are the same numbers.

       small     16 → 12        xlarge     44 → 33
       medium    20 → 15        xxlarge    56 → 42
       xmedium   36 → 27        huge       80 → 60
       large     40 → 30

   None stays 0, because three quarters of nothing is nothing.                          */

@media print, screen and (max-width: 64.06125em) {
  .flx-section--mob-pt-small { padding-top: 12px; }
  .flx-section--mob-pb-small { padding-bottom: 12px; }

  .flx-section--mob-pt-medium { padding-top: 15px; }
  .flx-section--mob-pb-medium { padding-bottom: 15px; }

  .flx-section--mob-pt-xmedium { padding-top: 27px; }
  .flx-section--mob-pb-xmedium { padding-bottom: 27px; }

  .flx-section--mob-pt-large { padding-top: 30px; }
  .flx-section--mob-pb-large { padding-bottom: 30px; }

  .flx-section--mob-pt-xlarge { padding-top: 33px; }
  .flx-section--mob-pb-xlarge { padding-bottom: 33px; }

  .flx-section--mob-pt-xxlarge { padding-top: 42px; }
  .flx-section--mob-pb-xxlarge { padding-bottom: 42px; }

  .flx-section--mob-pt-huge { padding-top: 60px; }
  .flx-section--mob-pb-huge { padding-bottom: 60px; }
}

/* --- Client logos, two across on a phone -----------------------------------
   They were one across, and the reason is arithmetic rather than intent: the theme sizes
   each box at `calc(50% - 4px)`, which is the right half-width only if the gap between
   them is 8. The gap is `--card-gap`, which is 12 — so two boxes plus the gap came to
   four pixels more than the row and the second one wrapped underneath.

   Sized off the token instead, so the two numbers can never disagree again: half the row,
   less half the gap, whatever the gap is set to.                                        */

@media only screen and (width <= 640px) {
  .flx-text-logos .logo-item {
    flex: 0 1 calc(50% - (var(--card-gap, 16px) / 2));
  }
}

/* --- 44px tap targets on a phone ------------------------------------------
   Every button on the site is 40px tall, which is the drawn height and correct on a
   desktop where the pointer is a pixel. A finger is not: 44 is the smallest target the
   accessibility guidelines will accept, and it is the number both Apple and the WCAG
   target-size rule land on.

   Height rather than min-height, because these are all pills with `line-height: 1` and
   centred content — the extra four pixels go equally above and below the label and
   nothing else in the row moves.

   One list, so a button added later that borrows one of these classes is covered without
   anybody remembering this rule exists.                                                */

@media only screen and (width <= 767px) {
  .header-cta,
  .header-nav__link,
  .header-nav__toggle,
  .flx-work__all,
  .flx-services__btn,
  .flx-people__btn,
  .m30__btn,
  .sb__btn,
  .ovw__btn,
  .m2__pill,
  .flx-masthead-display__btn,
  .sticky-cta__btn,
  .flx-contact-form .cform__opt {
    height: 44px;
  }

  /* The round ones stay round. */
  .header-nav__toggle {
    width: 44px;
  }
}

/* ==========================================================================
   MOBILE PASS
   Everything below applies at 767 and under unless it says otherwise.
   ========================================================================== */

/* --- The numbered row of cards was collapsing -------------------------------
   The 01/02/03 cards had their numbers sitting outside them, overlapping the card above.
   Measured at 390: each card was 48px tall holding 150px of content, and the number was
   46 pixels above its own card's top edge.

   `.row-card` carries `flex: 1 1 0`, which on the desktop row means "take an equal share
   of the WIDTH". On a phone the row becomes `flex-direction: column` — and the same
   declaration now means an equal share of the HEIGHT. Three cards divided the row's
   height between them, each ended up 48px tall, and `justify-content: center` pushed the
   overflow out of both ends, which is why the number appeared above the card and the
   text below it.

   `0 0 auto` puts them back on their own height. The desktop row is untouched.         */

@media only screen and (width <= 899px) {
  .card-row .row-card {
    flex: 0 0 auto;
  }
}

/* --- A rail instead of a wrap ----------------------------------------------
   Four things that were wrapping onto a second and third line on a phone: the work
   filters, the service links in a page header, the credits groups, and the enquiry pills
   on the contact form. Each becomes one line you push sideways with a thumb.

   Why a rail rather than a smaller wrap: a wrapped row of pills reads as a paragraph of
   buttons and the eye has to find the end of it. One line that visibly continues past the
   edge says "there is more this way" without a word, and a thumb already knows what to do
   with it.

   `flex: 0 0 auto` on the children is the part that matters — without it the items
   squash to fit rather than overflowing, and there is nothing to scroll.

   Scrollbar hidden on every engine, and `overscroll-behavior-x: contain` so pushing the
   rail past its end does not start dragging the page or trigger the browser's back
   gesture.                                                                              */

@media only screen and (width <= 767px) {
  .work-filters,
  .flx-m30 .m30__btns,
  .credits__groups,
  .flx-contact-form .cform__opts {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /*
     * A scroller has to be allowed to be NARROWER than what is in it, and a flex item's
     * default `min-width: auto` refuses that — it grows to its content instead. Measured
     * without this: the row of service links was 448px wide inside a 358px column, so
     * there was nothing left to scroll and it simply hung off the side. These two lines
     * are what make it a rail rather than an overflow.
     */
    min-width: 0;
    max-width: 100%;
  }

  .work-filters::-webkit-scrollbar,
  .flx-m30 .m30__btns::-webkit-scrollbar,
  .credits__groups::-webkit-scrollbar,
  .flx-contact-form .cform__opts::-webkit-scrollbar {
    display: none;
  }

  .work-filters > *,
  .flx-m30 .m30__btns > *,
  .flx-contact-form .cform__opts > * {
    flex: 0 0 auto;
  }

  /*
   * The credits groups are columns of writing rather than pills, so they need a width to
   * hold: one full group, and enough of the next showing to say there is one. 78% leaves
   * a fifth of the following group in view.
   */
  .credits__groups {
    grid-template-columns: none;
    gap: 32px;
  }

  .credits__group {
    flex: 0 0 78%;
  }

  /* The filters keep their 28px reading gap; it is what stops them running together. */
  .work-filters {
    gap: 28px;
    margin-bottom: 32px;
  }

  /*
   * AND EVERY BOX BETWEEN THE RAIL AND THE PAGE HAS TO ALLOW IT.
   *
   * `min-width: 0` on the scroller alone is not enough. Its parents are flex and grid
   * items too, and they carry the same automatic minimum — so instead of the rail being
   * narrower than its content, the whole column grew to fit it. Measured after the first
   * attempt: the contact page gained 173 pixels of sideways scroll and a case study 119,
   * neither of which had any before. The chain has to be opened the whole way up.
   */
  .credits__inner,
  .credits__body,
  .flx-contact-form .cform__inner,
  .flx-contact-form .cform__right {
    min-width: 0;
  }

  /*
   * And the credits column takes the width it is given rather than the width it wants.
   *
   * `.credits__inner` becomes a column on a phone, and its children are sized across by
   * the container's `align-items` rather than by any rule of their own — which left this
   * one at its content width, 493px inside a 358px column, and the page could be pushed
   * sideways by the difference. Stated rather than inherited.
   */
  .credits__body {
    width: 100%;
  }
}

/* --- The connect rows keep their shape on a phone --------------------------
   They were stacking: heading, then the button underneath on its own line, which made
   four tall cards out of four short ones. The row is the better shape at any width — a
   label on the left and the way in on the right — so it stays a row, and the button
   drops its label and becomes the arrow alone.

   The label is hidden rather than removed, and the link keeps an accessible name from the
   heading beside it via `aria-label` on the row's anchor — the arrow on its own is a
   direction, not a destination.                                                          */

@media only screen and (width <= 767px) {
  .connect-row {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
  }

  .connect-row .flx-services__btn {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    padding: 0;
    gap: 0;
    justify-content: center;
  }

  .connect-row .flx-services__btn > span {
    display: none;
  }
}

/* --- 40 between the buttons and the pictures -------------------------------
   Measured at 24, which is the stack's own gap. A button row and a picture are not two
   paragraphs of the same thing, and 24 read as a mistake.                               */

@media only screen and (width <= 767px) {
  .flx-service-bento .sb__btns {
    margin-bottom: 16px;
  }
}

/* --- The capability list reads as a sentence on a phone --------------------
   Six items down the page is six lines of nearly nothing, and it pushed the picture below
   the fold. Inline with commas it is one paragraph the eye takes in at once.

   `::after` rather than commas in the markup, so the list is still a list to a screen
   reader and to the CMS — the punctuation is presentation and lives in the stylesheet.
   No comma after the last one.                                                          */

@media only screen and (width <= 767px) {
  .flx-service-bento .sb__list {
    display: block;
    font-size: var(--fs-r);
  }

  .flx-service-bento .sb__list li {
    display: inline;
  }

  .flx-service-bento .sb__list li::after {
    content: ', ';
  }

  .flx-service-bento .sb__list li:last-child::after {
    content: '';
  }
}

/* --- The work tile title, a quarter larger on a phone ----------------------
   The name of the client is the thing being scanned in a column of cards; at the h6 step
   it was the same size as the line under it and the two ran together.                   */

@media only screen and (width <= 767px) {
  .work-tile__title,
  .work-card__title {
    font-size: calc(var(--fs-h6) * 1.25);
  }
}

/* --- Legal pages: no contents list on a phone ------------------------------
   The rail is a navigation aid for a document you can see beside you. On a phone it is
   not beside the document, it is a screen and a half in front of it — fifteen links to
   scroll past before reaching the first word of what they link to. The document is the
   point; the reader scrolls.

   Removed from the layout rather than hidden with `visibility`, so it takes no space, and
   the scroll-spy that highlights the current section simply finds nothing to highlight.  */

@media only screen and (width <= 767px) {
  .legal__toc {
    display: none;
  }
}

/* --- The people photographs sit at the card gap ----------------------------
   They were 24 apart against the work grid's 12, which made the same page look like two
   different grids. The ratio of the pictures is untouched — this is the space between
   them and nothing else.                                                                 */

@media only screen and (width <= 767px) {
  .flx-text-images__gallery .swiper-wrapper {
    gap: var(--card-gap, 16px);
  }
}

/* --- The people photographs: two, and the edge of a third -------------------
   They were 280px wide inside a 358px column with the section clipping, so a phone showed
   one photograph, a sliver of the second, and no sign at all that a third existed.

   44% each puts two side by side with about twenty pixels of the next one showing — the
   crop IS the invitation, and it is the only thing on a phone that says a row continues.
   And the row now scrolls, so the invitation leads somewhere: it was clipped by the
   section with no way to reach what had been cut off.

   THE RATIO IS NOT TOUCHED. The frame's height is a percentage of its own width, so a
   narrower slide is a shorter slide in exactly the same proportion. Nothing is cropped
   that was not cropped before.                                                          */

@media only screen and (width <= 767px) {
  .flx-text-images__gallery .swiper-wrapper {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }

  .flx-text-images__gallery .swiper-wrapper::-webkit-scrollbar {
    display: none;
  }

  .flx-text-images__gallery .swiper-slide {
    width: 44%;
    scroll-snap-align: start;
  }
}

/* --- A picture keeps its own shape on a phone ------------------------------
   Every picture on a case study was being drawn into a 16:9 box with `object-fit: cover`
   — measured at 390: every single image on the page came out 358 x 201, whatever it
   actually was. A screenshot that is taller than 16:9 loses its top and its bottom, and a
   card with a logo in the middle of it loses the logo. That is not a crop anybody chose.

   On a desktop the fixed box is doing real work: it keeps a row of pictures on the same
   baseline and stops a page of mixed shapes reading as a jumble. There is no row on a
   phone — every picture is alone on its own line — so the box buys nothing and costs the
   top and bottom of the image.

   So on a phone the frame stops being a shape and becomes whatever the picture is: the
   percentage padding goes, the image comes out of its absolute position, and the height
   follows the width the way an unstyled image always has.

   `aspect-ratio: auto` as well as the padding, because some of these frames are drawn
   with one and some with the other and this has to catch both.                          */

@media only screen and (width <= 767px) {
  .flx-featured-media .flx-featured-media__image,
  .flx-gallery .gallery-item,
  .flx-gallery .gallery-image,
  .case-study .flx-featured-media__image,
  .case-study .gallery-item {
    padding-bottom: 0;
    height: auto;
    aspect-ratio: auto;
  }

  .flx-featured-media .flx-featured-media__image > img,
  .flx-gallery .gallery-item > img,
  .flx-gallery .gallery-image > img,
  .case-study .flx-featured-media__image > img,
  .case-study .gallery-item > img {
    position: static;
    width: 100%;
    height: auto;
    /* Nothing left to fit into — the box is the picture now. */
    object-fit: fill;
  }
}

/* --- The 404 game ----------------------------------------------------------
   Conway's Life, drawn as the same field of squares the site is built out of. The board
   is a canvas rather than the WebGL background: the background's job is to sit behind a
   page and never be touched by it, and a game needs to own its pixels, know which square
   was clicked, and keep a second grid of state per cell. Same visual language, its own
   surface, and nothing about the site's background is put at risk by it.

   Colours are read off the theme at run time rather than set here — see LifeField.tsx —
   so the board is drawn in whatever the page is actually wearing.                      */

.life {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.life__board {
  position: relative;
  width: 100%;
  min-height: 360px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
  /* The canvas is drawn to the box's exact pixel size; anything spilling is a rounding
     remainder of a cell and should be cut rather than shown. */
  overflow: hidden;
  background: var(--bg-color);
}

.life__canvas {
  display: block;
  width: 100%;
  cursor: crosshair;
  /* Without this a drag on a phone is taken by the scroller and the stroke stops dead
     halfway across the board. */
  touch-action: none;
}

.life__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.life__controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.life__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 16px;
  border: 1px solid var(--border-default);
  border-radius: 9999px;
  background: transparent;
  color: var(--color-natural-100, #fff);
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.3s ease, background 0.3s ease, color 0.3s ease;
}

.life__btn:hover,
.life__btn:focus-visible {
  border-color: var(--color-natural-100, #fff);
}

/* The one filled button, because there is one thing you are most likely to want. */
.life__btn--go {
  background: var(--color-natural-100, #fff);
  border-color: var(--color-natural-100, #fff);
  color: var(--bg-color, #000);
}

.life__btn--go:hover,
.life__btn--go:focus-visible {
  opacity: 0.85;
}

.life__meta {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
}

.life__gen {
  font-variant-numeric: tabular-nums;
  color: var(--color-natural-100, #fff);
}

@media only screen and (width <= 767px) {
  .life__btn {
    height: 44px;
  }
  .life__board {
    min-height: 300px;
  }
}

/* --- The way in, for anyone who looks -------------------------------------
   A three-by-three of squares in the corner of the full-screen menu. No label, no
   tooltip, and it does not appear in the menu's list of links — it is a small live thing
   in a corner, and the only reward for noticing it is finding out what it does.

   It carries a real accessible name, because "hidden" is a visual game and not one worth
   playing with somebody using a screen reader — they get "Game of Life" and can decide
   for themselves whether that sounds interesting.                                      */

.site-menu__egg {
  position: absolute;
  right: var(--edge, 20px);
  bottom: var(--edge, 20px);
  display: grid;
  grid-template-columns: repeat(3, 4px);
  grid-template-rows: repeat(3, 4px);
  gap: 2px;
  padding: 10px;
  background: none;
  border: 0;
  cursor: pointer;
  opacity: 0.28;
  transition: opacity 0.4s ease;
}

.site-menu__egg span {
  display: block;
  width: 4px;
  height: 4px;
  background: currentColor;
  color: var(--color-natural-100, #fff);
  transition: opacity 0.5s ease;
}

/* Five of the nine, in a glider — which is the smallest thing in Life that moves, and
   the closest the game has to a signature. */
.site-menu__egg span:nth-child(2),
.site-menu__egg span:nth-child(4),
.site-menu__egg span:nth-child(8) {
  opacity: 0.15;
}

.site-menu__egg:hover,
.site-menu__egg:focus-visible {
  opacity: 1;
}

.site-menu__egg:focus-visible {
  outline: 1px solid var(--color-natural-100, #fff);
  outline-offset: 4px;
}

.site-menu__egg:hover span,
.site-menu__egg:focus-visible span {
  opacity: 1;
}

/* --- The 404 --------------------------------------------------------------
   A number, a rule that runs off the side of the screen, and a board. Nothing else: the
   heading and the two buttons that used to sit here were repeating what the navigation
   already offers, and a dead end does not need to be explained twice.

   The rule reaches the window rather than stopping at the container. `100%` in a margin
   resolves against the containing block — the eyebrow, which is the container's width —
   so half the difference between that and the window, less the gutter, is exactly how far
   the line has to be pulled out. Same arithmetic as `.bleed-edge` above, applied to one
   edge, so the number stays lined up with the board underneath it and only the rule
   travels.

   `overflow-x: clip` on the section for the same reason the picture bands have it: where
   a scrollbar takes up space, `100vw` is half a pixel wider than the window and the
   section absorbs it rather than the page.                                              */

.flx-404 {
  overflow-x: clip;
}

.flx-404 .eyebrow {
  margin-bottom: 40px;
}

/* The rule runs to the board's own right edge, which is the container's. No pull. */
.flx-404 .eyebrow__divider {
  margin-right: 0;
}

/* Taller, because the board is the page now rather than something under it. Clamped at
   both ends: never a strip on a short laptop, never so tall the controls fall off the
   bottom on a big monitor. */
.flx-404 .life__board {
  height: clamp(420px, 62vh, 760px);
}

@media only screen and (width <= 767px) {
  .flx-404 .life__board {
    height: clamp(360px, 56vh, 520px);
  }
}

/* --- The x-ray ------------------------------------------------------------
   The site as its own construction drawing. See XrayGrid.tsx for what turns it on and
   why the numbers are measured rather than declared.

   Everything here is scoped under `html.gd-xray`, so with it off not one of these rules
   applies and the page is exactly what it was. That is not tidiness — it is the whole
   safety argument for shipping a debug view on a live site.                             */

.xray {
  position: fixed;
  inset: 0;
  z-index: 4200;
  /* Above the page, below the menu (4600) and the header (5000) — you can still open the
     navigation with the grid on, which is how anybody would want to use it. */
  pointer-events: none;
}

/* The baseline, behind the columns. Half-pixel lines so the rhythm reads as a rhythm
   rather than as a fence. */
.xray__baseline {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-natural-100, #fff) 5%, transparent) 0 0.5px,
    transparent 0.5px var(--baseline, 8px)
  );
}

/* The columns sit in the real container, so they land exactly where the layout lands. */
.xray__cols {
  position: absolute;
  inset: 0;
  display: grid;
  /* Count and gutter come from the component, which picks them from the viewport width.
     Twelve at 48 was hard-coded here, and eleven gutters of 48 on a 390px screen is 528px
     of gutter, so the columns marched off the right-hand edge of every phone. */
  grid-template-columns: repeat(var(--xray-cols, 12), minmax(0, 1fr));
  gap: var(--xray-gutter, 48px);
  /* The container's own rules give it `flex-direction: column` and a gap on most
     sections; this is a grid and needs neither. */
  flex-direction: unset;
}

.xray__col {
  display: block;
  height: 100%;
  /* A wash you can read through, not a band. The first attempt filled the columns at 9%
     and the page underneath turned green — an overlay that makes the site unreadable is a
     screenshot, not a tool you browse with. The edges carry the information; the fill is
     only there so a column reads as a column. */
  background: color-mix(in srgb, var(--gd-xray-accent, #36ffae) 3.5%, transparent);
  border-left: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 20%, transparent);
  border-right: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 20%, transparent);
}

/* The readout. Bottom left, out of the way of the menu button on the right. */
/*
 * And every box that draws something, outlined.
 *
 * `outline` rather than `border`, because an outline is painted outside the box and takes
 * no space — a border would move every element on the page by a pixel the moment this
 * came on, and an x-ray that changes the thing it is measuring is worthless.
 *
 * `:where()` so the whole selector carries no specificity at all: it cannot win against
 * any rule the site already has, which means turning the x-ray on can change what you
 * see and never what the page does.
 */
html.gd-xray body :where(
  section, article, aside, nav, header, footer, figure, ul, ol, li,
  h1, h2, h3, h4, h5, h6, p, a, button, img, svg, picture, canvas, input, textarea, label
) {
  outline: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 20%, transparent);
  outline-offset: -1px;
}

/* The container itself, harder, because it is the thing the whole layout hangs off. */
html.gd-xray body :where(.grid-container) {
  outline: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 60%, transparent);
}

/* The accent, per theme. Mint on the dark site, a deep green on the cream mastheads —
   the mint is invisible on cream and this is a measuring tool, not a mood. */
html.gd-xray {
  --gd-xray-accent: #36ffae;
}

html.gd-xray body:has(.flx-masthead-display--cream),
html.gd-xray body:has(.flx-masthead-display--paper),
html.gd-xray.theme-light {
  --gd-xray-accent: #0f7a55;
}

/* The control in the menu. A labelled button rather than a hidden key, because this is a
   feature and not an easter egg — the glider in the corner is the easter egg. */
.site-menu__xray {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--color-natural-70);
  font-family: inherit;
  font-size: var(--fs-sm);
  line-height: 1.5;
  text-align: left;
  cursor: pointer;
  transition: color 0.3s ease;
}

.site-menu__xray:hover,
.site-menu__xray:focus-visible {
  color: var(--color-natural-100, #fff);
}

.site-menu__xray[aria-pressed='true'] {
  color: var(--gd-xray-accent, #36ffae);
}

@media only screen and (width <= 599px) {
  /* On a phone the invitation is longer than the row is wide, so it takes its own line
     under the icons rather than squeezing them. */
  .site-menu__foot {
    justify-content: flex-start;
  }

  .site-menu__xray {
    width: 100%;
  }
}

/* --- The sticky grid button -----------------------------------------------
   Bottom left, small, on every page. Deliberately tool-shaped rather than brand-shaped:
   this is a control that reveals how the thing is made, and it should look like one.

   Bottom LEFT rather than right, because the right-hand side of a phone screen is where
   the thumb lives and where the browser puts its own furniture.                         */

/*
 * The grid control.
 *
 * The same round bordered button as the menu toggle, at the same size, with a Phosphor
 * icon in it like every other icon on the site. It carried a word and a hand-drawn glyph
 * before, which made it the only control here that was one of a kind, and a control that
 * is one of a kind reads as one that was added later. It was.
 *
 * Bottom LEFT rather than right, because the right-hand side of a phone screen is where
 * the thumb lives and where the browser puts its own furniture.
 */
/* The dock holds the button and its label. The fixed position moved here so the two can
   sit on a line together rather than the label being positioned against the button. */
.xray-dock {
  position: fixed;
  left: var(--edge, 20px);
  bottom: var(--edge, 20px);
  z-index: 4300;
  display: flex;
  align-items: center;
  gap: 12px;
  pointer-events: none;
}

.xray-dock > * {
  pointer-events: auto;
}

/*
 * The label, sliding in from behind the button.
 *
 * Transform and opacity only, so it is composited and costs nothing, and it starts from
 * a small offset rather than a big one: a label that flies in is a distraction on a page
 * you have just covered in guides.
 */
/*
 * A tooltip that pops, rather than a label that slides.
 *
 * It grows from the button's own edge — scale and a small rise together, from a transform
 * origin on the left so the movement starts where the cursor already is. Transform and
 * opacity only, so it is composited and costs nothing.
 *
 * The overshoot in the curve is the whole character of it: it arrives slightly past its
 * resting size and settles back, which is what makes a popup read as popping rather than
 * fading. A tooltip that eases politely into place is a tooltip nobody notices arriving.
 */
.xray-dock__note {
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: var(--fs-sm);
  line-height: 1;
  white-space: nowrap;
  padding: 8px 12px;
  border: 1px solid var(--border-default);
  border-radius: 4px;
  background: color-mix(in srgb, var(--bg-color, #000) 88%, transparent);
  backdrop-filter: blur(8px);
  color: var(--color-natural-100, #fff);
  opacity: 0;
  transform-origin: left center;
  transform: translateX(-6px) scale(0.88);
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/*
 * Hover and focus. Nothing else.
 *
 * It also showed while the grid was on, and that state outlives the gesture that caused it:
 * turn the grid on, turn it off, move away, and the label is still sitting there because
 * nothing was watching the pointer. A tooltip whose visibility depends on two different
 * things has two ways to get stuck and only one of them is obvious.
 *
 * One condition now — is the cursor on it — so it cannot survive the cursor leaving.
 */
.xray-dock:hover .xray-dock__note,
.xray-dock:focus-within .xray-dock__note {
  opacity: 1;
  transform: translateX(0) scale(1);
}

/* Asked for less movement: it still appears, it just does not spring. */
@media (prefers-reduced-motion: reduce) {
  .xray-dock__note {
    transition: opacity 0.2s ease;
    transform: none;
  }

  .xray-dock:hover .xray-dock__note,
  .xray-dock:focus-within .xray-dock__note {
    transform: none;
  }
}

/* No room beside the button on a phone, and the grid itself is the answer to "is it on". */
@media only screen and (width <= 599px) {
  .xray-dock__note {
    display: none;
  }
}

.xray-btn {
  z-index: 4300;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg-color, #000) 82%, transparent);
  backdrop-filter: blur(8px);
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}

/*
 * Hover fills the circle and the icon goes dark.
 *
 * It brightened the stroke and the glyph before, which is a hover state that says "this is
 * still a border". Filling it makes the control briefly solid under the cursor and swaps the
 * icon to the page's own background colour — the same inversion the pill buttons do, so the
 * one round control on the page behaves like the rest of the family.
 */
.xray-btn:hover .ph,
.xray-btn:focus-visible .ph {
  color: var(--bg-color, #000);
}

.xray-btn:hover,
.xray-btn:focus-visible {
  background: var(--color-natural-100, #f4f4f4);
  border-color: var(--color-natural-100, #f4f4f4);
  color: var(--bg-color, #000);
  opacity: 1;
}



.xray-btn:focus-visible {
  outline: 2px solid var(--gd-xray-accent, #36ffae);
  outline-offset: 2px;
}

/*
 * On looks like hover: filled, with a dark icon.
 *
 * It went green, which is the overlay's own accent — a colour that belongs to the guides
 * being drawn, not to the control that draws them. On a page already carrying green lines
 * the button joined in and stopped reading as a button. Same treatment as hover, so the
 * control has two appearances rather than three.
 */
.xray-btn[aria-pressed='true'] {
  background: var(--color-natural-100, #f4f4f4);
  border-color: var(--color-natural-100, #f4f4f4);
  color: var(--bg-color, #000);
  opacity: 1;
}

.xray-btn[aria-pressed='true'] .ph {
  color: var(--bg-color, #000);
}

/* --- The 404 board is the page -------------------------------------------
   Edge to edge and one screen tall, so the game IS the background between the header and
   the footer rather than a card sitting on it. The number and the controls float on top
   of it, because a board with furniture stacked above and below it is a widget, and this
   is meant to read as the page itself.

   `svh` rather than `vh`: on a phone `vh` is the tallest the viewport ever gets, so a
   full-height board is always taller than what you can actually see and the controls sit
   below the fold behind the browser's own bar. `svh` is the smallest state, which is the
   one where everything has to fit.                                                       */

.flx-404 .grid-container {
  position: relative;
  gap: 0;
}

.flx-404 .life {
  position: relative;
  gap: 0;
}

/*
 * The board is the page width, not the window width.
 *
 * It was pulled full bleed with a negative margin and `100vw`, which made it the only
 * thing on the site that ignores the container. On a page whose whole argument is that
 * design is built on a grid, the one element that runs past the margins is the wrong
 * element to have chosen. It sits in the container now, so its edges land on column one
 * and column twelve like everything else, and it takes the height instead.
 */
.flx-404 .life__board {
  width: 100%;
  height: calc(100svh - 112px);
  min-height: 420px;
  border: 1px solid var(--border-default);
  /* The site radius. Four, everywhere, no exceptions. */
  border-radius: 4px;
}

/*
 * The number sits over the top-left of the board, where the eyebrow was.
 *
 * `left: var(--edge)` rather than `0`: an absolutely positioned child is laid out against
 * its ancestor's PADDING box, so `0` puts the type hard against the window edge and
 * ignores the gutter every other line on the site respects. The rule still runs to the
 * right edge, which is the point of it.
 */
/*
 * The number, centred on the board.
 *
 * It sat in the top-left corner, which is where an eyebrow goes when it is labelling the
 * section underneath it. This one is not labelling anything — the board IS the page and the
 * number is the whole message — so the corner made it look like a caption for a game rather
 * than the thing the page is about.
 *
 * Centred both ways, over the middle of the board, with the rule running out from it to
 * both edges. `pointer-events: none` matters more now than it did: it is sitting on top of
 * the part of the board people are most likely to draw on.
 */
.flx-404 .eyebrow {
  position: absolute;
  top: 50%;
  left: 24px;
  right: 24px;
  transform: translateY(-50%);
  z-index: 2;
  margin: 0;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 24px;
}

/* A rule either side, so the number reads as centred rather than as left-aligned type that
   happens to have a line after it. */
.flx-404 .eyebrow::before {
  content: '';
  flex: 1 1 0;
  height: 1px;
  background: var(--color-natural-30, rgba(255, 255, 255, 0.3));
}

.flx-404 .eyebrow__divider {
  flex: 1 1 0;
}

/*
 * And the controls over the bottom of it — pushed to the RIGHT, because the grid button
 * lives in the bottom-left corner of every page and two controls in one corner is two
 * controls you have to aim at.
 */
.flx-404 .life__bar {
  position: absolute;
  left: 24px;
  right: 24px;
  bottom: 24px;
  z-index: 2;
  margin: 0;
  justify-content: flex-end;
  gap: 24px;
}

/* The section carries no padding of its own any more — the board is the section. */
.flx-404 {
  padding-top: 24px;
  padding-bottom: 0;
}

@media only screen and (width <= 767px) {
  .flx-404 .life__board {
    height: calc(100svh - 108px);
    min-height: 360px;
  }

  .flx-404 .eyebrow {
    top: 16px;
    left: 16px;
    right: 16px;
  }

  .flx-404 .life__bar {
    left: 16px;
    right: 16px;
    bottom: 16px;
  }
}

/* --- The home page headline, a fifth larger --------------------------------
   One instance, not a change to the display step.

   `--fs-d` is the display size for the whole site — page headers, statements, the lot —
   and moving it would resize every one of them. This rule is the same ramp multiplied by
   1.2 and scoped to the rotating masthead, which exists on exactly one page: 36 → 43.2 at
   the narrow end, 88 → 105.6 at the wide end, and the slope between them scaled to match
   so it grows at the same rate rather than jumping at one width.

   Nothing else has to change with it. The clipping window each line travels through is
   `1.06em` and the leading is put back as `1.3em` margins — both relative to this size —
   so the whole mechanism scales with the type and the rotation still lands exactly where
   it did.                                                                                */

.flx-masthead-mid .rot-line {
  /* 20% over the display step, then another 15% on top of that: 36 and 88 became 43.2 and
     105.6, and are now 49.68 and 121.44. Written out rather than nested in calc() so the
     next person can see what the numbers are without doing arithmetic to find out.

     This selector is the whole point. `.flx-masthead-mid` is the homepage masthead and
     nothing else, so the display step is untouched everywhere it is used. */
  font-size: clamp(49.68px, 49.68px + 71.76 * (100vw - 1024px) / 416, 121.44px);

  /*
   * How many ems wide the longest line of this headline is.
   *
   * Measured from the real font: "confusing dashboards" is 10.84em in Swiza, and it is the
   * widest of the sixteen lines this masthead can show. RotatingLine.tsx re-measures after
   * the font loads and overwrites this, so an editor typing a longer phrase into the CMS
   * cannot silently break the phone layout — see the note there.
   */
  --fit: 11.06;
}

/*
 * Below 1024, the size is whatever fits.
 *
 * The clamp above only starts growing at 1024, which means every phone from a 320px SE to a
 * 430px Pro Max got the same 49.68px. At that size the longest line wants 538px of room, so
 * on a 390px screen its last two words were simply off the right-hand edge — and nothing
 * about the rule changed between a small phone and a large one, so there was no width at
 * which it came back.
 *
 * The width available is the window less the container's padding. Divide by how many ems
 * wide the longest line is and you have the largest size at which that line fits — which is
 * a different number on every device, which is the point. `min()` keeps the desktop value as
 * a ceiling, so nothing above about 590px changes at all.
 *
 * The `1.06em` clipping window and the `1.3em` leading are both relative to this, so the
 * rotation mechanism scales with it and still lands exactly where it should.
 */
@media only screen and (width <= 1023px) {
  .flx-masthead-mid .rot-line {
    font-size: min(49.68px, calc((100vw - 40px) / var(--fit, 11.06)));
  }
}

/* --- Choosing the colour of the grid --------------------------------------
   Seven of the site's own accents rather than a colour wheel. A wheel lets somebody pick
   a grey on a grey and decide the tool is broken, and it puts a colour on the site that
   the site does not own. Six accents and white is a choice; sixteen million is a support
   question.

   The pick is written onto the root as `--gd-xray-accent`, which every rule in the x-ray
   already reads — so the columns, the outlines, the readout's border, the numbers and the
   button all follow from one property.                                                  */

/* The one in use gets a ring rather than a tick — at 14px a tick is a smudge. */
/* --- The cheat code -------------------------------------------------------
   The page dissolves into the board. See KonamiLife.tsx — the interesting part is that
   the first generation turns every element on the page into its own outline, which nobody
   drew: a solid block of cells dies in the middle and survives at the edges, because eight
   neighbours is overcrowding and three is not.                                          */

.konami {
  position: fixed;
  inset: 0;
  z-index: 5500;
  /* Above the header, because the header is part of what gets eaten. */
}

.konami__canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.konami__hint {
  position: absolute;
  left: var(--edge, 20px);
  bottom: var(--edge, 20px);
  margin: 0;
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--color-natural-45, rgba(255, 255, 255, 0.45));
  pointer-events: none;
}

/* The page underneath goes, but is never touched — one property, and everything comes
   back exactly as it was because nothing about it changed. */
html.gd-konami body > *:not(.konami) {
  opacity: 0.06;
  transition: opacity 0.6s ease;
}

html.gd-konami {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html.gd-konami body > *:not(.konami) {
    transition: none;
  }
}

/* --- The idle glider ------------------------------------------------------
   Fades in when the page has been still for a minute, fades out the moment anything
   moves. It never takes a pointer event, so it walks over the page without ever being in
   the way of it.                                                                        */

.idle-glider {
  position: fixed;
  inset: 0;
  z-index: 4100;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.2s ease;
}

.idle-glider.is-walking {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .idle-glider {
    display: none;
  }
}

/* --- The x-ray, level two: the system, labelled ---------------------------
   Every value in these labels is read off the page and matched against the tokens as the
   browser resolved them — see XrayGrid.tsx. A label that could disagree with the thing it
   labels is worse than no label, so none of these numbers are written down anywhere.

   Three kinds, three positions, so they never stack on each other: the type step at the
   top-left of the element it describes, the surface token at the bottom-left, the gap
   centred on the top edge.                                                              */

/* The speed instrument. Fixed to the corner opposite the grid button, drawn in the
   x-ray's own language: same chip, same accent, tabular numbers. */
.xray__speed {
  position: fixed;
  right: var(--edge, 20px);
  bottom: var(--edge, 20px);
  z-index: 4300;
  padding: 8px 12px;
  background: color-mix(in srgb, var(--bg-color, #000) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 45%, transparent);
  border-radius: 4px;
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--gd-xray-accent, #36ffae);
  pointer-events: none;
}


@media only screen and (width <= 899px) {
  /* On a phone the line is wider than the screen; it spans the bottom instead,
     clear of the grid button in the corner. */
  .xray__speed {
    left: var(--edge, 20px);
    right: var(--edge, 20px);
    bottom: calc(var(--edge, 20px) + 64px);
    white-space: normal;
    text-align: center;
  }
}

.xray__note {
  position: absolute;
  z-index: 2;
  transform: translateY(-100%);
  padding: 2px 5px;
  background: var(--bg-color, #000);
  border: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 45%, transparent);
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--gd-xray-accent, #36ffae);
  pointer-events: none;
}

.xray__note--colour {
  transform: none;
  opacity: 0.75;
}

.xray__note--gap {
  transform: translate(-50%, -100%);
  opacity: 0.8;
}

/*
 * The unflattering ones.
 *
 * A gap that is not on the spacing scale, or a block whose outer edge does not land on a
 * column line. Dashed rather than a second colour, because the brief was one colour and
 * because a dashed border already means "provisional" to anybody who has ever drawn a
 * layout. It reads as a warning without adding a palette to a page about restraint.
 */
.xray__note--off {
  transform: translate(-50%, -100%);
  border-style: dashed;
  border-color: var(--gd-xray-accent, #36ffae);
  opacity: 1;
}

[data-xray-off] {
  outline: 1px dashed color-mix(in srgb, var(--gd-xray-accent, #36ffae) 70%, transparent);
  outline-offset: 0;
}

/*
 * The readout, at the cursor.
 *
 * There was a bar in the corner reciting six numbers, and it was ignorable precisely
 * because it was in the corner: a measurement is only useful next to the thing it
 * measures. This follows the pointer and names whatever part of the grid is under it.
 */
.xray__cursor {
  position: fixed;
  z-index: 4200;
  transform: translate(14px, 14px);
  padding: 4px 8px;
  border: 1px solid color-mix(in srgb, var(--gd-xray-accent, #36ffae) 55%, transparent);
  background: var(--bg-color, #000);
  color: var(--gd-xray-accent, #36ffae);
  font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
  font-size: 11px;
  line-height: 1.3;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
}

/* A pointer is a mouse. On a touch screen there is nothing hovering and a label pinned
   where a finger last was is just litter. */
@media (hover: none) {
  .xray__cursor { display: none; }
}

/* --- Why we might be wrong for you ----------------------------------------
   One page, one piece of writing, no pictures. The only place on the site where a full
   screen of text is the correct design rather than a section that has lost its images.

   Every line runs through the word scrub, which is doing real work here: it paces the
   reading. A list of eight reasons not to hire a studio, all arriving at full contrast at
   once, reads as a stunt. The same list revealed at the speed the reader scrolls reads as
   somebody being straight with you.                                                     */

/*
 * One 700px column, centred on the page, with every line starting at the same left edge.
 *
 * The page was built as a wide display heading with the reasons indented to a column on the
 * right, which is the shape the rest of the site uses for a body of copy under a full-width
 * title. This page is not that: it is one continuous piece of writing that happens to have
 * eight subheadings in it, and a reader going down it was tracking left and right between a
 * heading at one margin and a list at the other.
 *
 * Centred as a block, not centred as text. Every line still starts at the same left edge,
 * which is the only arrangement that stays readable when the lines are different lengths.
 */
.flx-wrong .grid-container {
  display: flex;
  flex-direction: column;
  gap: 96px;
  max-width: 700px;
  margin-inline: auto;
  text-align: left;
}

/* Everything inside it takes the column, so nothing is pushed to a margin that no longer
   exists. The `span-7` and `margin-left: auto` below were positioning against the old
   twelve-column shape. */
.flx-wrong .grid-container > *,
.flx-wrong .wrong__list,
.flx-wrong .wrong__sub,
.flx-wrong .wrong__end {
  width: 100%;
  max-width: none;
  margin-left: 0;
  margin-right: 0;
}

/* The heading takes the same measure as everything else rather than a 20ch one, because at
   700px the line breaks are the column's job. */
.flx-wrong .flx-wrong__lede {
  max-width: none;
}

.flx-wrong__lede {
  margin: 0;
  max-width: 20ch;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-d);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

/* The reasons sit in a reading column against the right of the grid, the way the rest of
   the site indents a body of copy under a full-width heading. */
.wrong__list {
  display: flex;
  flex-direction: column;
  gap: 64px;
  margin-left: auto;
}

/* The set-up line. Sits in the same reading column as the reasons so the eye lands where
   the list is about to start, rather than under the middle of the display heading. */
.wrong__sub {
  margin: -48px 0 0;
  font-size: var(--fs-xl);
  line-height: 1.5;
  color: var(--color-natural-70);
}

.wrong__item {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.wrong__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

.wrong__text {
  margin: 0;
  font-size: var(--fs-l);
  line-height: 1.6;
  letter-spacing: 0.01em;
  /* The scrub takes it from dim to bright as it passes; this is the colour it ends at. */
  color: var(--color-natural-70);
}

.wrong__end {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
  margin-left: auto;
}

.wrong__close {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h2);
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

@media only screen and (width <= 899px) {
  .flx-wrong .grid-container {
    gap: 56px;
  }

  .wrong__list {
    gap: 48px;
  }

  .wrong__sub {
    margin: -24px 0 0;
  }
}

/* --- The brief writer ------------------------------------------------------
   Figma 3719:73880. Six questions, one to a screen, centred in a 584 column under a
   full-width eyebrow. Every number here is a variable out of the design file.

   The controls are the contact form's own — same field, same notched label, same pills —
   defined once and worn by both. Only the arrangement below is local.                    */

.flx-brief .grid-container {
  display: flex;
  flex-direction: column;
  gap: 80px;
  padding-top: 32px;
  padding-bottom: 208px;
}

.brief__eyebrow {
  display: flex;
  align-items: center;
  gap: 24px;
  width: 100%;
  padding-left: 4px;
}

.brief__eyebrow-text {
  flex: 0 0 auto;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
}

.brief__eyebrow-rule {
  flex: 1 1 auto;
  height: 1px;
  background: rgba(255, 255, 255, 0.3);
}

/* The column. 584 is the design's, and it is deliberately narrower than the seven columns
   the rest of the site reads at: a single question wants to be read in one glance, not
   scanned across a measure. */
/* --- The chat skin -----------------------------------------------------------
   The brief writer presents as a conversation: Grid speaks in the page's own type
   behind the mint dot, the visitor's answers sit in quiet cards on the right, the
   options are suggestion chips that answer on click, and the input is a bar fixed
   to the bottom of the viewport. The sticky CTA stays off this page (StickyCta.tsx)
   so the two never fight over the same edge. */

/* Doubled class to outrank the base .brief max-width, which is written below this. */
.brief.brief--chat {
  max-width: 720px;
  gap: 24px;
}

.chat {
  display: flex;
  flex-direction: column;
}

.chat__msg {
  display: flex;
  flex-direction: column;
  margin: 0 0 36px;
}

.chat__msg--ai {
  align-items: flex-start;
}

.chat__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--color-natural-40, rgba(255, 255, 255, 0.4));
}

.chat__meta::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #36ffae;
}

.chat__q {
  max-width: 56ch;
  color: var(--color-natural-100, #fff);
  font-size: 24px;
  line-height: 1.4;
}

.chat__q p {
  margin: 0;
}

.chat__lede {
  margin: 14px 0 0;
  max-width: 56ch;
  font-size: 14px;
  color: var(--color-natural-70, rgba(255, 255, 255, 0.7));
}

.chat__msg--you {
  align-items: flex-end;
}

.chat__bubble {
  max-width: 48ch;
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
  padding: 12px 16px;
  font-size: 15px;
  white-space: pre-wrap;
}

.chat__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: -8px 0 8px;
}

.chat__chip {
  border: 1px solid var(--border-default);
  background: none;
  color: var(--color-natural-100, #fff);
  border-radius: 99999px;
  padding: 9px 18px;
  font-size: 14px;
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.25s ease;
}

/* Focus and active as well as hover: Foundation's `button:hover`, `:focus` and
   `:active` each outrank one class and would turn these into white blocks. */
.chat__chip:hover,
.chat__chip:focus,
.chat__chip:active,
.chat__chip:focus-visible {
  border-color: var(--border-hover, rgba(255, 255, 255, 0.4));
  background: rgba(255, 255, 255, 0.06);
  color: var(--color-natural-100, #fff);
}

/* In the flow, directly under the conversation, not pinned to the viewport — a fixed
   bar fought the footer on a short page and read as a widget stuck over the site. */
.chat__bar {
  margin-top: 8px;
}

.chat__bar-inner {
  max-width: 720px;
}

.chat__field {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-s);
  padding: 8px 8px 8px 16px;
}

.chat__field:focus-within {
  border-color: var(--border-hover, rgba(255, 255, 255, 0.4));
}

/*
 * Foundation paints a bare textarea white, light grey on focus and on disabled, and a
 * pseudo-classed element selector outranks one class — which is how the answer box
 * shipped as a white slab on a black page. Every state is restated here at two
 * classes deep, so nothing of Foundation's form theatre survives.
 */
.chat__field .chat__input,
.chat__field .chat__input:focus,
.chat__field .chat__input:disabled {
  flex: 1;
  background: none;
  border: 0;
  outline: 0;
  box-shadow: none;
  resize: none;
  color: var(--color-natural-100, #fff);
  font: inherit;
  font-size: 15px;
  line-height: 1.5;
  height: auto;
  min-height: 40px;
  margin: 0;
  padding: 8px 0;
  max-height: 120px;
  cursor: text;
}

.chat__field .chat__input:disabled {
  opacity: 0.5;
  cursor: default;
}

.chat__field .chat__input::placeholder {
  color: var(--color-natural-40, rgba(255, 255, 255, 0.4));
}

.chat__send {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 8px 20px;
  border: 0;
  border-radius: 99999px;
  background: var(--color-natural-100, #fff);
  color: #000;
  font-size: 14px;
  cursor: pointer;
  white-space: nowrap;
}

.chat__send:hover,
.chat__send:focus,
.chat__send:active {
  background: var(--color-natural-100, #fff);
  color: #000;
}

.chat__send:disabled {
  opacity: 0.4;
  cursor: default;
}

.chat__send svg {
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

.chat__send:hover:not(:disabled) svg {
  transform: translateX(3px);
}

.chat__under {
  display: flex;
  gap: 24px;
  margin: 10px 4px 0;
}

.chat__back,
.chat__retry {
  background: none;
  border: 0;
  padding: 0;
  font-size: 12px;
  color: var(--color-natural-40, rgba(255, 255, 255, 0.4));
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.chat__back:hover,
.chat__back:focus,
.chat__back:active,
.chat__retry:hover,
.chat__retry:focus,
.chat__retry:active {
  background: none;
  color: var(--color-natural-100, #fff);
}

/* Doubled up to outrank Foundation's grey `[type=button]:disabled`. */
.chat__under .chat__back:disabled {
  background: none;
  color: var(--color-natural-40, rgba(255, 255, 255, 0.4));
  opacity: 0.4;
  cursor: default;
  text-decoration: none;
}

@media only screen and (width <= 640px) {
  .chat__q {
    font-size: 19px;
  }

  .chat__send span {
    display: none;
  }

  .chat__send {
    width: 40px;
    padding: 8px;
    justify-content: center;
  }
}

/* The transcript and the bar are conversation furniture; the printed page is the
   document alone, which the print stylesheet already isolates by .brief__doc. */
@media print {
  .chat,
  .chat__bar,
  .chat__chips {
    display: none;
  }
}

.brief {
  display: flex;
  flex-direction: column;
  gap: 48px;
  width: 100%;
  max-width: 584px;
  margin-inline: auto;
  min-width: 0;
}

.brief__head {
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: center;
}

.brief__count {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
  font-variant-numeric: tabular-nums;
}

/* One h1 that changes rather than a heading per step. `text-wrap: balance` because a
   centred question that leaves one word on the last line looks like a mistake. */
.brief__q {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h1);
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: #f4f4f4;
  text-wrap: balance;
}

.brief__lede {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h6);
  line-height: 1.5;
  color: #f4f4f4;
}

.brief__form-wrap {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
  min-width: 0;
}

/* 36 between the field and the buttons, which is the file's `spacing/xl-5`. */
.flx-brief .cform__field--area {
  margin-bottom: 20px;
}

/* The design file draws this at 8. The site is 4 everywhere and stays 4 everywhere: one
   number, no exceptions, and a control that matches every other control on the site is
   worth more than a control that matches one frame. */
.flx-brief .cform__control--area {
  height: 133px;
  padding: 24px 16px;
}

.brief__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
}

.brief__actions--one {
  justify-content: center;
}

.brief__back {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 1px solid #333;
  border-radius: 9999px;
  background: transparent;
  color: #fff;
  cursor: pointer;
  transition: opacity 0.2s ease, border-color 0.2s ease;
}

.brief__back:disabled {
  opacity: 0.25;
  cursor: default;
}

.brief__back:not(:disabled):hover {
  border-color: #fff;
}

.brief__next {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 52px;
  padding: 8px 20px;
  border: 1px solid transparent;
  border-radius: 9999px;
  background: #fff;
  color: #000;
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

/* Outlined rather than dimmed while there is nothing to send. A white pill at low opacity
   is mid-grey with black text on it, which is unreadable, and unreadable is a poor way to
   say "not yet". */
.brief__next:disabled {
  background: transparent;
  border-color: var(--border-default);
  color: var(--color-natural-50, rgb(255 255 255 / 0.5));
  cursor: default;
}

.brief__next svg {
  flex: 0 0 auto;
  transition: transform 0.25s ease;
}

.brief__next:not(:disabled):hover svg {
  transform: translateX(3px);
}

.brief__skip {
  align-self: center;
  margin-top: 8px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-natural-50, rgb(255 255 255 / 0.5));
  font-family: inherit;
  font-size: var(--fs-sm);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color 0.2s ease;
}

.brief__skip:hover { color: #fff; }

/* ---- writing ---- */

/* --- The draw-in ------------------------------------------------------------
   The brief writer presents like the writing assistants people know: questions and
   the finished document arrive word by word (BriefChat's Stream component), the wait
   states shimmer through the words themselves, and sections of the document that
   have not had their turn hold their space invisibly so nothing reflows. All of it
   in the page's own type — no imported component kit. */

.stream__caret {
  display: inline-block;
  width: 0.5em;
  height: 0.95em;
  margin-left: 3px;
  vertical-align: -0.08em;
  background: currentColor;
  animation: gd-caret 1s steps(1) infinite;
}

@keyframes gd-caret {
  50% { opacity: 0; }
}

.brief__shimmer {
  background: linear-gradient(
    90deg,
    color-mix(in srgb, currentColor 35%, transparent) 30%,
    currentColor 50%,
    color-mix(in srgb, currentColor 35%, transparent) 70%
  );
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: gd-shimmer 1.5s linear infinite;
}

@keyframes gd-shimmer {
  to { background-position: -200% 0; }
}

.brief__pending {
  opacity: 0;
}

.stream-li {
  opacity: 0;
  transform: translateY(6px);
  animation: gd-svc-in 0.4s cubic-bezier(0.26, 1, 0.48, 1) forwards;
  animation-delay: calc(var(--d, 0) * 140ms);
}

.brief__after {
  animation: gd-svc-in 0.5s cubic-bezier(0.26, 1, 0.48, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .stream__caret { display: none; }
  .brief__shimmer {
    animation: none;
    background: none;
    -webkit-text-fill-color: currentcolor;
  }
  .stream-li,
  .brief__after {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* Printing mid-performance still prints the whole document. */
@media print {
  .brief__pending { opacity: 1; }
  .stream-li { animation: none; opacity: 1; transform: none; }
}

.brief__dots {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
}

.brief__dots i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-natural-50, rgb(255 255 255 / 0.5));
  animation: brief-pulse 1.1s ease-in-out infinite;
}

.brief__dots i:nth-child(2) { animation-delay: 0.15s; }
.brief__dots i:nth-child(3) { animation-delay: 0.3s; }

@keyframes brief-pulse {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}

/* ---- the document ---- */

/* The brief is a document rather than a question, so it takes the wider measure the rest
   of the site reads at instead of the narrow one a single sentence wants. */
.brief--doc {
  max-width: 46rem;
  gap: 32px;
}

.brief__doc {
  /*
   * The site's card, at the site's radius.
   *
   * This was a 16px radius with letterspaced uppercase labels and the x-ray's green on one
   * heading — none of which appear anywhere else on the site. Four is the radius, always.
   * The green is a debug colour borrowed from a tool, and using it as an editorial
   * highlight makes the document look like it came from a different studio than the one
   * it is introducing.
   */
  padding: 48px;
  border: 1px solid var(--border-default);
  border-radius: 4px;
  background: var(--surface-card, #0f0f0f);
  min-width: 0;
}

/* The eyebrow, doing what it does everywhere else: name the section, rule to the right. */
.brief__label {
  margin: 0 0 16px;
}

.brief__part .brief__label {
  margin-bottom: 12px;
}

.brief__headline {
  margin: 0 0 8px;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.brief__part { margin-top: 48px; }

.brief__part p,
.brief__part li {
  margin: 0 0 10px;
  font-size: var(--fs-r);
  line-height: 1.65;
  color: var(--color-natural-70);
  overflow-wrap: anywhere;
}

/*
 * The reframe, carried by size and colour rather than by a highlight.
 *
 * It is the reason anybody keeps this document, and a page where every section looks
 * equally important has no argument in it. The site says "this matters" the same way in
 * every other section: bigger, in the heading face, in white.
 */
.brief__part--key p {
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h4);
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

/* The services panel's covers list: a rule between rows, no bullet. The rule already does
   the separating, and a bullet as well is the same job done twice. */
.brief__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.brief__list li {
  margin: 0;
  padding: 12px 0;
  border-bottom: 1px solid var(--border-default);
}

.brief__list li:first-child {
  padding-top: 0;
}

.brief__list li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

.brief__shape {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.brief__shape li {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
}

.brief__shape strong {
  color: var(--color-natural-100, #fff);
  font-weight: 400;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h6);
}

.brief__part--doubt a {
  font-size: var(--fs-sm);
  color: var(--color-natural-70);
  text-underline-offset: 3px;
}

.brief__small {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--color-natural-50, rgb(255 255 255 / 0.5));
}

.brief__doc .brief__small {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--border-default);
}

/* ---- the details, asked for last ---- */

.brief__form,
.brief__done {
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-items: stretch;
  min-width: 0;
}

.brief__form h3,
.brief__done h3 {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.brief__intro,
.brief__done p {
  margin: 0;
  max-width: 48ch;
  font-size: var(--fs-r);
  line-height: 1.6;
  color: var(--color-natural-70);
}

@media only screen and (width <= 899px) {
  .flx-brief .grid-container {
    gap: 48px;
    padding-bottom: 96px;
  }

  .brief__doc { padding: 24px; }
}

@media (prefers-reduced-motion: reduce) {
  .brief__dots i {
    animation: none;
    opacity: 0.6;
  }
}

/* The field nobody can see. Off-screen rather than display:none, because the crude
   form-fillers this is aimed at do skip hidden fields, and out of the tab order and the
   accessibility tree so nobody navigating by keyboard or screen reader ever meets it. */
.brief__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* --- The other way in ------------------------------------------------------
   Figma 3719:73536, "AI brief writer". Sits in the contact page's left column, which was
   empty. It is the route we would rather people took, so it comes first in the reading
   order and the form beside it becomes the fallback rather than the only option.

   Every number below is a variable out of the design file rather than a measurement taken
   off a picture of it: 48 padding, 40 between the three groups, 16 inside the body, 8 in
   the rows, 16 radius, 52 button. Where the file names a token this uses ours by the same
   value, so a change to the type scale still moves it.                                   */

.brief-cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
  width: 100%;
  padding: 48px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: var(--radius-s);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.25s ease, background 0.25s ease;
}

/* The whole card is the link, so the whole card responds. The button inside is the
   affordance; this is the hit area. */
.brief-cta:hover,
.brief-cta:focus-visible {
  border-color: #fff;
  background: rgba(255, 255, 255, 0.02);
}

/* -- eyebrow: label and a rule out to the edge, the way every section here draws one -- */

.brief-cta__eyebrow {
  display: flex;
  align-items: center;
  gap: 24px;
  width: 100%;
  padding-left: 4px;
}

.brief-cta__eyebrow-text {
  flex: 0 0 auto;
  font-size: var(--fs-sm);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
}

.brief-cta__eyebrow-rule {
  flex: 1 1 auto;
  height: 1px;
  background: rgba(255, 255, 255, 0.3);
}

/* -- the body -- */

.brief-cta__body {
  display: flex;
  flex-direction: column;
  gap: 28px;
  width: 100%;
}

/* The sentence and the list are one group inside the body, 16 apart, so the 28 above only
   separates them from the heading. */
.brief-cta__inner {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
}

.brief-cta__title {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  /* h2 rather than h1: the card sits beside a form now instead of carrying the left half
     of the page on its own, and 44 next to a column of 48px fields shouted. */
  font-size: var(--fs-h2);
  line-height: 1.3;
  letter-spacing: -0.01em;
  /* primary/950 in the file. Not pure white: against a black card the full 255 rings. */
  color: #f4f4f4;
}

/* Both the opening line and the closing one. Set in the heading face at h6 rather than in
   body copy, which is what stops the card reading as a paragraph with a list in it. */
.brief-cta__lede {
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h6);
  line-height: 1.5;
  color: #f4f4f4;
}

.brief-cta__list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}

.brief-cta__row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-r);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--color-natural-80, rgba(255, 255, 255, 0.8));
}

/* -- the button -- */

.brief-cta__go {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 52px;
  padding: 8px 20px;
  border-radius: 9999px;
  background: #fff;
  color: #000;
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1;
}

.brief-cta__go svg {
  flex: 0 0 auto;
  transition: transform 0.25s ease;
}

.brief-cta:hover .brief-cta__go svg {
  transform: translateX(3px);
}

@media only screen and (width <= 899px) {
  .brief-cta {
    padding: 24px;
  }
}

.cform__left {
  display: flex;
  flex-direction: column;
  gap: 32px;
  min-width: 0;
}

@media only screen and (width <= 899px) {
  .brief-cta {
    max-width: none;
    padding: 24px;
  }
}

/* --- The beat ---------------------------------------------------------------
   A pause between the promise and the list. Held in the same reading column as the reasons
   so it reads as part of the argument rather than as an illustration parked beside it, and
   deliberately small: a punchline, not a hero image.                                     */

/* The figure takes the seven columns the reasons take, so the dog starts on the same line
   they do. The clip itself is capped inside it: a 320px punchline in a 780px column reads
   as an aside, which is what it is. */
.meme {
  margin: 0 0 0 auto;
}

.meme__clip {
  display: block;
  width: 100%;
  max-width: 320px;
  height: auto;
  border-radius: 12px;
  background: var(--surface-card, #0f0f0f);
}

.meme__credit {
  margin-top: 8px;
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  color: var(--color-natural-50, rgb(255 255 255 / 0.5));
}

.meme__credit a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
}

.meme__credit a:hover,
.meme__credit a:focus-visible {
  color: var(--color-natural-100, #fff);
}

@media only screen and (width <= 899px) {
  .meme {
    margin-left: 0;
  }

  .meme__clip {
    max-width: 240px;
  }
}

/* --- The eyebrow, closer on a phone -----------------------------------------
   The gap under an eyebrow is 80 by default and 96 on the logo grid, both drawn for a
   desktop frame. On a phone that is most of a screen of nothing between a two-word label
   and the heading it belongs to, and the two stop reading as a pair.

   Halved, and only halved: the two sections that already sit tight on purpose are excluded
   by name rather than swept up, because a blanket rule here would have made the smallest
   gaps on the site bigger while claiming to make them smaller.                            */

@media only screen and (width <= 899px) {
  .flx-section:not(.flx-case-text):not(.flx-404) .eyebrow {
    margin-bottom: 40px;
  }

  .flx-text-logos .eyebrow {
    margin-bottom: 48px;
  }
}

/* --- Cards you can still see at 25% brightness -------------------------------
   `--surface-card` is #0f0f0f on a black ground: a contrast ratio of about 0.4:1, which is
   invisible on a phone with the brightness down and marginal on any screen in daylight. A
   card nobody can see is not a card, it is a paragraph with padding.

   There is no way to read the device's brightness from a page and there should not be: it
   is a fingerprinting signal, and every browser has closed that door. So the fix belongs in
   the card rather than in a measurement of the room. A hairline at 10% white costs nothing,
   changes nothing at full brightness, and is the difference between a visible edge and no
   edge at all when the screen is dim.                                                     */

/*
 * ON A PHONE ONLY, and that is a correction rather than a caveat.
 *
 * These went on everywhere, and on a laptop they are a stroke nobody asked for on a card
 * the design draws without one. The problem they solve is a phone held at low brightness:
 * a #0f0f0f card on black is about 0.4:1 and simply is not there. A desk screen at normal
 * brightness has no such problem, so the fix does not belong there either.
 *
 * The rest of the reasoning stands: reading the device's brightness is impossible and
 * should stay impossible, so the answer lives in the card rather than in a measurement of
 * the room. It just lives in the card on the device that needs it.
 */
@media only screen and (width <= 899px) {
  .quote-card,
  .footer-card,
  .connect-row,
  .stat-row,
  .gallery-item,
  .menu-work__pic,
  .flx-text-logos .logo-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
  }
}

/* The ones that already carry a border of their own keep it. */
.brief-cta,
.brief__doc {
  border-color: rgba(255, 255, 255, 0.4);
}


/* --- Snapped to the grid ---------------------------------------------------
   Every measure below was picked by eye in the imported theme and lands between two column
   lines, which is the one width nothing else on the page agrees with. Each is rounded UP
   to the next whole column: rounding up keeps the line length the design chose rather than
   shortening it, and the block ends on a line every other section also ends on.

   Desktop only. Below 1024 the container is narrower than the measure and these would be
   wider than the space they sit in.                                                      */
@media only screen and (width >= 1024px) {
  /* 689 was seven columns at the 1216 the frame was drawn at, and seven columns of nothing
     at any other width. Six of the container, which is 724 at 1536 and correct at all of
     them. The inner box carries no horizontal padding at this breakpoint (see the timeline
     note above), so `100%` here is the container. */
  .timeline-item {
    width: calc((100% - 11 * 48px) / 12 * 6 + 5 * 48px);
    max-width: 100%;
  }

  /*
   * The sticky heading beside the belief cards.
   *
   * Two things were wrong and only one of them was the width. The outer box was on the
   * theme's `calc(41.6667% - 28px)`, which is 539 at this container and 24 short of a line.
   * That is the one I fixed last time.
   *
   * The other is why "What we believe" was breaking over two lines: the sticky box inside
   * it is shrink-to-fit, so it had collapsed to 196px, a quarter of the room it was given.
   * The heading was not too long for its column, it was in a box that had quietly shrunk to
   * the width of the longest word.
   */
  .flx-text-cards__title,
  .flx-text-cards .js-sticky-title {
    max-width: calc((100% - 11 * 48px) / 12 * 5 + 4 * 48px);
  }

  .flx-text-cards .js-sticky-title {
    width: 100%;
  }
}


/* --- Reverted, and the reasoning that was wrong ------------------------------
   These went to a 48 gutter on the argument that every edge should land on a column line.
   That argument does not apply here and the result was visibly worse: the footer's cards
   span the container to fill it, so opening 48 between them tore one band into four boxes
   with holes between.

   A card row that fills the width crosses the column lines on purpose. Sixteen, from
   `--card-gap`, which is where that decision now lives for every row like it.             */
.footer-new__cards,
.footer-new__cards-wrap {
  gap: var(--card-gap, 16px);
}


/* --- `hidden` has to actually hide ------------------------------------------
   The work filters set `card.hidden = true` on the cards that do not match, which is the
   right way to do it: it takes them out of the accessibility tree and out of tab order as
   well as off the screen. It did nothing.

   `[hidden] { display: none }` comes from the browser's own stylesheet, and any author rule
   with a class beats it. `.work-tile { display: flex }` therefore won, so every filtered
   card stayed on screen while being invisible to a screen reader — the worst of both. The
   "More coming soon" tile has been rendered on the Work page the whole time for the same
   reason: it ships with `hidden` set and has been visible since the day it was added.

   One rule, at the end of the sheet, where it beats a class on equal specificity. It says
   the same thing the browser already says; it just says it late enough to be heard.

   `until-found` is excluded because that value means "hidden, but findable by browser
   search, and revealed when found" — a thing that has to stay in the layout to work.      */
[hidden]:not([hidden='until-found']) {
  display: none;
}


/* --- What the cursor promises -----------------------------------------------
   A drag row sets `cursor: grab` on everything inside it, which is right for the row and
   wrong for the things in it you can actually click: the pointer said "drag me" while the
   element underneath was a link. The two rules below take it back for exactly those parts.

   Specific rather than blanket, because the rest of the card SHOULD still say grab — the
   card is draggable, and only the link inside it is not.                                  */
.work-card__head,
.work-card__head *,
.work-tile__head,
.work-tile__head *,
.drag-row.is-draggable .work-card__head,
.drag-row.is-draggable .work-card__head * {
  cursor: pointer;
}

.quote-card__go,
.quote-card__go *,
.drag-row.is-draggable .quote-card__go,
.drag-row.is-draggable .quote-card__go * {
  cursor: pointer;
}


/* --- A minute of stillness --------------------------------------------------
   Fills the window, sits above everything including the menu and the header, and goes the
   instant anything moves. See IdleRick.tsx for the rules about when it is allowed to.

   `object-fit: cover` because the clip's ratio is not the window's and letterboxing a joke
   makes it look like a mistake. The fade is on the way in only: coming in slowly reads as
   arriving, and going out slowly would read as lag on a gesture the visitor just made.    */
.rick {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: #000;
  animation: gd-rick-in 0.4s ease both;
  /* It takes the pointer so that the click which dismisses it does not also land on
     whatever was underneath. */
  cursor: pointer;
}

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

/* The mark, huge and centred, held against the film's bright frames by a soft shadow
   rather than a scrim - a scrim would dim the joke. */
.rick__logo {
  position: absolute;
  inset: 0;
  margin: auto;
  width: min(560px, 64vw);
  height: auto;
  filter: drop-shadow(0 8px 56px rgba(0, 0, 0, 0.55));
}

/* The small print. One line, bottom centre, same fade as everything else. */
.rick__note {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 28px;
  margin: 0;
  text-align: center;
  font-size: 14px;
  line-height: 1.5;
  color: #fff;
  opacity: 0.85;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.6);
}

@keyframes gd-rick-in {
  from { opacity: 0 }
  to { opacity: 1 }
}


/* --- Small print ------------------------------------------------------------
   No transform. There was one, and it was wrong twice over: it was the wrong case, and
   `::first-letter` does not apply to an inline element, so three of the four links came out
   entirely lowercase while the first one did not. Case belongs in the words, where an editor
   can see it and change it, not in a rule that rewrites them on the way to the screen.     */


/* --- The picture becomes the page -------------------------------------------
   The travelling half is named in script, on the image inside the card that was clicked —
   see HeroMorph.tsx. This is the other end of the pair: the picture at the top of a case
   study, which is what it grows into.

   Exactly one element may carry a given name at a time. Two is not a fallback, it is an
   error the browser reports by refusing to animate anything at all, which is why this is
   scoped to the first section of a case study rather than to every hero-shaped image on
   the site.

   If the selector matches nothing — a study whose first section is not a picture band —
   there is simply no partner waiting, and the card's image fades instead of travelling.
   Degrading to a crossfade is the right failure for a decoration.                        */
.case-study .flx-section:first-of-type .pic-band__cell--wide img {
  view-transition-name: gd-hero;
}

/*
 * `contain` on both halves, because the two images are cropped differently.
 *
 * The card is a 389:500 portrait and the hero is a wide band, so the browser is tweening
 * between two boxes with different aspect ratios. Left alone it stretches the screenshots
 * to fit, and a face going through that arrives back at the right shape having been briefly
 * the wrong one. Contained, the picture keeps its proportions the whole way across and the
 * box changes around it.
 */
::view-transition-old(gd-hero),
::view-transition-new(gd-hero) {
  object-fit: contain;
  /* No fade on the thing that is travelling. The whole point is that it is one object
     moving, not one object leaving while another arrives in the same place. */
  mix-blend-mode: normal;
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.26, 1, 0.48, 1);
}

/* The rest of the page: a plain crossfade, quicker than the morph so the picture is the
   thing the eye follows rather than one of several things moving at once. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.28s;
}

/* Asked for less movement: the script never starts a transition, and this is belt and
   braces for a browser that runs one anyway. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}


/* --- Width and height attributes need `height: auto` ------------------------
   The images now carry their intrinsic `width` and `height`, so the browser can reserve
   the right shape before the file arrives. That is worth having and it comes with one trap,
   which this rule closes.

   The imported foundation stylesheet declares `img { max-width: 100% !important }` and says
   nothing about height. On its own that was harmless, because an image with no attributes
   takes its shape from the file. With the attributes present it is not: in any container
   narrower than the picture, `max-width` shrinks the WIDTH while the height attribute holds
   the HEIGHT at its full number, and the picture squashes sideways. Every photograph on
   every phone, at the wrong aspect ratio.

   `height: auto` restores the ratio: the height follows whatever the width ends up being.
   Specificity keeps it out of the way of everything that sets its own height — a class
   beats a bare element selector, so `object-fit: cover` frames, avatars and logos are all
   untouched and keep the crops they were given.                                          */
img {
  height: auto;
}


/* --- The skip link and the privacy line -------------------------------------
   The skip link is the visually-hidden-until-focused pattern: parked off-screen, and the
   moment a keyboard reaches it, it appears as a small pill in the top corner, above
   everything. It must never be `display: none` — that removes it from the tab order,
   which is the whole point of it.                                                        */
.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 9500;
  padding: 10px 16px;
  border-radius: 4px;
  background: var(--color-natural-100, #f4f4f4);
  color: #000;
  font-size: var(--fs-sm);
  text-decoration: none;
  transform: translateY(-200%);
}

.skip-link:focus-visible {
  transform: none;
}

/* Small print under the forms: quiet, but present exactly where the hesitation is. */
.cform__privacy {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--color-natural-50, rgba(255, 255, 255, 0.5));
}

.cform__privacy a {
  color: inherit;
  text-underline-offset: 3px;
}


/* --- Insights: the reading list ---------------------------------------------
   A contents page, not a shop. Each row is the site's two-column shape: meta in the first
   three columns, the title and excerpt taking the reading measure from column four —
   the same split the FAQs use, so the two pages rhyme.

   The whole row is one link, stretched from the title's anchor, with the row's hover
   carrying the same 0.25s the rest of the site moves at.                                 */
.flx-insights .grid-container {
  display: flex;
  flex-direction: column;
  gap: 64px;
}

.flx-insights .eyebrow {
  margin-bottom: 0;
}

.insights__head {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.insights__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-d);
  line-height: 1.15;
  letter-spacing: -0.02em;
  max-width: calc((100% - 11 * 48px) / 12 * 8 + 7 * 48px);
}

.insights__lede {
  margin: 0;
  font-size: var(--fs-xl);
  line-height: 1.5;
  color: var(--color-natural-70);
  /* Six columns of reading measure on desktop. The calc collapses to a sliver once the
     48px gutters outweigh a phone's width, so below the grid it simply takes the line. */
  max-width: calc((100% - 11 * 48px) / 12 * 6 + 5 * 48px);
}

@media only screen and (width <= 899px) {
  .insights__lede {
    max-width: none;
  }
}

.insights__filters {
  display: flex;
  gap: 28px;
}

.insights__list {
  display: flex;
  flex-direction: column;
}

.post-row {
  position: relative;
  display: grid;
  /* Meta on three columns, the reading measure in the middle, the image on the last
     two - each edge of the row sits on the grid. A row with no image simply lets the
     text take the space; the template only renders the third cell when there is one. */
  grid-template-columns: calc((100% - 11 * 48px) / 12 * 3 + 2 * 48px) minmax(0, 1fr) calc((100% - 11 * 48px) / 12 * 2 + 48px);
  gap: 48px;
  padding: 48px 0;
  border-top: 1px solid var(--border-default);
  transition: border-color 0.25s ease;
}

/* Without an image the last track collapses instead of leaving a hole. */
.post-row:not(:has(.post-row__thumb)) {
  grid-template-columns: calc((100% - 11 * 48px) / 12 * 3 + 2 * 48px) minmax(0, 1fr);
}

.post-row__thumb {
  /* The theme's .img-obj-fit is position:absolute and fills the nearest positioned
     ancestor. Without this the nearest one is .post-row, and the image swallows the
     whole row - which is exactly what happened on the first deploy. */
  position: relative;
  overflow: hidden;
  border-radius: 4px;
  aspect-ratio: 3 / 2;
  align-self: start;
}

.post-row__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.4s ease;
}

.post-row:has(.post-row__link:hover) .post-row__thumb img {
  transform: scale(1.04);
}

.post-row:last-child {
  border-bottom: 1px solid var(--border-default);
}

.post-row__meta {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: var(--fs-sm);
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
}

.post-row__cat {
  color: var(--color-natural-100, #fff);
}

.post-row__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Seven columns of reading measure, not the full remainder. */
  max-width: calc((100% - 6 * 48px) / 7 * 6 + 5 * 48px);
}

.post-row__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h3);
  line-height: 1.25;
  letter-spacing: -0.015em;
}

/* THE WHOLE ROW IS THE LINK — the stretched-link pattern the connect rows use. One anchor
   in the markup, grown over the row, so a screen reader hears one destination. */
.post-row__link {
  color: inherit;
  text-decoration: none;
}

.post-row__link::before {
  content: '';
  position: absolute;
  inset: 0;
}

.post-row:has(.post-row__link:hover) .post-row__title,
.post-row:has(.post-row__link:focus-visible) .post-row__title {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
}

.post-row__excerpt {
  margin: 0;
  font-size: var(--fs-r);
  line-height: 1.6;
  color: var(--color-natural-70);
}

.post-row__more {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: var(--fs-sm);
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
}

.post-row__more svg {
  transition: transform 0.3s cubic-bezier(0.26, 1, 0.48, 1);
}

.post-row:has(.post-row__link:hover) .post-row__more svg {
  transform: translateX(3px);
}

.insights__none {
  margin: 0;
  font-size: var(--fs-r);
  color: var(--color-natural-70);
}

/* --- A single piece ---------------------------------------------------------
   The 700px reading column agreed for /right-fit: centred as a block, text left aligned,
   because both pages are one continuous argument.                                        */
.flx-post .grid-container {
  display: flex;
  flex-direction: column;
  gap: 128px;
}

.post {
  width: 100%;
  max-width: 700px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.post .eyebrow {
  margin-bottom: 0;
}

.post__title {
  margin: 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  font-size: var(--fs-h1);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.post__hero {
  /* Same lesson as .post-row__thumb: the image inside is absolutely positioned, so
     this box must be the positioned ancestor and must own its height - without both,
     the figure collapses and the image fills the page instead. */
  position: relative;
  aspect-ratio: 16 / 9;
  margin: 16px 0 0;
  border-radius: 4px;
  overflow: clip;
  background: rgba(255, 255, 255, 0.04);
}

.post__body {
  display: flex;
  flex-direction: column;
  gap: 20px;
  font-size: var(--fs-r);
  line-height: 1.7;
  color: var(--color-natural-70);
}

.post__body p {
  margin: 0;
}

.post__body h2,
.post__body h3 {
  margin: 28px 0 0;
  font-family: 'Swiza', sans-serif;
  font-weight: 400;
  color: var(--color-natural-100, #fff);
  line-height: 1.3;
}

.post__body h2 { font-size: var(--fs-h4); }
.post__body h3 { font-size: var(--fs-h6); }

.post__body a {
  color: var(--color-natural-100, #fff);
  text-underline-offset: 3px;
}

.post__body ul,
.post__body ol {
  margin: 0;
  padding-left: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/*
 * The pull quote. An editor marks a paragraph as a quote; this is what that decision
 * looks like: the heading face at the h4 step, white, with room either side, and no
 * quotation marks drawn on — if the sentence needs marks to read as a quote, it is the
 * wrong sentence to pull.
 */
.post__body blockquote {
  margin: 28px 0;
  padding: 0;
  border: 0;
  font-family: 'Swiza', sans-serif;
  font-size: var(--fs-h4);
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--color-natural-100, #fff);
}

.post__body blockquote p {
  margin: 0;
}

.post__author {
  margin: 8px 0 0;
  font-size: var(--fs-r);
  color: var(--color-natural-60, rgba(255, 255, 255, 0.6));
}

/* The two next reads share the index's row shape, held to the same 700 column. */
.post__more {
  width: 100%;
  max-width: 700px;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.post__more .eyebrow {
  margin-bottom: 0;
}

.post__more .post-row {
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
  padding: 32px 0;
}

/* --- Both, on a phone ------------------------------------------------------- */
@media only screen and (width <= 899px) {
  .post-row,
  .post-row:not(:has(.post-row__thumb)) {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
    padding: 32px 0;
  }

  /* On a phone the image leads the row, full width, then the text. */
  .post-row__thumb {
    order: -1;
    margin-bottom: 8px;
  }

  .post-row__meta {
    flex-direction: row;
    gap: 16px;
  }

  .post-row__body {
    max-width: none;
  }

  .flx-post .grid-container {
    gap: 96px;
  }
}

/* --- The brief, on its way out ----------------------------------------------
   The logo at the head of the document, the two actions at the end, and the
   print stylesheet that turns the page into the PDF the download button
   promises: just the document, black on white, the mark inverted to survive
   the paper. */

.brief__logo {
  display: block;
  width: 72px;
  height: auto;
  margin-bottom: 32px;
}

.brief__actions {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.brief__download {
  padding: 14px 24px;
  border: 1px solid var(--border-default);
  border-radius: 100px;
  background: none;
  color: #fff;
  font-size: var(--fs-r);
  cursor: pointer;
  transition: border-color 0.25s ease;
}

.brief__download:hover,
.brief__download:focus-visible {
  border-color: #fff;
}

@media print {
  /* Only the document. Everything else on the page - header, footer, form,
     pixels, buttons - stays on screen where it belongs. */
  body * {
    visibility: hidden;
  }

  .brief__doc,
  .brief__doc * {
    visibility: visible;
  }

  .brief__doc {
    position: absolute;
    inset: 0 auto auto 0;
    width: 100%;
    padding: 0;
    background: #fff !important;
    color: #000 !important;
  }

  .brief__doc * {
    background: transparent !important;
    color: #000 !important;
    text-shadow: none !important;
  }

  /* The mark is drawn white for the site; on paper it inverts to black. */
  .brief__logo {
    filter: invert(1);
  }

  .brief__doc a {
    text-decoration: underline;
  }
}
