/* ==========================================================================
   Riloq — colour tokens sampled from the logo (see README), not invented.
   Static CSS, no build step, no external font request (system stack, fast
   and keeps the site free of third-party calls).
   ========================================================================== */

:root {
  /* tokens:generated */
  --deep: #1d3fa8;
  --mid: #473fbe;
  --violet: #7a3db9;
  --accent: #8ee3f0;
  --ink: #15161c;
  --muted: #5b5f6b;
  --paper: #ffffff;
  /* /tokens:generated */

  /* role tokens: brand ink for kickers and the focus ring (remapped in dark mode) */
  --brand-ink: var(--mid);
  --focus: var(--mid);

  --grad: linear-gradient(120deg, var(--deep), var(--mid) 52%, var(--violet));
  /* h1 gradient text (large text, SC 1.4.3 >= 3:1 at every point): light mode stays on the
     dark brand stops (mid -> violet, >= 6.5:1 on white) */
  --grad-text: linear-gradient(100deg, var(--mid), var(--violet));

  /* light */
  --paper-2: #f7f7fb;
  --line: rgba(14, 15, 22, 0.1);
  /* The glass fill, ONE definition for the nav, the six cards and the about surface.
     It used to be stated twice -- the nav as 72% of --paper, --card as a literal
     rgba() -- which agreed in light mode (--paper is white) and diverged badly in
     dark, where the nav was 72% paper and a card was 4.5% white. Two copies of one
     value is how that happens; there is now one.

     30%: the backdrop has to be clearly visible through the glass or the blur is
     doing nothing and the surfaces read as flat panels. */
  --card: color-mix(in srgb, var(--paper) 30%, transparent);

  /* The CARD fill, deliberately thinner than --card and deliberately a SECOND value. The note
     above is right that two copies of one value is how the nav and the cards diverged, and this
     is not that: it is one value per KIND OF BACKDROP, which is what actually differs between
     them. The nav's backdrop is arbitrary page content scrolling under it, so its fill has to
     carry legibility against anything at all, and 30% is the floor for that. A card's backdrop
     is known -- the two section washes and nothing else -- so its fill only has to hold against
     those, and at 30% it was hiding the very thing the blur exists to bend.

     14% lets the wash through and still measures, worst case over both washes at their
     compounded 26% and under the brightest point of the sheen:
       light  --muted 5.79,  --ink 16.4      dark  --muted 5.90,  --ink 13.6
     against 4.5. Contrast is not the binding constraint on this value -- even 10% gives 5.76
     light and 5.84 dark -- so the limit here is visual: below about 12% the surface stops
     reading as a surface and the border does all the work. The nav, the eyebrow pill and
     anything else over unknown content keep --card. */
  --glass-fill: color-mix(in srgb, var(--paper) 10%, transparent);

  /* How strong the wash BEHIND the glass is. Thinning the card only helps if there is something
     to see through it, and in light mode there was not: the light wash tones are pale and sit on
     an almost-white surface, so a thinner card revealed more of nearly nothing. Dark mode never
     had that problem -- its stops are saturated against a near-black surface -- so this is one of
     the few values that genuinely differs by scheme rather than being tuned twice by accident.
     The two washes are SEPARATE layers, so where they overlap the effective alpha is
     1 - (1 - w)^2: 22% compounds to 39%, not 22%, and 39% is what the figures below assume.
       light  on the bare wash --muted 5.12,  through the card --muted 5.28,  --ink 14.95
     against 4.5, taking the darker of the two wash tones rather than the friendlier one. */
  --wash-strength: 22%;

  /* The wash behind the glass, per scheme. THIS is what made real transparency
     affordable. While both schemes washed with --deep (29,63,168), every step of
     transparency DARKENED the light-mode card, so --muted fell toward 4.5 and the
     fill could not go below ~50%. Light mode now washes with light brand tones, so
     thinning the fill leaves the card light and contrast rises instead of falling.
     Dark mode keeps the saturated stops, which stay dark against light text. */
  --wash-a: var(--accent);
  /* 45%, not 30%: thinning the card to 10% only pays if there is something behind it, and the
     old mix was so close to --paper that the glass had nothing to refract. How far this can go
     is NOT set by the cards -- it is set by .section-intro, which sits DIRECTLY on the wash with
     no glass over it, so that text is always the worst case and the cards are comfortably behind
     it. Measured at the compounded 39%, taking the darker wash:
       on the bare wash  --muted 4.66,  --ink 13.2
       through the card  --muted 4.81,  under the sheen 5.18
     against 4.5. Doubling the saturation, which is the obvious next step, puts the bare-wash
     figure at 4.18 and fails -- so this value is at its ceiling, and the ceiling belongs to the
     section text rather than to the glass. The two washes sit at opposite corners, so the true
     overlap is well under the 39% assumed here. */
  --wash-b: color-mix(in srgb, var(--violet) 45%, var(--paper));
  --shadow: 0 1px 2px rgba(14, 15, 22, 0.04), 0 12px 32px -12px rgba(14, 15, 22, 0.14);
  --shadow-lift: 0 2px 4px rgba(14, 15, 22, 0.05), 0 28px 56px -20px rgba(29, 63, 168, 0.3);

  /* hero glow strength. The two glows are SEPARATE layers, so where they overlap the effective
     alpha is 1 - (1 - g)^2, not g: at 0.35 that was 0.58, and the lede fell to 3.21:1 against a
     4.5 target, the h1 gradient end to 2.35:1 against 3. The earlier "DOM audit, 375px" sign-off
     measured one layer, which is why it passed. 0.20 is also the cap ADR 0001 D11 already records.
     Both drift paths carry a glow core onto the h1 and the lede, so this is not a corner case.
     Only transform is animated, so a translate/scale cannot raise local alpha: 1 - (1 - g)^2 is a
     closed-form worst case over every frame. Animating opacity, filter or a colour stop voids it. */
  --glow-opacity: 0.2;

  /* liquid glass: blur strength, saturation lift, and the specular top edge */
  /* 24px: with the fill down at 10% the surface is carried by refraction rather than by opacity,
     and a weak blur at that fill reads as a smudged hole rather than as glass. */
  --glass-blur: 24px;
  --glass-sat: 180%;
  --glass-edge: rgba(255, 255, 255, 0.55);

  /* The corner sheen: where the light enters the glass and where it leaves. The top edge above
     is one hairline; real glass also catches a broad highlight across the near corner and a
     weaker bounce at the far one. Two gradients, opposite diagonals, no extra element.

     THE ALPHAS ARE NOT INTERCHANGEABLE BETWEEN SCHEMES, and this is the whole reason they are
     tokens. White over a light card lightens it, so dark text gains contrast; white over a DARK
     card also lightens it, so light text LOSES contrast. 55% is generous in light and would be
     a defect in dark. Measured on the real tokens, worst case (card over both washes at their
     compounded 26%, at the brightest point of the sheen):
       light  --muted 5.74 -> 6.08,  --ink 16.25 -> 17.22   (both rise)
       dark   --muted 6.14 -> 5.45,  --ink 14.40 -> 13.0    (both stay well over 4.5)
     Raising the dark value is what would break this: 12% puts --muted at 4.79 and leaves no
     margin at all. */
  --sheen-hi: 25%;
  --sheen-lo: 12%;

  --maxw: 72rem;
  --r: 18px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --brand-ink: var(--accent);
    --focus: var(--accent);
    /* dark mode: only the light end of the brand gradient (its 76% point, violet 60% + accent,
       then accent), so every point is >= 5.4:1 on --paper */
    --grad-text: linear-gradient(
      100deg,
      color-mix(in srgb, var(--violet) 60%, var(--accent)),
      var(--accent)
    );
    /* dark: 0.5 put the h1 gradient start (violet 60% + accent) at 2.73:1 over the overlap */
    --glow-opacity: 0.35;
    --paper: #0a0b11;
    --paper-2: #10121c;
    --ink: #f3f4f9;
    --muted: #9ba1b4;
    --line: rgba(255, 255, 255, 0.11);
    /* --card is not restated here: it is 30% of --paper, and --paper is dark below. */
    --wash-a: var(--deep);
    --wash-b: var(--violet);
    /* dark stops are saturated against a near-black surface: they already show through */
    --wash-strength: 14%;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 12px 32px -12px rgba(0, 0, 0, 0.7);
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.5), 0 28px 60px -20px rgba(71, 63, 190, 0.5);
    /* dark glass catches far less light: a 0.55 edge would read as a white hairline */
    --glass-edge: rgba(255, 255, 255, 0.1);
    /* and the sheen drops by the same logic, plus the contrast one recorded above */
    --sheen-hi: 6%;
    --sheen-lo: 3%;
  }
}

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

