/* Case and service pages (works/<slug>/): the block library in works/BLOCKS.md. Built on the
   landing's tokens (tokens.css) and type (typography.css); sizes are written in --u, so the
   page grows to 2560 with the rest of the site. Phones: case-mobile.css. */

/* ---- Page ------------------------------------------------------------------------- */

.case {
  --cb-gap: max(72px, calc(128 * var(--u)));    /* between blocks */
  --cb-tight: max(24px, calc(32 * var(--u)));   /* a block that continues the one before */
  --cb-col: max(24px, calc(32 * var(--u)));     /* between the halves of a split, cards, pictures */
  --cb-head: max(24px, calc(40 * var(--u)));    /* a stacked title to its content */
  --cb-body: var(--fs-body);                   /* the landing's body copy, max(18px, 18u) */
  --cb-title-fs: max(36px, calc(72 * var(--u)));
  --cb-label-fs: var(--fs-h6);
  --cb-lead-fs: max(22px, calc(30 * var(--u)));
  --cb-measure: 38em;                           /* a text column's longest line */
  --cb-note-fs: var(--fs-small);   /* a note's small print (.cb-text--note) */
  --cb-row-fs: var(--fs-list);  /* a row's title: the services rows (typography.css .list) */
  --cb-copy-fs: var(--fs-copy); /* a row's copy: the services copy (sections.css .what-copy) */
  --cb-list-pad: max(14px, calc(18 * var(--u))); /* a hairline list's row, above and below its text */
  /* A split's content stands beside its title with the tops of their x-heights level: both
     start at the block's top, and the content is moved by the difference between where each
     one's x-height begins. From the top of a line box that is half the leading plus the
     ascender above the x-height -- Archivo's own metrics (typo, which it asks browsers to
     use): ascender 0.878, descender 0.210, x-height 0.526 -- so (line-height - 1.088) / 2
     + 0.352: a title at 1.08 -> 0.348em, body and note at 1.6 -> 0.608em, a label at 1.2 ->
     0.408em, a lead at 1.4 -> 0.508em. The move can be either way (a label beside a larger
     lead goes up). --cb-xh scales it (1 = as measured; phones 0, the split stacks there). */
  --cb-xh: 1;
  --cb-shift-title: calc(var(--cb-xh) * (0.348 * var(--cb-title-fs) - 0.608 * var(--cb-body)));
  --cb-shift-note: calc(var(--cb-xh) * (0.348 * var(--cb-title-fs) - 0.608 * var(--cb-note-fs)));
  --cb-shift-label: calc(var(--cb-xh) * (0.408 * var(--cb-label-fs) - 0.608 * var(--cb-body)));
  --cb-shift-lead: calc(var(--cb-xh) * (0.408 * var(--cb-label-fs) - 0.508 * var(--cb-lead-fs)));
  /* A title beside a lead (Client), and beside numbers whose first line is an eyebrow in
     capitals (Milestones: its cap height, not an x-height -- 0.686 -- at its 1.5 leading,
     0.398em down) */
  --cb-shift-title-lead: calc(var(--cb-xh) * (0.348 * var(--cb-title-fs) - 0.508 * var(--cb-lead-fs)));
  --cb-shift-title-stats: calc(var(--cb-xh) * (0.348 * var(--cb-title-fs) - 0.398 * 12 * var(--ui-label)));
  /* The page's side gutters: the landing's main one (--page-x, 80u -- its hero, its
     services card, its close). (Tried at --page-x-lg, 120u: the page read narrower.) */
  --cb-x: var(--page-x);
  padding: 0 var(--cb-x);
  background: var(--white);
}

/* The landing's scene ground (the grey under its canvas, lit from above): the hero and the
   closing call stand on it, so the page opens and closes where the landing lives. Written
   on each, so each can move its own light (--scene-glow-at). */
.cb-hero,
.cb-cta {
  --cb-ground: radial-gradient(var(--scene-glow-size) at var(--scene-glow-at),
    color-mix(in srgb, var(--scene-bg), white var(--scene-glow)) 0%,
    var(--scene-bg) var(--scene-glow-mid),
    color-mix(in srgb, var(--scene-bg), black var(--scene-edge)) 100%);
}

/* The landing keeps its ground grey under the canvas; here the page is the white sheet */
html.case-page body { background: var(--white); }
/* The landing hides selection (its text is a picture of a story); a case is read */
html.case-page ::selection { background: var(--plate-grey); }

/* The header is see-through everywhere, as on the landing (case.js sets .case-head-solid
   once the hero's edge has passed under it: it only says the logo is back over the page).
   Over a hero with a picture it is two (case.js): the ink header, and over it a white copy
   (.case-head-light) that shows down to the line the grey plate has left -- --plate-y, the
   plate's lifting edge, run on the plate's own --cb-unveil and --ease-io so the two move as one;
   then --floor-y, the hero's rising floor as the page comes up over it -- with the ink one
   from that line down. The header turns colour along that edge, as the hero's text does.
   The ink header itself is never clipped: it is the one that takes the clicks, and a
   clipped-away part of an element takes none (the logo stopped working under the copy).
   The white copy simply covers it where it shows. */
