/* ==========================================================================
   Inno-Centre — Livre blanc landing page
   Design v1 prototype. Mobile-first.
   Tokens mirror the live inno-centre.com Webflow variables so the Webflow
   build can map 1:1. Source: ic-main.webflow.shared.af9561bfc.min.css
   ========================================================================== */

/* --- Brand fonts (self-hosted, pulled from the live site) ---------------- */
@font-face {
  font-family: "Galaxie Copernicus";
  src: url("assets/fonts/GalaxieCopernicus-Book.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Galaxie Copernicus";
  src: url("assets/fonts/GalaxieCopernicus-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Suisse Intl";
  src: url("assets/fonts/SuisseIntl-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Suisse Intl";
  src: url("assets/fonts/SuisseIntl-Medium.otf") format("opentype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* --- Tokens -------------------------------------------------------------- */
:root {
  /* Swatches — verbatim from the live site */
  --swatch-dark: #2f2d2b;
  --swatch-off-white: #f4f3f1;
  --swatch-light-gray: #e3e4e3;
  --swatch-pale-pink: #e8c9c4;
  --swatch-pale-taupe: #b48f8b;
  --swatch-sage-gray: #879797;
  --swatch-mint: #a8cbbb;
  --swatch-cristal: #f7f7f6;  /* brand "Cristal" — the figures' own surface */
  --swatch-error: #ff5252;

  /* Theme (light section default).
     There is no muted text colour. Measured on inno-centre.com at 390px and
     1440px, every single text role resolves to rgb(47,45,43) at full opacity —
     body copy, eyebrows, dates, nav, footer links, captions. De-emphasis is
     done with size and position, never with a lighter grey. #2f2d2b on
     #f4f3f1 is 12.4:1. The 60%-alpha grey this file used to carry was 3.7:1,
     which fails WCAG AA for body text. */
  --bg: var(--swatch-off-white);
  --text: var(--swatch-dark);
  --border: #2f2d2b33; /* hairline rules only — decorative, not text */

  /* Type.
     Serif is reserved for exactly one element: the hero headline. That is
     what the live site does — "Performance d'affaires" is Galaxie Copernicus,
     and every other heading on the site is Suisse Intl at weight 400. */
  --font-display: "Galaxie Copernicus", Georgia, serif;
  --font-body: "Suisse Intl", "Helvetica Neue", Arial, sans-serif;
  --tracking-tight: -0.015em;
  --tracking-wide: 0.02em;
  --tracking-label: 0.02em; /* their uppercase tracking, measured */

  /* Display type scale — the live site's, verbatim.
     It is not a clamp: `.u-text-style-hN` is a pure svw value that steps at
     480 and 992, so every heading scales with the viewport inside each band
     and jumps at the two breakpoints. The clamps this replaces ran ~10% small
     on mobile (45 vs 48.4 for h1 at 440) and never matched between the ends.

     | | ≤479 | 480–991 | ≥992 |
     |---|---|---|---|
     | h1 | 11svw | 9svw | 5.15svw |
     | h2 | 9svw | 7svw | 3.88svw |
     | h3 | 7svw | 5.75svw | 2.22svw |
     | h4 | 6svw | 3.3svw | 1.38svw |

     Verified against the live site at 440 (48.4 / 39.6 / 30.8),
     992 (51.09 / 38.49) and 1440 (74.16 / 55.87 / 31.97). */
  --fs-display: 11svw;
  --fs-h2:      9svw;
  --fs-h3:      7svw;
  --fs-h4:      6svw;
  --fs-lg:      clamp(1.125rem, 1.6vw, 1.25rem); /* 18 → 20 */
  --fs-body:    clamp(1rem, 1.4vw, 1.25rem);     /* 16 → 20 */
  --fs-sm:      clamp(0.9375rem, 1.1vw, 1.0625rem); /* 15 → 17 */
  --fs-label:   0.9375rem;                       /* 15, flat — matches theirs */
  --bar-h:      clamp(0.5rem, 1.1vw, 0.6875rem);  /* figure bar thickness */
  --fs-btn:     clamp(1.125rem, 1.5vw, 1.25rem); /* 18 → 20 */

  /* Space.
     The gutter is the live site's grid, not an invented one. Webflow builds the
     container as `98svw - 2 * --site--margin`, centred, so the inset from the
     viewport edge is the leftover 1svw plus that margin. Verified in the browser
     at 500px: container 447.70px, inset 26.14px — this formula to the pixel.
     It lands within 2px of the clamp it replaces at both ends of the scale. */
  --site-margin: clamp(1rem, 0.428rem + 2.86vw, 3rem);
  --gutter: calc(1svw + var(--site-margin));
  --section-y: clamp(3.5rem, 9vw, 8rem);
  --stack: clamp(1.5rem, 3vw, 2.5rem);
  --hairline: 0.0625rem;

  /* Nav — mobile first. Every value below was read back out of the live site in
     the browser, not guessed. Reference measurement at a 500px viewport:
       bar             500 × 50      10svw tall, 3svw padding-block
       logo            150 × 79.14   position:fixed, top 15px, z-index max
       hamburger        25 × 25      lines 1.25px, gap 2.5px
       locale link   19.42 × 21      13.75px Suisse Intl 400, ls 0.3px
     i.e. 30svw / 3svw / 5svw / 0.25svw / 0.5svw / 2.75svw to the pixel.
     This base block is the live ≤479px step, since we build mobile first. */
  --nav-h:        18svw;    /* body --_custom-size---nav-height */
  --nav-pad:      4svw;     /* .nav_contain.u-container padding-block */
  --nav-logo-w:   35svw;    /* .display_logo_wrapper / .nav_logo_wrapper */
  --nav-logo-top: 3svw;     /* .display_logo_wrapper top */
  --burger-w:     7svw;     /* .nav_btn_wrap, aspect-ratio 1 */
  --burger-line:  0.57svw;  /* .nav_btn_line height/min/max */
  --burger-gap:   0.5svw;   /* --nav--hamburger-gap */
  --nav-lang-fs:  4.5svw;   /* .btn_nav_local font-size */
  --nav-tab-gap:  5.5svw;   /* .nav_menu_tab column-gap */
  --nav-lang-line: 0.0625svw; /* .button_hover-line height — 0.3125px @500 */

  /* The logo is the tallest thing in the header and it deliberately overhangs
     the bar, so the space main has to clear is the logo, not the bar.
     0.5276 is the logo's own aspect ratio (191/362). */
  --nav-clear: max(var(--nav-h), calc(var(--nav-logo-top) + var(--nav-logo-w) * 0.5276));
}

/* live: @media (max-width: 991px) — i.e. everything from 480px up */
@media (min-width: 30rem) {
  :root {
    --fs-display:  9svw;
    --fs-h2:       7svw;
    --fs-h3:       5.75svw;
    --fs-h4:       3.3svw;

    --nav-h:       10svw;
    --nav-pad:     3svw;
    --nav-logo-w:  30svw;
    --burger-w:    5svw;
    --burger-line: 0.25svw;
    --nav-lang-fs: 2.75svw;
    --nav-tab-gap: 3.5svw;
  }
}

/* live: the base (desktop) block, 992px up */
@media (min-width: 62rem) {
  :root {
    --fs-display:   5.15svw;
    --fs-h2:        3.88svw;
    --fs-h3:        2.22svw;
    --fs-h4:        1.38svw;

    --nav-h:        5.85svw;
    --nav-pad:      1.35svw;
    --nav-logo-w:   19.47svw;
    --nav-logo-top: 1.3svw;
    --burger-w:     2rem;      /* .nav_btn_wrap width: 2rem */
    --burger-line:  0.125rem;  /* live leaves --nav--hamburger-thickness unset
                                  above 991px, where it swaps to a full desktop
                                  nav and the hamburger is display:none */
    --burger-gap:   0.375rem;
    --nav-lang-fs:  1svw;      /* .btn_nav_local base — 14.4px at 1440 */
    /* 2svw, re-measured off the live site 2026-08-26, was 4.5svw and more than
       twice too wide. The live bar does not space these with a wrapper gap at
       all: .nav_actions_wrap is flex with `gap: normal`, and the separation
       comes from 1svw margins on either side of .nav_buttons_item — 14.4px
       each at 1440. Contact's right edge lands at 1312.9 and the locale's left
       at 1342.7, so 29.8px measured against the 28.8px this computes to; the
       last pixel is inline whitespace between two inline-blocks. Same svw
       basis, so it tracks their bar at every width. */
    --nav-tab-gap:  2svw;
  }
}

/* Dark sections re-point the theme, nothing else changes. */
.theme-dark {
  --bg: var(--swatch-dark);
  --text: var(--swatch-off-white);
  --border: #f4f3f133;
}
/* The figures section is the one place the brand palette is used as data, so it
   gets the brand's lightest surface to sit on. On mint, the pale tones (Quartz
   1.39:1, Marbre 1.19:1) had enough separation from the background to work as
   slices; against a light surface they would need to carry the chart alone, and
   the section reads cleaner white with the colour doing the work. */
.theme-cristal {
  --bg: var(--swatch-cristal);
  --text: var(--swatch-dark);
  --border: #2f2d2b33;
}
.theme-mint {
  --bg: var(--swatch-mint);
  --text: var(--swatch-dark);
  --border: #2f2d2b33;
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--swatch-off-white);
  color: var(--swatch-dark);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.5;
  letter-spacing: var(--tracking-wide);
  -webkit-font-smoothing: antialiased;
}

img, svg { display: block; max-width: 100%; }
button, input { font: inherit; color: inherit; }

/* Headings are Suisse Intl 400, matching the live site. The serif is applied
   only to .hero__title. */
h1, h2, h3, h4 {
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: var(--tracking-tight);
  margin: 0;
  /* No fixed heights anywhere — French copy runs 15–20% longer. */
  text-wrap: pretty;
}

p { margin: 0; text-wrap: pretty; }

/* Suisse Intl ships 400 and 500 only. Without this, browsers synthesise a
   fake 700 that doesn't exist in the brand. */
strong, b { font-weight: 500; }

a { color: inherit; }

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.u-sr-only {
  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 {
  position: absolute;
  left: var(--gutter); top: 0;
  transform: translateY(-200%);
  z-index: 1000; /* above the fixed nav (900) */
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  padding: 0.75rem 1.25rem;
  text-decoration: none;
  font-size: var(--fs-sm);
}
.skip-link:focus { transform: translateY(0.5rem); }

/* --- Layout primitives --------------------------------------------------- */
.section {
  background: var(--bg);
  color: var(--text);
  padding-block: var(--section-y);
}

/* No max-width. Webflow's --container--main is `98svw - 2 * --site--margin`
   with no rem cap at all, so the live layout keeps widening past 1440 while a
   90rem cap would freeze ours and re-centre it. At 1920 that was the difference
   between a 1785.6px content box starting at x67.2 (live) and a 1305.6px one
   starting at x307.2 (ours). The gutter already encodes the same formula, so
   dropping the cap makes the two identical at every width. */
.container {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The site's signature grid: a short label column beside a wide content
   column, separated by a hairline. On mobile the label becomes an eyebrow
   stacked above — which is also what keeps longer French labels safe. */
/* The live grid is 12 columns with a 1.5svw gutter (--site--gutter). The label
   column spans 3 and the content spans 9 starting at column 4 — measured at
   1440: label 312.6px at x62.4, content 981px at x396.6.

   Below 992px the live site drops the label column entirely rather than
   stacking it, so the content simply spans all 12. */
.labelled {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 1.5svw;
  border-top: var(--hairline) solid var(--border);
  padding-top: clamp(1.25rem, 2.5vw, 2rem);
}

.labelled__label {
  display: none; /* ≤991: not shown at all, same as the live site */
  grid-column: 1 / span 3;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-h2);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: var(--tracking-tight);
  text-transform: none;
  color: var(--text);
  text-wrap: balance;
  /* Breaks come from soft hyphens in the copy, not from the engine. `hyphens:
     auto` computes correctly here and <html lang> is set, but Chrome still
     refused to hyphenate "Acknowledgements" — it overflowed its box by 187px
     with overflow-wrap off, which is why it used to break mid-word with no
     hyphen. A &shy; needs no dictionary and always breaks where we say. */
  overflow-wrap: break-word; /* last resort only */
}

.labelled__body {
  grid-column: 1 / -1;
  min-width: 0;
}

/* Every section heading is one size on the live site — the h2 step. The
   acknowledgements heading used to be hardcoded to --fs-h3 inline, which made
   it the only section whose heading was a third smaller than the rest. */
.section__title {
  font-size: var(--fs-h2);
  line-height: 1.1;
  letter-spacing: var(--tracking-tight);
  margin: 0 0 2svw;
}

@media (min-width: 62rem) {
  .labelled__label {
    display: block;
    /* Bottom-left of the section, inside the padding, and sticky across the
       whole section until the next one takes over.

       Two rules do it together. `align-self: end` sets where the label COMES
       TO REST: the end of its grid area, which is the bottom of .labelled, so
       it finishes level with the last thing in the content column — the logo
       row under Credits, the source line under Figures, the button under Sign
       up. `bottom` then sticks it that far above the viewport edge on the way
       through, so it rides along the section and settles onto its resting
       place as the section ends.

       This replaced `top: 80svh` + `padding-bottom: 15svh`, which pinned the
       label to a fixed fraction of the VIEWPORT rather than to the section.
       Anchoring to the viewport is what left it floating mid-section instead
       of landing on the content it labels. Offsetting with `bottom` also means
       the padding no longer has to fake the inset, so the box is the glyphs. */
    position: sticky;
    bottom: var(--section-y);
    align-self: end;

    /* .section_nav's half-column inset — measured live at 44.9px of a 313px
       column, i.e. 14.4% of the label's own width, constant at every width.
       It used to be calc(var(--col) * 0.5) and silently resolved to 2.97px:
       --col is declared on .labelled with a 100%, but custom properties
       substitute as tokens, so the percentage resolved against the label's own
       309px grid area rather than the 1315px grid. That missing 42px was the
       whole reason there was no visible gap before the content. */
    padding-right: 14.4%;

    /* Down from the live 3.88svw. Our labels are words theirs never has to
       set: "Sommaire" alone measures 266px against the 264px box at h2 size,
       and "Acknowledgements" needs a break whose tail ("edgements") is 295px.
       At 3.35svw every label clears the box, the two long ones on one soft
       hyphen each. */
    font-size: 3.35svw;

    /* Set by app.js for the moments the label would sweep through the pinned
       logo on its way out of the section. */
    transition: opacity 0.2s linear;
  }
  /* The figures section is a scroll track: .figures-pin is already sticky for
     the whole 4-viewport run, so the label rides with it and needs no stickiness
     of its own. Left sticky, it would clamp to --section-y above the viewport
     edge while the pin's own bottom padding is 5vh, and the label would sit that
     difference (45px at 900) above the source line it should be level with. */
  .is-scroller .labelled__label { position: static; }
  .labelled__label.is-eclipsed { opacity: 0; }
  .labelled__body { grid-column: 4 / span 9; }
}

/* --- Nav ----------------------------------------------------------------- */
/* Two independent fixed layers — see the comment above the markup in
   index.html. Both slide out on scroll-down and return on scroll-up. */

.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 900;
  background: transparent; /* .nav_component is transparent; the bar inside isn't */
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
@media (prefers-reduced-motion: reduce) {
  .nav { transition: none; }
}
.nav.is-hidden { transform: translateY(-130%); } /* the live GSAP value */
/* Opaque off-white, as the live .nav_wrap is at every width and every scroll
   position. This is also what keeps the header legible where it passes over the
   dark Contents band. */
.nav__bar { background: var(--swatch-off-white); }

/* --- Desktop bar (≥992px) ------------------------------------------------ */
/* The live bar is three regions: a 19.47svw logo slot, the links taking the
   free space with their items CENTRED inside it, and a 19.47svw actions group.
   The centring is why their first link sits at x417 and not hard against the
   slot at x353.6 — and it only lands there if the actions group is the same
   19.47svw as the logo slot. */
/* Both hides are written with the parent in the selector so they outrank the
   base `.nav__link { display: inline-block }` further down the file — equal
   specificity would let the later base rule win, which is exactly what left
   Contact visible on mobile the first time round. */
.nav__inner > .nav__links,
.nav__tab .nav__link--contact { display: none; }

@media (min-width: 62rem) {
  .nav__inner > .nav__links {
    display: flex;
    align-items: center;
    flex: 1;
    justify-content: center;
    gap: 2.5svw; /* 36px at 1440 */
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .nav__tab .nav__link--contact { display: inline-block; }
  .nav__tab {
    width: var(--nav-logo-w);
    justify-content: flex-end;
    gap: 2svw;             /* Contact → locale, 28.8px at 1440 */
    padding-right: 1svw;   /* their locale item's trailing inset */
  }
}

/* Links share the locale switch's treatment: 1svw uppercase with the
   left-to-right hairline sweep on hover. 14.4px at 1440. */
.nav__link {
  position: relative;
  display: inline-block;
  overflow: hidden;
  padding-block: 0.125svw;
  color: var(--swatch-dark);
  font-size: 1svw;
  line-height: 1.3;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.nav__link-line {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--nav-lang-line);
  background: currentColor;
  transform: translateX(-105%);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (hover: hover) and (pointer: fine) {
  .nav__link:hover .nav__link-line { transform: translateX(0); }
}
.nav__link:focus-visible .nav__link-line { transform: translateX(0); }

/* .nav_contain — a fixed height, not a min-height, with the children stretched
   into the content box. At 500px that is 50px tall with a 20px content box. */
.nav__inner {
  display: flex;
  align-items: normal;
  justify-content: space-between;
  column-gap: 0.75svw;
  height: var(--nav-h);
  padding-block: var(--nav-pad);
}

/* Empty. The bar's own logo link is display:none on the live site — this only
   reserves the width, which is what pushes the hamburger and locale to where
   they sit. The visible logo is the separate fixed layer below. */
.nav__logo-slot { width: var(--nav-logo-w); flex: none; }

/* --- The oversized fixed logo ------------------------------------------- */
/* .display_logo_wrapper. Genuinely position:fixed and outside the bar, so it
   does not inherit the bar's transform and stays put exactly as the live one
   does. It is ~81px tall against a ~79px bar, so it overhangs into the hero —
   that overhang is the look, not a bug.

   The rail exists only to hand the logo a correct static position: `left` is
   left auto so the fixed box inherits the container's content edge, which is
   scrollbar-correct in a way `calc(100vw …)` is not. Same trick the live site
   uses — its .nav_cont_logo is a zero-height container for the same reason. */
.site-logo-rail {
  position: relative;
  z-index: 950; /* above the bar's 900, as the live 2147483647 is */
}
.site-logo-rail > .container { height: 0; }

.site-logo {
  position: fixed;
  top: var(--nav-logo-top);
  width: var(--nav-logo-w);
  color: var(--swatch-dark);
  line-height: 0;
  outline-offset: 0.5rem;
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1),
              color 0.25s linear;
}

/* Set by app.js from the background actually painted behind the mark — the
   dark sections and the footer, which the pinned logo now crosses. */
.site-logo.is-on-dark { color: var(--swatch-off-white); }
.site-logo svg { width: 100%; height: auto; }
@media (prefers-reduced-motion: reduce) {
  .site-logo { transition: none; }
}
.site-logo.is-hidden { transform: translateY(-130%); }

.nav__tab {
  display: flex;
  align-items: center;
  gap: var(--nav-tab-gap);
}

/* Hamburger. Geometry and the open animation are the live site's. */
.nav__burger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: var(--burger-gap);
  width: var(--burger-w);
  aspect-ratio: 1;
  flex: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--swatch-dark);
  cursor: pointer;
}
.nav__burger i {
  display: block;
  width: 100%;
  /* All three, as .nav_btn_line does — otherwise flex rounds them away. */
  height: var(--burger-line);
  min-height: var(--burger-line);
  max-height: var(--burger-line);
  flex: none;
  background: currentColor;
  transform-origin: center;
  transition: transform 0.3s ease, opacity 0.2s ease;
}
@media (prefers-reduced-motion: reduce) {
  .nav__burger i { transition: none; }
}
.nav__burger.is-open i:nth-child(1) {
  transform: translateY(calc(100% + var(--burger-gap))) rotate(45deg);
}
.nav__burger.is-open i:nth-child(2) { opacity: 0; }
.nav__burger.is-open i:nth-child(3) {
  transform: translateY(calc(-100% - var(--burger-gap))) rotate(-45deg);
}

/* Above 992 the live site has no hamburger at all — the desktop bar replaces
   it. This has to sit after the base .nav__burger rule, not with the rest of
   the desktop block further up, or equal specificity lets the base win. */
@media (min-width: 62rem) {
  .nav__burger { display: none; }
}

/* Language switch — .btn_nav_local. The live locale list sets display:none on
   the item you are already on, so only the language you can move to renders:
   on a French page that is "EN".

   V1 has nowhere to switch to, so this keeps the live site's position and type
   and drops every affordance that promises navigation. No hover line — that
   sweep is this page's link affordance and it would be a lie here. cursor:help
   carries it instead, with aria-disabled and the tooltip doing the telling.
   Measured at 500px: 19.42 × 21, 13.75px Suisse Intl 400, letter-spacing 0.3px
   (0.02em), padding 2.5px top / 0.625px bottom. */
.nav__lang { position: relative; display: flex; align-items: center; }
.nav__lang-btn {
  display: inline-block;
  /* padding-block matched to .nav__link, not to the live site's locale link.
     That link carried 0.5svw top against 0.125svw bottom, and this button
     inherited it — but the live site's locale sits alone, where nothing is
     beside it to disagree with. Here it sits next to CONTACT, .nav__tab
     centres the two boxes on each other, and 5.7px of extra height all on one
     side pushed the EN text 2.8px below CONTACT's baseline. Measured, then
     matched: same padding, same box height, same baseline. */
  padding-block: 0.125svw;
  padding-inline: 0;
  border: 0;
  background: none;
  color: var(--swatch-dark);
  font-family: var(--font-body);
  font-size: var(--nav-lang-fs);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  /* Full ink, no alpha. This file's own rule, measured off the live site and
     written up in NOTES.md: there is no muted text colour on this page, every
     text role is #2f2d2b at full opacity, and de-emphasis is done with size
     and position. The 0.55 this carried measured 3.27:1 and failed AA — the
     same mistake the 60%-alpha grey made before it. What says "unavailable"
     here is cursor:help, aria-disabled and the tooltip, not a lighter grey. */
  cursor: help;
}

/* The switch is 20.7px tall — the live site's size, kept — which is under the
   24px WCAG 2.2 asks of a tap target, and on a phone this IS a tap target
   because tapping opens the tooltip. The hit area is extended past the type
   instead of the type being grown, so the bar looks unchanged and the target
   clears 44px. */
.nav__lang-btn { position: relative; }
.nav__lang-btn::after {
  content: '';
  position: absolute;
  inset: -0.75rem -0.5rem;
}

/* The tooltip. It is a sibling of the header, not a child of it — see the note
   in index.html — so it is fixed to the corner the switch sits in rather than
   positioned against it. `right: var(--gutter)` is the container's own inset,
   which is exactly where the switch's right edge lands, so the two line up at
   every width without a measurement.

   Right-aligned so it grows back into the page: at 390px the switch is a
   thumb's width from the edge, and any left-anchored box this wide would leave
   the viewport. Capped near the sentence's own length rather than stretched. */
.nav__lang-tip {
  position: fixed;
  /* 16px below the switch itself, not below the bar. Those are different
     numbers — the switch is centred in an 84px bar at 1440 and a 70px one at
     390, so a gap measured from the bar's underside is a different distance
     from the EN at every breakpoint. app.js measures the button and publishes
     --lang-tip-top; the fallback here is the bar's own height, which is the
     right order of magnitude if that never runs. */
  top: var(--lang-tip-top, var(--nav-h));
  right: var(--gutter);
  z-index: 980; /* over the logo's 950, under the skip link's 1000 */
  width: max-content;
  max-width: min(21rem, calc(100vw - 2 * var(--gutter)));
  padding: 0.7rem 0.85rem;
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: 0;
  text-transform: none;
  /* One sentence per line. The slash the client wrote is a separator for a
     single run of text; on two lines the line break already separates them and
     the slash becomes a mark with no job. The hero keeps their string intact,
     slash and all, so the verbatim line still exists on the page. */
  text-wrap: nowrap;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.25rem);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}
/* Selectors run through :root because the trigger is no longer an ancestor.
   Hover only where hovering exists; touch gets the same tooltip from the tap
   handler in app.js, which is what .is-open is for. */
@media (hover: hover) and (pointer: fine) {
  :root:has(.nav__lang:hover) .nav__lang-tip {
    opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
  }
}
:root:has(.nav__lang-btn:focus-visible) .nav__lang-tip,
:root:has(.nav__lang.is-open) .nav__lang-tip {
  opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
}
.nav__lang-tip > span { display: block; }
.nav__lang-tip > span + span { margin-top: 0.15em; }

@media (prefers-reduced-motion: reduce) {
  .nav__lang-btn, .nav__lang-tip { transition: none; }
}

/* The header is fixed, so the page has to start below it. */
#main { padding-top: var(--nav-clear); }
html { scroll-padding-top: var(--nav-clear); }

/* ...but only below 992px. --nav-clear clears the *logo*, which overhangs the
   bar by ~82px at 1440, and that is the right number on mobile where content
   runs full width straight under it. From 992px up every section reserves the
   empty 3-column rail the logo sits in (see .hero__grid), so no copy is ever
   beneath it and main only has to clear the bar itself. Clearing the logo too
   was dead space: 178px of nothing between the bar and the eyebrow. */
@media (min-width: 62rem) {
  #main { padding-top: var(--nav-h); }
  html { scroll-padding-top: var(--nav-h); }
}

/* --- Buttons ------------------------------------------------------------- */
/* Rectangular, hairline border, uppercase — as used on the live site.
   Padding is generous on purpose so the French label has room to grow. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 3.5rem;
  padding: 1rem 1.75rem;
  border: var(--hairline) solid currentColor;
  background: transparent;
  color: inherit;
  font-size: var(--fs-btn); /* 18 → 20px. Theirs is 20px flat. */
  font-weight: 400;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}
.btn:hover { background: var(--swatch-dark); color: var(--swatch-off-white); }
.theme-dark .btn:hover { background: var(--swatch-off-white); color: var(--swatch-dark); }
.theme-mint .btn:hover { background: var(--swatch-dark); color: var(--swatch-mint); }

.btn--block { width: 100%; }

.btn--solid {
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  border-color: var(--swatch-dark);
}
.btn--solid:hover { background: transparent; color: var(--swatch-dark); }

/* --- Hero ---------------------------------------------------------------- */
.hero { padding-block: clamp(2.5rem, 6vw, 6rem) var(--section-y); }

.hero__grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
}

.eyebrow {
  font-size: var(--fs-label);
  font-weight: 400;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text);
  margin: 0 0 clamp(1rem, 2vw, 1.5rem);
}

