/* =========================================================================
   Ludic Advisory — components
   One rule per block from _build/blocks.py, matching the archived site's
   layout. Same breakpoint ladder as foundation.css: 36 / 48 / 64 / 80rem.
   Same naming: block__element--modifier.

   MEASURED off `_Webflow Archive/ludic-site.webflow/css/ludic-site.webflow.css`
   rather than eyeballed. The values that recur, with the archive class each
   comes from, so nothing below is a guess:

     .container-large            80rem, 2.5rem side padding
     .max-width-large            60rem  — the copy measure
     .layout25_component         60rem  — a service tile's copy column
     .padding-section-large       7rem   -> --space-2xl
     .padding-section-medium      5rem   -> --space-xl
     .padding-section-small       2rem   -> --space-md
     .heading-style-h3       2rem/400/1.2  -> --text-xl
     .heading-style-h4       1.5rem/400/1.4 -> --text-lg
     .heading-style-h5       1.25rem/600/1.5 -> --text-md
     .text-size-large        1.5rem  -> --text-lg
     .text-size-medium       1.25rem -> --text-md
     .text-size-regular      1rem    -> --text-base
     .text-size-small        0.9rem  -> --text-sm  (0.9375rem, on the ladder)
     .ludic-learning-logo    height 1.3rem — EVERY sub-brand lockup
     .org-logo/.academic-logo  max-height 5rem, object-fit: contain
     .powered-by-badge       8rem wide, 1.6rem in from the top left
     .button                 2px solid black, .5rem 1.5rem
   ========================================================================= */

/* ---------- home hero ---------------------------------------------------
   The live site's hero: a full-bleed looping background video with an
   off-white card floating over it, holding the circle mark, the h1 and one
   button. Original geometry: 619px tall, a 1.25fr/1fr grid inside an 80rem
   container, card padding 3rem 2.5rem, card background #f9fbf7.
   ----------------------------------------------------------------------- */
.hero--home { position: relative; overflow: hidden; background: var(--black); }

.hero__video,
.hero__video-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The poster sits under the video, so the hero is never a black hole while
   the video buffers and still reads correctly if it cannot play at all. */
.hero__video-poster { z-index: 0; }
.hero__video { z-index: 1; }

.hero__inner {
  position: relative;
  z-index: 2;
  display: grid;
  padding-block: var(--space-lg);
}
@media (min-width: 64rem) {
  /* align-items: start stops the card stretching to the full row height and
     leaving dead space under the button. */
  .hero__inner { grid-template-columns: 1.15fr 1fr; padding-block: var(--space-2xl); align-items: start; }
}

.hero__card {
  background: var(--header-cream);
  padding: var(--space-md) var(--space-sm);
}
@media (min-width: 48rem) { .hero__card { padding: var(--space-lg) var(--space-md); } }

.hero__mark { width: 4rem; height: 4rem; margin-bottom: var(--space-sm); }
@media (min-width: 48rem) { .hero__mark { width: 6rem; height: 6rem; } }

/* No measure cap here. The card is already a narrow column, and capping the
   title as well left a third of the card empty to the right of every line.
   The title fills the card and wraps where the card ends. */
.hero__title { max-width: none; text-wrap: balance; }
.hero__card .prose__p { margin-top: var(--space-xs); }

/* Not rendered by default — the live page hides its footage credit and the
   client asked for the same. Styling kept for the day one is wanted: a white
   line floating on pale footage is unreadable, so it gets a dark chip. */
.hero__credit {
  position: relative;
  z-index: 2;
  display: inline-block;
  margin-bottom: var(--space-xs);
  padding: 0.25rem 0.5rem;
  border-radius: 2px;
  background: rgb(0 0 0 / 0.6);
  color: var(--white);
  font-size: var(--text-xs);
}

/* ---------- interior page hero ------------------------------------------ */
.hero--page { padding-block: var(--space-lg); }
@media (min-width: 64rem) { .hero--page { padding-block: var(--space-xl); } }

.hero--page .hero__inner { display: grid; gap: var(--space-md); padding-block: 0; }
@media (min-width: 64rem) {
  .hero--page .hero__inner--split { grid-template-columns: 1.1fr 1fr; gap: var(--space-lg); align-items: center; }
  .hero--page .hero__inner:not(.hero__inner--split) .hero__content { max-width: 48rem; }
}

.page-hero__logo { height: 3rem; width: auto; margin-bottom: var(--space-sm); }
@media (min-width: 48rem) { .page-hero__logo { height: 4.5rem; } }

.page-hero__media img,
.page-hero__media picture { width: 100%; }

.hero__content .prose__p { margin-top: var(--space-xs); }

/* ---------- brand tiles -------------------------------------------------
   Three colour tiles butted together with no gap — the live site's
   .collection-list-business, 350px tall inside the 80rem container, with the
   logo centred and the description revealed on hover. A hover-only reveal is
   unreachable by touch, so below 64rem js/site.js reveals whichever tile is
   nearest the middle of the viewport, and the reveal is bound to
   :focus-visible as well as :hover.
   ----------------------------------------------------------------------- */
/* White field behind the tile row (client's call — the live site runs cream
   here, but the statement above it is white and they wanted the two to read
   as one band). */
.section--brand-tiles { background: var(--white); padding-block: var(--space-md); }

.brand-tiles { display: grid; margin: 0; padding: 0; list-style: none; }
@media (min-width: 64rem) { .brand-tiles { grid-template-columns: repeat(3, 1fr); } }

.brand-tile { display: flex; }

.brand-tile__link {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 15rem;
  padding: var(--space-lg) var(--space-md);
  text-decoration: none;
  overflow: hidden;
  transition: background-color var(--duration) var(--ease);
}
@media (min-width: 64rem) { .brand-tile__link { min-height: 21.875rem; } }

/* Logo and copy occupy the SAME grid cell. At rest the logo is visible and
   the copy is not; on hover they swap. Stacking them in one cell rather than
   one above the other is what lets each be positioned independently —
   neither reserves space that pushes the other around.

   All three logo rows are centred on the tile's vertical axis. */
.brand-tile__logo-row,
.brand-tile__body {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

/* Advisory and Media Studio are single-line wordmarks and sit dead centre.
   SmartLab is a stacked lockup — the node graphic rises above the word — so
   centring its whole artwork drops the word well below the other two. It is
   nudged UP by half the height difference, which lands its baseline on the
   same line as Media Studio's.

   Every SVG here is cropped to its ink box, so rendered height is
   width x (viewBox height / viewBox width):

     smartlab      94.25 / 267.50 = 0.35234 of its width
     media studio  35.00 / 389.38 = 0.08989 of its width

   The two width rules below are in the ratio 14.4 / 21 = 0.6857, and their
   percentage caps 64 / 93 = 0.6882 agree to a quarter of a percent, so
   h_media / h_smartlab = 0.372 in either regime. Shifting up by
   (h_smartlab - h_media) / 2 = 0.314 x h_smartlab puts the two bottoms on one
   line, and a percentage translateY resolves against the element's OWN
   height — so this holds as the tiles resize, with no fixed pixel value to
   go stale. Change a width rule and this has to be re-derived. */
.brand-tile--smartlab .brand-tile__logo { transform: translateY(-31.4%); }

/* Tile colours are the live site's and do not change on hover — the copy
   fading in is the whole affordance. NOTE: white on #5b8edb is 3.31:1, below
   AA for body text. It is here by explicit client direction (white SmartLab
   copy, no colour shift); checks.py records it as an accepted exception with
   the measured value rather than pretending it passes. */
.brand-tile--advisory .brand-tile__link { background: var(--bold-blue); color: var(--white); }
.brand-tile--smartlab .brand-tile__link { background: var(--smartlab-blue); color: var(--white); }
.brand-tile--media-studio .brand-tile__link { background: var(--yellow); color: var(--black); }

/* Sized to reproduce the live optical scale. The original draws all three
   450x198 assets at 388px wide inside a 426.7px tile — 0.8628 px per SVG
   unit. These SVGs have been cropped to their ink box, so each one is set to
   its own cropped width x 0.8628:
     advisory     304.25u -> 262.5px -> 16.4rem
     smartlab     267.50u -> 230.8px -> 14.4rem
     media studio 389.38u -> 335.9px -> 21.0rem
   Change a viewBox and these numbers have to be recomputed.

   The percentage is the fallback for narrow tiles, and it resolves against
   the link's CONTENT box — tile width minus 2 x 2rem of side padding, so
   362.7px at a 426.7px tile, not 426.7px. The caps were first written
   against the tile width, which made them the smaller value at every real
   width: the rem targets above never applied and every logo rendered ~14%
   under the live size. Percentages here are of 362.7px:
     262.5 / 362.7 = 72.4% -> 73
     230.8 / 362.7 = 63.6% -> 64
     335.9 / 362.7 = 92.6% -> 93 */
.brand-tile__logo { height: auto; }
.brand-tile--advisory .brand-tile__logo { width: min(16.4rem, 73%); }
.brand-tile--media-studio .brand-tile__logo { width: min(21rem, 93%); }
.brand-tile--smartlab .brand-tile__logo { width: min(14.4rem, 64%); }

.brand-tile__body {
  margin: 0 auto;
  max-width: 34ch;
  font-size: var(--text-base);
  line-height: 1.6;
  text-align: center;
}

/* The swap. Below the desktop breakpoint there is no hover, so js/site.js
   marks the tile nearest the middle of the viewport [data-in-view] and that
   one swaps instead. Both start visible and the script opts into hiding the
   copy, so with JavaScript off every description is simply on show. */
.brand-tiles[data-reveal] .brand-tile__body { opacity: 0; }
.brand-tiles[data-reveal] .brand-tile__logo-row,
.brand-tiles[data-reveal] .brand-tile__body {
  transition: opacity var(--duration) var(--ease);
}
.brand-tiles[data-reveal] .brand-tile__link:hover .brand-tile__body,
.brand-tiles[data-reveal] .brand-tile__link:focus-visible .brand-tile__body,
.brand-tiles[data-reveal] .brand-tile[data-in-view] .brand-tile__body { opacity: 1; }

.brand-tiles[data-reveal] .brand-tile__link:hover .brand-tile__logo-row,
.brand-tiles[data-reveal] .brand-tile__link:focus-visible .brand-tile__logo-row,
.brand-tiles[data-reveal] .brand-tile[data-in-view] .brand-tile__logo-row { opacity: 0; }

/* ---------- feature split ----------------------------------------------- */
.feature { display: grid; gap: var(--space-md); align-items: center; }
@media (min-width: 64rem) {
  .feature { grid-template-columns: 1fr 1fr; gap: var(--space-lg); }
  .feature--flip .feature__media { order: 2; }
}

/* THE ARTWORK AT THE TOP OF THE COPY, not centred against it. `.feature` is
   `align-items: center`, which is right when the two columns are close in
   height and wrong when the copy is much taller — the picture then floats in
   the middle of the band with air above it and the heading it belongs to is
   already three lines gone. Ludic Leadership V2's "Three journeys" is the
   first: the client asked for the image "up in line with the Leadership
   changes shape at… text". */
@media (min-width: 64rem) { .feature--top { align-items: start; } }

.feature__media { margin: 0; }
.feature__image { width: 100%; }
.feature__title { margin-bottom: var(--space-xs); }

/* A sentence-case line under the title, where the live page puts a long
   descriptor that would shout as an all-caps eyebrow. */
.feature__subhead {
  margin-bottom: var(--space-xs);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--dark-grey);
}
.section[data-tone="black"] .feature__subhead { color: var(--blue-on-dark); }
.section[data-tone="deep-blue"] .feature__subhead,
.section[data-tone="dh-purple"] .feature__subhead,
.section[data-tone="smartlab-blue"] .feature__subhead { color: var(--ink-muted); }

/* THE CHECKED LIST under a feature's prose. The tick is generated content
   with the list still marked up as a list, so a screen reader counts four
   items rather than reading "check mark" four times. It is `currentColor`
   because this band takes a tone. */
.feature__points {
  display: grid;
  gap: var(--space-2xs);
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-base);
  line-height: 1.6;
}
/* The tick is positioned, not a grid or flex child. As a grid item it split
   the line: an <li> holding "<strong>Branded & bespoke</strong> — your
   science, your identity" has TWO in-flow children, and a two-column grid put
   each of them — and each anonymous run of text between them — in its own
   cell, so every word landed on its own row. The content here has to stay
   inline; only the marker comes out of flow. */
.feature__points li { position: relative; padding-left: 1.6rem; }
.feature__points li::before {
  content: "";
  position: absolute;
  left: 0.1rem;
  top: 0.45em;
  width: 0.7rem;
  height: 0.42rem;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
}

.feature__badges-note { margin-top: var(--space-sm); font-size: var(--text-sm); }

.feature__badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-sm);
  margin-top: var(--space-2xs);
  padding: 0;
  list-style: none;
}
.badge { display: inline-flex; }
.badge img { height: 1.875rem; width: auto; }

/* ---------- people ------------------------------------------------------ */
.people { display: grid; gap: var(--space-md); }
@media (min-width: 48rem) { .people { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .people { grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); } }

/* No background and no forced square: the headshots are cut out on white,
   so a tinted box behind them just draws a grey rectangle around the subject. */
/* No background and no crop: the headshots are cut out on white, so a tinted
   box behind them draws a grey rectangle around the subject. A fixed box with
   object-fit: contain keeps the names aligned across the row even though the
   source files differ in height (503 vs 494 px). */
/* The portrait is wrapped in <picture> for its WebP source. display:contents
   takes that wrapper out of the layout so the sizing below still resolves
   against the card, not against an inline box in between. */
.person picture { display: contents; }

