/**
 * Havas Life Paris — main stylesheet.
 *
 * Values are taken from the "HAVAS LIFE WEBSITE - JUILLET" Figma file.
 * Reference desktop frame: 1440 x 960.
 */

/* -------------------------------------------------------------------------
 * Fonts
 *
 * Baikal Normal, converted to woff2 from the licensed OTF release.
 * Figma calls these "Baikal Normal: Light / Regular".
 * ---------------------------------------------------------------------- */

@font-face {
	font-family: "Baikal";
	src: url("../fonts/BaikalNormal-Light.woff2") format("woff2");
	font-weight: 300;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Baikal";
	src: url("../fonts/BaikalNormal-Regular.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Baikal";
	src: url("../fonts/BaikalNormal-Medium.woff2") format("woff2");
	font-weight: 500;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Baikal";
	src: url("../fonts/BaikalNormal-Bold.woff2") format("woff2");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}

/* Condensed cut — used for the navigation overlay and section headings. */

@font-face {
	font-family: "Baikal Cond";
	src: url("../fonts/BaikalCond-Regular.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Baikal Cond";
	src: url("../fonts/BaikalCond-Bold.woff2") format("woff2");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}

/* -------------------------------------------------------------------------
 * Design tokens
 * ---------------------------------------------------------------------- */

:root {
	/* Colours — from Figma variables + published styles. */
	--hlp-black: #000;
	--hlp-white: #fff;
	--hlp-red: #e30b13;
	--hlp-placeholder: #d7d7d7;

	/* Typography. */
	--hlp-font: "Baikal", "Helvetica Neue", helvetica, arial, sans-serif;
	--hlp-font-cond: "Baikal Cond", "Baikal", "Helvetica Neue", helvetica, arial, sans-serif;

	/* Layout — 1440 design width, 80px margins, 5 columns of 256px. */
	--hlp-design-width: 1440;
	--hlp-gutter: 80px;
	--hlp-content-max: 1280px;
	--hlp-columns: 5;

	/*
	 * The header's own height, which every section below reserves so that no
	 * copy is ever read through the menu.
	 *
	 * The frames do not supply this figure: each section is drawn as its own
	 * 1440x960 board, so their vertical offsets are measured from the top of
	 * the board and none of them account for a header fixed over all nine.
	 * Left alone, the section 02 collage starts 110px down and the section 03
	 * headline 117px, both of which land under the logo.
	 *
	 * It is the contracted height, not the 160px the header opens at: the logo
	 * shrinks 469px to 180px across the intro, and by the time any other
	 * section is on screen the 46px row of language links and burger is the
	 * tallest thing in the bar — 30 + 46 + 30. The intro is the one section
	 * that keeps the tall header, and it is also the one section meant to be
	 * read through, so it reserves nothing.
	 */
	--hlp-header-h: 106px;

	/*
	 * How much of what is drawn between things the window can afford, from 1
	 * at the 960 the boards are drawn at down to a half.
	 *
	 * Section 09 and the footer are one composition — Figma draws the footer
	 * inside the section's board, portraits ending at 578 and the footer
	 * running 673 to 930 of the same 960 — so they contract together or the
	 * fit is decided twice. 600 is what cannot give at all between them: 420
	 * of portrait and the footer's own 180. 360 is the air over which the rest
	 * is spread — 253 in the section, 107 in the footer.
	 */
	--hlp-fit: clamp(0.5px, calc((100svh - 600px) / 360), 1px);

	/*
	 * The footer's own height, which section 09 reserves so that the two are
	 * on screen together. Nothing but this token can say so — the footer is
	 * rendered outside #main, after the section, and CSS cannot measure it
	 * from there.
	 *
	 * 180 that cannot give — the top band's 104, the 26 of icons, the legal
	 * block's 48 (two lines of 20 and the 8 between them, since the frame
	 * added « Nos engagement sociaux » on a line of its own), and one pixel
	 * each for the two rules, which are borders and do not scale — and 107 of
	 * air laid out between them, which can. At a 960 window that is still the
	 * 287 the board draws: the second line was paid for out of the air above
	 * and below the legal row, not added to the footer's height.
	 *
	 * The two border pixels are the whole reason tools/check-footer-height.py
	 * exists. They were first filed with the air, which is exact at a 960
	 * window and wrong at every other one — 150 + 137 and 152 + 135 both make
	 * 287 at fit=1, and the error only opens up as the window shortens. A
	 * total checked at one window is not a checked total.
	 */
	--hlp-footer-h: calc(180px + 107 * var(--hlp-fit));

	/* Decorative rules. */
	--hlp-rule-opacity: 0.15;
}

/* Mobile reference frame is 430x932; the header sits in 376px, so 27px sides. */
@media (max-width: 768px) {
	:root {
		--hlp-gutter: 27px;

		/*
		 * Nothing on a phone is fitted to the window — the board is 2232 tall
		 * and read by scrolling — so the air is drawn at full size and the
		 * blocks below carry the phone frame's own figures.
		 */
		--hlp-fit: 1px;

		/*
		 * 27 + 27 around the logo, which is what sets the height here: the
		 * actions row is display:contents, so its 46px no longer applies.
		 *
		 * This is the tallest the bar gets. The logo asks for 218px but is
		 * capped at 52% of the content width, so it only reaches its full
		 * 218 — and the SVG's 469:76 only reaches 35px — from about 473px of
		 * viewport up. Narrower than that the bar is shorter and this reserves
		 * a few pixels more than it needs, which is the harmless direction.
		 */
		--hlp-header-h: 89px;
	}
}

/* -------------------------------------------------------------------------
 * Base
 * ---------------------------------------------------------------------- */

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

html {
	scroll-behavior: smooth;
	/*
	 * Always reserve the scrollbar track. Without this, locking body scroll
	 * for the nav overlay reclaims the scrollbar's width and shunts the
	 * header — and so the logo — sideways as the menu opens.
	 */
	scrollbar-gutter: stable;
}

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}
}

/*
 * Logged in, WordPress core's admin-bar.css gives <html> a scroll-padding-top
 * of the admin bar's height — 46px on a narrow screen. Below 600px, though,
 * core also takes the bar out of its fixed position and lets it scroll away
 * with the page, so the padding goes on reserving 46px for a bar that is no
 * longer there, and every jump from the menu lands that much short: section
 * 02's title at 163 instead of 117, every later section 46px down with the one
 * before it showing above. Visitors never see it; whoever checks the site
 * while logged in always does.
 *
 * :root outranks core's bare html selector whichever stylesheet loads last.
 * Between 600 and 782 the bar stays fixed, so core's padding is right there
 * and is left alone.
 */
@media screen and (max-width: 600px) {
	:root {
		scroll-padding-top: 0;
	}
}