/* The one serif on the page. */
.hero__title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--fs-display);
  margin-bottom: clamp(1.25rem, 2.5vw, 2rem);
  max-width: 18ch;
  /* Last resort only. `hyphens: auto` was wrong here: Chrome hyphenates
     opportunistically to even out the rag, so it split "transfor-mation" and
     "alimen-taire" at 1840 even though the word measured 572px against a 611px
     column. overflow-wrap only fires when a word genuinely cannot fit, which
     the column sizing above is meant to prevent outright. */
  overflow-wrap: break-word;
}

/* Fix round 2 (Aug 28): the client's 11-word title, set like their own cover:
   "PERFORMANCE DURABLE" leads, the rest sits a level down. The spans carry
   their own sizes, so the desktop .hero__title override cannot inflate them. */
.hero__title--split { line-height: 1; max-width: none; }
.hero__title-lead,
.hero__title-rest {
  display: block;
  font-size: clamp(1.9rem, 2.9svw, 3.3rem);
  line-height: 1.14;
}
.hero__title-rest { margin-top: 0.35rem; max-width: 24ch; }

.hero__subtitle--roomy {
  font-size: clamp(1.1rem, 1.35svw, 1.4rem);
  line-height: 1.45;
  max-width: 44ch;
}

.hero__subtitle {
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: 1.4;
  max-width: 40ch;
  margin-bottom: clamp(1rem, 2vw, 1.5rem);
}

