/* ═══════════════════════════════════════════════════════════
   Minimal build. White ground, one ink, one grey, one tint,
   and blue used only where something is meant to be pressed.

   The system is deliberately small: two weights (400 and 500),
   one family, four colours. Everything that reads as design
   here is spacing and type size, which is what makes the
   restraint hold — there is nothing decorative to fall back on.
   ═══════════════════════════════════════════════════════════ */

:root {
  /* Four values, and only two of them are text.
       --ink      primary
       --ink-55   secondary, ALL of it — #737373 on white, 4.6:1
       --ink-80   badges only, on the tint where 0.55 goes thin at 12px
       --ink-25   never text: dots and hairline marks
     There used to be a second secondary at 0.62 and a third at 0.40. Two
     greys 7% apart do not read as a hierarchy, they read as a mistake, and
     0.40 measured 2.85:1 which is under the floor for words. */
  --ink:        #121212;
  --ink-55:     rgba(0,0,0,0.55);
  --ink-25:     rgba(0,0,0,0.25);   /* non-text only: dots, hairline marks —
                                       2.3:1 on white, below the 4.5 floor */
  /* Badges only. 0.62 on the tint measures about 5.6:1 — passing, but thin at
     12px, which is the size every badge on the page uses. */
  --ink-80:     rgba(0,0,0,0.80);
  --bg:         #ffffff;
  --surface:    #f7f7f7;
  /* The Matas accent, carried over from the live case. Used only inside that
     sheet, on the frequency matrix. */
  --rose:       #fdafbf;
  --line:       rgba(0,0,0,0.10);
  --blue:       #2563eb;
  --blue-ink:   #1d4ed8;
  /* Availability is a state, not an action — green says it without
     borrowing the blue that means "you can press this". */
  --green:      #16a34a;

  /* Near edge-to-edge, not a centred column. The width of the page
     is the width of the window, and the gutter is small enough that
     the type has to carry the composition. */
  --nav-h:      64px;
  /* The case sheet's gutter. Sections and carousel tracks both read it, so a
     card can never come to rest out of line with the heading above it. */
  --cm-gutter:  clamp(24px, 4vw, 72px);
  /* One measure for a case section's headline AND the body under it. They were
     set independently — 38ch at 50px against 54ch at 18px — so the body ran to
     566px under a 1106px headline and the block read as two ragged columns
     rather than one.
     Two thirds of the content column, as a fraction rather than a pixel cap:
     the percentage resolves at the element, so the headline and the body land
     on the same edge at every width without either being pinned to a number
     that has to be kept in step with the gutter. Full width below 760, where
     two thirds of a phone is a 218px column. */
  --cm-measure: 100%;
  /* The hero's gap to the section under it — a rhythm value like any other,
     so it comes off --sec too rather than being its own clamp. It was 104,
     which is 7px from the minor value it was pretending not to be. */
  --hero-pb:    calc(var(--sec) * 0.62);
  --maxw:       1600px;
  --gutter:     48px;
  /* Where the content column's edge falls on any viewport: the gutter while
     the window is narrower than the column, and the leftover margin plus the
     gutter once it is wider. Full-bleed scrollers use this to line their
     first card up with the text above them. */
  --edge:       max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
  /* Exactly three cards across the content column, two gaps between them.
     Derived rather than guessed, so the third card lands flush with the
     column's right edge at every width up to the cap. */
  --col3:       calc((min(100vw, var(--maxw)) - 2 * var(--gutter) - 2 * 24px) / 3);
  --sec:        156px;      /* vertical rhythm between sections */
  --rule:       1px;
}

@media (max-width: 900px) {
  :root { --gutter: 24px; --sec: 88px; }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'General Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  font-size: 18px;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: -0.01em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img, canvas, svg { display: block; max-width: 100%; }
/* The other half of that rule, and it is not optional. max-width caps the
   width, but the width/height ATTRIBUTES still apply as presentational
   hints — and a CSS width paired with an attribute height does not preserve
   the ratio, it squashes. The team photo was arriving 4032x3024 and drawing
   at 1256x3024; the case screenshots were 1800x1188 drawing at 900x1188.
   Specificity 0,0,1 on purpose, so any component rule that wants a definite
   height (the design-system row, the card thumbnails) still wins. */
img { height: auto; }
a { color: inherit; text-decoration: none; }

/* ── Layout primitives ─────────────────────────────────── */
.wrap { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
/* Two values, and both come off --sec so they cannot drift apart:
     --sec          a major break between chapters
     --sec * 0.62   a minor one between related blocks
   There were five in play — 156, 97, 226 from an #about special case, 116
   in the case sheets, and the hero's own 48 — which is not a rhythm, it is
   five unrelated numbers. */
.section { padding-block: var(--sec); }
.section--tight { padding-block: calc(var(--sec) * 0.62); }
.rule { height: var(--rule); background: var(--line); border: 0; margin: 0; }

/* ── Type scale ────────────────────────────────────────────
   Regular weight at display sizes with tight tracking. The
   negative tracking is what stops 100px+ type reading as
   shouty — it closes the letterspacing that large sizes
   otherwise open up. */
/* Sized to the text column, not the window. The portrait takes 36% of the
   hero, so 7.4vw overflowed and broke "Design at scale." across two lines —
   the three statements only work as three lines. */
.display {
  font-size: clamp(42px, 6.6vw, 112px);
  font-weight: 400;
  /* 1.05 set the three hero statements almost touching. They are three
     separate claims rather than one wrapped sentence, so they want a little
     air between them. */
  line-height: 1.14;
  letter-spacing: -0.04em;
  /* Optical alignment. The box already sits on the column — measured, both
     start at exactly 172 — but General Sans carries a 0.081em left side
     bearing, so at 112px the D starts 9px inside its box while the 16px nav
     name starts 1.25px inside its own. Pulling back the difference lines the
     ink up rather than the boxes. In em, so it holds at every size. */
  margin: 0 0 0 -0.065em;
  text-wrap: balance;
}
.h2 {
  font-size: clamp(32px, 4.4vw, 64px);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.035em;
  margin: 0;
  text-wrap: balance;
}
.h3 {
  font-size: clamp(24px, 2.6vw, 40px);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.03em;
  margin: 0;
}
.lede {
  font-size: clamp(18px, 1.5vw, 22px);
  line-height: 1.55;
  letter-spacing: -0.015em;
  color: var(--ink-55);
  margin: 0;
  max-width: 60ch;
}
.body { font-size: 18px; line-height: 1.6; color: var(--ink-55); margin: 0; max-width: 62ch; }
/* margin-block, not margin. Plain `margin: 0` also zeroes the inline margins,
   so putting .label and .wrap on the same element killed .wrap's auto
   centring and the label sat at the window edge while the content beside it
   was centred in the 1600 column. */
/* One eyebrow for the whole site. The front page had 14px/500 in near-black
   and the case sheets had 18px/400 in grey — two systems for the same job,
   and the reader meets both within one click. The 18px grey is the better
   of the two and the one the reference uses: it reads as the section's name
   spoken quietly, where 14px/500 reads as a tag stuck on top of it.
   Every .label on the page already carried .muted, so the colour is not a
   change in practice — only the size and weight are. */
.label {
  font-size: 18px;
  font-weight: 400;
  letter-spacing: -0.015em;
  color: var(--ink-55);
  margin-block: 0;
}
.muted { color: var(--ink-55); }

/* The two-tone paragraph: the sentence that matters in ink, the
   rest dropped to grey. Does the work a bold weight would do,
   without introducing a second weight. */
.two-tone { color: var(--ink-55); }
/* Weight 500 as well as ink. Colour alone was a soft enough step that the
   opening line did not clearly lead the paragraph — and 500 is the second of
   the two weights the system already uses, so it costs nothing new. */
.two-tone strong { font-weight: 500; color: var(--ink); }

/* ── Buttons ───────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 52px;
  padding-inline: 30px;
  border-radius: 100px;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  border: 1px solid transparent;
  cursor: pointer;
  background: none;
  font-family: inherit;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease;
}
.btn--primary { background: var(--blue); color: #fff; }
.btn--primary:hover { background: var(--blue-ink); }
.btn--ghost { border-color: var(--line); color: var(--ink); }
.btn--ghost:hover { border-color: var(--ink); }
.btn--sm { height: 44px; padding-inline: 20px; font-size: 14px; }

/* ── Nav ───────────────────────────────────────────────── */
/* The bar spans the window; its contents sit on the content column. .wrap
   would have capped the bar itself at 1440, so the background, the blur and
   the bottom hairline all stopped short of the edges. */
.nav {
  position: sticky;
  top: 0;
  z-index: 60;
  padding-inline: var(--edge);
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--nav-h);
  /* 0.98, not 0.86, and the reason is iOS Safari rather than taste.
     Safari paints the band behind the status bar itself, as a SOLID colour —
     theme-color when the page declares one, the sampled page background
     otherwise. It is browser chrome, not page canvas, so no amount of
     backdrop-filter reaches it. Measured at 393px with the About section
     scrolled under the bar, the nav surface rendered 237 against that band's
     255: an 18-level seam straight across the top of the screen, 26 at its
     worst.

     Content blurred under the bar averages about 126, so the seam is
     (1 - alpha) * (255 - 126). At 0.86 that is 18 levels; at 0.98 it is 2-3,
     which is under the threshold on an OLED phone. The blur is kept because
     it still does the work under genuinely dark content, and because losing
     it changes how the bar reads on desktop, where there is no band to match
     and the effect was never the problem. */
  background: rgba(255,255,255,0.98);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: var(--rule) solid transparent;
  transition: border-color .25s ease;
}
.nav.is-stuck { border-bottom-color: var(--line); }
.nav__name { font-size: 16px; font-weight: 500; letter-spacing: -0.02em; }
.nav__links { display: flex; align-items: center; gap: 16px; }
.nav__link { font-size: 15px; color: var(--ink-55); transition: color .2s ease; }
.nav__link:hover { color: var(--ink); }
/* The nav is three items and stays three items; the CV link drops first
   if a phone runs out of room. */
@media (max-width: 560px) { .nav__link { display: none; } }

/* ── Hero ──────────────────────────────────────────────── */
/* Fills the first screen. The nav is sticky and therefore still in flow,
   so its height comes off the top or the hero would push the fold down by
   exactly that much. svh rather than vh: on mobile vh is the LARGEST
   viewport, so a hero sized to it sits partly under the browser chrome on
   load and only fits once the chrome retracts.

   min-height, not height — on a short window the content simply makes the
   section taller and the page scrolls, and the padding stops the centring
   from pushing the badge up under the nav. */
.hero {
  min-height: calc(100vh - var(--nav-h));
  min-height: calc(100svh - var(--nav-h));
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 36%);
  align-items: center;
  gap: 48px;
  padding-top: clamp(20px, 3.5vh, 48px);
  padding-bottom: var(--hero-pb);
}
.hero__text { display: flex; flex-direction: column; align-items: flex-start; }

/* Stretches to the hero's full height and stands on the base of a tint
   panel. The panel is what gives the cut-out a ground — free on white it
   floated, with nothing to stand on. Contained rather than bleeding past
   the padding, so the rounded corners stay intact. */
.hero__figure {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: var(--surface);
  border-radius: 20px;
  overflow: hidden;
  min-height: 0;
  position: relative;
  /* The panel sizes itself rather than stretching to the hero. Its ratio is
     deliberately WIDER than the plate's 0.49: cover then scales the image to
     the panel's width and crops the overflow off the bottom, so the sides are
     never cut and the grey meets the image at the left, right and top. A
     ratio narrower than the plate's would crop the arms instead. */
  align-self: center;
  aspect-ratio: 0.78;
  max-height: 100%;
}
/* The plate is a full standing figure at 0.49, and the panel lands at almost
   exactly that ratio — so contain (and plain cover) both showed him head to
   feet with dead tint above the head. Oversizing the box the image covers
   zooms in: the crop lands just above the pants and the head sits near the
   top, with the panel clipping the sides. */
.hero__figure img {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: center 8%;
  filter: grayscale(1) contrast(1.03);
}

