/* Atelier Ai, feuille de style partagée.
   Extracted from index.html on 2026-08-04 when the single page became five.
   Loaded by index, services, methode, preuve, contact and the privacy policy,
   so every rule here has to hold on all six. Charter: brand-guidelines/export/BRAND.md
   Visual system: DESIGN.md */

/* ---- Charter typography, self-hosted -----------------------------------
   Charte §4: Archivo (titres), Instrument Sans (texte), JetBrains Mono
   (libellés, numéros, données).

   Self-hosted deliberately. The charter's own <link> points at
   fonts.googleapis.com, which would hand every visitor's IP to Google and
   contradict this site's own privacy policy. These are the same OFL files
   served from our origin. Latin subset only: it covers French completely
   (U+0000-00FF for the accents and the guillemets, U+0152-0153 for oe,
   U+2000-206F for the non-breaking spaces, U+2122 for the trademark sign).
   One variable file per family, 94 KB for all three.

   Weight ranges are declared 400-700, which every one of the three supports
   natively, so nothing is ever synthesised. */
@font-face {
  font-family: "Archivo";
  src: url("assets/fonts/archivo-latin-var.woff2") format("woff2");
  font-weight: 400 700; font-stretch: 100%; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Instrument Sans";
  src: url("assets/fonts/instrument-sans-latin-var.woff2") format("woff2");
  font-weight: 400 700; font-stretch: 100%; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("assets/fonts/jetbrains-mono-latin-var.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
}

/* Metric-matched fallbacks, so `font-display: swap` swaps without reflowing.
   Every number below was measured out of the woff2 files themselves, not
   estimated: size-adjust equalises average character width against the
   fallback that resolves, and the ascent/descent overrides restate
   each real face's own metrics at that adjusted scale. JetBrains Mono lands
   at 99.98% against Courier New because both are 0.6em monospace. */
@font-face {
  font-family: "Archivo Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 97.07%; ascent-override: 90.45%; descent-override: 21.63%; line-gap-override: 0%;
}
@font-face {
  font-family: "Instrument Sans Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 103.69%; ascent-override: 93.55%; descent-override: 24.11%; line-gap-override: 0%;
}
@font-face {
  font-family: "JetBrains Mono Fallback";
  src: local("Courier New"), local("Menlo"), local("Consolas");
  size-adjust: 99.98%; ascent-override: 102.02%; descent-override: 30%; line-gap-override: 0%;
}

/* Palette is the charter's, exactly. See brand-guidelines/export/BRAND.md §3.
   Charter names in comments so the two documents can be diffed. */
:root {
  --ink: #14132B;        /* encre */
  --ink-soft: #5A5876;   /* encre-douce */
  --paper: #FBFBF8;      /* papier */
  --surface: #EDEFFA;    /* surface */
  --accent: #3227D6;     /* indigo */
  --accent-dark: #241C9E;/* indigo-deep */
  --amber: #F7A81C;      /* ambre */
  --navy: #0A0F2E;       /* marine */
  --line: #DCDCE8;       /* trait */

  /* Two roles the charter's nine colours do not cover, previously scattered as
     13 literal #fff and rgba(255,255,255,x) values. Naming them is what lets
     DESIGN.md describe the system honestly: --blanc is the card plane, which is
     deliberately a purer white than papier so a card reads as a sheet lying on
     the page; --on-marine and its soft variant are the only text colours used
     on the dark blocks. */
  --blanc: #FFFFFF;
  --on-marine: rgba(255,255,255,0.92);
  --on-marine-soft: rgba(255,255,255,0.72);
  --erreur: #b3261e;     /* the one non-charter hue: form failure */

  /* Brand lockup sizes. These are geometry, not type roles: they are set so the
     nav and footer lockups clear the charter s 100px minimum width (BRAND.md 2). */
  --fs-lockup: 1.25rem;
  --fs-lockup-sm: 1.125rem;
  --r: 8px;
  --r-sm: 4px;
  --sp-1: 8px; --sp-2: 12px; --sp-3: 16px; --sp-4: 24px; --sp-5: 32px; --sp-6: 48px; --sp-7: 72px;
  --section-pad: 120px;
  --ease: cubic-bezier(0.53, 0, 0, 1);

  /* ---- Elevation ---------------------------------------------------------
     Three steps, and only three. Every one carries a real offset and a soft
     blur, because a zero-offset halo is decoration rather than depth.
     Two shades of the same ink so a card lifting on hover reads as the same
     light source moving, not a different material. */
  --shadow-rest: 0 1px 2px rgba(20,19,43,0.04), 0 5px 16px rgba(20,19,43,0.06);
  --shadow-raised: 0 14px 34px rgba(20,19,43,0.12);
  --shadow-overlay: 0 12px 40px rgba(20,19,43,0.16);

  /* ---- Type roles --------------------------------------------------------
     Ten named roles replace the 31 loose font-size values this file used to
     carry (eight of which sat inside a single 0.16rem band and so could not
     possibly signal different jobs). Sizes follow the charter's scale:
     Display 76 / Titre 40 / Sous-titre 20 / Corps 16 / Libellé 11. */
  --font-display: "Archivo", "Archivo Fallback", -apple-system, "Segoe UI", Arial, sans-serif;
  --font-body: "Instrument Sans", "Instrument Sans Fallback", -apple-system, "Segoe UI", Arial, sans-serif;
  --font-mono: "JetBrains Mono", "JetBrains Mono Fallback", ui-monospace, Menlo, Consolas, monospace;

  --fs-display: clamp(2.75rem, 5.3vw, 4.75rem); /* 44 -> 76 */
  --fs-title: clamp(1.75rem, 3.4vw, 2.5rem);    /* 28 -> 40 */
  --fs-page-title: clamp(2rem, 3.8vw, 3rem);    /* interior page H1 */
  --fs-lede: clamp(1.125rem, 1.6vw, 1.25rem);   /* 18 -> 20 */
  --fs-subtitle: 1.25rem;                       /* 20, card and section h3 */
  --fs-subtitle-sm: 1.0625rem;                  /* 17, denser h3 contexts */
  --fs-body: 1rem;                              /* 16, the reading floor */
  --fs-body-sm: 0.9375rem;                      /* 15, secondary prose */
  --fs-ui: 0.875rem;                            /* 14, charter §6 controls */
  --fs-label: 0.6875rem;                        /* 11, charter Libellé */
  --fs-data: clamp(1.75rem, 2.6vw, 2.125rem);   /* 28 -> 34, case figures */

  /* Charter §4 tracking. The display value sits inside the -0.04em floor. */
  --tr-display: -0.035em;
  --tr-title: -0.03em;
  --tr-subtitle: -0.015em;
  --tr-mono: 0.16em;

  /* Leading tightens as type grows: a 76px headline needs far less than 1.6. */
  --lh-display: 1.02;
  --lh-title: 1.12;
  --lh-subtitle: 1.32;
  --lh-body: 1.6;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  /* Every weight in use is a real master of the variable font, so synthesis
     would only ever be a bug. */
  font-synthesis: none;
  overflow-x: hidden;
}
a { color: inherit; }
.skip-link {
  position: fixed; top: var(--sp-2); left: var(--sp-2); z-index: 100;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
  background: var(--ink); color: var(--blanc); text-decoration: none; transform: translateY(-160%);
}
.skip-link:focus { transform: translateY(0); }
/* Charter §4: the mono is always uppercase at 0.16em and never sets a
   paragraph. That is the whole licence for using it, so every rule that
   reaches for --font-mono below is a label, a number or a measurement. */
.label {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--accent);
}
h1, h2, h3 {
  margin: 0; color: var(--ink);
  font-family: var(--font-display); font-weight: 700;
  text-wrap: balance;
}
h1 { font-size: var(--fs-display); line-height: var(--lh-display); letter-spacing: var(--tr-display); }
h2 { font-size: var(--fs-title); line-height: var(--lh-title); letter-spacing: var(--tr-title); }
h3 { font-size: var(--fs-subtitle); line-height: var(--lh-subtitle); letter-spacing: var(--tr-subtitle); }
p { color: var(--ink-soft); margin: 0; font-size: var(--fs-body); }
.wrap { max-width: 1160px; margin: 0 auto; padding: 0 var(--sp-5); }
section { padding: var(--section-pad) 0; }
.surface { background: var(--surface); }

/* ---- Nav ---- */
/* Opaque --papier, not a translucent blur. The 0.9 alpha plus blur(8px) it
   replaced let the navy cover bleed through as a grey wash on scroll, and a
   frosted bar is decoration this page never chose. A solid bar against the
   navy cover also gives a crisp edge instead of a soft one. */
