/* ==========================================
   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);
  /* The tray: a step DARKER than the canvas, for the sections that need to
     read as a band. The white cards inside (--card-bg) float on it, with their
     hairline and shadow, instead of dissolving into the page. The pair of
     hairlines on the band edges carries the rest of the separation. */
  --band-bg: var(--bg-secondary);
}

/* 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;
  /* The closing block is dark and touches the white footer directly: no strip
     of canvas between the two. */
  margin-bottom: 0;
}

main:has(.home) + .site-footer {
  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-section) 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;
}

/* ---- The one dark block ----
   The closing keeps its .section-band class but leaves the tray: it is the
   single inverse (graphite) block of the page, and the only place the yellow
   button sits, because yellow is only legible ringed by ink. The text and
   hairline tokens are re-pointed inside it, so every rule below that names
   them stays correct without a dark variant of its own. */
.home > section.closing {
  --text-primary: var(--text-inverse);
  --text-secondary: var(--text-inverse-secondary);
  --border-color: var(--border-inverse);
  --hover-bg: var(--hover-bg-inverse);
  --focus-ring: var(--accent-on-inverse);
  background:
    radial-gradient(60% 120% at 50% 0%, color-mix(in srgb, var(--accent) 28%, transparent), transparent 70%),
    var(--surface-inverse);
  border: 0;
  color: var(--text-primary);
}

/* ---- 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-3xl);
  letter-spacing: var(--tracking-tighter);
  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 is wider than the average character of
   running French text. So a measure written as Nch renders noticeably more
   than N actual characters: the 62ch below and the 68ch on the case studies
   both land 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;
  isolation: isolate;
  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;
}

/* A faint dot grid behind the hero, faded out by a radial mask centred on
   the headline. Absolutely positioned, so it takes no cell of the section
   grid. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: radial-gradient(color-mix(in srgb, var(--text-primary) 10%, transparent) 1px, transparent 1.2px);
  background-size: 22px 22px;
  -webkit-mask-image: radial-gradient(ellipse 70% 75% at 28% 45%, black 25%, transparent 75%);
  mask-image: radial-gradient(ellipse 70% 75% at 28% 45%, black 25%, transparent 75%);
  pointer-events: none;
}

/* Wide enough for the display title to keep its two lines; the lead below
   carries its own, narrower measure. */
.hero-inner {
  position: relative;
  isolation: isolate;
  max-width: 36rem;
}

/* 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%,
    color-mix(in srgb, var(--accent) 12%, transparent) 0%,
    color-mix(in srgb, var(--accent) 4%, transparent) 45%,
    transparent 78%
  );
  filter: blur(20px);
  z-index: -1;
  pointer-events: none;
}

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

.hero-title {
  font-size: clamp(var(--text-display-md), 6.2vw, var(--text-5xl));
  font-weight: var(--font-weight-heavy);
  line-height: 1.02;
  letter-spacing: var(--tracking-display);
  color: var(--text-primary);
}

/* The end of the sentence takes the accent: the first line states, the
   violet one lands. The only coloured words of the page. */
.hero-title-dim {
  color: var(--accent);
}

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

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

/* The two hero actions are the largest buttons of the site (~48px tall). */
.hero-actions .btn-primary,
.hero-actions .btn-secondary {
  padding: 0.8rem 1.4rem;
  font-size: var(--text-md);
}

/* ==========================================
   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-sans);
  font-size: var(--text-4xl);
  font-weight: var(--font-weight-bold);
  letter-spacing: -0.035em;
  font-variant-numeric: tabular-nums;
  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), so it takes the stronger
   of the two greys. Tertiary stays for metadata. */
.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 in ONE white panel with internal hairlines. The rules are the
   separation between cells: six separate raised cards here would have
   competed with the demo cards four sections down. */

.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: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.service {
  background: var(--card-bg);
  padding: 1.25rem;
}

/* Cells stay white on a band too: the panel is a card, whatever it sits on. */
.section-band .service {
  background: var(--card-bg);
}

.service-number {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--accent);
  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);
  box-shadow: var(--shadow-md);
}

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

.intro-role {
  margin-top: 0.3rem;
  color: var(--accent);
  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. Same face, weight and tabular figures
   as .metric-value in the proof band above: the two are the same kind of
   statement, and reading them alike 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-sans);
  font-size: var(--text-2xl);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tighter);
  line-height: 1;
  color: var(--accent);
}