@media (max-width: 900px) {
  .hero {
    grid-template-columns: 1fr;
    align-content: center;
    gap: 8px;
  }
  /* Below the copy, and only as tall as it needs to be — a full-height
     figure on a phone would push the words off the screen. */
  .hero__figure { align-self: auto; height: clamp(260px, 34vh, 380px); margin-top: 24px; }
}
/* Hugs its content, so it stays a badge rather than a bar. The left
   padding runs a little tighter than the right: a round dot sits
   optically further from the edge than a letter does at the same
   measured distance. */
.hero__avail {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 14px;
  color: var(--ink-55);
  margin: 0 0 34px;
  padding: 9px 16px 9px 14px;
  background: var(--surface);
  border-radius: 100px;
  /* .hero is a flex column, and a flex item stretches to the cross axis by
     default — inline-flex does not save it. Without this the badge runs the
     full width of the page. */
  align-self: flex-start;
}
/* The ping ring from the current site, carried over. 2.4s rather than 2.8 —
   slightly more often, as asked. The ring is a pseudo-element so only it
   animates; the dot itself stays put. */
.hero__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--green);
  flex-shrink: 0;
  position: relative;
}
.hero__dot::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--green) 55%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
  .hero__dot::after { animation: avail-ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite; }
  @keyframes avail-ping {
    0%   { transform: scale(1);    opacity: 0.85; }
    70%  { transform: scale(1.9);  opacity: 0; }
    100% { transform: scale(1.9);  opacity: 0; }
  }
}
/* No font-size here. This rule used to carry clamp(46px, 9.2vw, 132px),
   which beat .display on specificity and source order — so the type scale
   looked like it was being ignored. Its comment claimed it was the
   case-study title, but .hero__display is the front page h1; the case title
   is .display inside .case-hero. Size comes from the scale now. */
.hero__display { margin-bottom: 34px; }
.hero__lede { margin-bottom: 44px; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* The portrait is a transparent cut-out at 1600x3272 — a 0.49 ratio,
   cut to stand full-height. A wide hero band was the wrong frame for
   it twice over: cover cropped it to a face, and contain left one
   small figure adrift in a very wide panel. A standing figure wants a
   portrait-shaped frame, so it sits beside the About copy instead,
   which is the one place on the page shaped to hold it.

   Greyscale so the page holds one colour story: the only hue is the
   blue on things you can press, and the product UI inside the films. */



/* ── Meta list (label over value) ──────────────────────── */
.meta { display: grid; gap: 40px; }
@media (min-width: 860px) { .meta--4 { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 860px) { .meta--3 { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 859px) { .meta { grid-template-columns: repeat(2, 1fr); gap: 32px 24px; } }
.meta__k { font-size: 14px; font-weight: 500; margin: 0 0 6px; }
.meta__v { font-size: 18px; color: var(--ink-55); margin: 0; }

/* ── Stats ─────────────────────────────────────────────── */
.stats { display: grid; gap: 44px 24px; }
@media (min-width: 860px) { .stats { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 859px) { .stats { grid-template-columns: repeat(2, 1fr); } }
.stat__n {
  font-size: clamp(40px, 4.4vw, 64px);
  font-weight: 400;
  letter-spacing: -0.04em;
  line-height: 1;
  margin: 0 0 12px;
  font-variant-numeric: tabular-nums;
}
/* No max-width: a 20ch cap broke "Years of design experience" onto a second
   line even where the column had room for it. The grid column is the limit. */
.stat__l { font-size: 15px; color: var(--ink-55); margin: 0; }

/* ── Numbered rows (how I work / experience) ───────────── */
.row {
  display: grid;
  gap: 20px 48px;
  padding-block: 44px;
  border-top: var(--rule) solid var(--line);
  align-items: start;
}
/* Title and body share the space rather than the title taking a fixed 300px.
   The body starts further right and runs wider, which stops the row from
   sitting in the left third with a page of white beside it. */
@media (min-width: 860px) { .row { grid-template-columns: 88px 1fr 1.4fr; } }
@media (max-width: 859px) { .row { grid-template-columns: 1fr; gap: 12px; padding-block: 32px; } }
.row__n { font-size: 14px; color: var(--ink-55); font-variant-numeric: tabular-nums; }
.row__t { font-size: clamp(22px, 2.2vw, 30px); font-weight: 400; letter-spacing: -0.03em; margin: 0; }
.row__b { color: var(--ink-55); font-size: 17px; line-height: 1.6; margin: 0; max-width: 62ch; }
.rows { border-bottom: var(--rule) solid var(--line); }

/* ── Experience rows ───────────────────────────────────── */
/* Block, not grid. The row itself carries the columns now — left as a grid
   here it placed .xp__detail in the second column, so an expanded paragraph
   wrapped inside 190px and ran 2662px tall. */
/* No padding here — it belongs to the button. With it on .xp, the 30px band
   above and below each row sat outside the click target, so the row only
   responded on the text itself. */
.xp {
  display: block;
  border-top: var(--rule) solid var(--line);
}
.xp__co { font-size: 19px; letter-spacing: -0.02em; }
.xp__when, .xp__role { font-size: 16px; color: var(--ink-55); }
.xp__role { color: var(--ink-55); }
/* Same pill as the tags under the client cards, so a category reads the same
   way wherever it appears. */
.xp__where {
  font-size: 12px;
  color: var(--ink-80);
  background: var(--surface);
  border-radius: 100px;
  padding: 5px 11px;
  justify-self: end;
  white-space: nowrap;
}

/* ── Work ──────────────────────────────────────────────── */
.cases { display: grid; gap: clamp(64px, 8vw, 120px); }
.case__media {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 20px;
  overflow: hidden;
  background: var(--surface);
  margin-bottom: 36px;
}
@media (max-width: 720px) { .case__media { aspect-ratio: 4 / 3; border-radius: 14px; } }
.case__poster, .case__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* No grayscale. The posters are now the films' own opening frames, captured
   from the live scene under prefers-reduced-motion — which holds SHOTS[0].from
   — so the still and the first rendered frame are the same picture. Filtering
   one of them made the handoff a visible colour pop, and the poster no longer
   matched what it was standing in for. */
.case__canvas { opacity: 0; transition: opacity .6s ease; }
.case__canvas.is-live { opacity: 1; }
.case__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px 32px;
  margin-bottom: 14px;
}
.case__num { font-size: 14px; color: var(--ink-55); font-variant-numeric: tabular-nums; }
/* Same grey as the About lede. --ink-40 measures about 3.3:1 on white, under
   the 4.5 floor, and at 18px this is body copy rather than a caption. */
.case__line {
  color: var(--ink-55);
  /* The same clamp as .lede. It was a flat 18px, which is the FLOOR of that
     clamp — so on a phone the two were already identical, and on a desktop
     the case description sat 4px under every other passage of body copy on
     the page. One scale for body copy, not two. */
  font-size: clamp(18px, 1.5vw, 22px);
  line-height: 1.55;
  letter-spacing: -0.015em;
  max-width: 62ch;
  margin: 0 0 28px;
}
/* The lead clause is the claim; the rest is context. Colour alone left the
   two halves reading as one flat line, so the highlighted half takes the
   heavier of the two weights too — matching .two-tone above. */
.case__line strong { font-weight: 500; color: var(--ink); }

/* ── Quote ─────────────────────────────────────────────── */
.quote { max-width: 24ch; }
.quote__t {
  font-size: clamp(26px, 3.2vw, 46px);
  font-weight: 400;
  line-height: 1.18;
  letter-spacing: -0.03em;
  margin: 0 0 36px;
  max-width: 20ch;
}
.quote__who { font-size: 15px; margin: 0; }
.quote__role { font-size: 15px; color: var(--ink-55); margin: 0; }
.quotes { display: grid; gap: 56px; }
@media (min-width: 900px) { .quotes { grid-template-columns: repeat(2, 1fr); gap: 72px 64px; } }
.quotes .quote { max-width: none; }

/* ── Footer ────────────────────────────────────────────── */
.foot { padding-top: var(--sec); padding-bottom: 56px; }
.foot__mail {
  display: inline-block;
  font-size: clamp(22px, 2.6vw, 40px);
  font-weight: 400;
  letter-spacing: -0.04em;
  line-height: 1.1;
  margin: 28px 0 0;
  border-bottom: 1px solid var(--line);
  padding-bottom: 6px;
  transition: border-color .25s ease, color .25s ease;
}
.foot__mail:hover { border-color: var(--blue); color: var(--blue); }

/* ── Reveal ────────────────────────────────────────────────
   Same approach as the main build: hidden state set in CSS
   before first paint and gated on .js-anim, so nothing flashes
   and no element animates from JavaScript. */
/* Timing taken off the reference rather than guessed: 0.55s on
   cubic-bezier(0.76, 0, 0.24, 1), with a 70ms stagger. Their content has no
   scroll reveal at all — this is the easing from their menu and hero, which
   is the motion that reads as "soft" there. Shorter travel too; 18px on a
   slow ease drew attention to the movement itself. */
.js-anim .rise {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .55s cubic-bezier(0.76, 0, 0.24, 1),
              transform .55s cubic-bezier(0.76, 0, 0.24, 1);
  transition-delay: var(--d, 0ms);
}
.js-anim .rise.is-in { opacity: 1; transform: none; }

/* Text also wipes up from under its own baseline — the move their menu links
   use. Confined to text: clip-path on a container would cut off a carousel
   mid-scroll, and it establishes a containing block that would break any
   sticky descendant. */
.js-anim .rise.display,
.js-anim .rise.h2,
.js-anim .rise.h3,
.js-anim .rise.label,
.js-anim .rise.lede,
.js-anim .rise.body {
  clip-path: inset(0 0 18% 0);
  transition: opacity .55s cubic-bezier(0.76, 0, 0.24, 1),
              transform .55s cubic-bezier(0.76, 0, 0.24, 1),
              clip-path .55s cubic-bezier(0.76, 0, 0.24, 1);
  transition-delay: var(--d, 0ms);
}
/* Ends BELOW the box, not at it. inset(0) still clips to the border box, and
   with line-height 1.1 a descender sits outside that box — so "together"
   lost the tail of its g once the wipe finished. A negative bottom inset
   keeps the property animatable while clipping nothing. */
.js-anim .rise.display.is-in,
.js-anim .rise.h2.is-in,
.js-anim .rise.h3.is-in,
.js-anim .rise.label.is-in,
.js-anim .rise.lede.is-in,
.js-anim .rise.body.is-in { clip-path: inset(0 -0.15em -0.3em -0.15em); }
@media (prefers-reduced-motion: reduce) {
  .js-anim .rise { opacity: 1; transform: none; transition: none; }
}

:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 4px; }

/* ── Case study page ───────────────────────────────────── */
.case-hero { padding-top: clamp(64px, 9vh, 120px); }

/* Label in a narrow left column, the argument in a wide right one.
   The label is doing the job a heading number would; keeping it in
   its own column is what makes a page of plain text feel structured
   without adding any rules or boxes. */
.split { display: grid; gap: 20px 48px; align-items: start; }
@media (min-width: 900px) { .split { grid-template-columns: 220px 1fr; gap: 72px; } }

.next { display: grid; gap: 10px; }
.next__t {
  font-size: clamp(30px, 4.4vw, 60px);
  font-weight: 400;
  letter-spacing: -0.035em;
  line-height: 1.1;
  transition: color .2s ease;
}
.next:hover .next__t { color: var(--blue); }

/* ── Experience rows ───────────────────────────────────── */
.xp__row {
  width: 100%;
  display: grid;
  gap: 8px 40px;
  align-items: baseline;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  padding-block: 30px;
  transition: opacity .2s ease;
}
/* Guarded. On iOS the first tap on an element carrying a :hover rule is
   spent applying that hover state rather than firing the click, so the row
   needed tapping twice to open. (hover: hover) keeps the effect on pointers
   that genuinely hover and takes it off touch entirely. */
@media (hover: hover) {
  .xp__row:hover { opacity: 0.62; }
  .xp__row:hover .xp__chev { color: var(--ink); }
}
@media (prefers-reduced-motion: reduce) { .xp__row { transition: none; } }
@media (min-width: 860px) { .xp__row { grid-template-columns: 200px 190px 1fr auto; } }
/* Badge and chevron are one cell, not two columns. As separate columns their
   widths came from the date spanning above them, so the space between them
   changed from row to row — 19px, 41px, 28px down the same list. */
.xp__meta { display: inline-flex; align-items: center; gap: 10px; justify-self: end; }
/* Named areas rather than flow order. With five children in a two-column
   grid the chevron fell to a third row on its own; this puts it beside the
   badge where it belongs, without wrapping the two in a container. */
@media (max-width: 859px) {
  .xp__row {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "co   when"
      "role meta";
    column-gap: 12px;
  }
  .xp__co   { grid-area: co; }
  .xp__when { grid-area: when; justify-self: end; }
  .xp__role { grid-area: role; }
  .xp__meta { grid-area: meta; }
}
.xp__chev {
  color: var(--ink-25);
  justify-self: end;
  transition: transform .3s cubic-bezier(0.22, 0.7, 0.3, 1), color .2s ease;
}
.xp[data-open="true"] .xp__chev { transform: rotate(180deg); }

/* 0fr → 1fr animates without anyone having to know the content's height, and
   without the max-height guess that overshoots on short entries and clips
   long ones. */
.xp__detail {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .42s cubic-bezier(0.22, 0.7, 0.3, 1);
}
.xp__detail > div { overflow: hidden; }
.xp[data-open="true"] .xp__detail { grid-template-rows: 1fr; }
/* The one-line summary each row carried on the old site. Hidden while the
   row is closed — that restraint is the point of the collapsed list — and
   used as the detail's own heading once it opens. */
.xp__lead {
  margin: 0 0 14px;
  /* Same size and leading as the copy below it — weight and colour do the
     separating, so it reads as that paragraph's heading rather than as a
     second, larger paragraph. */
  font-size: 16px;
  line-height: 1.65;
  font-weight: 500;
  color: var(--ink);
  max-width: 62ch;
}
.xp__text {
  /* No top margin: .xp__body already provides it, and having both meant the
     paragraph started 18px below the image it is supposed to sit level with. */
  margin: 0;
  max-width: 68ch;
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink-55);
}
@media (min-width: 860px) { .xp__text { margin-left: 240px; } }

/* Hidden until the toggle, and they animate in on their own rather than
   appearing all at once. */
/* display:none cannot animate, so collapsing snapped shut while opening
   faded in — the two directions did not match. The extra rows share one
   wrapper that animates 0fr to 1fr, which works both ways and needs nobody
   to know the content's height. */
.xp-more {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .5s cubic-bezier(0.76, 0, 0.24, 1),
              opacity .35s ease;
}
/* Exactly one child. grid-template-rows: 0fr sizes only the FIRST row, so
   with the seven rows as direct children the other six got auto-sized
   implicit rows and the wrapper collapsed to 553px instead of 0. */
.xp-more > div { overflow: hidden; min-height: 0; }
.rows.is-all .xp-more { grid-template-rows: 1fr; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .xp-more { transition: none; }
  .xp__detail { transition: none; }
}

.xp__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 36px; }
.xp-toggle { gap: 8px; }
.xp-toggle__chev { transition: transform .3s cubic-bezier(0.22, 0.7, 0.3, 1); }
.xp-toggle[aria-expanded="true"] .xp-toggle__chev { transform: rotate(180deg); }