html {
  scroll-behavior: smooth;
  /* sticky nav is 68px: keep anchor targets and focused elements clear of it (SC 2.4.11) */
  scroll-padding-top: 76px;
  -webkit-text-size-adjust: 100%;
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.6;
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: 16px;
}

a {
  color: inherit;
}

::selection {
  background: var(--mid);
  color: #fff;
}

/* skip link: off-screen until keyboard focus, then a solid chip above the nav */
.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 12px 18px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-weight: 650;
  text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:not(:focus-visible) {
  clip-path: inset(50%);
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
}
.skip-link:focus-visible {
  transform: none;
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  box-shadow: var(--shadow);
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: 6px;
}

/* ------------------------------------------------------------------ nav */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  /* solid fallback first: if color-mix() is unsupported the nav stays opaque, never transparent */
  background: var(--paper);
  background: var(--card);
  border-bottom: 1px solid var(--line);
}
.nav .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: 68px;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  text-decoration: none;
  font-weight: 650;
  font-size: 18px;
  letter-spacing: -0.02em;
}
.brand img {
  width: 34px;
  height: 34px;
  border-radius: 9px;
  display: block;
}
.nav-links {
  display: flex;
  align-items: center;
  gap: 12px;
}
.nav-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 8px;
  text-decoration: none;
  font-size: 15px;
  color: var(--muted);
  transition: color 0.2s var(--ease);
}
.nav-links a:hover {
  color: var(--ink);
}
/* language switch: a plain link, no script. The label is the language's own name, so it is
   readable without the border and without colour — the outline only marks it as a switch. */