.person__portrait {
  width: 100%;
  max-width: 13.75rem;
  height: 13.75rem;
  object-fit: contain;
  object-position: center top;
  margin-bottom: var(--space-xs);
}

/* The live pages rule off each name before the bio. */
.person__name {
  font-size: var(--text-md);
  font-weight: 600;
  padding-bottom: var(--space-2xs);
  margin-bottom: var(--space-xs);
  border-bottom: 1px solid var(--line);
}
.person__bio { font-size: var(--text-base); line-height: 1.7; }
.person__link { display: inline-block; margin-top: var(--space-2xs); font-size: var(--text-sm); }

/* ---------- podcast ----------------------------------------------------- */
.podcast { display: grid; gap: var(--space-md); align-items: center; }
@media (min-width: 64rem) { .podcast { grid-template-columns: 0.8fr 1fr; gap: var(--space-lg); } }

.podcast__art { margin: 0; }
.podcast__art img, .podcast__art picture { width: 100%; }

.podcast__title { margin-bottom: var(--space-xs); }

/* No cover art: heading full width, then copy beside the listen row. */
@media (min-width: 64rem) {
  .podcast--noart { grid-template-columns: 1fr; }
  .podcast--noart .podcast__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--space-2xl);
    align-items: start;
  }
  .podcast--noart .eyebrow,
  .podcast--noart .podcast__title { grid-column: 1 / -1; }
  .podcast--noart .podcast__aside { margin-top: 0; }
  .podcast--noart .podcast__subhead { margin-top: 0; }
}

.podcast__aside { margin-top: var(--space-sm); }

.podcast__subhead {
  margin-top: var(--space-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.podcast__platforms {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin: var(--space-2xs) 0 0;
  padding: 0;
  list-style: none;
}
.platform { display: inline-flex; }
.platform img { height: 2.5rem; width: auto; }

/* ---------- cards ------------------------------------------------------- */
.cards { display: grid; gap: var(--space-xs); margin: 0; padding: 0; list-style: none; }
@media (min-width: 48rem) { .cards--2, .cards--3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .cards--3 { grid-template-columns: repeat(3, 1fr); } }

.card {
  display: flex;
  flex-direction: column;
  padding: var(--space-sm);
  background: var(--white);
  border: 1px solid var(--line);
}
.section[data-tone="cream"] .card { border-color: transparent; }

/* A CARD ON A DARK BAND. `.card` paints itself white, and the dark tones set
   `--ink: var(--white)` — so white copy landed on a white card and three cards
   of ART came out as three empty boxes. Check 6 could not see it: it measures
   the PAIRS table, which names tokens, and nothing in the table pairs card
   copy with the card's own background. Found by rendering the page.

   On a dark band the card is the band with a rule round it: no ground of its
   own, the tone's own translucent `--line`, the tone's own ink. Ludic
   Leadership V2's ART band is the first band on this site to put cards on a
   dark tone; nothing that shipped before this reaches the rule. */
.section[data-tone="black"] .card,
.section[data-tone="deep-blue"] .card,
.section[data-tone="dh-purple"] .card,
.section[data-tone="purple"] .card,
.section[data-tone="smartlab-blue"] .card { background: transparent; }

/* The card's ordinal — E¹, E², E³ on Ludic Leadership, which is the only page
   that has one. MEASURED there: a plain 32px glyph at the top of the card, in
   the ink of the band. An earlier version set it as a 2rem black chip with
   white 14px type inside, which is a badge, and the archive draws a heading. */
.card__index {
  display: block;
  margin-bottom: var(--space-xs);
  font-size: var(--text-xl);
  font-weight: 400;
  line-height: 1;
}

.card__icon { width: 3rem; height: 3rem; margin-bottom: var(--space-xs); }
.card__title { font-size: var(--text-md); font-weight: 600; margin-bottom: var(--space-2xs); }
.card__body { flex: 1; font-size: var(--text-base); line-height: 1.7; }
.card__more { margin-top: var(--space-xs); font-size: var(--text-sm); }

/* The category over a card's name. Same treatment as the collection cards'
   eyebrow — small, tracked, muted — so a category looks like a category
   wherever it appears. */
.card__eyebrow {
  margin-bottom: var(--space-2xs);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.card__note {
  margin-top: var(--space-xs);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* The figures under a case study. A <dl>, because each one is a label and its
   value: "Investigators: >2k". The VALUE is displayed first and the label
   under it, which is the way the eye reads a statistic — `order` does that
   without inverting the markup, so the pair is still announced label-then-
   value. */
.card__stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  margin: var(--space-xs) 0 0;
  padding-top: var(--space-xs);
  border-top: 1px solid var(--line);
}
.card__stat { display: flex; flex-direction: column; }
.card__stat-value {
  order: -1;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.1;
}
.card__stat-label {
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--ink-muted);
}

/* A CARD ON ITS OWN IS A STAT PANEL, not a card in a grid. `cards--1` with a
   `stats` foot has no siblings to line up against, so the figures stop being a
   footnote under the copy and become the content: laid across the panel with
   room around them, at the largest size on the site's scale, and the label set
   to be read rather than to fit.

   `cards--1` IS OTHERWISE UNUSED — every `cards` band on this site is 2, 3 or
   4 across — and the only other `stats` foot is on Ludic Medical's `cards--2`
   case cards, which this cannot reach. Ludic Leadership V2's evidence panel is
   the one use, and it is two figures rather than four because those are the
   two the client will stand behind. */
.cards--1 .card { padding: var(--space-lg); }
/* A GRID RATHER THAN THE FLEX ROW the stat foot uses in a card grid. Flexed,
   the figures shrink-wrap and cluster at the left of a panel that runs the
   full container, leaving two thirds of it empty. `auto-fit` spreads them
   across it and still stacks them on a phone; the 22rem cap is on the STAT,
   not the column, so a label keeps a readable measure inside a wide cell. */
.cards--1 .card__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-md) var(--space-lg);
  margin-top: var(--space-md);
  padding-top: var(--space-md);
}
.cards--1 .card__stat { max-width: 22rem; }
.cards--1 .card__stat-value { font-size: var(--text-xl); }
.cards--1 .card__stat-label {
  margin-top: var(--space-2xs);
  font-size: var(--text-base);
  letter-spacing: 0;
  line-height: 1.5;
}

/* ---------- named columns with lists — blocks.list_columns -------------- */
.list-columns {
  display: grid;
  gap: var(--space-lg) var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 48rem) { .list-columns--3, .list-columns--4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) {
  .list-columns--3 { grid-template-columns: repeat(3, 1fr); }
  .list-columns--4 { grid-template-columns: repeat(4, 1fr); }
}
.list-column__index {
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink-muted);
}
.list-column__title {
  margin-top: var(--space-2xs);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.3;
}
/* THE SUB-LINE IS A HEADER ROW, and it has to look like one. Two problems,
   one fix. It was reading as the first item of the list rather than as a deck
   on the title; and because it is free to be one line in three columns and two
   in the fourth, the first rule of each column landed at a different height
   and the table had no top edge. Reserving two lines for it does both: the
   rules line up across the row, and the band above them reads as a head.

   `min-height` rather than a fixed height, so a sub-line that ever runs to
   three lines pushes its own column instead of being clipped — the rules would
   go out of step again, which is visible and therefore fixable, where clipped
   text is neither. */
.list-column__sub {
  margin-top: var(--space-2xs);
  margin-bottom: var(--space-xs);
  min-height: calc(2 * 1.5 * var(--text-sm));
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--ink-muted);
}
.list-column__list {
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
}
.list-column__item {
  padding: 0.6rem 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-base);
  line-height: 1.4;
}
.section--list-columns .button-group { justify-content: center; margin-top: var(--space-lg); }

/* ---------- the 7 Cs ---------------------------------------------------- */
.seven-cs { display: grid; gap: var(--space-xs); margin: 0; padding: 0; list-style: none; }
@media (min-width: 48rem) { .seven-cs { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .seven-cs { grid-template-columns: repeat(3, 1fr); } }

.seven-c { padding: var(--space-sm); background: var(--white); }
.seven-c__icon { width: auto; height: 4.5rem; margin-bottom: var(--space-xs); }

.seven-c__title {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: var(--space-2xs);
}
.seven-c__body { font-size: var(--text-base); line-height: 1.7; }

/* ---------- closing CTA ------------------------------------------------- */
.cta { max-width: 44rem; }
.cta__title { margin-bottom: var(--space-xs); }
.cta .button { margin-top: var(--space-sm); }

/* ---------- contact ----------------------------------------------------- */
.contact { display: grid; gap: var(--space-lg); }
@media (min-width: 64rem) { .contact { grid-template-columns: 0.8fr 1fr; align-items: start; } }

.contact__title { margin-bottom: var(--space-xs); }

.contact__list {
  display: grid;
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 2px solid var(--black);
}
.contact__list dt { margin-top: var(--space-xs); font-size: var(--text-xs); font-weight: 600; }
.contact__list dd { margin: 0; line-height: 1.7; }

.form-card { padding: var(--space-sm); background: var(--cream); }
@media (min-width: 48rem) { .form-card { padding: var(--space-md); } }

.form-card__title { font-size: var(--text-lg); font-weight: 400; margin-bottom: var(--space-2xs); }
.form-card__intro { margin-bottom: var(--space-sm); }

.form { display: grid; gap: var(--space-sm); }

.form__notice { padding: var(--space-xs); background: var(--white); font-size: var(--text-sm); line-height: 1.6; }

.form__row { display: grid; gap: var(--space-sm); }
@media (min-width: 36rem) { .form__row { grid-template-columns: repeat(2, 1fr); } }

.form__field { display: grid; gap: 0.375rem; }
.form__label { font-size: var(--text-sm); font-weight: 600; }
.form__optional { font-weight: 400; color: var(--dark-grey); }

/* The live site's input: 2px solid black, square corners, white fill. */
.form__input {
  width: 100%;
  padding: 0.625rem 0.75rem;
  background: var(--white);
  border: 2px solid var(--control-line);
  border-radius: 0;
  font: inherit;
  font-size: var(--text-base);
  color: var(--ink);
}
.form__input--area { resize: vertical; min-height: 8rem; }
.form__input--select { appearance: none; padding-right: 2.5rem; }

.form__field--check { display: flex; align-items: flex-start; gap: 0.625rem; }
.form__check { margin-top: 0.3rem; width: 1.125rem; height: 1.125rem; accent-color: var(--black); }
.form__check-label { font-size: var(--text-sm); line-height: 1.6; }
.form__legal { font-size: var(--text-sm); }

/* ---------- the HubSpot embed, in this site's clothes -------------------
   HubSpot injects its own markup and pulls its own stylesheet, which is a
   rounded-corner, grey-bordered, Helvetica form that looks like it came from
   somewhere else — because it did. These rules restyle what it injects with
   the same values the fallback above uses: 2px black borders, square corners,
   the site's type, the site's button.

   The selectors are HubSpot's own class names (`hs-form`, `hs-input`,
   `hs-button`, `hs-error-msg`), scoped to our target so nothing here can
   reach anything else. They are stable across the v2 embed but they are
   SOMEBODY ELSE'S class names, so if the form ever comes out looking like
   HubSpot again, this block is the first place to look.

   Nothing is hidden and no field is restyled out of existence: the fields,
   the labels, the required markers and the error messages are all HubSpot's,
   and only their appearance changes. */
.form-card__target:empty,
.newsletter__target:empty { display: none; }

.form-card__target .hs-form,
.newsletter__target .hs-form {
  display: grid;
  gap: var(--space-sm);
  font-family: inherit;
}
.form-card__target .hs-form-field,
.newsletter__target .hs-form-field { display: grid; gap: 0.375rem; }

.form-card__target .hs-form label,
.newsletter__target .hs-form label {
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.4;
}
.form-card__target .hs-form .hs-field-desc,
.newsletter__target .hs-form .hs-field-desc {
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ink-muted);
}

.form-card__target .hs-input,
.newsletter__target .hs-input {
  width: 100%;
  max-width: none;
  padding: 0.625rem 0.75rem;
  background: var(--white);
  border: 2px solid var(--control-line);
  border-radius: 0;
  box-shadow: none;
  font: inherit;
  font-size: var(--text-base);
  color: var(--ink);
}
.form-card__target textarea.hs-input,
.newsletter__target textarea.hs-input { resize: vertical; min-height: 8rem; }
.form-card__target .hs-input[type="checkbox"],
.form-card__target .hs-input[type="radio"],
.newsletter__target .hs-input[type="checkbox"],
.newsletter__target .hs-input[type="radio"] {
  width: 1.125rem;
  height: 1.125rem;
  accent-color: var(--black);
  border: 0;
}
/* HubSpot lays checkbox lists out as a <ul> with its own bullets. */
.form-card__target .inputs-list,
.newsletter__target .inputs-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.5rem;
}
.form-card__target .inputs-list label,
.newsletter__target .inputs-list label {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  font-weight: 400;
  font-size: var(--text-sm);
  line-height: 1.6;
}

.form-card__target .hs-button,
.newsletter__target .hs-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.75rem 1.5rem;
  background: var(--accent);
  border: 2px solid var(--accent);
  border-radius: 0;
  box-shadow: none;
  font: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--accent-on);
  cursor: pointer;
  transition: background var(--duration) var(--ease);
}
.form-card__target .hs-button:hover,
.newsletter__target .hs-button:hover { background: var(--accent-hover); }

