/* ==========================================
   HOME PAGE - Sotis AI
   ==========================================

   Loaded only by home.html. The hero used to live in style.css, which is
   loaded on all 19 pages; it is here now, where the rest of the landing page
   already was.

   Everything below the TESTIMONIALS marker is the recommendation component:
   a grid that collapses to six cards, plus the modal that shows a full quote.
   recommendations.js queries these class names and a set of data-reco-* hooks,
   and nothing tests them. Renaming one gives a page that renders and a block
   that never expands. The horizontal carousel that used to live here (cloned
   cards, infinite loop, arrows, progress bar) was removed: it cost one click
   per testimonial and hid how many there were, which is the only thing a list
   of eighteen named references has going for it. */

.home {
  /* The readable column. Sections span the full viewport (see the grid below)
     but their content is capped here, so every heading on the page starts on
     the same vertical line. */
  --content-width: 1008px;
  --gutter: var(--space-md);
  /* DARKER than the canvas (#0A0A0C), for the sections that need to read as a
     band: cards inside sit on --card-bg (#101013) and read as raised instead of
     dissolving. A LIGHTER band would land on the card colour and do the
     opposite. The separation is deliberately small, about 1.03:1 in relative
     luminance; the pair of hairlines carries the rest, which is how a dark
     interface bands without turning into stripes. */
  --band-bg: #050507;
}

/* The shared <main> is max-width:1200px with overflow-x:hidden, which would cap
   a full-bleed band at 1200px and cut it on wide screens. So main spans the full
   width here and the sections re-establish the column themselves, on the exact
   model of `main:has(.immo-explorer)` and `main:has(.viewer-container)`.
   Scoped via :has() so no other page is affected. */
main:has(.home) {
  max-width: none;
  padding: 0;
  overflow-x: visible;
  margin-top: 0;
}

/* Full-bleed grid: each section spans the viewport, its children land in the
   centre track. This is what lets a section carry a background band without a
   100vw breakout (which overshoots by the scrollbar width) and without an extra
   wrapper element in the template. */
.home > section {
  display: grid;
  grid-template-columns:
    [full-start] minmax(var(--gutter), 1fr)
    [content-start] minmax(0, var(--content-width)) [content-end]
    minmax(var(--gutter), 1fr) [full-end];
  padding: var(--space-xl) 0;
}

.home > section > * {
  grid-column: content;
}

.home > section + section {
  border-top: 1px solid var(--border-color);
}

/* ---- Bands ----
   Two sections sit on --band-bg. Without them the page was 6600px
   of one flat colour: every section carried the same padding, the same hairline
   and the same background, so nothing anchored the eye and the showcase read
   with the same weight as the tool list. A band brings its own edge, so the
   shared hairline is dropped where one starts or ends. */
.home > section.section-band {
  background: var(--band-bg);
  border-top: 1px solid var(--border-color);
  border-bottom: 1px solid var(--border-color);
}

.home > section.section-band + section {
  border-top: none;
}

/* ---- Section headings ----
   Left aligned, with a monospace eyebrow above. The previous design centred a
   violet title under a yellow underline pill and repeated it six times; the
   repetition was most of what made the page read as a template. */
.section-head {
  margin-bottom: var(--space-md);
}

.section-head h2 {
  font-size: var(--text-2xl);
  color: var(--text-primary);
  margin-top: 0.5rem;
}

/* ---- A note on `ch`, because every measure on this page is set with it ----
   `1ch` is the advance of "0", which in Geist is 0.663em, while running French
   text averages 0.45em per character. So a measure written as Nch renders about
   1.47 x N actual characters: the 62ch below is ~94 per line, and the 68ch on
   the case studies is ~98, both well past the ~75 of the typographic rule.
   That is deliberate: the wide, horizontally spread paragraph is the look this
   page wants. Do not "fix" these numbers down to ~50ch by applying the rule
   without looking at the page. */
.section-lead {
  margin-top: 0.7rem;
  color: var(--text-secondary);
  max-width: 62ch;
}

/* ==========================================
   1. HERO
   ========================================== */

/* The hero is the one section allowed to spend vertical space. It is sized so
   the top of the metrics band stays just visible at the fold on a laptop: that
   peek is the scroll affordance, cheaper and steadier than a bouncing chevron.
   The !important beats the shared `.home > section` padding, which is more
   specific than a single class. */
.hero {
  position: relative;
  min-height: min(72vh, 680px);
  align-content: center;
  padding-top: clamp(var(--space-xl), 8vh, 6rem) !important;
  padding-bottom: clamp(var(--space-xl), 8vh, 6rem) !important;
}