/* ── Carousels ─────────────────────────────────────────────
   Native scroll-snap, the same conclusion the main build
   arrived at the hard way: a JS gesture layer cannot match the
   platform's own finger tracking, momentum and snap. JS here
   owns only the chrome — dots, arrows, and the disabled state.

   Trailing padding matches the gutter so the last card stops
   with the inset every other card has, rather than flush
   against the viewport edge. */
.car { position: relative; }
.car-track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding-bottom: 4px;
  /* The track stays full-bleed so cards can run off both edges, but its own
     padding lines the first card up with the content column. scroll-padding
     is the half that matters: without it snap aligns to the scrollport edge,
     not the padding edge, so the first card came to rest flush against the
     window with 48px of dead space scrollable to its left. */
  padding-inline: var(--edge);
  scroll-padding-inline: var(--edge);
}
.car-track::-webkit-scrollbar { display: none; }
/* Stretch, so every card in a row is the same height. The trade is real and
   deliberate: nothing is clipped, so the longest recommendation sets the
   height and the shorter ones carry space above their author. Ragged card
   bottoms were the alternative and read worse than the empty space does. */
.car-track { align-items: stretch; }
.car-track > * { scroll-snap-align: start; flex: 0 0 auto; }
@media (prefers-reduced-motion: reduce) { .car-track { scroll-behavior: auto; } }

.car-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 36px;
  padding-inline: var(--edge);
}
/* The dot people see is 7px; the thing they can hit is 44. Drawing the dot
   as a pseudo-element inside a full-size button keeps the visual weight
   without a 7px tap target. */
.car-dots, .cm-car__dots { display: flex; align-items: center; position: relative; }
/* One pill spanning the cards on screen, rather than a run of separate dots.
   Drawn as a single element so the shape has two rounded ends instead of a
   chain of lozenges, and so it slides between positions. */
/* The modal's rows use the same dot, pill and container — they used to have
   their own, which never reset the UA button styling, so the case studies
   showed a row of 2px-outset grey boxes with a dot drawn on top of each. */
.car-pill {
  position: absolute;
  top: 50%;
  height: 7px;
  border-radius: 100px;
  background: var(--ink);
  transform: translateY(-50%);
  pointer-events: none;
  transition: left .38s cubic-bezier(0.76, 0, 0.24, 1),
              width .38s cubic-bezier(0.76, 0, 0.24, 1);
}
@media (prefers-reduced-motion: reduce) { .car-pill { transition: none; } }
.car-dot {
  width: 13px;
  height: 44px;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  position: relative;
}
.car-dot::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 7px; height: 7px;
  border-radius: 100px;
  background: var(--ink-25);
  transition: background-color .25s ease, width .25s ease;
}
/* Every card currently on screen gets a filled dot, so the indicator shows
   how much of the row you are looking at rather than a single position. Same
   shape throughout — the elongated pill implied one active item when three
   are visible. */
/* The dots stay grey; the pill above covers the active run. */
.car-btns { display: flex; gap: 8px; }
.car-btn {
  width: 44px; height: 44px;
  border-radius: 100px;
  border: 1px solid var(--line);
  background: none;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color .2s ease, background-color .2s ease, opacity .2s ease;
}
.car-btn:hover:not(:disabled) { border-color: var(--ink); }
.car-btn:disabled { opacity: 0.3; cursor: default; }

/* ── Clients ───────────────────────────────────────────── */
.cl-col { width: var(--col3); }
/* Grey at rest, the brand's own colour on hover. The colour is already on
   each card as --bg, so this is the whole of the interaction. */
.cl-card {
  aspect-ratio: 1 / 1;
  border-radius: 16px;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin-bottom: 22px;
  transition: background-color .35s cubic-bezier(0.22, 0.7, 0.3, 1);
}
/* --brand, not --bg. The global --bg token is the page's white, so a card
   that ever lost its inline value would hover to white instead of staying on
   the tint — a silent failure rather than a visible one. */
.cl-col:hover .cl-card { background: var(--brand, var(--surface)); }
/* The same 1.06 lift the current site uses, on the image rather than the
   card, so the tint frame stays put and only the artwork moves inside it. */
.cl-card img {
  width: 78%;
  height: auto;
  object-fit: contain;
  transition: transform .3s ease-in-out;
}
.cl-col:hover .cl-card img { transform: scale(1.06); }
@media (prefers-reduced-motion: reduce) {
  .cl-card img { transition: none; }
  .cl-col:hover .cl-card img { transform: none; }
}
.cl-name { font-size: 19px; letter-spacing: -0.02em; margin: 0 0 8px; }
.cl-desc { font-size: 15px; line-height: 1.55; color: var(--ink-55); margin: 0 0 16px; }
.cl-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.cl-tag {
  font-size: 12px;
  color: var(--ink-80);
  background: var(--surface);
  border-radius: 100px;
  padding: 5px 11px;
}

/* ── Testimonials ──────────────────────────────────────── */
/* White, with the cards outlined instead. A tint band behind white cards
   made the section a block of its own; a hairline keeps the page one
   continuous white surface and still separates each quote. */
.t-col { width: var(--col3); }
.t-card {
  background: var(--surface);
  border-radius: 18px;
  padding: 32px;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 28px;
}
.t-quote {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink-55);
  margin: 0;
}
.t-who { display: flex; align-items: center; gap: 14px; }
.t-avatar { width: 44px; height: 44px; border-radius: 100px; object-fit: cover; filter: grayscale(1); }
.t-name { font-size: 15px; margin: 0 0 1px; line-height: 1.35; }
.t-role { font-size: 14px; color: var(--ink-55); margin: 0; line-height: 1.35; }

/* Three across needs room. Below that the cards go to one, near full-width,
   with a sliver of the next showing so the row reads as scrollable. */
@media (max-width: 900px) {
  :root { --col3: calc(min(100vw, var(--maxw)) - 2 * var(--gutter) - 36px); }
}

/* Colour, as on the current site: these are the one warm thing in an
   otherwise grey section, and greyscaling them lost what they were for. */
/* The image takes the date column's width and the gap matches the row's, so
   the detail text starts exactly where the role column does above it —
   240 + 190 + 40. Sized by the grid rather than by eye. */
/* 150 with an 80px gap rather than 190 with 40 — the pair still sums to the
   230 that puts the paragraph on the role column above it. */
.xp__body { display: flex; gap: 80px; align-items: flex-start; margin: 0 0 30px; }
/* The two paragraphs stack in their own column beside the image. Left as
   siblings of the img they became flex items and sat side by side, which put
   the copy 400px right of where the role column sits above it. */
.xp__copy { min-width: 0; }
.xp__media {
  width: 150px;
  flex-shrink: 0;
  border-radius: 12px;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  /* Upper-biased: these are people and buildings, and a centred crop on a
     full-length portrait framed the torso and cut the head off. */
  object-position: center 18%;
  background: var(--surface);
}
@media (min-width: 860px) { .xp__body { margin-left: 240px; } .xp__text { margin-left: 0; } }
@media (max-width: 859px) {
  .xp__body { flex-direction: column; gap: 16px; }
  /* No max-width: the column is already the content width on a phone, and
     capping it at 260 left the image floating in the left two-thirds. */
  .xp__media { width: 100%; max-width: none; aspect-ratio: 4 / 3; }
}

/* A break the author chose, not one the line length happened to produce.
   Only where there is room for two long lines — below that the heading wraps
   on its own and a forced break would leave a stray short line. */
.brk { display: none; }
@media (min-width: 900px) { .brk { display: inline; } }

/* The stats end this section on a row of large numerals, which need more
   room beneath them than a paragraph would before the next label starts. */
/* Was * 1.45, a 226px one-off that existed nowhere else on the page. */
/* No padding above it: the hero already carries its own 97px underneath, and
   the section gap on top of that opened a 253px hole between the name and the
   first thing it says. */
#about { padding-top: 0; padding-bottom: var(--sec); }

/* ── Beyond the work ───────────────────────────────────────
   The original ran three tall image cards. Icons instead: the pictures were
   decorative rather than evidence, and this says the same thing in a third
   of the height. */
/* A wide column gap, because there is no card edge or rule separating these
   — the space is the only thing telling them apart. */
.bey-grid { display: grid; gap: 56px 88px; }
@media (min-width: 900px) { .bey-grid { grid-template-columns: repeat(3, 1fr); } }
.bey__icon { color: var(--ink); margin-bottom: 22px; }
.bey__t {
  font-size: 20px;
  font-weight: 500;
  letter-spacing: -0.02em;
  margin: 0 0 12px;
}
.bey__b { font-size: 16px; line-height: 1.62; color: var(--ink-55); margin: 0; }

/* ── Mobile ────────────────────────────────────────────────
   The nav ran out of room before 500px: the name and the links met with 3px
   between them and the CV link was squeezed to zero width rather than
   wrapping or hiding. It goes; the same link sits in the hero and the footer.
   The hero also needs to fit one screen — the figure was pushing nav + hero
   to 967px against an 844px viewport. */