/* Errors keep HubSpot's semantics and take the site's red. */
.form-card__target .hs-error-msg,
.form-card__target .hs-error-msgs,
.newsletter__target .hs-error-msg,
.newsletter__target .hs-error-msgs {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
  color: #b3261e;                   /* 5.9:1 on white */
}
.form-card__target .hs-input.invalid,
.newsletter__target .hs-input.invalid { border-color: #b3261e; }

.form-card__target .hs-richtext,
.form-card__target .legal-consent-container,
.newsletter__target .hs-richtext,
.newsletter__target .legal-consent-container {
  font-size: var(--text-sm);
  line-height: 1.6;
}
.form-card__target .submitted-message,
.newsletter__target .submitted-message {
  padding: var(--space-sm);
  background: var(--surface-alt);
  font-size: var(--text-base);
  line-height: 1.6;
}

/* =========================================================================
   INSIGHTS INDEX
   Mirrors the live /insights page — the Ludic Insights bar, the Roboto Slab
   heading, an inline Search field, a wrapping row of plain-text tags, and a
   two-column grid of white cards on cream with the format label and title
   ABOVE the image. What is added is the design the original never had: the
   controls sit in one bordered panel instead of floating loose, the cards
   are a fixed rhythm rather than ragged, and there is a real hover, a live
   result count and an empty state.
   ========================================================================= */
/* The lockup sits on the cream band with a full-width rule under it, and the
   copy below runs the full measure — as the live site has it. */
/* Equal air above and below the lockup, so the rule underneath reads as a
   deliberate margin rather than the logo resting on it. */
.insights-bar {
  background: var(--cream);
  padding-top: var(--space-sm);
}
.insights-bar__inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); padding-bottom: var(--space-sm); }
.insights-bar__logo { height: 1.3125rem; width: auto; }
.insights-bar__count { font-size: var(--text-xs); color: var(--dark-grey); }
.insights-bar__rule { height: 1px; margin: 0; border: 0; background: var(--black); }

/* --- intro --- */
.insights-intro { background: var(--cream); padding-block: var(--space-md) var(--space-md); }
@media (min-width: 64rem) { .insights-intro { padding-block: var(--space-lg) var(--space-lg); } }

.insights-intro__inner { display: grid; gap: var(--space-sm); }

.insights-intro__title {
  font-family: "Roboto Slab", Georgia, serif;
  font-size: clamp(1.5rem, 2.8vw, var(--text-xl));
  font-weight: 400;
  line-height: var(--leading-snug);
  max-width: 60ch;
}
.insights-intro__lede { font-size: var(--text-base); line-height: var(--leading-body); max-width: 60ch; }

/* --- the filter panel --- */
/* One continuous cream field from the lockup bar down to the last card, with
   the white panel and the white cards sitting on it. A white strip between
   the panel and the grid read as a seam. */
.insights-controls { background: var(--cream); padding-bottom: var(--space-lg); }

.filters {
  display: grid;
  gap: 0;
  border: 2px solid var(--black);
  background: var(--white);
}

.filters__row {
  display: grid;
  gap: var(--space-2xs);
  padding: var(--space-sm);
  border-bottom: 1px solid var(--line);
}
.filters__row:last-of-type { border-bottom: 0; }
@media (min-width: 48rem) {
  .filters__row { grid-template-columns: 7rem 1fr; gap: var(--space-sm); align-items: baseline; }
}

.filters__label,
.filters__legend {
  margin: 0;
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-md);
  font-weight: 500;
  line-height: 1.3;
}

.filters__input {
  width: 100%;
  max-width: 30rem;
  height: 2.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--white);
  border: 2px solid var(--control-line);
  border-radius: 0;
  font: inherit;
  font-size: var(--text-base);
  color: var(--ink);
}
/* --ink-muted, not --mid-grey: a placeholder is text, and #929292 on the
   panel's white is 3.11:1. #494949 is 9.00:1 and still reads as a hint
   against the #000 the field's own value is set in. */
.filters__input::placeholder { color: var(--ink-muted); }

/* Tags are plain text on the live site, in a wrapping row. They are buttons
   here because they now filter; the resting state keeps the original's look,
   and the active state is a filled chip so it does not rely on colour alone. */
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }

.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.375rem;
  padding: 0.3125rem 0.75rem;
  background: var(--white);
  border: 1px solid var(--line);
  color: var(--ink);
  font-family: "Archivo", sans-serif;
  font-size: var(--text-sm);
  line-height: 1.4;
  cursor: pointer;
  transition: background-color var(--duration) var(--ease),
              border-color var(--duration) var(--ease),
              color var(--duration) var(--ease);
}
.chip:hover { border-color: var(--black); }
.chip[aria-pressed="true"] { background: var(--black); border-color: var(--black); color: var(--white); }

/* THE ONE PLACE .chip__count IS COLOURED. There used to be a second rule in
   the collections half of this file setting `opacity: 0.7` on the same
   element; the two composed rather than one winning, and #929292 at 70% over
   white is 2.10:1 — a count nobody can read beside a filter that says how
   many things it will show. --ink-muted is 9.00:1, and the count is already
   distinguished from the chip's label by being a step smaller. */
.chip__count { font-size: var(--text-xs); color: var(--ink-muted); }
.chip[aria-pressed="true"] .chip__count { color: rgb(255 255 255 / 0.7); }

.filters__status {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  background: var(--cream);
  border-top: 1px solid var(--line);
}
.filters__count { font-size: var(--text-sm); color: var(--dark-grey); }
.filters__clear { font-size: var(--text-sm); font-weight: 600; }

/* --- the grid --- */
.section--insights { background: var(--cream); padding-block: 0 var(--space-xl); }

.insight-grid { display: grid; gap: var(--space-sm); margin: 0; padding: 0; list-style: none; }
@media (min-width: 48rem) { .insight-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 80rem) { .insight-grid--3 { grid-template-columns: repeat(3, 1fr); } }

/* (`[hidden]` is handled once, in foundation.css. It has to be, because a
   class that sets `display` outranks the UA rule — see the note there.) */

.insight-card__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--white);
  transition: transform var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.insight-card__inner:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 28px rgb(0 0 0 / 0.10);
}
@media (prefers-reduced-motion: reduce) { .insight-card__inner:hover { transform: none; } }

/* Text first, image second — the live site's card order. */
.insight-card__text {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: var(--space-sm);
}

.insight-card__type {
  font-family: "Archivo", sans-serif;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.4;
}

.insight-card__title {
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-md);
  font-weight: 400;
  line-height: var(--leading-snug);
  /* Two lines keeps every card's text block the same height, so the images
     below them line up across the row. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
/* The underline belongs to the LINK and nowhere else. Underlining the
   heading as well drew a second rule a couple of pixels below the first —
   the "double underline". */
.insight-card__link {
  text-decoration: none;
  transition: color var(--duration) var(--ease);
}
/* Stretch the link over the card so the whole card is the target, without
   nesting interactive elements. */
.insight-card__link::after { content: ""; position: absolute; inset: 0; }
.insight-card__inner:hover .insight-card__link,
.insight-card__link:focus-visible { color: var(--blue); text-decoration: underline; }

.insight-card__summary {
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--dark-grey);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* Card art is authored at 1200x627. object-fit keeps that ratio whatever the
   source happens to be, so the current 826x601 exports crop to it and future
   1200x627 files fit exactly. No zoom on hover. */
/* Every cover in the CMS export is 825x600 (a few are 826x601). The box is
   set to that ratio and the image is CONTAINed rather than covered, so the
   whole artwork shows — several of these covers carry titles and logos right
   at the edge and a cover-crop clipped them. An image with a different ratio
   letterboxes onto the cream instead of losing its edges. */
.insight-card__media { margin-top: auto; background: var(--cream); }
.insight-card__media img,
.insight-card__media picture { width: 100%; }
.insight-card__media img { aspect-ratio: 825 / 600; object-fit: contain; }

.insight-empty { margin-top: var(--space-md); font-size: var(--text-md); }

/* ---------- article ----------------------------------------------------- */
.article { padding-block: var(--space-md) var(--space-xl); }

/* The breadcrumb is defined ONCE, in foundation.css. A second set of rules
   lived here — carried over with the Ludic Group article styles, which had
   their own in-article trail — and being later in the cascade it won: it
   removed the band's padding, so the trail sat tight under the nav with no
   air at all, and it added a `::before` separator on top of foundation's, so
   every crumb had two slashes waiting to happen. Deleted, not merged; two
   definitions of one component is the bug.
   ----------------------------------------------------------------------- */

.article__type { font-size: var(--text-base); font-weight: 600; margin-bottom: var(--space-2xs); }

.article__title {
  font-family: "Roboto Slab", Georgia, serif;
  font-size: clamp(1.5rem, 2.8vw, var(--text-xl));
  font-weight: 400;
  line-height: var(--leading-snug);
  margin-bottom: var(--space-xs);
}

.article__summary { font-size: var(--text-md); line-height: 1.6; }
.article__meta { margin-top: var(--space-xs); font-size: var(--text-sm); color: var(--dark-grey); }

.article__figure { margin: var(--space-md) 0 0; }
.article__figure img, .article__figure picture { width: 100%; }

.article__listen, .article__download { margin-top: var(--space-md); }
.article__download { padding: var(--space-sm); background: var(--cream); }
.article__download-note { margin-top: var(--space-2xs); font-size: var(--text-base); }

.article__body { margin-top: var(--space-md); }

.article__footer { margin-top: var(--space-lg); padding-top: var(--space-sm); border-top: 1px solid var(--line); }
.article__footer-title { font-size: var(--text-xs); font-weight: 600; margin-bottom: var(--space-2xs); }

.tag-pills { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
.tag-pill {
  display: inline-block;
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--black);
  font-size: var(--text-sm);
  text-decoration: none;
  transition: background-color var(--duration) var(--ease), color var(--duration) var(--ease);
}
.tag-pill:hover { background: var(--black); color: var(--white); }

/* ---------- legal pages -------------------------------------------------
   Privacy policy and terms of use. These reuse .article__body for the prose
   itself — same job, same measure — so there is one set of body-copy rules
   on this site rather than two that drift.
   ----------------------------------------------------------------------- */
.legal { padding-block: var(--space-lg) var(--space-xl); }
.legal__title { margin-bottom: var(--space-2xs); }
.legal__dateline {
  font-size: var(--text-sm);
  color: var(--dark-grey);
}

/* --- article prose ---
   These style body copy generated from Markdown, so they are element
   selectors inside .article__body rather than classed. */
.article__body > :first-child { margin-top: 0; }

.article__body h2 {
  font-family: "Archivo", sans-serif;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: var(--leading-snug);
  margin: var(--space-md) 0 var(--space-2xs);
}
.article__body h3 {
  font-size: var(--text-md);
  font-weight: 600;
  margin: var(--space-sm) 0 var(--space-2xs);
}

.article__body p { margin-bottom: var(--space-xs); font-size: var(--text-base); line-height: var(--leading-body); }
.article__body ul, .article__body ol { margin: 0 0 var(--space-xs); padding-left: 1.35em; }
.article__body li { margin-bottom: 0.4em; line-height: var(--leading-body); }
.article__body a { text-decoration: underline; }
.article__body strong { font-weight: 600; }

.article__body blockquote {
  margin: var(--space-md) 0;
  padding-left: var(--space-xs);
  border-left: 2px solid var(--black);
  font-size: var(--text-md);
  line-height: 1.5;
}

.article__body img { margin: var(--space-md) 0; }
.article__body hr { margin: var(--space-md) 0; border: 0; border-top: 1px solid var(--line); }


/* =========================================================================
   LUDIC ADVISORY BLOCKS

   REBUILT AGAINST MEASUREMENT. The first pass at these read the archive from
   its markup and a thumbnail, and got the design wrong in three ways that all
   pointed the same direction — towards something tidier and quieter than the
   site actually is:

     * Sixteen band colours became three. The archive's service pages are a
       ROTATION OF SATURATED BRAND BANDS, one per offer — Learning runs light
       green, mid blue, turquoise, pink, lime. That is not decoration on top of
       the design; it is the design.
     * The hover cards became static text. Each offer in a `section_layout16`
       band is a title above a 16rem photograph with its description revealed
       over the picture on hover. Twenty-two images, dropped.
     * Every band became the same height. The archive's tiles are `100vh`.

   Every number below is measured, with `_build/migration/probe.py`, at 1440
   and 390. The archive class it comes from is named beside it.
   ========================================================================= */

/* ---------- service hero — section_layout1 ------------------------------
   MEASURED: 630px tall; a full-bleed background film, `object-fit: cover`;
   a `1.25fr 1fr` grid inside the 80rem container (711 / 569 at 1440); and the
   copy on an OFF-WHITE CARD in the left column — #f9fbf7, the same card the
   home hero uses.

   The first pass put white copy directly on a dark scrim over the film. That
   is a different page: the archive never darkens the footage, it floats a
   card on it, and the copy is BLACK.
   ----------------------------------------------------------------------- */
.hero--service {
  position: relative;
  overflow: hidden;
  background: var(--surface-alt);
}