.hero-inner {
  position: relative;
  isolation: isolate;
  max-width: 46ch;
}

/* A single soft violet glow, top-left of the headline. Replaces the old
   decorative blob plus an eight-particle animation: both drew the eye away
   from the one thing this section has to say.
   Anchored on .hero-inner, NOT on .hero: the section is full-bleed now, so a
   percentage offset from its left edge would park the glow off-screen. */
.hero-inner::before {
  content: '';
  position: absolute;
  top: -22%;
  left: -24%;
  width: 620px;
  height: 420px;
  /* Sized in PERCENT of the box (not `circle at`), so the gradient is fully
     transparent before it reaches any edge. A default-sized radial gradient is
     sized to the farthest corner and still carries alpha at the box boundary,
     which showed up as a visible rectangle over the headline. */
  background: radial-gradient(
    52% 58% at 32% 42%,
    rgba(var(--violet-primary-rgb), 0.16) 0%,
    rgba(var(--violet-primary-rgb), 0.06) 45%,
    transparent 78%
  );
  filter: blur(20px);
  z-index: -1;
  pointer-events: none;
}

.hero-eyebrow {
  margin-bottom: var(--space-sm);
}

.hero-title {
  font-size: clamp(2.25rem, 6vw, var(--text-5xl));
  line-height: 1.05;
  letter-spacing: var(--tracking-tighter);
  color: var(--text-primary);
}

/* The second line steps back so the first carries the sentence. Cheaper and
   steadier than colouring a word. */
.hero-title-dim {
  color: var(--text-tertiary);
}

.hero-lead {
  margin-top: var(--space-sm);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  max-width: 52ch;
}

.hero-actions {
  margin-top: var(--space-md);
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
}

/* ==========================================
   2. METRICS
   ========================================== */

.metrics-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
}

.metric {
  padding: 0 var(--space-sm);
  border-left: 1px solid var(--border-color);
}

.metric:first-child {
  padding-left: 0;
  border-left: none;
}

.metric-value {
  font-family: var(--font-mono);
  font-size: var(--text-4xl);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--tracking-tighter);
  line-height: 1;
  color: var(--text-primary);
}

.metric-label {
  margin: 0.5rem 0 0;
  font-size: var(--text-base);
  color: var(--text-primary);
}

/* --text-secondary, not --text-tertiary: this is prose that has to be read
   (what the 8 sites and the 3 patents actually are), and tertiary at 13px is
   4.11:1, under the 4.5:1 AA needs below 18px. Tertiary stays for decoration. */
.metric-detail {
  margin: 0.35rem 0 0;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

/* ==========================================
   3. SERVICES (ce que je fais)
   ==========================================
   Six offerings on a hairline grid. The rules ARE the separation: no card
   background, no border-radius, no shadow. Six raised panels here would have
   competed with the demo thumbnails four sections down, which are the only
   things on this page that should read as objects. */

.services-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* 1px of gap over a hairline-coloured background paints the grid lines
     themselves, so each cell needs padding but no border of its own. Negative
     margins would be the alternative and they fight the full-bleed grid. */
  gap: 1px;
  background: var(--border-color);
  border-block: 1px solid var(--border-color);
}

.service {
  background: var(--bg-primary);
  padding: var(--space-sm);
}

/* The band sections carry --band-bg behind them, so a cell painted with the
   canvas colour would show as a lighter tile. Services is not a band today;
   this keeps it correct if it ever becomes one. */
.section-band .service {
  background: var(--band-bg);
}

.service-number {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--violet-primary);
  margin-bottom: 0.6rem;
}

.service h3 {
  font-size: var(--text-base);
  color: var(--text-primary);
  margin-bottom: 0.35rem;
}

.service p {
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

@media (max-width: 860px) {
  .services-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .services-grid {
    grid-template-columns: 1fr;
  }
}

/* ==========================================
   4. INTRO (qui)
   ==========================================
   Class is .intro-section, NOT .about-section: the latter was also declared in
   style.css for a deleted /about/ page, and only some of its properties were
   overridden here, so the two silently combined. */

.intro-grid {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-md);
  align-items: start;
}

.intro-photo img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-color);
  filter: grayscale(0.25);
  transition: filter var(--transition-base);
}

.intro-photo img:hover {
  filter: grayscale(0);
}

.intro-body h2 {
  font-size: var(--text-2xl);
  margin-top: 0.5rem;
}

.intro-role {
  margin-top: 0.3rem;
  color: var(--violet-primary);
  font-size: var(--text-md);
}

.intro-text {
  margin-top: var(--space-sm);
  color: var(--text-secondary);
  max-width: 64ch;
  line-height: var(--leading-relaxed);
}