/* ---- Floating nav ---- */
/* A detached capsule rather than a bar flush to the top edge, Justin's call on
   2026-08-06 from a reference he liked. The nav was already sticky; what is new
   is that it floats, with the page visibly running past it on all four sides.

   CHARTER EXCEPTION, and the only one on the site. Charter §6 says "pas de forme
   pilule" and the 2026-08-03 tell sweep cut the radius system from five values to
   four by removing the three 999px radii it found. This puts one back, so the
   count is five again and the one pill is the most prominent element on every
   page. Taken knowingly at Justin's direction, recorded here rather than quietly.
   The CTA inside it keeps its charter §6 4px radius: one exception, not two, and
   a second pill nested in the first would make the rule look abandoned rather
   than overridden.

   The header keeps the outer gutter and the pill lives on .wrap, so the capsule
   never touches the viewport edge at any width. The band around it is
   transparent, which is what lets content scroll past visibly, and
   pointer-events has to be turned off on it or that invisible full-width strip
   swallows every click aimed at whatever sits underneath. */
header.topnav {
  position: sticky; top: 0; z-index: 40;
  padding: var(--sp-2) var(--sp-4);
  pointer-events: none;
}
.topnav .wrap {
  pointer-events: auto;
  position: relative;
  max-width: 1160px; margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-5);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  /* --shadow-rest, not --shadow-overlay. The heavier token is for the cookie
     card and the modal, which are transient; a permanent element carrying a
     transient element's shadow reads as something that should be dismissed. */
  box-shadow: var(--shadow-rest);
}
/* Horizontal lockup per charter §2: symbol + wordmark, gap = 0.3 x symbol height.
   Symbol rendered at 30px, so stroke-width steps to 12 ("sous 40 px" rule). */
.brand {
  display: inline-flex; align-items: center; gap: 9px; padding: 6px 0; min-height: 44px;
  font-family: var(--font-display);
  font-size: var(--fs-lockup); font-weight: 700; letter-spacing: var(--tr-subtitle);
  color: var(--ink); text-decoration: none;
}
/* The wordmark is two words and wrapped to two lines at 375px, which doubled the
   bar's height and pushed the lockup under the CTA. It never wraps. */
.brand > span { white-space: nowrap; }
.brand-mark { width: 30px; height: 30px; flex-shrink: 0; }
.brand-ai { color: var(--accent); }
nav.links { display: flex; gap: var(--sp-5); }
nav.links a {
  text-decoration: none; font-size: var(--fs-ui); font-weight: 600; color: var(--ink-soft);
  padding-bottom: 4px; border-bottom: 2px solid transparent; transition: color 150ms var(--ease), border-color 150ms var(--ease);
}
/* .active went with the scroll spy: the nav links are page links now, and the
   active state is a static aria-current="page" in each document. */
nav.links a:hover, nav.links a[aria-current="page"] { color: var(--ink); border-color: var(--accent); }
.nav-right { display: flex; align-items: center; gap: var(--sp-4); }
/* Charter §6 exactly: flat --indigo, --papier text, 4px radius, 13px 22px,
   hover to --indigo-deep. It used to be a 220%-wide gradient whose
   background-position slid on hover, with an indigo glow shadow underneath.
   Both were removed on 2026-08-03: the charter specifies a flat fill, and a
   gradient button under a colored glow is the most recognisable tell there is
   for a page assembled from defaults rather than designed. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--paper); border: none;
  border-radius: var(--r-sm); font-size: var(--fs-ui); font-weight: 600;
  padding: 13px 22px; text-decoration: none; cursor: pointer;
  transition: background 150ms var(--ease); white-space: nowrap;
}
.btn:hover { background: var(--accent-dark); }
/* 12px padding + 1px border = the primary's 13px, and 21 + 1 = its 22. The
   two buttons therefore measure identically instead of the outline sitting
   1.5px taller, which is charter §6's reason for the odd-looking numbers. */
.btn-outline {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--ink); border: 1px solid var(--ink);
  border-radius: var(--r-sm); font-size: var(--fs-ui); font-weight: 600;
  padding: 12px 21px; text-decoration: none; transition: border-color 150ms var(--ease), color 150ms var(--ease);
}
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
/* 44x44 minimum: this is the only navigation control that exists on mobile,
   and it measured 38x32. align-items centres the three 22px bars inside the
   larger hit area so the mark itself does not change size. */
.menu-toggle {
  display: none; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; background: none; border: none; cursor: pointer;
  min-width: 44px; min-height: 44px; padding: 8px;
}
.menu-toggle span { width: 22px; height: 2px; background: var(--ink); display: block; transition: transform 200ms var(--ease), opacity 200ms var(--ease); }
.menu-toggle.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.menu-toggle.open span:nth-child(2) { opacity: 0; }
.menu-toggle.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ---- Hero cover ---- */
/* Full-bleed brand cover, drawn rather than filmed.
   It replaced a generated video clip on 2026-08-03. The clip was unusable on
   three counts, not one: it lettered the pre-rename name "Atelier IA"; it drew
   the équerre as a glowing neon tube, which charter §2 forbids; and it put an
   amber disc beside an amber "IA" wordmark, which charter §3 forbids. Drawing
   it in SVG also fixes what the clip could not: it cannot go stale when the
   brand changes, it costs 0 bytes instead of 1.9 MB, it is resolution
   independent, and there is no pillarbox because there is no fixed frame.
   The geometry inside is the charter path at exact scale, never retraced. */
.hero-cover {
  background: var(--navy); line-height: 0; position: relative;
  /* Taller cover (2026-08-03), for the full-viewport confidence of Vooban's
     entry page. Stops at 70vh rather than 100vh: this site's one job is getting
     a dirigeant to request a diagnostic, and a full-screen brand splash before
     a single word of copy is a conversion cost a 225-person firm can absorb and
     a new solo practice cannot. At 70vh the headline's first line still shows,
     which is what pulls someone down the page. */
  height: clamp(360px, 70vh, 680px);
  display: flex; align-items: center;
  overflow: hidden;
  /* Overrides the 120px section padding, which used to frame the video and
     read as dead navy above and below it. */
  padding: 0;
}
/* Capped at the viewBox's own 460px and centred, rather than stretched to fill
   the band. A `slice` fill would scale the mark up with the taller cover, and
   the mark is being redrawn, so growing it now would only make the change more
   expensive. The band gets its extra height as marine around the composition;
   the grid's vignette already fades to nothing well before the SVG's edges, so
   there is no seam where the drawing stops. Below 460px of band the SVG shrinks
   with it instead of being cropped. */
.hero-cover svg { display: block; width: 100%; height: min(460px, 100%); }

/* Drafting field: the ground the parts get squared against. Papier at very low
   alpha rather than a lighter navy, so it stays keyed to the palette. */
.cover-grid-line { stroke: rgba(251, 251, 248, 0.055); stroke-width: 1; fill: none; }
.cover-rule { stroke: rgba(251, 251, 248, 0.14); stroke-width: 1; }
.cover-tick { stroke: rgba(251, 251, 248, 0.22); stroke-width: 1.5; }
.cover-datum { stroke: rgba(251, 251, 248, 0.16); stroke-width: 1; }
/* Papier équerre on marine, not indigo: this is the inverse colorway the
   footer already uses, because indigo on marine is two dark blues and fails
   contrast. Charter §2 geometry, scaled 3.4x, protection zone intact. */
/* stroke-width and the dash length are the charter's own numbers in charter
   units: the group's scale(3.4) turns 10 into 34 and the 132-unit path into
   449 rendered units. Setting the scaled values here instead would get them
   multiplied a second time. */
.cover-square { stroke: var(--paper); stroke-width: 10; fill: none;
  stroke-linecap: round; stroke-linejoin: round; }
/* The one amber on this screen. Charter §2: the piece in progress. */
.cover-disc { fill: var(--amber); }
/* Parts already squared up against the arms. Papier tints, never a second
   amber. Slightly rounded because a machined edge is not a hairline. */
.cover-part { fill: rgba(251, 251, 248, 0.11); }
.cover-part.is-near { fill: rgba(251, 251, 248, 0.17); }

/* One authored moment: the square draws itself, the parts arrive and square
   up, the amber piece is set last. Everything eases on the site's own curve.
   CSS animations rather than requestAnimationFrame on purpose, because rAF is
   paused in background tabs and used to leave this hero blank. */
.cover-square { stroke-dasharray: 132; stroke-dashoffset: 132;
  animation: cover-draw 1100ms var(--ease) 120ms forwards; }
/* Stagger comes from each plate's own --d, not nth-of-type: the grid is also a
   <rect>, so type counting would offset every plate by one and drop the last. */
.cover-part { opacity: 0; transform: translateX(26px);
  animation: cover-settle 760ms var(--ease) var(--d, 620ms) forwards; }
.cover-disc { opacity: 0; transform: scale(0.72);
  transform-box: fill-box; transform-origin: center;
  animation: cover-set 700ms var(--ease) 1020ms forwards; }
.cover-tick, .cover-grid, .cover-rule, .cover-datum { opacity: 0;
  animation: cover-fade 900ms var(--ease) forwards; }
.cover-grid { animation-delay: 0ms; }
.cover-datum { animation-delay: 180ms; }
.cover-rule { animation-delay: 300ms; }
.cover-tick { animation-delay: 420ms; }

@keyframes cover-draw { to { stroke-dashoffset: 0; } }
@keyframes cover-settle { to { opacity: 1; transform: translateX(0); } }
@keyframes cover-set { to { opacity: 1; transform: scale(1); } }
@keyframes cover-fade { to { opacity: 1; } }