.hero--service .hero__still,
.hero--service .hero__video,
.hero--service .hero__video-poster {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero--service .hero__video { z-index: 1; }

.hero--service .hero__inner {
  position: relative;
  z-index: 2;
  display: grid;
  align-items: center;
  min-height: 39.375rem;      /* 630px measured */
  padding-block: var(--space-lg);
}
@media (min-width: 64rem) {
  .hero--service .hero__inner { grid-template-columns: 1.25fr 1fr; }
}

.hero--service .hero__content {
  background: var(--header-cream);
  padding: var(--space-md) var(--space-md) var(--space-lg);
  color: var(--black);
  max-width: 34rem;
}

.hero--service .hero__title,
.hero--collection .hero__title {
  font-size: var(--text-xl);       /* 32px measured */
  font-weight: 400;
  line-height: var(--leading-tight);
  text-wrap: balance;
}
.hero--service .prose__p { color: var(--ink-muted); }
.hero--service .button-group { margin-top: var(--space-sm); }

/* ---------- sub-brand lockups ------------------------------------------
   MEASURED: `.ludic-learning-logo` renders at 150x21 — `height: 1.3rem` with
   width auto. Every one of the eleven marks uses that class or a clone.
   Sized by HEIGHT because an SVG's file box is not its artwork's box: these
   sit in boxes with wildly different padding baked in, so one width makes
   them different optical sizes.
   ----------------------------------------------------------------------- */
.hero__brandmark,
.tile__brandmark,
.detail-hero__brandmark,
.detail__brandmark,
.section__brandmark,
.entry-card__mark,
.smartlab__logo {
  width: auto;
  height: 1.3rem;
  margin-bottom: var(--space-sm);
}
.smartlab__logo { height: 2rem; }
.feature__logo { width: auto; height: 2.5rem; margin-bottom: var(--space-sm); }
.entry-card__mark { height: 1rem; margin-bottom: var(--space-2xs); }

/* A black-ink lockup is invisible on the two dark bands. Only Ludic Learning
   has a reversed twin drawn; the rest invert, and `invert(1)` on a
   single-colour black mark gives exactly white, so this is not an
   approximation. */
.section[data-tone="black"] .tile__brandmark,
.section[data-tone="deep-blue"] .tile__brandmark,
.section[data-tone="dh-purple"] .tile__brandmark,
.section[data-tone="black"] .section__brandmark,
.detail-hero__brandmark { filter: invert(1); }

/* ---------- service tile — section_layout25 ----------------------------
   THE WORKHORSE: 17 uses, and the band that carries the site's colour.

   MEASURED at 1440: a full-bleed `1fr 1fr` grid, 672px per column, inside a
   1408px wrapper — NOT inside the 80rem container. `height: 100vh`. Copy in
   the left column with the sub-brand lockup, an eyebrow with a rule under it,
   a Roboto Slab 2rem/500 heading and a row of buttons. Artwork in the right
   column, `object-fit: cover`, running to the band edge.
   ----------------------------------------------------------------------- */
/* THE PANEL IS INSET, AND THE WHOLE PANEL IS THE COLOUR.
   ------------------------------------------------------------------------
   REMEASURED against the live capture of /learning, which is not what the
   static export shows and is what the client is looking at:

     .padding-section-small.background-color-<tone>   x=16, w=1408, h=1000
     consecutive panels                               26px apart
     .image-tile-right-overflow-none                  672px wide, NOT cropped

   Three things were wrong before, and they were one mistake: the colour was
   painted on the section, so it ran to the viewport edge; the artwork was
   `object-fit: cover` in a stretched column, so the right half of the tile
   was a cropped photograph instead of the band colour; and the tile was
   `min-height: 100vh`, so four of them were four screens of scrolling.

   Now: the section carries the page colour, the TILE carries `--band` (every
   tone declares it — see foundation.css), and the artwork floats inside the
   panel at its own ratio with air around it. Shorter, too, at the client's
   request: 36rem rather than a full viewport. `min-height`, so a tile whose
   copy runs long grows rather than clipping. */
.section--tile {
  padding: 0.8rem 1rem;             /* 26px between panels, measured */
  background: var(--surface-alt);
}
/* Beats `.section[data-tone="x"]`, which is what paints every other band.
   Without the extra class this loses on specificity and the tone runs edge
   to edge again. */
.section[data-tone].section--tile { background: var(--surface-alt); }
.section--tile > .container { max-width: none; padding: 0; }

.tile {
  display: grid;
  align-items: center;
  background: var(--band, var(--surface-alt));
}
@media (min-width: 64rem) {
  .tile {
    grid-template-columns: 1fr 1fr;
    min-height: 36rem;
  }
  .tile--flip .tile__body { order: 2; }
  .tile--flip .tile__media { order: 1; }
  .tile--nomedia { grid-template-columns: 1fr; }
  .tile--nomedia .tile__body { max-width: 60rem; margin-inline: auto; }
}

.tile__body {
  padding: var(--space-xl) var(--container-pad);
  width: 100%;
}
/* MEASURED: the archive's copy measure inside a tile is 592px at 1440. An
   earlier version set `max-width: 40rem` AND `justify-self: end` on top of the
   asymmetric padding below, which left 472px and wrapped the heading onto five
   lines where the archive takes four. The column's own width plus the padding
   is the measure; nothing else needs to constrain it. */
@media (min-width: 64rem) {
  /* The copy column's inner edge lines up with the 80rem container's left
     edge, so a tile's first word sits directly under the heading of the band
     above it. Measured: 72px from the viewport at 1440. */
  .tile__body { padding-inline: max(var(--container-pad), calc((100vw - 80rem) / 2 + var(--container-pad))) var(--space-lg); }
}

/* The eyebrow, and its rule. MEASURED: 14.4px, and a 1px rule directly under
   it spanning the copy column. The first pass dropped the rule. */
.tile__eyebrow {
  margin-bottom: var(--space-sm);
  padding-bottom: 0.4rem;
  border-bottom: 1px solid currentColor;
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* MEASURED: Roboto Slab, 2rem, weight 500, line-height 40px. The archive's
   display face for a tile heading is the slab, not Archivo — Archivo carries
   the page headings (`heading-style-h3`) and the slab carries these
   (`heading-3`). Both are self-hosted; the Ludic Group build ships the same
   pair for the same reason. */
.tile__title {
  margin-bottom: var(--space-sm);
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-xl);
  font-weight: 500;
  line-height: 1.25;
  text-wrap: balance;
}
.tile__body .prose__p { max-width: 34rem; }
.tile__body .button-group { margin-top: var(--space-md); gap: 1.25rem; }

/* THE ARTWORK FLOATS. `contain`, not `cover`: the live tile's own class is
   `image-tile-right-overflow-none` and the picture is whole inside the panel,
   which is the client's "zoomed out (fully visible, not cropped) and
   floating". Padding on the cell is what makes it float rather than butt up
   against the panel edge; 1.5rem leaves 656px at 1440, against the live
   672px. A cap on the height keeps a portrait picture — Learning's artwork is
   604x1148 — from setting the height of the whole band. */
.tile__media {
  display: flex;
  align-items: center;
  justify-content: flex-end;        /* flush to the panel's outer edge */
  padding: var(--space-sm) 0;
}
/* A flipped tile puts the artwork in the LEFT column, so its outer edge is
   the left one. Without this the picture was pushed toward the middle of the
   band and left a gap against the panel edge it was meant to meet. */
.tile--flip .tile__media,
.tile--flip .tile__media--film { justify-content: flex-start; }
/* `max-width`/`max-height` rather than `width: 100%`, so the artwork keeps its
   own ratio and is never stretched or cropped — the cap simply makes it
   smaller. The height cap is what stops Learning's portrait artwork
   (604x1148) from setting a 1300px band. */
.tile__media img,
.tile__media picture,
.tile__media video {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 32rem;
  object-fit: contain;
}
/* THE BLEED VARIANT. The artwork fills its half of the panel and is cropped
   to it — for the montages that are composed to run off the edge. Everything
   else stays contained and whole, which is the default and the common case. */
.tile--bleed .tile__media { padding: 0; align-self: stretch; }
.tile--bleed .tile__media img,
.tile--bleed .tile__media picture,
.tile--bleed .tile__media video {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: cover;
}
@media (min-width: 64rem) {
  .tile--bleed .tile__media { position: relative; min-height: 100%; }
  .tile--bleed .tile__media > img,
  .tile--bleed .tile__media > picture,
  .tile--bleed .tile__media > video { position: absolute; inset: 0; }
}

@media (max-width: 63.999rem) {
  .tile__media { padding: var(--space-sm) var(--container-pad) 0; justify-content: center; }
  .tile__media img,
  .tile__media picture,
  .tile__media video { max-height: 22rem; }
  .tile__body { padding-block: var(--space-lg); }
}

/* (The desktop override that took the artwork out of flow and cropped it to
   the row is gone with `cover`. A contained picture cannot set the row height
   past `max-height`, so it no longer needs to be removed from flow.) */

/* One tile in the archive fills its artwork column with a looping film rather
   than a picture — Learning's "Off the shelf courses". The poster is a real
   <img> under the video, not the video's own `poster` attribute: that one
   vanishes the instant the first frame decodes, and never shows at all where
   autoplay is refused. Same reasoning as the hero; see blocks._bg_video.
   Stacked with grid so the two overlap at every width, including the narrow
   one where the column is an aspect-ratio box rather than a stretched cell. */
/* The film and its poster share one grid cell so they overlap exactly. Both
   are sized the same way as a plain still, and the video carries the poster's
   width and height attributes so the two agree before any metadata loads. */
.tile__media--film {
  display: grid;
  justify-content: flex-end;
  align-content: center;
  overflow: hidden;
}
.tile__media--film > * {
  grid-area: 1 / 1;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 32rem;
  object-fit: contain;
}

/* ---------- hover cards — section_layout16 / .cms3_link ----------------
   MEASURED: a white card of `padding: 3rem` inside the band; a heading with a
   rule under it; then a `1fr 1fr 1fr 1fr` grid, `align-items: end`, 2rem row
   gap. Each card is a title above a 16rem picture, `object-fit: cover`, with
   a WHITE panel — `position: absolute; inset: 0; padding: 1rem` — revealed
   over the picture on hover.

   This is the same shape as the collection card the client asked for, which
   is a good sign it was the right pattern to standardise on: title above,
   artwork beneath, detail on hover.
   ----------------------------------------------------------------------- */
/* THE PANEL IS FULL-BLEED-MINUS-16px, AND THE BAND AROUND IT IS THE PAGE
   GREY. REMEASURED on the live Events capture at 1440:

     section_layout16          #f2f2f2, full width
     .hover-card-section       x=16, w=1408, WHITE, padding 64px
     consecutive panels        26px apart
     .cms3_list                grid 320px x4, COLUMN GAP 0, row gap 32px
     .cms3_image_wrapper       320x256

   Two things were wrong. The panel was inside the 80rem container, so it was
   1280px in a 1440 viewport with 80px of band either side instead of 16px.
   And `probe.py` had recorded two of the three bands as white — it read the
   panel, not the section — so on those two a white panel sat on a white band
   and the section looked full width with no box at all. */
.section--offers { padding: 0.8rem 0; }
.section--offers > .container { max-width: none; padding-inline: 1rem; }

/* MEASURED: `.text-size-large`, 24px/36px, centred on a 960px measure. The
   accordion band's own lede is the same element on the same page — "Ludic's
   event frameworks have been used to engage large groups of people..." — and
   the client asked for it centred and bigger, which is what it already is on
   the live site. Both, in one rule. */
.section--offers > .container > .section__head--center .prose__p,
.section--accordion > .container > .section__head--center .prose__p {
  font-size: var(--text-lg);        /* 24px measured */
  line-height: 1.5;                 /* 36px measured */
  max-width: 60rem;                 /* 960px measured */
  margin-inline: auto;
  text-wrap: balance;
}
.section--accordion > .container > .section__head--center { margin-bottom: var(--space-xl); }
.section--offers > .container > .section__head--center { margin-bottom: var(--space-lg); }

.offers-panel {
  background: var(--surface);
  padding: 4rem;                    /* 64px measured */
}
@media (max-width: 63.999rem) { .offers-panel { padding: var(--space-lg); } }
@media (max-width: 47.999rem) { .offers-panel { padding: var(--space-md); } }

/* `section_ludic_media_studio` sets the same four cards straight on the band
   colour — a `hover-card-section-marketing` with no card behind it. Only
   `section_layout16` has the white panel. */
.offers-panel--bare { background: none; padding: 0; }

.offers-panel__title {
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-xl);
  font-weight: 400;
  line-height: var(--leading-tight);
}

.offers {
  display: grid;
  /* 32px row, ZERO column: the four pictures butt against each other and read
     as one strip, which is what the live band does and what the client's
     screenshots of it show. */
  gap: 2rem 0;
  margin-top: var(--space-md);
  /* `align-items: end` is the archive's, and it is what makes a row of cards
     with one-line and two-line titles put every picture on the same baseline
     — the titles grow upward, not the pictures downward. */
  align-items: end;
}
@media (min-width: 36rem) { .offers { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .offers { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 64rem) { .offers--3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 64rem) { .offers--6 { grid-template-columns: repeat(3, 1fr); } }

.offer { display: flex; flex-direction: column; height: 100%; }

.offer__title {
  margin-bottom: var(--space-sm);
  padding-inline: 0.5rem;
  font-size: var(--text-md);       /* 20px measured */
  font-weight: 600;
  line-height: 1.5;
}

.offer__media {
  position: relative;
  height: 16rem;                   /* .cms3_image_wrapper, measured */
  margin-top: auto;
  overflow: hidden;
}
.offer__media img,
.offer__media picture { width: 100%; height: 100%; object-fit: cover; }

/* The archive's own 25%-black wash over every card picture. Without it the
   photographs fight the copy above them. */
.offer__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.25);
  pointer-events: none;
}

/* The reveal. White panel, 1rem padding, over the whole picture.
   OPACITY, not `display: none` as the archive has it — a panel that is
   display:none is not in the accessibility tree at all, so a screen-reader
   user never gets the description. At opacity 0 it is announced normally and
   simply not painted, which is the same visual result and a strictly better
   one for anybody not using a mouse. */
.offer__reveal {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 1rem;
  background: var(--surface);
  color: var(--ink);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  overflow: auto;
  opacity: 0;
  transition: opacity var(--duration) var(--ease);
}
.offer:hover .offer__reveal,
.offer:focus-within .offer__reveal { opacity: 1; }

/* A touch device has no hover, so the panel would never open. There the
   description sits under the picture instead — same content, in flow. */