@property --plate-y { syntax: "<length>"; inherits: false; initial-value: 0px; }
/* How long the hero's plate takes to lift off its picture -- the plate, the header's split
   and the page map's track all run on it */
html.case-page { --cb-unveil: 0.8s; }
html.case-page :is(.case-head-ink, .case-head-light) {
  --floor-y: 100vh;
  transition: --plate-y var(--cb-unveil) var(--ease-io);
}
html.case-page:has(.cb-hero-media:not(.cb-hero-media--mark).is-in) :is(.case-head-ink, .case-head-light) { --plate-y: 100vh; }
html.case-page:not(.case-js) :is(.case-head-ink, .case-head-light) { --plate-y: 100vh; transition: none; }
.case-head-light {
  clip-path: inset(0 0 calc(100% - min(var(--plate-y), var(--floor-y))) 0);
  pointer-events: none;
}
.case-head-light .logo { color: var(--white); }
.case-head-light .logo img { filter: brightness(0) invert(1); }
.case-head-light .menu-icon-bar { stroke: var(--white); }

/* The page map (global.css .scroll-progress, in `difference`): its track is near-black so
   it reads light on the white page, which over the picture's dark overlay reads black and
   is lost. Over the picture the track is light instead (and reads light grey) -- down to
   the same edge the header turns at: --map-y runs with the plate (--cb-unveil, --ease-io), and
   --floor-y is the hero's rising floor (case.js). Each segment cuts its track at that line,
   measured from its own top (--seg-top, case-progress.js) in a bar centred on the screen
   (global.css: --sp-h tall, plus its --sp-shift). */
@property --map-y { syntax: "<length>"; inherits: true; initial-value: 0px; }
html.case-page .scroll-progress {
  --floor-y: 100vh;
  transition: translate 0.45s var(--ease-out), opacity 0.45s ease, --map-y var(--cb-unveil) var(--ease-io);
}
html.case-page:has(.cb-hero-media:not(.cb-hero-media--mark).is-in) .scroll-progress { --map-y: 100vh; }
html.case-page:not(.case-js):has(.cb-hero-media:not(.cb-hero-media--mark)) .scroll-progress { --map-y: 100vh; transition: none; }
html.case-page .sp-seg {
  --cut: clamp(0px, calc(min(var(--map-y), var(--floor-y)) - (50vh - var(--sp-h) / 2 + var(--sp-shift, 0px)) - var(--seg-top, 0px)), 100%);
  background-image: linear-gradient(to bottom, var(--map-track-on-photo) 0 var(--cut), var(--map-track) var(--cut));
}

/* ---- Motion (js/case.js) ---------------------------------------------------------- */

/* The text waits for its line masks: html.case-js is set before the first paint (the
   inline script in the page; not under reduced motion). Keep the list in sync with REVEAL
   in case.js. If the script never runs, the text simply shows after 3s. */
html.case-js :is(
  .cb > .eyebrow, .cb-points-list > .eyebrow, .cb-hero-plate > .eyebrow, .cb-hero-title, .cb-title, .cb-label, .cb-sub, .cb-lead,
  .cb-text > p, .cb-text li, .cb-list > li, .cb-point > p, .cb-point-body > p,
  .cb-point-body li, .cb-card > p,
  .cb-meta dt, .cb-meta dd, .cb-stat .eyebrow, .cb-stat-value,
  .cb-callout-text, .cb-callout-act, .cb-cta .eyebrow, .cb-cta .display, .cb-cta .cta-act,
  .footer-layer p, .footer-layer .label, .footer-layer .small,
  .footer-layer .footer-col a, .footer-layer .footer-bottom > span
):not(.rv):not(.visually-hidden) {
  visibility: hidden;
  animation: case-unhide 0s linear 3s forwards;
}
@keyframes case-unhide { to { visibility: visible; } }

/* The hero's picture waits under its grey plate (the works tile's, opened out) until the
   header and the title are in, then the plate lifts off it top down (case.js .is-in) --
   the site's own wipe, a picture arriving over its plate. The plate is a layer over the
   whole hero (case.js builds it, laid out as the hero is) carrying a dark copy of the
   text: as its edge passes, the dark copy goes with it and the white text under it shows,
   so the colour turns exactly along the edge. */
.cb-hero-plate {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--cb-unveil, 0.8s) var(--ease-io);
  pointer-events: none;
}
.cb-hero-media.is-in ~ .cb-hero-plate { clip-path: inset(100% 0 0 0); }
/* The first paint can come before case.js has run (a module waits for the page) and built
   the plate: a plain grey stands in for it until then, so the page never opens on the bare
   picture -- that one frame was a flash between the tile's grey and this one. (Should the
   script never run, it steps aside after 3s.) */
html.case-js .cb-hero:not(:has(.cb-hero-plate)) .cb-hero-media::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--plate-grey) var(--plate-lit);
  animation: case-stand-in-off 0s linear 3s forwards;
}
@keyframes case-stand-in-off { to { opacity: 0; } }

/* Split display titles: each line clips its own copy of the gradient (the parent's would
   paint the glyphs at their resting place while the lines move under the masks), and the
   masks run deeper for the display sizes' descenders -- the landing's .display.rv rules */