.intro-institutions {
  margin-top: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.intro-awards {
  margin-top: 0.4rem;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
  max-width: 70ch;
}

.intro-links {
  margin-top: var(--space-sm);
  margin-left: -0.6rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

/* ==========================================
   5. CASES
   ========================================== */

.case {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--border-color);
}

.case:first-of-type {
  border-top: none;
  padding-top: 0;
}

.case-meta {
  position: sticky;
  top: 80px;
  align-self: start;
}

.case-client {
  margin-top: 0.5rem;
  font-size: var(--text-base);
  color: var(--text-primary);
}

.case-period {
  margin-top: 0.2rem;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* The one number a scanner leaves with. It sits in the sticky meta column, so
   it stays on screen for the whole case. Mono at --text-2xl matches .metric-value
   in the proof band above: the two are the same kind of statement, and reading
   them in the same typeface is what makes the second one land. */
.case-figure {
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--border-color);
}

.case-figure b {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--tracking-tighter);
  line-height: 1;
  color: var(--violet-primary);
}

.case-figure span {
  display: block;
  margin-top: 0.4rem;
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

.case-title {
  font-size: var(--text-xl);
  color: var(--text-primary);
  margin-bottom: var(--space-sm);
}

.case-steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.case-step p:last-child {
  margin-top: 0.35rem;
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
  max-width: 68ch;
}

/* The outcome is the only part a reader skims for, so it gets the one accent
   in this section: a violet rule, not a filled panel. */
.case-step-outcome {
  border-left: 2px solid var(--violet-fill);
  padding-left: var(--space-sm);
}

.case-step-outcome p:last-child {
  color: var(--text-primary);
}

.case-stack {
  margin-top: var(--space-sm);
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.case-stack li {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
}

/* ==========================================
   6. CLIENTS (references)
   ==========================================
   Client marks stay recognisable, but the presentation belongs to this page:
   one quiet tile, one visual footprint, and one neutral treatment. The text
   branch remains available for references that do not yet have an approved
   logo. */

.clients-marquee {
  overflow: hidden;
  position: relative;
  margin-inline: calc(var(--space-md) * -1);
  padding-inline: var(--space-md);
  mask-image: linear-gradient(to right, transparent, black 5%, black 95%, transparent);
}

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

.clients-line {
  display: flex;
  width: max-content;
  animation: clients-scroll 34s linear infinite;
}

.clients-line--reverse {
  animation-name: clients-scroll-reverse;
  animation-duration: 39s;
}

.clients-row {
  list-style: none;
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-sm);
  padding-right: var(--space-sm);
}

@keyframes clients-scroll {
  to { transform: translateX(-50%); }
}

@keyframes clients-scroll-reverse {
  from { transform: translateX(-50%); }
  to { transform: translateX(0); }
}

.client-logo {
  display: block;
  width: min(100%, 9.5rem);
  height: 2.75rem;
  object-fit: contain;
  filter: grayscale(1) invert(1);
  opacity: 0.78;
  mix-blend-mode: screen;
}

.client {
  isolation: isolate;
  flex: 0 0 clamp(10rem, 13vw, 14rem);
  min-height: 5.75rem;
  padding: var(--space-sm);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  overflow: hidden;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
}

/* Techplaces is supplied as a white mark on transparency. It therefore needs
   the non-inverted branch while the other marks use the light-on-dark branch.
   The presentation remains monochrome in both cases. */
.client-logo--light-source {
  filter: grayscale(1);
}

/* This PNG includes generous transparent margins around the actual Toulouse
   Tech Transfer mark. Scale the canvas optically so the mark occupies the
   same space as the other references without distorting it. */
.client-logo--padded-source {
  transform: scale(1.55);
}

/* The CerCo source is square while its mark is optically horizontal. Scale
   it inside the same tile so its visual weight matches the other references. */
.client-logo--cerco {
  transform: scale(1.35);
}

/* Text fallback follows the same tile geometry and visual hierarchy as a logo. */
.client-name {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 500;
  text-align: center;
  line-height: 1.25;
  max-width: 14ch;
  letter-spacing: var(--tracking-tight);
  color: var(--text-secondary);
}

/* The Phoenix source is an emblem only; keep its name visible so the
   reference remains identifiable without relying on the image itself. */
.client-name--phoenix {
  font-family: var(--font-sans);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-primary);
  white-space: nowrap;
}

/* Chat Legal.IA has no approved local logo yet. Give the text fallback a
   proper wordmark treatment, with the IA suffix acting as the accent.

   The size is capped the same way `.client-logo` is capped: this wordmark
   stands in for a logo, so it must sit inside the tile with the same margin a
   logo gets, not fill it edge to edge. At 1.35rem it measured 131px inside a
   126px content box, so `overflow: hidden` cut the "IA" off and the remaining
   text ran flush into the next tile, which reads as two overlapping tiles
   rather than as clipped text. Anything above ~1.2rem reintroduces that. */