/* ---- Hero copy ---- */
/* Single column: the cover above owns the visual, so the old two-column split
   and the canvas it existed to separate are both gone. */
/* Tighter above than a normal section so the label and the headline clear the
   fold underneath the cover. */
.hero { position: relative; padding: var(--sp-7) 0 var(--section-pad); }
/* Was 68ch, which resolved against the container's 16px body size, so roughly
   650px. That was tuned for a 65px system-sans headline; it would wrap the
   76px Archivo display into five lines. Sized in rem against the actual
   display type instead. */
.hero-copy {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-4);
  max-width: min(100%, 68rem);
}
/* Archivo 500, the charter's weight for a phrase de mise en avant. It gives
   the hero one voice from headline to lede before the text face takes over,
   and it is a real master, not a synthesised weight. */
.hero p.lede {
  max-width: 54ch;
  font-family: var(--font-display); font-weight: 500;
  font-size: var(--fs-lede); line-height: 1.45; letter-spacing: var(--tr-subtitle);
}
.hero-ctas { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-2); }

/* ---- Méthode card ----
   The one floating object against the cover, taken from how Vooban anchors its
   entry page. What is not taken: their card is glass over a saturated field.
   This one is solid papier over marine, which is higher contrast and does not
   reach for blur as decoration. It is a real link to #methode carrying real
   content, so it still works with no clip in it at all. */
.hero-grid { display: grid; gap: var(--sp-6); }
.method-card { position: relative; z-index: 1; }
.method-card-link {
  display: block; text-decoration: none; color: inherit;
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--r); overflow: hidden;
  box-shadow: var(--shadow-overlay);
  transition: border-color 300ms var(--ease);
}
.method-card-link:hover { border-color: var(--accent); }
/* 16:10 rather than 4:3. At 4:3 the card ran 402px tall and the fold cut its
   footer off at 1440x900; shorter media brings the whole card, link line
   included, inside the first viewport. */
.method-card-media {
  position: relative; aspect-ratio: 16 / 10; background: var(--navy); overflow: hidden;
}
/* The clip, once it exists, fills this panel. `cover` not `contain`: the panel
   is a window onto a scene, so cropping it is correct, and there is no lettering
   in the clip for a crop to cut. */
.method-card-media video, .method-card-plate {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* Plates only, no amber. The cover already spends this screen's single amber on
   its disc, and charter §3 allows exactly one. */
.mc-plate-part { fill: rgba(251, 251, 248, 0.13); }
.mc-plate-part.is-near { fill: rgba(251, 251, 248, 0.2); }
.method-card-body { padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); }
.method-card-meta {
  display: flex; justify-content: space-between; gap: var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--tr-mono); text-transform: uppercase; color: var(--accent);
}
.method-card-meta span:last-child { color: var(--ink-soft); }
.method-card-phases { font-size: var(--fs-body-sm); color: var(--ink-soft); }
.method-card-cta { font-size: var(--fs-body-sm); font-weight: 600; color: var(--accent); }