:is(.cb-hero-title, .cb-title).rv:not(.rv-block) { background-image: none; }
:is(.cb-hero-title, .cb-title).rv:not(.rv-block) .rl {
  padding-bottom: 0.24em;
  margin-bottom: -0.24em;
}
:is(.cb-hero-title, .cb-title).rv:not(.rv-block) .rli {
  background-image: var(--text-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  padding-bottom: 0.2em;
  margin-bottom: -0.2em;
}

/* ---- Rhythm ----------------------------------------------------------------------- */

/* Block margins only: the full-bleed blocks (hero, closing call) set their own inline ones */
.case > .cb { margin-block: 0; }
.case > .cb + .cb { margin-top: var(--cb-gap); }
.case > .cb + .cb--tight { margin-top: var(--cb-tight); }

/* ---- Titles ----------------------------------------------------------------------- */

/* The display fill, as on the landing's headlines (typography.css .display) */
.cb-hero-title,
.cb-title {
  font-weight: 300;
  letter-spacing: var(--track-display);
  color: transparent;
  background-image: var(--text-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  text-wrap: balance;
}

.cb-hero-title { font-size: max(44px, calc(104 * var(--u))); line-height: 1.04; max-width: 14em; }
.cb-title      { font-size: var(--cb-title-fs);  line-height: 1.08; }
.cb-label {
  font-size: var(--cb-label-fs);
  font-weight: 300;
  line-height: 1.2;
  letter-spacing: var(--track-head);
  color: var(--ink);
}
.cb-sub {
  font-size: var(--cb-body);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: var(--track-body);
  color: var(--ink);
}
:is(.cb-hero-title, .cb-title, .cb-callout-text) em { font-style: italic; }

/* ---- Layouts ---------------------------------------------------------------------- */

/* Stacked: the title above, the content under it. (Not in the hero: its eyebrow and title
   are set by its own gap, as the plate's copy of them is.) */
.cb:not(.cb-hero) > :is(.cb-title, .cb-label, .eyebrow) + * { margin-top: var(--cb-head); }

/* Split: the title in the left half, the content in the right -- the right column starts
   on the page's centre line, where the landing's services copy and plate stand (the page's
   gutters are the landing's --page-x, so the centre is the same line on both); the title
   keeps a column's gap clear of it. Both from the block's top, the content then moved so
   the tops of the x-heights are level (--cb-xh above). */
.cb-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 0;
  align-items: start;
}
.cb-split > :first-child { padding-right: var(--cb-col); }
/* A split with no title: its content stands in the right half, where a split's content does */
.cb-split:not(:has(> :is(.cb-title, .cb-label, .eyebrow))) > :first-child { grid-column: 2; padding-right: 0; }
/* A split whose left column holds its title and a typed statement under it (a service's
   problem, its next step): the statement --cb-head under the title, as any content, and
   the text on the right, level with the title */
/* (the rows: the title's, the statement's, then one that takes what the text runs past them
   -- spanning the first two alone, the text spread its height over both and the statement
   fell far under its title) */
.cb-split:has(> .cb-typed) { grid-template-rows: auto auto 1fr; }
.cb-split > .cb-typed { grid-column: 1; padding-right: var(--cb-col); }
.cb-split > .cb-typed + .cb-text { grid-column: 2; grid-row: 1 / -1; margin-top: var(--cb-head); }
/* Figures after a split's text, under the same title (a service's "we've done this"): on
   the right too, under the text */
.cb-split > .cb-text + .cb-stats { grid-column: 2; margin-top: var(--cb-head); }
/* Side by side only: phones stack the split, and these (heavier than case-mobile.css's
   title gap) took the gap under every title there down to 0 */
@media (min-width: 768px) {
  .cb-split > :is(.cb-title, .cb-label, .eyebrow) + * { margin-top: 0; }
  .cb-split > .cb-title + .cb-text { margin-top: var(--cb-shift-title); }
  /* (a list: the row's padding first, then its 1.45 leading -> 0.533em to the x-height) */
  .cb-split > .cb-title + .cb-list { margin-top: calc(var(--cb-xh) * (0.348 * var(--cb-title-fs) - 0.533 * var(--cb-body) - var(--cb-list-pad))); }
  .cb-split > .cb-title + .cb-text--note { margin-top: var(--cb-shift-note); }
  .cb-split > .cb-label + .cb-text { margin-top: var(--cb-shift-label); }
  .cb-split > .cb-label + .cb-text:has(> .cb-lead:is(:first-child, .cb-inline-mark--float + *)) { margin-top: var(--cb-shift-lead); }
  .cb-split > .cb-title + .cb-text:has(> .cb-lead:is(:first-child, .cb-inline-mark--float + *)) { margin-top: var(--cb-shift-title-lead); }
  .cb-split > .cb-title + .cb-stats { margin-top: var(--cb-shift-title-stats); }
  .cb-split > .cb-title + .cb-typed { margin-top: var(--cb-head); }
  .cb-split > .cb-typed + .cb-text { margin-top: var(--cb-shift-title); }
}

/* ---- Hero: the scene's ground, full bleed, the title at its foot ------------------ */

/* The hero stays at the top while the page comes up over it and closes like the landing's
   screen before "what we do" (global.css .page-tint): its sides come in to the gutter and
   its floor rides on the edge of what follows (case.js writes --shrink, 0..1). Closed, it
   stops painting. */
.cb-hero {
  position: sticky;
  top: 0;
  clip-path: inset(0 calc(var(--shrink) * var(--cb-x)) calc(var(--shrink) * 100%) calc(var(--shrink) * var(--cb-x)));
  isolation: isolate;
  min-height: var(--case-hero-h);
  margin-inline: calc(-1 * var(--cb-x));
  background: var(--scene-bg) var(--cb-ground);
}
/* One layout for the hero and for the plate over it, so the plate's copy of the text lands
   on the text */
.cb-hero,
.cb-hero-plate {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  gap: max(20px, calc(32 * var(--u)));
  padding: calc(var(--header-h) + var(--cb-tight)) var(--cb-x) max(48px, calc(88 * var(--u)));
}

/* With a picture (the case's works tile, so the tile opens straight into it): the whole
   first screen, the picture behind the text under a dark overlay -- thick at the foot
   where the title stands and at the top under the header, thin between. The text on the
   picture is white (a solid, no display sweep); the plate over it carries the dark copy. */
.cb-hero.is-closed { visibility: hidden; }

.cb-hero:has(.cb-hero-media:not(.cb-hero-media--mark)) { min-height: 100svh; }
.cb-hero:has(.cb-hero-media) > .cb-hero-title { background-image: none; color: var(--white); }
.cb-hero:has(.cb-hero-media) > .cb-hero-title.rv .rli { background-image: none; }
.cb-hero:has(.cb-hero-media) > .eyebrow { color: var(--on-photo); }
.cb-hero-media {
  --cb-veil: linear-gradient(180deg,
    color-mix(in srgb, var(--plate-dark) 62%, transparent) 0%,
    color-mix(in srgb, var(--plate-dark) 28%, transparent) 30%,
    color-mix(in srgb, var(--plate-dark) 42%, transparent) 58%,
    color-mix(in srgb, var(--plate-dark) 88%, transparent) 100%);
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: var(--plate-grey) var(--plate-lit);
}
/* Larger than the screen by --cb-overscan, so the camera (case.js) can move it without
   showing an edge */
.cb-hero-media img {
  --cb-overscan: 1.08;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(var(--cb-overscan));
}

.cb-hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--cb-veil);
}