@media (max-width: 560px) {
  .nav__links { gap: 10px; }
  .nav__links .nav__link { display: none; }
  .nav__name { font-size: 15px; }
}
@media (max-width: 900px) {
  .hero { gap: 0; }
  /* aspect-ratio has to go here. With an explicit height it DERIVES the
     width — 260 x 0.78 gave a 171px strip on a 390px screen instead of a
     full-width band. */
  /* Square, not a letterbox. The plate is a standing figure at 0.49, so a
     342x219 band made cover crop 481px off the height and land on the head
     alone. A square frame crops far less and holds head to chest. */
  .hero__figure {
    aspect-ratio: 1 / 1;
    width: 100%;
    height: auto;
    max-height: 42vh;
    /* 18px put the portrait closer to the buttons than the buttons were to
       the lede above them (28px), so the image read as part of the button
       row. A picture starting wants more air than the gaps inside the text
       block, not less. */
    margin-top: 40px;
  }
  .hero__figure img { object-position: center 12%; }
  .hero__avail { margin-bottom: 24px; }
  .hero__display { margin-bottom: 24px; }
  .hero__lede { margin-bottom: 28px; }
}

/* Emil's recommendation runs about four times the length of the shortest one,
   and equal-height cards mean the longest sets every card in the row. Height
   comes back from type and spacing rather than from truncation, so every word
   of every recommendation stays on the page. */
@media (max-width: 900px) {
  .t-card { padding: 22px 20px; gap: 16px; }
  .t-quote { font-size: 15px; line-height: 1.5; }
  .t-who { gap: 12px; }
  .t-avatar { width: 38px; height: 38px; }
  .t-name { font-size: 14px; }
  .t-role { font-size: 13px; }
}

/* Both actions on one line at 390px. The two labels plus the gap come to
   more than the content width at the default padding, so the padding gives
   way rather than the buttons wrapping. */
@media (max-width: 430px) {
  .xp__actions .btn--sm { padding-inline: 14px; }
}

/* ═══════════════════════════════════════════════════════════
   CASE MODAL
   The sheet, and the case components re-expressed in this
   language. The markup is the current site's; everything
   visual here is new. Sections no longer carry their own
   grounds — the panel is white throughout and separation
   comes from space and hairlines, with one dark section kept
   as the single emphatic moment.
   ═══════════════════════════════════════════════════════════ */

.cm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity .45s ease;
  z-index: 90;
}
.cm-overlay.is-open { opacity: 1; }

.cm-modal { position: fixed; inset: 0; z-index: 100; visibility: hidden; }
.cm-modal.is-open { visibility: visible; }
/* An opaque backstop the exact size of the panel's column.
   The panel is 13,000+px tall and the scroller behind it is transparent, so on
   a fast fling the compositor outruns the raster and the un-painted band shows
   straight through to the dimmed page — which reads as the sheet being built
   in front of you. This cannot be fixed by making the SCROLLER opaque: it
   covers the viewport, and the dimmed page either side of the panel is the
   design. So the backstop matches the panel's own width and corner radius and
   sits directly behind it, and the worst a fast scroll can now show is white.
   Only once the sheet has arrived — it is position:fixed and cannot travel
   with the panel, so during the slide it would be a white card sitting where
   the panel is about to be. */
.cm-modal.is-settled::after {
  content: '';
  position: fixed;
  /* Runs the full height by default. It used to start at 40px — the sheet's
     resting inset — but the sheet only sits at that inset when the reader is
     at the top; scrolled at all, it fills the viewport from zero, and those
     top 40px then had no backstop behind them. That band is where a fast
     scroll up flashed through to the dimmed page. The inset comes back only
     with .is-at-start, below. */
  top: 0;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(100vw - 32px, 1400px);
  background: var(--bg);
  /* Square by default, for the same reason: a rounded corner at y=0 would
     leave two notches of dimmed page where the sheet itself is square. Each
     end rounds only when that end is actually in view. */
  border-radius: 0;
  z-index: -1;
  pointer-events: none;
}
.cm-modal.is-settled.is-at-start::after {
  top: 40px;
  border-top-left-radius: 20px;
  border-top-right-radius: 20px;
}
/* Pulled up off the floor once the reader has arrived at the end, so the gap
   under the sheet shows the dimmed page and not this. It stays full-height
   while there is still scrolling to do, which is when a fast fling could
   outrun the raster and see through. */
.cm-modal.is-settled.is-at-end::after {
  bottom: 40px;
  border-bottom-left-radius: 20px;
  border-bottom-right-radius: 20px;
}
@media (max-width: 900px) {
  .cm-modal.is-settled::after { width: calc(100vw - 16px); }
  .cm-modal.is-settled.is-at-start::after {
    top: 20px; border-top-left-radius: 14px; border-top-right-radius: 14px;
  }
  .cm-modal.is-settled.is-at-end::after {
    bottom: 20px; border-bottom-left-radius: 14px; border-bottom-right-radius: 14px;
  }
}
.cm-scroller {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* none, not contain. Both stop the scroll chaining to the page underneath,
     but contain still lets the browser show its overscroll affordance — the
     rubber-band bounce at either end of a trackpad fling. That bounce pushes
     the sheet away from the edge it is resting against and exposes the dimmed
     page behind it, which reads exactly like the modal flashing through at the
     top and bottom. It is not a paint problem, which is why chasing the
     compositor never fixed it. The current site's modal has used none all
     along, and does not do this. */
  overscroll-behavior: none;
  -webkit-overflow-scrolling: touch;
  padding-top: 40px;
}
/* The sheet floats: dimmed page above it and the same below, so scrolling to
   the end lands on a rounded bottom edge rather than running into the
   viewport. Matches the current site, whose own comment records that removing
   the lower half of this was a mistaken fix for the transient paint gap —
   that was a compositor problem and is handled by the backstop above. */
/* The sheet holds all three cases and shows one. `hidden` is enough here
   because nothing gives these wrappers a display of their own — the rule is
   the guard against that changing. */
[id^="modal-content-"][hidden] { display: none; }

.cm-panel {
  position: relative;
  margin: 0 auto 40px;
  width: min(100vw - 32px, 1400px);
  background: var(--bg);
  border-radius: 20px;
  /* overflow:hidden does TWO jobs here and both are load-bearing. It clips
     children to the rounded corners, and it makes the panel a block
     formatting context so the last row's margin-bottom stays inside instead
     of collapsing through the panel's edge.
     I removed it once, on a theory that the rounded mask over an
     eleven-thousand-pixel box was behind the scroll flash. It was not — that
     was overscroll bounce, fixed on the scroller — and removing this left the
     content flush against the panel's bottom edge with the corners unclipped,
     which is what showed up as artefacts in the corners at both ends.
     Measured before and after removal: the mask costs nothing detectable,
     median and p95 frame times identical to a tenth of a millisecond. */
  overflow: hidden;
  transform: translateY(100%);
}
@media (max-width: 900px) {
  .cm-scroller { padding-top: 20px; }
  .cm-panel { width: calc(100vw - 16px); border-radius: 14px; margin-bottom: 20px; }
}