@media (hover: none) {
  .offer__reveal {
    position: static;
    opacity: 1;
    padding: var(--space-xs) 0.5rem 0;
    background: none;
    overflow: visible;
  }
  .offer__media::after { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .offer__reveal { transition: none; }
}

/* ---------- accordion — section_faq1 -----------------------------------
   MEASURED: a THREE-COLUMN grid at desktop (373px each, 48px gap), not the
   single stacked list the first pass built. Each item has a 1px black bottom
   rule, a toggle of `padding: 1.4rem 0`, a 20px/1.4 label, and a 1.25rem
   plus/minus expander on the right that becomes a minus when open.

   Built on <details>/<summary>, which is the same click-to-expand the archive
   does with its dropdown script — and needs no JavaScript, is keyboard
   operable with no ARIA to get wrong, and prints expanded.
   ----------------------------------------------------------------------- */
.section--accordion { padding-block: var(--space-xl); }

.accordion {
  display: grid;
  gap: 0 var(--space-lg);
  margin-top: var(--space-lg);
  align-items: start;
}
/* THE COLUMNS ARE REAL COLUMNS, one list each, split by count in
   `blocks.accordion`. Each stacks on its own, so a two-line label pushes only
   its own column and every title sits directly under the rule above it —
   which is the live layout, measured at three columns of 373px.

   Neither CSS answer alone worked. A grid fills row by row, so a two-line
   label made its whole ROW taller and left 45px of air beside it. CSS
   multi-column balances by HEIGHT, so an open first panel dragged a nine-item
   band into one column, then six, then two. Splitting by count in the block
   is what gives both properties. */
.accordion-columns { display: grid; gap: var(--space-lg); margin-top: var(--space-lg); }
@media (min-width: 48rem) {
  .accordion-columns--2,
  .accordion-columns--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 64rem) {
  .accordion-columns--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .accordion-columns--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.accordion-columns > .accordion { margin-top: 0; align-content: start; }

/* The COLUMN-GROUPED variant — Ludic Transformation's "How we work". The
   archive's three columns each carry a phase name, a sub-line and four
   disclosures of their own, and a flat `accordion--3` cannot express that: a
   CSS grid fills row by row, so a phase's four steps would land in four
   different columns. Each group is its own list instead, and the groups are
   what the three columns hold. */
.accordion-groups { display: grid; gap: var(--space-lg); margin-top: var(--space-lg); }
@media (min-width: 64rem) {
  .accordion-groups--3 { grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); }
}
.accordion-group > .accordion { margin-top: var(--space-sm); }
.accordion-group__title { font-size: var(--text-lg); line-height: 1.3; }
.accordion-group__sub { font-size: var(--text-base); }

.accordion__row { border-bottom: 1px solid var(--ink); }

.accordion__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-block: 1.4rem;
  font-size: var(--text-md);
  line-height: 1.4;
  cursor: pointer;
  /* Firefox honours `list-style`, Safari honours the pseudo-element. Drop
     either and one browser keeps a stray triangle on the wrong side. */
  list-style: none;
}
.accordion__summary::-webkit-details-marker { display: none; }
.accordion__summary:hover { color: var(--blue-ink); }

.accordion__label { flex: 1; }

/* The archive's plus/minus, drawn rather than an icon file: two 1.25rem bars
   crossing, and the vertical one folds away when the row opens. */
.accordion__expander {
  position: relative;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
}
.accordion__expander::before,
.accordion__expander::after {
  content: "";
  position: absolute;
  background: currentColor;
  transition: transform var(--duration) var(--ease), opacity var(--duration) var(--ease);
}
.accordion__expander::before {          /* the horizontal bar — always there */
  top: 50%;
  left: 0;
  width: 1.25rem;
  height: 0.125rem;
  transform: translateY(-50%);
}
.accordion__expander::after {           /* the vertical bar — folds on open */
  left: 50%;
  top: 0;
  width: 0.125rem;
  height: 1.25rem;
  transform: translateX(-50%);
}
[open] > .accordion__summary .accordion__expander::after {
  transform: translateX(-50%) rotate(90deg);
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .accordion__expander::before,
  .accordion__expander::after { transition: none; }
}

.accordion__body { padding-bottom: var(--space-md); }
.accordion__p { margin-bottom: var(--space-xs); }
.accordion__p:last-child { margin-bottom: 0; }
.accordion__list {
  margin: 0 0 var(--space-xs);
  padding-left: 1.15rem;
  list-style: disc;
}
.accordion__list li { margin-bottom: 0.25rem; }
.accordion__list:last-child { margin-bottom: 0; }
.accordion__media { margin-top: var(--space-sm); }
.accordion__media img { width: 100%; height: auto; }
/* The band's own buttons, under the columns and centred — where
   `section_ludic_media_studio` puts them. */
.accordion__actions { margin-top: var(--space-xl); }
.accordion__actions .button-group { justify-content: center; }

/* ---------- figure accordion — section_faq6 ----------------------------
   Two columns: the disclosures on the left, one shared picture on the right
   that swaps to match whichever row is open. js/site.js does the swap; with
   no JavaScript each row simply carries its own picture inline, so the
   content is complete either way.
   ----------------------------------------------------------------------- */
.figure-accordion { display: grid; gap: var(--space-lg); align-items: start; }
@media (min-width: 64rem) {
  .figure-accordion { grid-template-columns: 1fr 1fr; gap: var(--space-xl); }
  .figure-accordion__stage { position: sticky; top: calc(var(--header-h) + var(--space-md)); }
}
.figure-accordion__stage img { width: 100%; height: auto; }
/* With JS, the inline pictures are hidden and the stage carries them. */
/* (The variant rules that own this live further down, with the panel. Two
   definitions of one component is the bug; this one is deleted.) */

/* ---------- statement — section_layout3 ---------------------------------
   MEASURED: cream, 32px Archivo at 400 / 38.4px, CENTRED, on a 60rem measure
   (`.max-width-large`), 237-372px tall depending on how long the sentence is.
   ----------------------------------------------------------------------- */
.section--statement { padding-block: var(--space-xl); }
.section--statement .container { text-align: center; }
.statement {
  margin-inline: auto;
  max-width: 46ch;
  font-family: "Archivo", system-ui, sans-serif;
  font-size: var(--text-xl);
  font-weight: 400;
  line-height: var(--leading-tight);
  text-wrap: balance;
}
.section--statement .prose__p {
  margin-top: var(--space-sm);
  margin-inline: auto;
  margin-bottom: 0;
  max-width: 52ch;
  font-size: var(--text-md);
  line-height: var(--leading-snug);
  color: var(--ink-muted);
}

/* THE ABOUT PAGE'S SHAPE: a centred heading, a centred lede, then the prose
   in two columns. MEASURED on the live capture at 1440 —

     p.text-size-medium (the lede)   20px/400/30px, centred, 960px measure
     .layout239_list                 grid 576px 576px, 32px gutter, 64px rows
     .layout239_item p               24px/400/36px, left aligned

   One column per entry in `groups`, which is how the archive builds it: the
   two long paragraphs are two items side by side, not one stream flowed
   across two columns. */
.statement__lede {
  margin: var(--space-sm) auto 0;
  max-width: 60rem;                 /* 960px measured */
  font-size: var(--text-md);        /* 20px measured */
  line-height: 1.5;                 /* 30px measured */
}
.statement__columns {
  display: grid;
  gap: var(--space-xl) var(--space-md);
  margin-top: var(--space-xl);
  text-align: left;
}
@media (min-width: 64rem) {
  .statement__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.statement__column .prose__p {
  margin-top: 0;
  margin-inline: 0;
  max-width: none;
  font-size: var(--text-lg);        /* 24px measured */
  line-height: 1.5;                 /* 36px measured */
  color: var(--ink);
}
.statement__column .prose__p + .prose__p { margin-top: var(--space-sm); }

/* ---------- capability grid — section_ludic-learning-grid --------------
   MEASURED: a light-lime band carrying sixteen short cells under one lockup.
   The archive runs them four across at every width above a phone, which puts
   eleven words on two lines in a 200px column; this steps 2 / 3 / 4.
   ----------------------------------------------------------------------- */
.section--capabilities { padding-block: var(--space-xl); }
.capabilities {
  display: grid;
  gap: var(--space-md) var(--space-lg);
  margin-top: var(--space-lg);
}
@media (min-width: 36rem) { .capabilities { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 48rem) { .capabilities { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 80rem) { .capabilities { grid-template-columns: repeat(4, 1fr); } }

.capability { border-top: 1px solid var(--line); padding-top: var(--space-sm); }
.capability__title {
  margin-bottom: var(--space-2xs);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--leading-snug);
  /* The archive types these in capitals in the CMS. Caps are a typographic
     choice, so the text is sentence case and this does the shouting — which
     is what stops a screen reader spelling them out letter by letter. */
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.capability__body { font-size: var(--text-sm); }

/* ---------- pillars — section.how-we-work ------------------------------ */
.section--pillars { padding-block: var(--space-xl); }
.section__head--center { max-width: 52rem; margin-inline: auto; text-align: center; }
.pillars { display: grid; gap: var(--space-lg) var(--space-md); margin-top: var(--space-lg); }
@media (min-width: 48rem) {
  .pillars--2, .pillars--4 { grid-template-columns: repeat(2, 1fr); }
  .pillars--3 { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 64rem) { .pillars--4 { grid-template-columns: repeat(4, 1fr); } }
.pillar { border-top: 1px solid var(--line); padding-top: var(--space-sm); }
.pillar__title {
  margin-bottom: var(--space-2xs);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: var(--leading-snug);
}
.pillar__body { color: var(--ink-muted); }
.pillar__action { margin-top: var(--space-sm); }

/* The collection indexes' `how-we-work-grid`: the rule is UNDER THE TITLE,
   between it and the copy, not over the whole cell. MEASURED: 272px x 4 at
   1184, a 20px/500 title, a 1px rule, then the copy — which the archive sets
   LARGER than its own title, at 24px. */
.pillars--ruled .pillar {
  border-top: 0;
  padding-top: 0;
  display: flex;
  flex-direction: column;
}
/* A cell that carries a "Learn more" puts it on the foot of the cell, so a
   row of three with two-, three- and four-line copy still has its buttons on
   one line — which is what the archive's stretched grid row does. */
.pillars--ruled .pillar__action { margin-top: auto; padding-top: var(--space-sm); }
.pillars--ruled .pillar__title {
  margin-bottom: var(--space-sm);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--ink);
  font-weight: 500;
}
.pillars--ruled .pillar__body { color: var(--ink); font-size: var(--text-md); }
/* The three collection indexes are the only pillars bands that carry a button
   of their own, and the archive centres it under the grid. */
.section--pillars .button-group { justify-content: center; margin-top: var(--space-lg); }

/* ---------- logo wall — section_layout20 ------------------------------
   MEASURED: white band; `.org-logo` and `.academic-logo` are
   `max-height: 5rem; object-fit: contain`, and the academic row is
   `filter: grayscale()` with a .25s transition.
   ----------------------------------------------------------------------- */
.section--logo-wall { padding-block: var(--space-xl); }
.logo-wall__title { max-width: 44rem; margin-inline: auto; }
.logo-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: var(--space-md) var(--space-sm);
  margin-top: var(--space-lg);
  align-items: center;
}
@media (min-width: 48rem) {
  .logo-wall { grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); }
}
.logo-wall__item { display: flex; align-items: center; justify-content: center; }
.logo-wall__mark {
  width: auto;
  max-width: 100%;
  max-height: 5rem;               /* the archive's own */
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.8;
  transition: filter var(--duration) var(--ease), opacity var(--duration) var(--ease);
}
.logo-wall__item:hover .logo-wall__mark { filter: none; opacity: 1; }

/* CERTIFICATION MARKS ARE NOT GREYED. The client and academic walls are set
   in grey because thirty marks in thirty brand colours is a quilt; a
   certification is a trust mark, and a greyed-out ISO or UKAS badge reads as
   lapsed. Full colour, full opacity, and a little larger. */
.logo-wall--4 .logo-wall__mark {
  max-height: 5.5rem;
  filter: none;
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) { .logo-wall__mark { transition: none; } }

/* ---------- quote — section_layout25, text only ------------------------ */
.section--quote { padding-block: var(--space-xl); }
/* ---------- testimonials — `.quote-wrapper` ----------------------------
   REMEASURED on the live captures, which set these very differently from the
   centred pull-quote this build had:

     .quote-wrapper        padding 64px 80px
     .layout25_component   max-width 960px
     the quote             24px/42px/400, LEFT aligned
     .large-quote-marks    64px, one before the text and one after it
     gap to the name       20px
     the name              20px/700/35px — BOLD

   The marks are `::before`/`::after` rather than characters in the copy: they
   are typography, not words, and putting them in the content would mean every
   quote in every copy module carried decoration the fidelity check would then
   have to know about. `line-height: 0` keeps a 64px glyph from stretching the
   line box it sits in — without it the first line of every quote gets 22px of
   extra leading and the block looks broken. */
.quote {
  max-width: 60rem;                 /* 960px measured */
  margin-inline: auto;
  padding: 1rem 0;
  text-align: left;
}
.quote__text {
  margin: 0;
  font-size: var(--text-lg);        /* 24px measured */
  font-weight: 400;
  line-height: 1.75;                /* 42px measured */
  text-wrap: pretty;
}
.quote__text::before,
.quote__text::after {
  position: relative;
  top: 0.22em;
  font-size: 4rem;                  /* 64px measured */
  line-height: 0;
  vertical-align: baseline;
}
.quote__text::before { content: "\201C"; }
.quote__text::after { content: "\201D"; }

.quote__cite { margin-top: 1.25rem; display: block; }   /* 20px measured */
.quote__source {
  font-style: normal;
  font-size: var(--text-md);        /* 20px measured */
  font-weight: 700;                 /* bold, measured */
  line-height: 1.75;
}
.quote__org { display: block; font-size: var(--text-sm); opacity: 0.85; }

@media (max-width: 47.999rem) {
  .quote__text::before,
  .quote__text::after { font-size: 2.5rem; }
}

/* ---------- note band, SmartLab band, reasons, CTA --------------------- */
.section--note { padding-block: var(--space-lg); }
/* `section_ready_to_accelerate` is 314px against `section_layout20`'s 353px
   for half the content, because it pays a full band's padding around one
   heading and one row of buttons. */
.section--note:has(.note-band__title) { padding-block: var(--space-xl); }
.note-band {
  display: grid;
  gap: var(--space-md);
  max-width: 60rem;
  margin-inline: auto;
  text-align: center;
}
.note-band__text { font-size: var(--text-md); line-height: var(--leading-snug); }
/* `section_ready_to_accelerate`, which is this shape with a real 32px h2 in
   it rather than a sentence. MEASURED: `h2.heading-style-h3.text-align-center`,
   32px/400, over a centred row of buttons. */
.note-band__title { font-weight: 400; }
.note-band .button-group { justify-content: center; }

/* THE SMARTLAB BAND IS A SERVICE TILE, and is now built like one.
   The client's note, on the blue band on Ludic Pivot: it should be styled
   "like the green one re: padding spacing border, not being edge to edge",
   with the type sizes matching too — and the same everywhere it appears.

   So it takes the tile's geometry exactly: the section carries the page
   colour with 0.8rem/1rem of padding, the PANEL carries `--band` and sits
   inset, and the copy column pays the same asymmetric padding that lines a
   tile's first word up with the 80rem container's left edge. The heading is
   the tile's Roboto Slab 2rem/500 rather than a step smaller, and the
   sentence beside the lockup is the tile's body size.

   Its ONE difference from `service_tile` stays: this band has a lockup, and
   the coloured tiles no longer do. That lockup is the point of the band —
   it is where SmartLab is named. */
.section--smartlab { padding: 0.8rem 1rem; background: var(--surface-alt); }
.section[data-tone].section--smartlab { background: var(--surface-alt); }
.section--smartlab > .container { max-width: none; padding: 0; }

.smartlab {
  display: grid;
  align-items: center;
  background: var(--band, var(--surface-alt));
}
@media (min-width: 64rem) {
  .smartlab { grid-template-columns: 1fr 1fr; min-height: 36rem; }
  .smartlab--nomedia { grid-template-columns: minmax(0, 1fr); }
}

.smartlab__body { padding: var(--space-xl) var(--container-pad); width: 100%; }
@media (min-width: 64rem) {
  .smartlab__body {
    padding-inline: max(var(--container-pad),
                        calc((100vw - 80rem) / 2 + var(--container-pad))) var(--space-lg);
  }
  .smartlab--nomedia .smartlab__body { max-width: 60rem; margin-inline: auto; }
}

.smartlab__title {
  margin-bottom: var(--space-sm);
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-xl);         /* the tile's 2rem, measured */
  font-weight: 500;
  line-height: 1.25;
  text-wrap: balance;
}
/* MEASURED: the sentence beside the lockup is `text-size-large` — 24px on all
   four pages that carry this band, not the 16px body the first pass gave it.
   20px is the site's own step nearest to it; going the whole way would make it
   the same size as the band's own heading on Leadership and Medical, where
   that heading IS the 24px line. */