/* No picture (a service's page): the plate of "what we do" opened out to the first screen,
   the service's mark on it (case.js), large on the right where the plate held it. The
   ground stays light: no veil, the text in its own ink -- the display fill, as the plate's
   copy has it, so nothing turns along the lifting edge -- and the header and the page map
   keep their colours (the picture's rules above leave a mark out) */
.cb-hero-media--mark::after { content: none; }
/* The mark's ground and the hero's plate over it lit as the screen-high grey the page opened
   under (html.hero-mark::after, below): one light from the top of the screen, whatever the
   hero's height */
.cb-hero-media--mark,
.cb-hero:has(.cb-hero-media--mark) .cb-hero-plate { background-size: 100% 100vh; }
.cb-hero:has(.cb-hero-media--mark) > .cb-hero-title:not(.rv) { background-image: var(--text-gradient); color: transparent; }
.cb-hero:has(.cb-hero-media--mark) > .cb-hero-title.rv { color: transparent; }
.cb-hero:has(.cb-hero-media--mark) > .cb-hero-title.rv .rli { background-image: var(--text-gradient); }
.cb-hero:has(.cb-hero-media--mark) > .eyebrow { color: var(--muted); }
.cb-hero:has(.cb-hero-media--mark) > .cb-hero-title { max-width: 9em; } /* the right is the mark's */
.cb-hero-media--mark canvas {
  position: absolute;
  top: 50%;
  right: calc(var(--cb-x) + 20px); /* (20px in from the gutter, by eye: 12 Oct) */
  width: min(56svh, 40vw);
  height: min(56svh, 40vw);
  translate: 0 -50%;
}
/* The landing opens a service's plate out to the whole screen, as a tile (case-leave.js),
   so the page opens under the whole screen's grey (html.hero-mark, in the page's markup: there
   from the first paint): its foot rises to the hero's foot and
   stops there (case.js .hero-curtain-up, --hero-rest) -- the text below shows on white --
   and the hero's own plate, the same grey under it, takes over: the title rises on it and
   it lifts off the mark, as a case's off its picture. All three greys -- this one, the
   hero's plate and the mark's ground -- are lit as one screen-high plate (below), so none
   of them shows where it ends: sized to the hero, the light stopped short of a hero its title
   had made taller and left a flat band under it. Should the script never run, it steps
   aside after 10s. */
html.case-js.hero-mark::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-header) - 1);
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-curtain) var(--ease-io);
  pointer-events: none;
  animation: case-stand-in-off 0s linear 10s forwards; /* (3s, the stand-ins', came before a slow first load had run case.js: the grey went clear mid-rise) */
}
html.case-js.hero-mark.hero-curtain-up::after { clip-path: inset(0 0 var(--hero-rest, 0px) 0); }
html.case-js.hero-mark.hero-curtain-done::after { content: none; }