.cm-close {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 120;
  width: 52px; height: 52px;
  border-radius: 100px;
  border: 1px solid var(--line);
  background: rgba(255,255,255,0.9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  transition: opacity .3s ease, background-color .2s ease;
}
.cm-close.is-visible { opacity: 1; }
.cm-close:hover { background: #fff; }
/* Over a dark picture the light disc disappeared into it. The live site keys
   this off section classes, which does not transfer: every section here is
   white, and what goes dark behind the button is the photographs and the
   monitor scenes inside them. So it is measured from the pixel, not inferred
   from the markup — see updateCloseTheme in v2.js. */
.cm-close.is-on-dark {
  background: rgba(0,0,0,0.34);
  border-color: rgba(255,255,255,0.36);
  color: #fff;
}
.cm-close.is-on-dark:hover { background: rgba(0,0,0,0.5); }
.cm-close svg { width: 18px; height: 18px; }

/* ── Section rhythm ─────────────────────────────────────── */
/* Measured off the reference: the case breathes far harder than the front
   page. Sections are tall and the type inside them is large, so the space
   between them has to be larger again or the page reads as a list. */
@media (min-width: 760px) { :root { --cm-measure: calc(100% * 2 / 3); } }
/* A heading and the row it introduces are one unit, so the full section gap
   between them read as a break in the middle of a thought. The gap BELOW the
   row keeps the section rhythm; only the one above it closes up. */
.cm-section--light:has(+ .cm-car-wrap),
.cm-section--dark:has(+ .cm-car-wrap) { padding-bottom: calc(var(--sec) * 0.28); }
/* ...but only where the heading IS the section's last word. A section that
   ends on a figure has already said something between its heading and the row
   below it, so the row is a new thought and takes the full section gap. The
   design-system row was reading as a continuation of the mess above it. */
.cm-section--light:has(+ .cm-car-wrap):has(> .cm-figure:last-child),
.cm-section--dark:has(+ .cm-car-wrap):has(> .cm-figure:last-child) {
  padding-bottom: calc(var(--sec) * 0.5);
}
.cm-section--light,
.cm-section--dark {
  /* The minor value, same token as the page's tight sections. The sheet had
     its own clamp, which is how it became a fifth number. */
  padding: calc(var(--sec) * 0.62) var(--cm-gutter);
}
/* Every section sits on white. There were two ink-ground sections and an
   ink-ground closing block; they were the only thing in the build that
   inverted, and one dark slab in the middle of a white case reads as a
   different site rather than as emphasis. The --dark class is kept on the
   markup so it can still be targeted, but it no longer inverts.
   Nothing separates the sections now but the space between them. A hairline
   went in when they turned white, and it drew a line across the widest part of
   the page every 150px — a lot of ruling for a build whose whole argument is
   that spacing does the work. The 150px of section padding IS the division. */
.cm-section--dark { background: var(--bg); color: var(--ink); }
.cm-section--dark .cm-headline,
.cm-section--dark .cm-hero__title { color: var(--ink); }
.cm-section--dark .cm-body,
.cm-section--dark .cm-label { color: var(--ink-55); }

/* A headline carrying a section on its own, with no paragraph restating it. */
.cm-statement { font-size: clamp(30px, 3.6vw, 56px); line-height: 1.1; letter-spacing: -0.035em; max-width: 20ch; }

/* The two or three facts a statement leaves behind, set as a footer rather
   than folded back into a sentence. */
/* No rule above it. Nothing else in the sheet separates with a line — the
   section dividers came out for the same reason — so the space does it. */

/* A caption belongs to its figure, so it lives in the figure. */

/* One line of somebody else's words, placed where the reader will reach it.
   The full recommendations stay in the row at the end. */
.cm-pull { margin: 0; }
/* The sheet ends on the quotes, and a closing block needs more room under it
   than a section that has another section coming. Same reason the last row
   carries extra margin. */
[id^="modal-content-"] > .cm-section--light:last-child { padding-bottom: clamp(96px, 11vw, 170px); }
.cm-pull-pair { display: grid; gap: 56px; }
/* Two quotes share the row from 900 up. At half the column the display size
   would run the shorter quote to four lines and the longer to five, so the
   paired quotes step down a notch and drop the 26ch cap — the column is the
   measure now. */
@media (min-width: 900px) { .cm-pull-pair { grid-template-columns: 1fr 1fr; gap: 96px; } }
@media (min-width: 900px) { .cm-pull-pair .cm-pull__q { font-size: clamp(20px, 1.9vw, 27px); max-width: none; } }
.cm-pull__q { font-size: clamp(24px, 2.6vw, 38px); font-weight: 400; line-height: 1.25; letter-spacing: -0.03em; margin: 0; max-width: 26ch; }
.cm-pull__who { display: flex; align-items: center; gap: 14px; margin-top: 30px; }
.cm-pull__who img { width: 44px; height: 44px; border-radius: 100px; object-fit: cover; filter: grayscale(1); }
.cm-pull__who p { margin: 0; font-size: 15px; }
.cm-pull__who span { display: block; color: var(--ink-55); font-size: 14px; }

/* ── Case hero ─────────────────────────────────────────── */
/* 18px, regular, grey. The reference sets these as a quiet sentence rather
   than an uppercase caption — they read as the section's name, not as a tag. */
.cm-hero__label,
.cm-label {
  font-size: 18px;
  font-weight: 400;
  letter-spacing: -0.015em;
  color: var(--ink-55);
  margin: 0 0 24px;
  text-transform: none;
}
.cm-hero__title {
  font-size: clamp(42px, 6.6vw, 112px);
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.04em;
  margin: 0 0 28px -0.065em;
}
.cm-hook {
  font-size: clamp(18px, 1.5vw, 22px);
  line-height: 1.55;
  letter-spacing: -0.015em;
  color: var(--ink-55);
  margin: 0 0 40px;
  /* The hook sits between the headline's 18ch and the body's 54ch, so it
     needs its own measure. It only ever had one inline, in px, and that is
     now gone with the rest of them. */
  max-width: 46ch;
}
/* The case facts, label above value, three across. They used to be four
   columns with two prose blocks under them, and five of the seven facts
   appeared in BOTH — the same three organisation numbers, the role and the
   team size, all within one screen. The prose also set its own labels inline
   at 10px/700/uppercase, a component that exists nowhere in this stylesheet,
   which is why it read as borrowed from another site.
   Ruled rows were tried first and carried too much vertical weight this early
   in the sheet; a paired grid is about a third shorter and the eye takes a
   whole row at once. No rules — the pairing does the grouping.
   Platform spans the full width because it is the one fact that is a
   sentence: it is the only line on the page that says what the product
   actually is, and squeezed into a third it either wrapped to four lines or
   had to be cut down to its name. */
.cm-facts { display: grid; gap: 36px 48px; margin: 44px 0 0; }
@media (min-width: 760px) {
  .cm-facts { grid-template-columns: repeat(3, 1fr); }
}
.cm-fact dt { font-size: 15px; color: var(--ink-55); margin-bottom: 7px; }
.cm-fact dd { font-size: 18px; color: var(--ink); margin: 0; max-width: 60ch; }

/* Stacked to one column, the desktop spacing turned six facts into most of a
   phone screen before the first image. The pair is tightened so a label reads
   as attached to its value rather than floating between two of them, and the
   gap between pairs stays comfortably larger than the gap inside one. */
@media (max-width: 759px) {
  .cm-facts { gap: 20px; margin-top: 32px; }
  .cm-fact dt { font-size: 13px; margin-bottom: 2px; }
  .cm-fact dd { font-size: 15px; line-height: 1.4; }
}

/* ── Headings and copy ─────────────────────────────────── */
/* 80px, not 52. On the reference the headline IS the argument — there is not
   one paragraph over 120 characters on the whole case — so it carries the
   weight that body copy carries elsewhere. */
.cm-headline {
  /* 52 and 36ch, measured rather than picked. At 80px every headline in the
     case broke over four or five lines inside an 18ch measure, which reads as
     cropped — the eye gets a column of fragments instead of a sentence. 52px
     44px against the two-thirds measure. Measured, not guessed: at 837px a
     50px headline breaks four of the eight over three lines, and nothing above
     38px fixes that by size alone. So the two longest headlines were trimmed
     instead — both were padded — and 44px then puts every one of them on two
     lines while keeping the type substantial. */
  font-size: clamp(28px, 2.9vw, 44px);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0 0 32px;
  max-width: var(--cm-measure);
  /* NOT balanced. Balance evens the two lines, which on this measure left the
     widest line using as little as 58% of the box — so a headline told to run
     to two thirds of the column visibly did not. Normal wrapping fills the
     measure (76-99% across the eight) and costs nothing: the line counts are
     identical either way, measured. */
}
/* Supporting, and sized to stay that way next to an 80px headline. */
.cm-body {
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-55);
  margin: 0;
  max-width: var(--cm-measure);
}

/* ── Challenge cards ───────────────────────────────────── */
/* The cards carry padding so the hover fill has somewhere to sit, and the
   grid is pulled out by that same amount so the first column's text still
   starts on the gutter. The gap comes down by roughly what the padding adds,
   so the space BETWEEN two blocks of text is unchanged. */
.cm-challenge-grid { display: grid; gap: 8px 16px; margin-top: 72px; margin-inline: -24px; }
@media (min-width: 900px) { .cm-challenge-grid { grid-template-columns: repeat(3, 1fr); } }
.cm-challenge-card {
  background: none;
  border: 0;
  padding: 24px;
  border-radius: 14px;
  /* Symmetric ease-in-out, so the fill arrives and leaves at the same rate.
     200ms: fast enough to feel attached to the pointer, slow enough to read as
     a transition rather than a flash. One rule, all three sheets. */
  transition: background-color 200ms cubic-bezier(0.45, 0, 0.55, 1);
}
.cm-challenge-card:hover { background: var(--surface); }
@media (hover: none) { .cm-challenge-card:hover { background: none; } }
/* Drawn, not a webfont. These nine slots held Phosphor classes for a font the
   page never loaded, so each one reserved 24px of nothing. Same line weight
   and viewBox as the front page's icons, and they take currentColor. */
.cm-challenge-icon { color: var(--ink); margin-bottom: 16px; display: block; }
.cm-challenge-title { font-size: 19px; font-weight: 500; letter-spacing: -0.02em; margin: 0 0 10px; color: var(--ink); }
.cm-challenge-body { font-size: 16px; line-height: 1.6; color: var(--ink-55); margin: 0; }

/* ── Result stats ──────────────────────────────────────── */
/* White too. The closing stats were the last inverted block. */
/* It carries --light now, so it takes the gutter from the section rule like
   everything else. It never had padding of its own: it matched neither
   --light nor --dark, so the one rule that sets the sheet's gutter skipped
   it entirely and the closing headline and stats sat flush against the panel
   edge while every section above them was indented 72px. Its quote row moved
   out to be a sibling at the same time — every other row in the sheet is one,
   which is what lets a row bleed while its heading stays on the gutter. */
.cm-close-section { background: var(--bg); color: var(--ink); }
/* A section ending on the team photo does not need the full gap under it —
   the photo is the closing image, and a section-sized void below it read as
   the sheet having finished early. */
/* Close up under a trailing figure only when something follows it. On Maersk
   and Formalize the quotes come next, so the tight gap joins them; on Matas the
   close IS the last block, and the same rule left the photograph sitting almost
   on the bottom edge of the sheet. */
.cm-close-section:has(> .cm-team:last-child):has(+ *),
.cm-close-section:has(> .cm-photos:last-child):has(+ *) { padding-bottom: calc(var(--sec) * 0.28); }
/* The quotes are the close's last beat, not a new section. A full section gap
   above them read as a break after the result rather than the end of it. */
.cm-close-section + .cm-section--light { padding-top: calc(var(--sec) * 0.36); }
.cm-close-headline {
  font-size: clamp(28px, 3.4vw, 52px);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -0.035em;
  color: var(--ink);
  margin: 0 0 56px;
  /* 22ch put all three close headlines on three lines, and the last line was
     a word or two — the shape read as an overflow rather than a decision. */
  max-width: 30ch;
}
/* Five stats in a grid hard-pinned to four columns left the fifth alone on a
   row of its own. auto-fit takes the count from the content instead, so the
   row reflows at any width without anyone having to keep a number in the
   stylesheet in step with a number in the markup. */
.cm-close-stats {
  display: grid;
  gap: 44px 32px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.cm-close-stat { border: 0 !important; padding: 0 !important; }
.cm-close-stat__val {
  font-size: clamp(36px, 4vw, 60px);
  font-weight: 400;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--ink);
  margin: 0 0 12px;
  font-variant-numeric: tabular-nums;
}
.cm-close-stat__label { font-size: 15px; color: var(--ink-55); margin: 0; max-width: 24ch; }
/* The team, under the numbers that describe it. Cropped to 16:9 from the
   photograph's own 4:3, and anchored to the BOTTOM so the whole of that
   quarter comes off the top — which is ceiling and a light fitting. Anchored
   any other way it would start taking feet. */
.cm-team { margin: 56px 0 0; border-radius: 16px; overflow: hidden; }
.cm-team img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: bottom;
  display: block;
}
/* The arrow inherited the stat's 60px and came out heavier than the numbers it
   joins. It is a connector, not a value. */
.cm-stat-arrow {
  font-size: 0.45em;
  color: var(--ink-55);
  margin: 0 0.22em;
  vertical-align: 0.32em;
  letter-spacing: 0;
}

/* ── Modal carousels ───────────────────────────────────── */
/* The rows are full-bleed so cards can run off the right edge, but the
   track's OWN padding lines the first card up with the section content, and
   scroll-padding is the half that matters — without it snap aligns to the
   scrollport edge, so every first card came to rest 72px left of the heading
   above it and the arrows sat flush against the panel. Same fix, and the same
   reason, as the front page's rows. */
.cm-car-wrap { padding: 0 !important; overflow: visible; }
/* The sheet ends on a row, and a row has no padding of its own, so the last
   card sat hard against the bottom of the panel. */
.cm-car-wrap:last-child { margin-bottom: clamp(72px, 9vw, 150px); }
.cm-car__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
  padding-bottom: 4px;
  padding-inline: var(--cm-gutter);
  scroll-padding-inline: var(--cm-gutter);
  /* Stretch, so every card in a row is the same height — the same trade the
     page rows make: nothing is clipped, so the longest card sets the height. */
  align-items: stretch;
}
.cm-car__track::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { .cm-car__track { scroll-behavior: auto; } }
/* Nearly the whole width. The reference runs its case imagery big enough that
   one slide is unambiguously the subject; the next card is present only as a
   sliver, enough to say the row continues and no more. At 72% the second
   monitor was showing a third of itself and competing with the first.
   The calc leaves the gap plus about 80px of the next card. */
.cm-car__col {
  scroll-snap-align: start;
  flex: 0 0 auto;
  width: min(1160px, calc(100% - 104px));
}
/* Nine result cards, so they run three across rather than one at a time.
   The 72% width above belongs to the case screenshots, where one slide IS the
   subject; a result card is a caption with a picture over it, and at 900px it
   read as a banner. The calc divides the track's own content box, so three
   land exactly inside the gutters. */
.cm-car__col--narrow { width: clamp(260px, calc((100% - 48px) / 3), 420px); }
/* The quote row is three across too. Its cards used the old site's inner
   class names — testimonial-quote, testimonial-avatar, testimonial-meta —
   none of which exist in this stylesheet, so they rendered unstyled: raw
   paragraphs, default margins, and an avatar drawing at its natural 800px.
   They use the front page's card now, which is the same card the reader has
   already seen once. */
.cm-car__col--ds { width: auto; }
/* One card per screen on a phone, with the least sliver that still says the
   row continues. This has to sit AFTER the modifiers: --narrow and --quote
   carry a clamp whose 260px floor is what actually applied at this width, and
   at equal specificity the later rule is the one that wins. Every extra pixel
   of width here is a line of quote the card does not have to grow to hold.
   --ds keeps its intrinsic width. */
@media (max-width: 900px) {
  .cm-car__col,
  .cm-car__col--narrow { width: calc(100% - 18px); }
}
/* No dimming. It belonged to a design that showed one card at a time; here
   several are visible and fading the neighbours just makes the row murky. */
.cm-car__col.is-peek, .cm-car__col.is-hidden { opacity: 1 !important; }
.cm-car__slide, .cm-car__card-img {
  border-radius: 14px;
  overflow: hidden;
  background: var(--surface);
}
/* A little taller than the 1.647 the mockups were rendered at. The extra
   height is taken off the sides rather than added to the top, so the monitor
   keeps its proportions and the frame just sits closer around it. */
.cm-car__slide { aspect-ratio: 3 / 2; }
.cm-car__slide img { width: 100%; height: 100%; object-fit: cover; }
/* On a phone the card can only get so wide before the next one stops peeking,
   so the scene gets taller instead — a 5:4 box makes the monitor read a third
   larger without touching the width. The crop that buys it takes 24% off the
   sides, and all three monitors sit inside the remaining window; any squarer
   and the one in the operator shot starts losing its right edge. */
@media (max-width: 900px) { .cm-car__slide { aspect-ratio: 5 / 4; } }