.nav-links a.nav-lang {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0 12px;
}
.nav-links a.nav-lang:hover {
  border-color: var(--brand-ink);
}
/* small screens: links stay visible, on a second row under the brand (no JS menu) */
@media (max-width: 560px) {
  /* the sticky nav measures 155px at 375px once the links wrap onto a second row
     (brand row + two link rows); an in-page anchor has to clear all of it. */
  html {
    scroll-padding-top: 160px;
  }
  .nav .wrap {
    flex-wrap: wrap;
    height: auto;
    min-height: 68px;
    padding-block: 8px;
    gap: 4px 12px;
  }
  .brand {
    min-height: 44px;
  }
  /* four items now (the language switch is the fourth) and the French labels are the long
     ones, so the row is allowed to wrap rather than break a label across lines. */
  .nav-links {
    width: 100%;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 2px 10px;
  }
  .nav-links a {
    font-size: 14.5px;
    white-space: nowrap;
  }
  .nav-links a.nav-lang {
    padding: 0 10px;
  }
}

.btn,
.btn-sm {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: var(--grad);
  color: #fff !important;
  text-decoration: none;
  font-weight: 600;
  border-radius: 999px;
  white-space: nowrap;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), filter 0.25s var(--ease);
}
.btn {
  padding: 15px 28px;
  font-size: 16.5px;
  box-shadow: 0 10px 30px -10px rgba(71, 63, 190, 0.7);
}
.btn-sm {
  padding: 9px 18px;
  min-height: 44px;
  font-size: 14.5px;
}
.btn:hover,
.btn-sm:hover {
  transform: translateY(-2px);
  filter: brightness(1.08);
  box-shadow: 0 16px 40px -12px rgba(71, 63, 190, 0.75);
}
.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 15px 26px;
  border-radius: 999px;
  border: 1px solid var(--line);
  text-decoration: none;
  font-weight: 600;
  font-size: 16.5px;
  color: var(--ink);
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.btn-ghost:hover {
  border-color: color-mix(in srgb, var(--mid) 60%, transparent);
  transform: translateY(-2px);
}

/* ---------------------------------------------------------------- hero */
.hero {
  position: relative;
  padding: clamp(72px, 13vw, 140px) 0 clamp(64px, 10vw, 112px);
  isolation: isolate;
  overflow: hidden;
}
/* soft gradient field behind the hero */
.hero::before,
.hero::after {
  content: '';
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  filter: blur(90px);
  opacity: var(--glow-opacity);
  pointer-events: none;
}
.hero::before {
  width: 46rem;
  height: 46rem;
  top: -22rem;
  left: 50%;
  translate: -62% 0;
  background: radial-gradient(circle, var(--mid), transparent 68%);
  animation: drift-a 13s var(--ease) infinite alternate;
}
.hero::after {
  width: 34rem;
  height: 34rem;
  top: -10rem;
  right: -8rem;
  background: radial-gradient(circle, var(--violet), transparent 66%);
  animation: drift-b 17s var(--ease) infinite alternate-reverse;
}
/* Two paths, not one shared path run backwards. The glows previously differed only in duration and
   direction, so they moved as one object; separate paths read as two lights at different depths.
   Travel and cycle length are set so the motion is actually perceptible — the earlier 5%/7% over
   22s worked out to about 2px per second through a 90px blur, which reads as a static gradient.
   Only transform and opacity are animated, so this stays on the compositor and costs no layout. */