.hero__intro {
  font-size: var(--fs-lg);
  line-height: 1.45;
  max-width: 46ch;
}

.thanks__lca { margin-top: 1rem; font-weight: 500; }
.hero__lca-link {
  color: inherit;
  text-underline-offset: 0.18em;
  font-weight: 500;
}

.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  padding-top: clamp(1.25rem, 2vw, 1.5rem);
  border-top: var(--hairline) solid var(--border);
  font-size: var(--fs-sm);
  color: var(--text);
}
.hero__meta span { display: inline-flex; align-items: center; gap: 0.5rem; }

/* The hero was the one section off the site grid, and that is the whole reason
   the logo used to sit on top of the title: everything else here is .labelled,
   whose body starts at column 4, but .hero__grid spanned all 12 and ran its
   copy straight under the fixed logo.

   So it gets the same rail. Measured on the live site, its content box starts
   at x=397 of 1440 and x=471 of 1728 — both exactly 3 columns + 3 gutters of
   the 12-column grid (25.41% of the container's inner width at both widths),
   which is what this margin reproduces. 100% in a margin resolves against the
   containing block, so --col is the full container column, not the shrunk one.

   Kept at the 62rem breakpoint, not 64rem, so it turns on in the same step as
   .labelled__label and the desktop nav — below that the live site drops the
   rail and hides the logo instead. */
@media (min-width: 62rem) {
  .hero__grid {
    --col: calc((100% - 11 * 1.5svw) / 12);
    margin-left: calc(3 * var(--col) + 4.5svw);
    /* Even halves. Capping the form instead left it ~336px wide, which folded
       "Get the white paper", the consent line and the submit label onto two
       lines each and read as broken. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(1.5rem, 2.75vw, 2.5rem);
    align-items: start;
  }

  /* Smaller than the measured live --fs-display (5.15svw), which assumes a
     full-bleed heading. Here the title has half of a 9-column rail, and the
     binding case is French: "transformation" is the longest word on the page
     and needs 591px at 74.16px. At 3.9svw it comes to ~447px against a ~465px
     column at 1440 — and because both the column and the type scale with svw,
     that ~4% margin holds at every width from 992 up. */
  .hero__title { font-size: 3.9svw; }

  /* ~100px of air below the bar. Flat, not svw, because the binding constraint
     up here is viewport *height* — the whole hero including the form card has
     to be readable without scrolling — and a proportional value grows fastest
     on exactly the wide-but-short screens where that is hardest to hold. */
  .hero { padding-block-start: 6.25rem; }
}

/* --- Form ---------------------------------------------------------------- */
.form-card {
  background: #fff;
  border: var(--hairline) solid var(--border);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  /* Grows with content instead of holding a fixed height. */
}

.form-card__title {
  font-size: var(--fs-h4);
  margin-bottom: 0.5rem;
}
.form-card__note {
  font-size: var(--fs-sm);
  color: var(--text);
  margin-bottom: clamp(1.5rem, 3vw, 2rem);
}

.field { margin-bottom: 1.5rem; }

/* Stacked by default; side by side only where there is room for it. The row
   itself carries no bottom margin — its children already do — so collapsing
   back to one column below 62rem restores the plain stack exactly. */
.field-row > .field:last-child { margin-bottom: 1.5rem; }
@media (min-width: 62rem) {
  .field-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 1.25rem;
    align-items: start;
  }
}

.field__label {
  display: block;
  font-size: var(--fs-label);
  font-weight: 400;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: 0.5rem;
}

