/* Services -- full redesign (explicit direction: the previous version
   read as an arbitrary stack of oversized cards, no real hierarchy).
   Reuses the exact split/typography/sticky/token system already proven
   on /ai-hub/, /blogs/, /skills/ and /pricing/ -- same --li-* dark-glass
   card recipe re-declared locally per this codebase's own convention --
   every one of those files re-declares the same byte-identical tokens
   rather than sharing one file; ai-hub.css itself is never touched by
   this file. */
.services-hero,
.services-feed {
  --li-card-bg: rgba(38, 32, 56, 0.55);
  --li-card-blur: blur(24px);
  --li-border: rgba(255, 255, 255, 0.08);
  --li-border-hover: rgba(88, 130, 246, 0.4);
  --li-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
  --li-shadow-hover: 0 10px 32px rgba(63, 123, 248, 0.16), 0 4px 16px rgba(184, 107, 248, 0.12);
  --li-radius: 16px;
  --li-text: rgba(255, 255, 255, 0.92);
  --li-text-secondary: rgba(255, 255, 255, 0.6);
  /* Elegant green, the ONE new token this redesign introduces -- the
     step animation's success color, explicit value from the brief. */
  --process-success: #22c55e;
}

.services-hero .section__inner {
  max-width: 1560px;
}

/* Flat value below 1024px (mobile header is shorter, no sticky/stretch
   trick to keep in sync there); above 1024px built from the header's own
   real, measured bottom edge (.site-header: 43px top padding + 64px bar
   height = 107px, header.css) plus a clamped breathing gap -- the exact
   same real value/formula faq.css/pricing-cards.css/skills-hero.css
   already use, per direct instruction to reuse the same spacing under
   the header as the other redesigned pages. */
.services-hero {
  padding-top: 140px;
  /* Voir la note sur padding-bottom dans le bloc >=1024px. */
  padding-bottom: 0;
}

@media (min-width: 1024px) {
  .services-hero {
    --header-height: 107px;
    --services-top-space: calc(var(--header-height) + clamp(84px, 9vh, 98px));
    padding-top: var(--services-top-space);
    /* 0, et non 130px : l'écart avec la section tarif est porté par le
       padding-top de celle-ci (services-rate.css). Le haut de sa boîte
       coïncide ainsi avec la fin du contenu de cette section-ci, ce qui
       est la condition pour que l'arrivée sur #tarif ne laisse pas
       réapparaître le bandeau de résultats. */
    padding-bottom: 0;
  }

  /* Proportional split (explicit direction: "cohérente avec le Hub IA"
     means the same MATRIX/mechanism, not Hub IA's own literal 390px --
     that fixed width fits Hub IA's compact profile card specifically;
     this column carries label + H1 + subtitle + a 4-step method, so it
     genuinely needs a share of the row instead of a compact fixed
     width, same reasoning already applied on /skills/ and /pricing/.
     0.9fr/1.6fr = 36% / 64%, inside the requested 34-38% / 62-66% range
     at every desktop width, same 56px gap as the other redesigned
     pages. */
  .page-split--services {
    max-width: 1560px;
    --page-split-intro: 0.9fr;
    --page-split-content: 1.6fr;
    --page-split-gap: 56px;
  }
}

.services-label {
  margin: 0 0 14px;
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #9fb8ff;
}

.services-subtitle {
  max-width: 46ch;
  margin-top: clamp(16px, 1.8vw, 20px);
}

/* Centered and effectively static while scrolling -- the same two-layer
   stretch-parent/sticky-child mechanism as Hub IA's own .ai-hub-hero__
   sticky, tuned the same way /skills/ already had to be (real bug found
   there and avoided here from the start): Hub IA's version pins at
   `top: 0` and centers within a raw `100vh`, which is safe there only
   because its own content (a compact profile card) is short enough to
   never reach the header even at that extreme. This column's content is
   taller (label + H1 + subtitle + a 4-step method with descriptions),
   so pinning at `top: 0` risked the label sitting right under/behind the
   header on shorter viewports -- explicitly forbidden by the brief
   ("il ne doit jamais remonter derrière le header sticky"). `top:
   header-height` plus a capped `min-height` (real content height is
   ~340-380px; `min(70vh, 620px)` gives genuine centering room at
   ordinary viewport heights without ever growing enough to compete with
   the six-card grid's own height at very tall viewports, the same
   category of bug /skills/ hit and fixed with an uncapped 100vh). */