/* One grey card, top to bottom. The illustration plates were a pale blue that
   sat 20 levels off this grey — close enough to look like a mistake rather
   than a choice, so the plates were repainted to --surface and the card now
   reads as a single surface with the artwork floating on it.
   height:100% makes the cards in a row match: the track already stretches the
   COLUMN, but the card inside it was sized by its own content. */
.cm-car__text-card {
  background: var(--surface);
  border-radius: 16px;
  overflow: hidden;
  height: 100%;
  display: flex;
  flex-direction: column;
}
.cm-car__text-card .cm-car__card-body-wrap { flex: 1; }
/* The illustrations are 800x560 and they are drawings, not photographs — a
   crop throws away the subject rather than the edges. They were being covered
   into a 140px band, which at card width is a 6.4:1 slot: what survived was a
   giant cropped head on one card and half an arrow on the next. The box takes
   the artwork's own ratio instead, so nothing is cut. */
.cm-car__card-img { aspect-ratio: 800 / 560; }
.cm-car__card-img img { width: 100%; height: 100%; object-fit: contain; }
.cm-car__card-body-wrap { padding: 14px 24px 24px; }
/* The plate already carries 6% of its own height as margin above and below the
   artwork, so the 14px on top of that made the gap under the picture nearly
   twice the gap above it. Dropping it leaves the plate's own margin doing both
   sides, which is the only way the two can actually match: one of them is
   inside the image and cannot be reached from here. */
.cm-car__text-card--flush .cm-car__card-body-wrap { padding-top: 0; }
.cm-car__text-card--flush .cm-car__card-img { aspect-ratio: 800 / 350; }
/* These three are flat illustrations at their own odd ratios, contained in a
   box wider than any of them — so each one meets the top and bottom of its
   box exactly, with no margin at all. The padding is the margin. */
/* Percentage padding, so it holds at any card width. 26px left the artwork at
   82% of the box height, which is what "not fitting" looked like; this brings
   it to about 60% with real room on every side. */
.cm-car__text-card--inset .cm-car__card-img { padding: 14% 12%; }
.cm-car__card-num { font-size: 13px; color: var(--ink-55); margin: 0 0 10px; font-variant-numeric: tabular-nums; }
.cm-car__card-title { font-size: 18px; font-weight: 500; letter-spacing: -0.02em; margin: 0 0 8px; color: var(--ink); }
.cm-car__card-body { font-size: 15px; line-height: 1.55; color: var(--ink-55); margin: 0; }
.cm-car__cap { margin-top: 20px; }
.cm-car__cap-title { font-size: 17px; font-weight: 500; margin: 0 0 6px; color: var(--ink); }
.cm-car__cap-body { font-size: 15px; color: var(--ink-55); margin: 0; }
.cm-car__footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: 24px; margin-top: 32px;
  padding: 0 var(--cm-gutter) !important;
}
/* The hidden attribute only carries display:none from the UA sheet, and the
   author rule above beats it. Without this the footer stayed on screen while
   the JS believed it had put it away. */
.cm-car__footer[hidden], .car-nav[hidden] { display: none; }
/* Tighter under the row on a phone. 32px is right beside a wide desktop row;
   under a single full-width card it reads as a gap rather than a join.
   This has to sit AFTER .cm-car__cap and .cm-car__footer, not in the mobile
   block further up: a media query adds no specificity, so at 900px the later
   base rule was simply winning and neither value applied. */
@media (max-width: 900px) {
  .cm-car__cap { margin-top: 14px; }
  .cm-car__footer { margin-top: 18px; }
}
.cm-car__controls { display: flex; gap: 8px; }
.cm-car__btn {
  width: 44px; height: 44px; border-radius: 100px;
  border: 1px solid var(--line); background: none; color: var(--ink);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  transition: border-color .2s ease, opacity .2s ease;
}
.cm-car__btn:hover:not(:disabled) { border-color: var(--ink); }
.cm-car__btn:disabled { opacity: 0.3; cursor: default; }
/* The design-system row sizes by HEIGHT, not width. Its twelve component
   sheets run from 0.68 to 2.76 in aspect, so a common width would make the
   tall ones tiny and the wide ones enormous; a common band height keeps them
   comparable. Nothing sized them at all before, so they rendered at intrinsic
   size — the first sheet is 3322px wide in a 1400px track. The arrows could
   not reach its far end, and mandatory snap refused to rest anywhere that
   would have shown it. The cap is in px and vw, both definite: a percentage
   against a shrink-to-fit column is circular. */
/* No radius and no clip. The card lost its grey ground, so the rounded corner
   had nothing left to round except the artwork — and these sheets carry their
   label 2 to 5 source pixels from the left edge, which at 3322px shown in
   862px is under one display pixel. The corner was taking a bite out of the
   N in "Navigation". The glyph was fine in the file all along. */
.cm-ds-card {
  background: none;
  height: clamp(300px, 40vh, 480px);
}
.cm-ds-card__img {
  display: block;
  height: 100%;
  width: auto;
  max-width: min(900px, 84vw);
  object-fit: contain;
  margin: 0 auto;
}

/* ── Before / after reveal ───────────────────────────────
   Four screenshots became two. Side by side, the reader has to hold one image
   in their head while looking at the other; stacked in one frame with a
   divider they drag, the comparison happens in one place and the changes
   announce themselves.
   The "after" is the base layer and the "before" is clipped over it, so the
   frame shows the finished product when nothing has been touched. */
.cm-reveal {
  --x: 50%;
  position: relative;
  margin: 44px 0 0;
  overflow: hidden;
  border-radius: 12px;
  background: var(--bg);
  box-shadow: 0 1px 2px rgba(0,0,0,.05), 0 10px 28px rgba(0,0,0,.09);
  touch-action: pan-y;          /* vertical scroll still belongs to the page */
}
/* Dragging across a picture is, to the browser, either a text selection or the
   start of a native image drag. Both fire here: the first painted the left
   image in selection blue, the second would hand the file to whatever the
   pointer was released over. Selection is refused on the whole frame — Safari
   still wants the prefix — and the images are taken out of hit-testing
   entirely, which also stops them being drag sources. The frame keeps every
   pointer event because nothing inside it competes for them. */
.cm-reveal, .cm-reveal * { -webkit-user-select: none; user-select: none; }
.cm-reveal img { -webkit-user-drag: none; pointer-events: none; }
/* These are two separate comparisons, not two rows of one image, so they take
   the space between two figures. 28 read as a seam; 56 still had them touching
   as a pair. */
.cm-reveal + .cm-reveal { margin-top: 88px; }
.cm-reveal__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* clip-path rather than width, so the before image is cropped instead of
   squashed — the two halves have to stay in register as the divider moves. */
.cm-reveal__clip { position: absolute; inset: 0; clip-path: inset(0 calc(100% - var(--x)) 0 0); }
.cm-reveal__handle {
  position: absolute; top: 0; bottom: 0; left: var(--x);
  width: 44px; margin-left: -22px;      /* 2px line, 44px grab area */
  cursor: ew-resize; display: flex; align-items: center; justify-content: center;
}
.cm-reveal__line { width: 2px; height: 100%; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.14); }
/* The dot is a real element rather than ::after, so the grip can sit inside
   it — a pseudo-element paints over its own children and would have covered
   the icon. */
.cm-reveal__grip {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
  width: 38px; height: 38px; border-radius: 100px; background: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.2);
  display: flex; align-items: center; justify-content: center; color: var(--ink);
}
.cm-reveal__handle:focus-visible { outline: none; }
.cm-reveal__handle:focus-visible .cm-reveal__grip { box-shadow: 0 0 0 3px var(--blue); }
/* Solid grey, not translucent white. These labels sit on captures of a white
   app whose own panels are near-white, so a 90% white chip had nothing to
   separate it from the screenshot behind it and read as part of the UI. Grey
   is a value the app itself does not use at this size, and ink on it measures
   well past the floor. */
.cm-reveal__tag, .cm-reveal figcaption {
  position: absolute; z-index: 2;
  font-size: 13px; border-radius: 100px; padding: 6px 13px;
  background: #e6e6e6; color: var(--ink);
}
.cm-reveal__tag--a { top: 14px; left: 14px; }
.cm-reveal__tag--b { top: 14px; right: 14px; }
.cm-reveal figcaption { left: 14px; bottom: 14px; color: var(--ink); }

/* ── Section breakers ────────────────────────────────────
   Full-bleed photographs between sections, at the three points the live case
   puts them: after the hero, after the research findings, and after the
   loyalty design. They are the only photographs in this sheet and the only
   place it carries colour beyond the product UI — which is the same licence
   the films already have.
   The membership figure sits on the second one, in ink. The live version sets
   it in rose, which measures 2.3:1 on that pink; ink measures 11:1 on the same
   ground and the number is the thing being read. */
.cm-breaker { margin: 0; padding: clamp(56px, 7vw, 96px) var(--cm-gutter); line-height: 0; position: relative; }
.cm-breaker img { width: 100%; display: block; border-radius: 16px; }
.cm-breaker__stat { position: absolute; right: calc(var(--cm-gutter) + 6%); top: 50%;
                    transform: translateY(-50%); text-align: center; line-height: 1.05; }
.cm-breaker__num { display: block; font-size: clamp(44px, 7vw, 96px); font-weight: 400;
                   letter-spacing: -0.04em; color: var(--ink); }
.cm-breaker__lbl { display: block; font-size: clamp(14px, 1.7vw, 22px); color: var(--ink); letter-spacing: 0.02em; }

/* ── Old against new ─────────────────────────────────────
   The live case's comparison, on --surface instead of navy: the same screen
   before and after, labelled and with an arrow between. It replaced a
   before/after slider, which hid half the evidence behind a gesture — here the
   whole point is that the two are unrecognisable from each other, and that
   only lands if both are on screen at once. */
.cm-oldnew { display: grid; gap: 24px; margin-top: 44px; justify-items: center; align-items: center;
             background: var(--surface); border-radius: 20px;
             padding: clamp(32px, 5vw, 64px) clamp(24px, 4vw, 48px); }
@media (min-width: 760px) { .cm-oldnew { grid-template-columns: 1fr auto 1fr; gap: clamp(24px, 5vw, 72px); } }
.cm-oldnew__col { display: flex; flex-direction: column; align-items: center; width: 100%; }
.cm-oldnew__lab { font-size: 14px; color: var(--ink-55); margin: 0 0 20px; }
.cm-oldnew__col img { width: min(100%, 300px); height: auto; display: block; border-radius: 14px;
                      box-shadow: 0 1px 2px rgba(0,0,0,.05), 0 10px 28px rgba(0,0,0,.09); }
.cm-oldnew__arrow { display: flex; align-items: center; justify-content: center; color: var(--ink); }
@media (max-width: 759px) { .cm-oldnew__arrow svg { transform: rotate(90deg); } }

/* ── The two mechanics ───────────────────────────────────
   The live case's layout: each mechanic gets a panel, with the badge the app
   actually shows above the screen it lives on. On --surface rather than the
   original pink, and the badges keep their colour because they are product UI,
   which is one of the two places colour is allowed here. */
/* 72, not 40. These panels are a new idea rather than a continuation of the
   paragraph above them, and at 40 they sat closer to that paragraph than the
   panels sit to each other. */
.cm-mech { display: grid; gap: 20px; margin-top: 72px; }
@media (min-width: 760px) { .cm-mech { grid-template-columns: 1fr 1fr; gap: 24px; } }
.cm-mech__panel { background: var(--surface); border-radius: 18px; padding: 36px 28px 36px;
                  display: flex; flex-direction: column; align-items: center; }
.cm-mech__who { font-size: 14px; color: var(--ink-55); margin: 0 0 8px; }
.cm-mech__what { font-size: 24px; font-weight: 500; letter-spacing: -0.025em; color: var(--ink); margin: 0 0 28px; }
.cm-mech__badge { width: clamp(150px, 19vw, 200px); height: auto; display: block; margin-bottom: 32px; }
/* A fixed frame the capture sits in, rather than running off the bottom of the
   panel — that way both end on the same line and both are rounded on all four
   corners. The ratio is the points capture's own 657:1800, so the frame shows
   the whole screen at any width and needs no length of its own; the rewards
   capture is 649:1800 and loses ~13px off the bottom to match. */