.field__input {
  width: 100%;
  min-height: 3rem;
  padding: 0.5rem 0;
  border: 0;
  border-bottom: var(--hairline) solid var(--swatch-dark);
  border-radius: 0;
  background: transparent;
  font-size: var(--fs-lg);
  transition: border-color 0.2s ease;
}
.field__input::placeholder { color: #2f2d2b4d; }
.field__input:focus { outline: 0; border-bottom-width: 0.125rem; }
/* Subtle focus (Gabriel, Aug 28): the full black box fought the underline
   fields. Keyboard focus now thickens and darkens the underline instead;
   still clearly visible, still WCAG-visible against the off-white. */
.field__input:focus-visible {
  outline: 0;
  border-bottom: 0.1875rem solid var(--swatch-dark);
}

.field--invalid .field__input { border-bottom-color: var(--swatch-error); }
.field--invalid .field__label { color: var(--swatch-error); }

.field__error {
  display: none;
  margin-top: 0.5rem;
  font-size: var(--fs-sm);
  color: #d63333;
}
.field--invalid .field__error { display: block; }

/* Consent — deliberately full size and readable, not buried. LCAP. */
.consent {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.75rem;
  align-items: start;
  margin-bottom: 1rem;
  padding-top: 0.5rem;
}
.consent__box {
  width: 1.5rem;
  height: 1.5rem;
  margin: 0;
  accent-color: var(--swatch-dark);
  flex: none;
}
.consent__text {
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--swatch-dark);
}
/* The consent copy stays black — it is legal text and must remain readable.
   The box and the message carry the error, not the wording itself. */
.consent--invalid .consent__box { outline: 0.125rem solid var(--swatch-error); outline-offset: 0.125rem; }
.consent__text .field__error { margin-top: 0.5rem; }
.consent--invalid .field__error { display: block; }

.privacy-link {
  display: inline-block;
  margin-bottom: clamp(1.5rem, 3vw, 2rem);
  font-size: var(--fs-sm);
  color: var(--text);
  text-underline-offset: 0.25em;
}

/* From 62rem up the card competes with the viewport's *height*, not its width:
   the whole hero, form included, is meant to be readable without scrolling.
   These are the same values one notch tighter — nothing here changes what the
   form says, only how much air sits between the rows. Mobile keeps the looser
   rhythm, where the page scrolls anyway and thumbs need the target spacing. */
@media (min-width: 62rem) {
  .form-card { padding: 2rem; }
  .form-card__note { margin-bottom: 1.5rem; }
  .field, .field-row > .field:last-child { margin-bottom: 1.25rem; }
  .privacy-link { margin-bottom: 1.25rem; }
}

/* Short-and-wide screens — a 13" laptop is ~840px of viewport — cannot hold the
   full 100px of hero air *and* the card. The air is what gives, since it is the
   part carrying no information. Above this height the hero keeps the 100px. */
@media (min-width: 62rem) and (max-height: 56.25rem) {
  .hero { padding-block-start: 3.5rem; }
}

/* Success state replaces the form body in place. */
.form-success { display: none; }
.form-card.is-success .form-body { display: none; }
.form-card.is-success .form-success { display: block; }

.form-success__mark {
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: 1.25rem;
  color: var(--swatch-dark);
}
.form-success__title { font-size: var(--fs-h4); margin-bottom: 0.75rem; }
.form-success__body { font-size: var(--fs-sm); color: var(--text); line-height: 1.6; }
.form-success__body + .form-success__body { margin-top: 0.875rem; }
.form-success__hint {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: var(--hairline) solid var(--border);
  font-size: var(--fs-sm);
  color: var(--text);
}

/* --- Thanks banner ------------------------------------------------------- */
/* v1 had one sentence here and set it at --fs-lg. The client's real text is
   eight paragraphs, and --fs-lg over eight paragraphs stops being a statement
   and starts being a wall, so the running text drops to body size and the
   column tightens to 66ch. .thanks__text is kept for the one-line case. */