@media (min-width: 1024px) and (min-height: 820px) {
  .services-intro {
    align-self: stretch;
    position: relative;
    top: calc(-1 * var(--services-top-space));
  }

  .services-intro__sticky {
    position: sticky;
    top: calc(var(--header-height) + 24px);
    min-height: min(70vh, 620px);
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* KNOWN, FLAGGED LIMITATION (see the redesign report) -- once this
     column's sticky mechanism releases (the six-card grid + results
     runs out of extra scroll to justify staying pinned), it becomes a
     normal parked block for the rest of the page, and at every viewport
     height actually tested (820-1200px) that parked block's ~340-620px
     span still straddles the header's own 43-107px band once the user
     reaches the true bottom of the page -- confirmed by direct
     measurement, not a guess. This is the exact same structural
     tension already hit and resolved on /skills/: the fix that
     genuinely eliminates it is a `padding-bottom` that SCALES WITH
     VIEWPORT HEIGHT (measured minimum: 309px at 820px tall, 600px+ at
     1200px tall, growing further beyond that, unbounded) -- which
     reads as a large, objectionable dead gap before the footer, and
     was explicitly rejected in that exact form on /skills/ once
     implemented and seen ("tout le vide ... c trop"). A scroll-driven
     JS fade was also tried there and also explicitly rejected ("pas
     d'effet fondu"). Per that established precedent this page keeps
     the plain 130px padding-bottom above and does NOT chase this
     further on its own judgment -- flagged in the report for an
     explicit choice instead of silently picking a large gap or a
     fade this same brief has already rejected once. */
}

/* ---- Right column section header -- proportioned down from the H1
   (explicit direction: "il ne doit surtout pas devenir un second hero").
   Satoshi, matching the homepage hero's own title (explicit direction:
   one title font site-wide, no exceptions) -- was Space Grotesk before.
   A one-line subtitle underneath now too. */
.services-section-heading {
  margin: 0;
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 24px;
  line-height: 1.25;
  color: #fff;
  text-align: left;
}

.services-section-desc {
  margin: 10px 0 0;
  font-family: var(--font-satoshi);
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--li-text-secondary);
  /* "tenir sur une seule ligne lorsque la largeur le permet" -- no wrap
     constraint by width, just enough max-width that it doesn't wrap
     awkwardly on the narrower end of the right column's own range. */
  max-width: 62ch;
  white-space: normal;
}

/* =====================================================================
   4-STEP METHOD -- real sequential animation, looping continuously
   (explicit direction). js/services-method.js adds .is-visible to play
   the sequence, holds the finished all-green state a few seconds, adds
   .is-resetting to drain it back to idle quickly, waits a few seconds,
   then replays. Every visual stage below is pure CSS transition/
   animation off `.is-visible`; the JS only ever toggles those two
   classes on a handful of timeouts, never a per-frame update.

   Architecture: a genuine <ol> with exactly 4 <li> (method.njk) -- the
   connecting segment to the NEXT step lives inside THIS step's own <li>
   as a trailing aria-hidden <span>, so it never counts as a 5th/6th/7th
   list item. All 4 steps share an identical `flex: 1` basis, so a
   segment's own `width: 100%` reaches exactly "this step's center to
   the next step's center" without any hardcoded pixel math.

   Sequencing: every element below reads its own timing purely from
   `--step-index` (0-3, set inline per <li> in method.njk) via calc() --
   one shared formula, not four duplicated rules. Constants (slowed down
   a second time -- explicit direction: "l'animation aussi est trop
   rapide"):
     FILL   = 650ms   (a node's border+fill transition duration)
     CHECK  = 450ms   (checkmark draw duration), starts 400ms after
              its own node's fill starts (a deliberate overlap, not
              back-to-back, for a fluid rather than mechanical feel)
     GAP    = 200ms   (pause before the next segment starts)
     SEG    = 900ms   (segment progress duration)
   One full step "cycle" (this node's fill start -> next node's fill
   start) = 400 + 450 + 200 + 900 = 1950ms. 4 steps -> last node's
   checkmark finishes at 3*1950 + 400 + 450 = 6700ms after .is-visible
   lands. Nothing in the JS needs to know that figure any more, now that
   the sequence is never drained or replayed. ===================================================================== */