.client-name--chat-legal {
  display: inline-flex;
  align-items: baseline;
  gap: 0.08em;
  font-family: var(--font-sans);
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  color: var(--text-primary);
  white-space: nowrap;
}

.client-name--chat-legal strong {
  color: var(--text-secondary);
  font-weight: 750;
}

.client-via {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  text-align: center;
}

/* Repli sans JavaScript : la bande s'arrete net au survol. `clients_marquee.js`
   remplace cet arret par un ralentissement (il pose `animation-play-state` en
   style inline, qui gagne sur cette regle). Garder les deux : la pause seche
   reste preferable a une bande qu'on ne peut pas arreter. */
.clients-line:hover,
.clients-line:focus-within {
  animation-play-state: paused;
}

/* ==========================================
   7. DEMOS
   ========================================== */

/* Animated previews. The grid is capped at two columns on wide screens. The
   minimum is capped by the available width as well, so the single-column
   layout below the two-column threshold always uses the full content track. */
.demos-featured {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(min(100%, 440px), 1fr));
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

/* The featured demos are read as three visual chapters. The category rows use
   the same mono eyebrow and hairline vocabulary as the rest of the home page;
   the large index gives the eye a landmark without adding another heavy title. */
.demo-category {
  --category-accent: var(--violet-primary);
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: 3.75rem minmax(0, 1fr);
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  padding: var(--space-sm) 0 0.8rem;
  border-top: 1px solid color-mix(in srgb, var(--category-accent) 45%, var(--border-color));
  border-left: 2px solid var(--category-accent);
  padding-left: var(--space-sm);
}

.demo-category:first-child {
  margin-top: 0;
}

.demo-category-index {
  color: color-mix(in srgb, var(--category-accent) 38%, transparent);
  font-family: var(--font-mono);
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: 600;
  letter-spacing: -0.08em;
  line-height: 0.8;
}

.demo-category .eyebrow {
  margin-bottom: 0.3rem;
  color: var(--category-accent);
}

.demo-category h3 {
  color: var(--text-primary);
  font-size: var(--text-lg);
  letter-spacing: var(--tracking-tight);
}

.demo-category div > p:last-child {
  margin-top: 0.25rem;
  color: var(--text-tertiary);
  font-size: var(--text-sm);
}

.demo-category-generative {
  --category-accent: var(--gray-medium);
}

.demo-category-utility {
  --category-accent: var(--yellow-primary);
}

.demo-feature {
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.demo-feature:hover {
  background: var(--surface-raised);
  border-color: var(--border-strong);
}

/* The wash is what keeps the still from reading as a flat screenshot pasted
   on a card: the app window sits in a lit corner rather than on a slab. */
/* Fixed box, image centred inside it. The captures do not share an aspect
   ratio (a map is 16:10, a three-plane viewer is much wider), and without this
   the cards of one row would start their text at different heights. A wider
   capture simply gets a little more padding above and below, which reads as
   padding rather than as letterboxing.
   Worth knowing when framing a new capture: because the image is capped on
   BOTH axes minus 2 * --media-pad, the break-even aspect is about 1.669 for a
   496px card. Below it, extra rows of pixels make the thumbnail render
   narrower rather than taller. */
/* The image is absolutely positioned rather than centred with flex, because
   `max-height: 100%` on a flex item resolves against an indefinite container
   height and is silently dropped: the first four captures (aspect 1.60, 1.55,
   1.89 and 1.43) each pushed the box to their own height, measured at 323, 331,
   309 and 357px against the 309px the aspect-ratio asks for, and the two cards
   of a row started their text up to 48px apart. Against an absolutely positioned
   child the containing block IS definite, so the percentages hold and every
   media box is exactly 16/10.
   Sizing by max-* (not object-fit) keeps the border, radius and shadow hugging
   the picture instead of a letterboxed frame around it. */
.demo-feature-media {
  --media-pad: var(--space-sm);
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background:
    radial-gradient(75% 65% at 0% 0%, var(--violet-wash-strong), transparent 68%),
    var(--bg-secondary);
  border-bottom: 1px solid var(--border-color);
}

.demo-feature-media img {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  max-width: calc(100% - 2 * var(--media-pad));
  max-height: calc(100% - 2 * var(--media-pad));
  width: auto;
  height: auto;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
}

.demo-feature-body {
  display: block;
  padding: var(--space-sm);
}

.demo-feature-body .eyebrow {
  display: block;
  margin-bottom: 0.35rem;
}

.demo-feature h3 {
  font-size: var(--text-lg);
  color: var(--text-primary);
}

.demo-feature p {
  margin-top: 0.4rem;
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
  max-width: 54ch;
}

.demo-feature .demo-go {
  margin-top: 0.9rem;
}

.demo-feature:hover .demo-go svg {
  transform: translateX(3px);
}

@media (max-width: 560px) {
  .demos-featured {
    grid-template-columns: 1fr;
  }

  .demo-category {
    grid-template-columns: 2.75rem minmax(0, 1fr);
    gap: 0.7rem;
    padding-left: 0.7rem;
  }

  .demo-feature-media {
    --media-pad: 0.6rem;
  }
}

.demo-go {
  margin-top: 0.9rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--text-sm);
  color: var(--violet-primary);
}