.thanks__text {
  font-size: var(--fs-lg);
  max-width: 60ch;
  color: var(--text);
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

/* --- Running prose ------------------------------------------------------- */
/* 52ch, not the 66ch that looks right in a stylesheet. `ch` is the width of
   the "0" glyph, and Suisse Intl's is wide: 66ch measured 872px here, which
   set the client's paragraphs at 87 characters a line — well past the 45-75
   a reader tracks comfortably, and this section is eight paragraphs long.
   52ch measures ~69 characters. Checked, not assumed. */
.prose {
  max-width: 52ch;
  color: var(--text);
  /* 1.65 rather than the body's 1.6. Eight paragraphs is long enough that the
     extra 1px per line stops the block reading as a slab, and short enough
     that it does not start reading as a poem. */
  line-height: 1.65;
  margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

/* The opening paragraph is the section's thesis — it says what the whole thing
   is — so it gets the size that says so and the rest sit under it. One step,
   not a drop cap: this is a formal acknowledgement, and a drop cap would make
   it an editorial. It is also the only entry point eight identical paragraphs
   otherwise offer the eye. */
.prose > p:first-child {
  /* Not --fs-lg: at 1440 that clamp and --fs-body both land on 20px, so the
     "step" was no step at all. An explicit one, ~24.5px at 1440. */
  font-size: clamp(1.1875rem, 1.7vw, 1.5rem);
  line-height: 1.45;
  margin-bottom: 1.3em;
}

/* 1.75em, not the 1.15em this carried. Same fault as the mobile one, hiding at
   desktop: the body sets 20px on a 1.65 line-height, so the gap between two
   lines of the SAME paragraph is 33px — and 1.15em of paragraph margin is
   23px. The break BETWEEN paragraphs was tighter than the break between lines,
   which is why eight paragraphs read as one slab and why the block felt wrong.
   1.75em is 35px and clears the line box.

   That is the actual fix here. Justification was treating the symptom. */
.prose > p {
  margin: 0 0 1.75em;
  /* The rag — the unevenness of the right edge — is what actually looked
     wrong, not the alignment. Browsers break lines greedily by default: each
     line takes as many words as fit and the next word falls to the next line,
     so one long word can leave 148px of hole. `pretty` makes the engine look
     at the paragraph as a whole and pick the set of breaks with the least
     total badness, which is what typesetting software has always done.
     Costs nothing, needs no dictionary, and degrades to the old greedy
     behaviour where it is unsupported.

     Measured honestly: on this text at this measure it changes the rag not at
     all — Chrome's `pretty` currently only rescues a short last line. It is
     kept for that, not for the rag.

     What is NOT here is hyphens: auto, and that is now measured rather than
     argued. Turned on over this exact copy, Chrome produced compétitiv-ité,
     Pêcher-ies, l'Environn-ement, d'aff-aires, référ-ence, internation-ale.
     Every one is wrong: the French breaks are compétiti-vité, Pê-che-ries,
     l'Environ-nement, d'af-faires, ré-fé-rence, interna-tionale. That is an
     English dictionary guessing at French, which is precisely what Chrome
     ships, and it would put six broken-looking words in front of the client.

     text-wrap: balance was tried too and is worse: it evens the line lengths
     by pulling the whole column in, taking one paragraph's shortfall from
     136px to 191px. And widening the measure to 58ch fixes two paragraphs,
     breaks a third, and puts the line at 77 characters, past the 75 that
     reads comfortably.

     So the rag stays. At ~20% of the measure it is inside what typography
     tolerates, and the thing that actually made this block look wrong was the
     paragraph rhythm above, not the right edge. If it ever does need
     tightening, the portable answer is a curated &shy; in the few long words
     — no dictionary, breaks where we say — which is what the rail labels
     already do. */
  text-wrap: pretty;
}
.prose > p:last-child { margin-bottom: 0; }

/* A phone sets body at 16px on 1.6, so the same rule gives a different number.
   1.9em there. */
@media (max-width: 61.99rem) {
  .prose > p,
  .prose > .prose__list { margin-bottom: 1.9em; }
}

/* Ragged right, everywhere, and NOT justified — reversing what shipped an
   hour ago. Justification looked better in a screenshot and is the wrong call
   on the evidence:

     · WCAG 1.4.8 asks that text not be justified at all. Rivers of white
       break visual tracking, which matters most for dyslexic readers —
       roughly 10% of any audience.
     · The hyphenation this depended on is not portable. Firefox ships
       dictionaries for every language, but Chrome and Edge carry only
       English, and Safari only a handful. Our French breaks LOOKED right in
       Chrome, which is the trap: an English dictionary applied to French will
       be plausible most of the time and wrong some of the time, and "some of
       the time" is a mis-hyphenated French word in front of the client.
     · Where hyphenation does not fire at all, justified French is the worst
       version of this block, not the best — long words, no breaks, gaps torn
       open to fill the line.

   Widening the column was the other option Ignacio raised, and it is the
   right instinct — more words per line means smaller adjustments per gap —
   but the measure is already at 67 characters against the 45-75 that reads
   comfortably. There is no room to widen into.

   So the raggedness stays, and the block is balanced by measure, leading and
   paragraph rhythm instead. Those are the levers that work in every browser
   and for every reader. */

/* The client's list is eight organisations they are thanking by name. It is
   bulleted, because it is bulleted in the source they sent — this shipped with
   a left rule and no markers on my reasoning that bullets make a thank-you
   read as a checklist, which was a preference dressed up as a principle, and
   it lost the reader the one signal that says "this is a list of names, not
   more prose". The rule went when the bullets arrived: marking the same block
   twice is one accessory too many.

   Medium weight, because these are the proper nouns the section exists to
   name — the only place in eight paragraphs where the weight should lift. The
   marker stays regular and quiet so the name is what carries. Their own
   semicolons end each line and are left exactly as they wrote them. */
.prose__list {
  list-style: none;
  margin: 0 0 1.4em;
  padding: 0;
}
.prose__list li {
  position: relative;
  margin: 0 0 0.55em;
  padding-left: 1.5em;
  font-weight: 500;
  /* Never justified, even where the paragraphs are: these lines are short
     enough that justification would tear single names apart. */
  text-align: left;
}
.prose__list li:last-child { margin-bottom: 0; }
.prose__list li::before {
  content: '';
  position: absolute;
  left: 0.15em;
  /* Centred on the first line's x-height rather than its box, so the marker
     sits with the word and not above it. */
  top: 0.62em;
  width: 0.3em;
  height: 0.3em;
  background: var(--text);
}

/* --- Drawn figures: the pie ----------------------------------------------
   Figure 1 is a pie, at the client's request. Plain SVG and CSS, no chart
   library, the same rule as figure 2's bars. (Figures 3 and 4, the second
   pie and the grouped columns, were cut on the client's Aug 28 revision;
   their rules went with them.)

   The slice colours are Inno-Centre's own palette (Ardoise / Acier / Bronze /
   Quartz and their tints), and their ORDER around the pie is not decorative:
   the brand is near-neutral, chroma 0.001 to 0.059, so hue separates almost
   nothing and lightness has to do the work. Ordering the pie so neighbours sit
   far apart, the wrap from last slice to first included, gives a worst
   adjacent pair of dE 18.3 on the nine-part pie, against a floor of 15.
   Reordering the slices means reordering these colours; do not treat the
   list as arbitrary.

   Marbre is absent on purpose: at 1.19:1 against this surface it is not a
   visible mark. Ardoise 25% is in for the opposite reason — dropping it left
   nine slices for nine colours, no ordering freedom, and a worst pair of 14.9.

   Colour is not the only channel: every part is named and valued in the key
   beside the chart, which is the relief the low surface-contrast tones (Quartz
   at 1.39:1, Ardoise 25% at 1.56:1) require to be used at all. */
.viz {
  /* Space between the rule under the head and the drawing. The bar figures set
     their own rhythm through the first bar's label; a drawing has no such lead-in
     and sat hard against the rule without this. */
  margin-top: clamp(1rem, 2.5vh, 2rem);
  display: grid;
  /* A fixed share for the drawing, the rest to the key. An `auto` first track
     let the drawing's max-content squeeze the key too narrow to set its own
     rows; 18rem was measured when the grouped columns still lived here and
     the pie has no reason to differ. */
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.75rem, 4vw, 3rem);
}
@media (max-width: 47.99rem) {
  .viz { grid-template-columns: minmax(0, 1fr); justify-items: start; }
}

.viz__pie {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.viz__slice {
  /* No `fill` here: each slice carries its own fill attribute, a solid colour or
     a <pattern> baked with that colour. A CSS fill would beat the attribute and
     wipe the texture out.

     The separator between slices is the SURFACE colour, not an ink outline.
     Neighbouring steps read apart because of the gap; a stroke would add ink
     that is not data. */
  stroke: var(--bg);
  stroke-width: 2;
  stroke-linejoin: round;
  /* Long enough to read as a fade rather than a flicker as the pointer runs
     across the pie, short enough not to lag behind it. */
  transition: opacity 0.22s ease;
  cursor: default;
}
/* Focus ring on chart marks. Chrome draws its default ring on plain :focus for
   SVG elements (not :focus-visible as for HTML), so the rules below are written
   against :focus. Ring off on any focus; back on only when the focus did not
   come from a click or tap (app.js tags pointer focus with .is-pointer-focus).
   Pointer users get the pick highlight and the readout instead. */
.viz [data-viz-i]:focus { outline: 0; }
.viz [data-viz-i]:focus:not(.is-pointer-focus) { outline: 0.125rem solid var(--text); outline-offset: 0.125rem; }

/* The key is the legend AND the reference list: every part is named and
   numbered here, so identity never rests on colour alone. */
/* No gap between the rows: the same 0.3rem is carried as padding INSIDE each
   one instead. As a gap it was dead space, and running the pointer down the
   key crossed it between every pair of lines — each crossing cleared the
   highlight and lit the whole pie back up, which read as a flash (Ignacio,
   Aug 28). Contiguous rows hand the pointer straight from one to the next.
   The key pulls up by its own edge padding so the rhythm above and below it
   is unchanged. */
.viz__key { display: grid; margin: -0.15rem 0; }
.viz__row {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.15rem 0;
  transition: opacity 0.15s linear;
}
.viz__name {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
  font-size: var(--fs-sm);
  line-height: 1.35;
  color: var(--text);
}
/* Bigger than a bullet, so it reads as a swatch rather than a marker, and the
   same mark in all four keys. */
.viz__chip {
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  background: var(--c);
  transform: translateY(0.06em);
}
.viz__num {
  margin: 0 0 0 auto;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}

/* Two different marks, because the key and the pie are read differently
   (Ignacio, Aug 28).

   The KEY is a list of labels, and greying eight of nine made it look
   disabled. So no label changes colour: the hovered line takes an underline
   and every other line stays exactly as it was set.

   The PIE is a set of areas, and there the opposite is true — an outline
   drawn on the hovered slice read as a defect rather than as emphasis, while
   holding one slice at full strength and easing the others back is legible at
   a glance. Opacity only: no fill is rewritten, so a slice that comes back
   comes back to its own colour. */
.viz__row.is-on .viz__name > span:last-child,
.viz__row.is-hl .viz__name > span:last-child {
  text-decoration: underline;
  text-decoration-thickness: 0.08em;
  text-underline-offset: 0.2em;
}
.viz.is-picking .viz__slice:not(.is-on) { opacity: 0.3; }
/* The key is a run of text; an I-beam over it says the words are selectable
   copy rather than a control. The pie already sets its own default arrow. */
.viz__key, .viz__row { cursor: default; }

/* In the pinned scroller the figure has to fit the viewport HEIGHT, and the
   sizes above are width-driven. Cap the pie against vh there, the same way
   every other measurement in the scroller is, so a short window shrinks the
   drawing instead of pushing the section past the point where it still fits
   and drops to the stacked fallback. */
.is-scroller .viz__pie { max-width: 42vh; }

/* Hover/focus readout. Fixed, so it is never clipped by the pinned scroller. */
.viz-tip {
  position: fixed;
  z-index: 60;
  top: 0;
  left: 0;
  max-width: 20rem;
  padding: 0.4rem 0.6rem;
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  font-size: var(--fs-label);
  line-height: 1.35;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s linear;
}
.viz-tip.is-on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .viz__slice, .viz__row, .viz-tip { transition: none; }
}

/* --- Partner logos ------------------------------------------------------- */
/* One row, wrapping on mobile, every mark on one optical height.
   The row is centred as a block but each cell is centred in itself, so a wrap
   leaves the last line centred under the first rather than ragged left. */
/* A grid, not a wrapping flex row. Six cells at a fixed 52px height are 156px
   wide on the 3:1 artboard, which with the old gaps came to ~1216px against a
   970px column at 1440 — flex-wrap put three of them on a second row, and the
   brief asks for one row that wraps only on mobile. Equal fr columns fix that:
   the cells size to the column instead of the column sizing to the cells. Six
   across on desktop, three from 480px, two below it. */
/* The row stays in the content column. It briefly spanned all 12 so its left
   edge met the "Credits" label — but the label belongs at the BOTTOM of the
   rail, and a full-width row would have run straight through it. The label
   comes down to the row's level instead; the row keeps its column. */
.partners {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-items: center;
  /* The gap is back at Ignacio's call. The marks stay as large as it allows —
     the max-height cap that used to hold them at 52px is still gone, so they
     fill whatever the columns leave rather than being capped on top of being
     gapped. */
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
@media (min-width: 30rem) {
  .partners { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 62rem) {
  .partners { grid-template-columns: repeat(6, 1fr); }
}
/* Every file shares one 180x60 artboard from Figma, so the cell carries that
   3:1 ratio and each mark lands at the size Gabriel balanced it to. The height
   now comes from the ratio rather than being fixed, so all six stay on one
   optical height whatever the column does. Without a width here the masked
   span would collapse: it is 100% of an item with no intrinsic content to
   measure. */
.partner {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-width: 0; /* grid items floor at min-content without this, and a wide
                   mark would push its column past 1fr and overflow the row */
  aspect-ratio: 3 / 1;
  /* No max-height either. --partner-h was capping the row at 52px, which threw
     away the width the gap had just given back. The artboard ratio keeps all
     six on one optical height whatever the column does, which is what the cap
     was there to protect. */
}

/* The file supplies the shape, --swatch-dark supplies the colour, so six files
   drawn in six different colours all render as the site's ink and the row reads
   as one. Set --logo on the <li>.

   A CSS mask is alpha, not luminance: any opaque pixel becomes ink. Figma's
   export bakes the frame's canvas fill in as a full-bleed
   <rect width="180" height="60" fill="#1E1E1E"/>, which would mask to a solid
   block and hide the mark entirely. That rect is stripped from all six files in
   assets/logos. Strip it again from anything re-exported from that frame. */
.partner__mark {
  display: block;
  width: 100%;
  height: 100%;
  background-color: var(--swatch-dark);
  -webkit-mask: var(--logo) center / contain no-repeat;
          mask: var(--logo) center / contain no-repeat;
}

/* Funder marquee — a seamless horizontal loop.
   The track holds N identical copies of the set and slides left by exactly one
   set width, so the frame it lands on is the frame it started from and the
   restart is invisible. app.js measures the set and sets --marquee-shift and
   --marquee-duration; the values below are a working fallback without JS. */
.marquee {
  --marquee-gap: clamp(1.25rem, 5vw, 2.5rem);
  --marquee-item-w: clamp(8.5rem, 34vw, 11rem);
  --marquee-shift: -50%;
  --marquee-duration: 30s;

  position: relative;
  overflow: hidden;
  /* Fade both ends so logos travel in and out instead of being guillotined at
     the column edge. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--marquee-gap),
                      #000 calc(100% - var(--marquee-gap)), transparent);
          mask-image: linear-gradient(to right, transparent, #000 var(--marquee-gap),
                      #000 calc(100% - var(--marquee-gap)), transparent);
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-scroll var(--marquee-duration) linear infinite;
}

/* Let people stop it to read a logo. */
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }

.marquee__set {
  display: flex;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Spacing lives on the items, never as a flex `gap`. The loop travels by an
   exact multiple of the set width; a gap *between* sets would not be part of
   that multiple and would add half a gap of drift on every cycle. A trailing
   margin on every item makes each one occupy width + gap, so a set is exactly
   count × (width + gap) and the seam closes. */
.marquee__set > .logo-slot,
.marquee__set > .partner {
  flex: none;
  width: var(--marquee-item-w);
  margin-right: var(--marquee-gap);
}

/* --- Which of the two the partner row is, by width ------------------------ */
/* These live AFTER the .marquee rules on purpose. Written above them first and
   every declaration lost to later-wins: the track stayed flex, the animation
   kept running and .partners stayed a flex set at 1440. An override has to sit
   after the thing it overrides, not next to the thing it is about.

   From 992px the wrapper is inert — no clipping, no mask, no animation, track
   is a plain block — and .partners is the six-column grid again. app.js does
   not clone at this width either, so the grid gets exactly the six real items. */
@media (min-width: 62rem) {
  .marquee { overflow: visible; -webkit-mask-image: none; mask-image: none; }
  .marquee__track { display: block; width: auto; animation: none; }
  .partners {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: clamp(1.5rem, 3vw, 2.5rem);
  }
  .partners > .partner { width: 100%; margin-right: 0; }
}

/* Below 992px .partners IS the set: one flex row, items at a fixed width with
   a trailing margin rather than a flex gap, because the loop travels by an
   exact multiple of the set width and a gap between sets would not be part of
   that multiple. */
@media (max-width: 61.99rem) {
  .partners { display: flex; align-items: center; gap: 0; }
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--marquee-shift)); }
}

/* The global reduced-motion reset collapses every animation to 0.01ms, which on
   an infinite loop would strobe. Kill the animation outright instead and hand
   back a plain scrollable rail with a single set of logos. */
@media (prefers-reduced-motion: reduce) {
  .marquee {
    overflow-x: auto;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .marquee__track { animation: none !important; }
  .marquee__set[aria-hidden="true"] { display: none; }
}

.logo-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 9;
  padding: 1rem;
  border: var(--hairline) dashed var(--border);
  background: #2f2d2b08;
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text);
  text-align: center;
}

/* --- Summary ------------------------------------------------------------- */
.summary__grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  align-items: start;
}