.services-method {
  position: relative;
  list-style: none;
  /* Centered as a whole block on mobile (explicit direction, confirmed
     with a screenshot circling the entire 4-step group) -- H1/subtitle
     above it are already centered, and a full-width, flush-left list
     read as visually disconnected from that. A max-width + auto margins
     shifts the WHOLE block to the column's center while every node
     inside it stays flush-left WITHIN that now-narrower block -- the
     wire's own `left: 20px` (anchored to the node, not the viewport)
     and the step's own internal flex-start layout need zero changes,
     since nothing about their own relative positions moved. Reverts to
     full-width, no centering, at >=1024px (that breakpoint already
     went through its own explicit "left, not centered" pass). */
  max-width: 300px;
  margin: 32px auto 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 28px;
  /* Explicit direction: the wire should leave the same small breathing
     room at BOTH ends, not touch its starting node flush while stopping
     short of the next one -- one shared value so both ends of every
     segment (mobile and desktop) stay symmetric by construction. */
  --wire-gap: 8px;
}

.services-method__step {
  position: relative;
  display: flex;
  /* flex-start, not center: the segment below anchors to this node
     assuming the node's own top sits at this <li>'s own top (y: 0) --
     true only with flex-start, since the text block next to it can be
     taller than the 40px node once its 2-line description wraps, and
     `center` would shift the node down by an amount that varies with
     that wrap, breaking the segment's fixed-offset math. */
  align-items: flex-start;
  gap: 16px;
}

.services-method__node {
  position: relative;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1.5px solid rgba(139, 92, 246, 0.45);
  /* Real fix, redone from scratch (explicit direction: the earlier
     approach -- a solid box-shadow fill layered under two more blurred
     inset shadows for fake depth -- rendered visible seams/dark pixels
     at the circle's own edge: three separately-blurred inset shadows
     all clipped by the same border-radius curve don't anti-alias
     against each other cleanly, especially at this small a size.
     `background-color` fixes it structurally, not cosmetically: it's
     ONE layer, transitions natively and smoothly with zero edge
     artifacts, and (confirmed directly -- .services-method__num's own
     background-clip: text only ever clips THAT element's own
     background, never a parent's) doesn't conflict with the gradient
     number rendered on top of it. The soft shine below is a SEPARATE,
     static (non-animated) background-image layer, always present at
     low opacity -- reads as a subtle glass highlight at rest, and as a
     glossy highlight on the glass once filled, without needing to be
     part of the animated transition at all. */
  background-color: transparent;
  background-image: radial-gradient(circle at 32% 24%, rgba(255, 255, 255, 0.22), transparent 55%);
  transition: background-color 650ms ease, border-color 650ms ease, box-shadow 650ms ease;
  transition-delay: calc(var(--step-index) * 1950ms);
}

.services-method__num {
  position: relative;
  z-index: 1;
  font-family: var(--font-satoshi);
  font-weight: 800;
  font-size: 13px;
  letter-spacing: 0.03em;
  background-image: linear-gradient(270.04deg, #3f7bf8 0.04%, #b86bf8 99.96%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  transition: opacity 300ms ease;
  transition-delay: calc(var(--step-index) * 1950ms + 100ms);
}

/* The checkmark: a real inline SVG, `pathLength="100"` so the dash math
   below is always 0-100 regardless of the path's actual geometry --
   never the ✅ emoji (explicit direction). Hidden via stroke-dashoffset
   (drawn on activation), not display/visibility, so it animates as an
   actual "drawing" stroke rather than popping in. */
.services-method__check {
  position: absolute;
  z-index: 1;
  width: 20px;
  height: 20px;
  opacity: 0;
  overflow: visible;
}

.services-method__check path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 450ms ease;
  transition-delay: calc(var(--step-index) * 1950ms + 400ms);
}