/* ---- Text ------------------------------------------------------------------------- */

.cb-text {
  max-width: var(--cb-measure);
  font-size: var(--cb-body);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-body);
  color: var(--text);
}
.cb-text > * + * { margin-top: 1em; }
.cb-text > .cb-sub + * { margin-top: 0.5em; }
.cb-text > * + .cb-sub { margin-top: 2em; }
/* A list of facts in running text stands off it: room over and under its rows */
.cb-text > * + .cb-list,
.cb-text > .cb-sub + .cb-list { margin-top: 1.5em; }
.cb-text > .cb-list + * { margin-top: 2em; }
.cb-text :is(strong, b) { font-weight: 500; color: var(--ink); }
.cb-text ul:not(.cb-list) { padding-left: 1.1em; }
.cb-text ul:not(.cb-list) > li + li { margin-top: 0.4em; }
.cb-text ul:not(.cb-list) > li::marker { color: var(--line); }

/* A standfirst: bigger, in ink, set like the landing's running headlines */
.cb-lead {
  font-size: var(--cb-lead-fs);
  line-height: 1.4;
  letter-spacing: var(--track-head);
  color: var(--ink);
}
.cb-text > .cb-lead:is(:first-child, .cb-inline-mark--float + *) { max-width: 30em; }
/* A text across the page (a service's standfirst, its one-line promise) */
.cb-text.cb-text--wide,
.cb-text.cb-text--wide > .cb-lead { max-width: none; }
/* A note: the landing's descriptive small print (sections.css .team-lead), a narrow column */
.cb-text--note {
  max-width: 26em;
  font-size: var(--cb-note-fs);
  line-height: 1.6;
}

.case a:not(.btn) {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--line);
  transition: text-decoration-color var(--dur-fast) ease;
}
.case a:not(.btn):hover { text-decoration-color: currentColor; }

/* One fact per row, a hairline under each; under the cursor the row's hairline inks in,
   left to right */
.cb-list {
  list-style: none;
  padding: 0;
  font-size: var(--cb-body);
  font-weight: 300;
  line-height: 1.45;
  letter-spacing: var(--track-body);
  color: var(--ink);
}
.cb-list > li {
  position: relative;
  padding: var(--cb-list-pad) 0;
  border-bottom: 1px solid var(--hairline);
}
.cb-list > li::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.45s var(--ease-out);
}
.cb-list > li:hover::after { transform: scaleX(1); }
/* Numbered (ol.cb-list--numbered): each row's number in the eyebrow voice the landing's
   services count in (sections.css .what-num: "/ 01"), in a column of its own, on the
   text's first baseline -- text baseline 1.059em down its 1.45 line, the number's 0.878em
   down its own */
.cb-list--numbered { counter-reset: cb-item; }
.cb-list--numbered > li {
  --cb-num-fs: var(--fs-cap);
  position: relative;
  padding-left: max(56px, calc(72 * var(--u)));
  counter-increment: cb-item;
}
/* A row can carry a title and its text (Challenge): the title set as a plain row reads,
   the text under it in the services' small print */
.cb-list-title { display: block; }
.cb-list-text {
  display: block;
  /* 44ch on the mock; a wider screen gives it more of the row, up to 62ch (~70 letters a
     line: inside the 45..75 a comfortable measure allows) */
  max-width: min(62ch, max(44ch, 72%));
  margin-top: 0.6em;
  font-size: var(--cb-copy-fs);
  line-height: 1.5;
  color: var(--text);
}
.cb-list--numbered > li::before {
  content: "/\2002" counter(cb-item, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: calc(var(--cb-list-pad) + 1.059 * var(--cb-body) - 0.878 * var(--cb-num-fs));
  font-size: var(--cb-num-fs);
  font-weight: 400;
  line-height: normal;
  letter-spacing: var(--track-caps);
  color: var(--muted);
}

/* ---- Meta: the facts of the project, side by side --------------------------------- */

/* Small print: the keys are eyebrows (dt.eyebrow, slash and all -- typography.css), the
   values a step under the body */
/* Under the row, a hairline closes it off from what follows */
.cb:has(> .cb-meta) {
  padding-bottom: var(--cb-head);
  border-bottom: 1px solid var(--hairline);
}
.cb-meta > div { padding-right: var(--cb-col); } /* each fact keeps clear of the next, the last of the edge */
.cb-meta {
  display: flex;
  justify-content: space-between;
  align-items: start;
  gap: var(--cb-head) var(--cb-col);
  font-size: var(--fs-body-sm);
  letter-spacing: var(--track-body);
}
.cb-meta dd {
  margin: max(10px, calc(12 * var(--u))) 0 0;
  white-space: nowrap; /* each value on one line, however long (phones let them wrap) */
  font-weight: 300;
  line-height: 1.5;
  color: var(--ink);
}

/* ---- Stats: set like the numbers around the landing's globe (sections.css .stat): the
   label an eyebrow over its figure in the stats' heading step (.h4), no rules. Each row
   shares its width among its numbers. -------------------------------------------- */

.cb-stats { display: flex; flex-direction: column; gap: max(32px, calc(48 * var(--u))); }
.cb-stats-row { display: flex; gap: max(48px, calc(64 * var(--u))); } /* two figures side by side: twice --cb-col, they read as two */
.cb-stat {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* A label here can run to a sentence: let it wrap, unlike an eyebrow standing alone */
.cb-stat .eyebrow { white-space: normal; line-height: 1.5; }
.cb-stat-value {
  font-size: var(--fs-h4);
  font-weight: 300;
  line-height: 1.4;
  letter-spacing: var(--track-head);
  color: var(--ink-2);
  white-space: nowrap;
}

/* ---- A starry stretch behind a block (.cb--sky, js/ui/case-sky.js) ---------------------
   The landing's works sky on the white page, behind a few blocks only: across the page's
   width, half a gap into the white above and below, and fading out there, so it has no
   edge. Under the text (the block is its own layer). */
.cb--sky { position: relative; isolation: isolate; }
.cb-sky {
  position: absolute;
  z-index: -1;
  inset: calc(-0.5 * var(--cb-gap)) calc(-1 * var(--cb-x));
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
}
.cb-sky canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- Cards: panels of the "what we do" ground, equal widths ----------------------- */

.cb-cards {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--cb-col);
}
.cb-card {
  padding: max(24px, calc(36 * var(--u)));
  background: var(--plate-grey) var(--plate-lit);
  font-size: var(--cb-body);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-body);
  color: var(--text);
}
.cb-card > .cb-sub { margin-bottom: 0.75em; }