@keyframes drift-a {
  to {
    transform: translate3d(13%, 9%, 0) scale(1.2);
  }
}
@keyframes drift-b {
  to {
    transform: translate3d(-11%, 13%, 0) scale(1.16);
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero::before,
  .hero::after {
    animation: none;
  }
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px 7px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  font-size: 13.5px;
  color: var(--muted);
  margin-bottom: 26px;
}
.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent);
}

h1 {
  font-size: clamp(38px, 7.2vw, 74px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 700;
  margin: 0 0 22px;
  max-width: 16ch;
}
h1 em {
  font-style: normal;
  background: var(--grad-text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.lede {
  font-size: clamp(17.5px, 2.2vw, 21px);
  line-height: 1.55;
  /* sits on the hero glow: --muted alone drops to 3.9:1 there in light mode (DOM audit, 683px);
     a 30% step toward --ink keeps >= 4.5:1 over the glow in both schemes */
  color: color-mix(in srgb, var(--muted) 70%, var(--ink));
  max-width: 50ch;
  margin: 0 0 36px;
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

/* -------------------------------------------------------------- sections */
section {
  padding: clamp(64px, 9vw, 112px) 0;
}
.section-alt {
  background: var(--paper-2);
  border-block: 1px solid var(--line);
}

.kicker {
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand-ink);
  margin: 0 0 14px;
}
h2 {
  font-size: clamp(28px, 4.4vw, 42px);
  line-height: 1.12;
  letter-spacing: -0.03em;
  font-weight: 700;
  margin: 0 0 16px;
  max-width: 22ch;
}
.section-intro {
  color: var(--muted);
  max-width: 54ch;
  margin: 0 0 48px;
  font-size: 17.5px;
}

/* ---------------------------------------------------------------- cards */
.grid {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.card {
  position: relative;
  padding: 30px 28px 32px;
  border-radius: var(--r);
  border: 1px solid var(--line);
  /* solid fallback first, as the nav does: without color-mix() the surface stays opaque */
  background: var(--paper);
  background: var(--glass-fill);
  box-shadow: var(--shadow);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease),
    border-color 0.35s var(--ease);
}
.card:hover {
  transform: translateY(-5px);
  box-shadow: var(--shadow-lift);
  border-color: color-mix(in srgb, var(--mid) 45%, transparent);
}
.card .ico {
  width: 42px;
  height: 42px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: var(--grad);
  margin-bottom: 18px;
  box-shadow: 0 8px 20px -8px rgba(71, 63, 190, 0.8);
}
.card .ico svg {
  width: 21px;
  height: 21px;
  stroke: #fff;
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.card h3 {
  font-size: 19.5px;
  letter-spacing: -0.02em;
  margin: 0 0 9px;
  font-weight: 650;
}
.card p {
  margin: 0;
  color: var(--muted);
  font-size: 16px;
  line-height: 1.6;
}

/* services grid: 1 / 2 / 3 equal columns, six cards, no orphan (DOM order = visual order) */
.grid-services { grid-template-columns: 1fr; }
@media (min-width: 768px) {
  .grid-services { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
  .grid-services { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.card-list {
  margin: 14px 0 0;
  padding-left: 1.15em;
  color: var(--ink);
  font-size: 16px;
  line-height: 1.6;
}
.card-list li { margin: 0 0 4px; }
.card-list li::marker { color: var(--brand-ink); }
/* advisory: outlined icon instead of the filled gradient tile */
.card-advisory .ico {
  background: transparent;
  border: 1.5px solid var(--brand-ink);
  box-shadow: none;
}
.card-advisory .ico svg { stroke: var(--brand-ink); }
@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
  .card:hover { transform: none; }
}

/* -------------------------------------------------------------- process */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0;
  counter-reset: step;
}
.steps li {
  position: relative;
  display: grid;
  grid-template-columns: 46px 1fr;
  gap: 22px;
  padding-bottom: 36px;
}
.steps li:last-child {
  padding-bottom: 0;
}

/* Staggered reveal: each step fades up a beat after the one above it, once the list
   comes into view. assets/js/reveal.js adds .js-reveal to <html> and .revealed to the
   list; the hidden state below is scoped to .js-reveal, so with JavaScript off or
   blocked the steps are plainly visible and this stylesheet cannot hide them.

   A TRANSITION, NOT AN ANIMATION, ON PURPOSE. manual/dom-audit.js models @keyframes and
   accepts only transform/translate/scale inside them: a fading keyframe would make the
   audit report its own model stale and stop being contrast evidence. Transitions are
   what its __freezeTransitions() is built to pin, so the reveal stays measurable. */
.js-reveal .steps > li {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 520ms ease, transform 520ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.js-reveal .steps > li:nth-child(2) {
  transition-delay: 130ms;
}
.js-reveal .steps > li:nth-child(3) {
  transition-delay: 260ms;
}
.js-reveal .steps > li:nth-child(4) {
  transition-delay: 390ms;
}
.js-reveal .steps.revealed > li {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .js-reveal .steps > li {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
/* connecting line */
.steps li:not(:last-child)::before {
  content: '';
  position: absolute;
  left: 22px;
  top: 46px;
  bottom: 6px;
  width: 2px;
  background: linear-gradient(var(--line), transparent);
}
.steps .num {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  background: var(--grad);
  box-shadow: 0 8px 20px -8px rgba(71, 63, 190, 0.8);
}
.steps h3 {
  margin: 10px 0 6px;
  font-size: 19px;
  letter-spacing: -0.02em;
  font-weight: 650;
}
.steps p {
  margin: 0;
  color: var(--muted);
  max-width: 56ch;
}

/* -------------------------------------------------------------- contact */
.panel {
  position: relative;
  overflow: hidden;
  border-radius: 26px;
  padding: clamp(40px, 6vw, 68px);
  background: var(--grad);
  color: #fff;
  text-align: center;
  box-shadow: 0 30px 70px -30px rgba(29, 63, 168, 0.8);
}
.panel::after {
  content: '';
  position: absolute;
  inset: auto -20% -60% -20%;
  height: 70%;
  background: radial-gradient(ellipse at center, rgba(142, 227, 240, 0.2), transparent 70%);
  pointer-events: none;
}
.panel h2 {
  position: relative;
  z-index: 1;
  color: #fff;
  margin: 0 auto 14px;
  max-width: 18ch;
}
.panel p {
  position: relative;
  z-index: 1;
  color: #fff;
  max-width: 48ch;
  margin: 0 auto 32px;
  font-size: 17.5px;
}
.panel .btn-white {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  background: #fff;
  color: var(--deep) !important;
  text-decoration: none;
  font-weight: 650;
  padding: 15px 30px;
  border-radius: 999px;
  position: relative;
  z-index: 1;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
}
.panel .btn-white:hover {
  transform: translateY(-2px);
  box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.55);
}
.panel .lines {
  position: relative;
  z-index: 1;
  list-style: none;
  margin: 28px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 28px;
  justify-content: center;
  font-size: 15.5px;
}
.panel .lines a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: #fff;
  text-decoration: none;
}
.panel .lines a:hover {
  text-decoration: underline;
}
/* postal address: full white, not a translucent white — the panel is a gradient, so any opacity
   below 1 reopens the contrast question that was already settled for .lines a at this colour. */
.panel .postal {
  position: relative;
  z-index: 1;
  margin: 18px 0 0;
  font-size: 14.5px;
  font-style: normal;
  line-height: 1.6;
  color: #fff;
}

/* focus inside the panel: the gradient passes through --mid/--accent tones, so the global ring
   vanishes there. White ring (>= 4.8:1 on every panel point incl. the glow) with a --deep inner
   ring filling the offset gap (>= 9:1 against both the white ring and .btn-white). SC 2.4.7/1.4.11 */
.panel :focus-visible {
  --focus-inner: var(--deep);
  outline-color: #fff;
  box-shadow: 0 0 0 3px var(--focus-inner);
}

/* --------------------------------------------------------------- footer */
footer {
  padding: 56px 0 40px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.92rem;
}

footer .wrap {
  display: grid;
  gap: 32px;
}

/* Three columns, then two, then one; the brand column is the wide one. */
.footer-cols {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: 32px 40px;
}

@media (max-width: 760px) {
  .footer-cols { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
}

@media (max-width: 440px) {
  .footer-cols { grid-template-columns: 1fr; }
}

.footer-brand,
.footer-col {
  display: grid;
  justify-items: start;
  align-content: start;
}

.footer-brand { gap: 12px; }
.footer-col { gap: 8px; }

.footer-logo {
  gap: 10px;
  font-weight: 650;
  font-size: 1.05rem;
  color: var(--ink);
}

/* A <p>, not a heading: the nav's aria-label already names each column. */
.footer-head {
  margin: 0 0 4px;
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.75;
}

/* The consent re-opener is a button because it acts rather than navigates. */
footer a,
.footer-consent {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0;
  padding-block: 2px;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

footer a:hover,
.footer-consent:hover { text-decoration: underline; }

.footer-base {
  padding-top: 24px;
  border-top: 1px solid var(--line);
  font-size: 0.86rem;
}


/* ---------------------------------------------------------------- about
   The one section on the page that is about a person, so it gets a surface of
   its own rather than sitting loose in the --section-alt band: same card recipe
   as .card (--card / --line / --shadow / --r), one size up. It is not
   interactive, so it has no hover lift — a surface that moves but cannot be
   clicked reads as a broken affordance (SC 3.2 spirit), and .card's lift is
   what tells you the service tiles are a grid, not a link. */
.about {
  display: grid;
  gap: 26px;
  justify-items: start;
  /* the text is one short paragraph: across the full 72rem wrap the card ran to
     a half-empty right side. Capped and centred, the portrait and the copy read
     as one object and the band around it is deliberate, not leftover. */
  max-width: 56rem;
  margin-inline: auto;
  padding: clamp(26px, 4.4vw, 48px);
  border-radius: var(--r);
  border: 1px solid var(--line);
  /* solid fallback first, as the nav does: without color-mix() the surface stays opaque */
  background: var(--paper);
  background: var(--glass-fill);
  box-shadow: var(--shadow);
}
/* 720px, not the 560px nav breakpoint: below this the 168px portrait plus a
   readable measure no longer fit side by side without the text dropping under
   ~34ch, where the paragraph starts to rag badly. */
@media (min-width: 720px) {
  .about {
    grid-template-columns: auto minmax(0, 1fr);
    gap: clamp(34px, 5vw, 58px);
    align-items: center;
  }
  .about-portrait {
    justify-self: start;
  }
}
.about-text {
  min-width: 0;
}
/* inside the card the intro is the last element: the 48px section-intro gutter
   would leave a dead band above the card edge. */
.about .section-intro {
  margin-bottom: 0;
  max-width: 48ch;
}
/* the About copy is two paragraphs now, and the rule above zeroes the gutter on both, so the gap
   has to go BETWEEN them rather than after the last one. */
.about-text > p + p {
  margin-top: 15px;
}
/* 17ch suited the old 29-character heading. "Software built around your business" is 35 and broke
   onto a third line, which left the portrait taller than the text beside it. */
.about h2 {
  max-width: 22ch;
}

/* The portrait sits in a brand-gradient ring — the same --grad as the service
   icons and the process numbers. It is the only photograph on the site, and the
   ring is what makes it read as part of the system instead of a dropped-in
   asset. Decorative only: the <img> keeps its own alt text, and the ring carries
   no information, so it needs no contrast floor of its own. */
.about-portrait {
  padding: 4px;
  border-radius: 50%;
  background: var(--grad);
  box-shadow: 0 14px 34px -14px rgba(71, 63, 190, 0.75);
  line-height: 0;
  /* stacked single column: centred. Left-aligned over a left-aligned paragraph
     it read as an orphan in the corner rather than the subject of the section.
     The two-column layout below puts it back to start, where the grid column
     already does the aligning. */
  justify-self: center;
}
.portrait {
  display: block;
  width: clamp(116px, 17vw, 168px);
  height: clamp(116px, 17vw, 168px);
  border-radius: 50%;
  object-fit: cover;
}

/* ------------------------------------------------------- legal prose
   /mentions-legales and /confidentialite are long-form text, not landing
   sections. Layout only: every colour here is already an audited pair
   (check 6 audits --ink on --paper for body text), so the policy pages add
   no new text/background combination to audit. Deliberate — the one page
   on this site whose errors are published falsehoods should not also be the
   page that introduces an unmeasured contrast pair. */
.prose {
  max-width: 68ch;
}
.prose h2 {
  margin-top: 2.4em;
}
.prose h3 {
  margin-top: 1.8em;
  font-size: 17px;
}
.prose ul {
  margin: 0 0 1.1em;
  padding-left: 1.3em;
}
.prose li {
  margin-bottom: 0.45em;
}
.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.92em;
}
.prose .updated {
  margin-top: 3em;
  font-size: 14.5px;
  color: var(--muted);
}

/* ------------------------------------------------------- consent gate
   The banner. Two things here are legal requirements rather than taste, and
   both are expressed in CSS, so changing them is a legal change:

   1. DECLINE IS EXACTLY AS EASY AS ALLOW. Both controls are the same element
      (<button>), the same component (.btn-ghost), the same size and the same
      visual weight. There is deliberately NO primary/secondary pair here:
      styling Allow as the only real button is the dark pattern the CNIL
      names. If you are about to add .btn to one of them, stop.
   2. NOTHING IS PRE-SELECTED and nothing fires before a choice: the banner
      ships with the `hidden` attribute and is revealed by consent.js, which
      is also the only thing that may later load a measurement tag.

   Colours: --paper / --ink / --line / --muted only, i.e. pairs check 6
   already audits for body text and for .btn-ghost. The banner introduces no
   new colour pair. The border and the shadow are decorative; the controls are
   identified by their labels (SC 1.4.11 exception, as .btn-ghost already is). */
.consent {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 60;
  background: var(--paper);
  border-top: 1px solid var(--line);
  box-shadow: var(--shadow);
  padding-block: 16px;
  padding-bottom: max(16px, env(safe-area-inset-bottom));
  font-size: 15px;
}
.consent-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 28px;
  align-items: center;
  justify-content: space-between;
}
.consent-text {
  margin: 0;
  max-width: 62ch;
}
.consent-text a {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.consent-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
/* Both controls are given the same width, not merely the same style: "Decline"
   is a longer word than "Allow" and a measurement of the two buttons would
   otherwise differ by label length. Equal size is the thing being asserted, so
   it is set rather than left to the text. */
.consent-actions .btn-ghost {
  min-width: 11ch;
  justify-content: center;
}
/* .btn-ghost on a <button>: the component was written for <a>, so only the
   element defaults are reset. No colour, size or weight is changed, which is
   what keeps the two controls identical. */
button.btn-ghost {
  background: transparent;
  font-family: inherit;
  cursor: pointer;
  padding: 11px 22px;
  min-height: 44px;
  font-size: 15px;
}
@media (max-width: 560px) {
  .consent-actions {
    width: 100%;
  }
  .consent-actions .btn-ghost {
    flex: 1 1 0;
    justify-content: center;
  }
}

/* The withdrawal control. It lives in the footer partial on every page, so the
   choice is revocable from anywhere (GDPR art. 7(3)). It carries the `hidden`
   attribute in the markup and consent.js removes it: a control that cannot
   work without script must not be visible without script. Hence :not([hidden])
   rather than a display rule that would override the attribute. */
[data-consent-reopen]:not([hidden]) {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 2px 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: none;
}
[data-consent-reopen]:hover {
  text-decoration: underline;
}

/* ------------------------------------------------------------ liquid glass
   The six service cards, the about surface and the nav are already translucent
   (--card is 0.72 white in light, 0.045 white in dark). Translucency alone is
   not glass: the band behind them was flat, so there was nothing to refract and
   the surfaces read as a slightly off-white fill. Glass needs three things, and
   all three are below -- something behind it, a backdrop blur that bends it,
   and a specular edge where the light catches.

   WHY THE WASH IS CAPPED AT 14%. The two radial washes are SEPARATE layers, so
   where they overlap the effective alpha is 1 - (1 - 0.14)^2 = 0.26, not 0.14.
   That is the same compounding that put the hero h1 at 2.35:1 and is recorded
   against --glow-opacity above; assuming the stated alpha there is exactly how
   that defect passed review. Worst case is therefore 19% of the darkest brand
   stop sitting under the card, with the card's own white over it:
     light  --muted on 30% paper over (26% --wash-a on --paper-2) = 5.66:1
     dark   --muted on 30% paper over (26% --wash-a on --paper-2) = 6.72:1
   against a 4.5 requirement, and --ink is far better in both. The two washes sit
   at opposite corners so the real overlap is well under 0.19, but the figures
   hold even if they coincided exactly. Nothing here is animated, so these are
   worst cases over every frame, not samples.

   backdrop-filter is a progressive enhancement: --card carries the contrast on
   its own, so a browser without it loses the refraction and keeps the text. */
.section-alt {
  background-color: var(--paper-2);
  background-image: radial-gradient(
      58% 78% at 12% 0%,
      color-mix(in srgb, var(--wash-a) var(--wash-strength), transparent),
      transparent 72%
    ),
    radial-gradient(
      52% 72% at 88% 100%,
      color-mix(in srgb, var(--wash-b) var(--wash-strength), transparent),
      transparent 72%
    );
}
.card,
.about {
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: var(--shadow), inset 0 1px 0 var(--glass-edge);
}
/* the lift must not drop the specular edge, or the card flattens on hover */
.card:hover {
  box-shadow: var(--shadow-lift), inset 0 1px 0 var(--glass-edge);
}
/* The two sheen layers. Pseudo-elements rather than a wrapper div, because the markup is
   generated and the head/nav/footer partials are byte-compared across both language trees:
   a wrapper would have to be added to every card on every page, and the first one anybody
   forgot would be invisible. inset 0 + border-radius: inherit keeps them inside the rounded
   corner without restating the radius. */
.card::before,
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}
.card::before {
  background: linear-gradient(135deg, color-mix(in srgb, #fff var(--sheen-hi), transparent), transparent 45%);
}
.card::after {
  background: linear-gradient(315deg, color-mix(in srgb, #fff var(--sheen-lo), transparent), transparent 40%);
}
/* An absolutely positioned pseudo-element paints ABOVE its in-flow siblings, so without this
   the sheen would sit over the icon and the text rather than under them. This is what the
   Tailwind original expresses as "relative z-10" on an inner wrapper; the stacking fact is the
   same, only the markup differs. */
.card > * {
  position: relative;
  z-index: 1;
}
.nav {
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: inset 0 1px 0 var(--glass-edge);
}
/* A reader who asks for less transparency gets opaque surfaces, not a weaker
   blur: the point of the setting is that the backdrop stops showing through. */
@media (prefers-reduced-transparency: reduce) {
  .card,
  .about,
  .nav {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  /* the sheen is the glass reading as glass; with the blur gone it is just a smear */
  .card::before,
  .card::after {
    background: none;
  }
  .nav {
    background: var(--paper);
  }
  .section-alt {
    background-image: none;
  }
}

/* the one link in the about card: a named person, so it reads as a signature
   line rather than a call to action -- no button, just the brand ink. */
.about-link {
  margin: 0;
}
.about-link a {
  color: var(--brand-ink);
  font-weight: 600;
  text-decoration: none;
}
.about-link a:hover {
  text-decoration: underline;
}

/* Three short clauses, one per line. Spans rather than <br>: a <br> forces the
   break at every width whether it fits or not, and leaves the heading as one run
   of text that a screen reader reads without the pause the full stops imply. */
.h2-stack span {
  display: block;
}

/* ----------------------------------------------------- french palette (/fr/)
   The French tree runs the teal/amber palette instead of the blue/violet brand,
   on the owner's instruction (the Instagram set uses it). It is a SCOPED TOKEN
   OVERRIDE, not a second stylesheet: the base :root block is written by
   build-meta from site.config.json and cross-checked against the vendored brand
   colours by check 6, so re-theming there would re-theme every Riloq site and
   fail the gate. Everything downstream -- the hero glow, --grad-text, kickers,
   buttons, the glass wash -- is already expressed in these four tokens, so they
   are all that has to change.

   IT IS KEYED ON A CLASS THE TREE OPTS INTO (html.theme-fr on /fr/), NOT ON
   html[lang="fr"]. /mentions-legales, /confidentialite and /conditions-utilisation are
   French documents linked from BOTH footers: a language selector would paint them green
   while the English homepage linking them stayed blue. They keep the base palette.

   html.theme-fr is specificity 0,1,1 and beats the :root rules inside the dark
   @media block (0,1,0) REGARDLESS of the media query, so the dark overrides this
   file makes for --wash-a/--wash-b would be silently lost. That is why the dark
   half below is restated at the same specificity rather than left to inherit.

   CONTRAST IS NOT COVERED BY CHECK 6 HERE: its cross-check reads the generated
   tokens, so it tests the blue palette and says nothing about this one. These
   values were chosen against the real requirement and then MEASURED with
   manual/dom-audit.js on /fr/, light and dark, 375 and 1280:
     --mid #14706c as the light kicker ink on paper   = 5.89:1 (needs 4.5)
     --grad-text light end #1a7569 on paper            = 5.21:1 (needs 3, large)
     white .btn label on the gradient's lightest stop  = 5.21:1 (needs 4.5)
     dark h1 start (violet 60% + accent) on --paper    = 6.59:1 (needs 3, large)
   The green end is deliberately #1a7569 and not the brighter #2e9e8f of the
   source image. Two reasons, and the second is the binding one: #2e9e8f measures
   3.28:1 on paper, clearing the large-text 3.0 by so little that the hero glow
   underneath could take it below; and the gradient's LIGHTEST stop is what the
   white .btn label sits on, where the bar is 4.5, not 3. At #1e8578 the audit
   measured that label at 4.53:1 -- passing, but by 0.03, which is not a margin.
   #1a7569 puts it at 5.21:1. The button is why the green is this dark. */
html.theme-fr {
  --deep: #0c4a50;
  --mid: #14706c;
  --violet: #1a7569;
  --accent: #fbcb7a;
  /* light wash: pale teal and pale amber, so thinning the glass keeps the card
     light and contrast rises rather than falls (same reasoning as the base). */
  --wash-a: color-mix(in srgb, var(--violet) 25%, var(--paper));
  --wash-b: color-mix(in srgb, var(--accent) 60%, var(--paper));
}
@media (prefers-color-scheme: dark) {
  html.theme-fr {
    /* dark: the saturated teal and green themselves, which stay dark under light text */
    --wash-a: var(--deep);
    --wash-b: var(--violet);
  }
}