.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(--accent-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;
  background: var(--card-bg);
  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); }
}

/* Dark marks on a white tile. `multiply` drops the white backgrounds some
   sources carry (a JPEG cannot be transparent). */
.client-logo {
  display: block;
  width: min(100%, 9.5rem);
  height: 2.75rem;
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.72;
  mix-blend-mode: multiply;
  transition: opacity var(--transition-fast);
}

/* Text wordmarks carry the same weight as the greyed image logos. */
.client-name--phoenix,
.client-name--chat-legal {
  opacity: 0.72;
  transition: opacity var(--transition-fast);
}

.client:hover .client-logo,
.client:hover .client-name--phoenix,
.client:hover .client-name--chat-legal {
  opacity: 1;
}

.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(--card-bg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

/* Techplaces is supplied as a white mark on transparency. It is therefore the
   one mark that gets inverted, to dark, while the others keep their own dark
   ink. The presentation remains monochrome in both cases. */
.client-logo--light-source {
  filter: grayscale(1) invert(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: var(--text-2xs);
  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: var(--text-lg);
  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(--accent);
  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) 66%, transparent);
  font-family: var(--font-mono);
  font-size: clamp(var(--text-display-sm), 5vw, var(--text-display-xl));
  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(--text-secondary);
}

/* The yellow family as readable ink: the bright yellow is illegible as text
   on a light surface. */
.demo-category-utility {
  --category-accent: var(--spark-ink);
}

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

.demo-feature:hover {
  background: var(--card-bg);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-hover);
}

/* 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(--accent-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 color-mix(in srgb, var(--text-primary) 14%, transparent);
  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(--accent);
}

.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-xl);
  /* Opaque, so the copy always sits on a card even before the mosaic loads. */
  background: var(--card-bg);
  box-shadow: var(--shadow-md);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

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

/* 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.95);
  transition: transform var(--transition-slow);
}

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

/* Fades the bottom of the band into the white card instead of stopping at a
   hard edge. No veil at the top: a light veil would grey the dark tiles.
   Roles and color-mix only: DesignTokenTest fails on a colour literal written
   outside tokens.css, including a hex inside a comment. */
.catalog-mosaic::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    transparent 40%,
    color-mix(in srgb, var(--card-bg) 70%, transparent) 76%,
    var(--card-bg) 96%
  );
}

.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(--accent);
}

.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. */
.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(--surface-inverse-hover);
}

.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 take the stronger of
     the two greys. */
  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);
  letter-spacing: var(--tracking-tighter);
  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-section) 0;
  }
  .home {
    --gutter: var(--space-sm);
  }
  .section-head h2,
  .closing h2 {
    font-size: var(--text-2xl);
  }
  .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-lg);
    box-shadow: var(--shadow-sm);
}

/* 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(--accent);
    opacity: 0.35;
    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: var(--text-base);
    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(--accent);
    font-family: inherit;
    font-weight: var(--font-weight-medium);
    font-size: var(--text-ui);
    cursor: pointer;
}

.testimonial-readmore:hover {
    color: var(--accent-strong);
    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(--accent-strong);
    color: var(--text-on-fill);
    font-weight: var(--font-weight-bold);
    font-size: var(--text-md);
}

.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: var(--text-base);
}

.testimonial-role {
    color: var(--text-secondary);
    font-size: var(--text-sm);
    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(--accent-strong);
}

.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(--scrim);
    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-xl);
    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(--accent-strong);
    color: var(--text-on-fill);
}

.reco-modal-close:focus-visible {
    outline: 2px solid var(--focus-ring);
    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(--accent) 0%, var(--accent-strong) 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(--card-bg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.reco-modal-avatar.is-initials {
    background: var(--accent-strong);
    color: var(--text-on-fill);
    font-weight: var(--font-weight-bold);
    font-size: var(--text-xl);
}

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

.reco-modal-name {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-bold);
    color: var(--text-on-fill);
}

.reco-modal-role {
    color: var(--text-on-fill);
    opacity: 0.9;
    font-size: var(--text-ui);
}

.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(--accent);
    font-weight: var(--font-weight-medium);
    font-size: var(--text-ui);
    text-decoration: none;
}

.reco-modal-profile:hover {
    color: var(--accent-strong);
}

.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;
    }
}