.chapters {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: chapter;
}
.chapters li {
  counter-increment: chapter;
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr);
  gap: 1rem;
  align-items: baseline;
  padding-block: clamp(0.875rem, 1.8vw, 1.125rem);
  border-top: var(--hairline) solid var(--border);
  font-size: var(--fs-lg);
  line-height: 1.35;
}
.chapters li:last-child { border-bottom: var(--hairline) solid var(--border); }
/* Numbering is real here: a white paper's chapters are an ordered sequence. */
.chapters li::before {
  content: counter(chapter, decimal-leading-zero);
  font-size: var(--fs-sm);
  font-weight: 400;
  letter-spacing: var(--tracking-label);
  color: var(--text);
}

/* Placeholder cover: drawn, not an image, so nobody mistakes it for final art. */
.cover {
  position: relative;
  width: min(18rem, 70%);
  margin-inline: auto;
  aspect-ratio: 3 / 4;
  background: var(--swatch-light-gray);
  border: var(--hairline) dashed var(--swatch-sage-gray);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: clamp(1.25rem, 3vw, 2rem);
  color: var(--swatch-dark);
  box-shadow: -0.75rem 0 0 #00000026;
}
.cover__kicker {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.cover__title {
  font-family: var(--font-body);
  font-size: clamp(1.25rem, 3.5vw, 1.75rem);
  line-height: 1.15;
}
.cover__foot {
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}

@media (min-width: 60rem) {
  .summary__grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    column-gap: clamp(3rem, 6vw, 6rem);
  }
  .cover { width: 100%; margin-inline: 0; }
}

/* --- Stats --------------------------------------------------------------- */
.stats {
  display: grid;
  gap: 0;
  margin-top: clamp(2rem, 4vw, 3rem);
}

/* --- Figures ------------------------------------------------------------- */
/* Two figures, no script and no library. The bars are a <dl>: the term is the
   client's label, the definition is the bar and its number. Bars are plain
   elements rather than SVG because the labels wrap; see index.html.

   Two columns from 62rem, one below it: one figure each. */
.figures {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}
@media (min-width: 62rem) {
  .figures { grid-template-columns: 1fr 1fr; gap: clamp(3rem, 5vw, 4.5rem); }
}

.figure { margin: 0; }
.figure__head {
  padding-bottom: clamp(1rem, 2vw, 1.25rem);
  margin-bottom: clamp(1.25rem, 2.5vw, 1.75rem);
  border-bottom: var(--hairline) solid var(--border);
}
/* Body sans, not --font-display. The display serif is the hero h1's and the
   mocked-up cover's; every section heading on this page is the body face, so a
   serif on a chart title would put the page's loudest typeface one level BELOW
   a sans section heading. Separation here comes from size and the rule under
   the head, which is how the rest of the page does it. */
/* The client numbers these figures in the white paper, so the page numbers them
   too — it is how a reader ties a chart on screen back to the document. Not
   translated: "Figure 1" is the same string in both languages, so a data-i18n
   key here would only add a lookup that returns its own default. */
.figure__n {
  margin: 0 0 0.4rem;
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text);
}
/* In the pinned scroller the kicker runs INTO the title line rather than above
   it. As its own line it cost ~40px, and the scroller only engages while one
   whole figure fits the viewport (app.js measures pin.scrollHeight against
   innerHeight) — that 40px moved the cutoff from ~780px to ~820px, which is
   exactly where a 1440x900 laptop lands once browser chrome is taken off. On
   the title's line it costs nothing and the cutoff stays where it was. */
.is-scroller .figure__n {
  display: inline;
  margin: 0;
}
/* Colon, but only here. Inline the kicker and the title form one line —
   "Figure 1 : Répartition des émissions par source" — which is how a figure
   caption is punctuated, and the colon is what tells you the second half
   names the first rather than continuing it. Stacked, the number sits on its
   own line above the title and takes no colon: there would be nothing after
   it on the line to introduce.

   \00a0 before the colon, not a space. Canadian French sets a non-breaking
   space before a high punctuation mark — the same rule this file's French
   dictionary already follows in "Source : CIRAIG" and "objectif commun :" —
   and non-breaking is what stops the colon from starting the wrapped line. */
.is-scroller .figure__n::after { content: '\00a0:\2003'; }
.is-scroller .figure__title { display: inline; }

.figure__title {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-lg);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
  text-wrap: pretty;
}
/* Figure 2 is the only one whose bars are not percentages, so its unit has to
   be read before the numbers are, not after. */
.figure__unit {
  margin: 0.5rem 0 0;
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-wide);
  color: var(--text);
}

.bars { margin: 0; }
/* Label above, bar below. The alternative — label in a fixed left column —
   is what forces either truncation or a 60-character label into a narrow
   gutter, and the client's labels are not ours to shorten. */
.bar { margin-bottom: clamp(1rem, 2vw, 1.4rem); }
.bar:last-child { margin-bottom: 0; }
.bar__label {
  font-size: var(--fs-sm);
  line-height: 1.35;
  color: var(--text);
}
.bar__value {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 0.4rem 0 0;
}
.bar__track {
  flex: 1;
  height: var(--bar-h);
  background: var(--border);
}
.bar__fill {
  display: block;
  width: var(--v);
  height: 100%;
  background: var(--swatch-dark);
}
/* Tabular figures so the numbers line up down the column, and a fixed min
   width so a "< 1 %" does not shove its bar shorter than the rest. */
.bar__num {
  flex: none;
  min-width: 3.5em;
  text-align: right;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* --- Figure 2: paired bars ------------------------------------------------ */
/* Two editions of one question. Same dl language as the single bars, but each
   term takes two definitions, 2026 then 2025, told apart by fill colour: the
   ink the single bars already use for this year, Bronze for last year. Bronze
   is a brand swatch already used as data in the pie, and at ~3.2:1 on Cristal
   it clears the 3:1 WCAG asks of a graphic. The pair sits tighter than the
   groups do, so the two bars read as one term's answer. */
.bars--pair .bar__value + .bar__value { margin-top: 0.3rem; }
.bar__fill--b { background: var(--swatch-pale-taupe); }

/* The client's two totals, set as chips above the chart. Verbatim, already
   uppercase in her text, so no transform here. */
.figure__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 clamp(1rem, 2vw, 1.25rem);
}
.figure__chip {
  padding: 0.35rem 0.65rem;
  border: var(--hairline) solid var(--text);
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
}
/* Bold on the number alone. The label names it; the figure is the thing being
   read off the chip, and bolding both left nothing standing out. */
.figure__chip-v { font-weight: 600; }

/* Series legend: the pie key's swatch, repurposed for the two editions. */
.pairs__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0 0 clamp(1rem, 2vw, 1.4rem);
}
.pairs__key {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--fs-sm);
  color: var(--text);
}
.pairs__swatch {
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  background: var(--c, var(--swatch-dark));
  transform: translateY(0.06em);
}

/* The client's reading of the figure, under the chart. Her prose, so it sets
   at a prose measure rather than running the chart's full width. */
.figure__note {
  margin: clamp(1.25rem, 2.5vw, 1.75rem) 0 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  max-width: 62ch;
  color: var(--text);
}

/* One source line inside each figure. This replaces the single section-level
   .figures__source: with two figures pinned one at a time, a shared line under
   the steps belonged to neither of them. */
.figure__source {
  margin: clamp(1rem, 2vw, 1.25rem) 0 0;
  font-size: var(--fs-label);
  letter-spacing: var(--tracking-wide);
  color: var(--text);
}

/* --- Figures as a pinned sequence ---------------------------------------- */
/* Applied by app.js only, as .is-scroller on the section. Everything below is
   an enhancement over the stacked grid above it: no script, a viewport too
   short to hold one figure, or prefers-reduced-motion, and none of this
   applies — the two figures render one after another and the section is its
   own height again. That fallback is the authored state, not an afterthought.

   The section becomes the scroll track: app.js sets --figsteps and the height
   below, and the pin sticks to the top of it. Scrolling the track advances the
   figure in place; when the track is spent the page moves on to the form. */
.is-scroller { height: calc(var(--figsteps, 2) * 100svh); padding-block: 0; }
.figures-pin {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: flex;
  align-items: center;
  /* The section's own vertical rhythm has to live here now, because the
     section itself is a track and cannot carry padding without adding it to
     every step of the scroll. */
  padding-block: clamp(2rem, 5vh, var(--section-y));
}

/* One figure at a time, stacked in a single grid cell rather than positioned
   absolutely: the cell keeps the height of the tallest figure, so the pin does
   not resize as the steps change, and both stay in normal flow where a
   screen reader still reads them in order. */
.is-scroller .figures {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "stack";
  gap: 0;
}
.is-scroller .figure {
  grid-area: stack;
  opacity: 0;
  transform: translateY(1.25rem);
  transition: opacity 0.55s cubic-bezier(0.4, 0, 0.2, 1),
              transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}