/* ---- Typed statement: one strong sentence, typed in as it comes up the screen -- the
   landing's featured review (featured-review.js, sections.css .quote-featured: the
   first-line indent, the glyphs, the caret), without its signature. --------------------- */
.cb-typed { --quote-indent: 0px; } /* a statement, not a quotation: no first-line indent (tried, dropped) */
/* Its title rises quicker than the page's others: the typing waits on it (case.js) */
.cb:has(> .cb-typed) > .cb-title.rv.is-in .rli { transition-duration: 0.45s; }
.cb-typed .h3 { color: var(--ink); }
/* A mark in the sentence (the Hengebytes strokes in a goal) is square, a line tall, and
   arrives once the typing reaches it (featured-review.js sets .is-in on .quote-portrait) --
   quickly: the scroll is already moving on, and the strokes close in with it */
.cb-typed .cb-inline-mark { --mark-w: 1.3em; }
html.case-js .cb-typed .cb-inline-mark { transition-duration: var(--dur-wipe-inline); }

/* ---- Inline mark: a client's logo standing in a line of text like a word, the way the
   featured review's portrait does -- a grey plate one line tall, its particles gathering
   into the logo as it scrolls in (case.js, tab-figure.js inline). The width suits a
   wordmark; --mark-w changes it. ------------------------------------------------------ */
.cb-inline-mark {
  position: relative;
  display: inline-block;
  width: var(--mark-w, 4.6em);
  height: 1.3em;
  margin: 0 0.12em;
  vertical-align: -0.3em;
  overflow: hidden;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
}
.cb-inline-mark canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Plate and mark wipe down together, as a picture with its plate -- no grey first (case.js
   sets .is-in as it scrolls in; in a typed statement, featured-review.js does, when the
   typing reaches it) */
html.case-js .cb-inline-mark { transition: clip-path var(--dur-wipe) var(--ease-io); }
html.case-js .cb-inline-mark:not(.is-in):not(.is-shown) { clip-path: inset(0 0 100% 0); }

/* A bigger mark the text flows round (.cb-inline-mark--float, just before its paragraph,
   a lead): at the paragraph's head, --float-lines of the lead tall -- from the first line's
   cap height down to the last one's baseline (Archivo: cap 0.686, ascent 0.878, content
   1.088em) -- and wide, so a wordmark (sized by the plate's width) reads big. The
   paragraph's own lines run beside it. Phones: a plate of its own above the text
   (case-mobile.css). */
.cb-inline-mark--float {
  --float-lines: 3;
  float: left;
  font-size: var(--cb-lead-fs);
  line-height: 1.4; /* the lead's: 1lh is its line */
  width: var(--mark-w, 9em);
  height: calc((var(--float-lines) - 1) * 1lh + 0.686em);
  margin: calc((1lh - 1.088em) / 2 + 0.192em) 0.7em 0 0;
  vertical-align: baseline;
}
.cb-text > .cb-inline-mark--float + * { margin-top: 0; }
/* It comes with the text beside it, not after: a shorter wipe */
html.case-js .cb-inline-mark--float { transition-duration: var(--dur-wipe-panel); }


/* ---- Points: titled paragraphs in the left half ----------------------------------- */

.cb-points {
  display: flex;
  flex-direction: column;
  gap: max(28px, calc(40 * var(--u)));
  max-width: calc((100% - var(--cb-col)) / 2);
}
.cb-point {
  font-size: var(--cb-body);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-body);
  color: var(--text);
}
.cb-point > .cb-sub + * { margin-top: 0.5em; }
/* A line under the title, in the landing's services voice (sections.css .what-tag) */
.cb-point-tag {
  font-size: var(--fs-tag);
  font-style: italic;
  font-weight: 400;
  color: var(--ink);
}