body {
	margin: 0;
	background: var(--hlp-white);
	color: var(--hlp-black);
	font-family: var(--hlp-font);
	font-weight: 400;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

img,
svg,
video {
	display: block;
	max-width: 100%;
}

a {
	color: inherit;
}

button {
	font: inherit;
	color: inherit;
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
}

/* -------------------------------------------------------------------------
 * Utilities
 * ---------------------------------------------------------------------- */

.container {
	width: 100%;
	max-width: calc(var(--hlp-content-max) + (var(--hlp-gutter) * 2));
	margin-inline: auto;
	padding-inline: var(--hlp-gutter);
}

.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.skip-link:focus {
	position: fixed;
	top: 0;
	left: 0;
	z-index: 999;
	width: auto;
	height: auto;
	padding: 1rem;
	clip: auto;
	background: var(--hlp-white);
}

/* -------------------------------------------------------------------------
 * Decorative column rules
 *
 * Six 1px verticals at every column boundary of the 5-column grid,
 * including both outer edges. Fixed so they run the length of the page.
 * ---------------------------------------------------------------------- */

.grid-rules {
	position: fixed;
	inset: 0;
	z-index: 0;
	pointer-events: none;
}

/*
 * This box has to be the content box of .container, exactly, at every width —
 * the lines are placed on it as percentages, and the whole point of them is
 * that the grid they mark is the one the sections are laid out on.
 *
 * A plain max-width of the content measure is only that box once the window is
 * wide enough for .container to have stopped growing. Below 1440 the container
 * keeps its 80px gutters and gives up width from the middle, while a bare
 * max-width box gives up nothing and stays edge to edge — so the two drifted
 * apart by up to a full gutter, 80px at 1280, and the rules stopped landing on
 * anything. The expertise cards and the section 06 logos are both drawn to sit
 * on these lines and neither did.
 *
 * Subtracting the gutters here rather than padding them tracks the container
 * through both of its states without a second breakpoint: below 1440 the
 * window less two gutters is the container's content box, and above it the
 * max-width caps at the same measure the container caps at.
 */
.grid-rules__inner {
	position: relative;
	height: 100%;
	width: calc(100% - (var(--hlp-gutter) * 2));
	max-width: var(--hlp-content-max);
	margin-inline: auto;
}

/*
 * Mobile rules run edge to edge rather than inside the content column — a
 * different rhythm at sixths, not a subset of the desktop six.
 *
 * width goes back to auto so that inset: 0 is what sizes this: an inherited
 * width would win over the right offset and pull these in by a gutter too.
 */
.grid-rules__inner--mobile {
	position: absolute;
	inset: 0;
	width: auto;
	max-width: none;
	display: none;
}

.grid-rules__line {
	position: absolute;
	top: 0;
	bottom: 0;
	width: 1px;
	background: var(--hlp-black);
	opacity: var(--hlp-rule-opacity);
}

/*
 * In the design the rules sit behind page content but in front of the nav
 * overlay, so they stay visible once the menu is open.
 */
body.nav-is-open .grid-rules {
	z-index: 26;
}

@media (max-width: 768px) {
	.grid-rules__inner--desktop {
		display: none;
	}

	.grid-rules__inner--mobile {
		display: block;
	}
}

/* -------------------------------------------------------------------------
 * Section progress nav (vertical ticks, left edge)
 * ---------------------------------------------------------------------- */

.section-nav {
	position: fixed;
	left: 33px;
	top: 50%;
	transform: translateY(-50%);
	z-index: 20;
	display: flex;
	flex-direction: column;
	gap: 15px;
	align-items: flex-start;
}

/*
 * The tick below is the only underline this item has. Without this reset the
 * anchor also draws the browser's own underline across "09", which lands a few
 * pixels above the tick and a few pixels shorter than it — reading as two
 * broken segments rather than one rule.
 */
.section-nav__item {
	display: block;
	position: relative;
	padding: 0;
	text-decoration: none;
}

.section-nav__number {
	display: block;
	font-size: 14px;
	line-height: 1.1;
	margin-bottom: 1px;
	opacity: 0;
	transition: opacity 0.3s ease;
}

.section-nav__tick {
	display: block;
	width: 10px;
	height: 2px;
	background: var(--hlp-black);
	opacity: 0.5;
	transition: width 0.3s ease, opacity 0.3s ease;
}

.section-nav__item[aria-current="true"] .section-nav__number {
	opacity: 1;
}

.section-nav__item[aria-current="true"] .section-nav__tick {
	width: 21px;
	opacity: 1;
}

.section-nav__item:hover .section-nav__tick,
.section-nav__item:focus-visible .section-nav__tick {
	opacity: 1;
}

@media (max-width: 768px) {
	.section-nav {
		display: none;
	}
}

/* -------------------------------------------------------------------------
 * Site header
 * ---------------------------------------------------------------------- */

.site-header {
	position: fixed;
	inset: 0 0 auto;
	z-index: 30;
	padding-block: 30px;
}

/* -------------------------------------------------------------------------
 * Content clipped at the header
 *
 * The header is transparent by design: section 03's photograph runs behind it
 * and the column rules read through it, so an opaque bar is not available to
 * hide anything. Copy is cut off at the header's lower edge instead — the
 * layer that carries the reading matter is clipped, while the layer that
 * carries the picture is not. Backgrounds keep passing behind the menu;
 * headlines, paragraphs and cards stop at the line.
 *
 * Which wrapper is reading matter is the section's business, not the header's,
 * so this looks for data-clip-at-header rather than carrying a list of class
 * names — the same turn as data-section-invert. A section built later joins by
 * marking its own wrapper; nothing here has to be edited to let it in, which is
 * what section 05 needed and did not get while this was two hard-coded lists.
 *
 * What a section leaves unmarked is as deliberate as what it marks.
 * .expertise__background is the photograph, and .campaigns__overlay and
 * .commitments__overlay are the film pop-ins, meant to cover the page. All
 * three are siblings of a marked wrapper rather than children, so the clip
 * never reaches them.
 *
 * A clip has to track the viewport while the element it sits on scrolls, which
 * CSS alone cannot express, so main.js publishes --clip-top per element. The
 * other three insets are pulled well outside the box so that only the top edge
 * ever cuts: the campaigns and commitments decks push cards beyond their own
 * bounds and they must not be trimmed at the sides.
 *
 * The intro marks nothing. Its video is supposed to grow over the header,
 * which is the one place content crossing the bar is the design.
 * ---------------------------------------------------------------------- */

[data-clip-at-header] {
	clip-path: inset(var(--clip-top, 0px) -100vw -100vh -100vw);
}

.site-header__inner {
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: 24px;
}

/*
 * The logo contracts continuously across the intro rather than snapping at a
 * threshold: --hero-p runs 0 to 1 over the hero's scroll and is published on
 * :root by main.js, so the header — which lives outside the hero — can read
 * it. No transition here on purpose; the value is already scroll-linked, and
 * easing it as well would leave the logo lagging behind the page.
 *
 * Falls back to the full size when --hero-p is unset, so no JavaScript means
 * no shrinking rather than no logo.
 */
/*
 * Off the front page there is no intro, so nothing publishes --hero-p and the
 * logo would open at its full 469 — over a legal page whose padding reserves
 * --hlp-header-h, which is the contracted height. The header therefore starts
 * contracted everywhere but home: the section-09 size, from the first paint.
 * On the front page this selector never matches and the scroll-linked value
 * stands.
 */
body:not(.home) {
	--hero-p: 1;
}

.site-header__logo {
	--logo-from: 469px;
	--logo-to: 180px;

	display: block;
	width: calc(
		var(--logo-from) + ((var(--logo-to) - var(--logo-from)) * var(--hero-p, 0))
	);
	max-width: 42vw;
	margin-top: calc(24px * (1 - var(--hero-p, 0)));
}

.site-header__logo img {
	width: 100%;
	height: auto;
}

.site-header__actions {
	display: flex;
	align-items: center;
	gap: 22px;
	height: 46px;
}

.lang-switch {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 18px;
	line-height: 1;
}

.lang-switch__link {
	text-decoration: none;
}

.lang-switch__link[aria-current="true"] {
	color: var(--hlp-red);
	text-decoration: underline;
	text-underline-position: from-font;
}

.nav-toggle {
	width: 25px;
	height: 20px;
	display: grid;
	place-items: center;
}

.nav-toggle__icon {
	grid-area: 1 / 1;
	width: 100%;
	height: auto;
}

.nav-toggle__icon--close {
	width: 20px;
	display: none;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__icon--burger {
	display: none;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__icon--close {
	display: block;
}

/* ----- Header over a dark section -----
 *
 * Frame 1:211 draws the logo, the burger and the inactive language in white
 * where the header crosses section 03's photograph; the current language stays
 * red on both grounds. main.js adds .is-over-dark while a section carrying
 * data-section-invert sits under the logo, and takes it off again on the way
 * out, so the change reads as a crossfade in both directions rather than a
 * switch thrown at a threshold.
 *
 * The logo and the burger are single-colour SVGs served as <img>, which CSS
 * cannot recolour. Inverting them is exact rather than approximate: the only
 * painted fill in either file is black — the logo's one white rectangle is a
 * clip path in <defs> and is never drawn — so invert(1) lands on pure white,
 * and it animates.
 *
 * How far up the widths a section is dark is the section's own business too,
 * because the mobile frames and the desktop ones do not agree. Both mobile
 * frames that put the header over a photograph — 1:2350 for section 03 and
 * 1:2604 for section 07 — draw it black, and both draw it on ground that
 * measures around 22 of 255, where a black wordmark all but disappears. That
 * is a slip in the drawings rather than a decision, and both sections are
 * built white at every width for it — section 07 first, section 03 once the
 * client called it for that one too.
 *
 * So a section says data-section-invert="all" when it means at every width,
 * and plain data-section-invert when it means the frames' desktop only;
 * main.js reflects the first as .is-over-dark-all and the phone stands the
 * inversion down without it.
 *
 * And while the nav overlay is open the header sits over a white panel, so the
 * inversion has to stand down whatever is scrolled behind it.
 */
.site-header__logo img,
.nav-toggle__icon,
.lang-switch__link {
	transition: filter 0.4s ease, color 0.4s ease;
}

/*
 * The rest state written as a filter, not left as the initial `none`: the
 * transition interpolates between two filter lists, and Safari does not
 * treat `none` as one — it throws the switch instead of fading it. invert(0)
 * paints identically and gives the crossfade a real starting point in every
 * engine.
 */
.site-header__logo img,
.nav-toggle__icon {
	filter: invert(0);
}

body:not(.nav-is-open) .site-header.is-over-dark .site-header__logo img,
body:not(.nav-is-open) .site-header.is-over-dark .nav-toggle__icon {
	filter: invert(1);
}

body:not(.nav-is-open) .site-header.is-over-dark .lang-switch__link {
	color: var(--hlp-white);
}

body:not(.nav-is-open)
	.site-header.is-over-dark
	.lang-switch__link[aria-current="true"] {
	color: var(--hlp-red);
}

/*
 * The phone, unless the section under the logo asked for every width. Written
 * as a stand-down rather than as a second copy of the three rules above, so
 * there is one statement of what inverting means and one of where it does not.
 */
@media (max-width: 768px) {
	body:not(.nav-is-open)
		.site-header.is-over-dark:not(.is-over-dark-all)
		.site-header__logo img,
	body:not(.nav-is-open)
		.site-header.is-over-dark:not(.is-over-dark-all)
		.nav-toggle__icon {
		/* invert(0) rather than none, for the reason the rest state is. */
		filter: invert(0);
	}

	body:not(.nav-is-open)
		.site-header.is-over-dark:not(.is-over-dark-all)
		.lang-switch__link {
		color: var(--hlp-black);
	}

	body:not(.nav-is-open)
		.site-header.is-over-dark:not(.is-over-dark-all)
		.lang-switch__link[aria-current="true"] {
		color: var(--hlp-red);
	}
}

@media (prefers-reduced-motion: reduce) {
	.site-header__logo img,
	.nav-toggle__icon,
	.lang-switch__link {
		transition-duration: 0.01ms;
	}
}

/* -------------------------------------------------------------------------
 * Navigation overlay
 * ---------------------------------------------------------------------- */

.nav-overlay {
	position: fixed;
	inset: 0;
	z-index: 25;
	background: var(--hlp-white);
	display: flex;
	align-items: center;
	overflow-y: auto;
	opacity: 0;
	visibility: hidden;
	transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}

.nav-overlay.is-open {
	opacity: 1;
	visibility: visible;
	transition: opacity 0.45s ease, visibility 0s linear 0s;
}

.nav-overlay__nav {
	width: 100%;
	max-width: calc(var(--hlp-content-max) + (var(--hlp-gutter) * 2));
	margin-inline: auto;
	padding-inline: var(--hlp-gutter);
}

.nav-overlay__list {
	/* Design puts the list 167px from the viewport edge at 1440. */
	margin: 0 0 0 87px;
	padding: 0;
	list-style: none;
}

/* Items rise into place in sequence once the overlay opens. */
.nav-overlay__item {
	opacity: 0;
	transform: translateY(14px);
	transition:
		opacity 0.5s ease,
		transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
	transition-delay: 0s;
}

.nav-overlay.is-open .nav-overlay__item {
	opacity: 1;
	transform: none;
	transition-delay: calc((var(--i, 0) * 45ms) + 120ms);
}

.nav-overlay__link {
	position: relative;
	z-index: 0;
	display: inline-block;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 30px;
	line-height: 2.1;
	text-transform: uppercase;
	text-decoration: none;
	/* Keeps the text on 167px while the red field bleeds past it. */
	margin-left: -9px;
	padding-inline: 9px 23px;
	transition: color 0.35s ease;
}

/* The red field wipes across from the left rather than snapping on. */
.nav-overlay__link::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: var(--hlp-red);
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}

.nav-overlay__link:hover,
.nav-overlay__link:focus-visible {
	color: var(--hlp-white);
}

.nav-overlay__link:hover::before,
.nav-overlay__link:focus-visible::before {
	transform: scaleX(1);
}

body.nav-is-open {
	overflow: hidden;
}

@media (max-width: 768px) {
	.nav-overlay__list {
		margin-left: 0;
	}

	.nav-overlay__link {
		font-size: 24px;
		line-height: 1.9;
	}
}

/*
 * Mobile header: logo, language and burger spread evenly across the row.
 * display:contents dissolves the actions wrapper so all three become direct
 * flex items and space-between can distribute them, as in the mobile frame.
 */
@media (max-width: 768px) {
	.site-header {
		padding-block: 27px;
	}

	.site-header__inner {
		align-items: center;
	}

	/* Mobile has no intro growth, so the logo keeps one size throughout. */
	.site-header__logo {
		width: 218px;
		max-width: 52%;
		margin-top: 0;
	}

	.site-header__actions {
		display: contents;
	}

	/* Mobile sets the language switch in the bold cut. */
	.lang-switch {
		font-weight: 700;
	}
}

/* -------------------------------------------------------------------------
 * Hero / intro video
 * ---------------------------------------------------------------------- */

/*
 * Two viewports tall. Only the video is pinned, so the arc, the arrow and
 * everything below scroll normally while the video opens out.
 *
 * --p runs 0 to 1 across that scroll, and is set by main.js.
 */
.hero {
	position: relative;
	height: 200svh;
	--p: 0;
	/*
	 * The arc is 1732px wide and centred, so on any window narrower than that
	 * it hangs off both sides of the page — 159px past the right edge at 1280.
	 * Unclipped that is a horizontal scrollbar on every page of the site, and
	 * the damage is not the bar itself:
	 *
	 *  - the bar takes 15px off the foot of the window, and section 06's grid
	 *    closes 7px off the foot of its section, so the last row of logos is
	 *    read through it;
	 *  - and the page can then be panned sideways, which pulls every section
	 *    out of line with the column rules, because those are position: fixed
	 *    and do not pan with it.
	 *
	 * clip, not hidden. hidden would make the hero a scroll container, and
	 * .hero__pin is position: sticky — it would start sticking to the hero
	 * instead of to the window and the video would stop being pinned. clip
	 * establishes no scroll container, and unlike hidden it may be set on one
	 * axis while the other stays visible, which is what keeps the growing
	 * video free to cross the header. Browsers too old to know clip drop the
	 * declaration and are no worse off than they are today.
	 */
	overflow-x: clip;
}

.hero__shape {
	position: absolute;
	top: 50svh;
	left: calc(50% - 74px);
	translate: -50% -50%;
	width: 1732px;
	height: 1060px;
	opacity: calc(var(--hlp-rule-opacity) * (1 - var(--p)));
	pointer-events: none;
	z-index: 0;
}

.hero__shape img {
	width: 100%;
	height: 100%;
}

.hero__pin {
	position: sticky;
	top: 0;
	z-index: 1;
	height: 100svh;
	display: grid;
	place-items: center;
}

/*
 * The box itself grows, so the video is sized to it rather than cropped out
 * of a larger one. At rest the box is 16:9 and so is the footage, which is
 * why the whole frame is visible; the crop only begins as the box widens
 * past that ratio on its way to filling the screen.
 *
 * Percentages resolve against .hero__pin, which is exactly one viewport —
 * using 100vw here would overshoot by the width of the scrollbar gutter.
 */
.hero__media {
	--base-w: min(770px, calc(100% - (var(--hlp-gutter) * 2)));
	--base-h: calc(var(--base-w) * 433 / 770);

	width: calc(var(--base-w) + ((100% - var(--base-w)) * var(--p)));
	height: calc(var(--base-h) + ((100% - var(--base-h)) * var(--p)));
	background: var(--hlp-placeholder);
	overflow: hidden;
	/*
	 * The transport controls are absolute, and this is what they hang off.
	 * Without it they escape to .hero__pin — which is sticky, so it counts as
	 * a containing block — and land at the foot of the full-height stage:
	 * 1425px wide and 164px clear of the film at 1440. They belong on the
	 * picture, and grow and shrink with it.
	 */
	position: relative;
}

/*
 * Positioned so it paints over the cover, which comes first in the markup:
 * transparent until the film has a frame, then opaque over it.
 */
.hero__video {
	position: relative;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.hero__cover {
	position: absolute;
	inset: 0;
}

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

/* A portrait cover with no landscape one: phones only. */
@media (min-width: 769px) {
	.hero__cover--phone-only {
		display: none;
	}
}

/* -------------------------------------------------------------------------
 * Film transport controls
 *
 * No frame draws these; the intro is a bare rectangle in Figma. They are
 * built in the site's own language — Baikal, the brand red on the volume
 * track — rather than left as the browser's default chrome, which would be
 * the only piece of another vendor's design on the page.
 *
 * Laid over the foot of whatever carries data-video-player, so on the intro
 * they ride the film as it opens out to fill the window. The scrim is what
 * keeps them legible: the footage runs from near-black to a bright red, and
 * white alone disappears against the second.
 * ---------------------------------------------------------------------- */

.video-controls {
	position: absolute;
	inset: auto 0 0;
	z-index: 2;
	display: flex;
	flex-direction: column;
	gap: 2px;
	padding: 28px 16px 10px;
	color: var(--hlp-white);
	background: linear-gradient(to top, rgb(0 0 0 / 55%), transparent);
	/*
	 * Held back until the film is pointed at or tabbed into, so the intro is
	 * first seen as the frame draws it. focus-within is what keeps that from
	 * shutting the bar out of the keyboard.
	 */
	opacity: 0;
	transition: opacity 0.25s ease;
}

/* Keyed off the player rather than the hero, so the partial stays portable. */
[data-video-player]:hover .video-controls,
.video-controls:focus-within,
.video-controls.is-held {
	opacity: 1;
}

.video-controls__row {
	display: flex;
	align-items: center;
	gap: 4px;
}

/*
 * Pushes the sound controls to the far end, so the row reads transport on the
 * left, sound on the right, whatever the film's width.
 */
.video-controls__time {
	margin: 0 auto 0 8px;
	font-size: 13px;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
	opacity: 0.85;
}

.video-controls__button {
	display: grid;
	place-items: center;
	width: 34px;
	height: 34px;
	flex: none;
	color: inherit;
	border-radius: 50%;
	transition: background-color 0.2s ease;
}

.video-controls__button:hover,
.video-controls__button:focus-visible {
	background: rgb(255 255 255 / 18%);
}

.video-controls__icon {
	width: 20px;
	height: 20px;
	display: block;
}

/*
 * One button, two glyphs: the state lives on the player so the film's own
 * properties drive the picture, rather than a class the script has to
 * remember to keep in step.
 */
.video-controls__icon--pause,
[data-video-player].is-playing .video-controls__icon--play {
	display: none;
}

[data-video-player].is-playing .video-controls__icon--pause {
	display: block;
}

.video-controls__icon--sound-off {
	display: none;
}

[data-video-player].is-muted .video-controls__icon--sound-on {
	display: none;
}

[data-video-player].is-muted .video-controls__icon--sound-off {
	display: block;
}

/*
 * Shared by the seek bar and the volume slider; only their widths differ.
 * --level is the filled portion, set by the script, so each track reads as a
 * level rather than a bare groove. Both engines' tracks and thumbs have to be
 * styled separately, and neither inherits from the other — hence the pairs.
 */
.video-controls__range {
	--level: 0%;

	background: none;
	appearance: none;
	-webkit-appearance: none;
	cursor: pointer;
}

/* Full width of the film, and the taller hit area of the two. */
.video-controls__seek {
	width: 100%;
	height: 20px;
}

.video-controls__volume {
	width: 88px;
	height: 34px;
	flex: none;
	margin-left: 4px;
}

.video-controls__range::-webkit-slider-runnable-track {
	height: 3px;
	border-radius: 3px;
	background: linear-gradient(
		to right,
		var(--hlp-red) var(--level),
		rgb(255 255 255 / 45%) var(--level)
	);
}

.video-controls__range::-moz-range-track {
	height: 3px;
	border-radius: 3px;
	background: linear-gradient(
		to right,
		var(--hlp-red) var(--level),
		rgb(255 255 255 / 45%) var(--level)
	);
}

.video-controls__range::-webkit-slider-thumb {
	appearance: none;
	-webkit-appearance: none;
	width: 11px;
	height: 11px;
	margin-top: -4px;
	border: 0;
	border-radius: 50%;
	background: var(--hlp-white);
}

.video-controls__range::-moz-range-thumb {
	width: 11px;
	height: 11px;
	border: 0;
	border-radius: 50%;
	background: var(--hlp-white);
}

.video-controls__range:focus-visible {
	outline: 2px solid var(--hlp-white);
	outline-offset: 2px;
}

/*
 * The subtitles button: an outline when off and a solid plate when on, read
 * straight off aria-pressed so the picture and the announced state cannot
 * disagree.
 */
.video-controls__icon--captions-on,
[aria-pressed="true"] > .video-controls__icon--captions-off {
	display: none;
}

[aria-pressed="true"] > .video-controls__icon--captions-on {
	display: block;
}

/* -------------------------------------------------------------------------
 * Film subtitles
 *
 * Drawn by main.js from the film's <track>. Sized in container units so the
 * text grows with the picture: on the intro the film goes from 770px wide to
 * the whole window, and type fixed in pixels would either shout over the
 * small frame or be lost in the large one. Each line gets its own dark plate,
 * because the footage runs to a bright red that white text alone disappears
 * against.
 * ---------------------------------------------------------------------- */

[data-video-player] {
	container-type: inline-size;
}

.video-captions {
	position: absolute;
	inset: auto 0 max(12px, 3.5cqi);
	z-index: 1;
	padding-inline: 8%;
	font-size: clamp(12px, 2.4cqi, 30px);
	line-height: 1.4;
	text-align: center;
	color: var(--hlp-white);
	/* The film behind has to stay clickable, and the controls above it. */
	pointer-events: none;
	transition: bottom 0.25s ease;
}

.video-captions__cue {
	padding: 0.08em 0.4em;
	white-space: pre-line;
	background: rgb(0 0 0 / 62%);
	-webkit-box-decoration-break: clone;
	box-decoration-break: clone;
}

/*
 * Lifted clear of the bar whenever the bar is up, on the same three triggers
 * that raise it. 94px is the bar's full height: 28px of scrim padding, the
 * 20px seek row, the 2px gap, the 34px buttons and 10px beneath.
 */
[data-video-player]:hover .video-captions,
.video-controls:focus-within ~ .video-captions,
.video-controls.is-held ~ .video-captions {
	bottom: 98px;
}

/*
 * A phone has no hover to reveal them with, so they stay up; and the volume
 * slider goes, since the hardware keys do that job and the row would
 * otherwise crowd a 400px film.
 */
/*
 * A phone has no hover to reveal the bar with, so main.js puts it up on a tap
 * and takes it down again after a few seconds — .is-held above is that hook.
 * The volume slider goes: the hardware keys do that job, and the row would
 * otherwise crowd a 400px film.
 */
@media (hover: none) {
	.video-controls__volume {
		display: none;
	}
}

/*
 * And for the same reason on a portrait film, which is a quarter the width of
 * the landscape one the bar was drawn for: 284px against 1031, where the
 * slider alone overran the film by 57. The mute button stays — on a pointer
 * there are no hardware keys to fall back on, and turning the sound on is the
 * whole reason this bar is here.
 */
.campaigns__popin[data-film="portrait"] .video-controls__volume {
	display: none;
}

/*
 * Now that the bar sits on the picture, its height is read against the film's
 * own: 78px over a 217px film is more than a third of it. Trimmed so it reads
 * as a strip on the footage rather than a shelf across it.
 */
@media (max-width: 768px) {
	.video-controls {
		gap: 0;
		padding: 10px 6px 4px;
	}

	.video-controls__row {
		gap: 0;
	}

	.video-controls__button {
		width: 28px;
		height: 28px;
	}

	.video-controls__icon {
		width: 16px;
		height: 16px;
	}

	.video-controls__seek {
		height: 12px;
	}

	.video-controls__time {
		margin-left: 6px;
		font-size: 11px;
	}

	/* The trimmed bar: 10 + 12 + 28 + 4. */
	[data-video-player]:hover .video-captions,
	.video-controls:focus-within ~ .video-captions,
	.video-controls.is-held ~ .video-captions {
		bottom: 58px;
	}
}

@media (prefers-reduced-motion: reduce) {
	.video-controls,
	.video-controls__button,
	.video-captions {
		transition: none;
	}
}

.hero__scroll {
	position: absolute;
	left: 50%;
	translate: -50%;
	top: calc(100svh - 55px);
	z-index: 2;
	width: 18px;
	display: block;
	opacity: calc(1 - var(--p));
	transition: opacity 0.2s linear;
}

.hero__scroll img {
	width: 100%;
	height: auto;
}

/*
 * Mobile frame 1:2268 shows the video parked near the top at its natural
 * size, with no decorative arc, no scroll cue and no growth — so the pin
 * comes off and the section is simply as tall as its contents.
 */
@media (max-width: 768px) {
	.hero {
		height: auto;
		/* Video top edge sits 111px down in a 430x932 frame. */
		padding-top: 111px;
		padding-bottom: 60px;
	}

	.hero__pin {
		position: static;
		display: block;
		height: auto;
	}

	.hero__media {
		/* 400px wide in a 430 frame — 15px either side. */
		width: calc(100% - 30px);
		height: auto;
		margin-inline: auto;
		aspect-ratio: 1031 / 580;
	}

	/*
	 * A portrait cut has been set for phones, and no frame draws one: the box
	 * turns 9:16 to fit it. At full width that is 711px tall on a 430 frame,
	 * more than an iPhone SE has, so the width also yields to the height —
	 * the film fits under the 111px above it with some page still showing
	 * below, and centres in what it leaves.
	 */
	.hero__media--portrait {
		width: min(calc(100% - 30px), calc((100svh - 150px) * 9 / 16));
		aspect-ratio: 9 / 16;
	}

	.hero__shape,
	.hero__scroll {
		display: none;
	}
}

/* -------------------------------------------------------------------------
 * Section 02 — l'agence
 *
 * Frame 1:119, 1440 x 960. Every horizontal figure below is a percentage of
 * the 1280 content box, so the whole section scales with the container while
 * keeping the frame's proportions:
 *
 *   collage    100 -> 640      text column  650 -> 1267
 *   arrows     705 -> 805      slide text   849 -> 1360
 *
 * Vertically the frame puts the collage 110px down and the PS footnote 24px
 * off the bottom, which is where the section's asymmetric padding comes from.
 * ---------------------------------------------------------------------- */

.about {
	position: relative;
	z-index: 1;
	padding-top: max(110px, var(--hlp-header-h));
	padding-bottom: 24px;
	/*
	 * The slides that are not on stage are parked 28px to one side, which is
	 * where they drift in from. visibility: hidden takes them off the screen
	 * but not out of the layout — a hidden box still counts towards what the
	 * page can be scrolled to — so on any window whose gutter is narrower than
	 * 28px they push the document wider than itself. At 412 the gutter is 15
	 * and the page came out 13px too wide, which is the whole of the sideways
	 * scrollbar that was cutting the foot off section 06.
	 *
	 * Clipped here, at the section, rather than around the slides. The section
	 * runs the full width of the window, so the cut lands on the edge of the
	 * screen and not on the slide: every window with a gutter of 28px or more
	 * — which is every desktop width, where it is 80 — sees the drift entire
	 * and is untouched by this. Only where the offset would have gone off the
	 * screen anyway is anything lost.
	 *
	 * clip, not hidden, so no scroll container is made and the axis can be
	 * clipped on its own with the other left visible.
	 */
	overflow-x: clip;
}

.about__inner {
	display: grid;
	/* 570 / 710 of the 1280 content box. */
	grid-template-columns: minmax(0, 570fr) minmax(0, 710fr);
	align-items: start;
}

/* ----- Collage ----- */

.about__media {
	position: relative;
	/* 20px in from the content edge, 10px short of the text column. */
	margin-left: 3.509%;
	margin-right: 1.754%;
	aspect-ratio: 540 / 740.15;
}

/*
 * Each photo is absolutely placed on the collage box and covered into its
 * frame — the design crops rather than fits. Later photos stack on earlier
 * ones, which is the overlap in the frame.
 */
.about__photo {
	position: absolute;
	object-fit: cover;
}

/*
 * Entrance reveal, on a timer rather than tied to the scroll position: the
 * page ends at this section until 03 onwards are built, which left under one
 * viewport of scroll to play across and read as instant. main.js adds
 * .is-revealing to arm the closed state and .is-revealed once the collage
 * reaches the viewport, and the transition between the two is the animation.
 *
 * Both classes come only from main.js, so without JavaScript the photos are
 * simply visible.
 *
 * Each photo takes 1.1s and they start 350ms apart, so the sequence runs 1.8s
 * end to end and reads as three arrivals rather than one group fade.
 */
.about.is-revealing .about__photo {
	--delay: 0s;
	--exit-delay: 0.18s;
	/* How far it travels in, matched to where it sits in the collage. */
	--dx: 0px;
	--dy: 64px;

	/* Closed until the collage has been seen at least once. */
	opacity: 0;
}

.about.is-revealing.is-revealed .about__photo {
	animation: hlp-photo-in 1.1s cubic-bezier(0.22, 1, 0.36, 1) var(--delay) both;
}

.about.is-revealing.is-leaving .about__photo {
	animation: hlp-photo-out 0.5s cubic-bezier(0.55, 0, 1, 0.45) var(--exit-delay) both;
}

/*
 * Arriving, the lower two come in from the side they sit on, which spreads the
 * collage open. Leaving, the order reverses — bottom photo first — so the exit
 * unwinds the collage instead of mirroring the entrance.
 */
.about.is-revealing .about__photo--2 {
	--delay: 0.35s;
	--exit-delay: 0.09s;
	--dx: -34px;
}

.about.is-revealing .about__photo--3 {
	--delay: 0.7s;
	--exit-delay: 0s;
	--dx: 34px;
}

/* Reduced-motion pair: opacity only, no geometry. */
@keyframes hlp-photo-fade-in {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

@keyframes hlp-photo-fade-out {
	from {
		opacity: 1;
	}

	to {
		opacity: 0;
	}
}

/* In: a slow left-to-right wipe that rises into place. */
@keyframes hlp-photo-in {
	from {
		opacity: 0;
		clip-path: inset(0 100% 0 0);
		transform: translate(var(--dx), var(--dy));
	}

	to {
		opacity: 1;
		clip-path: inset(0 0 0 0);
		transform: none;
	}
}

/*
 * Out: deliberately not the entrance in reverse. Where that wipes sideways and
 * rises over 1.1s, this collapses from the top edge down and sinks away in
 * half the time, accelerating rather than easing out.
 */
@keyframes hlp-photo-out {
	from {
		opacity: 1;
		clip-path: inset(0 0 0 0);
		transform: none;
	}

	to {
		opacity: 0;
		clip-path: inset(100% 0 0 0);
		transform: translateY(28px) scale(0.98);
	}
}

.about__photo--1 {
	left: 0;
	top: 0;
	width: 88%;
	height: 48.207%;
}

.about__photo--2 {
	left: 6.111%;
	top: 40.127%;
	width: 58.518%;
	height: 28.507%;
}

.about__photo--3 {
	left: 35%;
	top: 62.035%;
	width: 65%;
	height: 37.965%;
}

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

.about__content {
	/* The headline sits at 201 in the frame, 91px below the collage. */
	padding-top: 91px;
}

.about__title {
	margin: 0;
	/* The frame's box is the full text column now, and breaks the lines
	   itself rather than leaving them to a narrower one. */
	max-width: none;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-transform: uppercase;
}

/*
 * The closing line is set apart from the headline rather than continuing it:
 * 27px against 38, and out of the capitals the block is in. The frame gets
 * there by lower-casing all but the first letter; <small> is the same thing
 * said once, and it survives the client rewording the sentence.
 */
.about__title small {
	/*
	 * A block of its own, so the line is 27 x 1.05 tall like the frame's.
	 * Left inline it keeps the 38px headline's strut and stands 40 tall,
	 * which pushed everything under it 12px down.
	 */
	display: block;
	font-size: 27px;
	text-transform: none;
}

/*
 * The break that used to put that line on its own is redundant once it is a
 * block, and would leave an empty line where it stands.
 */
.about__title br:has(+ small) {
	display: none;
}

.about__lead {
	/* 414 in the frame, under a title that closes at 309. */
	margin: 105px 0 0;
	max-width: 86.901%; /* 617 of 710 */
	font-size: 24px;
	line-height: 1.2;
}

.about__ps {
	/* 882 in the frame, under a carousel whose box closes at 812. */
	margin: 70px 0 0;
	max-width: 88.592%; /* 629 of 710 */
	font-size: 16px;
	line-height: 26px;
}

/* No thin italic cut is licensed, so the light weight stands in for it. */
.about__ps em {
	font-weight: 300;
	font-style: italic;
}

/* 10.32px on a 16px run; line-height 0 keeps it out of the 26px line box. */
.about__ps sup {
	font-size: 0.645em;
	line-height: 0;
}

/* ----- Slider ----- */

.about__slider {
	margin-top: 97px;
	/* Indented to 705 in the frame; the row that follows is 655 wide. */
	margin-left: 7.746%;
	display: grid;
	grid-template-columns: 15.267% minmax(0, 1fr); /* 100 / 655 */
	grid-template-areas:
		"controls slides"
		".        dots";
	column-gap: 6.718%; /* 44 of 655 */
	/*
	 * 53 puts the dashes where frame 1:119 draws them, 757, under the slide it
	 * shows. The other three frames sit their dashes 68, 19 and 44 below their
	 * own last line rather than 60, so no single figure — and no rule — fits
	 * all four; two of those come from leftover invisible text and from
	 * container gaps set by hand, not from a spacing system.
	 */
	row-gap: 53px;
	align-items: center;
	/*
	 * The dashes follow the slide in front, but the block keeps the room the
	 * longest of them needs — 187 of text, the 53 gap, the 3 of dash — so the
	 * post-scriptum under it does not move as the carousel turns. The frames
	 * get the same steadiness from text boxes of a fixed height.
	 */
	min-height: 243px;
	align-content: start;
}

.about__controls {
	grid-area: controls;
	display: flex;
	justify-content: space-between;
	/*
	 * All four frames put the arrows at 650 whichever slide is in front, so
	 * they are pinned rather than centred on the text beside them — that row
	 * is now as tall as the slide showing, and centring would walk them up and
	 * down with it. 81 below the carousel's own top is that 650.
	 */
	align-self: start;
	margin-top: 81px;
}

/*
 * The arrows are masked rather than dropped in as <img> so the disabled tint
 * comes from one file per direction instead of a second colour variant.
 */
.about__arrow {
	width: 38px;
	height: 38px;
	color: var(--hlp-black);
	background-color: currentColor;
	transition: color 0.25s ease;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
}

.about__arrow--prev {
	-webkit-mask-image: url("../images/icon-slider-prev.svg");
	mask-image: url("../images/icon-slider-prev.svg");
}

.about__arrow--next {
	-webkit-mask-image: url("../images/icon-slider-next.svg");
	mask-image: url("../images/icon-slider-next.svg");
}

.about__arrow:disabled {
	color: var(--hlp-placeholder);
	cursor: default;
}

.about__arrow:not(:disabled):hover,
.about__arrow:not(:disabled):focus-visible {
	color: var(--hlp-red);
}

/*
 * The slides used to share one grid cell, which made the block as tall as the
 * longest of them and held the dashes at a single height. The frames draw the
 * dashes under each slide's own text, so the ones not showing are taken out of
 * the flow instead: the block is as tall as the slide in front, and the dashes
 * and the post-scriptum follow it as the carousel turns.
 *
 * Out of the flow rather than display:none, which would take the entrance
 * animation with it.
 */
.about__slides {
	grid-area: slides;
	position: relative;
}

/*
 * Slide change: the incoming slide drifts in from the side it is travelling
 * from and fades up; the outgoing one only fades, since the incoming covers
 * it anyway and two moving blocks read as noise. --slide-dir is +1 going
 * forward and -1 going back, and main.js sets it before the swap.
 */
.about__slide {
	transition:
		opacity 0.45s ease,
		transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

.about__slide[inert] {
	position: absolute;
	inset: 0 0 auto;
	visibility: hidden;
	opacity: 0;
	transform: translateX(calc(var(--slide-dir, 1) * 28px));
	/*
	 * Offset and visibility are held back until the fade is done, so the
	 * outgoing slide leaves without appearing to move.
	 */
	transition:
		opacity 0.25s ease,
		transform 0s linear 0.25s,
		visibility 0s linear 0.25s;
}

.about__slide-title {
	/* The frame butts the text straight onto the heading, no gap. */
	margin: 0;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 25px;
	line-height: 1.24;
	text-transform: uppercase;
	color: var(--hlp-red);
}

.about__slide p {
	margin: 0;
	font-size: 16px;
	line-height: 26px;
}

/* The frame's blank line between the two paragraphs. */
.about__slide p + p {
	margin-top: 26px;
}

/*
 * On the "Notre mission" slide the initial of each line is set in red bold and
 * the four of them spell LIFE. Any emphasis in the block takes the accent.
 */
.about__slide strong {
	font-weight: 700;
	color: var(--hlp-red);
}

.about__slide em {
	font-style: italic;
}

/*
 * Pagination dashes. Plain rectangles in the frame rather than artwork, and
 * each one is a control, so they are drawn in CSS: 13x3 outlined, and 24 wide
 * filled red for the current slide.
 */
.about__dots {
	grid-area: dots;
	display: flex;
	align-items: center;
	gap: 7px;
}

.about__dot {
	width: 13px;
	height: 3px;
	background: var(--hlp-white);
	border: 1px solid var(--hlp-black);
	transition: width 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}

.about__dot[aria-current="true"] {
	width: 24px;
	background: var(--hlp-red);
	border-color: var(--hlp-red);
}

/*
 * Between 768 and 1024 there is no frame: the 1440 two-column layout crowds
 * well before the theme's mobile breakpoint, so the columns simply stack and
 * the desktop indents are dropped. The mobile frame takes over below 768.
 */
@media (max-width: 1024px) {
	.about {
		padding-top: max(60px, var(--hlp-header-h));
		padding-bottom: 60px;
	}

	.about__inner {
		grid-template-columns: minmax(0, 1fr);
		row-gap: 60px;
	}

	.about__media {
		margin-inline: 0;
	}

	.about__content {
		padding-top: 0;
	}

	.about__title,
	.about__lead,
	.about__ps {
		max-width: none;
	}

	.about__title {
		font-size: 30px;
	}

	/* Held to the frame's 27/38 against each width's own headline. */
	.about__title small {
		font-size: 21px;
	}

	.about__lead {
		font-size: 20px;
		/* The frame's 105 belongs to a 38px title; this stack sets 30. */
		margin-top: 83px;
	}

	.about__slider {
		margin-top: 48px;
		margin-left: 0;
		/*
		 * Same reserve as the desktop, re-measured for this stack: 187 of
		 * text, the 24 gap, the 3 of dash. Without it the section grows and
		 * shrinks as the carousel turns and everything below it moves.
		 */
		min-height: 214px;
		grid-template-columns: minmax(0, 1fr);
		grid-template-areas:
			"slides"
			"dots"
			"controls";
		row-gap: 24px;
	}

	.about__controls {
		justify-content: flex-start;
		gap: 24px;
	}

	.about__ps {
		margin-top: 60px;
	}
}

/* -------------------------------------------------------------------------
 * Section 02 — mobile
 *
 * Frame 1:2297, 430 x 932. Not the desktop layout reflowed: the copy comes
 * first, the collage below it is a scattered arrangement bleeding off both
 * edges rather than the desktop's stacked overlap, and the carousel loses its
 * arrows. The percentages below are of the 430 frame.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	/*
	 * The frame puts the title 117 down the screen. That used to be carried
	 * entirely by padding the header's full height, 89, which a plain scroll
	 * down from the film paid for as a band of white — 149 with the hero's
	 * own tail — and which left the bottom of the section below the fold.
	 *
	 * Now a jump from the menu lands 60 above the section, and the padding
	 * makes up the rest: 60 + 57 = 117. The 60 is not arbitrary — it is the
	 * hero's white tail, its padding-bottom on a phone. The header is
	 * see-through, so whatever sits under it on landing shows; landing any
	 * higher than that tail puts the bottom of the film behind the logo.
	 * If the hero's tail changes, this has to follow it.
	 */
	.about {
		padding-top: 57px;
		padding-bottom: 48px;
		scroll-margin-top: 60px;
	}

	/*
	 * The frame sets this section on 15px margins, narrower than the 27px the
	 * header keeps, so the container's own padding is overridden here.
	 */
	.about__inner {
		display: flex;
		flex-direction: column;
		padding-inline: 15px;
		/* The stacked layout above sets one; here the frame's own margins
		   carry the spacing, so the gap has to go. */
		row-gap: 0;
	}

	/*
	 * Mobile puts the collage between the lead and the carousel, so the text
	 * wrapper is dissolved and its children ordered against the collage
	 * directly. Without display: contents the collage could only sit wholly
	 * above or below that block.
	 */
	.about__content {
		display: contents;
	}

	.about__title {
		order: 1;
		max-width: none;
		margin-bottom: 15px;
		font-size: 32px;
		line-height: 1;
	}

	/*
	 * The headline's breaks are cut for the desktop's 710 column; at 400 the
	 * first of them drops "CRÉATIVE" onto a line of its own. Let the headline
	 * reflow here instead — the phone's own frame breaks its title in its own
	 * place, for its own wording, and not in ours.
	 *
	 * The closing sentence still needs its own line, so it is a block here
	 * rather than something a break puts there.
	 */
	.about__title br {
		display: none;
	}

	.about__title small {
		display: block;
		font-size: 23px;
	}

	.about__lead {
		order: 2;
		max-width: none;
		margin: 0;
		font-weight: 300;
		font-size: 16px;
		line-height: 1.4;
	}

	/*
	 * Full bleed — the outer two photos run off the edges of the frame.
	 *
	 * The width is stated rather than left to stretch: every photo inside is
	 * absolutely positioned, so in a column flex container the collage's
	 * content height is zero, and aspect-ratio has no width to resolve
	 * against until the cross size is settled. It collapses to nothing.
	 */
	.about__media {
		order: 3;
		width: calc(100% + 30px);
		/* 442 in the frame, under a lead that closes at 426. */
		margin: 16px -15px 0;
		aspect-ratio: 430 / 209;
	}

	.about__photo--1 {
		left: -0.93%;
		top: 7.656%;
		width: 59.767%;
		height: 81.34%;
	}

	.about__photo--2 {
		left: 52.558%;
		top: 45.455%;
		width: 39.767%;
		height: 54.545%;
	}

	.about__photo--3 {
		left: 78.14%;
		top: 0;
		width: 21.86%;
		height: 35.885%;
	}

	/*
	 * The collage is under a third of its desktop height here, so the entrance
	 * travels proportionally less, and the side each photo arrives from is
	 * remapped to where it sits in this arrangement.
	 */
	.about.is-revealing .about__photo {
		--dy: 28px;
		--dx: -24px;
	}

	.about.is-revealing .about__photo--2,
	.about.is-revealing .about__photo--3 {
		--dx: 24px;
	}

	.about__slider {
		order: 4;
		/* Indented to 30px in the frame, 15 of which is the section margin. */
		margin: 42px 0 0 15px;
		grid-template-columns: minmax(0, 1fr);
		grid-template-areas:
			"slides"
			"dots";
		column-gap: 0;
		/* The frame leaves 13 between the last line and the dashes. */
		row-gap: 13px;
		/*
		 * The reserve matters most here: the carousel is swiped, and without
		 * it the section grew and shrank by up to 59px under the reader's own
		 * finger, carrying section 03 with it. 169 is the longest slide in
		 * French, 149 in English.
		 */
		min-height: 185px;
	}

	/* The frame drops the arrows; the dots carry the carousel on their own. */
	.about__controls {
		display: none;
	}

	/* Mobile sets the slide heading in the normal cut, not the condensed one. */
	.about__slide-title {
		margin-bottom: 7px;
		font-family: var(--hlp-font);
		font-size: 20px;
		line-height: 1.2;
	}

	.about__slide p {
		font-weight: 300;
		font-size: 14px;
		line-height: 1.4;
	}

	.about__slide p + p {
		margin-top: 20px;
	}

	/*
	 * The frame's line breaks are cut for its 511px column; at 385 they leave
	 * orphans — "accompagnant" alone on a line. The phone's own frame runs
	 * this text unbroken, so here the breaks are dropped and the words reflow.
	 * nl2br leaves the newline in the markup beside the tag, so hiding the tag
	 * still keeps the words apart.
	 *
	 * Except where a break is the point: slide two's four lines spell L-I-F-E
	 * down their red initials, and that acrostic is the only thing in the
	 * block set in <strong>. Those keep their breaks. A browser without :has()
	 * keeps every break, which is the layout as it was.
	 */
	.about__slide p:not(:has(strong)) br {
		display: none;
	}

	/* The mobile frame carries no post-scriptum. */
	.about__ps {
		display: none;
	}
}

/* -------------------------------------------------------------------------
 * Section 03 — les expertises
 *
 * Frame 1:211, 1440 x 960. A photograph fills the section and fourteen white
 * cards sit on a five-by-four grid over it, six cells left empty so the picture
 * shows through.
 *
 * Every card is exactly one column of the 5-column grid — 256 of the 1280
 * content box — and 160 tall, so the whole grid is the content box and the card
 * edges land on the decorative rules. That is why nothing here is a percentage:
 * the grid is the layout the rest of the site is already measured against.
 * ---------------------------------------------------------------------- */

/*
 * --v is one of the frame's vertical pixels. The frame is 960 tall and is meant
 * to be one screenful, but its vertical measurements are fixed while a laptop
 * window is routinely 800 — which spilled the last row of cards below the fold
 * whatever you did with the scroll position. Every vertical figure below is
 * stated as the frame's own number times --v, so on a shorter window the whole
 * rhythm contracts together instead of the section overflowing.
 *
 * At 960 and above it resolves to 1px and the section is the frame exactly. The
 * floor stops a very short window from squeezing the cards past legibility;
 * below roughly 690px tall the section scrolls again, which is the lesser evil.
 *
 * Horizontal measurements are deliberately untouched: the cards stay one
 * 256px column wide and keep landing on the decorative rules, which is the
 * part of this composition that has to survive.
 */
.expertise {
	--v: clamp(0.72px, 0.10417svh, 1px);

	position: relative;
	/* The frame puts the headline 117px down and the grid 59px off the bottom. */
	padding-top: max(calc(117 * var(--v)), var(--hlp-header-h));
	padding-bottom: calc(59 * var(--v));
}

/*
 * Deliberately not a background on the section: the frame keeps the decorative
 * column rules visible across the photograph, and those are fixed at z-index 0.
 * A negative order puts the photograph under them while .expertise itself makes
 * no stacking context, and the cards — being opaque — hide the rules again
 * wherever one sits. Which is exactly how the frame is drawn.
 */
.expertise__background {
	position: absolute;
	inset: 0;
	z-index: -1;
	overflow: hidden;
}

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

.expertise__layout {
	position: relative;
	z-index: 1;
}

.expertise__title {
	margin: 0 auto;
	/*
	 * The frame's box is 769 of 1280, which the French fills to 711 at its
	 * widest. The English headline's second line wants 818 and so fell to a
	 * third line, and a third line is 40px the section does not have — it is
	 * sized to stand in one screen. Widened to 832 for it. Nothing moves in
	 * French: the box has no visible edge and the lines are centred in it, so
	 * a wider one draws them in exactly the same place.
	 */
	max-width: 65%; /* 832 of 1280; the frame's own box is 769 */
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: calc(38 * var(--v));
	line-height: 1.05;
	text-align: center;
	text-transform: uppercase;
	color: var(--hlp-white);
}

/*
 * Measured to put the first row on 261, where the frame has it. Figma gives the
 * headline an 87px box for two lines that actually set 80, so taking the
 * frame's 57px gap at face value would carry that 7px slack into the grid.
 */
.expertise__grid {
	margin-top: calc(64 * var(--v));
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	/*
	 * The frame's 160 rather than the card's own aspect ratio: the cards are
	 * pinned to the column width horizontally, so on a short window the only
	 * thing left to give is their height.
	 */
	grid-auto-rows: calc(160 * var(--v));
}

/*
 * The row and column come from the template, so the frame's scatter is stated
 * once — as the grid it is — instead of being spread across a class per cell.
 */
.expertise__card {
	position: relative;
	grid-row: var(--row);
	grid-column: var(--column);
	perspective: 1200px;
	color: var(--hlp-black);
	text-decoration: none;
}

/*
 * The turn. Only the wrapper rotates, which leaves the card itself free for the
 * entrance animation below — the two would otherwise be fighting over the same
 * transform.
 */
.expertise__flip {
	position: absolute;
	inset: 0;
	transform-style: preserve-3d;
	transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

/*
 * .is-turned is main.js's, for a phone: there is no hover to turn the card
 * with there, and the card is a link, so a tap used to leave for the
 * destination before anything turned — the back could not be seen at all.
 * Listed here rather than given a rule of its own so the three ways a card
 * turns cannot drift apart.
 */
.expertise__card--linked:hover .expertise__flip,
.expertise__card--linked:focus-visible .expertise__flip,
.expertise__card--linked.is-turned .expertise__flip {
	transform: rotateY(180deg);
}

.expertise__face {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	padding: 10px;
	background: var(--hlp-white);
	border: 1px solid var(--hlp-black);
	font-size: 18px;
	font-weight: 500;
	line-height: normal;
	text-align: center;
	backface-visibility: hidden;
	-webkit-backface-visibility: hidden;
}

/* Frame 69:213: red ground, white wording, the number still in the corner. */
.expertise__face--back {
	background: var(--hlp-red);
	color: var(--hlp-white);
	transform: rotateY(180deg);
}

.expertise__number {
	position: absolute;
	left: 12px;
	top: 7px;
	font-size: 14px;
	font-weight: 500;
	line-height: 1.1;
}

.expertise__scroll {
	position: absolute;
	left: 50%;
	translate: -50%;
	bottom: 35px;
	z-index: 1;
	width: 18px;
	display: block;
}

.expertise__scroll img {
	width: 100%;
	height: auto;
}

/* ----- Entrance -----
 *
 * The cards are asked to arrive scattered rather than sweeping across the grid
 * in reading order, so each one is dealt a variant and a place in the running
 * order at random. main.js does the dealing and writes both into --enter and
 * --delay; all this end knows is that there are four ways in.
 *
 * .is-revealing arms the closed state and .is-revealed releases it, exactly as
 * in section 02. Both come only from main.js, so without JavaScript the cards
 * are simply there.
 */

.expertise.is-revealing .expertise__card {
	opacity: 0;
}

.expertise.is-revealing.is-revealed .expertise__card {
	animation: var(--enter, hlp-card-rise) 1.4s cubic-bezier(0.22, 1, 0.36, 1)
		var(--delay, 0s) both;
}

@keyframes hlp-card-rise {
	from {
		opacity: 0;
		transform: translateY(42px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

@keyframes hlp-card-drop {
	from {
		opacity: 0;
		transform: translateY(-42px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* The same left-to-right wipe the section 02 collage arrives on. */
@keyframes hlp-card-wipe {
	from {
		opacity: 0;
		clip-path: inset(0 100% 0 0);
	}

	to {
		opacity: 1;
		clip-path: inset(0 0 0 0);
	}
}

@keyframes hlp-card-zoom {
	from {
		opacity: 0;
		transform: scale(0.82);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* Reduced-motion stand-in: the shuffled order survives, the geometry does not. */
@keyframes hlp-card-fade {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

/*
 * Between 769 and 1024 there is no frame. The scatter is a five-column
 * composition and cannot survive fewer, so the cards drop their placement and
 * simply flow — which the source order already puts in reading order. This is
 * the one provisional range left in the section; 768 down is drawn.
 */
@media (max-width: 1024px) {
	/*
	 * Fitting the section to the window stops here. Three columns make the grid
	 * taller than any laptop screen, so there is nothing to be gained by
	 * contracting the rhythm — --v goes back to a plain pixel and the cards go
	 * back to taking their height from their own proportions.
	 */
	.expertise {
		--v: 1px;

		padding-top: max(90px, var(--hlp-header-h));
		padding-bottom: 60px;
	}

	.expertise__title {
		max-width: none;
		font-size: 30px;
	}

	.expertise__grid {
		margin-top: 40px;
		grid-template-columns: repeat(3, minmax(0, 1fr));
		grid-auto-rows: auto;
	}

	.expertise__card {
		grid-row: auto;
		grid-column: auto;
		aspect-ratio: 256 / 160;
	}

	.expertise__face {
		font-size: 16px;
	}
}

/* -------------------------------------------------------------------------
 * Section 03, mobile — frame 1:2350, 430 x 1925
 *
 * The desktop scatter becomes a staircase: one card per row, alternating left
 * and right, each 214 of the 400 the frame gives them. 214 + 214 is more than
 * 400, so the two sides overlap by 28px through the middle — the step down the
 * page is 186, not the card's own width. That overlap is the whole figure, and
 * it is why this is not simply a two-column grid.
 *
 * Rows are the card's own height with nothing between them, so the staircase is
 * continuous: every card shares an edge with the one above it.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.expertise {
		/* Headline at 117, last card 27 off the bottom — as on desktop. */
		padding-top: max(117px, var(--hlp-header-h));
		padding-bottom: 27px;
	}

	/*
	 * The frame insets this section by 15 rather than the 27 the header and
	 * every other mobile block use, so the cards sit wider than the page.
	 */
	.expertise__layout {
		padding-inline: 15px;
	}

	.expertise__title {
		/* 400 of 430 is the full content width once inset by 15. */
		font-size: 32px;
		line-height: 1;
	}

	/*
	 * One card per row. The desktop placement is already off at this width, so
	 * the cards auto-flow and the staircase comes from the offset below.
	 */
	.expertise__grid {
		margin-top: 45px;
		grid-template-columns: minmax(0, 1fr);
	}

	.expertise__card {
		width: 53.5%; /* 214 of 400 */
	}

	/* 186 of 400: the step, which is short of the card's own width. */
	.expertise__card:nth-child(even) {
		margin-left: 46.5%;
	}

	.expertise__face {
		font-size: 16px;
	}

	.expertise__number {
		left: 11px;
		top: 10px;
		font-size: 12px;
	}

	/* The frame ends on the last card; there is no scroll cue on mobile. */
	.expertise__scroll {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	.expertise.is-revealing.is-revealed .expertise__card {
		animation: hlp-card-fade 0.3s ease var(--delay, 0s) both;
	}

	.expertise__flip {
		transition-duration: 0.01ms;
	}
}

/*
 * Reduced motion keeps the carousel a plain cut, and strips the collage
 * reveal back to the fade — the wipe and the drift are the motion, the
 * opacity is not.
 */
@media (prefers-reduced-motion: reduce) {
	/* Fade in and out on the spot: no wipe, no drift, no scale. */
	.about.is-revealing.is-revealed .about__photo {
		animation: hlp-photo-fade-in 0.3s ease var(--delay) both;
	}

	.about.is-revealing.is-leaving .about__photo {
		animation: hlp-photo-fade-out 0.3s ease var(--exit-delay) both;
	}

	.about__arrow,
	.about__dot,
	.about__slide,
	.about__slide[inert] {
		transition-duration: 0.01ms;
		transition-delay: 0s;
	}

	.about__slide[inert] {
		transform: none;
	}
}

/* Scoped above the mobile breakpoint, which already has no growth to undo. */
@media (prefers-reduced-motion: reduce) and (min-width: 769px) {
	.hero {
		height: 100svh;
	}
}

@media (prefers-reduced-motion: reduce) {
	.hero__media {
		--p: 0;
	}

	.nav-overlay,
	.nav-overlay.is-open,
	.nav-overlay__item,
	.nav-overlay__link,
	.nav-overlay__link::before {
		transition-duration: 0.01ms;
		transition-delay: 0s;
	}

	.nav-overlay__item {
		transform: none;
	}
}

/* -------------------------------------------------------------------------
 * Section 04 — les campagnes
 *
 * Figma frames 1:351 (1440x960) for the deck, 1:1640 and 1:1720 for the two
 * turns of it, 1:1821 / 1:1890 / 1:1958 for the film open.
 *
 * Every figure below is the frame's, taken against its 1281-wide content band
 * (79 to 1360) and expressed against the theme's 1280 column. The deck is one
 * grid cell with all three cards stacked in it; a card's whole appearance
 * follows from data-offset, its signed distance from the front, which main.js
 * writes and only this file reads. 0 is upright and in colour, ±1 is pushed
 * out to 46.9% and turned red, anything further is parked out of sight — so a
 * fourth campaign needs no rule of its own.
 * ---------------------------------------------------------------------- */

.campaigns {
	position: relative;
	/*
	 * The frame's own vertical: headline at 130, dashes ending at 781, and the
	 * 960 closes at 179 below them.
	 */
	padding-top: max(130px, var(--hlp-header-h));
	padding-bottom: 179px;
	/*
	 * The two cards behind reach past the content column by design. Clipping
	 * here keeps that bleed off the document's width — which is the bug the
	 * hero arc still has from 769 up.
	 */
	overflow: hidden;
}

.campaigns__inner {
	position: relative;
}

/* Set left on column 2 — 334 of the frame, 254 into the 1280 column. */
.campaigns__title {
	margin: 0 0 0 19.844%;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-transform: uppercase;
}

/*
 * Up on the headline's band at the right, not under the deck: frame 1:360 is
 * a 100x38 pair at x=1005, so its right edge lands on the fourth column rule.
 * Absolute because it shares that band with a headline of unknown depth.
 */
.campaigns__controls {
	position: absolute;
	top: 15px; /* 145 of the frame, less the section's 130 padding. */
	left: 72.266%; /* 925 of 1280 */
	display: flex;
	gap: 24px;
}

/*
 * Section 02's arrows, masked the same way off the same two files. Written out
 * again rather than shared with .about__arrow: that one is measured against a
 * different frame and carries a disabled state this deck has no use for, so a
 * single class would tie the two sections' geometry together.
 */
.campaigns__arrow {
	width: 38px;
	height: 38px;
	color: var(--hlp-black);
	background-color: currentColor;
	transition: color 0.25s ease;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
}

.campaigns__arrow--prev {
	-webkit-mask-image: url("../images/icon-slider-prev.svg");
	mask-image: url("../images/icon-slider-prev.svg");
}

.campaigns__arrow--next {
	-webkit-mask-image: url("../images/icon-slider-next.svg");
	mask-image: url("../images/icon-slider-next.svg");
}

.campaigns__arrow:hover,
.campaigns__arrow:focus-visible {
	color: var(--hlp-red);
}

/* ----- The deck ----- */

/*
 * 769 of 1280, centred — which puts its left edge on 255, the column the
 * headline is set from. The frame draws the two as sharing that edge.
 */
.campaigns__deck {
	margin: 39px auto 0; /* 249 less the headline's 130 + 80 */
	width: 60.078%;
	display: grid;
}

/*
 * The resting state is "somewhere behind the deck": out of sight, out of the
 * tab order, out of the way. The three rules after it are the only places a
 * card is visible.
 */
.campaigns__slide {
	grid-area: 1 / 1;
	/*
	 * The containing block for the caption, which sits outside the picture in
	 * the markup so mobile can stack it. The slide's box is the picture's, so
	 * placing against it is the same as placing against the frame — except on
	 * the card in front, whose transform is none and which would otherwise
	 * hand the caption up to the section.
	 */
	position: relative;
	z-index: 0;
	opacity: 0;
	visibility: hidden;
	transform: scale(0.42);
	transition:
		transform 0.6s ease,
		opacity 0.6s ease,
		visibility 0s linear 0.6s;
}

.campaigns__slide[data-offset="-1"],
.campaigns__slide[data-offset="0"],
.campaigns__slide[data-offset="1"] {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s, 0s;
}

/*
 * 361 of 769 is 46.94%, and the frame sets both cards' centres 420 from the
 * front's — 54.62% of the card's own width. Their centre line is the front
 * card's, so there is no vertical offset to carry.
 */
.campaigns__slide[data-offset="-1"] {
	z-index: 1;
	transform: translateX(-54.616%) scale(0.4694);
}

.campaigns__slide[data-offset="1"] {
	z-index: 1;
	transform: translateX(54.616%) scale(0.4694);
}

.campaigns__slide[data-offset="0"] {
	z-index: 2;
	transform: none;
}

/* ----- The card ----- */

.campaigns__frame {
	position: relative;
	aspect-ratio: 769 / 493;
	overflow: hidden;
	/* Keeps the colour blend below inside the card. */
	isolation: isolate;
	background: var(--hlp-placeholder);
}

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

/*
 * "COLOR IMG": flat red, blended on colour rather than multiplied. The colour
 * blend takes the red's hue and saturation and keeps the picture's own
 * lightness, so the card behind stays a photograph in red rather than becoming
 * a red panel — which is why the frames can put white wording on it and still
 * be read.
 */
.campaigns__tint {
	position: absolute;
	inset: 0;
	z-index: 1;
	background: var(--hlp-red);
	mix-blend-mode: color;
	opacity: 1;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

.campaigns__slide[data-offset="0"] .campaigns__tint {
	opacity: 0;
}

/* 366 / 278 of the frame, against a card at 335 / 249. */
.campaigns__caption {
	position: absolute;
	z-index: 2;
	left: 4.031%; /* 31 of 769 */
	top: 5.882%; /* 29 of 493 */
	right: 4.031%;
	display: flex;
	flex-direction: column;
	gap: 5px;
	color: var(--hlp-white);
}

/*
 * White is the common case, but not the only one: frames 1:351 and 1:1761 set
 * GUERBET and CNUP in white over dark artwork while 1:1678 sets CNOP in black,
 * because its collage is pale enough to swallow white. It follows the picture
 * rather than the carousel position, so the choice is the editor's, per
 * campaign — see hlp_campaign_fields().
 *
 * Mobile has no say in it: frame 1:2427 lifts the caption off the picture and
 * on to the page, where it is black for every campaign. The rule that does
 * that sits further down and wins on order.
 */
.campaigns__caption--dark {
	color: var(--hlp-black);
}

.campaigns__name {
	margin: 0;
	font-size: 25px;
	font-weight: 700;
	line-height: 1;
}

.campaigns__tagline {
	margin: 0;
	font-size: 16px;
	font-weight: 700;
	line-height: 1;
}

/*
 * The description belongs to the pop-in at this width; the mobile frame moves
 * it onto the card. Written in both places, shown in one.
 */
.campaigns__text {
	display: none;
}

/*
 * Centred across the card, 30 off its foot. The frame gives it no radius and
 * no border — 20/10 of padding around a 20px line is the whole button.
 */
.campaigns__cta {
	position: absolute;
	z-index: 3;
	left: 50%;
	transform: translateX(-50%);
	/*
	 * 418 of 493, less whatever this campaign asked to be lifted by to clear
	 * its own artwork — see the "Remonter le bouton" field.
	 */
	top: calc(84.787% - var(--cta-lift, 0px));
	padding: 10px 20px;
	background: var(--hlp-red);
	color: var(--hlp-white);
	font-size: 20px;
	line-height: 1.25;
	white-space: nowrap;
	transition: background-color 0.25s ease;
}

.campaigns__cta:hover,
.campaigns__cta:focus-visible {
	background: var(--hlp-black);
}

/*
 * A card that is behind is not offering its film, it is offering to come
 * forward — so its button stops being a target and the cover below takes the
 * whole card instead.
 */
.campaigns__slide:not([data-offset="0"]) .campaigns__cta {
	pointer-events: none;
}

.campaigns__pick {
	position: absolute;
	inset: 0;
	z-index: 4;
}

/* ----- Dashes ----- */

/*
 * Frame 1:363: hairlines rather than dots — 11 wide outlined, 22 wide and
 * filled red for the campaign in front, 9 apart. Centred under the deck.
 */
.campaigns__dots {
	margin-top: 28px;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
}

.campaigns__dot {
	width: 11px;
	height: 1px;
	background: var(--hlp-white);
	border: 1px solid var(--hlp-black);
	transition: width 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}

.campaigns__dot[aria-current="true"] {
	width: 22px;
	background: var(--hlp-red);
	border-color: var(--hlp-red);
}

/* ----- The film, open -----
 *
 * Frames 1:1821 / 1:1890 / 1:1958. The scrim is 1440x960 — the section, not
 * the page — so this covers the section and the document goes on scrolling
 * behind it, which is how it is drawn.
 * ---------------------------------------------------------------------- */

.campaigns__overlay {
	/*
	 * Fixed to the window, not laid over the section. The section is a fixed
	 * 945 tall whatever the window is, so an overlay pinned to it put the foot
	 * of a tall pop-in at 900 — below the fold on any laptop, where the awards
	 * were simply cut off with no way to reach them.
	 *
	 * This is also the more faithful reading of the frames. Their scrim is
	 * 1440x960, which is the whole drawing including the header, not a section
	 * that happens to be the same size — so covering the window covers what
	 * they cover, the dimmed header included.
	 */
	position: fixed;
	inset: 0;
	z-index: 40;
	display: grid;
	/*
	 * One row, definite. Left to size itself the row takes its height from the
	 * pop-in, and a percentage max-height on the pop-in then has nothing to
	 * measure against and resolves to none — which is why capping it did
	 * nothing until this was here.
	 */
	grid-template-rows: minmax(0, 1fr);
	/*
	 * Clear of the header at the head, and the same again at the foot. The
	 * reserve is symmetric so that a pop-in with room to spare sits centred on
	 * the window, exactly as the frame centres it on the section.
	 */
	padding: var(--hlp-header-h, 106px) 0;
	opacity: 0;
	visibility: hidden;
	transition:
		opacity 0.3s ease,
		visibility 0s linear 0.3s;
}

.campaigns.is-playing .campaigns__overlay {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s;
}

.campaigns__scrim {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.89);
}

/*
 * 1031 of 1280 wide, and centred down the section rather than hung from its
 * top. The frame puts the pop-in's top on 141, but that figure is only what
 * centring gives for the pop-in it draws: 960 less the 678 it is tall, halved,
 * is 141. Pinning the number instead of the rule was fine until the campaigns
 * stopped being the same height — a description that runs to three lines and
 * four lines of awards make CNOP 794 tall, which left it 141 from the top and
 * 10 from the bottom, sitting on the section's edge.
 *
 * Centred by auto margins rather than align-self, and the two are not the same
 * when the pop-in is taller than the room: auto margins resolve to zero and it
 * settles at the top of the reserve, where centring would carry its caption up
 * under the fixed header.
 *
 * Every pop-in shares the one cell, and the ones that are not open are taken
 * out of the layout rather than merely hidden. Hidden, they still sized the
 * row — so the row was as tall as the tallest of them and every pop-in was
 * centred inside CNOP's height instead of inside the section, which is why
 * Guerbet sat 164 from the top and 103 from the bottom.
 */
.campaigns__popin {
	position: relative;
	grid-area: 1 / 1;
	z-index: 1;
	justify-self: center;
	margin-block: auto;
	width: 80.547%;
	max-width: calc(var(--hlp-content-max) * 0.80547);
	/*
	 * Never taller than the room it has. What gives way is the film, below —
	 * the wording cannot be the thing that is cut, and the film is already
	 * letterboxing whatever it is handed.
	 */
	max-height: 100%;
	display: none;
}

.campaigns__popin.is-current {
	display: flex;
	flex-direction: column;
}

/*
 * The caption shares a 40px band with the close button — 141 to 181 in the
 * frame — and its own line sits 6 into it, at 147.3. The band is what sets the
 * film below, so the height is stated here and the film adds nothing.
 */
.campaigns__popin-title {
	margin: 0;
	padding: 6px 60px 0 0; /* The right keeps it clear of the close button. */
	min-height: 40px;
	font-size: 16px;
	font-weight: 700;
	line-height: 1.25;
	color: var(--hlp-white);
}

/* 40 square, red, its right edge on the pop-in's. */
.campaigns__close {
	position: absolute;
	top: 0;
	right: 0;
	width: 40px;
	height: 40px;
	display: grid;
	place-items: center;
	background: var(--hlp-red);
	color: var(--hlp-white);
	font-size: 24px;
	line-height: 1;
	transition: background-color 0.25s ease;
}

.campaigns__close:hover,
.campaigns__close:focus-visible {
	background: var(--hlp-white);
	color: var(--hlp-red);
}

/*
 * 1031 x 580. The grey is --hlp-placeholder, which is what the frame fills it
 * with: it is the film's ground while one loads, and the whole of it when a
 * campaign has none.
 */
.campaigns__screen {
	position: relative;
	/*
	 * 1031 x 580 given the room, and less when there is not: shrink-only, so
	 * it never grows past the frame's proportion but yields on a short window
	 * instead of pushing the description and the awards off the bottom. The
	 * film is fitted inside whatever shape is left, so it letterboxes rather
	 * than crops.
	 */
	flex: 0 1 auto;
	min-height: 0;
	aspect-ratio: 1031 / 580;
	display: grid;
	place-items: center;
	overflow: hidden;
	background: var(--hlp-placeholder);
}

.campaigns__video {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
	background: var(--hlp-black);
}

/* The frame's own stand-in for a film that has not been supplied. */
.campaigns__screen-label {
	font-size: 86px;
	line-height: 1;
	color: var(--hlp-black);
}

.campaigns__popin-text {
	margin-top: 14px; /* 775 of the frame, against a screen ending at 761. */
	color: var(--hlp-white);
}

.campaigns__popin-text p {
	margin: 0;
	font-size: 16px;
	line-height: 1.375; /* 22 on 16 */
}

/*
 * Awards, under the description. No frame draws them, so they are set to read
 * as a credit rather than as a second paragraph: at the description's own 16px
 * CNOP's four prize lines would outweigh the sentence they belong to.
 */
.campaigns__awards {
	margin: 14px 0 0;
	color: var(--hlp-white);
	font-size: 14px;
	line-height: 1.43; /* 20 on 14 */
	opacity: 0.8;
}

/* ----- A film taller than it is wide -----
 *
 * The frames draw one film box, 1031 x 580, because the films they were drawn
 * around are landscape. A portrait film in that box is a sliver down the
 * middle with two thirds of the width left black, which is what CNOP looked
 * like.
 *
 * So a portrait film gets the other half of the pop-in instead of a void: the
 * film down the left at its own shape, the wording beside it. The pop-in comes
 * out shorter than the landscape one rather than taller — 560 against 688 —
 * which is the opposite of what a portrait film usually costs a layout.
 *
 * Only portrait. A landscape film fills the frame's own box as drawn, and
 * splitting that into columns would only make it small. The shape is decided
 * in PHP from the media library, so this is right on the first paint; see
 * hlp_film_shape().
 * ---------------------------------------------------------------------- */

.campaigns__popin[data-film="portrait"].is-current {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	/*
	 * The wording is centred against the film rather than hung from the top of
	 * it: a description and a prize list are together much shorter than a
	 * portrait film, and left at the top they sit against its head with a hole
	 * under them. The two empty rows either side take the slack equally.
	 */
	grid-template-rows: auto 1fr auto auto 1fr;
	grid-template-areas:
		"title  title"
		"screen ."
		"screen text"
		"screen awards"
		"screen .";
	column-gap: 46px;
}

.campaigns__popin[data-film="portrait"] .campaigns__popin-title {
	grid-area: title;
}

.campaigns__popin[data-film="portrait"] .campaigns__screen {
	grid-area: screen;
	/*
	 * The film's own ratio, written onto the pop-in by PHP. Sized from its
	 * height, because the width follows from that — and tied to the window
	 * rather than to the column beside it, since the wording is short and a
	 * film matched to it would be a postage stamp.
	 */
	aspect-ratio: var(--film-ratio);
	height: min(560px, 56vh);
	width: auto;
}

.campaigns__popin[data-film="portrait"] .campaigns__popin-text {
	grid-area: text;
	/* It sits beside the film now, not under it. */
	margin-top: 0;
	align-self: start;
}

.campaigns__popin[data-film="portrait"] .campaigns__awards {
	grid-area: awards;
	align-self: start;
}

/*
 * No room for two columns on a phone, so the film goes back on top — at its
 * own shape still, and capped so the wording under it is not pushed off.
 */
@media (max-width: 768px) {
	.campaigns__popin[data-film="portrait"].is-current {
		display: flex;
		flex-direction: column;
	}

	.campaigns__popin[data-film="portrait"] .campaigns__screen {
		height: min(420px, 46vh);
		align-self: center;
	}

	.campaigns__popin[data-film="portrait"] .campaigns__popin-text {
		margin-top: 14px;
	}
}

/*
 * Below 1024 there is no frame. The deck keeps its arrangement — the two cards
 * behind are the composition and cannot be dropped — but the front card takes
 * more of the width and they are pushed further out, so they still show at the
 * edges instead of being swallowed. They bleed past the content column; the
 * section clips them.
 */
@media (max-width: 1024px) {
	.campaigns {
		padding-top: max(90px, var(--hlp-header-h));
		padding-bottom: 90px;
	}

	.campaigns__title {
		margin-left: 0;
		font-size: 30px;
	}

	.campaigns__controls {
		top: 4px;
		left: auto;
		right: 0;
	}

	.campaigns__deck {
		margin-top: 32px;
		width: 74%;
	}

	.campaigns__slide[data-offset="-1"] {
		transform: translateX(-58%) scale(0.44);
	}

	.campaigns__slide[data-offset="1"] {
		transform: translateX(58%) scale(0.44);
	}

	.campaigns__name {
		font-size: 20px;
	}

	.campaigns__tagline {
		font-size: 14px;
	}

	.campaigns__cta {
		padding: 8px 16px;
		font-size: 16px;
	}

	.campaigns__popin {
		width: 100%;
		max-width: none;
	}

	.campaigns__screen-label {
		font-size: 56px;
	}
}

/* -------------------------------------------------------------------------
 * Section 04, mobile — frame 1:2427, 430 x 2083
 *
 * Not the desktop deck narrowed: the frame throws the carousel away. There is
 * no deck, no arrows, no dashes and no red tone — the three campaigns are
 * simply stacked down the page, and the tone only ever meant "this card is not
 * the one in front", which stops being a thing that needs saying once they are
 * all in front.
 *
 * The card turns inside out with it. On desktop the name, the baseline and the
 * picture are one object, wording laid over photograph in white; here the
 * wording comes first in black on the page, the description follows — which
 * desktop keeps for the pop-in — and the photograph comes last, carrying
 * nothing but the red button.
 *
 * Insets are 15, not the 27 the header and most mobile blocks use. Section 03
 * does the same; both frames draw their content 400 wide in a 430 page.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.campaigns {
		/* Headline at 117, last card ending 51 off the foot of the frame. */
		padding-top: max(117px, var(--hlp-header-h));
		padding-bottom: 51px;
	}

	.campaigns__inner {
		padding-inline: 15px;
	}

	/*
	 * Centred here, unlike desktop, where frame 1:1640 starts the headline on
	 * the second column rule. Mobile frame 1:2427 centres both lines in the
	 * 430, so the indent comes off and the alignment changes with it.
	 */
	.campaigns__title {
		margin-left: 0;
		font-size: 32px;
		line-height: 1;
		text-align: center;
	}

	/* No carousel, so nothing to drive it with. */
	.campaigns__controls,
	.campaigns__dots {
		display: none;
	}

	/*
	 * The deck becomes the list: one column, 50 between cards, starting 38
	 * under the headline.
	 */
	.campaigns__deck {
		margin: 38px 0 0;
		width: 100%;
		display: flex;
		flex-direction: column;
		gap: 50px;
	}

	/*
	 * Every card is in front now, so the placement, the stacking and the
	 * transform all come off — whatever data-offset each one is carrying.
	 */
	.campaigns__slide,
	.campaigns__slide[data-offset="-1"],
	.campaigns__slide[data-offset="0"],
	.campaigns__slide[data-offset="1"] {
		z-index: auto;
		opacity: 1;
		visibility: visible;
		transform: none;
		transition: none;
	}

	/* 15 between the wording, the description and the picture. */
	.campaigns__slide {
		display: flex;
		flex-direction: column;
		gap: 15px;
	}

	/* Out of the picture and onto the page, in black. */
	.campaigns__caption {
		position: static;
		gap: 5px;
		color: var(--hlp-black);
	}

	/*
	 * Back to full size. The 1024 rules above shrink the wording because it is
	 * competing with a photograph behind it; here it is set on the page and the
	 * frame gives it the same 25 and 16 as desktop.
	 */
	.campaigns__name {
		font-size: 25px;
	}

	.campaigns__tagline {
		font-size: 16px;
	}

	.campaigns__text {
		display: block;
	}

	.campaigns__text p {
		margin: 0;
		font-size: 16px;
		line-height: 1.4;
	}

	.campaigns__text p + p {
		margin-top: 1em;
	}

	/* 400 x 375 — squarer than the 769 x 493 of the desktop card. */
	.campaigns__frame {
		aspect-ratio: 400 / 375;
	}

	/*
	 * The red tone says "behind"; with nothing behind, it has nothing to say.
	 * Hidden rather than zeroed so it cannot be animated back in.
	 */
	.campaigns__tint {
		display: none;
	}

	/*
	 * Nothing to bring forward either. Hidden as well as removed by the
	 * script, so a card the script never reached cannot swallow a tap meant
	 * for the button under it.
	 */
	.campaigns__pick {
		display: none;
	}

	/*
	 * PHP writes data-offset at every width — it is what keeps the deck from
	 * being empty before the script runs — so the desktop rule that takes the
	 * button away from a card standing behind is still matching here, on the
	 * two cards that happen to have been dealt offsets 1 and -1. Stacked there
	 * is no behind, and every button is the one on its own card.
	 */
	.campaigns__slide:not([data-offset="0"]) .campaigns__cta {
		pointer-events: auto;
	}

	/* Centred on the picture, 20 off its foot — and at its desktop size. */
	.campaigns__cta {
		top: auto;
		bottom: 20px;
		padding: 10px 20px;
		font-size: 20px;
	}

	.campaigns__popin {
		width: calc(100% - 30px);
		max-width: none;
	}

	.campaigns__popin-title {
		font-size: 14px;
	}

	.campaigns__screen-label {
		font-size: 34px;
	}
}

/*
 * The turn and the fade are motion; the red is not — it is what tells a card
 * at the back from the one in front, so it stays and only stops being
 * animated. The same reading as section 02, where reduced motion keeps the
 * carousel and drops the slide.
 */
@media (prefers-reduced-motion: reduce) {
	.campaigns__slide,
	.campaigns__tint,
	.campaigns__cta,
	.campaigns__arrow,
	.campaigns__dot,
	.campaigns__overlay,
	.campaigns__close {
		transition-duration: 0.01ms;
		transition-delay: 0s;
	}
}

/* -------------------------------------------------------------------------
 * The deck's entrance
 *
 * Written against the component rather than against section 04, because the
 * deck is the same object in 04 and 05 and is built by the same initDeck.
 * Only the pictures move. The headline, the arrows and the dashes are there
 * from the moment the section is — they were animated too and it was too much
 * happening at once; the deck is what the section is for, and it holds the eye
 * better as the only thing arriving.
 *
 * main.js puts .deck-card on those pictures and deals each one its path and
 * its turn. .is-revealing arms the closed state, .is-revealed releases it and
 * .is-leaving plays it out, exactly as in sections 02 and 03. All of it comes
 * only from main.js, so without JavaScript the deck is simply there.
 *
 * The movement is written with `translate`, never `transform`. The deck holds
 * each card's place in `transform` — the two behind are pushed out 54.6% and
 * scaled to 0.4694 — and `translate` is applied before it rather than
 * replacing it, so the entrance can say "and 62vw further right" without
 * having to restate the arrangement or know which card it is talking about.
 * ---------------------------------------------------------------------- */

/*
 * The cards, and only the three that are in play. A deck holds more cards than
 * it has places — section 05 has four — and the spare is parked at opacity 0
 * behind the others by the deck's own placement rules. The entrance must not
 * touch it: its animation fills both ways, so it would hold the spare at
 * opacity 1 after it ended and at the closed offset before it began, in each
 * case overruling the rule that put it away. Naming the three places keeps the
 * entrance to the cards that have one.
 */
.is-revealing .deck-card[data-offset="-1"],
.is-revealing .deck-card[data-offset="0"],
.is-revealing .deck-card[data-offset="1"],
.is-revealing .deck-card--fades {
	translate: var(--from-x, 0) var(--from-y, 0);
	rotate: var(--from-r, 0deg);
	scale: var(--from-s, 1);
	/*
	 * Hidden here, not just in the keyframes. The animation's backwards fill
	 * only starts applying once the card has been released, so a closed state
	 * that merely offsets the card leaves it in plain view — settled, because
	 * the offsets are not dealt until release either — for the whole of the
	 * scroll down to it. What that looks like is the finished carousel
	 * blinking out and rebuilding itself, which is worse than no entrance.
	 */
	opacity: 0;
}

/*
 * Two ways in, one animation, as in section 06. .is-revealed releases the deck
 * at once — the three cards in turn; .is-in releases a single card, which is
 * how a deck too tall to be watched at once arrives, one card at a time as the
 * reader reaches it. A card never gets both.
 */
.is-revealing.is-revealed .deck-card[data-offset="-1"],
.is-revealing.is-revealed .deck-card[data-offset="0"],
.is-revealing.is-revealed .deck-card[data-offset="1"],
.is-revealing .deck-card.is-in {
	animation: hlp-deck-card-in 1.5s cubic-bezier(0.32, 0.72, 0.28, 1)
		var(--delay, 0ms) both;
}

/* Leaving is quicker than arriving, and runs the other way round. */
.is-revealing.is-leaving .deck-card[data-offset="-1"],
.is-revealing.is-leaving .deck-card[data-offset="0"],
.is-revealing.is-leaving .deck-card[data-offset="1"] {
	animation: hlp-deck-card-out 560ms cubic-bezier(0.55, 0, 1, 0.45)
		var(--exit-delay, 0ms) both;
}



/*
 * The card that is in front comes up from below; the two behind come in from
 * the side they sit on, so the deck assembles outwards instead of arriving as
 * a block. main.js writes both distances, reading the arrangement as it
 * stands — the deck may have been turned since it was last seen.
 *
 * The fade comes free with the delay. The animation fills backwards, so a card
 * waiting its turn is already holding this first keyframe — opacity 0, and
 * offset — which is why a card does not sit visibly displaced while it waits.
 */
@keyframes hlp-deck-card-in {
	from {
		opacity: 0;
		translate: var(--from-x, 0) var(--from-y, 0);
		rotate: var(--from-r, 0deg);
		scale: var(--from-s, 1);
	}

	/*
	 * The top of the arc. A card dealt in from the left is by now well inside
	 * the section and riding above the row, not two thirds of the way along a
	 * straight line to it — that is the difference between a card being laid
	 * down and a panel sliding in.
	 */
	40% {
		opacity: 1;
		translate: var(--arc-x, 0) var(--arc-y, 0);
		rotate: calc(var(--from-r, 0deg) * 0.3);
		scale: 0.985;
	}

	/* Past the middle and dipping under it, turned back through level. */
	73% {
		translate: var(--over-x, 0) var(--over-y, 0);
		rotate: calc(var(--from-r, 0deg) * -0.16);
		scale: 1.035;
	}

	to {
		opacity: 1;
		translate: none;
		rotate: none;
		scale: 1;
	}
}

/*
 * The card goes back out the way it came, and takes its opacity with it. The
 * fade has to be stated here: the closed state holds these cards at opacity 0,
 * so without it the card would be gone on the first frame of the exit and the
 * movement would play on nothing — which is exactly what it did.
 */
@keyframes hlp-deck-card-out {
	from {
		opacity: 1;
		translate: none;
		rotate: none;
		scale: 1;
	}

	to {
		opacity: 0;
		translate: calc(var(--from-x, 0px) * 0.4) calc(var(--from-y, 0px) * 0.55);
		rotate: calc(var(--from-r, 0deg) * 0.5);
		scale: 0.88;
	}
}

/*
 * Reduced motion keeps the arrival and drops the travel: the order the deck
 * assembles in is information about how it is put together, the 80px is not.
 */
@media (prefers-reduced-motion: reduce) {
	/*
	 * The closed state's offset has to go with the travel. It is a plain
	 * declaration rather than a keyframe, so an animation that only touches
	 * opacity never puts it back, and the cards would sit where they started
	 * for good.
	 */
	/*
	 * Spelled out to the same weight as the closed state it is undoing. That
	 * state names the three offsets, so a shorter selector here loses to it
	 * and the cards keep the pose they were meant to travel out of — sitting
	 * 92px low for good, which is what happened.
	 */
	.is-revealing .deck-card[data-offset="-1"],
	.is-revealing .deck-card[data-offset="0"],
	.is-revealing .deck-card[data-offset="1"],
	.is-revealing .deck-card--fades {
		translate: none;
		rotate: none;
		scale: 1;
	}

	.is-revealing.is-revealed .deck-card[data-offset="-1"],
	.is-revealing.is-revealed .deck-card[data-offset="0"],
	.is-revealing.is-revealed .deck-card[data-offset="1"],
	.is-revealing .deck-card.is-in {
		animation: hlp-deck-fade-in 0.3s ease var(--delay, 0ms) both;
	}

	.is-revealing.is-leaving .deck-card[data-offset="-1"],
	.is-revealing.is-leaving .deck-card[data-offset="0"],
	.is-revealing.is-leaving .deck-card[data-offset="1"] {
		animation: hlp-deck-fade-out 0.3s ease var(--exit-delay, 0ms) both;
	}
}

@keyframes hlp-deck-fade-in {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

@keyframes hlp-deck-fade-out {
	from {
		opacity: 1;
	}

	to {
		opacity: 0;
	}
}

/* =========================================================================
 * Section 05 — les engagements
 *
 * Figma frame 1:411 "5 (ENGAGEMENTS)" (1440x960). Structurally the same
 * carousel as section 04 — a deck of cards with a red-tint behind state,
 * prev/next arrows, pagination dashes and a pop-in overlay — so the CSS is
 * the same arrangement under a different name.
 * ======================================================================= */

.commitments {
	position: relative;
	padding-top: max(130px, var(--hlp-header-h));
	padding-bottom: 179px;
	overflow: hidden;
}

/*
 * An engagement may carry a film to play on its own picture. It covers the
 * artwork exactly, lies over the tint and under the red button — which stays
 * in front, as the frames draw it — and fades in while the pointer rests on
 * the card. At rest the picture is what the card shows, so the film is
 * decoration: out of the pointer's way and hidden from assistive software.
 */
.commitments__player {
	position: absolute;
	inset: 0;
	z-index: 2;
	opacity: 0;
	/*
	 * Hidden as well as clear, so a bar nobody can see cannot be tabbed into
	 * or clicked either. Visibility waits for the fade on the way out and
	 * lands at once on the way in.
	 */
	visibility: hidden;
	transition: opacity 0.4s ease, visibility 0s 0.4s;
}

.commitments__player.is-showing {
	opacity: 1;
	visibility: visible;
	transition: opacity 0.4s ease, visibility 0s;
}

.commitments__film {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Shown as the picture under it is: a card that fits its artwork fits the film. */
.commitments__frame--fit .commitments__film {
	object-fit: contain;
}

/*
 * The bar sits where the partial puts it, along the foot of the film. An
 * engagement whose artwork needs its red button lifted clear of it says so per
 * entry — see --commitment-cta-lift.
 */
.commitments__player .video-controls {
	padding: 24px 12px 8px;
}



.commitments__inner {
	position: relative;
	z-index: 1;
}

.commitments__title {
	margin: 0 0 0 19.844%;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-transform: uppercase;
}

.commitments__controls {
	position: absolute;
	top: 15px;
	left: 72.266%;
	display: flex;
	gap: 24px;
}

.commitments__arrow {
	width: 38px;
	height: 38px;
	color: var(--hlp-black);
	background-color: currentColor;
	transition: color 0.25s ease;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
}

.commitments__arrow--prev {
	-webkit-mask-image: url("../images/icon-slider-prev.svg");
	mask-image: url("../images/icon-slider-prev.svg");
}

.commitments__arrow--next {
	-webkit-mask-image: url("../images/icon-slider-next.svg");
	mask-image: url("../images/icon-slider-next.svg");
}

.commitments__arrow:hover,
.commitments__arrow:focus-visible {
	color: var(--hlp-red);
}

/* ----- The deck ----- */

.commitments__deck {
	margin: 39px auto 0;
	width: 60.078%;
	display: grid;
}

.commitments__slide {
	grid-area: 1 / 1;
	position: relative;
	z-index: 0;
	opacity: 0;
	visibility: hidden;
	transform: scale(0.42);
	transition:
		transform 0.6s ease,
		opacity 0.6s ease,
		visibility 0s linear 0.6s;
}

.commitments__slide[data-offset="-1"],
.commitments__slide[data-offset="0"],
.commitments__slide[data-offset="1"] {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s, 0s;
}

.commitments__slide[data-offset="-1"] {
	z-index: 1;
	transform: translateX(-54.616%) scale(0.4694);
}

.commitments__slide[data-offset="1"] {
	z-index: 1;
	transform: translateX(54.616%) scale(0.4694);
}

.commitments__slide[data-offset="0"] {
	z-index: 2;
	transform: none;
}

/* ----- The card ----- */

.commitments__frame {
	position: relative;
	aspect-ratio: 769 / 493;
	overflow: hidden;
	isolation: isolate;
	background: var(--commitment-backdrop, var(--hlp-placeholder));
}

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

/*
 * Artwork that sets its wording to its own edges is fitted rather than filled,
 * so no width is lost. Here at 769/493 the two are the same instruction — that
 * is the ratio the artwork is drawn at, so neither crops — and they part only
 * at the mobile 400/375 below, where fitting leaves a band above and below.
 * --commitment-backdrop, carried by the frame, paints those bands in the
 * artwork's own ground so the join does not read.
 */
.commitments__frame--fit .commitments__image {
	object-fit: contain;
}

.commitments__tint {
	position: absolute;
	inset: 0;
	z-index: 1;
	background: var(--hlp-red);
	mix-blend-mode: color;
	opacity: 1;
	pointer-events: none;
	transition: opacity 0.6s ease;
}

.commitments__slide[data-offset="0"] .commitments__tint {
	opacity: 0;
}

.commitments__caption {
	position: absolute;
	z-index: 2;
	left: 4.031%;
	top: 5.882%;
	right: 4.031%;
	display: flex;
	flex-direction: column;
	gap: 5px;
	color: var(--hlp-white);
}

.commitments__caption--dark {
	color: var(--hlp-black);
}

.commitments__name {
	margin: 0;
	font-size: 25px;
	font-weight: 700;
	line-height: 1;
	text-transform: uppercase;
}

.commitments__tagline {
	margin: 0;
	font-size: 16px;
	font-weight: 700;
	line-height: 1;
}

.commitments__text {
	display: none;
}

/*
 * A button on the engagements that open their film, an anchor on the two that
 * leave for a site of their own, so the underline an anchor brings with it is
 * taken off here rather than in either branch of the template.
 */
.commitments__cta {
	position: absolute;
	z-index: 3;
	left: 50%;
	transform: translateX(-50%);
	/* The frames' place for it, less whatever this card's artwork asks back. */
	top: calc(84.787% - var(--commitment-cta-lift, 0px));
	padding: 10px 20px;
	background: var(--hlp-red);
	color: var(--hlp-white);
	font-size: 20px;
	line-height: 1.25;
	white-space: nowrap;
	text-decoration: none;
	transition: background-color 0.25s ease;
}

.commitments__cta:hover,
.commitments__cta:focus-visible {
	background: var(--hlp-black);
}

.commitments__slide:not([data-offset="0"]) .commitments__cta {
	pointer-events: none;
}

.commitments__pick {
	position: absolute;
	inset: 0;
	z-index: 4;
}

/* ----- Dashes ----- */

.commitments__dots {
	margin-top: 28px;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
}

.commitments__dot {
	width: 11px;
	height: 1px;
	background: var(--hlp-white);
	border: 1px solid var(--hlp-black);
	transition: width 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}

.commitments__dot[aria-current="true"] {
	width: 22px;
	background: var(--hlp-red);
	border-color: var(--hlp-red);
}

/* ----- The film, open ----- */

.commitments__overlay {
	position: absolute;
	inset: 0;
	z-index: 5;
	display: grid;
	opacity: 0;
	visibility: hidden;
	transition:
		opacity 0.3s ease,
		visibility 0s linear 0.3s;
}

.commitments.is-playing .commitments__overlay {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s, 0s;
}

.commitments__scrim {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.89);
}

.commitments__popin {
	position: relative;
	grid-area: 1 / 1;
	z-index: 1;
	align-self: start;
	justify-self: center;
	margin-top: 141px;
	width: 80.547%;
	max-width: calc(var(--hlp-content-max) * 0.80547);
	visibility: hidden;
}

.commitments__popin.is-current {
	visibility: visible;
}

.commitments__popin-title {
	margin: 0;
	padding: 6px 60px 0 0;
	min-height: 40px;
	font-size: 16px;
	font-weight: 700;
	line-height: 1.25;
	color: var(--hlp-white);
}

.commitments__close {
	position: absolute;
	top: 0;
	right: 0;
	width: 40px;
	height: 40px;
	display: grid;
	place-items: center;
	background: var(--hlp-red);
	color: var(--hlp-white);
	font-size: 24px;
	line-height: 1;
	transition: background-color 0.25s ease;
}

.commitments__close:hover,
.commitments__close:focus-visible {
	background: var(--hlp-white);
	color: var(--hlp-red);
}

.commitments__screen {
	position: relative;
	aspect-ratio: 1031 / 580;
	display: grid;
	place-items: center;
	overflow: hidden;
	background: var(--hlp-placeholder);
}

.commitments__video {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
	background: var(--hlp-black);
}

.commitments__screen-label {
	font-size: 86px;
	line-height: 1;
	color: var(--hlp-black);
}

.commitments__popin-text {
	margin-top: 14px;
	color: var(--hlp-white);
}

.commitments__popin-text p {
	margin: 0;
	font-size: 16px;
	line-height: 1.375;
}

/* ----- Tablet ----- */

@media (max-width: 1024px) {
	.commitments {
		padding-top: max(90px, var(--hlp-header-h));
		padding-bottom: 90px;
	}

	.commitments__title {
		margin-left: 0;
		font-size: 30px;
	}

	.commitments__controls {
		top: 4px;
		left: auto;
		right: 0;
	}

	.commitments__deck {
		margin-top: 32px;
		width: 74%;
	}

	.commitments__slide[data-offset="-1"] {
		transform: translateX(-58%) scale(0.44);
	}

	.commitments__slide[data-offset="1"] {
		transform: translateX(58%) scale(0.44);
	}

	.commitments__name {
		font-size: 20px;
	}

	.commitments__tagline {
		font-size: 14px;
	}

	.commitments__cta {
		padding: 8px 16px;
		font-size: 16px;
	}

	.commitments__popin {
		margin-top: 60px;
		width: 100%;
		max-width: none;
	}

	.commitments__screen-label {
		font-size: 56px;
	}
}

/* -------------------------------------------------------------------------
 * Section 05, mobile — frame 1:2481, 430 x 2698
 *
 * Same turn as campaigns mobile: the carousel goes, the four engagements
 * stack down the page. The card flips inside out — the wording comes first
 * in black, the description follows (which desktop keeps for the pop-in),
 * and the photograph comes last carrying only the red CTA.
 *
 * Insets are 15, matching campaigns mobile and section 03.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	/*
	 * 117 to the headline and the last card closing at 2604 of the frame's
	 * 2698, which is the 94 below it.
	 */
	.commitments {
		padding-top: max(117px, var(--hlp-header-h));
		padding-bottom: 94px;
	}

	.commitments__inner {
		padding-inline: 15px;
	}

	/*
	 * Node 1:2486 sets the headline on one 32px line, where desktop breaks it
	 * in two. The break is the registry's, so it is only hidden here: the
	 * newline behind the <br> is then an ordinary space.
	 */
	.commitments__title {
		margin-left: 0;
		font-size: 32px;
		line-height: 1;
		text-align: center;
	}

	.commitments__title br {
		display: none;
	}

	.commitments__controls,
	.commitments__dots {
		display: none;
	}

	/* 117, the headline's 32, and 70 more put the first card on the frame's 219. */
	.commitments__deck {
		margin: 70px 0 0;
		width: 100%;
		display: flex;
		flex-direction: column;
		gap: 50px;
	}

	.commitments__slide,
	.commitments__slide[data-offset="-1"],
	.commitments__slide[data-offset="0"],
	.commitments__slide[data-offset="1"] {
		z-index: auto;
		opacity: 1;
		visibility: visible;
		transform: none;
		transition: none;
	}

	.commitments__slide {
		display: flex;
		flex-direction: column;
		gap: 15px;
	}

	.commitments__caption {
		position: static;
		gap: 5px;
		color: var(--hlp-black);
	}

	/*
	 * The frame sets the name and the tagline at Baikal's normal line height,
	 * which Figma draws 31 and 20 tall. Written out rather than left to
	 * `normal`, which each browser reads off the font's metrics its own way.
	 */
	.commitments__name {
		font-size: 25px;
		line-height: 1.24;
	}

	.commitments__tagline {
		font-size: 16px;
		line-height: 1.25;
	}

	.commitments__text {
		display: block;
	}

	/*
	 * 16/22, as the frame has it. The frame draws this text in Inter, which
	 * reads as Figma's default rather than a choice — the same call section
	 * 04's pop-in made. Baikal runs a little narrower, so the fourth card's
	 * description takes four lines where the frame's takes five.
	 */
	.commitments__text p {
		margin: 0;
		font-size: 16px;
		line-height: 22px;
	}

	.commitments__text p + p {
		margin-top: 1em;
	}

	/* Frame 11524, the mobile artwork frame, is 400 x 375 in all four cards. */
	.commitments__frame {
		aspect-ratio: 400 / 375;
	}

	.commitments__tint {
		display: none;
	}

	.commitments__pick {
		display: none;
	}

	.commitments__slide:not([data-offset="0"]) .commitments__cta {
		pointer-events: auto;
	}

	.commitments__cta {
		top: auto;
		bottom: calc(20px + var(--commitment-cta-lift, 0px));
		padding: 10px 20px;
		font-size: 20px;
	}

	.commitments__overlay {
		position: fixed;
		z-index: 40;
		place-items: center;
	}

	.commitments__popin {
		margin-top: 0;
		align-self: center;
		width: calc(100% - 30px);
		max-width: none;
	}

	.commitments__popin-title {
		font-size: 14px;
	}

	.commitments__screen-label {
		font-size: 34px;
	}
}

@media (prefers-reduced-motion: reduce) {
	.commitments__slide,
	.commitments__tint,
	.commitments__cta,
	.commitments__arrow,
	.commitments__dot,
	.commitments__overlay,
	.commitments__close {
		transition-duration: 0.01ms;
		transition-delay: 0s;
	}
}

/* =========================================================================
 * Section 06 — les clients
 *
 * Figma frame 1:517 "6 (CLIENTS)" (1440x960). Twenty-five logos hung on the
 * site's own five-column grid: a cell is one 256px column, so the logos line
 * up on the decorative rules without a measurement of their own — the footing
 * section 03 builds its cards on. Rows are 150 tall, which is also the square
 * the frame draws twenty of the twenty-five logos in.
 * ======================================================================= */

/*
 * --v is one of the frame's vertical pixels, as in section 03, and for the
 * same reason: the frame is 960 tall and means to be one screenful, but its
 * figures are fixed while a laptop window is routinely 800 or 900. At 916 the
 * headline sat 170 down a section that wanted 960, and the last row of logos —
 * Roche to Urgo — went under the fold and stayed there. Every vertical figure
 * in this section is the frame's own number times --v, so a shorter window
 * contracts the whole rhythm together instead of pushing the foot off the end.
 *
 * The divisor is 939 rather than 960, and the 40 comes off the top, because
 * the headline is the one thing here that does not contract: 38px of Baikal
 * Cond on 1.05 is a 40px line box at every window size, since shrinking the
 * type to fit is a worse answer than closing the space around it. What is left
 * to share out is 939 — 170 above the headline, 12 of air under it, 750 of
 * rows and 7 below. So the section comes to 939 * --v + 40, which is the
 * window exactly wherever --v is free to move.
 *
 * At 960 and above it resolves to 1px and the section is the frame exactly.
 * The floor holds the rows at 108 rather than let a very short window squeeze
 * the logos to nothing; below roughly 700px tall the section scrolls again,
 * which is the lesser evil and what section 03 settles for too.
 *
 * Horizontal measurements are untouched. The logos stay one 256px column wide
 * and keep landing on the decorative rules, which is the part of this
 * composition that has to survive.
 */
.clients {
	--v: clamp(0.72px, calc((100svh - 40px) / 939), 1px);

	position: relative;
	/* 170 down to the headline; the grid closes at 953 of the frame's 960. */
	padding-top: max(calc(170 * var(--v)), var(--hlp-header-h));
	padding-bottom: calc(7 * var(--v));
	/*
	 * The entrance starts every logo 96px off its cell in a direction of its
	 * own, and a transform still counts towards what the page can be scrolled
	 * to. Unclipped, the logos on the outside of the grid reach past the
	 * section on all four sides while the wave runs: the page grows by most of
	 * that 96 and then shrinks back as they land, which moves the bottom of
	 * the document out from under a reader who is standing on it. The grid
	 * closes 7px off the foot of the section, so there is nowhere for that
	 * bleed to go.
	 *
	 * Same clip and same reason as section 04, where the two cards behind the
	 * deck reach past the content column by design.
	 */
	overflow: hidden;
}

.clients__inner {
	position: relative;
}

.clients__title {
	margin: 0;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-align: center;
	text-transform: uppercase;
}

.clients__grid {
	/*
	 * The frame opens the grid band at 203 and closes the headline's box at
	 * 210, so it overlaps the headline by seven pixels. That works in Figma,
	 * where a logo is a mark on nothing and the seven pixels fall in the air
	 * around it. It cannot work here: every one of the twenty-five exports is
	 * flattened onto white, so a cell is not a mark with space around it but
	 * an opaque white rectangle, and the seven pixels were painted straight
	 * over the bottom of NOS CLIENTS.
	 *
	 * Nor is closing the overlap to nothing enough. The headline is set on
	 * 1.05, which is tighter than the font's own ascent and descent, so the
	 * capitals hang a little below the line box that is supposed to hold them
	 * — a grid flush to that box would still shave them. So the seven pixels
	 * of overlap become twelve of clearance, and the section is a little
	 * taller than the frame for it, which --v then gives back.
	 */
	margin: calc(12 * var(--v)) 0 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	grid-auto-rows: calc(150 * var(--v));
}

.clients__item {
	display: flex;
	align-items: center;
	justify-content: center;
	/* Keeps a wide wordmark off the column rule its cell is drawn against. */
	padding-inline: 12px;
}

/*
 * The width and height attributes carry the size the frame draws each logo at,
 * so the grid stands at its full height before a single PNG has landed and
 * nothing shifts underneath the entrance. max-width lets the same figures give
 * way on a narrower cell rather than overflow it.
 */
.clients__logo {
	max-width: 100%;
	/*
	 * Both axes, because an uploaded logo arrives at its own size: the shipped
	 * marks are all drawn inside their 150px row, but a tall upload would keep
	 * its width and overflow the row without the height cap. contain keeps
	 * whatever is capped in proportion.
	 */
	max-height: 100%;
	height: auto;
	object-fit: contain;
}

/* ----- Entrance -----
 *
 * The logos are asked to arrive rather than to be there, each sliding in from
 * a direction of its own and fading up as it comes, with the wave leaving the
 * middle of the grid and gathering pace as it spreads to the edges.
 *
 * The figures come from main.js, which measures the grid as it is actually
 * laid out — the five columns of the desktop frame or the two of the phone, it
 * never has to be told which. --from-x and --from-y are the offset the logo
 * starts at, drawn at 96px on a circle so any direction is available at one
 * speed; --delay is the logo's place in the wave, plus a scatter of its own so
 * no two leave together. The hold before any of it breaks is not here — it is
 * a pause in main.js, because the grid is measured when it ends.
 *
 * The duration is stated here rather than passed per logo, because it is the
 * same for every logo and has to stay that way: paired with the fixed 96px it
 * is what makes all twenty-five move at one speed, so the wave is the only
 * thing that accelerates. The easing overshoots very slightly, so a logo
 * settles onto its cell instead of stopping dead.
 *
 * .is-revealing arms the closed state and .is-revealed releases it, as in
 * sections 02 and 03. Both come only from main.js, so without JavaScript the
 * clients are simply there.
 */

.clients.is-revealing .clients__item {
	opacity: 0;
}

/*
 * Two ways in, one animation. .is-revealed releases the whole grid at once,
 * which is the wave; .is-in releases a single logo, which is how a grid too
 * tall to be watched at once arrives — band by band, as the reader reaches
 * it. main.js decides which, and a logo never gets both.
 */
.clients.is-revealing.is-revealed .clients__item,
.clients.is-revealing .clients__item.is-in {
	animation: hlp-client-in 720ms cubic-bezier(0.2, 1.1, 0.3, 1)
		var(--delay, 0ms) both;
}

@keyframes hlp-client-in {
	from {
		opacity: 0;
		transform: translate(var(--from-x, 0px), var(--from-y, 0px));
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* Reduced-motion stand-in: the order of the wave survives, the travel does not. */
@keyframes hlp-client-fade {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

/*
 * Between 769 and 1024 there is no frame. Five columns are the composition —
 * the logos are what line the grid up — so they stay, and it is the cell that
 * gives way instead: 173px at a 1024 window, still room for the logo at full
 * size, and 122 by 769, where it starts scaling down with the cell.
 *
 * The gutter is deliberately left alone. Widening the content column by
 * narrowing it would buy the logos about 40px and cost them the thing they are
 * here for — the column rules are drawn on the gutter, and a section that sets
 * its own stops landing on them.
 */
@media (max-width: 1024px) {
	/*
	 * Fitting the section to the window stops here, as it does for section 03.
	 * Three of the five columns' worth of width has gone and the rows carry
	 * their own height instead, so --v goes back to a plain pixel and the
	 * figures below are read literally.
	 */
	.clients {
		--v: 1px;

		padding-top: max(120px, var(--hlp-header-h));
		padding-bottom: 60px;
	}

	.clients__title {
		font-size: 34px;
	}

	.clients__grid {
		margin-top: 30px;
		grid-auto-rows: 130px;
	}

	.clients__item {
		padding-inline: 8px;
	}
}

/* -------------------------------------------------------------------------
 * Section 06, mobile — frame 1:2543, 430 x 1670
 *
 * Two columns instead of five, the rows still 150 tall, and five of the
 * twenty-five withdrawn. The frame's figures come out exact: 117 down to the
 * headline, a 32 headline, 21 of air, then ten rows of 150 closing on 1670.
 * Everything above the foot of the grid is the frame's; what goes under it is
 * not, and is argued for where it is set.
 *
 * The five it drops are the five drawn as wide wordmarks rather than square
 * marks, and they are marked as such in clients.php. Twenty logos is ten
 * clean rows of two; twenty-five would leave a row half empty.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.clients {
		padding-top: max(117px, var(--hlp-header-h));
		/*
		 * The frame closes the grid flush on 1670 and has nothing under it,
		 * which is true of a board and false of a page. A board has no foot to
		 * the window: no scrollbar along the bottom, no address bar folding
		 * away and back. A section that ends flush with the end of the
		 * document has no tolerance for any of them, and section 06 is the
		 * last one built, so it is the document's end that it meets.
		 *
		 * Measured at 412x924 it cost the last row 30px, which is the height
		 * of a horizontal scrollbar: fixed chrome — the header, the overlay,
		 * the column rules — is laid out against the initial containing block,
		 * which still counts the 13px that scrollbar-gutter reserves, so it
		 * runs 13px wider than the content box and the page acquires a
		 * sideways scrollbar it never wanted. Servier and Urgo sat under it,
		 * and no amount of scrolling brought them out.
		 *
		 * 60px, which is what the other sections leave on this breakpoint.
		 * Under the last row it is white space and reads as nothing; between
		 * this section and 07 when that arrives, it is the gap those two want
		 * anyway.
		 */
		padding-bottom: 60px;
	}

	.clients__inner {
		padding-inline: 15px;
	}

	.clients__title {
		font-size: 32px;
		line-height: 1;
	}

	.clients__grid {
		margin-top: 21px;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		grid-auto-rows: 150px;
	}

	.clients__item {
		padding-inline: 12px;
	}

	.clients__item--wide {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	.clients.is-revealing.is-revealed .clients__item,
	.clients.is-revealing .clients__item.is-in {
		animation: hlp-client-fade 0.3s ease var(--delay, 0ms) both;
	}
}

/* =========================================================================
 * Section 07 — le manifeste
 *
 * Figma frame 1:646 "7 (MANIFESTO)" (1440x960). A portrait fills the section
 * under a quarter-black scrim, the intro's arc sweeps across it, and the
 * manifesto is set in white over the middle: the headline on the second column
 * rule, its second line red, then three paragraphs three columns wide.
 *
 * There is no grid here and nothing to place, so the only thing this has to
 * get right is where the text sits on a window that is not 960 tall.
 * ======================================================================= */

/*
 * --v is one of the frame's vertical pixels, as in sections 03 and 06 — but
 * only the empty half of the section is measured in it. The type is not: 38px
 * of Baikal Cond and 24px on 30 are the frame's sizes at every window height,
 * because a manifesto set smaller to fit is no longer the manifesto.
 *
 * So 425 of the frame's 960 is fixed — an 80px headline, 15 of air and 330 of
 * text — and the remaining 535 is the space above and below it, 308 and 227.
 * Sharing that 535 out against the window puts the block where the frame puts
 * it, in the middle of the picture, whatever the window is: at 960 --v is 1px
 * and the section is the frame exactly.
 *
 * The floor is 0.35 rather than section 06's 0.72: what contracts here is
 * white space over a photograph, not the composition, so it can give up almost
 * all of it before anything is lost. 0.35 is where the top space meets the
 * header's own height and the max() below takes over; under about 612px of
 * window the section scrolls, which is the lesser evil.
 */
.anniversary {
	--v: clamp(0.35px, calc((100svh - 425px) / 535), 1px);

	position: relative;
	/* 308 down to the headline; the text closes 227 off the foot of the frame. */
	padding-top: max(calc(308 * var(--v)), var(--hlp-header-h));
	padding-bottom: calc(227 * var(--v));
	/*
	 * The arc is 1732px wide and hangs 477px off the left edge, so at any
	 * window narrower than about 1730 its right end reaches past the page.
	 * Clipped here rather than shortened: the same answer sections 02, 04 and
	 * 06 reached, and the one the hero still owes.
	 */
	overflow: hidden;
	color: var(--hlp-white);
}

/*
 * Under the decorative column rules, like section 03's photograph: a negative
 * stacking order puts both the picture and the scrim below them while
 * .anniversary itself makes no stacking context, so the rules keep reading
 * across the section as the frame draws them.
 */
.anniversary__background,
.anniversary__scrim {
	position: absolute;
	inset: 0;
	z-index: -1;
}

.anniversary__background {
	overflow: hidden;
}

/*
 * The frame draws the photograph 1616 wide in a 1440 board — 12% wider than
 * the section, pushed 9px left — and takes a 1440x960 window out of it. A box
 * that is 112.22% wide and the section's own height, filled by object-fit,
 * is that crop exactly at the frame's size and stays a sensible one at every
 * other: cover keeps the picture filling the section, and the extra 12% keeps
 * the subject at the size it is drawn rather than 12% smaller.
 */
.anniversary__background img {
	position: absolute;
	top: 0;
	left: -0.65%;
	width: 112.22%;
	height: 100%;
	max-width: none;
	object-fit: cover;
}

/* The frame's quarter-black over the whole picture, arc excepted — the arc is
 * drawn above it, which is why this is its own layer rather than a tint on the
 * photograph. */
.anniversary__scrim {
	background: rgb(0 0 0 / 25%);
}

/*
 * The intro's arc, in white. hero-shape.svg is a single-colour SVG served as
 * <img>, which CSS cannot recolour, but every stroke in it is black and it has
 * no fill — so invert(1) lands on pure white and one file serves both sections.
 * The same turn the header already makes over a dark section.
 *
 * Placed as the frame places it: the shape's centre on 25% of the width plus
 * 29px, and on the section's own middle. Left of the section by 477px at the
 * frame's width, which is what the clip above is for.
 */
.anniversary__shape {
	position: absolute;
	top: 50%;
	left: calc(25% + 29px - 866px);
	width: 1732px;
	height: 1060px;
	transform: translateY(-50%);
	pointer-events: none;
}

.anniversary__shape img {
	width: 100%;
	height: 100%;
	filter: invert(1);
}

.anniversary__inner {
	position: relative;
}

/*
 * 336 of the frame is 256 into the 1280 content box — one column in, on the
 * second decorative rule, where sections 04 and 05 start their headlines too.
 * 769 wide is three of the five columns, closing on the fifth rule.
 */
.anniversary__title {
	margin: 0 0 0 20%;
	width: 60.078%;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-transform: uppercase;
}

.anniversary__accent {
	display: block;
	color: var(--hlp-red);
}

/*
 * The frame opens the text box at 403, 95 under the headline's — but that box
 * is 87 tall for two lines that set 80, so the air between them is 15, not the
 * 8 the two figures suggest. Same slack section 03 measures out of the same
 * headline.
 */
.anniversary__text {
	margin: 15px 0 0 20%;
	width: 60.078%;
	font-size: 24px;
	line-height: 30px;
}

.anniversary__text p {
	margin: 0;
}

/* The frame's blank line between paragraphs, which is one line of the text. */
.anniversary__text p + p {
	margin-top: 30px;
}

/* -------------------------------------------------------------------------
 * Section 07, mobile — frame 1:2604, 430 x 1312
 *
 * The same section rearranged rather than narrowed. The headline is centred
 * instead of indented, and it sets three lines at 400 wide where desktop sets
 * two. The text drops to the lower third, so the gesture is read before the
 * words are — which is why the gap between the two is a measurement here and
 * not the 15px of air desktop leaves.
 *
 * The blank lines between the paragraphs close up: the frame runs all three
 * together, one under the other. That is the margin below and not a second
 * string, so the copy stays one text in the string table.
 *
 * Insets are 15, not the 27 the header uses — sections 03 and 04 do the same,
 * and this frame draws its content 400 wide in a 430 page.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.anniversary {
		/* A fixed 1312, not a screenful: the picture is the section and the
		 * text is placed on it, so there is nothing here to fit to a window. */
		padding-top: max(117px, var(--hlp-header-h));
		/* The text box closes on 1200 of the frame's 1312. */
		padding-bottom: 112px;
	}

	/* The mobile crop is drawn to fill the frame, with none of desktop's 12%. */
	.anniversary__background img {
		left: 0;
		width: 100%;
	}

	.anniversary__inner {
		padding-inline: 15px;
	}

	.anniversary__shape {
		top: 203px;
		left: -1065px;
		transform: none;
	}

	.anniversary__title {
		margin-left: 0;
		width: auto;
		text-align: center;
	}

	/*
	 * The frame opens the text at 656, which is 419 below a headline that sets
	 * three lines from 117. Stated as the gap rather than as the frame's 656,
	 * so a headline the client shortens to two lines takes the text up with it
	 * instead of leaving a hole.
	 */
	/*
	 * 34px, not the 1.4 the frame reports. 1.4 of 24 is 33.6, and a text set
	 * on 33.6 lands its lines on alternating 33s and 34s — which is what this
	 * did, and it walked 6px up the frame over sixteen lines. The frame's own
	 * render puts every line exactly 34 below the last, fifteen times over, so
	 * 34 is the figure and 1.4 was the rounding of it.
	 */
	.anniversary__text {
		margin: 419px 0 0;
		width: auto;
		line-height: 34px;
	}

	.anniversary__text p + p {
		margin-top: 0;
	}
}

/* =========================================================================
 * Section 08 — le contact
 *
 * Figma frame 1:722 "8 ( CONTACT)" (1440x960). The headline centred, four
 * ruled fields under it, then a foot in two columns: the red button against
 * the second decorative rule, the RGPD mention against the third, and the
 * Welcome to the Jungle lockup and its sentence on the same two below.
 *
 * Horizontally this is the site's own grid and nothing else. Every block runs
 * from the second decorative rule to the fifth — 336 to 1105 of the frame, 20%
 * to 80% of the content box — so 60% and centred is the whole measurement. The
 * frame actually opens the fields at 343.35 rather than 336, seven pixels off
 * the rule it closes on at the other end; that is a nudge, not a grid, and the
 * rule wins.
 * ======================================================================= */

/*
 * --v is one of the frame's vertical pixels, as in sections 03, 06 and 07.
 * What is fixed here is everything with type or a picture in it: an 80px
 * headline, 103 of field rows, the 60 of the three-line RGPD mention and the
 * 101 of the lockup — 344 in all, at every window height, because a form set
 * smaller to fit is a form nobody wants to type in.
 *
 * The remaining 616 is space: 246 above the headline, 61 under it, 71 and 63
 * between the rules, 31 down to the foot, 28 down to the lockup and 116 below.
 * Sharing that out against the window puts every rule where the frame puts it
 * whatever the window is; at 960 --v is 1px and the section is the frame.
 *
 * The floor is 0.5 because that is where the space above the headline, 123,
 * still clears the header's own 106 — below roughly 650px of window the max()
 * takes over and the section scrolls, which is the lesser evil and what
 * sections 06 and 07 settle for too.
 */
.contact {
	--v: clamp(0.5px, calc((100svh - 344px) / 616), 1px);

	position: relative;
	padding-top: max(calc(246 * var(--v)), var(--hlp-header-h));
	padding-bottom: calc(116 * var(--v));
}

.contact__inner {
	position: relative;
}

/*
 * 769 of the frame's 1440 centred is the second rule to the fifth, which is
 * the same span section 07's headline is indented to — three of the five
 * columns. Centred rather than indented, as the frame draws it.
 */
.contact__title,
.contact__form,
.contact__jobs {
	width: 60%;
	margin-inline: auto;
}

.contact__title {
	margin-block: 0;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-align: center;
	text-transform: uppercase;
}

/*
 * The frame opens the first field at 387, 141 under the headline's box — but
 * that box is 87 tall for two lines that set 80, so the air between them is
 * 61 and not 54. Same slack sections 03 and 07 measure out of the same
 * headline.
 */
.contact__form {
	margin-top: calc(61 * var(--v));
}

/* Positioned for the error message below, which is hung off the rule. */
.contact__field {
	position: relative;
	margin: 0;
}

/*
 * A field is its own label: the frame writes the wording on the rule, inside
 * the box, which is a placeholder. It is drawn in full black rather than the
 * grey a browser would choose, so an empty field and a filled one are set the
 * same — which is the frame's decision and the reason the hidden <label> in
 * the markup is not optional.
 *
 * The rule is the field's own bottom border, so it is exactly as wide as the
 * box and moves with it. Padding-bottom is the frame's gap between the
 * wording and the rule, which it draws differently for each of the three
 * rows — 12, 7 and 9 — and those are transcribed rather than averaged.
 */
.contact__input {
	/*
	 * block, so the field is the box and nothing else. Left inline, as a form
	 * control is by default, the paragraph around it opens a line box and sits
	 * the control on that line's baseline — which put the rules a pixel below
	 * where the frame draws them at the narrow end, for no reason a reader
	 * could see or an author could tune.
	 */
	display: block;
	width: 100%;
	padding: 0 0 12px;
	font-family: var(--hlp-font);
	font-weight: 400;
	font-size: 20px;
	/*
	 * The cut's own normal leading, stated as the ratio it works out to: 24 on
	 * 20 here, 19.2 on the 16 the mobile frame sets. One figure for both, and
	 * the same one the frame's field boxes measure.
	 */
	line-height: 1.2;
	color: var(--hlp-black);
	background: none;
	border: 0;
	border-bottom: 1px solid var(--hlp-black);
	border-radius: 0;
	appearance: none;
}

.contact__input::placeholder {
	color: var(--hlp-black);
	opacity: 1;
}

/*
 * Black, not the site's red: red is what the invalid state below says, and a
 * field that is merely being typed in should not be wearing the same colour as
 * one that is wrong. Offset so the ring stands off the rule rather than
 * doubling it.
 */
.contact__input:focus-visible {
	outline: 2px solid var(--hlp-black);
	outline-offset: 4px;
}

/*
 * The invalid state, which no frame draws: the rule turns red under the field
 * it belongs to, and the reason is written under the rule.
 *
 * Hung off the rule rather than laid under it, because the composition has no
 * height to give. Every vertical figure in this section is measured from the
 * frame, and the air between the rows — 71 and 63 — is what a window shorter
 * than 960 takes back; a message that occupied any of it would move every rule
 * below it down the page as someone typed. Absolute, so it costs nothing and
 * lands in the gap that is already there.
 *
 * Empty in the markup and filled by initContactForm(), so a form that has
 * nothing to complain about draws nothing at all.
 */
.contact__input[aria-invalid="true"] {
	border-bottom-color: var(--hlp-red);
}

.contact__error {
	position: absolute;
	top: 100%;
	left: 0;
	margin-top: 4px;
	color: var(--hlp-red);
	font-size: 12px;
	line-height: 20px;
}

/*
 * The two middle fields share a row: 361.6 each with 38.4 between them, which
 * on a 768 band is 47.5% and 5%. Stated as percentages so the pair keeps the
 * frame's proportions at every width rather than only at 1440.
 */
.contact__row {
	margin-top: calc(71 * var(--v));
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	column-gap: 5%;
}

.contact__row .contact__input {
	padding-bottom: 7px;
}

.contact__field--project {
	margin-top: calc(63 * var(--v));
}

.contact__field--project .contact__input {
	padding-bottom: 9px;
}

/*
 * The dropdown the frame draws, when the client has said what it offers. The
 * caret is the frame's own export — the same chevron the intro uses to say
 * "scroll", in black and at 10 x 9 rather than red at 18 x 16, which is a
 * second file because the two are not the same shape scaled: 1.18 against
 * 1.11. It is hung on the right end of the rule, centred on the padding box,
 * which is where the frame puts it.
 */
.contact__input--select {
	padding-right: 22px;
	background: url("../images/icon-field-caret.svg") no-repeat right center / 10px 9px;
	cursor: pointer;
}

/*
 * The honeypot: a field no person is offered and most robots fill in anyway.
 * Clipped to a pixel rather than display:none, which the cruder ones know to
 * skip, and out of the tab order so a keyboard never lands in it.
 */
.contact__trap {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/*
 * The foot: one grid column for the button, the rest for the mention, with the
 * 16px the frame leaves between the third rule and the start of the text.
 * 100%/3 of the 768 band is 256, which is one column of the site's grid, so
 * the button stands on the second rule and the mention just off the third.
 */
.contact__foot,
.contact__jobs {
	display: grid;
	grid-template-columns: calc(100% / 3) 1fr;
	column-gap: 16px;
	align-items: start;
}

.contact__foot {
	margin-top: calc(31 * var(--v));
}

/*
 * Only ever here after a redirect, so it costs the composition nothing on a
 * first view and is allowed to push the foot down when it does appear.
 */
.contact__status {
	grid-column: 1 / -1;
	margin: 0 0 calc(20 * var(--v));
	font-size: 16px;
	line-height: 20px;
}

/*
 * Focus is moved here on arrival, so it takes a ring — the same black one the
 * fields take. Left to the browser it is drawn in blue, which is the one
 * colour on this page nothing else uses.
 */
.contact__status:focus-visible {
	outline: 2px solid var(--hlp-black);
	outline-offset: 4px;
}

/*
 * Every status that is not "it went" is red — expired included. It is the
 * gentlest of the three, and it still means the message did not arrive; left
 * black it was set exactly like the success line above it, which is the one
 * thing a status must never be.
 */
.contact__status--invalid,
.contact__status--expired,
.contact__status--error {
	color: var(--hlp-red);
}

/*
 * 20/10 of padding around a 20px line and no radius, the same button sections
 * 04 and 05 already draw — but unlike theirs this one does not let the wording
 * size it. The frame fixes it at 226 of the 256 column and centres the words
 * in that, which is 88.281% and leaves 22 of the wording's own padding unused:
 * "Contactez-nous" sets 163 here, and 163 + 40 is 203, not 226. So the width
 * is the measurement and the padding is only the floor under it.
 *
 * The frame says Baikal Normal Semi Bold. No semi-bold cut is licensed, so 600
 * resolves to the Bold one — the nearer of the two real weights on either side
 * of it, and the same gap the section 02 italic has.
 *
 * The 7px is the frame's own: it opens the mention at 655 and the button at
 * 662, so the two columns of the foot do not start level.
 */
.contact__submit {
	justify-self: start;
	width: 88.281%;
	min-width: fit-content;
	margin-top: calc(7 * var(--v));
	padding: 10px 20px;
	background: var(--hlp-red);
	color: var(--hlp-white);
	font-weight: 600;
	font-size: 20px;
	line-height: 24px;
	text-align: center;
	white-space: nowrap;
	transition: background-color 0.25s ease;
}

.contact__submit:hover,
.contact__submit:focus-visible {
	background: var(--hlp-black);
}

/*
 * The frame breaks this after "demande de contact." and starts the second
 * sentence on its own line. pre-line, so the newline in the string is that
 * break and the narrow end can simply stop honouring it — which is what its
 * own frame does.
 */
.contact__legal {
	margin: 0;
	font-size: 12px;
	line-height: 20px;
	white-space: pre-line;
	opacity: 0.7;
}

.contact__jobs {
	margin-top: calc(28 * var(--v));
}

.contact__jobs-logo {
	width: 192px;
	height: auto;
}

/*
 * The frame drops the sentence 35 below the top of the lockup, not level with
 * it. pre-line, like the RGPD mention: the frame breaks this after
 * "candidater," and the narrow end, where the block is stacked under the
 * lockup rather than beside it, sets the sentence to its own width instead.
 */
.contact__jobs-text {
	margin: calc(35 * var(--v)) 0 0;
	font-size: 16px;
	line-height: 20px;
	white-space: pre-line;
	opacity: 0.7;
}

/*
 * The link is inside the 70% the frame applies to the whole block, so it is
 * dimmed with the sentence around it rather than picked out of it.
 */
/*
 * nowrap because of the hyphen in it: left alone, a line that runs out mid
 * phrase breaks after the hyphen and leaves "ici" on its own — which happens
 * at 400 wide, where the sentence sets to three lines rather than the frame's
 * two.
 */
.contact__jobs-link {
	white-space: nowrap;
	color: var(--hlp-red);
	text-decoration: underline;
}

/* -------------------------------------------------------------------------
 * Section 08, mobile — frame 1:2650, 430 x 622
 *
 * The same form rearranged, and set differently. The two side-by-side fields
 * become a fourth full-width one, the type drops from 20 to 16 and the rules
 * close up under it — 3 of air where desktop leaves 12 — while the space
 * between the fields opens out to a flat 40. The button leaves the foot
 * altogether: the frame hangs it under the fourth rule, centred, at 129 x 40
 * and set in the regular cut at 14 rather than desktop's semi-bold 20.
 *
 * The headline is set in the normal cut, not the condensed one, and not in
 * capitals — 32 on 36 where desktop is 38 on 1.05 uppercase. It is the one
 * headline on the site the two frames case differently, which is why the
 * string is stored in sentence case and desktop is what applies the capitals.
 *
 * Insets are 15, not the 27 the header uses — sections 03, 04 and 07 do the
 * same, and this frame draws its content 400 wide in a 430 page.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	/*
	 * --v is pinned: 622 is not a screenful and there is nothing here to fit
	 * to a window, so every figure below is the frame's own pixel. Same answer
	 * section 07's mobile reaches.
	 */
	.contact {
		--v: 1px;

		padding-top: max(117px, var(--hlp-header-h));
		/* The mention closes on 603 of the frame's 622. */
		padding-bottom: 19px;
	}

	.contact__inner {
		padding-inline: 15px;
	}

	.contact__title,
	.contact__form,
	.contact__jobs {
		width: auto;
	}

	/*
	 * Baikal Normal where desktop is Baikal Cond, and the frame says Extra
	 * Bold: the family stops at Bold, so 800 resolves to that — the same gap
	 * the semi-bold button has, and the same OTF release would close both.
	 */
	.contact__title {
		font-family: var(--hlp-font);
		font-weight: 800;
		font-size: 32px;
		line-height: 36px;
		text-transform: none;
	}

	/* The frame opens the fields at 237, 48 under a headline that sets 72. */
	.contact__form {
		margin-top: 48px;
	}

	/*
	 * 3 of air under the wording, against desktop's 12 and 7 — including under
	 * the pair, whose 7 is a desktop measurement and has to be said again here
	 * to be undone.
	 */
	.contact__input,
	.contact__row .contact__input {
		font-size: 16px;
		padding-bottom: 3px;
	}

	/*
	 * Four fields of one column, 40 apart. The pair desktop sets side by side
	 * is only a pair because there is room for one; here the row is a stack
	 * and its gap is the same 40 as the margins around it.
	 */
	.contact__row,
	.contact__field--project {
		margin-top: 40px;
	}

	.contact__row {
		grid-template-columns: minmax(0, 1fr);
		row-gap: 40px;
	}

	.contact__field--project .contact__input {
		padding-bottom: 5px;
	}

	/*
	 * The frame hangs a 14.5 box under the fourth rule and leaves 5 after it
	 * before the button — 19.5 of air, less the pixel the rule takes here,
	 * where in the frame the line is drawn inside the top of that box.
	 *
	 * 12.7 is what is left between the button's foot at 510 and the mention's
	 * top at 523.
	 */
	.contact__foot {
		margin-top: 18.5px;
		grid-template-columns: minmax(0, 1fr);
		row-gap: 12.7px;
	}

	.contact__status {
		margin-bottom: 0;
	}

	/*
	 * Centred and fixed at 129 x 40, which is narrower than 20/10 of padding
	 * around the wording would make it — the frame lets the words run into
	 * the padding rather than widen the box, so the padding goes.
	 */
	.contact__submit {
		justify-self: center;
		width: 129px;
		height: 40px;
		margin-top: 0;
		padding: 0;
		font-weight: 400;
		font-size: 14px;
	}

	/* The frame runs the two sentences together here; desktop breaks them. */
	.contact__legal {
		white-space: normal;
	}

	/*
	 * The jobs block is not in the mobile frame. 1:2650 ends on the RGPD
	 * mention at 603 and stops, which would have left a phone with no route to
	 * the agency's job listings at all — so it is carried over from desktop on
	 * the client's instruction rather than dropped. Nothing here is
	 * transcribed, because there is nothing to transcribe.
	 *
	 * What can be borrowed is: the 28 down to the lockup is the desktop
	 * frame's own figure for joining these two blocks, and it arrives on its
	 * own because --v is 1 here. Stacked, the lockup keeps its drawn 192 and
	 * the sentence goes underneath it — desktop's 35 is an offset between two
	 * columns and means nothing in one. The 20 between them is one line of the
	 * sentence's own leading, which is the section's leading throughout, and
	 * is the closest this has to a measurement.
	 */
	.contact__jobs {
		grid-template-columns: minmax(0, 1fr);
		row-gap: 20px;
	}

	/*
	 * 400 wide sets the sentence better than the frame's break does, and the
	 * 35 desktop offsets it by is the row gap above instead.
	 */
	.contact__jobs-text {
		margin-top: 0;
		white-space: normal;
	}
}

/* -------------------------------------------------------------------------
 * Section 09 — les dirigeants. Frame 1:868, 1440 x 960
 *
 * Four cards of 277 in a row, opened at 158 and 420 tall. Three gaps of very
 * nearly 40 — the frame draws 39, 40 and 40 — so the row measures 1227 where
 * the content box measures 1280. That is 53 short, which is why the row cannot
 * hang on the five 256px columns the way sections 03 and 06 do: four cards
 * wider than a column do not divide into five of them. It is inset from the
 * box instead, as section 08's form is, and the inset is written as a
 * proportion so the row scales as one piece with everything on it.
 *
 * Only the picture scales. The type inside a card is the frame's own 18, 15
 * and 16 and stays there, and so does the 10px the frame insets it by, which
 * is why the name block is placed on a percentage of the picture's height
 * rather than a fixed 268.
 * ---------------------------------------------------------------------- */

.leadership {
	/*
	 * The section's air is the page's air: --hlp-fit is shared with the footer
	 * so that the two contract as the one composition Figma draws them as. On
	 * a 960 window it is 1 and the section sets exactly as drawn; shorter, the
	 * 158 above the row and the 95 under it go first and the portraits and the
	 * footer stay whole.
	 */
	--v: var(--hlp-fit);

	padding-top: max(calc(158 * var(--v)), var(--hlp-header-h));

	/*
	 * The drawn 95, contracted with everything else — but allowed to go below
	 * what --v would give it, and all the way to nothing.
	 *
	 * --v stops at half, and the top padding stops at the header's height
	 * whatever --v does, because copy read through the fixed bar is worse than
	 * a footer under the fold. Between them they leave this the last thing
	 * with anything to give: without the middle term the section stops fitting
	 * at about an 842px window, and with it the whole of the footer is still on
	 * screen down to 760 — a 13-inch laptop with room to spare, and worth the
	 * clamp.
	 *
	 * 759.5 is the floor and not a figure to tune: 106 of header, 420 of
	 * portrait and the footer contracted as far as --hlp-fit lets it, to
	 * 233.5, is the window with nothing left over.
	 */
	padding-bottom: clamp(
		0px,
		calc(100svh - var(--hlp-header-h) - 420px - var(--hlp-footer-h)),
		calc(95 * var(--v))
	);
}

/* 1227 of the 1280 content box, centred: the row's own inset. */
.leadership__row {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 3.233%; /* 39.67 of 1227 */
	width: 95.859%;
	margin: 0 auto;
	padding: 0;
	list-style: none;
}

/*
 * The box that clips. The entrance blurs and settles the picture inside it,
 * which is why that is a layer of its own — a filter is not clipped by the
 * element it is on, and the crop has to hold whatever the filter does.
 */
.leadership__photo {
	position: relative;
	overflow: hidden;
	aspect-ratio: 277 / 378;
	background: var(--hlp-placeholder);
}

.leadership__frame {
	position: absolute;
	inset: 0;
}

/*
 * Anchored to the top rather than the middle: a portrait taller than 277:378
 * loses its feet that way instead of its head, which is the crop the field's
 * own instructions promise. The four shipped with the theme are square and
 * lose nothing vertically either way — this is for what the client uploads.
 */
.leadership__portrait {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center top;
}

/*
 * Drawn over the fourth portrait and no other. The frame's own angle is
 * 180.148deg, which is a hand-drawn 180 and is read as one; the stops are
 * transcribed.
 */
.leadership__scrim {
	position: absolute;
	inset: 0;
	background: linear-gradient(
		180deg,
		rgba(0, 0, 0, 0) 48.679%,
		rgba(0, 0, 0, 0.39) 86.354%
	);
}

/*
 * Anchored to the foot of the picture rather than to the 268 the frame opens
 * it at. For the copy as drawn the two are the same block in the same place —
 * the frame opens all four at 268 and closes them 26 to 28 above the picture's
 * bottom edge — but only one of them survives a reworded function. Held at the
 * foot, a longer title grows up into the photograph; held at 268 it grows down
 * onto the address. 27 of 378 is 7.143%.
 */
.leadership__naming {
	position: absolute;
	inset: auto 0 7.143% 0;
	padding-inline: 10px;
	color: var(--hlp-white);
}

/*
 * The frame says Baikal Normal Semi Bold. No semi-bold cut is licensed, so 600
 * resolves to the Bold one — the same gap section 08's button reads across.
 */
.leadership__name {
	margin: 0;
	font-weight: 600;
	font-size: 18px;
	line-height: normal;
	text-transform: uppercase;
}

/*
 * 15, except on the first card, which the frame draws at 14.
 *
 * That point is not a stray. "President & Chief Operations Officer" is the
 * longest of the four functions and the only one that will not hold two lines
 * at 15 — at 15 it takes three, and the block crowds the foot of the picture.
 * The frame sized that card to its title, and so does this.
 */
.leadership__role {
	margin: 0;
	font-size: var(--role, 15px);
	line-height: normal;
}

.leadership__card:nth-child(1) {
	--role: 14px;
}

/*
 * The frame draws the address as plain text on white. It goes out as a mailto
 * and is set to look exactly as drawn: the underline is held back until it is
 * pointed at or focused, so the row reads as the frame draws it and the link
 * is still a link.
 */
.leadership__email {
	margin: 0;
	padding: 10px;
	font-size: 16px;
	line-height: 22px;
}

.leadership__email a {
	color: inherit;
	text-decoration: none;
}

.leadership__email a:hover,
.leadership__email a:focus-visible {
	text-decoration: underline;
}

/* ----- Entrance -----
 *
 * A focus pull on the photograph, and a second beat for the words.
 *
 * The picture arrives blurred and a little too large and settles sharp, which
 * is a lens and therefore has no business on the typography — so the name, the
 * function and the address wait, and arrive already sharp. That waiting is the
 * 520ms below, and it is measured from the picture's own start rather than
 * from the section's, so every card keeps the same two beats.
 *
 * The 1.12 is not decoration. A blur is not clipped by the box it is drawn in,
 * so at the edges it samples the transparency outside the frame and lightens
 * them; oversizing the picture while it is blurred gives it real pixels to
 * sample, and it is back at 1 by the time it is sharp, so the crop at rest is
 * exactly the one the frame draws.
 *
 * .is-revealing arms the closed state and .is-revealed releases it, as in
 * sections 02, 03 and 06. Both come only from main.js — which also holds the
 * release until the four portraits are decoded, so the sequence can never play
 * over empty frames — so without JavaScript the dirigeants are simply there.
 */

.leadership.is-revealing .leadership__frame,
.leadership.is-revealing .leadership__naming,
.leadership.is-revealing .leadership__email {
	opacity: 0;
}

/* Nothing to show through: the picture is what arrives, not a grey box. */
.leadership.is-revealing .leadership__photo {
	background: transparent;
}

/*
 * Two ways in, one animation — the pair section 06 already draws.
 *
 * .is-revealed releases the whole row at once, which is the staggered run and
 * is what a row four cards wide gets. .is-in releases a single card, which is
 * how a column too tall to be watched at once arrives: on a phone the four are
 * stacked over 1677px, so a single run would have dealt cards three and four
 * long before anyone reached them. main.js measures the row and decides which;
 * a card never gets both.
 */
.leadership.is-revealing.is-revealed .leadership__frame,
.leadership.is-revealing .leadership__card.is-in .leadership__frame {
	animation: hlp-focus-in 1.1s cubic-bezier(0.16, 1, 0.3, 1)
		calc(var(--i, 0) * 140ms) both;
}

.leadership.is-revealing.is-revealed .leadership__naming,
.leadership.is-revealing .leadership__card.is-in .leadership__naming {
	animation: hlp-text-in 600ms cubic-bezier(0.22, 1, 0.36, 1)
		calc(var(--i, 0) * 140ms + 520ms) both;
}

.leadership.is-revealing.is-revealed .leadership__email,
.leadership.is-revealing .leadership__card.is-in .leadership__email {
	--dy: 10px;

	animation: hlp-text-in 520ms cubic-bezier(0.22, 1, 0.36, 1)
		calc(var(--i, 0) * 140ms + 640ms) both;
}

/*
 * A card arriving on its own is its own event and waits for nobody: the place
 * in the running order is only meaningful when the row goes as one.
 */
.leadership.is-revealing .leadership__card.is-in {
	--i: 0;
}

/* The running order is the reading order, so the stylesheet can deal it. */
.leadership__card:nth-child(2) {
	--i: 1;
}

.leadership__card:nth-child(3) {
	--i: 2;
}

.leadership__card:nth-child(4) {
	--i: 3;
}

/*
 * The pair section 09 arrives on, and the footer under it with them: a lens
 * finding the picture, then the words landing sharp behind it. Named for what
 * they do rather than for the section, because two blocks now share them.
 */
@keyframes hlp-focus-in {
	from {
		opacity: 0;
		transform: scale(1.12);
		/*
		 * 18 is the figure for a 277px portrait. A 26px social mark blurred by
		 * the same amount is a smudge with nothing left to come into focus, so
		 * the depth is the picture's own and set where it is used.
		 */
		filter: blur(var(--blur, 18px));
	}

	to {
		opacity: 1;
		transform: none;
		filter: blur(0);
	}
}

@keyframes hlp-text-in {
	from {
		opacity: 0;
		transform: translateY(var(--dy, 16px));
	}

	to {
		opacity: 1;
		transform: none;
	}
}

/* Reduced-motion stand-in: the two beats survive, the lens does not. */
@keyframes hlp-reveal-fade {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}

@media (prefers-reduced-motion: reduce) {
	.leadership.is-revealing.is-revealed .leadership__frame,
	.leadership.is-revealing.is-revealed .leadership__naming,
	.leadership.is-revealing.is-revealed .leadership__email,
	.leadership.is-revealing .leadership__card.is-in .leadership__frame,
	.leadership.is-revealing .leadership__card.is-in .leadership__naming,
	.leadership.is-revealing .leadership__card.is-in .leadership__email {
		animation: hlp-reveal-fade 300ms ease calc(var(--i, 0) * 60ms) both;
	}
}

/*
 * Between 769 and 1024 there is no frame — the file draws this section at 1440
 * and at 430 and nothing between. Four cards of 277 need 1227 and there is not
 * that much room, so the row folds to two columns, the turn section 06 makes at
 * the same width, and the section stops being fitted to the window: two rows
 * are taller than the fold, so contracting the rhythm buys nothing and --v goes
 * back to a plain pixel.
 *
 * This is the one provisional range left in the section; 768 down is drawn.
 */
@media (max-width: 1024px) {
	.leadership {
		--v: 1px;

		padding-top: max(90px, var(--hlp-header-h));
		padding-bottom: 60px;
	}

	/*
	 * Capped at the 277 the card is drawn at rather than left to divide the
	 * box. Two columns of a 740px box would hand each card 358, half again as
	 * wide as drawn, and the type inside does not scale with it — a card a
	 * third too big with the frame's own 18 and 15 in it reads as a mistake.
	 * Below about 620 the pair no longer fits at full size and minmax lets
	 * them shrink together, which is where the type steps down too.
	 */
	.leadership__row {
		grid-template-columns: repeat(2, minmax(0, 277px));
		justify-content: space-between;
		gap: 40px 3.233%;
		width: 100%;
	}
}

/* -------------------------------------------------------------------------
 * Section 09, mobile — frame 1:2742, 430 x 2232
 *
 * The row becomes a column: one card per row, 289 of the 430 board, centred on
 * margins of 71 and 70. That is far wider than the 27 the header and every
 * other mobile block inset by, and it is the whole composition — a single
 * narrow column of portraits rather than a page-width one. Cards are 408 tall
 * with 15 between them.
 *
 * The card itself is barely redrawn. The picture widens from 277 to 289 and
 * the type does not move at all: 18 for the name, 14 and 15 for the function,
 * 16 for the address, exactly as on desktop. Two things do change. The address
 * band contracts from 42 to 30 and centres its line in it rather than padding
 * it down from the top. And the naming block keeps its desktop width of 277
 * inside the wider card — which is not a stray: it holds the text column at
 * the same 257 on both frames, so a function wraps identically on a phone and
 * on a desktop, and that is worth more than the 10px inset being even.
 *
 * The frame also rewords all four functions and two of the names; that is not
 * built here. See the note in leadership.php.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.leadership {
		/*
		 * Fitting the section to the window stops here, as it does for
		 * sections 03 and 06. Four stacked cards are more than twice any
		 * phone screen, so there is nothing to contract for.
		 */
		--v: 1px;

		/* 128 to the first card; 66 from the last one to the footer's logo. */
		padding-top: max(128px, var(--hlp-header-h));
		padding-bottom: 66px;
	}

	/*
	 * 289 of the 376 the container is left with once it is inset by the mobile
	 * gutter — the frame's 289 of 430 read through that inset. The margin is
	 * the section's own, not the page's, which is why it is set here and not
	 * left to .container.
	 */
	.leadership__row {
		grid-template-columns: minmax(0, 1fr);
		gap: 15px;
		width: 76.862%;
	}

	.leadership__photo {
		aspect-ratio: 289 / 378;
	}

	/* 277 of the 289 card, so the text column stays the desktop 257. */
	.leadership__naming {
		padding-right: 22px;
	}

	/*
	 * 30 rather than 42, and the line centred in it rather than padded down
	 * from the top: 22 in 30 leaves the 4 the frame draws above and below.
	 */
	.leadership__email {
		display: flex;
		align-items: center;
		height: 30px;
		padding-block: 0;
	}
}

/* -------------------------------------------------------------------------
 * The footer — frame 1:876, drawn inside section 09's board
 *
 * Four marks across the content column rather than four even columns: the
 * lockup at 78, the office at 559, the awards at 777, the labels at 1085. Two
 * of those are the site's own grid — 78 is the first column rule and 1085 is
 * near the fifth — and two are not, so the block is a five-column grid the
 * pieces are placed on by hand, not a flow.
 *
 * Vertically it is two rules and three bands: the top band from 673, a rule at
 * 785, the social row at 821, a rule at 883, and the legal row at 910. Those
 * are transcribed as they are drawn, because the whole point of --hlp-footer-h
 * is that this block is a known height.
 * ---------------------------------------------------------------------- */

/*
 * 287 tall, and the figure --hlp-footer-h reserves: a top band of 112 to the
 * first rule, 98 from there to the second, 64 to the foot of the legal
 * block's second line, and 13 under it. Each of those is set on the block below it, so the total
 * cannot drift from the token without one of them being edited.
 */
.site-footer {
	position: relative;
	padding-bottom: calc(13 * var(--hlp-fit));
	background: var(--hlp-white);
	color: var(--hlp-black);
}

/*
 * The columns are the frame's four marks expressed against the 1280 content
 * box: 78 is its left edge, 559 and 777 are 481 and 699 into it, 1085 is
 * 1007. Written as fractions so the whole block moves with the container.
 */
.site-footer__inner {
	display: grid;
	grid-template-columns:
		[brand] 37.578%   /* 78 to 559 */
		[office] 17.031%  /* 559 to 777 */
		[awards] 24.062%  /* 777 to 1085 */
		[badges] minmax(0, 1fr);
	align-items: start;
	row-gap: 0;
}

.site-footer__brand {
	grid-column: brand;
	display: flex;
	flex-direction: column;
	gap: 18px;
	/* The lockup opens 3 under the office and the labels, which start at 673. */
	padding-top: 3px;
}

.site-footer__logo {
	display: block;
	width: 392.66px;
	max-width: 100%;
	height: auto;
}

/*
 * 19 with 3.42 of tracking, which is 0.18em — the widest letter-spacing on the
 * site, and the reason this line is set in the Light cut rather than Regular.
 */
.site-footer__tagline {
	margin: 0;
	font-weight: 300;
	font-size: 19px;
	line-height: normal;
	letter-spacing: 0.18em;
	text-transform: uppercase;
}

.site-footer__office {
	grid-column: office;
}

.site-footer__office-title {
	margin: 0;
	font-weight: 600;
	font-size: 18px;
	line-height: 36px;
}

.site-footer__address {
	margin: 0;
	font-size: 12px;
	line-height: 20px;
	font-style: normal;
}

.site-footer__address a {
	color: inherit;
	text-decoration: underline;
	text-underline-position: from-font;
}

.site-footer__awards {
	grid-column: awards;
	/* 677 against the band's 673. */
	padding-top: 4px;
}

.site-footer__awards-agences {
	display: block;
	width: 300px;
	max-width: 100%;
	height: auto;
}

.site-footer__badges {
	grid-column: badges;
	/* 673.94, so a pixel under the office rather than level with it. */
	padding-top: 1px;
}

.site-footer__badges-img {
	display: block;
	width: 271px;
	max-width: 100%;
	height: auto;
}

/*
 * The social row and the legal row each sit under a full-width rule, and the
 * rules are the rows' own top borders so they cannot drift from them. Both
 * span the whole grid.
 *
 * The rule is solid black at 1px — sampled off the frame's own render at
 * y=784 and y=882, where it is (0,0,0), and deliberately not the 15% the
 * vertical column rules are drawn at. These two are a different mark.
 */
.site-footer__social,
.site-footer__legal {
	grid-column: 1 / -1;
	border-top: 1px solid var(--hlp-black);
}

/*
 * 785 to 821 is 36 of air over a 26px row that ends at 847, and 883 - 847 is
 * another 36. The row is pushed to the right-hand end of the grid, where the
 * frame sets it at 1130 — 1007 into the content box, which is the badges
 * column it sits under.
 */
.site-footer__social {
	display: flex;
	align-items: flex-end;
	justify-content: flex-end;
	gap: 44px;
	/*
	 * The top band is as tall as the tallest thing on it, which is the awards
	 * picture: 4 down and 100 tall, so it ends at 777. The rule is drawn at
	 * 785, which is the 8 below.
	 */
	margin-top: calc(8 * var(--hlp-fit));
	/* Rule at 785, icons 821 to 847, next rule at 883. */
	padding-block: calc(35 * var(--hlp-fit)) calc(36 * var(--hlp-fit));
}

.site-footer__social-title {
	margin: 0;
	font-weight: 600;
	font-size: 16px;
	line-height: 24px;
}

.site-footer__social-list {
	display: flex;
	align-items: flex-end;
	gap: 44px;
	margin: 0;
	/* Ends at 1333 of 1360: the icon is inset from the column rule, not on it. */
	padding: 0 27px 0 0;
	list-style: none;
}

.site-footer__social-link {
	display: block;
	color: inherit;
	transition: opacity 0.25s ease;
}

.site-footer__social-link:hover,
.site-footer__social-link:focus-visible {
	opacity: 0.6;
}

.site-footer__social-icon {
	display: block;
	height: 26px;
	width: auto;
}

/*
 * Rule at 883, the first line set at 899 and the second at 927, so the block
 * closes at 947 — 13 above the foot of the 960 board.
 */
.site-footer__legal {
	padding-top: calc(15 * var(--hlp-fit));
}

/*
 * The frame spreads the first line across the whole column — its six layers
 * end at 1281 of 1283, a fixed 41 between them that is exactly what the width
 * leaves — so this is space-between rather than that 41. Split into seven
 * links, as the build splits the last layer, a fixed 41 would overrun the
 * column; space-between keeps the frame's two edges instead. 14 is only the
 * floor under the gap. 8 between the lines is the frame's.
 */
.site-footer__legal-list {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: 8px 14px;
	margin: 0;
	padding: 0;
	list-style: none;
	font-size: 12px;
	line-height: 20px;
}

/*
 * A link that opens a line of its own — « Nos engagement sociaux », which the
 * frame sets under the row rather than in it. Taking the whole line is what
 * puts it there, and it keeps the line above free to spread.
 */
.site-footer__legal-item--break {
	flex-basis: 100%;
}

/*
 * 70% rather than a grey: the frame sets these on the black at seven tenths,
 * so they sit on whatever the ground is instead of on one named colour.
 */
.site-footer__legal-link {
	color: inherit;
	opacity: 0.7;
	text-decoration: underline;
	text-underline-position: from-font;
	transition: opacity 0.25s ease;
}

a.site-footer__legal-link:hover,
a.site-footer__legal-link:focus-visible,
button.site-footer__legal-link:hover,
button.site-footer__legal-link:focus-visible {
	opacity: 1;
}

/* « Gestion des cookies » is a button, dressed as the links beside it. */
.site-footer__legal-button {
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	letter-spacing: inherit;
	text-align: inherit;
	cursor: pointer;
}

/*
 * Between 769 and 1024 the four marks stop being four marks — 1280 of content
 * is not there to be divided — so the block folds to two columns and the rows
 * under the rules stay where they are. Provisional, as section 09's own
 * middle range is: the file draws this at 1440 and at 430 and nothing between.
 */
@media (max-width: 1024px) {
	.site-footer__inner {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 40px;
	}

	.site-footer__brand,
	.site-footer__office,
	.site-footer__awards,
	.site-footer__badges {
		grid-column: auto;
		padding-top: 0;
	}
}

/* -------------------------------------------------------------------------
 * The footer, mobile — the foot of board 1:2742
 *
 * Everything centres and stacks, and the frame drops two things: the Agences
 * de l'Année picture, and the label over the social icons. The icons in turn
 * go from one to five and spread across the full width.
 *
 * The lockup comes down to 266 from 392.66, which is 0.677 — the same figure
 * every piece of it scales by, so the composed SVG carries it without help.
 *
 * The legal row is the one thing here that is not the frame. The phone draws
 * no legal links at all; they are kept and wrapped instead. See the note in
 * site-footer.php.
 * ---------------------------------------------------------------------- */

@media (max-width: 768px) {
	.site-footer {
		/* 2205 to 2232 under the labels. */
		padding-bottom: 27px;
	}

	.site-footer__inner {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 0;
		text-align: center;
	}

	/* 1871, against a section that ends at 1871. */
	.site-footer__brand {
		align-items: center;
		gap: 11px;
		padding-top: 0;
	}

	.site-footer__logo {
		width: 266px;
	}

	/* 38 for two lines rather than desktop's 27 for one. */
	.site-footer__tagline {
		font-size: 16px;
		line-height: 19px;
	}

	/* 1947, which is 22 under the signature. */
	.site-footer__office {
		margin-top: 22px;
	}

	.site-footer__office-title {
		font-size: 16px;
	}

	.site-footer__address {
		font-size: 14px;
	}

	/* Drawn on desktop only. */
	.site-footer__awards {
		display: none;
	}

	/*
	 * The rule at 2063 is 20 under the address and the icons open 27 under it,
	 * so the row is centred and full width rather than hung on the right.
	 */
	.site-footer__social {
		justify-content: center;
		width: 100%;
		margin-top: 20px;
		padding-block: 27px 0;
	}

	/* Drawn, but hidden: the phone frame sets the icons on their own. */
	.site-footer__social-title {
		position: absolute;
		width: 1px;
		height: 1px;
		padding: 0;
		margin: -1px;
		overflow: hidden;
		clip: rect(0, 0, 0, 0);
		white-space: nowrap;
		border: 0;
	}

	/*
	 * 64 between five icons across 397, which does not survive a narrower
	 * phone — so the row spreads instead and the gap is only its floor.
	 */
	.site-footer__social-list {
		flex: 1;
		justify-content: space-between;
		gap: 16px;
		padding: 0;
	}

	/*
	 * Spreading five marks edge to edge puts a single one hard against the
	 * left, which is how the row ships — LinkedIn alone until the client
	 * fills in the others. Alone, it takes the middle of the row instead.
	 */
	.site-footer__social-item:only-child {
		margin-inline: auto;
	}

	/* 2154, 37 under the icons, and centred on the page. */
	.site-footer__badges {
		order: 1;
		margin-top: 37px;
		padding-top: 0;
	}

	.site-footer__badges-img {
		width: 159px;
	}

	/*
	 * Kept where the frame drops them, and given a row of their own under the
	 * labels. No rule over them: the frame draws only one on a phone and it is
	 * already over the icons.
	 */
	.site-footer__legal {
		order: 2;
		width: 100%;
		margin-top: 27px;
		padding-top: 0;
		border-top: 0;
	}

	.site-footer__legal-list {
		justify-content: center;
		gap: 4px 14px;
	}
}

/* ----- The footer's entrance -----
 *
 * The pair section 09 arrives on, used again here so that the portraits and
 * the block under them read as the one composition Figma draws them as. The
 * pictures come into focus, the words land sharp behind them, and the running
 * order is the reading order of the block.
 *
 * .is-revealing arms and .is-revealed releases, both from main.js — see
 * initFooter(), which holds the release until the footer's own pictures are
 * decoded, exactly as section 09 does. Without JavaScript the footer is simply
 * there.
 */

.site-footer.is-revealing .site-footer__logo,
.site-footer.is-revealing .site-footer__awards-agences,
.site-footer.is-revealing .site-footer__badges-img,
.site-footer.is-revealing .site-footer__social-icon,
.site-footer.is-revealing .site-footer__tagline,
.site-footer.is-revealing .site-footer__office,
.site-footer.is-revealing .site-footer__legal-list {
	opacity: 0;
}

.site-footer.is-revealing.is-revealed .site-footer__logo,
.site-footer.is-revealing.is-revealed .site-footer__awards-agences,
.site-footer.is-revealing.is-revealed .site-footer__badges-img,
.site-footer.is-revealing.is-revealed .site-footer__social-icon {
	animation: hlp-focus-in 1.1s cubic-bezier(0.16, 1, 0.3, 1)
		calc(var(--i, 0) * 140ms) both;
}

.site-footer.is-revealing.is-revealed .site-footer__tagline,
.site-footer.is-revealing.is-revealed .site-footer__office,
.site-footer.is-revealing.is-revealed .site-footer__legal-list {
	animation: hlp-text-in 600ms cubic-bezier(0.22, 1, 0.36, 1)
		calc(var(--i, 0) * 140ms + 520ms) both;
}

/*
 * The reading order of the block, which is not its source order: the labels
 * are drawn to the right of the awards and arrive after them, and the legal
 * row closes it whatever the frame does with the two above it.
 */
.site-footer__office {
	--i: 1;
}

.site-footer__awards-agences {
	--i: 2;
}

.site-footer__badges-img {
	--i: 3;
}

.site-footer__social-icon {
	--i: 4;

	/* A 26px mark wants a 26px-sized depth of field, not a portrait's. */
	--blur: 6px;
}

.site-footer__legal-list {
	--i: 5;

	/* A row of small type, so it rises the shorter distance the address does. */
	--dy: 10px;
}

@media (prefers-reduced-motion: reduce) {
	.site-footer.is-revealing.is-revealed .site-footer__logo,
	.site-footer.is-revealing.is-revealed .site-footer__awards-agences,
	.site-footer.is-revealing.is-revealed .site-footer__badges-img,
	.site-footer.is-revealing.is-revealed .site-footer__social-icon,
	.site-footer.is-revealing.is-revealed .site-footer__tagline,
	.site-footer.is-revealing.is-revealed .site-footer__office,
	.site-footer.is-revealing.is-revealed .site-footer__legal-list {
		animation: hlp-reveal-fade 300ms ease calc(var(--i, 0) * 60ms) both;
	}
}

/* -------------------------------------------------------------------------
 * Legal pages
 *
 * page.php: the mentions légales, the politique cookies, and the legal
 * documents to come. Figma frame 1:2026 — named "Mentions legales" on the
 * board but titled and written as the politique en matière de cookies — is
 * the one legal-page composition the file draws, so every legal page sets to
 * it: the content column at 107 of the 1440 frame, which is the container's
 * 80 plus the same 27 inset the footer's social row uses, 1231 wide; the
 * title 52 under the 106 header; body text 16 on 22 in Baikal Light with 10
 * of air between blocks; section heads in Baikal Bold at 25; and the cookie
 * tables — grey head row, hairline black borders — that are the reason the
 * text runs the full column rather than a reading measure.
 *
 * The document itself is written in the page editor, one page per language,
 * so the selectors dress whatever the editor produces: headings, paragraphs,
 * lists, addresses, and core/table blocks. The frame is desktop-only — no
 * mobile board draws a legal page — so below the breakpoint the type stands
 * and the tables scroll sideways inside their own figure, the page never.
 * ---------------------------------------------------------------------- */

.page-content {
	/* The header is fixed and owns the top of the window; frame: title at 158. */
	padding-top: calc(var(--hlp-header-h) + 52px);
	padding-bottom: 120px;
}

.page-content__title,
.page-content__body {
	/* 107 into the frame: the container's gutter plus the footer's 27. */
	margin-inline-start: 27px;
	max-width: 1231px;
}

.page-content__title {
	margin-block: 0;
	font-family: var(--hlp-font-cond);
	font-weight: 700;
	font-size: 38px;
	line-height: 1.05;
	text-transform: uppercase;
}

.page-content__body {
	margin-top: 15px;
	font-size: 16px;
	font-weight: 300;
	line-height: 22px;
	/* The frame breaks long words on every text layer; the written-out URLs
	   in the policy need it on a phone's measure. */
	overflow-wrap: break-word;
}

/* Frame: a 25px Baikal Bold head, set in capitals, 20 of air over the 10. */
.page-content__body h2 {
	margin: 30px 0 10px;
	font-size: 25px;
	font-weight: 700;
	line-height: normal;
	text-transform: uppercase;
}

/* The numbered sub-heads ("1. Des cookies strictement nécessaires"). */
.page-content__body h3 {
	margin-block: 0 10px;
	font-size: 16px;
	font-weight: 700;
	line-height: normal;
}

.page-content__body p,
.page-content__body ul,
.page-content__body ol {
	margin-block: 0 10px;
}

.page-content__body address {
	margin-block: 0 10px;
	font-style: normal;
}

.page-content__body a {
	color: inherit;
	text-decoration: underline;
	text-underline-position: from-font;
	transition: color 0.25s ease;
}

.page-content__body a:hover,
.page-content__body a:focus-visible {
	color: var(--hlp-red);
}

/*
 * The cookie tables. The frame draws each row as a full-width band with
 * hairline black borders, the head row on grey; text starts 19 in, rows are
 * 43 tall when one line sets them. Column marks at 19, 210, 400 and 1008 of
 * 1231 give the widths. The editor produces figure.wp-block-table around a
 * plain table, and the figure is the horizontal scroller on small screens.
 */
.page-content__body figure.wp-block-table {
	margin: 0 0 20px;
	overflow-x: auto;
}

.page-content__body table {
	width: 100%;
	min-width: 720px;
	border-collapse: collapse;
}

.page-content__body th,
.page-content__body td {
	border: 1px solid var(--hlp-black);
	padding: 10px 18px;
	font-size: 16px;
	line-height: 22px;
	text-align: left;
	vertical-align: middle;
	/* The body breaks long words for the phone's measure; a cookie name
	   snapped in half ("tartaucitr / on") is worse than a wider column,
	   and the table has its own scroller to pay for one. */
	overflow-wrap: normal;
}

.page-content__body th {
	background: #d9d9d9;
	font-weight: 700;
	line-height: normal;
}

.page-content__body th:nth-child(1),
.page-content__body td:nth-child(1) {
	width: 15.5%;
}

.page-content__body th:nth-child(2),
.page-content__body td:nth-child(2) {
	width: 15.5%;
}

.page-content__body th:nth-child(3),
.page-content__body td:nth-child(3) {
	width: 49%;
}

.page-content__body th:nth-child(4),
.page-content__body td:nth-child(4) {
	width: 20%;
}

@media (max-width: 1024px) {
	.page-content__title,
	.page-content__body {
		margin-inline-start: 0;
	}
}

@media (max-width: 768px) {
	.page-content {
		padding-top: calc(var(--hlp-header-h) + 40px);
		padding-bottom: 80px;
	}

	.page-content__title {
		font-size: 34px;
	}

	/*
	 * Break the table's scroll area out to the full viewport width —
	 * the container's 27px side padding is precious on a phone.
	 */
	.page-content__body figure.wp-block-table {
		margin-inline: calc(-1 * var(--hlp-gutter));
	}

	.page-content__body th,
	.page-content__body td {
		padding: 8px 10px;
		font-size: 13px;
		line-height: 18px;
	}

	.page-content__body th:nth-child(1),
	.page-content__body td:nth-child(1) { width: 22%; }
	.page-content__body th:nth-child(2),
	.page-content__body td:nth-child(2) { width: 18%; }
	.page-content__body th:nth-child(3),
	.page-content__body td:nth-child(3) { width: 43%; }
	.page-content__body th:nth-child(4),
	.page-content__body td:nth-child(4) { width: 17%; }

	.page-content__body table,
	.page-content__body table.has-fixed-layout {
		min-width: 580px;
		table-layout: auto;
	}
}