.is-scroller .figure.is-active {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

/* Compaction, scroller only. Nine key rows or eight paired bars plus a head,
   a title, the step buttons and a source line have to clear ONE viewport:
   pinned content taller than the screen is content the reader cannot reach.
   Unpinned it measured 1045.7px against 900.

   So every gap in here is vh-based rather than rem or vw. The stacked fallback
   keeps the original rhythm, which has all the room it wants; these values
   only exist while the section is a track, and they shrink with the screen
   instead of holding a size the screen cannot afford. The floors are what the
   type needs to stay legible, not what looks generous. */
/* The bar is fixed at the top of the viewport and this pin is stuck to the
   same edge, so the bar sits over the section heading whenever it is showing.
   It auto-hides on the way down, which is the direction this section is read
   in, but it comes back the moment you scroll up — and a heading that is only
   covered sometimes is worse than one that is never covered. The pin reserves
   its height. Every gap below was tightened to pay for it. */
.is-scroller .figures-pin {
  padding-top: calc(var(--nav-h) + 0.5rem);
  padding-bottom: clamp(0.25rem, 1vh, 1.25rem);
}
.is-scroller .section__title {
  /* Full --fs-h2, like every other section heading. This carried a
     min(--fs-h2, 4.5vh) cap to buy height for the panel, and at 1440x900 that
     resolved to 40.5px against the 55.9px every other heading on the page
     sets — visibly the odd one out, which Ignacio caught. The panel had ~27px
     of slack, which is more than the 15px the full size costs, so the cap was
     buying room it did not need. Phones are a different matter and keep their
     own step-down below. */
  margin-bottom: clamp(0.5rem, 1vh, 1.25rem);
}
.is-scroller .figures { margin-top: clamp(0.5rem, 0.9vh, 1.1rem); }
@media (max-width: 61.99rem) {
  /* On a phone --fs-h2 is 9svw, which wraps this heading to two lines and
     spends 77px of an 844px screen on it — the single largest block after the
     bars. Stepping to the h3 size buys that back and is the difference between
     the sequence running on a phone and falling back to the stack. Scroller
     only; the heading is untouched everywhere else on the page. */
  .is-scroller .section__title { font-size: min(var(--fs-h3), 4.5vh); }
}
.is-scroller .figure__head {
  padding-bottom: clamp(0.35rem, 0.8vh, 0.9rem);
  margin-bottom: clamp(0.5rem, 1vh, 1.2rem);
}
.is-scroller .bar { margin-bottom: clamp(0.25rem, 0.55vh, 0.75rem); }
.is-scroller .bar__value { margin-top: 0.3rem; }
.is-scroller .bars--pair .bar__value + .bar__value { margin-top: 0.2rem; }
/* Figure 2's chips are the first thing under its caption, so they carry the
   same opening gap the pie gets — paid for by the shorter bars below. */
.is-scroller .figure__chips {
  margin-top: clamp(1.25rem, 3.5vh, 2.5rem);
  margin-bottom: clamp(0.5rem, 1vh, 1rem);
}
.is-scroller .pairs__legend { margin-bottom: clamp(0.5rem, 1vh, 1.1rem); }
.is-scroller .figure__note { margin-top: clamp(0.5rem, 1.2vh, 1.25rem); }
.is-scroller .figure__source { margin-top: clamp(0.4rem, 1vh, 1rem); }
.is-scroller .figsteps { margin-top: clamp(0.4rem, 0.9vh, 1.1rem); }
@media (min-width: 62rem) {
  /* Out of the flow, bottom right, stacked. Below 992px they stay under the
     charts: the heading runs the full width there and has no corner to spare,
     and the viewport is tall enough relative to the content that the bottom
     edge is not the pressure point it is on a short desktop window. */
  .is-scroller .labelled__body { position: relative; }
  .is-scroller .figsteps {
    position: absolute;
    top: auto;
    bottom: 0;
    right: 0;
    margin: 0;
    flex-direction: column;
    gap: 0.375rem;
  }
}

/* The section divider, dropped while pinned. .labelled draws a hairline across
   the top of every section, which is right when sections follow one another —
   but pinned, this one sits a few pixels under the fixed bar for the whole
   time you are in the section, where it reads as a stray rule rather than as
   the edge of anything. The stacked fallback keeps it, because there it is
   doing its actual job. */
.is-scroller .labelled { border-top: 0; padding-top: 0; }

/* Bars stop short of the full column. At the pinned width they ran the whole
   970px, which makes a 9-row chart read as a block of lines rather than a set
   of lengths you can compare — the eye needs the ragged right edge to do the
   comparing, and it cannot see one that far away from the labels. The cap is
   in rem so it holds the same measure whatever the column does. */
.is-scroller .bars { max-width: 42rem; }
/* Figure 2's chart and its prose take the cap as well. Uncapped, the columns
   ran the full 1275px column while figure 1's key stopped at the heading, and
   the client's note wrapped at its own 62ch — three different right edges in
   one section, which is what made figure 2 read as too wide and unbalanced
   (Ignacio, Aug 28). One measure for chips, legend, chart, note and source. */
.is-scroller .lgr,
.is-scroller .figure__chips,
.is-scroller .pairs__legend,
.is-scroller .figure__note,
.is-scroller .figure__source { max-width: min(100%, calc(42rem + 200px)); }
/* The drawn figures take the same cap. Their key puts the value last in the row,
   exactly like the bars do, so capping both to one measure lands every number in
   the section on a single right edge — otherwise the pies' percentages ran to
   the full 970px column while figure 2's values stopped at the heading, and the
   two figures read as two different objects. */
.is-scroller .viz {
  max-width: 42rem;
  /* ~64px between the rule under the caption and the top of the pie. The
     inherited step (~30px) read as squeezed against the rule; the pinned
     panel had the slack for it, since figure 1 is the shorter of the two. */
  margin-top: clamp(2rem, 6vh, 4rem);
}
@media (min-width: 62rem) {
  /* End where the heading ends. app.js measures the rendered heading and
     publishes --figures-measure; the 42rem above is the fallback if it never
     runs. Aligning the two right edges is what makes the block read as one
     object rather than a chart that happens to sit under a title — and since
     the number is the last thing in the row, it is the number that lands on
     the heading's edge. */
  .is-scroller .bars { max-width: var(--figures-measure, 42rem); }
  .is-scroller .viz { max-width: var(--figures-measure, 42rem); }
  /* Figure 2 takes the heading's measure PLUS 200px. The pie's key ends on
     the heading; the columns need the extra to stop reading as undersized,
     and the note and source follow them so the figure keeps one right edge
     of its own. min() so the column, not this sum, is still the ceiling. */
  .is-scroller .lgr,
  .is-scroller .figure__chips,
  .is-scroller .pairs__legend,
  .is-scroller .figure__note,
  .is-scroller .figure__source { max-width: min(100%, calc(var(--figures-measure, 42rem) + 200px)); }
}
/* --- Step buttons -------------------------------------------------------- */
/* 56px squares. They were 28px circles sitting under the charts, where two
   things went wrong: the bottom of the pin is the first thing a short viewport
   eats, so the numbers were being cropped, and at 28px they read as decoration
   rather than as the control they are.

   From 992px they move out of the flow entirely and stack in the top right of
   the section, in the space the heading's 18ch measure leaves empty. That
   takes them off the bottom edge — which is what was cropping them — and gives
   the pin back the height they were spending. */
.figsteps {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  list-style: none;
  margin: clamp(1.25rem, 3vh, 2rem) 0 0;
  padding: 0;
}
.figsteps button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.5rem;   /* 56 */
  height: 3.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  /* 500, not 700: the project self-hosts Suisse Intl Regular and Medium only,
     so 700 would be a browser-synthesised fake bold — heavier, blurrier and
     not the family. 500 is the weight this design system means by bold. */
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
/* The square IS the button now, so the hit area and the drawn thing are the
   same 56px — no more 44px box around a 28px dot. */
.figsteps button > span {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border: var(--hairline) solid var(--text);
  transition: background-color 0.25s ease, color 0.25s ease;
}
/* The active numeral reads out of the section's own surface rather than a
   named colour. It was --swatch-mint, which was right when this section was
   theme-mint and became a stray green the moment it moved to theme-cristal —
   a hardcoded colour outliving the theme it was picked for. var(--bg) is
   whatever the section is standing on, so it stays correct through the next
   theme change too. */
.figsteps button[aria-current="true"] > span {
  background: var(--swatch-dark);
  color: var(--bg);
}
@media (hover: hover) and (pointer: fine) {
  .figsteps button:hover > span { background: #2f2d2b1a; }
  .figsteps button[aria-current="true"]:hover > span { background: var(--swatch-dark); }
}

@media (prefers-reduced-motion: reduce) {
  .is-scroller .figure { transition: none; }
  .figsteps button > span { transition: none; }
}

/* Phone sizing for the steps. This block sits AFTER the base rules on purpose:
   it was written above them first and lost every declaration to later-wins,
   which is the failure mode of putting an override next to the thing it is
   conceptually about rather than next to the thing it has to beat.

   40px, not 56. On a phone the buttons are in the flow under the charts, so
   their height is height the bars cannot have — and 40 still clears the 24px
   WCAG 2.2 asks of a target. The 16px that frees pays for the gap above them,
   which was ~8px and left them sitting on the last bar. */
@media (max-width: 61.99rem) {
  .figsteps button { width: 2.5rem; height: 2.5rem; }
  .is-scroller .figsteps { margin-top: clamp(1rem, 2.4vh, 1.75rem); }
}

.stat {
  padding-block: clamp(1.75rem, 3.5vw, 2.5rem);
  border-top: var(--hairline) solid var(--border);
}
.stat__value {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(2.1875rem, 8vw, 3.5rem); /* same as an h2 — these are headlines */
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  display: block;
}
.stat__label {
  display: block;
  margin-top: 0.875rem;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--text);
  max-width: 32ch;
}

@media (min-width: 48rem) {
  .stats {
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    column-gap: clamp(1.5rem, 3vw, 3rem);
  }
  /* Cards are top-aligned and free to differ in height — French labels wrap. */
  .stat { border-top: var(--hairline) solid var(--border); }
}

/* --- CTA ----------------------------------------------------------------- */
.cta__title {
  font-size: var(--fs-h2);
  max-width: 20ch;
  margin-bottom: clamp(1rem, 2vw, 1.5rem);
}
.cta__text {
  font-size: var(--fs-lg);
  color: var(--text);
  max-width: 46ch;
  margin-bottom: clamp(2rem, 4vw, 2.5rem);
}

/* --- Footer -------------------------------------------------------------- */
/* 1:1 with the live footer. Every value below was read out of the live
   stylesheet and then checked against computed styles in the browser at 440px
   and 1440px. Mobile first, so this base block is the live ≤479px step.

   Reference, live @440 / @1440:
     contain padding    35.2 / 80 top, 44 / 160 bottom, 1px solid top border
     layout             1 col / 2 col, row-gap 66 / 40.32, col-gap 6.6 / 21.6
     side gap           35.2 / 115.2   (8svw at both)
     menu gap           11 / 72
     large link         21.34 / 24px
     nav link           16px uppercase at both
     back-to-top        63.8 (compact) / 74.88 (wide)
     bottom             gap 17.6 / 14.4, pad 47.8+35.2 / 28.8+28.8
     legal              15.4 / 16px uppercase
     wordmark           100% of the container at both */
.footer {
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  /* The live footer runs 1.3, not the body's 1.5. This matters: the list items
     are block boxes holding inline-block links, so the leading of the *item's*
     line box sets the spacing between links. At 1.5 they sit 36px apart against
     the live 28px. */
  line-height: 1.3;
}

.footer__contain {
  padding-top: 8svw;
  padding-bottom: 10svw;
  /* .footer_divider — solid, full-opacity off-white, not a faded hairline */
  border-top: 0.0625rem solid var(--swatch-off-white);
}

/* .u-grid-autofit. The live min track is min(100%, 30rem) on mobile, which is
   what forces a single column, and min(100%, 9rem) on desktop, which packs
   eight tracks and lets auto-fit collapse the six empty ones down to two. */
.footer__layout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr));
  row-gap: 15svw;
  column-gap: 1.5svw;
}

.footer__group { display: flex; justify-content: space-between; }

.footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  row-gap: 1svw;
}
/* Flex, not the default block. An inline-block link sitting on a text baseline
   makes the row height depend on the font's descent metrics — our Suisse Intl
   build reports a different descent from their web font, which came out 32.8px
   a row against the live 28.02px with every other value identical. As a flex
   item the link's own box is the row height, so the rhythm is the same
   whatever font loads. */
.footer__list > li { display: flex; }
.footer__list--nav { row-gap: 3svw; }

.footer__side {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 8svw;
}

.footer__menu {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex: 1;
  gap: 2.5svw;
}

/* Links. Same hover-line sweep as the header's locale switch — 1px, in from
   the left, clipped by the link's own overflow. */