@media (min-width: 1100px) {
  .hero-grid { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
  /* Straddles the cover/hero boundary, half on marine and half on papier.
     Justin asked for the headline to cross "a little" onto the hero field back
     in July; this is the same instinct given to an object that can carry an
     overlap without colliding with type. The grid column is what guarantees
     they never collide: the copy is bounded by its track, so there is no
     negative-margin tuning to re-measure every time the headline changes. */
  .method-card { margin-top: calc((var(--sp-7) + 180px) * -1); }
}

/* ---- Reveal ---- */
.reveal { opacity: 0; transform: translateY(28px); transition: opacity 700ms var(--ease), transform 700ms var(--ease); transition-delay: var(--rd, 0ms); }
.reveal.in { opacity: 1; transform: translateY(0); }
/* Applied by JS only, so the hero is never invisible when JS is off. */
.seq-init { opacity: 0; }

/* ---- Hero highlight sweep ---- */
.hl { position: relative; }
.hl::after {
  content: ""; position: absolute; left: 0; right: 100%; bottom: 0.04em; height: 0.1em;
  background: var(--amber);
  animation: hl-sweep 800ms var(--ease) 600ms forwards;
}
@keyframes hl-sweep { to { right: 0; } }

/* ---- Le traçage ----
   The page drafts itself against the scroll: every rule in the system draws
   rather than appears, so reading the page feels like watching a drawing get
   annotated. One idea, three places, no new colour and no new shape.

   Entirely CSS. There is no JavaScript here and no library, which matters twice
   over: this site loads zero third-party requests by policy (a CDN would hand
   each visitor's IP to someone else and contradict the privacy page), and a
   brand whose whole argument is restraint cannot afford to ship a 40 KB motion
   runtime to move four hairlines.

   Nested inside `no-preference` rather than overridden later: the static base
   above IS the reduced-motion state, so nothing has to be undone. Firefox has
   no scroll-driven animation, so it falls through to that same base and gets a
   complete, annotated page rather than a broken one. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* Every drawn line shares one keyframe: reveal, never move. clip-path
       rather than scaleX because scaling a 1px rule distorts its end ticks. */
    @keyframes trace-line { to { clip-path: inset(0 0 0 0); } }

    /* 1 · The document's own datum, still a hairline rather than a progress bar,
       but it moved inside the capsule on 2026-08-06. It used to fill the nav's
       own 1px bottom border, which the floating pill no longer has: a straight
       line across the bottom of a capsule either runs into the curves or gets
       clipped by them, and both read as a mistake.

       Inset by --sp-6 at each end instead, which clears the ~38px radius at every
       width, so it reads as a measurement drawn inside the capsule rather than as
       a bar bolted to it. `scroll(root)` needs no ranges, it is the whole document
       by definition. */
    .topnav .wrap::after {
      content: ""; position: absolute; left: var(--sp-6); right: var(--sp-6); bottom: 0; height: 1px;
      background: var(--accent); transform-origin: left; transform: scaleX(0);
      animation: trace-datum linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes trace-datum { to { transform: scaleX(1); } }

    /* 2 · Section dimension lines draw as each heading arrives. The timeline is
       named on .section-head and read by its own ::after, because an anonymous
       view() on a pseudo-element resolves inconsistently. */
    .section-head { view-timeline-name: --sh-view; view-timeline-axis: block; }
    .section-head::after {
      clip-path: inset(0 100% 0 0);
      animation: trace-line linear both;
      animation-timeline: --sh-view;
      animation-range: entry 30% entry 85%;
    }

    /* 3 · La Méthode rail. Since the phases moved to their own page they
       stack vertically at every width, so each row arrives on its own and each
       gets its own view() timeline. What draws is the row's datum, left to
       right. The five overlapping ranges on a shared container timeline that
       used to stagger one horizontal row went with the row. */
    .phase::before {
      clip-path: inset(0 100% 0 0);
      animation: trace-line linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 58%;
    }

    /* 4 · The home page's méthode walkthrough plays itself as the list crosses
       the viewport: the rail fills top to bottom, the équerre rides down it from
       the first ordinal to the last, and each phase lifts out of half opacity as
       it arrives on its own view() timeline. The mark's range stops short of the
       edges so it sits on 01 when the list arrives and on 05 when it leaves,
       rather than starting and finishing off the rail. */
    .parcours-list { view-timeline-name: --parcours; view-timeline-axis: block; }
    .parcours-rail {
      clip-path: inset(0 0 100% 0);
      animation: trace-line linear both;
      animation-timeline: --parcours;
      animation-range: cover 12% cover 78%;
    }
    @keyframes mark-travel { to { top: calc(100% - 28px); } }
    .parcours-mark {
      animation: mark-travel linear both;
      animation-timeline: --parcours;
      animation-range: cover 12% cover 78%;
    }
    /* No opacity fade on the phases themselves, deliberately. Dimming them to 42%
       and lifting each one as it arrives reads well while the timeline ticks, and
       leaves five paragraphs at roughly 2:1 contrast the moment it does not:
       someone landing mid-section, a browser that supports the syntax but does
       not advance the timeline, a compositor that drops it. The rail and the mark
       are aria-hidden decoration and can safely fail to nothing; the text cannot.
       The sequence is carried by the équerre's position, which is enough. */

    /* 5 · The Acheter / Intégrer / Construire map draws itself. The trunk fills as
       the section passes, and each branch draws left to right as its own row
       arrives, so the three routes appear in order rather than all at once. */
    .rmap::before {
      clip-path: inset(0 0 100% 0);
      animation: trace-line linear both;
      animation-timeline: view();
      animation-range: entry 15% entry 70%;
    }
    .rstep::before {
      clip-path: inset(0 100% 0 0);
      animation: trace-line linear both;
      animation-timeline: view();
      animation-range: entry 25% entry 75%;
    }
    .rstep::after { animation: node-set 400ms var(--ease) both; animation-timeline: view(); animation-range: entry 45% entry 70%; }
    @keyframes node-set { from { transform: scale(0); } to { transform: scale(1); } }

    /* 6 · The phase ordinals are the one thing on the page that keeps moving
       while you scroll rather than settling into place. Each numeral is clipped
       to nothing as its row first appears, grows upward as the row arrives, then
       drifts against the scroll for the rest of the passage. view() sits on the
       numeral itself, so five ordinals never share one progress value the way a
       container timeline would force them to. */
    /* The drift is deliberately small and centred on the passage, not a long
       travel: a numeral parked 30px above the word it belongs to looks broken
       rather than alive, and someone landing mid-page sees whatever offset the
       row happens to be at. Peak displacement is 18px, and it crosses zero
       around the middle of the row's passage, so the lockup always reads. */
    @keyframes phase-num-trace {
      0%   { clip-path: inset(100% 0 0 0); transform: translateY(18px); }
      30%  { clip-path: inset(0 0 0 0);    transform: translateY(9px); }
      100% { clip-path: inset(0 0 0 0);    transform: translateY(-12px); }
    }
    .phase-num {
      animation: phase-num-trace linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    /* 7 · The three commercial terms. Each row's datum draws left to right as the
       row arrives, so the three clauses are read one at a time rather than met as
       a wall. Same keyframe and same direction as the section dimension line: a
       hairline that draws is this page's only way of saying "new thing here". */
    .term::before {
      clip-path: inset(0 100% 0 0);
      animation: trace-line linear both;
      animation-timeline: view();
      animation-range: entry 20% entry 65%;
    }

    /* 8 · The cover keeps moving after its opening. Everything in the composition
       plays once on load and then the whole first screen is static, so scrolling
       away from it felt like leaving a photograph rather than a place. The
       composition now leads the scroll while the drafting field behind it lags,
       which is the one job parallax is actually good for: saying the mark sits in
       front of the field rather than printed on it.

       Net travel is 30px up for the mark and 10px down for the grid, which is
       small on purpose. The band is 680px tall at most and the drawing is capped
       at 460px, so there is room, but a cover whose contents visibly slide is a
       parallax effect rather than a drawing that breathes.

       Both are clipped by .hero-cover's own overflow: hidden, so nothing escapes
       the marine band, and the grid's vignette has already faded to zero opacity
       well before its edges, so moving it exposes no hard edge. */
    @keyframes cover-depart { to { transform: translateY(-30px); } }
    @keyframes cover-field-lag { to { transform: translateY(40px); } }
    .hero-cover svg {
      animation: cover-depart linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
    .cover-grid {
      animation: cover-field-lag linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
  }
}

/* ---- Cross-page transitions ---- */
/* Five pages that share a nav, a footer, a background and a type system: without
   this, every navigation is a white flash and a hard cut, which was the one place
   the site still read as five documents rather than one. Two declarations, no
   JavaScript, no bytes on the wire, and browsers without the feature navigate
   exactly as they did before.

   The nav and the footer are named so the compositor treats each as the same
   element on both sides of the navigation and holds it still while only the main
   content crosses. Without the names the entire page including the nav bar
   cross-fades, and a nav that dissolves and reassembles reads as a full reload,
   which is the opposite of the point. The nav is position: sticky, which is safe
   here because navigation resets scroll to the top: both snapshots sit at the
   top of the viewport, so there is nothing to interpolate. It also cross-fades
   its own aria-current highlight from the old page's link to the new one's,
   which is the cheapest correct way to animate that change.

   Gated on prefers-reduced-motion like everything else. Reduced motion falls back
   to the instant cut, which is the behaviour the site had before. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }

  header.topnav { view-transition-name: topnav; }
  footer { view-transition-name: sitefooter; }

  /* The UA default is 250ms linear. Slowed slightly and moved onto the page's own
     curve so a navigation eases the way every other transition here does. */
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 320ms;
    animation-timing-function: var(--ease);
  }
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .seq-init { opacity: 1; }
  .hl::after { animation: none; right: 0; }
  html { scroll-behavior: auto; }
  /* The cover holds its finished frame. Nothing is hidden, so there is no
     failure mode where the composition never appears. */
  .cover-square { animation: none; stroke-dashoffset: 0; }
  .cover-part { animation: none; opacity: 1; transform: none; }
  .cover-disc { animation: none; opacity: 1; transform: none; }
  .cover-tick, .cover-grid, .cover-rule, .cover-datum { animation: none; opacity: 1; }
  /* When the atelier clip is activated, hiding it uncovers the plate that sits
     underneath, so this panel always shows something. Add an explicit
     video.pause() alongside it at that point: display:none does not reliably
     halt playback, which is the same trap the old hero clip had. */
  .method-card-video { display: none; }
}

/* ---- Sections ---- */
/* Every measure below is set in ch, but note that ch is the width of "0",
   which in Instrument Sans runs about 30% wider than the average lowercase
   letter. A 66ch box therefore sets roughly 86 real characters, well past the
   comfortable band. These values are back-calculated from measured
   characters-per-line, not from the nominal ch figure: 52ch renders ~68. */
/* The block stays wide enough for a 40px heading to hold its line; only the
   paragraph takes the reading measure. Constraining the whole head forced
   the h2 to wrap early. */
.section-head { max-width: 68ch; margin-bottom: var(--sp-6); }
.section-head .label { display: block; margin-bottom: var(--sp-2); }
.section-head h2 { margin-bottom: var(--sp-3); }
.section-head p { max-width: 52ch; }
/* Charter §6 asks for a 1px filet in the section head. It arrives as a dimension
   line with end ticks, the same annotation the cover already uses on the
   équerre's arms, so a heading gets measured the way the mark does.
   Bounded by the section head's own measure and never full-bleed: DESIGN.md
   reserves the papier/surface alternation as the only separator between
   sections, and a rule spanning the container would read as a second one.
   Static here on purpose. Le traçage draws it, but a browser without
   scroll-driven animation still gets the annotation, not a missing element. */
.section-head::after {
  content: ""; display: block; margin-top: var(--sp-4); height: 7px;
  border-bottom: 1px solid var(--line);
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
}
/* Except in the méthode section, where the rail is already the drawn line and
   is the signature component. Measured: a dimension line lands 53px above the
   rail, so the section would show two parallel hairlines drawing at once and
   the annotation would steal from the thing it is annotating. One drawn line
   per section, and in this section the rail is it. */
#methode .section-head::after { display: none; }

/* ---- Cards ---- */
/* .grid-3 was deleted 2026-08-06 with offer 01. Its only two users were the two
   offer grids, home page and services, and both are .grid-2 now. */
/* Two offers since 2026-08-06. Deliberately not two columns across the full
   wrap: stretched over three columns' worth of width each card gets a ~560px
   measure, and a card whose paragraph runs past 100 characters stops reading as
   a card and starts reading as a column of prose in a box. Capped so the pair
   sits at about the width the outer two of three used to occupy, and left
   aligned rather than centred so the grid stays flush with the section head
   above it. */
.grid-2 {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4);
  max-width: 920px;
}
.card {
  position: relative; overflow: hidden;
  height: 100%; border-radius: var(--r); background: var(--blanc);
  border: 1px solid var(--line); padding: var(--sp-5);
  box-shadow: var(--shadow-rest);
  display: flex; flex-direction: column; gap: var(--sp-2);
  transition: border-color 300ms var(--ease);
}
/* A solid indigo rule, not an amber-to-indigo gradient. The gradient put a
   second amber on screen beside the amber "À venir" badge, against charter §3,
   and gradient-on-hover is decoration rather than feedback. The badge is gone
   with offer 01 (2026-08-06); the rule stays solid on its own merits. */
.card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 400ms var(--ease);
}
/* No lift, no shadow jump. Cards used to rise 4px into --shadow-raised while
   mini-cases rose 3px, so the two never matched, and a card that jumps under
   the cursor is the single most common hover tell. The border and the rule are
   enough to say "this one": both are state, not motion for its own sake. */
.card:hover { border-color: var(--accent); }
.card:hover::before { transform: scaleX(1); }
.card .offer-num {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--accent);
}
/* The offer card's title is an h3 inside the home page's teaser section and an h2
   on services.html, where the page H1 sits directly above it and skipping to h3
   would break the outline. Same card, same size, either level. */
.card :is(h2, h3) {
  font-size: var(--fs-subtitle); line-height: var(--lh-subtitle);
  letter-spacing: var(--tr-subtitle);
}
/* .card-top, .badge-soon and .card.soon were deleted 2026-08-06 with offer 01.
   They existed only to dress the "À venir" card: a flex row holding the ordinal
   beside the badge, an amber chip, a dashed border and an amber top rule. Nothing
   else used them, confirmed with tools/check-css.py in both directions. */
.card p { flex: 1; font-size: var(--fs-body-sm); }
/* Sans, not mono: this is a French phrase, and the charter forbids mono in
   running text. It is a spec line, so it takes the UI size and weight. */
/* Was a chip: --surface fill, 4px radius, fit-content, sitting exactly where a
   price tag sits. None of the three contains a price and none ever will, because
   principle 01 is that the price is fixed in advance, not published. A container
   shaped like a price promised a number three times and withheld it three times.
   A hairline and a line of text state the same terms and promise nothing. */