.demo-go svg {
  width: 14px;
  height: 14px;
  transition: transform var(--transition-fast);
}

/* ==========================================
   8. CATALOG (the door to /portfolio/)
   ========================================== */

/* The one block on the home page that leads OFF the home page. It used to be a
   card in a compact grid, sitting under a heading that says "applications that
   run", which made it read as a seventh app instead of a way out. Everything
   here exists to say "different place": its own section, a background made of
   real project stills rather than an icon, and a filled button rather than the
   small arrow link the demo cards use.

   The mosaic is a SINGLE composed image (tools/portfolio_mosaic). Eight
   separate <img> would have cost about 2.5MB on the most visited page of the
   site for a decorative element. */
.catalog-door {
  display: block;
  overflow: hidden;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  /* Opaque, so the copy always sits on a card even before the mosaic loads. */
  background: var(--card-bg);
  transition: border-color var(--transition-fast);
}

.catalog-door:hover {
  border-color: var(--border-strong);
}

/* The mosaic is a BAND IN FLOW, not a background behind the text. Two reasons.
   Contrast: the tiles are real screenshots, several of them figures on white,
   so any copy laid over them is one regenerated mosaic away from being
   unreadable. Legibility of the mosaic itself: laid behind the text it had to
   be dimmed to about 18% to keep the copy readable, at which point the thing
   meant to SHOW the catalogue showed nothing.
   In flow, it also sizes itself: aspect-ratio here matches the file exactly
   (2016x600, see tools/portfolio_mosaic), so `cover` crops nothing, and no
   padding has to be kept in sync with an absolutely positioned image. */
.catalog-mosaic {
  position: relative;
  display: block;
  aspect-ratio: 2016 / 600;
  overflow: hidden;
}

.catalog-mosaic img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.85);
  transition: transform var(--transition-slow);
}

.catalog-door:hover .catalog-mosaic img {
  transform: scale(1.03);
}

/* Fades the band into the card instead of stopping at a hard edge, and takes
   the top corners down a notch so a bright tile never touches the border.
   Tokens and color-mix only: DesignTokenTest fails on a brand hex written
   outside style.css, including inside a comment. */
.catalog-mosaic::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--bg-primary) 38%, transparent) 0%,
    color-mix(in srgb, var(--bg-primary) 12%, transparent) 26%,
    color-mix(in srgb, var(--card-bg) 60%, transparent) 74%,
    var(--card-bg) 94%
  );
}

.catalog-body {
  position: relative;
  display: block;
  padding: 0 var(--space-md) var(--space-md);
  /* Pulled up into the fade: the copy starts where the band has already gone
     to card colour, so the two read as one object rather than as an image with
     a caption under it. */
  margin-top: calc(-1 * var(--space-xs));
  max-width: 62ch;
}

.catalog-body .eyebrow {
  display: block;
  margin-bottom: 0.35rem;
  color: var(--violet-primary);
}

.catalog-body h2 {
  font-size: var(--text-2xl);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}