/* Pinned (case-points.js, points with a form each): the landing's "what we do" -- the block
   holds the screen for one step of scroll per point. The titles stand in a column in the
   services rows' size, the one being read in ink with its paragraph open under it, the
   rest held back in the quiet grey; the plate beside them stands on "what we do"'s ground. */
.cb-points--pinned {
  --cb-step: 60svh;    /* scroll per point */
  display: block;
  max-width: none;
  height: calc(100svh + var(--steps) * var(--cb-step));
}
.cb-points-panel {
  position: sticky;
  top: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 0; /* the plate's column starts on the centre line, as the landing's services plate */
  height: 100svh;
  padding-top: var(--header-h);
}
.cb-points-list {
  padding-right: var(--cb-col);
  display: flex;
  flex-direction: column;
  gap: max(16px, calc(24 * var(--u)));
}
.cb-points--pinned .cb-point {
  display: grid;
  grid-template-rows: auto 0fr;
  transition: grid-template-rows var(--dur-accordion) var(--ease-out);
}
.cb-points--pinned .cb-point.is-active { grid-template-rows: auto 1fr; }
/* Points that are names only (a list of systems): set as the landing's integrations rows,
   close together, their eyebrow --gap-eyebrow over them. (Phones keep the accordion's
   spacing: each name is a tap target there.) */
@media (min-width: 768px) {
  .cb-points--pinned:not(:has(.cb-point-body > *)) .cb-points-list { gap: max(5px, calc(9 * var(--u))); }
  .cb-points--pinned:not(:has(.cb-point-body > *)) .cb-points-list > .eyebrow { margin-bottom: calc(var(--gap-eyebrow) - max(5px, calc(9 * var(--u)))); }
}
.cb-points--pinned .cb-point > .cb-sub {
  font-size: var(--fs-list);
  font-weight: 300;
  line-height: 1.08;
  letter-spacing: var(--track-head);
  color: var(--quiet-lg);
  transition: color var(--dur-base) ease;
}
.cb-points--pinned .cb-point.is-active > .cb-sub { color: var(--ink); }
/* The body (case-points.js wraps the tagline, the copy and any list in it) opens under
   the title; its copy is the services' (sections.css .what-copy): small, 36ch */
.cb-points--pinned .cb-point-body {
  min-height: 0;
  max-width: 36ch;
  overflow: hidden;
}
.cb-points--pinned .cb-point-body > :first-child { padding-top: max(12px, calc(16 * var(--u))); }
.cb-points--pinned .cb-point-body > * + * { margin-top: 0.75em; }
.cb-points--pinned .cb-point-body > p:not(.cb-point-tag),
.cb-points--pinned .cb-point-body li {
  font-size: var(--fs-copy);
  line-height: 1.5;
}
.cb-points--pinned .cb-point-body ul { padding-left: 1.1em; }
.cb-points--pinned .cb-point-body li + li { margin-top: 0.3em; }
.cb-points--pinned .cb-point-body li::marker { color: var(--line); }
.cb-points-plate {
  position: relative;
  justify-self: end;
  width: min(100%, 68svh);
  aspect-ratio: 1;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe-panel) var(--ease-io); /* = sections.css .what-slot::before */
}
/* The plate and its mark arrive through the landing's top-down mask as the panel comes in
   (case-points.js .is-visual-in), once */
html.case-js .cb-points--pinned:not(.is-visual-in) .cb-points-plate {
  clip-path: inset(0 0 100% 0);
  transition: none;
}
.cb-points-plate canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---- Pictures: a grey plate first, the picture over it ---------------------------- */

figure.cb { margin: 0; }
.cb-plate {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--ratio, 1352 / 534);
  margin: 0;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe) var(--ease-io);
}
.cb-plate img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Below the fold the whole plate waits hidden, and once its picture is in it wipes down
   with it from the top (case.js) -- no grey first */
.cb-plate.is-armed:not(.is-in) { clip-path: inset(0 0 100% 0); }

.cb-media--offset .cb-plate {
  --ratio: 3 / 2;
  width: 72.5%;
  margin-left: 11%;
}

/* The second picture starts on the centre line, as everything on the page's right half
   does (a split's content, the pinned points' plate); the gap is all the first one's */
.cb-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  align-items: start;
}
.cb-pair > :first-child { margin-right: var(--cb-col); }
.cb-pair > .cb-plate { --ratio: 3 / 2; }
.cb-pair > .cb-plate--tall { --ratio: 1 / 1; }

/* ---- Callout: the page's one panel --------------------------------------------------- */

/* A panel of the grey every surface is (the points' plate above it, the landing's "what we
   do": --plate-grey lit as --plate-lit), with the landing's equaliser in the
   same blue as those marks along its foot (js/ui/callout-eq.js reads the grains' colour
   from --cb-eq-ink). It arrives as a picture does: wiping down from the top as it scrolls
   in (case.js). */