.card .card-terms {
  margin-top: auto; padding-top: var(--sp-3); border-top: 1px solid var(--line);
  font-size: var(--fs-ui); font-weight: 600; color: var(--ink);
}
.card .btn-outline { margin-top: var(--sp-1); align-self: flex-start; }

/* ---- Acheter / Intégrer / Construire: the drawn map ---- */
/* A trunk and three branches, in the page's own hairline vocabulary rather than a
   diagram pasted into it. The trunk draws top to bottom and each branch draws left
   to right as its row arrives, so the section explains itself while you scroll and
   asks nothing of you. It replaced a click-to-choose picker that was more
   machinery than the idea needed.

   Everything below is the static, finished state: the drawing lives in le traçage,
   inside prefers-reduced-motion: no-preference, so a reader who does not want
   motion, or a browser without scroll-driven animation, gets the complete map. */
.rmap { list-style: none; margin: 0; padding: 0; position: relative; }
/* The trunk. Stops at the last branch rather than running past it: there is no
   fourth route. */
.rmap::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 50%;
  width: 1px; background: var(--line);
}
/* min-height so the three nodes divide the trunk evenly. Without it the first
   step's two-line sentence made its row 99px against the others' 74px, and a map
   whose stops are unevenly spaced reads as a mistake rather than as a sequence. */
.rstep {
  position: relative; min-height: 6rem;
  display: grid; grid-template-columns: minmax(0, 1fr); align-content: center;
  gap: 4px; padding: var(--sp-4) 0 var(--sp-4) var(--sp-5);
}
/* The branch, and the node where it leaves the trunk. */
.rstep::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: var(--sp-4); height: 1px; background: var(--line);
}
.rstep::after {
  content: ""; position: absolute; left: -4px; top: calc(50% - 4px);
  width: 9px; height: 9px; border-radius: 50%; background: var(--accent);
}
.rstep:last-child::after { background: var(--amber); }
.rstep-verb {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--accent);
}
.rstep p { font-size: var(--fs-body); max-width: 62ch; }

@media (min-width: 721px) {
  .rstep { grid-template-columns: 8.5rem minmax(0, 1fr); gap: var(--sp-5); align-items: baseline; }
  .rstep { padding-left: var(--sp-6); }
  .rstep::before { width: var(--sp-5); }
}

/* ---- Commercial terms: what the client keeps ---- */
/* Three rows, each opening on a full-width hairline that draws as the row
   arrives. Deliberately NOT the rmap's trunk and branches: that figure means
   "three alternatives, pick one", and these three are cumulative, all true at
   once on the same mandate. A repeated datum says "three clauses of one
   agreement", which is what they are.

   Static, finished state here; the drawing is in le traçage below. */
.terms { list-style: none; margin: 0; padding: 0; }
.term {
  position: relative;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-2);
  padding: var(--sp-5) 0;
}
.term::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0;
  height: 1px; background: var(--line);
}
.term-name {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--accent);
}
.term p { font-size: var(--fs-body); max-width: 62ch; }
/* 12rem, wider than the rmap's 8.5rem column: "Le forfait mensuel" is three
   words and wraps to two lines at 8.5rem, which breaks the baseline alignment
   with the paragraph beside it. */
@media (min-width: 721px) {
  .term { grid-template-columns: 12rem minmax(0, 1fr); gap: var(--sp-5); align-items: baseline; }
}

/* ---- La Méthode Atelier Ai: five phases ---- */
/* Five plates on the bench. Closed, a plate shows only its number and its name
   and covers its own detail; hovering it, or focusing it from the keyboard,
   retracts the plate and the detail emerges from underneath. That is one
   gesture, sliding a cover sheet off a drawing, and it is built from two
   compositor-only properties: clip-path on the plate, opacity and transform on
   the detail. Nothing animates a track width or a box offset, so no row ever
   reflows its text mid-transition.

   Row height is fixed in rem at the tallest detail, so opening one plate never
   pushes the four below it down the page. A rail that jumps as the pointer
   crosses it is worse than no reveal at all. rem rather than px so the row
   still fits when the reader raises their browser text size.

   The detail is never display:none and never visibility:hidden, so it sits in
   the accessibility tree in both states and a screen reader reads all five
   phases whichever way the page is met. Below 901px, and on any device without
   a fine pointer, there is nothing to hover with: the collapse never applies
   and plate and detail are stacked and open. */
.methode { list-style: none; margin: 0; padding: 0; }
/* No left gutter for the node. --line on --surface is all but invisible, so a
   node parked 48px out in the surface looked unattached to the datum it is
   supposed to sit on; against the paper plate the same hairline reads clearly.
   The node now marks the datum's own left end, which is what a tick does. */
.phase { position: relative; padding-top: var(--sp-5); }
/* The row's datum, drawn left to right by le traçage, and one node sitting on
   it like a dimension tick. Both absolute, so neither becomes a grid item. */
/* z-index 2, above the plate's 1: the plate is inset:0 over the whole row and
   would otherwise bury the datum it is supposed to be squared against, leaving
   five nodes floating with no line through them. */
.phase::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  z-index: 2; background: var(--line);
}
.phase::after {
  content: ""; position: absolute; top: -5px; left: 0; width: 11px; height: 11px;
  z-index: 2; border-radius: 50%; background: var(--accent);
}
/* Charter §3, one amber per screen: it goes to the last phase, the part still
   in progress, which is exactly what the symbol's amber disc means. */
.phase:last-child::after { background: var(--amber); }

.phase-slide { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
/* A div, not a button. A heading is not phrasing content and cannot legally sit
   inside a <button>, and AT flattens a button's contents into its name anyway, so
   the five phase names carried no heading semantics at all. The plate is now a
   plain container holding a real h3, and the cue beside it is the button, with its
   hit area stretched across the plate exactly as .proj-toggle does.

   baseline, not centre: a 41px ordinal and a 20px word are one lockup, and sitting
   them on a shared baseline is what makes them read as one. */
.phase-plate {
  position: relative;
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: baseline;
  column-gap: var(--sp-4);
  width: 100%; text-align: left; padding: 0;
}
.phase-toggle {
  display: inline-flex; align-items: center; padding: 3px;
  background: none; border: none; cursor: pointer; font-family: inherit;
}
.phase-toggle::after { content: ""; position: absolute; inset: 0; }
.phase-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Off-screen but announced, and never display:none, which would take it out of the
   accessibility tree along with the sighted-only cue it labels. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* The ordinal, at display size, is the page's largest piece of type after the
   H1. Charter §4 licenses mono for labels, numbers and measurements, and this is
   a number; tracking is 0 because the label's 0.16em exists to open up 11px
   uppercase and would pull a 60px numeral apart.
   The sequence is the information here, so the numerals are earned rather than
   decoration. */
.phase-num {
  display: block;
  font-family: var(--font-mono);
  font-size: clamp(2.25rem, 4.4vw, 3.75rem); font-weight: 500;
  letter-spacing: 0; color: var(--accent);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.phase-head { display: flex; align-items: center; gap: var(--sp-3); }
.phase-name {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--fs-subtitle); letter-spacing: var(--tr-subtitle);
  line-height: var(--lh-subtitle); color: var(--ink);
}
/* The same drawn cross the FAQ uses, at the same stroke weight, so the page has
   one icon for "there is more here" rather than two. */
.phase-cue {
  display: none; width: 18px; height: 18px; flex-shrink: 0;
  fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round;
  transition: transform 300ms var(--ease);
}
.phase-detail p { font-size: var(--fs-body-sm); color: var(--ink-soft); max-width: 62ch; }
/* "Survolez une phase" is only true where there is something to hover with. On
   touch, and below 901px, every detail is already open and the instruction would
   be wrong, so it is shown from inside the same query that collapses the rows. */
.phase-hint { display: none; }
.phase + .phase { margin-top: var(--sp-5); }

@media (hover: hover) and (pointer: fine) and (min-width: 901px) {
  .phase + .phase { margin-top: 0; }
  .phase { padding-top: 0; }
  /* The tallest detail measures 7.5rem, so this is the height the content asks
     for rather than padding: every row matches the longest one and none is
     taller than it needs to be. The row cannot shift on hover in any case, since
     the detail is always in the flow and only the plate over it moves. */
  .phase-slide { align-items: center; min-height: 7.5rem; gap: 0; }
  .phase-detail { grid-column: 1; position: relative; padding: var(--sp-4) 0 var(--sp-4) 34%; }
  .phase-detail p { padding-left: var(--sp-5); }
  /* The edge the plate leaves behind, at exactly the 34% the clip cuts to, so
     the retracted plate reads as an object with a side rather than as text that
     merely stopped. It fades in with the detail it belongs to. */
  .phase-detail::before {
    content: ""; position: absolute; left: 34%; top: 0; bottom: 0;
    width: 1px; background: var(--line);
  }
  /* Opaque --paper, covering the row it shares with the detail. The plate is
     the only thing that moves; the detail underneath never changes box, which
     is why its text cannot rewrap while the transition runs. */
  .phase-plate {
    position: absolute; inset: 0; z-index: 1;
    align-content: center; cursor: pointer;
    background: var(--paper);
    clip-path: inset(0 0 0 0);
    transition: clip-path 620ms var(--ease);
  }
  .phase-cue { display: block; }
  .phase-hint { display: block; }
  .phase-detail { opacity: 0; transform: translateX(-18px); transition: opacity 460ms var(--ease) 90ms, transform 620ms var(--ease) 90ms; }

  .phase:hover .phase-plate,
  .phase:focus-within .phase-plate,
  .phase.is-open .phase-plate { clip-path: inset(0 66% 0 0); }
  .phase:hover .phase-detail,
  .phase:focus-within .phase-detail,
  .phase.is-open .phase-detail { opacity: 1; transform: none; }
  .phase:hover .phase-cue,
  .phase:focus-within .phase-cue,
  .phase.is-open .phase-cue { transform: rotate(45deg); }
}

@media (prefers-reduced-motion: reduce) {
  .phase-plate, .phase-detail, .phase-cue { transition: none; }
}

/* ---- FAQ ---- */
.faq-item { border-bottom: 1px solid var(--line); }
.faq-q {
  width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  padding: var(--sp-4) 0; display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fs-subtitle-sm); font-weight: 600; color: var(--ink);
  font-family: var(--font-body); line-height: 1.4;
}
/* Drawn, not a typed "+". The glyph inherited the body face's plus sign, so its
   weight and optical centre were whatever Instrument Sans happened to give,
   which never matched the 1px rules around it. Two stroked lines at the same
   2px weight as the rest of the icon set instead. Rotating 45° still turns the
   cross into an ×, exactly as the character did. */