.catalog-lead {
  margin-top: 0.6rem;
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* Mono, so it reads as technical signage like the other metadata lines. But
   --text-secondary, not --text-tertiary, despite looking exactly like the
   decorative counters tertiary is meant for: this line IS the claim the block
   makes ("31 projets · 5 domaines · depuis 2015"), the densest information on
   the card. The three figures live in facts.yaml and nowhere else, and
   PortfolioGateTest compares them to the portfolio's own YAML so they cannot
   go stale, which is not something you do to decoration. At 13px, tertiary
   measures 4.11:1 against --bg-primary where AA asks for 4.5:1 below 18px. */
.catalog-stats {
  margin-top: 0.8rem;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* A filled button, not the small arrow link the demo cards use: the whole point
   of this block is that it does not do the same thing as its neighbours. */
.catalog-cta {
  margin-top: var(--space-sm);
}

.catalog-cta svg {
  transition: transform var(--transition-fast);
}

/* The CTA is a <span> inside the <a>, so .btn-primary:hover only fires when the
   pointer is literally on the button: hovering the mosaic or the lead used to
   move the arrow inside a button that stayed unlit, an affordance wired up
   halfway. The whole card is the link, so the whole card has to light it.
   Only the ONE declaration that .btn-primary:hover changes is restated here,
   by its token; padding, radius and the colour transition already come from
   the .btn-primary class the span carries, so nothing is duplicated and a
   restyle of the button in style.css still reaches this card. */
.catalog-door:hover .catalog-cta {
  background-color: var(--white);
}

.catalog-door:hover .catalog-cta svg {
  transform: translateX(3px);
}

/* The band keeps its width, so on a narrow screen its natural 3.36:1 ratio
   would flatten it to about 110px and turn eight readable stills into a strip
   of confetti. A taller box lets `cover` crop the sides instead: fewer tiles,
   still legible, which is the point of showing them at all. */
@media (max-width: 640px) {
  .catalog-mosaic {
    aspect-ratio: 16 / 10;
  }

  .catalog-body {
    padding: 0 var(--space-sm) var(--space-sm);
    max-width: none;
  }
}

/* ==========================================
   9. ENGAGEMENTS
   ========================================== */

/* Two columns, so the four items make a clean 2x2. auto-fit with a 280px floor
   resolved to three columns at 1008px and left the fourth item orphaned beside
   two empty cells; a 2x2 also gives each blurb a comfortable measure instead of
   a 30-character ribbon. */
.engagements-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md);
}

@media (max-width: 640px) {
  .engagements-list {
    grid-template-columns: 1fr;
    gap: var(--space-sm);
  }
}

.engagement {
  display: flex;
  gap: 0.8rem;
  align-items: flex-start;
}

.engagement-icon {
  flex-shrink: 0;
  color: var(--text-tertiary);
  margin-top: 0.15rem;
}

.engagement-icon svg {
  width: 18px;
  height: 18px;
}

.engagement h3 {
  font-size: var(--text-base);
  color: var(--text-primary);
}

.engagement p {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}

/* ==========================================
   10. STACK
   ========================================== */

.stack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-md);
}

.stack-col h3 {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  /* These are the column headings, not decoration: they need to be legible at
     12px, where --text-tertiary is 4.11:1 and AA asks for 4.5:1. */
  color: var(--text-secondary);
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--border-color);
}

.stack-col ul {
  list-style: none;
  margin-top: 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.stack-col li {
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

/* ==========================================
   12. CLOSING
   ========================================== */

.closing {
  text-align: center;
}

.closing h2 {
  font-size: var(--text-3xl);
  color: var(--text-primary);
}

.closing p {
  margin: 0.8rem auto 0;
  max-width: 46ch;
  color: var(--text-secondary);
  font-size: var(--text-lg);
}

.closing-actions {
  margin-top: var(--space-md);
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  justify-content: center;
  align-items: center;
}

/* ==========================================
   RESPONSIVE (sections above; the testimonial
   component carries its own further down)
   ========================================== */

@media (max-width: 900px) {
  .metrics-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-sm) 0;
  }
  .metric:nth-child(odd) {
    padding-left: 0;
    border-left: none;
  }
}

@media (max-width: 768px) {
  /* Horizontal room comes from --gutter now (the section is full-bleed and its
     side padding would sit OUTSIDE the gutter tracks, doubling the inset). */
  .home > section {
    padding: var(--space-lg) 0;
  }
  .home {
    --gutter: var(--space-sm);
  }
  .intro-grid {
    grid-template-columns: 1fr;
    gap: var(--space-sm);
  }
  .intro-photo {
    max-width: 160px;
  }
  .case {
    grid-template-columns: 1fr;
    gap: var(--space-sm);
  }
  .case-meta {
    position: static;
  }
  .hero-actions .btn-primary,
  .hero-actions .btn-secondary {
    flex: 1 1 auto;
    justify-content: center;
  }
}

@media (max-width: 480px) {
  .metrics-grid {
    grid-template-columns: 1fr;
  }
  .metric {
    padding-left: 0;
    border-left: none;
  }
}
/* ==========================================
   TESTIMONIALS / RECOMMANDATIONS
   ========================================== */

/* (Le padding de cette section vient de `.home > section`, plus specifique. Une
   regle `.testimonials-section { padding: ... }` a vecu ici sans jamais
   s'appliquer ; ne pas la reintroduire.) */