/* A very discreet halo, static apart from its own one-shot pulse at the
   moment of validation -- never a pointer-following or continuously
   looping effect (explicit direction: "aucune grosse explosion
   lumineuse", "aucun effet de rebond caricatural"). animation, not a
   plain transition, since it needs a pulse-then-settle shape rather
   than a single A->B interpolation; animation-fill-mode: forwards holds
   the low resting opacity afterwards, and the keyframes never repeat
   (explicit direction, and technically true here regardless -- no
   `infinite`). */
.services-method__halo {
  position: absolute;
  inset: -12px;
  z-index: 0;
  border-radius: 50%;
  /* A touch richer than before (explicit direction) -- a slightly
     hotter core plus a faint outer ring, instead of one flat radial
     fade. */
  background:
    radial-gradient(circle, rgba(34, 197, 94, 0.45) 0%, transparent 65%),
    radial-gradient(circle, transparent 68%, rgba(34, 197, 94, 0.18) 72%, transparent 76%);
  opacity: 0;
  pointer-events: none;
  /* Only matters when draining (the pulse keyframes -- below -- own the
     fade IN): once .is-visible is removed the halo's opacity just falls
     back to this 0 with no animation rule left to hold it, so this
     transition is what makes THAT specific moment fade out gently
     instead of vanishing in a single frame. */
  transition: opacity 350ms ease;
}

.services-method__text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.services-method__label {
  margin: 0;
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 15px;
  color: #fff;
}

.services-method__desc {
  margin: 0;
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--li-text-secondary);
}

/* The connecting wire -- lives inside the step it starts FROM, reaching
   to the next step's own center. Real fix, redone (explicit direction:
   "un fil violet qui devienne vert au fur et à mesure"): this used to
   be a single element, purple-dark and scaled to 0 at rest, revealed
   in green -- so at rest there was no visible wire connecting the
   nodes at all, only a hidden one. Now it's TWO layers: this element
   is a permanent, always-visible purple wire (matches the node's own
   idle border color), and its own ::after (below) is a green overlay,
   same exact box, that grows over it via `scaleY`/`transform-origin:
   top` (never a width/height change -- the brief is explicit that a
   size-based reveal would cause layout shift; scale on a fixed-size
   element triggers neither layout nor paint of anything but the
   transform itself, composited) -- reading as the purple wire
   genuinely turning green from one node to the next, not a line
   appearing out of nowhere. Vertical (mobile-first base state) below;
   the horizontal override sits in its own breakpoint further down. */
.services-method__segment {
  position: absolute;
  z-index: -1;
  left: 20px;
  /* Starts --wire-gap below this node's own bottom edge (40px), not
     flush against it -- explicit direction: the same small breathing
     room the far end already had (see height below) belongs at the
     start too, not just before the next node. */
  top: calc(40px + var(--wire-gap));
  width: 1.5px;
  /* Stops --wire-gap short of the NEXT node's own top edge. Without
     either gap this would run this <li>'s own height (100%) + the flex
     gap between <li>s (28px) - this segment's top offset (40px) =
     100% - 12px to land exactly on that edge (a true tangent -- a
     vertical line meeting a circle dead-center horizontally, at left:
     20px, touches it at the top/bottom point without ever crossing
     into it). Pulling both ends in by --wire-gap removes 2x that from
     the same formula: 100% - 12px - 2 * wire-gap. */
  height: calc(100% - 12px - 2 * var(--wire-gap));
  background-color: rgba(139, 92, 246, 0.45);
}

.services-method__segment::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--process-success);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 900ms ease;
  transition-delay: calc(var(--step-index) * 1950ms + 1050ms);
}