.smartlab__body .prose__p { font-size: var(--text-md); line-height: var(--leading-snug); }
.smartlab__body .button-group { margin-top: var(--space-md); gap: 1.25rem; }

/* The artwork floats whole and flush to the panel's outer edge, exactly as a
   tile's does — same rule, same cap, same reason.

   THE OUTER EDGE IS THE CONTAINER'S, NOT THE VIEWPORT'S, and that is a fix
   rather than the original intent. The copy column is pinned to the 80rem
   container's left edge by the asymmetric padding below; the artwork was
   pinned to the viewport's right edge. On a narrow window the two are the same
   thing, and the band is correct. On a wide one they pull apart: the column is
   half the viewport, the image stops growing at `max-height: 32rem` (1217px at
   this aspect), and `justify-content: flex-end` puts every pixel of the
   leftover between the copy and the picture. Measured on a ~3900px window:
   730px of empty band down the middle, which is what the client saw.

   Mirroring the body's padding puts both halves on the same container, so they
   stay adjacent at any width. It costs the artwork 120px at 1440 — it now
   stops at the container edge instead of the wrapper edge, which is where the
   copy already starts. This band also ships on Ludic Pivot and Ludic Medical,
   and they get the same fix; nothing about their markup changes. */
.smartlab__media {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: var(--space-sm) 0;
  padding-inline-end: max(var(--container-pad),
                          calc((100vw - 80rem) / 2 + var(--container-pad)));
}
@media (max-width: 63.999rem) {
  /* Stacked: the artwork is centred under the copy and the mirrored padding
     would shove it off to one side. */
  .smartlab__media { padding-inline-end: var(--container-pad); }
}
.smartlab__media img,
.smartlab__media picture {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 32rem;
  object-fit: contain;
}
@media (max-width: 63.999rem) {
  .smartlab__media { padding: var(--space-sm) var(--container-pad) 0; justify-content: center; }
  .smartlab__media img,
  .smartlab__media picture { max-height: 22rem; }
  .smartlab__body { padding-block: var(--space-lg); }
}

/* MEASURED: a 960px wrapper, a 32px centred h2, one bold 24px line across the
   full measure, and the reasons themselves in the archive's `layout239_list` —
   TWO 464px columns, 32px apart, with no rules on anything. */
.section--reasons { padding-block: var(--space-xl); }
.section--reasons .section__head--center { max-width: 60rem; }
.reasons__lede {
  margin-top: var(--space-sm);
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
}
.reasons { display: grid; gap: var(--space-md) var(--space-md); margin-top: var(--space-lg); }
@media (min-width: 48rem) {
  .reasons--2 { grid-template-columns: repeat(2, 1fr); }
  .reasons--3 { grid-template-columns: repeat(3, 1fr); }
}
.reasons--2 { max-width: 60rem; margin-inline: auto; }
.reason { font-size: var(--text-md); line-height: var(--leading-snug); }

/* MEASURED: the closing CTA band is lime (`cta-block`, rgb(203,255,0)) and
   288px tall, centred. The first pass made it black. */
.section--cta { padding-block: var(--space-xl); }
.cta { max-width: 52rem; margin-inline: auto; text-align: center; }
.cta__title {
  font-size: var(--text-xl);
  font-weight: 400;
  line-height: var(--leading-tight);
}
.cta .button-group { justify-content: center; margin-top: var(--space-md); }

/* `section_layout16` used as a closer — both campaign pages. The band measures
   CREAM and the lime is a `cta-card` of 1248px inside it, so the colour is on
   the card and the band keeps its own tone. */
.cta--panel {
  max-width: none;
  padding: var(--space-lg) var(--space-md);
}
.cta--panel[data-panel="lime"] { background: var(--band-lime); }

/* ---------- video banner ----------------------------------------------- */
.video-banner { position: relative; overflow: hidden; background: var(--black); aspect-ratio: 21 / 9; }
.video-banner .hero__video,
.video-banner .hero__video-poster {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
@media (max-width: 47.999rem) { .video-banner { aspect-ratio: 16 / 9; } }

/* ---------- newsletter — section_newsletter-signup --------------------
   MEASURED: Deep Blue (#44597b), 236px, 40px of vertical padding.
   ----------------------------------------------------------------------- */
.section--newsletter { padding-block: var(--space-lg); }
.newsletter { display: grid; gap: var(--space-lg); }
@media (min-width: 64rem) {
  .newsletter { grid-template-columns: 0.85fr 1fr; gap: var(--space-xl); align-items: start; }
}
.newsletter__title {
  font-size: var(--text-lg);
  font-weight: 400;
  line-height: var(--leading-tight);
  margin-bottom: var(--space-sm);
}
.newsletter__intro .prose__p { max-width: 34rem; }
.section--newsletter[data-tone="deep-blue"] .form__input,
.section--newsletter[data-tone="deep-blue"] .form__check {
  border-color: var(--white);
  background: transparent;
  color: var(--white);
}
.section--newsletter[data-tone="deep-blue"] .form__input::placeholder { color: rgb(255 255 255 / 0.72); }
.form--inline .form__legal { margin-top: var(--space-xs); }

.section--thanks { padding-block: var(--space-2xl); }
.section--thanks .button-group { margin-top: var(--space-md); }

/* ---------- collection hero — header.learning-hero --------------------
   MEASURED at 1440: `.learning-course-grid` is FULL-BLEED, 720px + 720px, and
   the band is three parts — a lockup bar with a rule under it, the copy on the
   band's own cream in the left half, and a PAINTED PANEL filling the right
   half to the edge of the viewport with the SmartLab badge and the key
   artwork in it. All three pages paint that panel light green.

   The heading is small on purpose: `h1.roboto-slab-medium` measures 20.8px at
   weight 500, because on these three pages the h1 is a sentence rather than a
   banner and the body copy under it is 16px.
   ----------------------------------------------------------------------- */
.hero--collection { background: var(--surface-alt); }

.hero__bar { border-bottom: 1px solid var(--ink); }
.hero__bar-inner {
  padding: var(--space-sm) var(--container-pad);
  max-width: 80rem;
  margin-inline: auto;
}
@media (min-width: 64rem) {
  /* The bar's copy lines up with the copy column below it, which at 1440 puts
     it 120px in — the archive's own `padding-global`. */
  .hero__bar-inner {
    max-width: none;
    margin-inline: 0;
    padding-inline: max(var(--container-pad), calc((100vw - 80rem) / 2 + var(--container-pad)));
  }
}
.hero--collection .hero__brandmark { margin-bottom: 0; }
.hero--collection .eyebrow { margin-top: var(--space-2xs); margin-bottom: 0; }

.hero__split { display: grid; }
@media (min-width: 64rem) {
  .hero__split { grid-template-columns: 1fr 1fr; align-items: center; }
}
.hero--collection .hero__content {
  position: relative;
  padding: var(--space-lg) var(--container-pad) var(--space-xl);
}
@media (min-width: 64rem) {
  .hero--collection .hero__content {
    padding-inline: max(var(--container-pad), calc((100vw - 80rem) / 2 + var(--container-pad))) var(--space-lg);
  }
}
.hero--collection .hero__title--collection {
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-md);       /* 20.8px measured */
  font-weight: 500;
  line-height: var(--leading-snug);
}
.hero--collection .hero__content .prose__p { margin-top: var(--space-sm); }
.hero--collection .button-group { margin-top: var(--space-md); }

.hero__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
  gap: var(--space-sm);
  padding: var(--space-md) var(--container-pad);
}
.hero__panel[data-panel="light-green"] { background: var(--band-light-green); }
.hero__badge { width: 8rem; height: auto; }
.hero__media img { width: 100%; height: auto; }

/* ---------- page head --------------------------------------------------- */
.section--page-head { padding-block: var(--space-lg) var(--space-md); }
.page-lede {
  margin-top: var(--space-sm);
  max-width: 44rem;
  font-size: var(--text-md);
  line-height: var(--leading-snug);
  color: var(--ink-muted);
}
.page-title__mark { width: auto; height: 2.25rem; margin-bottom: var(--space-sm); }

/* ---------- collection card and grid — MEASURED OFF THE LIVE SITE -------
   `_webarchives/Discovery Labs - Ludic.webarchive` is a Safari capture of the
   live page, which is a later build than the static export. Its numbers:

     .collection-list   grid-template-columns: 1fr 1fr; gap: 0.4rem
                        1408px wide at a 1440 viewport — FULL BLEED with 1rem
                        of air either side, not inside the 80rem container
     .collection-item   white, flex column
     .cms-card-header   width 100%, padding 1.1rem 1.8rem 1rem
     .layout9_item      height 32rem desktop / 28rem tablet
     .case-study-image  object-fit: cover, fills
     .case-study-video  position: absolute; inset: 0; z-index: 2 — the film,
                        shown instead of the still on the items that have one

   A FIXED MEDIA HEIGHT, not an aspect ratio. The ratio machinery this build
   had before took the median of each collection's own images, which is the
   better answer when the columns can be any width — but the live grid is two
   fixed columns and a fixed height, and that makes every card in the grid
   exactly the same shape. It is the archive's answer and it is simpler.
   ----------------------------------------------------------------------- */
.section--entries { padding-block: var(--space-xl); }

/* Full bleed with a little air, per the client: "span full width of the page
   with some light spacing/padding so it isn't completely edge to edge and so
   the cards aren't completely touching". 1rem outside, 0.4rem between —
   measured, both. */
.section--entries > .container--grid {
  max-width: none;
  padding-inline: 1rem;
}

.entry-grid {
  display: grid;
  gap: 0.4rem;
  align-items: start;
  --card-media-h: 20rem;
  --card-header-h: 5.5rem;
  --card-h: calc(var(--card-media-h) + var(--card-header-h));
}
/* 28rem, MEASURED: the live grid's `.layout9_item` is 448px tall at a 1440
   viewport and stays 448px at every desktop width — the card is 701px wide
   and a fixed 448 high, so the whole grid is one shape. An earlier pass grew
   it to 32rem at 80rem and the client's note was that fewer cards fit on a
   page than on the live site. Width and column count are unchanged. */
@media (min-width: 48rem) { .entry-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .entry-grid { --card-media-h: 28rem; } }