.footer__link {
  position: relative;
  display: inline-block;
  overflow: hidden;
  padding: 0.125rem 0;
  color: var(--swatch-off-white);
  /* Flat 1rem, not --fs-body. The live footer link is 16px at 440 and at 1440;
     --fs-body clamps up to 20px on desktop and would run 4px large there. */
  font-size: 1rem;
  line-height: 1.3;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  /* Their button base carries a transparent hairline border on every link.
     It is invisible but it is 2px of height and 1px of inset, and dropping it
     leaves every link 2px short of the live one. */
  border: 0.0625rem solid transparent;
}
.footer__link--large {
  font-size: 4.85svw;
  line-height: 1; /* the live large link is 1em below 480px */
  text-transform: none;
}

.footer__line {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 0.0625rem;
  background: currentColor;
  transform: translateX(-105%);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (hover: hover) and (pointer: fine) {
  .footer__link:hover .footer__line { transform: translateX(0); }
}
.footer__link:focus-visible .footer__line { transform: translateX(0); }

/* .button_back-to-top — a square box with the triangle pinned to its top. */
.footer__totop {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  flex: none;
  align-self: flex-start;
  width: 14.5svw;
  aspect-ratio: 1;
  padding: 0.125rem 0;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
}
.footer__totop svg { width: 100%; height: auto; }
/* The live footer really does carry two of these, swapped at 991px. */
.footer__totop--wide { display: none; }

.footer__bottom-contain {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4svw;
  padding-top: 5svh;
  padding-bottom: 8svw;
}

.footer__legal {
  margin: 0;
  font-size: min(3.5svw, 1rem);
  line-height: 1.3;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--swatch-off-white);
}

/* The wordmark spans the full container width at every size. */
.footer__wordmark {
  display: block;
  width: 100%;
  color: var(--swatch-off-white);
  line-height: 0;
}
.footer__wordmark svg { width: 100%; height: auto; }

/* live: @media (max-width: 991px), i.e. 480px up */
@media (min-width: 30rem) {
  .footer__contain { padding-bottom: 20svw; }
  .footer__menu { gap: 5svw; }
  .footer__link--large { font-size: max(4.25svw, 1rem); line-height: 1.3; }
  .footer__list { row-gap: 2.5svw; }
  .footer__list--nav { row-gap: 3.5svw; }
  .footer__bottom-contain { padding-top: 2svw; }
  .footer__legal { font-size: max(2.5svw, 1rem); }
}

/* live: the base (desktop) block, 992px up */
@media (min-width: 62rem) {
  .footer__contain {
    /* --section-space--small / --section-space--large, which are clamps, not
       flat rems. Flat values left a 38px step at the 992 breakpoint. */
    padding-top: clamp(3rem, 2.428rem + 2.86vw, 5rem);
    padding-bottom: clamp(5.5rem, 4.214rem + 6.43vw, 10rem);
  }
  .footer__layout {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
    row-gap: 2.8svw;
  }
  .footer__list, .footer__list--nav { row-gap: 0; }
  .footer__link--large { font-size: max(1.38svw, 1.5rem); }
  .footer__totop--compact { display: none; }
  .footer__totop--wide { display: flex; width: min(5.2svw, 6.25rem); }
  .footer__bottom-contain { gap: 1svw; padding-top: 2svw; padding-bottom: 2svw; }
  .footer__legal { font-size: max(0.93svw, 1rem); }
}



/* --- Fix round 2 (Aug 28, 2026): logos, cover, partner strip ------------- */
/* Five partners now (cover model), color on white. The imgs keep their own
   aspect inside the cell the masked marks used to fill. */
/* Logos sit directly on the page ground, like the mono strip did — the
   white card clashed with the off-white (Gabriel, Aug 28). Color versions,
   no box; Ignacio owns further balance in the polish pass. */
.partners--color .partner { background: none; padding: 0; }
/* Each mark is set to its own share of the cell HEIGHT and lets its file's
   ratio decide the width — one number per logo, and the five land on the
   widths of Ignacio's Figma row. Letting all five fill the cell instead is
   what the row did before, and it sizes marks by how much padding their
   exporter happened to leave rather than by how big they should read.
   max-width is the floor under a narrow column: a mark shrinks rather than
   running into its neighbour. */
.partner__img {
  display: block;
  height: var(--partner-h, 100%);
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
/* Léger alone sits on the bottom of its cell, as it does in the Figma. Its
   red g drops a full descender: centred on the box, the word itself rides
   high and reads as floating against four marks that have no descender. */
.partner--baseline { align-items: flex-end; }
@media (min-width: 62rem) {
  .partners--color { grid-template-columns: repeat(5, 1fr); }
}

/* The real cover art replaces the styled placeholder card. */
.cover--art { padding: 0; background: none; border: none; outline: none; box-shadow: none; }
.cover--art .cover__img { display: block; width: 100%; height: auto; }

/* Top logo: the horizontal lockup replaces the bare IC mark. Two renders,
   swapped by the same luma logic that used to flip currentcolor. */
.site-logo__img { display: block; width: 100%; height: auto; }
.site-logo__img--light { display: none; }
.site-logo.is-on-dark .site-logo__img--dark { display: none; }
.site-logo.is-on-dark .site-logo__img--light { display: block; }

/* Footer: white lockup instead of the giant wordmark. */
.footer__lockup { display: block; width: min(38svw, 17rem); height: auto; }

/* --- Server error line (form backend, added 2026-08-28) ------------------ */
/* .field__error is display:none until a .field--invalid ancestor shows it;
   this one lives under the submit button, outside any field, so it carries
   its own switch. */
.form__server-error { margin-top: 0.75rem; }
.form__server-error.is-on { display: block; }

/* --- Fig 2: vertical grouped columns, the Leger original's shape ---------- */
/* Four groups of two columns (2026 ink, 2025 taupe), value above each
   column, category label under the group - the client's own chart geometry,
   in the site's palette. Same DOM as the horizontal build; only geometry. */
.bars--pair {
  display: flex;
  align-items: stretch;
  gap: clamp(0.9rem, 2.6vw, 2.6rem);
  height: clamp(200px, 30svh, 280px);
  margin-top: 1.25rem;
  border-bottom: var(--hairline) solid var(--border);
  padding-bottom: 0;
}
.bars--pair .bar {
  flex: 1;
  min-width: 0;
  margin: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr auto;
  column-gap: clamp(0.2rem, 0.6vw, 0.5rem);
}
.bars--pair .bar__label {
  grid-row: 2;
  grid-column: 1 / 3;
  text-align: center;
  padding: 0.6rem 0 0;
  font-size: clamp(0.72rem, 1vw, 0.95rem);
  line-height: 1.25;
}
.bars--pair .bar__value {
  grid-row: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
.bars--pair .bar__track {
  display: block;
  width: 100%;
  height: var(--v, 0%);
  background: none;
  border-radius: 0;
}
.bars--pair .bar__fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 0;
}
.bars--pair .bar__num {
  position: absolute;
  bottom: calc(var(--v, 0%) + 0.3rem);
  left: 50%;
  transform: translateX(-50%);
  font-weight: 600;
  font-size: clamp(0.78rem, 1.1vw, 1rem);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* The edition stays for screen readers only - the original chart names the
   editions once, in the legend. */
.bars--pair .bar__num .u-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); }

/* LCA registration link: visible hover (thicker underline). */
.hero__lca-link { text-underline-offset: 0.18em; transition: text-decoration-thickness 120ms ease; }
.hero__lca-link:hover,
.hero__lca-link:focus-visible { text-decoration-thickness: 0.16em; }

/* Fig 1 hover link: the hovered pair stays lit, the rest step back.
   Pointer devices only; no layout shift at any breakpoint. */
/* Hovering a legend line eases back every slice but its own, and hovering a
   slice underlines its line in the key. Pointer devices only. The rows keep
   their colour in both directions — only the pie dims. */
@media (hover: hover) {
  .viz--linked .viz__slice { transition: opacity 0.22s ease; }
  .viz--linked[data-hl] .viz__slice { opacity: 0.3; }
  .viz--linked[data-hl] .viz__slice.is-hl { opacity: 1; }
}

/* Green CTA (Gabriel, Aug 28 evening): the client's 13-word invitation kept
   verbatim but set a step calmer than the display size built for short
   titles, and the button goes solid so it reads as the action on green. */
.cta__title {
  font-size: clamp(1.5rem, 2.6svw, 2.6rem);
  line-height: 1.25;
  max-width: 26ch;
}
.theme-mint .btn {
  background: var(--swatch-dark);
  color: var(--swatch-off-white);
  border-color: var(--swatch-dark);
  padding: 1rem 2.5rem;
}
.theme-mint .btn:hover,
.theme-mint .btn:focus-visible { opacity: 0.88; }

/* Scroller-mode overrides for the vertical columns: the pin mode's compact
   horizontal-bar rules must not flatten the column geometry. */
.is-scroller .bars--pair .bar { margin: 0; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr auto; }
.is-scroller .bars--pair .bar__value { height: auto; display: flex; flex-direction: column; justify-content: flex-end; position: relative; }
.is-scroller .bars--pair .bar__value + .bar__value { margin-top: 0; }
.is-scroller .bars--pair .bar__track { height: var(--v, 0%); width: 100%; }
.is-scroller .bars--pair .bar__fill { width: 100%; height: 100%; }
.is-scroller .bars--pair .bar__num { position: absolute; bottom: calc(var(--v, 0%) + 0.3rem); left: 50%; transform: translateX(-50%); }

/* --- Fig 2, standalone chart (lgr = Leger-shaped). New class family so no
   legacy bar/scroller rule can interfere; heights are inline per column,
   scaled to the series max (50). ---------------------------------------- */
.lgr {
  display: flex;
  gap: clamp(1rem, 3vw, 2.75rem);
  margin-top: 1.5rem;
  border-bottom: var(--hairline) solid var(--border);
}
.lgr__grp { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.lgr__cols {
  /* A step shorter than they were. The columns were the tallest block in the
     pinned panel and left nothing around them; the height they give back is
     what pays for the gap under the caption and the room under the labels. */
  height: clamp(150px, 21svh, 210px);
  display: flex;
  align-items: flex-end;
  gap: clamp(0.25rem, 0.7vw, 0.6rem);
}
.lgr__col { flex: 1; position: relative; min-width: 0; }
.lgr__col--a { background: var(--swatch-dark); }
.lgr__col--b { background: var(--swatch-pale-taupe); }
.lgr__val {
  position: absolute;
  bottom: 100%;
  margin-bottom: 0.3rem;
  left: 50%;
  transform: translateX(-50%);
  font-weight: 600;
  font-size: clamp(0.75rem, 1vw, 0.95rem);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.lgr__lab {
  text-align: center;
  padding: 0.6rem 0 0;
  font-size: clamp(0.72rem, 1vw, 0.95rem);
  line-height: 1.25;
}