.faq-q .icon {
  width: 18px; height: 18px; flex-shrink: 0; margin-left: var(--sp-3);
  fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round;
  transition: transform 200ms var(--ease);
}
.faq-item.open .faq-q .icon { transform: rotate(45deg); }
/* Was `max-height: 0 -> 280px`. Two problems with that: a fixed pixel cap
   silently truncated the longest answers once the reader raised their browser
   text size (at 150% the six answers needed 370/331/370/254/216/408px against
   a 280px ceiling, so four of them were cut mid-sentence, including the
   ownership answer), and animating max-height animates a layout property.
   The 0fr -> 1fr grid row solves both: it measures the content instead of
   guessing at it, so it cannot truncate at any text size. */
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms var(--ease); }
/* min-height: 0 is load-bearing. A grid item defaults to min-height: auto,
   which refuses to shrink below its content, so the 0fr row still leaked the
   first line of every collapsed answer. */
.faq-a > * { overflow: hidden; min-height: 0; }
.faq-item.open .faq-a { grid-template-rows: 1fr; }
/* The paragraph is the grid item, and overflow:hidden clips content but never
   padding, so a resting padding-bottom here kept every collapsed answer 24px
   tall. The spacing belongs to the open state only. */
.faq-a p { padding-bottom: 0; max-width: 53ch; }
.faq-item.open .faq-a p { padding-bottom: var(--sp-4); }

/* ---- Contact ---- */
/* Named areas rather than two column divs, because the aside has to sit under the
   intro on desktop and AFTER the form on mobile. Nesting it inside the intro
   column, which is what it looked like it wanted, put 380px of drawing between
   the lede and the first field on a phone: the same "decoration before the thing
   you came for" that the 42vh cover already had to be cut for. */
.contact-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-7);
  grid-template-areas: "intro form" "aside form";
  grid-template-rows: auto auto;
  align-items: start;
}
.contact-intro { grid-area: intro; }
.contact-formwrap { grid-area: form; }
form { display: flex; flex-direction: column; gap: var(--sp-3); }
/* Prénom and Nom share a row. minmax(0,1fr) rather than 1fr: an input carries an
   intrinsic minimum width from its size attribute, and plain 1fr lets that floor
   push the row wider than its column instead of shrinking. */
.field-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
/* Form labels take the same mono label role as every other label on the page,
   so "Nom" and "Courriel" read as the same kind of thing as "Services" or
   "Secteur" rather than as a fifth unrelated text style. */
label {
  display: block; margin-bottom: 7px;
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--ink-soft);
}
/* 16px is a functional floor, not a taste call: iOS Safari zooms the viewport
   on focus for any field under 16px, and the old 0.98rem (15.7px) tripped it. */
input, textarea {
  width: 100%; padding: var(--sp-3); border-radius: var(--r-sm); border: 1px solid var(--line);
  background: var(--paper); color: var(--ink);
  font-family: var(--font-body); font-size: var(--fs-body); line-height: 1.5;
}
/* 0.38 alpha measured 2.42:1, well under the 4.5:1 AA floor, and in the
   message field the placeholder is the only guidance on what to write.
   0.62 measures 4.87:1 and still reads as placeholder rather than as a value. */
input::placeholder, textarea::placeholder { color: rgba(20,19,43,0.62); }
textarea { resize: vertical; min-height: 110px; }
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
/* Same charter §6 metrics as .btn, which is an <a> and therefore inherits the
   body's line-height. A <button> gets the UA's `line-height: normal` instead
   and does not inherit, so this rendered 43px tall beside a 48px primary CTA:
   the same control at two sizes on one page. The 1.6 has to be stated, not
   assumed; inline-flex alone does not fix it, because the flex item's line box
   still uses the UA value. */
.submit-btn {
  display: inline-flex; align-items: center; justify-content: center;
  margin-top: var(--sp-1); background: var(--accent); color: var(--paper); border: none;
  padding: 13px 22px; border-radius: var(--r-sm);
  font-family: var(--font-body); font-size: var(--fs-ui); font-weight: 600; line-height: 1.6;
  cursor: pointer; transition: background 150ms var(--ease);
  /* Full width since 2026-08-06, from the reference Justin liked. It was
     align-self: flex-start, which left a 160px button under a 480px column of
     fields and read as an afterthought. Charter §6 fixes the padding and the
     radius, not the width, so this stays inside the system. Height is unchanged
     at 48.4px: the padding and the stated line-height still do that work. */
  align-self: stretch; width: 100%;
}
.submit-btn:hover { background: var(--accent-dark); }
.submit-btn:disabled { opacity: 0.6; cursor: default; }
.form-success {
  display: none; align-items: center; gap: var(--sp-2); padding: var(--sp-4);
  border-radius: var(--r); background: var(--surface); border: 1px solid var(--line);
}
.form-success.show { display: flex; }
.form-success .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
/* Four of the six fields are required, so the useful marker goes on the two that
   are not: four asterisks are noise, two "(facultatif)" are information. Kept
   over the red asterisks in the reference deliberately. Marking the exception
   rather than the rule is fewer marks, and "(facultatif)" is a word a screen
   reader announces as itself, where a bare * has to be explained by a legend
   most forms never write. */
.field-opt { font-weight: 400; color: var(--ink-soft); }

/* ---- Contact aside ---- */
/* Fills the column beside the form. Marine with the papier inverse colorway, and
   no amber: the footer's disc is this page's single amber under charter §3, and
   on a tall viewport both are on screen at once. */
.contact-aside {
  grid-area: aside;
  border-radius: var(--r); overflow: hidden;
  background: var(--navy);
}
/* 16/9, matching the viewBox so nothing is cropped at the column's width. 4/3 was
   tried first and ran the left column 184px past the bottom of the form, which
   reads as the page having lost its footing rather than as a deliberate overhang. */
.contact-plate { display: block; width: 100%; aspect-ratio: 16 / 9; }
.contact-facts {
  margin: 0; padding: var(--sp-4);
  display: grid; gap: var(--sp-3);
  border-top: 1px solid rgba(251, 251, 248, 0.14);
}
.contact-facts dt {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--on-marine-soft);
  margin-bottom: 4px;
}
.contact-facts dd { margin: 0; font-size: var(--fs-body-sm); color: var(--on-marine); }
/* Papier rather than indigo: indigo on marine is two dark blues and fails
   contrast, which is the same call the cover's équerre made. */
.contact-facts a { color: var(--on-marine); text-decoration-color: rgba(251, 251, 248, 0.5); }
.contact-facts a:hover { text-decoration-color: var(--on-marine); }
/* Above the submit button, not below it. It describes what pressing the button
   consents to, and it used to sit 16px underneath the button that did it. */
.form-consent { font-size: var(--fs-ui); max-width: 52ch; margin-bottom: var(--sp-3); }
.form-consent a, .form-error a { color: var(--accent); }

.form-error { display: none; margin-top: var(--sp-2); font-size: var(--fs-ui); color: var(--erreur); }
.form-error.show { display: block; }
input:focus-visible, textarea:focus-visible, .submit-btn:focus-visible, a:focus-visible, button:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* ---- Back to top ---- */
.to-top {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4); z-index: 40;
  width: 44px; height: 44px; border-radius: 50%; background: var(--ink); color: var(--blanc);
  border: none; display: flex; align-items: center; justify-content: center; cursor: pointer;
  /* visibility, not opacity alone: an opacity-0 button keeps its place in the tab
     order, so a keyboard user reached an invisible control on every page. */
  opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 200ms var(--ease);
}
/* Was the "↑" character, whose weight and baseline came from the body face
   rather than from a drawn icon set. Same 2px stroke as the FAQ cross. */