/* La grille occupe la meme colonne de contenu que le reste de la page
   (--content-width), centree dans main : l'alignement des sections du dessus
   est conserve. */
.testimonials-block {
    max-width: var(--content-width);
    margin: 0 auto;
}

/* GRILLE, plus carrousel horizontal.
   Le carrousel affichait une carte a 70% de la largeur, ses voisines depassant
   de 15% de chaque cote, et demandait une fleche par temoignage. Dix-huit
   recommandations nommees sont un actif : leur nombre est l'argument, et un
   carrousel le cache par construction. La grille en montre six d'un coup et
   annonce le total sur le bouton.
   auto-fit avec un plancher de 280px : trois colonnes dans la colonne de 1008px,
   deux vers 900px, une sous 620px, sans media query. */
/* `stretch` et non `start` : les cartes d'une rangee s'alignent sur la plus haute
   d'entre elles. C'est ce qui remplace la hauteur fixe des cartes (voir le
   commentaire de .testimonial-card) : meme resultat visuel, sans rogner le texte. */
.testimonials-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-sm);
    align-items: stretch;
}

/* Repli : pose par recommendations.js, jamais present dans le HTML servi. Sans
   JS les dix-huit cartes restent visibles, ce qui est le bon defaut (le bouton,
   lui, reste masque). */
.testimonials-grid.is-collapsed .testimonial-card.is-extra {
    display: none;
}

.testimonials-more {
    margin-top: var(--space-md);
    display: flex;
    justify-content: center;
}

.testimonials-more[hidden] {
    display: none;
}

/* Le chevron pivote quand la liste est depliee : l'etat du bouton se lit sans
   avoir a relire son libelle. */
.testimonials-more [aria-expanded="true"] svg {
    transform: rotate(180deg);
}

.testimonials-more svg {
    transition: transform var(--transition-base);
}

/* Plus de hauteur FIXE : `min-height` + `align-items: stretch` sur la grille.
   Les cartes d'une meme rangee gardent la meme hauteur (c'est ce qu'on voulait),
   mais c'est desormais la rangee qui s'adapte a son contenu, et non le contenu
   qui se fait couper pour tenir dans une hauteur decidee d'avance.

   Pourquoi ce changement : la hauteur fixe rationnait le corps de texte APRES
   coup, via le `overflow: hidden` de .testimonial-body. Le budget de lignes
   restant dependait donc de la hauteur du pied de carte, qui varie avec la
   longueur du nom et du titre. Concretement, "Jonathan Curot, M.D., Ph.D. /
   Dr. in Medicine and in Neuroscience, Neurologist" occupe trois lignes de pied
   et volait deux lignes de citation a sa propre carte : son extrait etait coupe
   la ou celui du voisin passait. Et ce rognage-la est INVISIBLE au test
   `text.scrollHeight > text.clientHeight` (c'est le PARENT qui coupe, pas le
   clamp), donc il survivait a la verification. Mesurer .testimonial-body pour
   controler ce point, pas .testimonial-text. */
.testimonial-card {
    display: flex;
    flex-direction: column;
    min-height: 340px;
    padding: var(--space-md);
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
}

/* En-tete de carte : icone citation (le numero est desormais au niveau des
   fleches, plus sur chaque carte). */
.testimonial-head {
    display: flex;
    align-items: center;
    margin-bottom: var(--space-xs);
}

.testimonial-quote-icon {
    color: var(--violet-primary);
    opacity: 0.5;
    line-height: 0;
}

.testimonial-quote-icon svg {
    width: 28px;
    height: 28px;
}

.testimonial-body {
    position: relative;
    overflow: hidden;
}

/* Le clamp est un FILET DE SECURITE, plus le mecanisme d'affichage : la vignette
   recoit deja un extrait borne (cle `highlight` du YAML, longueur verifiee par
   RecommendationsHighlightTest). 8 lignes est le budget qui absorbe le plus long
   extrait dans la colonne la plus etroite ; si le clamp se declenche encore,
   c'est qu'un extrait a grossi, pas que la carte est trop petite. */
.testimonial-text {
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.95rem;
    line-height: 1.65;
    display: -webkit-box;
    -webkit-line-clamp: 8;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Degrade en bas du texte tronque */
.testimonial-card.is-clamped .testimonial-body::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2.4rem;
    background: linear-gradient(to bottom, transparent, var(--card-bg));
    pointer-events: none;
}

/* min-height rather than padding: the label was a 74x16 target, under the 40px
   minimum, and inflating it with padding would have moved the text down. */
.testimonial-readmore {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    margin-top: 0;
    padding: 0;
    background: none;
    border: none;
    color: var(--violet-primary);
    font-weight: var(--font-weight-medium);
    font-size: 0.9rem;
    cursor: pointer;
}