.cb-callout {
  position: relative;
  isolation: isolate;
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe-panel) var(--ease-io); /* = the landing's plates (sections.css .what-slot); its text follows it in */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: max(32px, calc(56 * var(--u)));
  padding: max(32px, calc(72 * var(--u)));
  --cb-eq-ink: var(--blue);
  background: var(--plate-grey) var(--plate-lit);
  color: var(--ink);
}
/* The equaliser: a strip pressed to the panel's foot, under the text -- a picture in the
   panel, not a space of its own (the panel keeps the height its text gives it) */
.cb-callout-eq {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: -1;
  width: 100%;
  height: 42%;
  pointer-events: none;
}
.cb-callout.is-armed:not(.is-in) { clip-path: inset(0 0 100% 0); }
/* The button's wrapper is what rises (the landing's way: .hero-actions, .cta-act), so the
   button keeps its own hover lift and shadow */
.cb-callout-act { display: inline-block; }
.cb-callout-text {
  max-width: 22em;
  font-size: max(26px, calc(48 * var(--u)));
  font-weight: 300;
  line-height: 1.2;
  letter-spacing: var(--track-head);
  text-wrap: pretty;
}

/* ---- Closing call: back on the scene's ground, full bleed, set as the landing's own
   close -- the call on the left, and on the right the particle globe turning into the
   Hengebytes mark (story/case-mark.js, css/case-mark.css), the ground's light under it ---- */

/* The landing's close itself (sections.css .s-contact: the overline, the three-line title
   and the pill on a sticky screen, the mark on the right), on a board that opens the way
   the landing's team board does (sections.css .s-team): as the section comes up from
   below, the screen is pinned and its window opens -- the top edge rising, both side
   insets closing to the edges -- following the section's top (case.js writes --cta-open,
   .is-fixing). The white outside the window is the page. At top = 0 the pinned and the
   in-flow screen coincide, so the pin comes off with no jump and the screen then sticks as
   the landing's does. */
.cb-cta {
  --scene-glow-at: 75% 50%;
  --cta-open: 1;
  --cta-cut: calc((1 - var(--cta-open)) * 100%);
  --cta-inset: calc((1 - var(--cta-open)) * var(--cb-x));
  overflow: clip;
  margin-inline: calc(-1 * var(--cb-x));
}
html.case-js .cb-cta { --cta-open: 0; }
.cb-cta-panel {
  z-index: 1;
  background: var(--scene-bg) var(--cb-ground);
  clip-path: inset(var(--cta-cut) var(--cta-inset) 0);
}
.cb-cta.is-fixing .cb-cta-panel {
  position: fixed;
  inset: 0;
}
/* The call rises into place as the window opens (30vh below at closed, home when open) */
.cb-cta-panel > :not(.cb-cta-mark) { translate: 0 calc((1 - var(--cta-open)) * 30vh); }

/* ---- What's next, then the call: a service's close (js/ui/case-next.js) -------------- */

/* On the desktop the block is the board's first screen, set in the call's own places: its
   eyebrow where the call's overline stands, its title and text where the call stands -- left
   of the mark, which comes in as the AI's where ours will stand (story/case-mark.js) and
   spins into it. The call rises in their places once they have gone. The board is longer
   by the screen they hold (case-next.js SWITCH_AT: 0.6 of it, then the call's own 45svh). */
.cb-cta.cb-cta--next { height: 205svh; }
/* Its text quicker than the page's own: the two groups take turns (what's next goes, then
   the call comes; back up, the other way round), so each turn is short -- the shared
   tokens (global.css line-rise / line-exit / line-drop, the button's follow), here only */
.cb-cta.cb-cta--next {
  --dur-reveal: 0.5s;   /* 0.7s */
  --dur-exit: 0.3s;     /* 0.4s */
  --dur-drop: 0.35s;    /* 0.55s */
  --exit-stagger: 0.4;  /* 0.6 */
  --cta-follow: 0.2s;   /* 0.3s */
}
.cb-cta-panel > .cb-next {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0 var(--page-x) 10svh; /* = sections.css .contact-sticky */
  pointer-events: none;           /* (the call's button is under it) */
}
.cb-cta-panel > .cb-next > .eyebrow {
  position: absolute;
  top: clamp(112px, 18svh, 186px); /* = sections.css .contact-overline */
  left: var(--page-x);
  font-size: clamp(9px, 0.76vw, calc(12 * var(--ui-label)));
}
.cb-cta-panel > .cb-next > .cb-title { max-width: min(11em, 52vw); }
/* Its text in two columns, a paragraph each, in the services' copy beside a mark */
.cb-cta-panel > .cb-next > .cb-text {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--cb-col);
  max-width: min(44em, 52vw);
  font-size: var(--fs-copy);
  line-height: 1.5;
}
.cb-cta-panel > .cb-next > .cb-text > p { margin-top: 0; }

/* Back to top is the phones' (case-mobile.css) */
.to-top { display: none; }

@media (prefers-reduced-motion: reduce) {
  html.case-page header,
  .cb-plate,
  .case a:not(.btn) { transition: none; }
}

/* The footer pull (js/ui/case-footer.js) holds the page's own scrolling for its glide, the
   closing call holds it till its mark has formed (js/ui/cta-hold.js), and a service's
   what's next holds it as its rest (js/ui/next-rest.js) */
html:is(.footer-pulling, .cta-holding, .next-holding) { overflow: hidden; }