.to-top svg {
  width: 18px; height: 18px;
  fill: none; stroke: var(--paper); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.to-top.show { opacity: 1; visibility: visible; pointer-events: auto; }

footer { background: var(--navy); padding: var(--sp-5) 0; }
footer .wrap { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--sp-3); font-size: var(--fs-ui); color: var(--on-marine-soft); }
/* Inverse lockup (charter: papier equerre on marine), 28px symbol so the whole
   lockup clears the 100px minimum width. */
.footer-brand { display: inline-flex; align-items: center; gap: 9px; font-family: var(--font-display); font-size: var(--fs-lockup-sm); font-weight: 700; letter-spacing: var(--tr-subtitle); color: var(--paper); }
.footer-brand .brand-mark { width: 28px; height: 28px; }
footer a { text-decoration: none; color: var(--on-marine-soft); display: inline-flex; align-items: center; min-height: 44px; }
footer a:hover { color: var(--amber); }
/* Vertical padding gives this text button a 44px hit area without changing
   how it reads next to the footer's plain links. */
.footer-btn-link { background: none; border: none; padding: 11px 0; font: inherit; cursor: pointer; color: var(--on-marine-soft); text-decoration: none; }
.footer-btn-link:hover { color: var(--amber); }

/* ---- Cookie consent ---- */
/* Bottom-right card, not a 640px bar centred across the bottom. A notice that
   occupies one corner reads as a notice; one spanning the viewport reads as a
   barrier. Full width at the bottom under 560px, where a 380px card has
   nowhere to sit. */
.cookie-banner {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4); left: auto; z-index: 60;
  width: min(380px, calc(100vw - var(--sp-4) * 2));
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--r); box-shadow: var(--shadow-overlay);
  padding: var(--sp-4); display: none;
}
/* The back-to-top button parks in this same corner, so the banner would bury
   it. Offsetting the button instead would need the banner's height, which
   changes as its copy wraps; the button yields until the visitor has
   answered, which they only have to do once. Driven off the banner's own state
   class, so there is no JS coupling to keep in sync. */
body:has(.cookie-banner.show) .to-top { opacity: 0; pointer-events: none; }
.cookie-banner.show { display: block; }
.cookie-banner p { font-size: var(--fs-body-sm); margin-bottom: var(--sp-3); }
.cookie-banner p a { color: var(--accent); }
.cookie-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.cookie-actions .btn-outline { padding: 12px 16px; font-size: var(--fs-ui); flex: 1 1 auto; min-height: 44px; }
/* Accept and refuse are the same control at the same weight, deliberately.
   Accept used to be the indigo primary and refuse an outline, which nudges the
   choice; under Loi 25 refusing has to be as easy and as prominent as
   accepting, and this is the one site that cannot afford to get that wrong.
   Preferences is secondary, so it drops to a text link rather than
   competing as a third button. */
.cookie-prefs-link {
  background: none; border: none; cursor: pointer;
  min-height: 44px; padding: 12px 8px;
  font-family: inherit; font-size: var(--fs-body-sm); font-weight: 600;
  color: var(--ink-soft); text-decoration: underline;
  text-decoration-color: var(--amber); text-underline-offset: 2px;
  margin-left: auto;
}
.cookie-prefs-link:hover { color: var(--accent); }

.cookie-modal-overlay {
  position: fixed; inset: 0; z-index: 70; background: rgba(20,19,43,0.45);
  display: none; align-items: center; justify-content: center; padding: var(--sp-4);
}
.cookie-modal-overlay.show { display: flex; }
.cookie-modal {
  background: var(--paper); border-radius: var(--r); max-width: 480px; width: 100%;
  padding: var(--sp-5); max-height: 86vh; overflow-y: auto;
}
.cookie-modal h3 { margin-bottom: var(--sp-3); }
.cookie-pref-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--line);
}
.cookie-pref-row:last-of-type { border-bottom: none; }
.cookie-pref-row strong { display: block; font-size: var(--fs-body-sm); font-weight: 600; }
.cookie-pref-row p { font-size: var(--fs-ui); margin: 4px 0 0; }
.cookie-toggle {
  position: relative; width: 40px; height: 22px; flex-shrink: 0; margin-top: 2px;
}
.cookie-toggle input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.cookie-toggle .track {
  /* The one surviving 999px in the page, and the only justified one: a switch
     track is a capsule by definition, and squaring it reads as broken rather
     than as precise. Every other pill was flattened to --r-sm on 2026-08-03
     per charter §6, "pas de forme pilule". */
  position: absolute; inset: 0; background: var(--line); border-radius: 999px; transition: background 150ms var(--ease);
}
.cookie-toggle .thumb {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: var(--blanc); transition: transform 150ms var(--ease); box-shadow: 0 1px 3px rgba(20,19,43,0.3);
}
.cookie-toggle input:checked + .track { background: var(--accent); }
.cookie-toggle input:checked ~ .thumb { transform: translateX(18px); }
.cookie-toggle input:disabled + .track { background: var(--ink-soft); opacity: 0.4; }
.cookie-modal-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-4); flex-wrap: wrap; }

@media (max-width: 1020px) {
}

@media (max-width: 860px) {
  nav.links { display: none; }
  .menu-toggle { display: flex; }
  .grid-2 { grid-template-columns: 1fr; max-width: none; }
  /* Form before the panel: on one column the aside is decoration standing between
     the lede and the first field, and the form is what the page is for. */
  .contact-grid {
    grid-template-columns: 1fr;
    grid-template-areas: "intro" "form" "aside";
  }
  /* No display override: the band is flex-wrap, so the heading takes the row and
     the button drops to the next one on its own. */
  .cta-band .btn { margin-top: var(--sp-4); }
  :root { --section-pad: 72px; }

  /* A card floating under the capsule, not a panel welded to its underside. The
     border-top that used to join it to the old flush bar has nothing to join to
     now. --r and not the pill radius: a tall drawer shaped like a capsule wastes
     both its corners and pushes the first link inward. */
  .mobile-menu {
    display: none; flex-direction: column; gap: var(--sp-3);
    pointer-events: auto;
    max-width: 1160px; margin: var(--sp-2) auto 0;
    padding: var(--sp-4) var(--sp-5);
    background: var(--paper);
    border: 1px solid var(--line); border-radius: var(--r);
    box-shadow: var(--shadow-overlay);
  }
  .mobile-menu.open { display: flex; }
  .mobile-menu a { text-decoration: none; color: var(--ink); font-weight: 600; font-size: var(--fs-body); }
  .mobile-menu a[aria-current="page"] { color: var(--accent); }
}
@media (min-width: 861px) { .mobile-menu { display: none; } }

/* "+943 900 €" cannot wrap, so give each figure a full row on small phones. */
@media (max-width: 560px) {
  /* 42vh of a phone screen is 341px of decoration before a visitor reaches a
     single word, which pushed the H1 to 552px and both CTAs off the fold.
     The composition still reads at 244px because the crop only ever removes
     empty marine from the sides, never the mark. */
  .hero-cover { height: clamp(200px, 30vh, 260px); }

  /* Prénom and Nom stack. Side by side at 375px each field is about 148px, which
     truncates a typed family name inside its own box while it is being typed. */
  .field-row { grid-template-columns: 1fr; }

  .brand { font-size: var(--fs-lockup-sm); }
  .nav-right { gap: var(--sp-2); }
}

/* Below 480px the lockup, a 22px CTA and a 44px toggle do not fit on one bar at
   any type size that is still legible, and 320px is a real phone width. The CTA
   moves into the drawer, where it sits above the four links and is the first
   thing the menu offers. */
@media (max-width: 480px) {
  .nav-right > .btn { display: none; }
}
@media (min-width: 481px) {
  .mobile-menu > .btn { display: none; }
}

/* ---- Interior page headers ---- */
/* A modifier on .section-head, not a second component: the interior pages want
   the same dimension line, the same measure and the same rhythm, and only the
   heading size differs. --fs-display is the home page H1 and would set 76px on
   a page whose job is to name one section rather than open the site. */
.page-head h1 {
  font-size: var(--fs-page-title);
  line-height: var(--lh-title); letter-spacing: var(--tr-title);
  margin-bottom: var(--sp-3);
}
.page-head p { font-size: var(--fs-lede); max-width: 56ch; }
/* Archivo sets ™ at full glyph height, which parks a 48px mark beside the
   wordmark and reads as part of the name. Sized and raised so it annotates the
   name instead of competing with it. Mono weight, no tracking. */
.tm { font-size: 0.4em; font-weight: 500; vertical-align: 0.9em; letter-spacing: 0; }
/* The nav link and the drawer, where the type is 14px rather than 48px. The em
   ratios above hold because vertical-align in em resolves against .tm's own size,
   but the bar needs a touch more weight to stay legible that small. The wordmark
   lockup itself carries no mark: Justin took it off on 2026-08-04, and one mark at
   the most prominent first use is what convention asks for anyway. */
nav.links .tm, .mobile-menu .tm { font-size: 0.6em; vertical-align: 0.7em; font-weight: 600; }