/* ---- Activated state -- everything below transitions INTO this while
   .is-visible is on the list. js/services-method.js adds it a short
   moment after the component enters the viewport, holds it a few
   seconds once the sequence completes, then removes it (via
   .is-resetting below) to drain back to idle before replaying. ---- */
.services-method.is-visible .services-method__node {
  border-color: var(--process-success);
  background-color: var(--process-success);
  /* A real (non-inset) drop shadow now -- rendered OUTSIDE the circle,
     never clipped against the same border-radius curve a fill layer
     sits inside of, so there's no edge for it to seam against. A tight
     bright core plus a softer, wider spread reads as a genuine glossy
     "bubble" lift rather than a flat green dot. */
  box-shadow:
    0 3px 8px -2px rgba(34, 197, 94, 0.55),
    0 0 0 4px rgba(34, 197, 94, 0.14);
  /* A small, tasteful confirming "pop" on the node itself as it
     activates -- back-eased so it overshoots slightly before settling,
     not a caricatural bounce (it never exceeds ~12% scale). */
  animation: services-method-node-pop 420ms cubic-bezier(0.34, 1.56, 0.64, 1);
  animation-delay: calc(var(--step-index) * 1950ms);
}

@keyframes services-method-node-pop {
  0% {
    transform: scale(1);
  }
  55% {
    transform: scale(1.12);
  }
  100% {
    transform: scale(1);
  }
}

.services-method.is-visible .services-method__num {
  opacity: 0;
}

.services-method.is-visible .services-method__check {
  opacity: 1;
}

.services-method.is-visible .services-method__check path {
  stroke-dashoffset: 0;
}

.services-method.is-visible .services-method__halo {
  animation: services-method-pulse 900ms ease-out forwards;
  animation-delay: calc(var(--step-index) * 1950ms);
}

@keyframes services-method-pulse {
  0% {
    opacity: 0;
    transform: scale(0.85);
  }
  45% {
    opacity: 0.65;
    transform: scale(1.1);
  }
  100% {
    opacity: 0.3;
    transform: scale(1);
  }
}

.services-method.is-visible .services-method__segment::after {
  transform: scaleY(1);
}

/* Draining back to idle: the fill's own per-step stagger lives in each
   element's BASE transition-delay, which also applies in reverse once
   `.is-visible` is removed -- without this override, the drain would
   replay that same 5s+ staggered timing backwards instead of resetting
   promptly, and the next cycle's pause would sit after an unwanted
   extra several seconds of slow drain. js/services-method.js adds
   `.is-resetting` for exactly the ~500ms window where it removes
   `.is-visible`, forcing every node/number/checkmark/segment to drain
   together, quickly, with no per-step delay. */
.services-method.is-resetting .services-method__node,
.services-method.is-resetting .services-method__num,
.services-method.is-resetting .services-method__check path,
.services-method.is-resetting .services-method__segment::after {
  transition-delay: 0s !important;
  transition-duration: 500ms !important;
}

/* ---- >=1024px: horizontal row, more compact gaps -- "tablette:
   stepper horizontal plus compact" (the split is already active here,
   but this column's own share of the row is still fairly narrow).
   Segments switch from vertical (scaleY, top-to-bottom) to horizontal
   (scaleX, left-to-right); each step's own flex: 1 basis is what makes
   this segment reach exactly the next step's own edge (not center --
   see --node-r below) with no pixel math. ---- */