.testimonial-readmore:hover {
    color: var(--violet-dark);
    text-decoration: underline;
}

.testimonial-author {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-top: auto;
    padding-top: var(--space-sm);
    border-top: 1px solid var(--border-color);
}

.testimonial-avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    border: 1px solid var(--border-strong);
}

.testimonial-avatar-initials {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--violet-dark);
    color: var(--white);
    font-weight: var(--font-weight-bold);
    font-size: 1rem;
}

.testimonial-author-info {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1;
}

.testimonial-name {
    font-weight: var(--font-weight-bold);
    color: var(--text-primary);
    font-size: 0.95rem;
}

.testimonial-role {
    color: var(--text-secondary);
    font-size: 0.8rem;
    line-height: 1.3;
}

/* The icon stays 20px; the box around it reaches the 40px minimum target. The
   negative right margin keeps the visual footprint unchanged, absorbed by the
   card's own padding. */
.testimonial-link {
    flex-shrink: 0;
    color: var(--text-secondary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 40px;
    margin-right: -10px;
    transition: color var(--transition-base);
}

.testimonial-link:hover {
    color: var(--violet-dark);
}

.testimonial-link svg {
    width: 20px;
    height: 20px;
}

/* ----- Modale temoignage complet ----- */

.reco-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}

.reco-modal[hidden] {
    display: none;
}

.reco-modal-overlay {
    position: absolute;
    inset: 0;
    background: var(--overlay-dark);
    backdrop-filter: blur(2px);
}

.reco-modal-dialog {
    position: relative;
    width: 100%;
    max-width: 640px;
    max-height: 85vh;
    overflow-y: auto;
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--space-lg);
}

.reco-modal-close {
    position: absolute;
    top: var(--space-sm);
    right: var(--space-sm);
    width: 36px;
    height: 36px;
    border-radius: var(--radius-full);
    border: none;
    background: var(--bg-secondary);
    color: var(--text-primary);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: all var(--transition-base);
}

.reco-modal-close:hover {
    background: var(--violet-dark);
    color: var(--white);
}

.reco-modal-close:focus-visible {
    outline: 2px solid var(--violet-primary);
    outline-offset: 2px;
}

.reco-modal-close svg {
    width: 20px;
    height: 20px;
}

.reco-modal-header {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
    padding: var(--space-md);
    background: linear-gradient(135deg, var(--violet-primary) 0%, var(--violet-dark) 100%);
    border-radius: var(--radius-md);
    /* La regle navbar de style.css est desormais scopee `body > header` et
       n'atteint plus cet element ; ces deux lignes ne neutralisent plus rien. */
    position: static;
    z-index: auto;
    box-shadow: var(--shadow-md);
}

.reco-modal-avatar {
    width: 64px;
    height: 64px;
    flex-shrink: 0;
    border-radius: 50%;
    background-size: cover;
    background-position: center;
    border: 2px solid var(--white);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.reco-modal-avatar.is-initials {
    background: var(--violet-dark);
    color: var(--white);
    font-weight: var(--font-weight-bold);
    font-size: 1.4rem;
}

.reco-modal-ident {
    display: flex;
    flex-direction: column;
}

.reco-modal-name {
    font-size: 1.2rem;
    font-weight: var(--font-weight-bold);
    color: var(--white);
}

.reco-modal-role {
    color: var(--white);
    opacity: 0.9;
    font-size: 0.9rem;
}

.reco-modal-body {
    color: var(--text-secondary);
    line-height: 1.75;
}

.reco-modal-body p {
    margin: 0 0 var(--space-sm);
}

.reco-modal-body p:last-child {
    margin-bottom: 0;
}

.reco-modal-profile {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: var(--space-md);
    color: var(--violet-primary);
    font-weight: var(--font-weight-medium);
    font-size: 0.9rem;
    text-decoration: none;
}

.reco-modal-profile:hover {
    color: var(--violet-dark);
}

.reco-modal-profile svg {
    width: 16px;
    height: 16px;
}

body.reco-modal-open {
    overflow: hidden;
}

@media (max-width: 600px) {
    .clients-marquee {
        margin-inline: calc(var(--space-sm) * -1);
        padding-inline: var(--space-sm);
    }

    .client {
        flex-basis: 10rem;
    }

    /* Une seule colonne : plus de rangee a aligner, donc plus de raison
       d'imposer un plancher. La carte fait la taille de son contenu. */
    .testimonial-card {
        min-height: 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    .clients-line {
        animation: none;
    }

    .clients-line .clients-row[aria-hidden="true"] {
        display: none;
    }
}