.cm-mech__shot { width: min(100%, 394px); aspect-ratio: 657 / 1800;
                 overflow: hidden; border-radius: 14px;
                 box-shadow: 0 1px 2px rgba(0,0,0,.05), 0 10px 28px rgba(0,0,0,.09); }
.cm-mech__screen { width: 100%; height: auto; display: block; }
/* Mobile takes the live case's proportions. The frame is sized to the panel on
   desktop, and on a phone that let one screen run ~318px wide and ~870px tall —
   a single mechanic filling most of the viewport. The live case pins the shot
   at 170px and the badge at 150px and tightens the gaps, so both mechanics read
   as a pair on one screen. Below the base rules, not above: a media query adds
   no specificity, so source order is what decides this. */
@media (max-width: 759px) {
  .cm-mech { margin-top: 44px; }
  .cm-mech__panel { padding: 28px 20px; }
  .cm-mech__what { font-size: 21px; margin-bottom: 20px; }
  .cm-mech__badge { width: 150px; margin-bottom: 14px; }
  .cm-mech__shot { width: 170px; }
}

/* ── Equal spend ─────────────────────────────────────────
   The live case's shape, on --surface instead of navy: a panel holding two
   tracks, each with the share of the membership filled in. The bottom 50% fill
   up from the floor and the top 1% sits as a band at the ceiling, which is
   where those groups actually are on a spending scale. The pill between makes
   the claim the two are equal.
   The 1% band is drawn at 9%: a true 1% of a 260px track is under 3px, which
   reads as a hairline rather than a quantity. The number is written inside it. */
/* Full width, with the two tracks meeting in the middle. A 1fr/auto/1fr grid
   centred each bar inside its own half, which pushed them to opposite ends of
   the panel and left the comparison happening across a gap. Flex sizes them to
   their content and centres the group. */
.cm-spend { display: flex; justify-content: center; align-items: center;
            gap: clamp(24px, 6vw, 88px); margin-top: 44px;
            background: var(--surface); border-radius: 20px;
            padding: clamp(32px, 5vw, 56px) clamp(24px, 5vw, 64px); }
.cm-spend__col { display: flex; flex-direction: column; align-items: center; width: clamp(120px, 18vw, 170px); }
.cm-spend__bar { width: 100%; height: clamp(180px, 24vw, 260px);
                 background: var(--bg); border-radius: 12px; overflow: hidden;
                 display: flex; flex-direction: column; }
/* Rose, with the number in ink on top of it — white on this pink measures
   1.4:1 and would not be readable; ink measures 10.8:1. */
.cm-spend__fill { display: flex; align-items: center; justify-content: center;
                  background: var(--rose); color: var(--ink); font-size: 15px; font-weight: 500; }
/* Rounded only where the fill meets the end of its track. A fill is a portion
   of the track, so the edge it shares with the track takes the track's radius
   and the edge that cuts through the middle stays square — rounding all four
   made each fill read as its own floating pill. */
.cm-spend__fill--bottom { margin-top: auto; border-radius: 0 0 12px 12px; }
.cm-spend__fill--top { border-radius: 12px 12px 0 0; }
.cm-spend__k { text-align: center; margin: 16px 0 0; }
.cm-spend__k b { display: block; font-size: 17px; font-weight: 500; color: var(--ink); }
.cm-spend__k span { font-size: 14px; color: var(--ink-55); }
.cm-spend__eq { font-size: 14px; color: var(--ink); margin: 0 0 46px; background: var(--bg);
                border-radius: 100px; padding: 8px 16px; white-space: nowrap; }

/* ── The frequency matrix ────────────────────────────────
   The live case's shape, which carries an argument the ledger could not: the
   Points band runs the full width because everyone earns them, and the Rewards
   band sits under the high-frequency column alone. The spans ARE the point —
   two rows of matched text cannot say that. */
.cm-freq { margin-top: 44px; }
.cm-freq__row { display: grid; gap: 10px 28px; padding: 22px 0; border-top: var(--rule) solid var(--line); }
@media (min-width: 900px) { .cm-freq__row { grid-template-columns: 150px repeat(3, 1fr); gap: 28px; } }
.cm-freq__row--head { border-top: 0; padding-top: 0; }
.cm-freq__lab { font-size: 14px; color: var(--ink-55); margin: 0; }
.cm-freq__col { font-size: 17px; font-weight: 500; color: var(--ink); margin: 0; }
.cm-freq__dots { display: flex; gap: 7px; align-items: center; }
.cm-freq__dots i { width: 9px; height: 9px; border-radius: 100px; background: none; box-shadow: inset 0 0 0 1.5px var(--line); }
/* Rose for the filled dots, taken from the live case. The empty ones are a
   ring rather than a paler fill: rose on this grey measures about 1.2:1, so
   two filled-vs-unfilled tints of it would be indistinguishable — the
   difference has to be fill against no fill, not colour against colour. */
.cm-freq__dots i.is-on { background: var(--rose); }
.cm-freq__list { list-style: none; margin: 0; padding: 0; }
.cm-freq__list li { font-size: 16px; line-height: 1.55; color: var(--ink); }
/* Points spans all three columns, Rewards only the last. On a phone the grid
   collapses to one column and both bands simply stack, which still reads
   correctly — the labels say who each one is for. */
/* The rule is a pseudo-element, not a border-left. A border follows the box's
   own border-radius, so on a 12px-rounded card it bows away at both ends —
   which is what read as a curved line. This one is a straight 2px bar. */
/* Square on the left, rounded on the right. The rule is a straight bar and the
   card was rounded on all four corners, so the ground showed through in two
   little crescents where the corner curved away from it and the bar read as
   detached from the card it belongs to. */
.cm-freq__band { position: relative; background: var(--surface); border-radius: 0 12px 12px 0; padding: 18px 20px 18px 22px; }
.cm-freq__band::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--rose);
}
.cm-freq__band b { display: block; font-size: 17px; font-weight: 500; color: var(--ink); margin-bottom: 4px; }
.cm-freq__band span { font-size: 15px; color: var(--ink-55); }
@media (min-width: 900px) {
  .cm-freq__row--offer { row-gap: 12px; }
  .cm-freq__band--all  { grid-column: 2 / 5; }
  .cm-freq__band--high { grid-column: 4 / 5; }
}


/* ── Two products, design outside them ───────────────────
   Replaces an SVG that was blue, set in a serif, and drew two macOS windows
   with traffic lights — none of which this build uses. The claim is spatial:
   two products were inside the company and design was not, so a border does
   the work and the dashed card sits beyond it. Dashed means the same thing
   here as on the bet's open chip: a slot with nothing in it yet. */
.cm-inout { display: grid; gap: 24px; margin-top: 44px; }
/* Sized to content, not to the column. Stretched full width the two product
   cards read as panels of a layout rather than as two things sitting inside a
   boundary, and the boundary is the whole point. */
@media (min-width: 900px) { .cm-inout { grid-template-columns: auto auto auto; justify-content: start; align-items: center; gap: 20px; } }
.cm-inout__box { border: var(--rule) solid var(--line); border-radius: 18px; padding: 22px; position: relative; }
/* The label sits on the border, so it needs the page's own ground behind it
   to punch a hole in the line rather than crossing it. */
.cm-inout__lab { position: absolute; top: -9px; left: 22px; background: var(--bg); padding: 0 10px; font-size: 13px; color: var(--ink-55); }
.cm-inout__pair { display: grid; gap: 14px; }
/* 290, not 230. At 230 "Whistleblower Software" broke across two lines and
   its descriptor across two more, which made the two products look unequal
   when the point is that they are a matched pair. */
@media (min-width: 640px) { .cm-inout__pair { grid-template-columns: repeat(2, minmax(0, 290px)); } }
.cm-inout__prod { background: var(--surface); border-radius: 14px; padding: 26px 28px; }
/* 230px is the width it wants beside the box; stacked under it on a phone a
   short dashed card floating at half width reads as unfinished rather than as
   the pair's other half. */
.cm-inout__out { border: var(--rule) dashed var(--line); border-radius: 14px; padding: 26px 28px; width: 100%; }
@media (min-width: 900px) { .cm-inout__out { width: 230px; } }
/* Dashed, and pointing INTO the box. Design was outside and got pulled in a
   task at a time — the broken line is the "on request" part, and the direction
   is the only thing in the diagram that says design came from outside rather
   than simply sitting beside the products. */
.cm-inout__arrow { display: flex; align-items: center; justify-content: center; color: var(--ink-25); }
@media (max-width: 899px) { .cm-inout__arrow svg { transform: rotate(90deg); } }
.cm-inout__prod b, .cm-inout__out b { display: block; font-size: 19px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 6px; }
.cm-inout__out b { color: var(--ink-55); }
.cm-inout__prod span, .cm-inout__out span { font-size: 14px; color: var(--ink-55); }

/* ── Many obligations, one platform ──────────────────────
   The bet was two grey cards labelled Pain and Solution, which stated the
   argument without showing it. Naming the actual regulations does the showing:
   six of them against one panel is the whole claim, and it needs no adjective.
   The rules the earlier version drew under each name are gone — a list of six
   short names does not need six horizontal lines to be read as a list. */
.cm-many { display: grid; gap: 40px; margin-top: 44px; align-items: center; }
@media (min-width: 900px) { .cm-many { grid-template-columns: 1fr auto 0.9fr; gap: 48px; } }
/* The caption earns its place by adding the reason, not by describing the
   list. It read "what a mid-sized company is now on the hook for", which
   named the audience the headline had already named. Six regulations is only
   a problem because each one carries its own deadlines and evidence — that is
   the sentence, and it is what the panel opposite answers. */
.cm-many__cap { font-size: 15px; color: var(--ink-55); margin: 0 0 18px; }
.cm-many__arrow { display: flex; align-items: center; justify-content: center; color: var(--ink); }
@media (max-width: 899px) { .cm-many__arrow svg { transform: rotate(90deg); } }
.cm-many__one { border: var(--rule) solid var(--line); border-radius: 16px; padding: 34px; }
.cm-many__one-lab { font-size: 15px; color: var(--ink-55); margin: 0 0 10px; }
.cm-many__one-t { font-size: 24px; font-weight: 500; letter-spacing: -0.025em; line-height: 1.25; margin: 0 0 12px; }
.cm-many__one-b { font-size: 15px; line-height: 1.55; color: var(--ink-55); margin: 0; }

.cm-chips { display: flex; flex-wrap: wrap; gap: 10px; }
.cm-chip { border: var(--rule) solid var(--line); border-radius: 100px; padding: 9px 18px; font-size: 16px; color: var(--ink); }
/* The open slot is a shape, not a sentence. An earlier version ended the list
   with "and whatever lands next", which read as a joke where the section wants
   a claim; a dashed chip says the same thing and says it as the diagram. */
.cm-chip--open { color: var(--ink-55); border-style: dashed; }

/* ── Utilities the Formalize sheet needs ─────────────────
   Three shapes, each used more than once, rather than a bespoke block per
   section. Everything else on that sheet reuses what Maersk already built. */

/* Two blocks side by side, each a small label over a paragraph: the platform
   and my role, pain and solution, the two influence cards. */
.cm-duo { display: grid; gap: 40px; margin-top: 44px; }
@media (min-width: 900px) { .cm-duo { grid-template-columns: 1fr 1fr; gap: 48px; } }
.cm-duo__lab { font-size: 15px; color: var(--ink-55); margin: 0 0 10px; }
.cm-duo__t { font-size: 20px; font-weight: 500; letter-spacing: -0.02em; color: var(--ink); margin: 0 0 10px; }
.cm-duo p:last-child { margin: 0; }
.cm-duo__b { font-size: 15px; line-height: 1.55; color: var(--ink-55); margin: 0; }
.cm-duo--card > div { background: var(--surface); border-radius: 16px; padding: 32px; }
/* The one outbound link in any sheet. Blue is allowed here — the system
   reserves it for things you can press, and this is one. */
.cm-link { color: var(--blue); text-decoration: none; font-size: 15px; }
.cm-link:hover { text-decoration: underline; text-underline-offset: 3px; }