/* ---- Photographs ---- */
/* Duotone, not a dropped-in stock frame. The files ship grayscale and the navy
   underneath supplies hue and saturation through a luminosity blend, so a
   photograph lands in the page's own two colours instead of importing a second
   palette. isolation confines the blend to this box, or it would reach for
   whatever sits behind the section. width and height are on the <img> so the
   band reserves its box before the file arrives and CLS stays at 0.

   Both photographs are stock, and neither depicts Atelier Ai, its office or a
   client, which is why no caption claims otherwise. Replace them the day there
   are photographs of the real workshop. */
.photo {
  /* <figure> carries a 40px inline margin by default, which pulled the plate
     80px narrower than every other block in the wrap. */
  margin: 0;
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: var(--r); border: 1px solid var(--line);
  background: var(--navy); box-shadow: var(--shadow-rest);
}
.photo img { display: block; width: 100%; height: auto; mix-blend-mode: luminosity; }
/* The same datum the cover draws, so the plate reads as something squared on
   the bench rather than a banner. */
.photo::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--accent);
}

/* ---- Closing call to action, interior pages ---- */
.cta-band {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-5); flex-wrap: wrap;
  margin-top: var(--section-pad); padding-top: var(--sp-6);
  border-top: 1px solid var(--line);
}
.cta-band h2 { font-size: var(--fs-title); max-width: 32ch; }

/* The contact page keeps the two-column split the section used to have, so its
   heading is an H1 in the same slot the H2 held and needs the interior size. */
.contact-grid h1 {
  margin-top: var(--sp-3);
  font-size: var(--fs-page-title);
  line-height: var(--lh-title); letter-spacing: var(--tr-title);
}
/* Was two inline margin-top values and an inline max-width, which put layout in
   the markup and out of the spacing scale. */
.contact-lede { margin-top: var(--sp-3); max-width: 42ch; }

.case-marks { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.case-mark {
  display: inline-grid; place-items: center; height: 52px; padding: 0 14px;
  background: var(--blanc); border: 1px solid var(--line); border-radius: var(--r-sm);
}
.case-mark img { display: block; width: auto; height: auto; max-height: 26px; max-width: 118px; }


/* ---- Projects: a roster card that opens ---- */
/* The detail lives inside the card you open, not in a case study further down the
   page. The 0fr -> 1fr grid row is the same disclosure the FAQ uses, for the same
   reason: it measures the content instead of guessing a pixel ceiling, so nothing
   truncates at any text size. One card open at a time, or the grid becomes a wall.

   No figures anywhere on this page. Naming a client and publishing their SKU
   counts and recovered-margin amounts pull in opposite directions, and
   confidentiality won. */
.projs {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4);
  list-style: none; margin: 0; padding: 0;
}
/* align-items: start, not the default stretch. Stretched, opening one card
   dragged its row-mate to the same height and left a 400px empty plate beside it.
   Each card now sizes to its own content, so the shut one stays shut-sized. */
@media (min-width: 721px) {
  .projs { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}

.proj {
  display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--blanc); box-shadow: var(--shadow-rest);
  transition: border-color 300ms var(--ease);
}
.proj:hover { border-color: var(--accent); }
/* No hover affordance on a card with nothing to open. */
.proj:not(:has(.proj-toggle)):hover { border-color: var(--line); }

/* position: relative for the toggle's stretched hit area, which covers the face
   and stops there: the external link inside an open detail stays clickable. */
.proj-face {
  position: relative; flex: 1;
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2);
  padding: var(--sp-5);
}
/* Deliberately NOT the mono label role, unlike every other chip on the site: one
   of these tags is "Chirurgie buccale et maxillo-faciale". Uppercase mono at
   0.16em would turn a 36-character specialty into the loudest thing in the card,
   and the charter's own rule is that mono never sets running text. These were
   deleted by accident with .mini-case on 2026-08-04, which left the three tags
   running together as one unstyled string. */
.mc-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.mc-tags span {
  font-size: var(--fs-ui); font-weight: 600; color: var(--ink-soft);
  background: var(--surface); padding: 5px 11px; border-radius: var(--r-sm);
}
.proj-name { font-size: var(--fs-subtitle-sm); }
.proj-sum { flex: 1; font-size: var(--fs-body-sm); }

/* A real button with 44px of target, whose ::after stretches the hit area across
   the whole face. That way the card reads as one clickable object without nesting
   a heading or a div inside a <button>, which is invalid. */
.proj-toggle {
  align-self: stretch;
  display: flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-1); padding: 11px 0; min-height: 44px;
  background: none; border: none; cursor: pointer; font-family: inherit;
  text-align: left; font-size: var(--fs-ui); font-weight: 600; color: var(--accent);
}
.proj-toggle::after { content: ""; position: absolute; inset: 0; }
.proj-toggle .icon {
  width: 18px; height: 18px; flex-shrink: 0;
  fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round;
  transition: transform 200ms var(--ease);
}
.proj.is-open .proj-toggle .icon { transform: rotate(45deg); }

.proj-detail { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 380ms var(--ease); }
/* min-height: 0 is load-bearing: a grid item defaults to min-height: auto and
   refuses to shrink below its content, which leaks the first line when collapsed. */
.proj-detail > * { overflow: hidden; min-height: 0; }
.proj.is-open .proj-detail { grid-template-rows: 1fr; }
/* overflow: hidden clips content but never padding, so the spacing belongs to the
   open state only or every shut card carries it. */
.proj-detail > div { padding: 0 var(--sp-5); }
.proj.is-open .proj-detail > div { padding-bottom: var(--sp-5); }

.proj-dl { margin: 0; border-top: 1px solid var(--line); padding-top: var(--sp-4); }
.proj-dl dt {
  font-family: var(--font-mono);
  font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--tr-mono);
  text-transform: uppercase; color: var(--accent); margin-bottom: 5px;
}
.proj-dl dd { margin: 0 0 var(--sp-4); font-size: var(--fs-body-sm); color: var(--ink-soft); line-height: 1.55; }
.proj-dl dd:last-of-type { margin-bottom: 0; }
.proj-link {
  display: inline-block; margin-top: var(--sp-3); padding: 6px 0; min-height: 24px;
  font-size: var(--fs-ui); font-weight: 600; color: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .proj-detail, .proj-toggle .icon { transition: none; }
}

/* ---- The méthode walkthrough on the home page ---- */
.parcours { background: var(--surface); }
/* The five phases pass under the reader on the way to the offers, and the équerre
   rides the rail from 01 to 05 as the list crosses the viewport. Two things carry
   the sequence: the rail fills by clip-path, and each phase lifts out of half
   opacity as it arrives. Everything below is the finished, static state; the
   movement lives in le traçage, inside prefers-reduced-motion: no-preference, so
   a reader who does not want motion gets all five phases at full strength. */
.parcours-list { position: relative; list-style: none; margin: 0; padding: 0 0 0 var(--sp-6); }
.parcours-rail {
  position: absolute; left: 13px; top: 6px; bottom: 6px; width: 1px;
  background: var(--line);
}
/* The mark travels by animating top rather than transform: the distance is the
   rail's height, which no transform percentage can reference. One 28px element on
   one scroll timeline, which is well inside budget. */
.parcours-mark {
  position: absolute; left: 0; top: 0; width: 28px; height: 28px;
  display: block;
}
.pstep {
  position: relative;
  display: grid; grid-template-columns: 3.25rem minmax(0, 1fr);
  gap: var(--sp-3); align-items: baseline;
  padding: var(--sp-4) 0;
}
.pstep + .pstep { border-top: 1px solid var(--line); }
/* A tick from the rail to each ordinal. Without it the rail ran down 59px of
   empty gutter and read as a margin rule beside the phases rather than as the
   spine the équerre is travelling along. Same vocabulary as the routes map. */
.pstep::before {
  content: ""; position: absolute; left: calc(var(--sp-6) * -1 + 14px); top: 2.15rem;
  width: 22px; height: 1px; background: var(--line);
}
/* The ordinals are mono because they are numbers, the charter's third mono role.
   Smaller than the méthode page's 60px: this is the passing version, not the
   destination. */
.pstep-num {
  font-family: var(--font-mono);
  font-size: 1.75rem; font-weight: 500; letter-spacing: 0; line-height: 1;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.pstep-name { font-size: var(--fs-subtitle-sm); margin-bottom: 4px; }
.pstep p { font-size: var(--fs-body-sm); max-width: 62ch; }

.parcours-more {
  display: inline-block; margin-top: var(--sp-5);
  font-size: var(--fs-ui); font-weight: 600; color: var(--accent);
  padding: 6px 0; min-height: 24px;
}
.parcours-more:hover { text-decoration: underline; text-underline-offset: 3px; }

@media (min-width: 721px) {
  .parcours-list { padding-left: 52px; }
  .pstep { grid-template-columns: 4.5rem minmax(0, 1fr); gap: var(--sp-5); }
  .pstep::before { left: -38px; top: 2.6rem; width: 24px; }
  .pstep-num { font-size: 2.5rem; }
}
