/* ==========================================================================
   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 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);
  --wash-b: color-mix(in srgb, var(--violet) 30%, 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 */
  --glass-blur: 18px;
  --glass-sat: 180%;
  --glass-edge: rgba(255, 255, 255, 0.55);

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

*,
*::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(--card);
  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: 40px 0 56px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 14.5px;
}
/* Two rows: who this is and the legal pages first, then where the business can be found.
   The rows are grid tracks, not wrapped flex items, so the split holds at every width
   instead of depending on how much happens to fit on one line. */
footer .wrap {
  display: grid;
  gap: 16px;
  justify-items: start;
}
.footer-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 24px;
  align-items: center;
}
footer a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding-block: 2px;
  text-decoration: none;
}
footer a:hover {
  text-decoration: underline;
}
/* Where the business can be found. These are the only outbound links on the site, and
   they are links, not embeds: nothing is requested from LinkedIn or Instagram until the
   visitor clicks, which is what keeps the privacy policy's "no social network" true.
   The group is the second row of footer .wrap and carries .footer-row too; the only thing
   it changes is the gap between its own links, which is tighter than the legal row's. */
.footer-social {
  gap: 10px 18px;
}

/* ---------------------------------------------------------------- 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(--card);
  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) 14%, transparent),
      transparent 72%
    ),
    radial-gradient(52% 72% at 88% 100%, color-mix(in srgb, var(--wash-b) 14%, 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);
}
.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;
  }
  .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);
  }
}