/* Product screens. Geometry ported verbatim from the live case: each card
   carries its own aspect ratio and the capture sits at natural scale, pinned
   by percentage and clipped by the card. That crop is load-bearing — these
   files carry a pale blue margin around the UI, and positioning them this way
   pushes it outside the card rather than showing it as a halo.
   The card is white with the same shadow the redesign captures use, not the
   blue-50 fill the live version had: colour is spent elsewhere in this build. */
.cm-shots { margin-top: 44px; display: grid; gap: 20px; }
.cm-shots__row { display: grid; gap: 20px; }
@media (min-width: 760px) {
  .cm-shots__row--2 { grid-template-columns: 1fr 1fr; }
  .cm-shots__row--3 { grid-template-columns: 1fr 1fr 1fr; }
}
.cm-shot {
  position: relative;
  overflow: hidden;
  border-radius: 16px;
  background: var(--surface);
  /* The card is --surface on a white page, which is a 8-level difference and
     effectively no edge at all. The captures are white-grounded, so without a
     line the UI ran straight into the page. */
  border: var(--rule) solid var(--line);
}
/* Centred horizontally and pinned by percentage from the top, at a width that
   leaves an even margin either side. A capture taller than its card runs past
   the bottom edge and is clipped there; one that fits is centred instead. The
   numbers are computed per card from the two ratios, not eyeballed. */
.cm-shot img {
  position: absolute; left: 50%; transform: translateX(-50%);
  height: auto; display: block;
  /* drop-shadow, not box-shadow: one of these captures is a cut-out with a
     transparent ground, and box-shadow would draw the rectangle it sits in
     rather than the shape you can see. */
  filter: drop-shadow(0 4px 14px rgba(0,0,0,.09)) drop-shadow(0 1px 2px rgba(0,0,0,.05));
}
/* Two of the captures are anchored into the corner and cropped by the card
   rather than sitting whole inside it, which is how the reference frames them:
   the panel reads as a window onto a larger screen instead of a picture of a
   panel. */
.cm-shot--corner img { left: 6%; transform: none; }

/* Three quotes rather than two. */
@media (min-width: 1100px) { .cm-pull-pair--three { grid-template-columns: repeat(3, 1fr); gap: 48px; } }
@media (min-width: 1100px) { .cm-pull-pair--three .cm-pull__q { font-size: clamp(18px, 1.5vw, 22px); } }

/* Three photographs, one row, sitting under the result stats the way the
   Maersk team shot does. */
.cm-photos { display: grid; gap: 20px; margin-top: 56px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
/* In colour, on request. The rule elsewhere is that colour belongs to
   pressable things and the product UI in the films, which is why the portrait
   and every avatar are greyscaled. These three are the deliberate exception. */
/* Anchored to the top, not centred. These are 0.77 portraits in a box nearer
   1.4, so about 46% of the height is cropped on a phone — a centred window
   takes the slice through the middle of the frame, which is a torso. 18% was
   not far enough; two of the three still lost the head. */
.cm-photos img { width: 100%; height: clamp(240px, 26vw, 380px); object-fit: cover; object-position: center top; border-radius: 14px; display: block; }

/* ── The transformation ledger ───────────────────────────
   Two grey cards holding two five-item lists became one table of five rows,
   because the content is five MATCHED PAIRS and the cards were presenting
   them as two independent lists — the reader had to pair them up and most
   would not. The comparison is the whole point of the section, so it is the
   structure now.
   The cards, the two icons and the arrow between them all went with it: the
   cards were doing the separating, and once the pairs are the structure there
   is nothing left for them to separate.
   These hairlines are the one place in the sheet that keeps a rule. They
   delimit the rows of a table, which is a different job from the section
   dividers that came out — those were separating a block from its heading,
   where space does it better. */
.cm-ledger { margin: 44px 0 0; }
.cm-ledger__head { display: grid; gap: 6px 48px; padding-bottom: 20px; }
@media (min-width: 900px) { .cm-ledger__head { grid-template-columns: 1fr 1fr; } }
.cm-ledger__lab { font-size: 15px; color: var(--ink-55); margin: 0 0 6px; }
.cm-ledger__t { font-size: 20px; font-weight: 500; letter-spacing: -0.02em; color: var(--ink); margin: 0; }
.cm-ledger__row {
  display: grid;
  gap: 6px 48px;
  padding: 20px 0;
  border-top: var(--rule) solid var(--line);
}
@media (min-width: 900px) { .cm-ledger__row { grid-template-columns: 1fr 1fr; } }
.cm-ledger__row:last-child { border-bottom: var(--rule) solid var(--line); }
/* Grey on the left, ink on the right — the same step the whole sheet uses to
   say "this was, this is". */
.cm-ledger dt { font-size: 17px; line-height: 1.5; color: var(--ink-25); }
.cm-ledger dd { font-size: 17px; line-height: 1.5; color: var(--ink); margin: 0; }

/* ── Before / after ────────────────────────────────────── */

.cm-ba-dark { display: grid; gap: 24px; margin-top: 48px; }
@media (min-width: 900px) { .cm-ba-dark { grid-template-columns: 1fr auto 1fr; align-items: center; } }
.cm-ba-dark__col { background: var(--surface); border-radius: 16px; padding: 32px; }
/* The redesign pair drops the card fill. These two hold real screenshots
   whose own ground is white, and a grey card behind a white screenshot draws
   a frame around the frame. A shadow separates the capture from the page
   instead — the picture gets an edge, the layout does not gain a box.
   Maersk's bet and Matas's transformation keep the fill: theirs hold
   illustrations on a grey plate, which needs the card to sit on. */
.cm-ba-dark__label { font-size: 15px; font-weight: 400; color: var(--ink-55); margin: 0 0 12px; }
.cm-ba-dark__title { font-size: 20px; font-weight: 500; color: var(--ink); margin: 0 0 12px; text-wrap: balance; }
/* Below 900 the two cards stack, so the arrow between them is pointing across
   a gap that no longer exists. Rotating the one glyph is enough — the markup
   stays a single arrow for both layouts.
   The titles balance because in a stacked card "Move containers from A to B"
   put the B alone on a second line. */
@media (max-width: 899px) {
  .cm-ba-dark__arrow svg { transform: rotate(90deg); }
  .cm-ba-dark__title { font-size: 18px; }
}
.cm-ba-dark__body { font-size: 15px; line-height: 1.6; color: var(--ink-55); margin: 0; }
.cm-ba-dark__arrow { color: var(--ink); display: flex; justify-content: center; align-items: center; }

/* ── Flow rows and spreads ─────────────────────────────── */
.cm-lego-row { display: grid; gap: 20px 40px; padding-block: 36px; align-items: start; }
/* The first row sits under the section's own paragraph, not under another
   row, so it needs the gap that separates a block from its intro rather than
   the one that separates two peers. Set on the intro, not the row: the rows
   are divs and so is the intro, so .cm-lego-row:first-of-type matched the
   intro instead — :first-of-type keys off the element type, never the class. */
.cm-lego-section > div:first-child { margin-bottom: 36px; }
/* 150, not 260. The label is two words and the row beside it is an eight-step
   flow that needs every pixel — at 260 the last step wrapped to its own line
   and broke the sequence in half. */
@media (min-width: 900px) { .cm-lego-row { grid-template-columns: 150px 1fr; gap: 20px 32px; } }
.cm-lego-row__label { display: flex; align-items: center; gap: 10px; }
.cm-lego-row__title { font-size: 19px; font-weight: 500; letter-spacing: -0.02em; color: var(--ink); margin: 0; }
/* Drawn marks, not emoji. A red cross and a green tick put two more hues on
   a page whose whole argument is that colour appears in two places only —
   and they were the last emoji left in the build. The cross is grey and the
   tick is ink, so the pair still reads as no/yes without any hue at all. */
.cm-lego-mark { display: inline-flex; color: var(--ink-25); flex: 0 0 auto; }
.cm-lego-mark--yes { color: var(--ink); }
.cm-lego-img { border-radius: 12px; }
/* ── The design-system flow ──────────────────────────────
   Was two flat composites with the labels and arrows baked into the artwork,
   which meant the type inside them was not the page's type and could not be
   read by anything but an eye. Each step is its own image now, the arrows are
   drawn, and the labels are real text in the site's own font.
   Steps size by HEIGHT and keep their own width: the first row's plates are
   937, 818 and 1176 wide against a common 800, and the second row's are all
   square, so a shared width would squash three of the eight. */
.flow {
  display: flex;
  align-items: flex-start;
  gap: clamp(10px, 1.2vw, 20px);
  flex-wrap: wrap;
}
/* Both rows span the same width so they start and end on the same verticals.
   The chains are different lengths — three steps against five — so packed
   left the short one stopped 328px early and the two read as unrelated. The
   gap is the only thing that stretches, which suits a flow: the arrows stay
   centred between their plates and the row just breathes wider.
   Wide only. Below 900 the row wraps, and space-between on a wrapped last
   line pushes its two survivors to opposite edges. */
@media (min-width: 900px) { .flow { justify-content: space-between; } }
.flow__step { margin: 0; flex: 0 1 auto; min-width: 0; }
.flow__step img {
  height: clamp(88px, 11vw, 148px);
  width: auto;
  display: block;
  border-radius: 10px;
}
.flow__step figcaption {
  font-size: 15px;
  color: var(--ink);
  /* Centred under its own plate. Left-aligned, a caption shorter than its
     image hung off the left edge and the label stopped reading as belonging
     to the picture above it. The 18ch cap stays so a long caption wraps
     instead of widening the step. */
  margin: 12px auto 0;
  max-width: 18ch;
  text-align: center;
}
/* Black, per the brief, and centred against the plate rather than the whole
   step — the caption below must not drag the arrow down with it. */
.flow__link {
  color: var(--ink);
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  height: clamp(88px, 11vw, 148px);
}
.flow__link--plus { font-size: 22px; line-height: 1; color: var(--ink-55); }
/* The two captions the comparison actually turns on. Everything else in the
   two chains is identical wording, so without a mark the reader has to spot a
   two-letter difference at the end of a row. Underlined rather than coloured
   or bolded: colour is reserved for pressable things here, and weight is
   already carrying the row labels. */
.flow__key { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
/* The first chain has three steps where the second has five, so at a shared
   height the short row looked like a thinner version of the same thing. The
   lead row goes bigger: it is the state the reader has to understand before
   the fix means anything. The link matches so the arrows stay centred on the
   plates rather than floating above them. */
.cm-lego-row--lead .flow__step img,
.cm-lego-row--lead .flow__link { height: clamp(110px, 14.5vw, 200px); }
/* The system and its instructions are two inputs to one step, so they are
   held together rather than reading as two more links in the chain. */
.flow__pair {
  display: flex;
  align-items: flex-start;
  gap: clamp(10px, 1.2vw, 20px);
  padding: 16px;
  margin: -16px;
  border-radius: 14px;
  background: var(--surface);
}
@media (max-width: 899px) {
  .flow { gap: 12px; }
  .flow__step figcaption { font-size: 13px; margin-top: 8px; }
}
/* A full-width image answering a headline. It sat 48px below it, which read as
   a caption rather than the evidence for the claim. */
/* 20px to match .cm-spend, .cm-oldnew and .cm-mech — this sits among them and
   was the only block still on the smaller radius. */
/* margin, not margin-top: a bare `figure` carries the UA stylesheet's
   `margin: 1em 40px`, so overriding only the top left 40px standing on each
   side and the map sat 80px narrower than every other image in the case. */
.cm-figure { border-radius: 20px; overflow: hidden; margin: 56px 0 0; background: var(--surface); }
/* Full width of the grid, not of the sheet: the same gutter every other image
   in the case sits behind, so the map lines up with the breakers above it. */
.cm-figure img { width: 100%; display: block; }
.cm-svg-spread { display: grid; gap: 40px; margin-top: 72px; }
@media (min-width: 900px) { .cm-svg-spread { grid-template-columns: repeat(3, 1fr); } }
.cm-svg-spread-label { font-size: 14px; font-weight: 500; color: var(--ink); margin: 0 0 14px; }
.cm-svg-spread-img { width: 100%; border-radius: 12px; background: var(--surface); }
.cm-skeleton { background: var(--surface); }