/* The three-across strips on a service page are inside the 80rem container
   and are a third of the width, so they take a shorter box. */
.entry-grid--3 { --card-media-h: 18rem; }
@media (min-width: 64rem) {
  .entry-grid--3 { grid-template-columns: repeat(3, 1fr); --card-media-h: 20rem; }
  .entry-grid--4 { grid-template-columns: repeat(4, 1fr); --card-media-h: 16rem; }
}

/* EVERY CARD IN A ROW IS THE SAME HEIGHT, AND THE PICTURE ABSORBS THE
   DIFFERENCE. A row of case studies has titles of one, two and sometimes
   three lines; with the header sized by its content and the media a fixed
   height, each card came out as tall as its own title needed and the pictures
   in a row started and ended at three different heights.

   The client's own suggestion, and it is the right one: fix the CARD and let
   the media take what is left. `--card-h` is the header allowance plus the
   media height, the header keeps its natural height inside that, and the
   media is the remainder — so a two-line title costs its picture 30px rather
   than costing the row its alignment. */
.entry-card {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  background: var(--surface);
  /* Position: the hover panel is absolute against the MEDIA, and the title's
     link overlay is absolute against the CARD. Both need a positioned
     ancestor, and the absence of one is what made the reveal cover the whole
     viewport in white. */
  position: relative;
}

.entry-card__header { flex: 0 0 auto; width: 100%; padding: 1.1rem 1.8rem 1rem; }
@media (max-width: 47.999rem) { .entry-card__header { padding-inline: 1rem; } }

/* Reserve two lines of title, so a one-line card and a two-line card in the
   same row start their pictures at the same y. Three lines still pushes —
   which is a better failure than truncating a case study's name. 5.5rem is
   the live header wrapper's 99px rounded down: the eyebrow, two lines of
   title, and nothing spare, because every pixel here is a pixel of card that
   does not fit on the screen. */
@media (min-width: 48rem) {
  .entry-card__header { min-height: var(--card-header-h); }
  /* The fixed height, and only where the grid has columns. Below 48rem a card
     is the full width, the reveal is in flow rather than on hover, and a fixed
     height would clip the blurb it is now showing. */
  .entry-card { height: var(--card-h); }
}

/* THE EYEBROW, ABOVE THE TITLE. The live site puts this line UNDER the name
   in Deep Blue; the client asked for it above, styled as an eyebrow, and not
   blue — so it reads as a category rather than as a second title. Same
   treatment as the eyebrow on every service tile. */
.entry-card__eyebrow {
  margin-bottom: 0.35rem;
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* Caps are for a LABEL. Case studies, labs and AI services put a category in
   this slot — "Transformation", "Organization activation" — and capitals read
   as a category. The off-the-shelf courses put their tagline there ("Drive
   better results, faster, through teamwork"), and a whole sentence in tracked
   capitals shouts. Same slot, same muted ink, sentence case. `blocks.py`
   picks the variant on length. */
.entry-card__eyebrow--sentence {
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
}

.entry-card__title {
  font-size: var(--text-md);        /* 20px measured */
  font-weight: 600;
  line-height: 1.5;
  text-wrap: balance;
}

/* One anchor, on the title, whose ::after covers the card. That is what makes
   the whole card clickable without wrapping an <iframe> in an <a> — which is
   invalid, and which no amount of CSS makes keyboard-operable. */
.entry-card__link { color: inherit; text-decoration: none; }
.entry-card__link::after { content: ""; position: absolute; inset: 0; z-index: 3; }
.entry-card__link:hover { color: var(--blue-ink); }

.entry-card__media {
  position: relative;
  overflow: hidden;
  width: 100%;
  /* The remainder of the card, not a fixed height. `min-height: 0` because a
     flex item's default `min-height: auto` refuses to shrink below its
     content, which is exactly the shrink this needs. */
  flex: 1 1 auto;
  min-height: 0;
  background: var(--surface-alt);
}
.entry-card__media img,
.entry-card__media picture { width: 100%; height: 100%; object-fit: cover; }

/* THE FILM, AND HOW IT COVERS ITS BOX.
   ------------------------------------------------------------------------
   The live site puts a `<video>` here with `object-fit: cover`, so a film of
   any shape fills the card and is cropped. We cannot: the source MP4s were
   signed Vimeo URLs that have expired, so the film is an <iframe>, and an
   iframe has no object-fit — the player letterboxes the film inside whatever
   frame it is given. A portrait film in a 16:9 frame shows grey down both
   sides, which is exactly what the client reported.

   So the FRAME is sized to the film's own ratio and then grown until it
   covers the box, and the box clips it. `--film-ar` is width ÷ height:

       width  = max(box width,  box height x ar)
       height = max(box height, box width  / ar)

   Whichever term wins, the other follows from it at the same ratio, so the
   frame is always exactly `ar` and always at least as big as the box. This
   needs the box's width and height in one expression, which is what
   `container-type: size` on `.entry-card__media` provides.

   `--film-ar` defaults to 16/9 and is corrected at runtime: `js/site.js`
   asks each player for its real dimensions over postMessage — no extra
   script, just the iframe that is already there — and writes the true ratio
   back. If that never arrives the default stands and a 16:9 film, which is
   most of them, is right anyway.

   The backdrop is what shows until the film arrives — or at all, on a network
   that blocks Vimeo. Black punches a hole in the page and cream is the band
   colour, so the card would look like it ends at its title. `--line` is
   neither, and it is only ever seen for a moment. */
.video-bg {
  --film-ar: 1.7778;
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--line);
  /* A size container. It is `inset: 0` inside a positioned box, so its own
     width and height are always definite, which is what lets the frame below
     be sized against both of them at once — in a card and in a hero alike. */
  container-type: size;
}
.video-bg__frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100cqw, calc(100cqh * var(--film-ar)));
  height: max(100cqh, calc(100cqw / var(--film-ar)));
  transform: translate(-50%, -50%);
  border: 0;
  pointer-events: none;
}

/* ALL OF THE ABOVE EXISTS BECAUSE AN IFRAME CANNOT DO object-fit. A `<video>`
   can, and every film on this site is one now — see blocks.vimeo_background
   for why the Vimeo player had to go. So the cover is one declaration, the
   container query is not consulted, and `--film-ar` does not matter: the
   element knows its own shape.

   The selector is `video.video-bg__frame` so it outranks the rule above on
   specificity rather than on source order alone. */
video.video-bg__frame {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform: none;
  object-fit: cover;
  /* Safari paints a black box for the poster gap on a film it has not
     buffered; the band colour underneath is a better first frame. */
  background: transparent;
}
/* Without a size container to measure against — an older browser, or a box
   whose height is not definite — cqw/cqh resolve to zero and the film would
   vanish. This is the 16:9 cover it used to do, as the floor. */
@supports not (container-type: size) {
  .video-bg__frame {
    width: 100%; height: 100%;
    min-width: 177.7778%; min-height: 100%;
    aspect-ratio: 16 / 9;
  }
}

/* The reveal, INSIDE the media so `inset: 0` has something to resolve
   against. White panel, 1rem, over the picture — the archive's own
   `.cms3_content-wrapper`. */
.entry-card__reveal {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 1.8rem;
  background: var(--surface);
  color: var(--ink);
  overflow: hidden;
  opacity: 0;
  transition: opacity var(--duration) var(--ease);
  /* THE WHOLE CARD IS THE LINK. The panel sits above the link's overlay, so
     while it was interactive a click on the revealed blurb — which is most of
     the card, and the part the pointer is already over — did nothing. It is
     text, not a control; it does not need the pointer. */
  pointer-events: none;
}
.entry-card:hover .entry-card__reveal,
.entry-card:focus-within .entry-card__reveal { opacity: 1; }
.entry-card__blurb { font-size: var(--text-md); line-height: var(--leading-snug); }

/* A touch device has no hover, so the panel would never open and the blurb
   would be unreachable. There it sits under the title instead. */
@media (hover: none) {
  .entry-card__reveal {
    position: static;
    opacity: 1;
    padding: 0 1.8rem 1.1rem;
    background: none;
    overflow: visible;
  }
  .entry-card__blurb { font-size: var(--text-sm); }
}
@media (prefers-reduced-motion: reduce) { .entry-card__reveal { transition: none; } }

.entry-card__mark { height: 1rem; margin-bottom: var(--space-2xs); }
.collection__feed { margin-top: var(--space-lg); font-size: var(--text-sm); }

/* ---------- filters ----------------------------------------------------- */
.section--filters { padding-block: var(--space-md); }
.filters {
  display: grid;
  gap: var(--space-sm);
  border: 2px solid var(--control-line);
  padding: var(--space-sm);
}
@media (min-width: 64rem) {
  .filters { grid-template-columns: 1fr auto; align-items: start; column-gap: var(--space-lg); }
  .filters__count, .filters__empty { grid-column: 1 / -1; }
}
.filters__title,
.filters__search-label {
  display: block;
  margin-bottom: var(--space-2xs);
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.filters__chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  border: 2px solid var(--control-line);
  padding: 0.3rem 0.7rem;
  font-size: var(--text-sm);
  text-decoration: none;
  background: var(--surface);
}
.chip:hover { color: inherit; background: var(--surface-alt); }
.chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
.chip__count { font-size: var(--text-xs); color: var(--ink-muted); }
.chip[aria-pressed="true"] .chip__count { color: rgb(255 255 255 / 0.8); }
.filters__search-input {
  width: 100%;
  min-width: 14rem;
  border: 2px solid var(--control-line);
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: var(--text-sm);
  background: var(--surface);
  color: inherit;
}
.filters__count { font-size: var(--text-sm); color: var(--ink-muted); }
.filters__empty { font-size: var(--text-sm); }

/* ---------- detail hero and body ---------------------------------------- */
.detail-hero { position: relative; overflow: hidden; background: var(--black); color: var(--white); }
.detail-hero__media { position: absolute; inset: 0; z-index: 0; }
.detail-hero__media img,
.detail-hero__media picture { width: 100%; height: 100%; object-fit: cover; }
.detail-hero__media.video-frame { position: relative; }
.detail-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(100deg, rgb(0 0 0 / 0.82) 0%, rgb(0 0 0 / 0.66) 55%, rgb(0 0 0 / 0.55) 100%);
}
.detail-hero:has(.video-frame)::after { display: none; }
/* The COPY is on a 52rem measure; the CONTAINER is not. Capping the container
   itself put the whole grid — card and all — in an 832px box centred in the
   page, so the grey card floated in the middle of the band instead of sitting
   on the container's left edge where the live page has it. */
.detail-hero__inner { position: relative; z-index: 2; padding-block: var(--space-xl); }
.detail-hero__plain { max-width: 52rem; }
.detail-hero__title {
  font-family: "Roboto Slab", Georgia, serif;
  font-size: var(--text-xl);
  font-weight: 500;
  line-height: var(--leading-tight);
  text-wrap: balance;
}
.detail-hero__sub { margin-top: var(--space-xs); font-size: var(--text-md); color: rgb(255 255 255 / 0.88); }
.detail-hero .prose__p { color: rgb(255 255 255 / 0.9); }

.section--detail { padding-block: var(--space-xl); }
.detail { display: grid; gap: var(--space-lg); }
@media (min-width: 64rem) {
  .detail { grid-template-columns: minmax(0, 1fr) 18rem; gap: var(--space-xl); }
  .detail--noaside { grid-template-columns: minmax(0, 46rem); }
}
.detail__main { min-width: 0; }
.detail--noaside .detail__main { max-width: 46rem; }
.detail__part + .detail__part { margin-top: var(--space-lg); }
.detail__part-title {
  margin-bottom: var(--space-xs);
  font-size: var(--text-lg);
  font-weight: 400;
  line-height: var(--leading-snug);
}
.detail__aside { align-self: start; }
@media (min-width: 64rem) {
  .detail__aside {
    position: sticky;
    top: calc(var(--header-h) + var(--space-md));
    border-top: 2px solid var(--accent);
    padding-top: var(--space-sm);
  }
}
.detail__meta { font-size: var(--text-sm); }
.detail__meta-row + .detail__meta-row { margin-top: var(--space-xs); }
.detail__meta dt { font-weight: 700; }
.detail__meta dd { margin: 0; color: var(--ink-muted); }
.detail__aside .button-group { margin-top: var(--space-md); flex-direction: column; align-items: stretch; }

/* ---------- prose ------------------------------------------------------- */
.section--prose { padding-block: var(--space-xl); }
.prose > :first-child { margin-top: 0; }
.prose p { margin-bottom: var(--space-xs); }
.prose p:last-child { margin-bottom: 0; }
.prose ul, .prose ol { margin: 0 0 var(--space-xs); padding-left: 1.15rem; }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li { margin-bottom: 0.25rem; }
.prose__aside {
  border-left: 2px solid var(--advisory-blue);
  padding-left: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.prose__figure { margin-block: var(--space-md); }
.prose__figure img { width: 100%; height: auto; }
.prose__figure figcaption { margin-top: var(--space-2xs); font-size: var(--text-sm); color: var(--ink-muted); }
.prose__figure--wide { margin-block: var(--space-lg); }

/* ---------- gated third-party video ------------------------------------- */
.video-frame { position: relative; overflow: hidden; aspect-ratio: 16 / 9; background: var(--black); }
.video-frame img, .video-frame picture { width: 100%; height: 100%; object-fit: cover; }
.video-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.video-frame__gate {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-sm);
  background: rgb(0 0 0 / 0.62);
  color: var(--white);
  text-align: center;
}
.video-frame__note { font-size: var(--text-sm); max-width: 26rem; }
.video-frame__alt { font-size: var(--text-xs); }
.video-frame__gate .button { border-color: var(--white); background: var(--white); color: var(--black); }
.video-frame__gate .button:hover { background: transparent; color: var(--white); }
.video-frame--body { margin-block: var(--space-md); }