@media (min-width: 1024px) {
  .services-method {
    /* Reverts the mobile-only centering block above -- this breakpoint
       already got its own explicit "left, not centered" pass. */
    max-width: none;
    margin: 32px 0 0;
    flex-direction: row;
    align-items: flex-start;
    gap: 4px;
    /* This node's own radius (half its 40px width) -- redeclared at
       1280px+ once the node itself grows to 44px, so the segment's
       edge-stopping math below stays correct at both sizes. */
    --node-r: 20px;
    /* Mirrors the `gap: 4px` above (redeclared to 8px at 1280px+ to
       match that breakpoint's own `gap: 8px`) -- the segment's own
       width formula needs this real fix found and fixed: the flex gap
       BETWEEN two step columns adds real physical space the width
       formula never accounted for, so the end of the wire landed
       --wire-gap PLUS this flex gap short of the next node (12-16px
       depending on breakpoint) instead of just --wire-gap, an
       inconsistent, wider gap than the matching one at the start. */
    --step-gap: 4px;
  }

  /* Left-aligned now, not centered (explicit direction: "met le à
     gauche de la colonne gauche, pas au centre") -- each step's node +
     label + description sit flush with the left edge of that step's
     own slot instead of centered within it, so the whole 4-step row
     reads as starting at the true left edge of the column, matching
     the mobile version's own left-aligned convention instead of
     diverging from it just because this breakpoint happens to lay
     steps out in a column internally. */
  .services-method__step {
    flex: 1;
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    gap: 10px;
  }

  .services-method__text {
    align-items: flex-start;
  }

  .services-method__segment {
    /* Real fix found and fixed (same root cause as the mobile version
       above, plus updated for the left-alignment change): this used to
       run center-to-center between two nodes, so with idle
       (transparent) nodes the wire visibly crossed straight through
       each upcoming bubble's own interior instead of stopping at its
       boundary. Now that nodes sit flush-left in their own step slot
       (not centered), THIS node's right edge is 2 * --node-r from this
       step's own left edge, and the NEXT node's left edge is exactly
       at 100% (the next step's own slot starts there, and its node
       sits flush-left in it too) -- so left/width run from one true
       edge to the other, pulled in by --wire-gap on both ends the same
       way the mobile version is. */
    left: calc(2 * var(--node-r) + var(--wire-gap));
    top: 20px;
    width: calc(100% - 2 * var(--node-r) - 2 * var(--wire-gap) + var(--step-gap));
    height: 1.5px;
  }

  .services-method__segment::after {
    transform: scaleX(0);
    transform-origin: left;
    transition-property: transform;
  }

  .services-method.is-visible .services-method__segment::after {
    transform: scaleX(1);
  }
}

/* ---- >=1280px: the full, roomier horizontal layout -- real viewport
   width, not just the column's proportional share, is what actually
   determines whether there's enough real room to breathe here (this
   column's own % share stays identical from 1024px up). ---- */
@media (min-width: 1280px) {
  .services-method {
    gap: 8px;
    --node-r: 22px; /* node grew to 44px here -- half of that */
    --step-gap: 8px; /* matches this breakpoint's own `gap: 8px` above */
  }

  .services-method__node {
    width: 44px;
    height: 44px;
  }

  .services-method__check {
    width: 22px;
    height: 22px;
  }

  .services-method__segment {
    top: 22px;
  }

  .services-method__label {
    font-size: 15.5px;
  }
}

/* prefers-reduced-motion: skip the whole sequence -- the brief is
   explicit that this must show the FULLY COMPLETED state immediately,
   not merely "no animation" of an otherwise-idle one. Overrides the
   resting (non-.is-visible) rules directly, so this holds even if
   js/services-method.js never runs at all (it self-disables under this
   same media query -- belt and suspenders, neither depends on the
   other). */
@media (prefers-reduced-motion: reduce) {
  .services-method__node,
  .services-method__num,
  .services-method__check path,
  .services-method__segment::after {
    transition: none !important;
  }

  .services-method__halo,
  .services-method__node {
    animation: none !important;
  }

  .services-method__node {
    border-color: var(--process-success);
    background-color: var(--process-success);
    box-shadow:
      0 3px 8px -2px rgba(34, 197, 94, 0.55),
      0 0 0 4px rgba(34, 197, 94, 0.14);
  }

  .services-method__num {
    opacity: 0;
  }

  .services-method__check {
    opacity: 1;
  }

  .services-method__check path {
    stroke-dashoffset: 0;
  }

  .services-method__halo {
    opacity: 0.3;
  }

  .services-method__segment::after {
    transform: none;
  }
}