.section--video { padding-block: var(--space-xl); }
.video-feature { display: grid; gap: var(--space-lg); align-items: center; }
@media (min-width: 64rem) {
  .video-feature { grid-template-columns: 1fr 1fr; gap: var(--space-xl); }
  .video-feature--flip .video-feature__frame { order: 2; }
  .video-feature--flip .video-feature__body { order: 1; }
}

@media (min-width: 48rem) { .cards--4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 80rem) { .cards--4 { grid-template-columns: repeat(4, 1fr); } }
.footer__list--feeds { align-self: start; }

/* =========================================================================
   SEAMS

   Every band pays its own 5rem top and bottom, which is right when the band
   above it is a different colour and wrong when it is the same — the two add
   up to 160px of nothing. So the seam between two bands of the SAME tone
   collapses, and only that seam: where a cream band meets a white one the
   padding IS the design.

   Written out per tone because CSS cannot say "the same value as the previous
   sibling's attribute". The attribute selectors put these above the per-block
   `padding-block` rules, which is why they go last in this file.
   ========================================================================= */
.section:not([data-tone]) + .section:not([data-tone]),
.section[data-tone="cream"] + .section[data-tone="cream"],
.section[data-tone="white"] + .section[data-tone="white"],
.section[data-tone="black"] + .section[data-tone="black"],
.section[data-tone="deep-blue"] + .section[data-tone="deep-blue"],
.section[data-tone="light-green"] + .section[data-tone="light-green"],
.section[data-tone="turquoise"] + .section[data-tone="turquoise"],
.section[data-tone="pink"] + .section[data-tone="pink"],
.section[data-tone="lime"] + .section[data-tone="lime"],
.section[data-tone="smartlab-blue"] + .section[data-tone="smartlab-blue"] { padding-top: 0; }

/* A tile pays no padding at all — it is a full-bleed band whose height is its
   own — so it never joins the seam rule. */
.section--tile + .section--tile { padding-top: 0; }

/* ONE EXCEPTION, and it is an archive band split in two rather than two
   bands. `section_ludic_media_studio` holds a tile AND its run of offers
   inside a single section, with a real `padding-medium-2` gap between them.
   Built as two sections of the same tone that gap meets the seam rule above
   and collapses to nothing — which is right for two adjacent bands and wrong
   for the two halves of one. The yellow copy of the band keeps its gap only
   because yellow is not in the seam list; this is what makes the turquoise one
   behave the same way. Same specificity as the seam rule, and after it. */
.section.section--smartlab + .section.section--accordion,
.section.section--smartlab + .section.section--offers { padding-top: var(--space-xl); }

.section--page-head + .section--filters { padding-top: 0; }
.section__head:last-child { margin-bottom: 0; }
.section__head + .accordion,
.section__head + .pillars,
.section__head + .offers,
.section__head + .capabilities,
.section__head + .entry-grid,
.section__head + .logo-wall,
.section__head + .reasons,
.section__head + .cards,
.section__head + .insight-grid { margin-top: 0; }

/* ---------- statement: the second sibling heading ----------------------
   Three archive bands carry two h2s at the same 32px. The second is a
   supporting line, not a smaller one — dropping it to body size is a visibly
   different band. Same size, same weight, real space above it, and only one
   of the two is a heading in the outline.
   ----------------------------------------------------------------------- */
.statement--sub {
  margin: var(--space-sm) auto 0;
  max-width: 46ch;
  font-family: "Archivo", system-ui, sans-serif;
  font-size: var(--text-xl);
  font-weight: 400;
  line-height: var(--leading-tight);
  text-wrap: balance;
}

/* The 16px line the archive puts ABOVE the home hero's h1, inside the card. */
.hero__lede {
  margin-bottom: var(--space-xs);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--ink-muted);
}

/* MEASURED column counts: the client wall is six across (638px tall) and the
   academic wall five (532px). `auto-fill` gave seven and left two orphans on
   the last row of thirty. */
@media (min-width: 48rem) {
  .logo-wall--6 { grid-template-columns: repeat(3, 1fr); }
  .logo-wall--5 { grid-template-columns: repeat(3, 1fr); }
  /* Four certification marks on Ludic Medical. They are a row rather than a
     wall, so they are centred and given room, not spread to the container's
     full width where four marks would sit in an acre of white. */
  .logo-wall--4 {
    grid-template-columns: repeat(4, minmax(0, 10rem));
    justify-content: center;
    gap: var(--space-md) var(--space-lg);
  }
}
@media (min-width: 64rem) {
  .logo-wall--6 { grid-template-columns: repeat(6, 1fr); }
  .logo-wall--5 { grid-template-columns: repeat(5, 1fr); }
}

/* ---------- film statement — About's `our-approach` -------------------- */
.hero--film-statement {
  position: relative;
  overflow: hidden;
  background: var(--surface-alt);
}
/* REMEASURED off the live About capture, which is the band the client asked
   for and is not what the static export shows:

     .padding-global.padding-small   padding 16px 16px 0
     .background-image (the film)    x=16, w=1408, h=600
     .header-container (the card)    x=120, 631x376, padding 48px 40px,
                                     background #f9fbf7, vertically centred
     h4.heading-style-h3             32px/400
     .button                         black, white label

   The film is inset 16px like every other panel on that page, so the page
   colour frames it; `inset: 0 1rem` is that inset, applied to the film rather
   than to the section, because the section is what paints the frame. */
.hero--film-statement .hero__still,
.hero--film-statement .hero__video,
.hero--film-statement .hero__video-poster {
  position: absolute;
  inset: 0 1rem;
  z-index: 0;
  width: calc(100% - 2rem);
  height: 100%;
  object-fit: cover;
}
.hero--film-statement .hero__video { z-index: 1; }
.hero--film-statement .hero__inner {
  position: relative;
  z-index: 2;
  display: grid;
  align-items: center;
  min-height: 37.5rem;            /* 600px measured */
  padding-block: var(--space-lg);
}
@media (min-width: 64rem) {
  .hero--film-statement .hero__inner { grid-template-columns: 1.25fr 1fr; }
}
.hero--film-statement .hero__content {
  background: var(--header-cream);
  padding: 3rem 2.5rem;             /* 48px 40px measured */
  max-width: 39.5rem;               /* 631px measured */
  color: var(--black);
  max-width: 34rem;
  text-align: left;
}
/* THE PHOTOGRAPH VARIANT. About's band is the measured original — 600px tall
   with an opaque card — and it stays exactly that. These two are different
   work: a photograph carrying one line, where 600px took more of the page than
   the picture was worth and an opaque card covered the part of the frame the
   reader wants. Shorter, and the card at 75%.

   THE CARD STILL CARRIES BLACK TEXT AT 11.01:1 over a pure black image, which
   is the worst thing that can be behind it — asserted in checks.PAIRS rather
   than assumed, because a translucent surface is exactly where a declared
   colour and a rendered pixel stop agreeing. */
.hero--film-statement--compact .hero__inner { min-height: 28rem; }
.hero--film-statement--compact .hero__content { background: rgb(249 251 247 / 0.75); }

.hero--film-statement .statement { margin-inline: 0; text-align: left; }
.hero--film-statement .button-group { margin-top: var(--space-md); }

/* ---------- two-column prose — About's `layout239_list` ---------------- */
.section--prose-columns { padding-block: var(--space-xl); }
.prose-columns { display: grid; gap: var(--space-lg); }
@media (min-width: 48rem) { .prose-columns { grid-template-columns: 1fr 1fr; } }
.prose-columns__col .prose__p:last-child { margin-bottom: 0; }

/* MEASURED: the About montage banner is 1408x450 — 3.13:1, not the 21/9 the
   block assumed. */
.video-banner { aspect-ratio: 3.13 / 1; }
@media (max-width: 47.999rem) { .video-banner { aspect-ratio: 16 / 9; } }

/* TWO VARIANTS. See `blocks.figure_accordion` for why.

   `--art` is the live Comms & Engagement band: the right column is one
   picture that swaps, and each row's prose stays under its own label on the
   left. Only the row's INLINE copy of the picture is hidden, because the
   stage is already showing it.

   `--prose` is Learning's "How we deliver", which has no artwork: there the
   open row's body is cloned into the right column, because otherwise half the
   band is empty.

   Getting this wrong is what shipped: both variants moved the prose across
   AND re-showed the row's inline picture inside the panel, so the right
   column ran picture, text, the same picture again — and the left column,
   which is where the reader is looking, was empty under every label. */
.figure-accordion__panel:empty { display: none; }
.figure-accordion__panel .accordion__body { padding-bottom: 0; }

[data-js] .figure-accordion--art .accordion__media { display: none; }

[data-js] .figure-accordion--prose .accordion__body { display: none; }
[data-js] .figure-accordion--prose .figure-accordion__panel .accordion__body { display: block; }

/* ---------- case-study detail — MEASURED OFF THE LIVE PAGE --------------
   `_webarchives/Launching a new operating model - Case Study - Ludic`:

     section_layout1                605px
     .layout1_component             grid 711.109px / 568.891px inside 1280
     .header-container.color-grey   631x381, background var(--dark-grey),
                                    padding 3rem 2.5rem
     .line-divider-small            30% wide, 6px tall, white
     h2.heading-style-h5            20px/700   "Case study", and the name
     h1.heading-style-h3            32px/400   the question
     .layout10_component            1fr 1fr 1fr, gap 2rem, place-items: start
     .text-size-large.text-weight-bold  24px/700/42px  the part titles
     p, li                          20px/400/30px
   ----------------------------------------------------------------------- */
.detail-hero--card { background: var(--black); }
.detail-hero--card .detail-hero__inner {
  display: grid;
  align-items: center;
  min-height: 37.8rem;              /* 605px measured */
  padding-block: var(--space-lg);
}
@media (min-width: 64rem) {
  .detail-hero--card .detail-hero__inner { grid-template-columns: 1.25fr 1fr; }
}
/* No scrim: the card is solid, so the film behind it needs no darkening —
   and darkening it would flatten the one thing the band is showing. */
.detail-hero--card::after { display: none; }

.detail-hero__card {
  background: var(--dark-grey);
  padding: 3rem 2.5rem;
  color: var(--white);
  max-width: 39.5rem;               /* 631px measured */
}
@media (max-width: 47.999rem) { .detail-hero__card { padding: var(--space-md) var(--space-sm); } }

.detail-hero__rule {
  display: block;
  width: 30%;
  height: 6px;
  margin-bottom: var(--space-sm);
  background: var(--white);
}
.detail-hero__label,
.detail-hero--card .detail-hero__title {
  font-size: var(--text-md);        /* 20px */
  font-weight: 700;
  line-height: 1.5;
  font-family: "Archivo", system-ui, sans-serif;
}
.detail-hero__label { margin-bottom: 0; }
.detail-hero__question {
  margin-top: var(--space-sm);
  font-size: var(--text-xl);        /* 32px */
  font-weight: 400;
  line-height: var(--leading-tight);
  text-wrap: balance;
}

.detail-columns {
  display: grid;
  gap: var(--space-md);
  align-items: start;
}
@media (min-width: 64rem) {
  .detail-columns--3 { grid-template-columns: repeat(3, 1fr); }
  .detail-columns--2 { grid-template-columns: repeat(2, 1fr); }
}
.detail-columns .detail__part + .detail__part { margin-top: 0; }
.detail-columns .detail__part-title {
  margin-bottom: var(--space-sm);
  font-size: var(--text-lg);        /* 24px */
  font-weight: 700;
  line-height: 1.75;
}
.detail-columns .prose p,
.detail-columns .prose li { font-size: var(--text-md); line-height: 1.5; }

/* ---------- the facts panel — blocks._facts ----------------------------
   The scannable run of facts at the foot of a case study body: scope,
   scale, what was delivered, what it measured.

   IT SITS ON THE BODY'S WHITE, NOT ON THE PAGE'S CREAM. It was its own
   `section--numbers` band on a cream tone, which put the figures on the
   page background and made them read as a footer bolted under the case
   study rather than the closing part of it. It is now drawn inside
   `.section--detail`, so there is no band, no tone and no seam — just a
   hairline and a change of scale after the third column.

   FIGURES INLINE, NOT STACKED. `.card__stats` puts the value above its
   label because a card stat is read alone and the number is the whole
   message. These are read as a list, and stacking them turned a run of
   facts into a wall of statistics three times the height. The number is
   bold; the sentence after it carries the meaning. Set at --text-sm,
   which is two steps down from the 20px prose beside it — small enough
   to read as reference material rather than more argument.

   Two columns from 48rem, not three: several labels run to eight or nine
   words and a third column wraps most of them onto three lines, which is
   the opposite of scannable. */
.detail-facts {
  margin-top: var(--space-lg);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--line);
}
.detail-facts__title {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.detail-facts__list {
  margin: 0;
  padding-left: 1.15rem;
  list-style: disc;
  columns: 1;
}
@media (min-width: 48rem) {
  .detail-facts__list { columns: 2; column-gap: var(--space-lg); }
}
.detail-facts__item {
  margin-bottom: 0.4rem;
  font-size: var(--text-sm);
  line-height: 1.5;
  /* Keep a bullet and its wrapped lines in one column. Without this a
     two-line item splits across the gap and the second half reads as a
     fragment under the wrong heading. */
  break-inside: avoid;
}
.detail-facts__figure { font-weight: 700; }
/* The lines that are not statistics — an award, a participant quote, the
   one-line summary of an approach. They keep the bullet and the measure
   so the list stays one list, and lose only the bold. */
.detail-facts__item--plain { color: var(--ink-muted); }
