/* ---------- Archivo (self-hosted, variable 100-900) ---------- */
@font-face {
  font-family: "Archivo";
  src: url("/static/archivo-latin.woff2?v=8f704806db") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg: #f7f7f5;
  --bg-soft: #eeefec;
  --bg-card: #ffffff;
  --fg: #101216;
  --fg-soft: #2e323a;
  --muted: #666c76;
  --muted-soft: #666c76;
  --muted-inv: #9aa1ac; /* muted label on the theme's inverted surface */
  --rule: #d6d8d4;
  --rule-strong: #2d323a;
  --accent: #1b3a8a; /* navy, AA on the page background */
  --accent-inv: #8aa9f0; /* the other theme's accent, for surfaces painted in --fg */
  --chip-fg: #545a64; /* text on a --rule-filled chip, AA on --rule */

  /* functional status set — severity badges, admonition stripes */
  --status-slate: #5a636d;
  --status-moss: #466b41;
  --status-amber: #86590f;
  --status-orange: #9a4410; /* severite haute : entre l'ambre et la brique */
  --status-brick: #a83b2a;

  /* tints kept low so badge/callout labels stay AA on their own fill */
  --tint-badge: 6%;
  --tint-callout: 3%;

  /* focus ring; inverted surfaces override it below */
  --focus-ring: var(--fg);

  /* Type ramp. Every font-size in this sheet resolves to one of these steps;
     the only exceptions are em-relative sizes (arrows, inline code) which
     scale with their context, and .team-photo-fallback, whose glyph is sized
     to its avatar circle rather than to the text ramp. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Two text sizes and the headings above them (Adam, 2026-10-26: too many sizes, too small, "faut pas que le
     site pique les yeux"). Small, 14px: labels, dates, tags, table heads, hints, dense UI. Text, 16px: anything
     meant to be read, supporting copy included. The old steps stay as names so every rule keeps working;
     they now resolve to one of the two. Nothing is set below 0.875rem. */
  --fs-3xs: 0.875rem;  /* micro labels: table heads, stacked-row labels, badges */
  --fs-2xs: 0.875rem;  /* label layer: tile numbers, tags, mono labels */
  --fs-xs: 0.875rem;   /* metadata: dates, pills, footnotes */
  --fs-sm: 0.875rem;   /* dense UI: tables, code, secondary rows, nav */
  --fs-md: 1rem;       /* supporting copy */
  --fs-lg: 1rem;       /* body */
  --fs-xl: 1.15rem;     /* card and tile titles */
  --fs-2xl: 1.35rem;   /* prose h2, blog card titles */

  --fs-lede: clamp(1.05rem, 1.7vw, 1.2rem);      /* ledes, chapo, contact CTA */
  --fs-subhead: clamp(1.3rem, 2.6vw, 1.7rem);    /* group titles, team names */
  --fs-heading: clamp(1.5rem, 2.6vw, 2rem);      /* section h2 */
  --fs-post: clamp(1.85rem, 4.2vw, 2.6rem);      /* post title, narrow column */
  --fs-title: clamp(2rem, 5vw, 3rem);            /* page heroes */
  --fs-display: clamp(2.2rem, 4.6vw, 3.4rem);    /* home hero, type specimen */

  --max-w: 1120px;
  --pad-x: clamp(1.25rem, 4vw, 2.5rem);
  --r-md: 1px;
  --r-lg: 1px;

  /* LA mesure, et elle est en REM, pas en CH.
     Il y en avait quatre pour le meme objet — le chapeau sous un titre : 56ch
     sur l'accueil et l'equipe, 60ch sur les advisories, 62ch sur les services,
     64ch sur le blog et la marque. Quatre nombres pour une seule intention.
     🔴 Mais l'unite comptait plus que la valeur : un `ch` vaut la largeur du
     zero de la police A SON CORPS. Le chapeau d'une page de service et sa note
     de rappel declaraient tous deux `62ch` et rendaient 682 px et 778 px — meme
     valeur, deux bords droits, parce que les deux corps different. Une mesure
     sert a ALIGNER autant qu'a limiter la longueur de ligne : elle doit donc
     etre independante du corps. 42rem tient environ 70 caracteres au corps de
     lecture. */
  --measure: 42rem;

  /* Cote des boutons carres de la nav : loupe, hamburger, bascule de theme.
     Ils etaient dessines a 34, 40 et 32 px et ne s'alignaient donc pas, ce qui
     se voyait surtout sur mobile ou les trois sont cote a cote. */
  --nav-btn: 34px;
}

/* `balance` equilibre un titre sur ses deux ou trois lignes ; `pretty` empeche
   qu'un paragraphe finisse sur un mot seul. Pose ici une fois plutot que
   selecteur par selecteur : sur 19 articles longs, la veuve se trouve toujours
   dans le paragraphe auquel personne n'a pense. */
h1, h2, h3, h4, h5, h6 {
  text-wrap: balance;
}

p, li, dd, dt, figcaption, blockquote, summary {
  text-wrap: pretty;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: "Archivo", "Helvetica Neue", Arial, sans-serif;
  font-weight: 400;
  font-size: 1rem; /* 16px by default, the reader's size when they set one */
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
p,
ul,
ol,
dl,
dt,
dd {
  margin: 0;
  padding: 0;
}

ul,
ol {
  list-style: none;
}

img {
  display: block;
  max-width: 100%;
}

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

/* Inverted surfaces (The Inversion Rule) paint ink where the page paints paper,
   so the ring has to flip with them or it lands invisible on its own colour. */
.panel,
.schema-root,
.post-body pre,
.post-body .code-copy,
.paper-bibtex {
  --focus-ring: var(--bg);
}

/* A scrolling code block takes the keyboard (Chrome, Firefox): its page-coloured ring is drawn inside the ink,
   where it shows, rather than outside it on the page, where it vanished. */
.post-body pre:focus-visible,
.paper-bibtex:focus-visible {
  outline-offset: -4px;
}

.footer {
  --focus-ring: var(--bg);
}

html.dark .footer {
  --focus-ring: var(--fg); /* dark footer sits on --bg-soft, not inverted */
}

html.dark .post-body pre,
html.dark .paper-bibtex {
  --focus-ring: var(--fg); /* dark code blocks sit on their own ink, not inverted */
}

/* La selection porte l'accent. Le bleu par defaut du navigateur est la seule
   couleur de la page que la charte n'a pas choisie. */
::selection {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--fg);
}

/* Same flip as the focus ring: on a surface painted in --fg, the page's own
   selection put --fg text on --fg ground and a dark-blue tint on near black.
   The footer and the code blocks stop being inverted in dark mode and get the
   page's selection back below. */
.panel ::selection,
.schema-root ::selection,
.post-body pre ::selection,
.paper-bibtex ::selection,
.footer ::selection {
  background: color-mix(in srgb, var(--accent-inv) 45%, transparent);
  color: var(--bg);
}

/* Skip link — visually hidden until focused (WCAG 2.4.1). */
.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -100%;
  z-index: 200;
  padding: 0.7rem 1rem;
  background: var(--bg-card);
  color: var(--fg);
  border: 1px solid var(--fg);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.skip-link:focus {
  top: 0.5rem;
}

/* ---------- Nav ---------- */
.nav {
  position: relative; /* anchors the mobile nav panel */
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 1.25rem var(--pad-x);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.brand-logo {
  border-radius: 1px;
  flex-shrink: 0;
}

.brand-name {
  font-size: var(--fs-lg);
}

.nav-contact {
  font-size: var(--fs-sm);
  color: var(--muted);
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 160ms ease, border-color 160ms ease;
}

.nav-contact:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

.nav-links {
  display: flex;
  gap: 1.25rem;
  margin-left: auto;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.nav-links a {
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 160ms ease, border-color 160ms ease;
}

.nav-links a:hover,
.nav-links a[aria-current] {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* Hamburger — hidden until the pre-paint script confirms JS by stamping
   html.js-nav, so a reader without JS keeps the links laid out inline. */
.nav-toggle {
  display: none;
  width: var(--nav-btn);
  height: var(--nav-btn);
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 160ms ease;
}

.nav-toggle:hover {
  border-color: var(--fg);
}

.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 16px;
  height: 1.5px;
  background: currentColor;
  transition: transform 200ms ease, background 160ms ease;
}

.nav-toggle-bars {
  position: relative;
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.nav-toggle-bars::before {
  top: -5px;
}

.nav-toggle-bars::after {
  top: 5px;
}

/* Open: the two outer bars close into a cross, the middle one drops out. */
.nav.is-open .nav-toggle-bars {
  background: transparent;
}

.nav.is-open .nav-toggle-bars::before {
  transform: translateY(5px) rotate(45deg);
}

.nav.is-open .nav-toggle-bars::after {
  transform: translateY(-5px) rotate(-45deg);
}

/* Forced colors repaint backgrounds as Canvas, which drew the bars out of the
   button: paint them in the button's text colour instead. The open state keeps
   its transparent middle bar (higher specificity). */
@media (forced-colors: active) {
  .nav-toggle-bars,
  .nav-toggle-bars::before,
  .nav-toggle-bars::after {
    forced-color-adjust: none;
    background: ButtonText;
  }
  .nav.is-open .nav-toggle-bars {
    background: transparent;
  }
}

/* ---------- Layout ---------- */
main {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

/* ---------- Hero ---------- */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  padding: clamp(1.5rem, 3vw, 2.5rem) 0 clamp(2rem, 4vw, 3rem);
}

.hero-text {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.hero-title {
  font-size: var(--fs-display);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  text-wrap: balance;
  overflow-wrap: break-word;
}

.hero-lede {
  font-size: var(--fs-lg);
  color: var(--muted);
  max-width: var(--measure);
  line-height: 1.6;
}

.hero-proof {
  font-size: var(--fs-md);
  color: var(--muted);
  margin-top: 0.25rem;
}

.hero-proof a {
  color: var(--accent);
  font-weight: 500;
  border-bottom: 1px solid transparent;
  transition: border-color 160ms ease;
}

.hero-proof a:hover {
  border-bottom-color: var(--accent);
}

.hero-proof-arrow {
  display: inline-block;
  margin-left: 0.35rem;
  transition: transform 200ms ease;
}

.hero-proof a:hover .hero-proof-arrow {
  transform: translateX(3px);
}

/* ---------- Hero schema (umbrella) ---------- */
.hero-schema {
  margin: 0;
  padding: 1.25rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
}

.schema-root {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.2rem;
  padding: 0.85rem 1rem;
  background: var(--fg);
  color: var(--bg);
  border-radius: 1px;
}

.schema-root .schema-name {
  font-weight: 600;
  font-size: var(--fs-md);
  letter-spacing: -0.005em;
}

.schema-arrow {
  color: var(--muted-soft);
  font-size: var(--fs-md);
  line-height: 1;
}

.schema-branches {
  width: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: 1px;
  overflow: hidden;
  background: var(--bg);
}

.schema-branches li {
  border-bottom: 1px solid var(--rule);
}

.schema-branches li:last-child {
  border-bottom: none;
}

/* Each branch is a title and its detail line, the .t and .s of a diagram band:
   what the branch holds, with the research counted by the build. */
.schema-branches a {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.7rem 0.9rem 0.75rem;
  color: inherit;
  transition: background 160ms ease, color 160ms ease;
}

.schema-branches a:hover {
  background: var(--bg-soft);
  color: var(--fg);
}

/* The rows' list clips its corners (overflow: hidden), which cut the sides off
   the page's ring drawn 3px outside: drawn inside the row instead. */
.schema-branches a:focus-visible {
  outline-offset: -3px;
}

.schema-label {
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.005em;
}

@media (max-width: 560px) {
  /* The figure without its card on a phone: the rows keep their hairlines and
     the root block its ink, and the figure stops filling half the screen. */
  .hero-schema {
    padding: 0;
    background: none;
    border: 0;
    gap: 0.45rem;
  }
  .schema-root {
    padding: 0.7rem 0.9rem;
  }
  .schema-branches a {
    padding: 0.6rem 0.9rem 0.65rem;
  }
}

/* ---------- Sections ---------- */
.section {
  padding: clamp(2.5rem, 4vw, 3.75rem) 0;
}

.section + .section,
.prev + .section {
  border-top: 1px solid var(--rule);
}

.section-head {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin-bottom: clamp(1.5rem, 2.5vw, 2.25rem);
  max-width: var(--measure);
}

.kicker {
  font-size: var(--fs-sm);
  letter-spacing: 0.01em;
  color: var(--muted);
  font-weight: 600;
}

.section-head h2,
.section-products > h2 { /* Products has no header of its own: the same heading size as its neighbours */
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.section-head p {
  font-size: var(--fs-lg);
  color: var(--muted);
  line-height: 1.6;
}

/* ---------- Previously-at strip ---------- */
.prev {
  border-top: 1px solid var(--rule);
  padding: clamp(1.5rem, 3vw, 2rem) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.1rem;
}

.prev-logos {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: clamp(1.5rem, 4vw, 2.75rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.prev-logos li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  max-width: 160px;
  text-align: center;
}

.prev-logos img {
  max-height: 34px;
  max-width: 150px;
  width: auto;
  height: auto;
  opacity: 0.62;
  transition: opacity 200ms ease;
}

.prev-logos li:hover img {
  opacity: 0.9;
}

.prev-name {
  font-size: var(--fs-2xs);
  color: var(--muted);
  letter-spacing: 0.02em;
}

/* ---------- Services grid ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
}

.tile {
  position: relative;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: 1.4rem 1.4rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  transition: border-color 160ms ease, transform 200ms ease, box-shadow 220ms ease;
}

.tile:hover {
  border-color: var(--fg);
  transform: translateY(-1px);
  box-shadow: 0 12px 28px -22px rgba(14, 13, 10, 0.35);
}

.tile h3,
.training-offers .tile h2 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.018em;
}

.training-offers .tile h2 {
  line-height: 1.3;  /* the online course's name runs to two lines */
}

/* The /training/ hub is shorter than a 1080 px window: without this a band of
   page showed under the black footer. The page-confirm recipe, scoped to the
   hub's own body class, so no other page's layout moves. */
.page-training-hub {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.page-training-hub > .nav,
.page-training-hub > main {
  width: 100%;
}

.page-training-hub > main {
  flex: 1 0 auto;
}

.tile p {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.55;
}

/* ---------- Research list (home) ---------- */
.research-list {
  border-top: 1px solid var(--rule);
}

.research-list li {
  border-bottom: 1px solid var(--rule);
}

.research-list a {
  display: flex;
  align-items: baseline;
  gap: 1.25rem;
  padding: 0.9rem 0.2rem;
  transition: background 160ms ease;
}

.research-list a:hover {
  background: var(--bg-soft);
}

.research-date {
  flex: 0 0 6.5rem;
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.research-title {
  font-weight: 500;
  letter-spacing: -0.005em;
}

.research-item-arrow {
  display: inline-block;
  margin-left: 0.4rem;
  color: var(--muted-soft);
  transition: transform 200ms ease, color 160ms ease;
}

.research-list a:hover .research-item-arrow {
  transform: translateX(3px);
  color: var(--fg);
}

.research-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem 1.5rem;
  margin-top: 1.1rem;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.research-foot a {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  transition: color 160ms ease;
}

.research-foot a:hover {
  color: var(--fg);
}

.research-cve {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--muted);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  padding: 0.16rem 0.6rem;
  letter-spacing: 0.01em;
}

.research-foot-arrow {
  transition: transform 200ms ease;
}

.research-foot a:hover .research-foot-arrow {
  transform: translateX(3px);
}

/* ---------- Products (quiet) & track-record panel ---------- */
.products-note {
  margin-top: 0.7rem;
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.6;
  max-width: var(--measure);
}

.section-track .panel h2 {
  max-width: 30ch;
  text-wrap: balance;
}

.section-products {
  padding: clamp(2rem, 3vw, 3rem) 0;
}

.panel {
  background: var(--fg);
  color: var(--bg);
  border-radius: var(--r-lg);
  padding: clamp(1.75rem, 3.5vw, 2.75rem);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  max-width: 100%;
}

.panel .kicker-light {
  color: var(--muted-inv);
}

.panel h2 {
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
  max-width: 22ch;
}

.panel p {
  font-size: var(--fs-lg);
  /* softened against the panel's own ink; follows the theme instead of a
     hard-coded paper value that needed a dark-mode twin */
  color: color-mix(in srgb, currentColor 78%, transparent);
  line-height: 1.6;
  max-width: var(--measure);
}

/* ---------- Contact ---------- */
.section-contact {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding-bottom: clamp(3rem, 5vw, 4.5rem);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.contact-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  align-self: flex-start;
}

.contact-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 1px;
  color: var(--muted-soft);
  cursor: pointer;
  transition: color 160ms ease;
}

.contact-copy:hover,
.contact-copy:focus-visible {
  color: var(--fg);
}

.contact-copy svg {
  width: 15px;
  height: 15px;
}

.contact-copy-tick {
  display: none;
  color: var(--accent);
}

.contact-copy.is-copied {
  color: var(--accent);
}

.contact-copy.is-copied .contact-copy-clip {
  display: none;
}

.contact-copy.is-copied .contact-copy-tick {
  display: block;
}

.contact-note {
  font-size: var(--fs-md);
  color: var(--muted);
  margin-top: 0.15rem;
  line-height: 1.9;
}

/* Links are colour-inherit site-wide, so a link in this note needs the rule
   underline to read as one at all. */
.contact-note a:not([class]),
.post-course a {
  color: var(--fg-soft);
  font-weight: 500;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: color 160ms ease, border-color 160ms ease;
}

.contact-note a:not([class]):hover,
.post-course a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

.contact-pgp {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.01em;
  color: var(--muted);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: color 160ms ease, border-color 160ms ease;
}

.contact-pgp:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* Sized to match .nl-button (0.55rem 1.25rem at --fs-md): the Subscribe button
   now sits directly above this one at the end of every post, and at the old
   1.1rem/--fs-lede the CTA read as a banner rather than as the sibling control
   it is. One size for one action, hero included. */
.contact-cta {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  padding: 0.6rem 1.2rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  font-size: var(--fs-md);
  font-weight: 500;
  letter-spacing: -0.005em;
  align-self: flex-start;
  max-width: 100%;
  overflow-wrap: anywhere; /* the address breaks after the @ rather than pushing past a narrow screen */
  transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
}

.contact-cta:hover {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}

.contact-arrow {
  font-size: 1.1em;
  transition: transform 200ms ease;
}

.contact-cta:hover .contact-arrow {
  transform: translateX(3px);
}

/* ---------- Footer ---------- */
.footer {
  border-top: 1px solid var(--rule-strong);
  background: var(--fg);
  color: var(--bg);
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
}

.footer-inner {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: clamp(2.5rem, 5vw, 4rem) var(--pad-x) clamp(1.5rem, 3vw, 2rem);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.footer-brand {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
}

.footer-brand .brand-logo {
  margin-top: 2px;
}

.footer-name {
  font-weight: 600;
  font-size: var(--fs-md);
  letter-spacing: -0.005em;
  color: var(--bg);
  margin-bottom: 0.25rem;
}

.footer-meta {
  font-size: var(--fs-sm);
  color: var(--muted-inv);
  line-height: 1.55;
}

.footer-nav {
  display: flex;
  flex-wrap: wrap; /* six links do not fit a 320px screen on one line */
  justify-content: center;
  gap: 0.6rem 1.5rem;
  font-size: var(--fs-sm);
  color: var(--muted-inv);
}

.footer-nav a {
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 160ms ease, border-color 160ms ease;
}

.footer-nav a:hover {
  color: var(--bg);
  border-bottom-color: var(--bg);
}

.footer-bottom {
  max-width: var(--max-w);
  margin: 0 auto;
  padding: 1rem var(--pad-x) 2rem;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  font-size: var(--fs-xs);
  color: var(--muted-inv);
  border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
}

/* ---------- Legal pages ---------- */
.legal-main {
  max-width: 880px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

.legal-hero {
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(1.5rem, 3vw, 2rem);
  border-bottom: 1px solid var(--rule);
}

.legal-hero .kicker {
  display: inline-block;
  margin-bottom: 0.85rem;
}

.legal-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.05;
  margin-bottom: 0.75rem;
}

.legal-meta {
  font-size: var(--fs-sm);
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* These pages are read looking for one clause, not front to back, so the
   numbered sections get an index. A blog post already had one; a 14-clause
   legal notice needed it more. */
.legal-toc {
  margin-top: clamp(1.5rem, 3vw, 2rem);
  padding: 1rem 1.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--bg-card);
}

.legal-toc-title {
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  font-weight: 600;
  color: var(--muted-soft);
  margin-bottom: 0.6rem;
}

.legal-toc-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 0.35rem 1.5rem;
}

.legal-toc-list a {
  font-size: var(--fs-sm);
  color: var(--muted);
  border-bottom: 1px solid transparent;
  transition: color 160ms ease, border-color 160ms ease;
}

.legal-toc-list a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* The course's terms of sale: a numbered index that reads down each column,
   as the numbered clauses do. The shared grid fills row by row, so reading
   down a column gave 1, 3, 5, 7. One column below 500 px or so. */
.legal-toc-numbered {
  display: block;
  columns: 2 240px;
  column-gap: 1.5rem;
}

.legal-toc-numbered li {
  break-inside: avoid;
  padding-bottom: 0.35rem;
}

.legal-toc-numbered li:last-child {
  padding-bottom: 0;
}

/* Its way back to the course page, the "← Training" link of the course page. */
.legal-hero > .service-back {
  margin-bottom: 0.9rem;
}

.legal-body {
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(2.5rem, 5vw, 4rem);
}

.prose-legal {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  font-size: var(--fs-lg);
  line-height: 1.7;
  color: var(--fg);
}

.prose-legal h2 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.005em;
  margin-top: 2rem;
  margin-bottom: 0.1rem;
  scroll-margin-top: 1.5rem; /* jumped to from .legal-toc */
}

.prose-legal h2:first-child {
  margin-top: 0;
}

.prose-legal a {
  color: var(--fg);
  border-bottom: 1px solid var(--rule);
  transition: border-color 160ms ease;
}

.prose-legal a:hover {
  border-bottom-color: var(--fg);
}

.prose-legal strong {
  font-weight: 600;
}

.legal-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-left: 1.1rem;
  list-style: disc;
}

.legal-list li::marker {
  color: var(--muted-soft);
}

/* ---------- Blog ---------- */
.blog-main {
  max-width: 880px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

/* Category filter — client-side, revealed by JS (hidden without it, so the
   no-JS reader still gets every card). Pills follow the tag idiom; the active
   one inverts (ink on paper) per the Inversion rule. */
.blog-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: clamp(1.25rem, 2.5vw, 1.75rem);
}

.blog-filter[hidden] {
  display: none;
}

.blog-filter-btn {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--muted-soft);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  padding: 0.35rem 0.85rem;
  cursor: pointer;
  transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
}

.blog-filter-btn:hover {
  color: var(--fg);
  border-color: var(--fg);
}

.blog-filter-btn.is-active {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.blog-list {
  display: flex;
  flex-direction: column;
  padding: clamp(1.5rem, 3vw, 2.25rem) 0 clamp(2.5rem, 5vw, 4rem);
}

.blog-list-empty {
  font-size: var(--fs-md);
  color: var(--muted);
  padding: 2rem 0;
}

.blog-card {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1.5rem 0.5rem;
  color: inherit;
  transition: background-color 200ms ease;
}

/* rules between cards only — the hero bounds the top, the footer the bottom */
.blog-card + .blog-card {
  border-top: 1px solid var(--rule);
}

/* Filtering hides cards with [hidden]; .blog-card's own display:flex would
   otherwise beat the UA rule. .is-lead is set by JS on the first visible card
   so a filtered list never shows a stray top rule. */
.blog-card[hidden] {
  display: none;
}

.blog-card.is-lead {
  border-top: 0;
}

.blog-card:hover {
  background: var(--bg-soft);
}

.blog-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.blog-card-cat {
  color: var(--muted-soft);
}

.blog-card-date {
  font-size: var(--fs-2xs);
  color: var(--muted-soft);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.blog-card-title {
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.blog-card-desc {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.55;
  max-width: var(--measure);
  margin-bottom: 0.2rem;
}

.blog-card-foot {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
  font-size: var(--fs-xs);
  color: var(--muted-soft);
}

.blog-card-author {
  color: var(--muted);
  font-weight: 500;
}


.blog-card-read {
  font-variant-numeric: tabular-nums;
}

.blog-card-tags {
  display: inline-flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-left: 0.15rem;
}

.blog-card-tag {
  font-size: var(--fs-2xs);
  font-family: var(--font-mono);
  color: var(--chip-fg);
  background: var(--rule);
  padding: 0.1rem 0.45rem;
  border-radius: 1px;
  letter-spacing: 0.01em;
}

.blog-card-arrow {
  display: inline-block;
  margin-left: 0.4rem;
  color: var(--muted-soft);
  transition: transform 200ms ease, color 160ms ease;
}

.blog-card:hover .blog-card-arrow {
  transform: translateX(3px);
  color: var(--fg);
}

/* The intro under the blog's title is a lede, as on every other page. It was
   set in the legal pages' small print (.legal-meta, tracked at 0.04em), with
   its links underlined by inline style attributes. */
.blog-lede {
  max-width: var(--measure);
  font-size: var(--fs-lede);
  color: var(--muted);
  line-height: 1.6;
}

.blog-lede a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.blog-lede a:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* The Apple internals series as one entry of the list (build.py
   render_series_block): the card's head, title and line, then the stack the
   series covers, one cell per post. The cells are laid out as the figure in
   the series index draws the layers, side by side where it puts them side by
   side, on a 12-column grid whose 1px gaps show the rule colour underneath:
   rows that share their hairlines, as the brand guide's HTML schematic does. */
.blog-series {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1.5rem 0.5rem;
}

.blog-series[hidden] {
  display: none;
}

/* The series' posts as a list, folded under its grid: closed by default, opened by its summary. */
.series-list {
  margin-top: 0.4rem;
  border-top: 1px solid var(--rule);
}

.series-list > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 0;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--fg);
  cursor: pointer;
  list-style: none;
}

.series-list > summary::-webkit-details-marker {
  display: none;
}

.series-list > summary::after {
  content: "";
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border: solid var(--muted);
  border-width: 0 1px 1px 0;
  transform: translateY(-25%) rotate(45deg);
  transition: transform 160ms ease;
}

.series-list[open] > summary::after {
  transform: translateY(25%) rotate(-135deg);
}

.series-list-cards > .blog-card + .blog-card {
  border-top: 1px solid var(--rule);
}

@media (prefers-reduced-motion: reduce) {
  .series-list > summary::after {
    transition: none;
  }
}

.blog-card + .blog-series,
.blog-series + .blog-card {
  border-top: 1px solid var(--rule);
}

/* Restated after the rules above, which would otherwise win on source order
   and draw a stray top rule over the first visible entry. */
.blog-card.is-lead,
.blog-series.is-lead {
  border-top: 0;
}

/* The series title links to its index (#0), underlined on intent like a link
   in a post body. It is not stretched over the block: the cells are links. */
.series-title-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.16em;
  transition: text-decoration-color 160ms ease;
}

.series-title-link:hover {
  text-decoration-color: var(--fg);
}

.series-stack {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 1px;
  margin: 0.45rem 0 0.2rem;
  padding: 0;
  list-style: none;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 1px;
}

.series-cell {
  display: flex;
  grid-column: span 12;
}

.series-span-6 { grid-column: span 6; }
.series-span-4 { grid-column: span 4; }
.series-span-3 { grid-column: span 3; }

.series-link {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "layer num"
    "post post";
  align-content: start;
  gap: 0.1rem 0.75rem;
  padding: 0.7rem 0.9rem 0.75rem;
  color: inherit;
  background: var(--bg-card);
  transition: background-color 160ms ease;
}

.series-layer {
  grid-area: layer;
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--fg);
}

.series-num {
  grid-area: num;
  font-size: var(--fs-xs);
  color: var(--muted-soft);
  font-variant-numeric: tabular-nums;
}

.series-post {
  grid-area: post;
  font-size: var(--fs-sm);
  line-height: 1.4;
  color: var(--muted);
  transition: color 160ms ease;
}

.series-link:hover,
.series-link:focus-visible {
  background: var(--bg-soft);
}

.series-link:hover .series-post,
.series-link:focus-visible .series-post {
  color: var(--fg);
}

.series-link:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -3px;
}

/* #0, the index that maps every layer to its post, opens the stack in ink,
   like the root of the home page's schematic (The Inversion Rule). */
.series-index .series-link {
  --focus-ring: var(--bg);
  background: var(--fg);
}

.series-index .series-layer {
  color: var(--bg);
}

.series-index .series-num,
.series-index .series-post {
  color: var(--muted-inv);
}

.series-index .series-link:hover,
.series-index .series-link:focus-visible {
  background: var(--fg-soft);
}

.series-index .series-link:hover .series-post,
.series-index .series-link:focus-visible .series-post {
  color: var(--bg);
}

/* A phone stacks every layer, in the series' order. */
@media (max-width: 560px) {
  .blog-series {
    padding: 1.3rem 0;
  }

  .series-span-6,
  .series-span-4,
  .series-span-3 {
    grid-column: span 12;
  }
}

@media (prefers-reduced-motion: reduce) {
  .series-link,
  .series-post {
    transition: none;
  }
}

/* ---------- Single post ---------- */
.post-main {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

.post-hero {
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(1.25rem, 2.5vw, 1.75rem);
  border-bottom: 1px solid var(--rule);
}

.post-title {
  font-size: var(--fs-post);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  margin-bottom: 0.75rem;
}

.post-byline {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin-top: 1.6rem;
}

.post-avatar {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  object-fit: cover;
  filter: grayscale(0.2) saturate(0.92);
}

.post-avatar--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--muted-soft);
  background: linear-gradient(150deg, var(--bg-soft) 0%, var(--rule) 100%);
}

.post-byline-text {
  min-width: 0;
}

.post-author {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--fg);
  letter-spacing: -0.005em;
}

.post-meta {
  margin: 0.3rem 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

.post-meta .post-meta-sep {
  margin: 0;
  color: var(--muted-soft);
  white-space: pre;
}

.post-author-link {
  color: inherit;
  transition: color 160ms ease;
}

.post-author-link:hover {
  color: var(--accent);
}

.post-author-sep {
  margin: 0;
  font-weight: 400;
  color: var(--muted-soft);
  /* pre-wrap, not pre: the space is kept AND the line may break after it,
     which is where the title goes when it does not fit beside the name. */
  white-space: pre-wrap;
}

.post-author-role {
  font-weight: 400;
  color: var(--muted);
  /* "Founder & security researcher" moves to its own line as one piece on a
     phone, instead of breaking after the ampersand; still wraps inside itself
     on a screen too narrow to hold it. */
  display: inline-block;
}

.post-avatar-link {
  display: inline-flex;
  flex: 0 0 auto;
  border-radius: 1px;
}

.post-avatar-link:hover .post-avatar {
  border-color: var(--fg);
}

/* Chapô / dek */
.post-dek {
  margin: 0;
  max-width: var(--measure);
  font-size: var(--fs-lede);
  line-height: 1.5;
  color: var(--muted);
  letter-spacing: -0.003em;
}

/* Reading progress bar */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 80ms linear;
  z-index: 100;
}

/* Table of contents */
.post-toc {
  margin: clamp(1.5rem, 3vw, 2rem) 0 0;
  padding: 0.7rem 1.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--bg-card);
}

/* Collapsed by default: the list is navigation, and eight entries was the
   tallest thing standing between the title and the first sentence. <details>
   is native, so it still opens with JS off. */
.post-toc-details[open] {
  padding-bottom: 0.55rem;
}

.post-toc-title {
  margin: 0;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  transition: color 160ms ease;
}

/* Safari draws its own disclosure triangle through this pseudo-element. */
.post-toc-title::-webkit-details-marker {
  display: none;
}

.post-toc-title:hover {
  color: var(--fg);
}

/* The chevron is drawn here rather than left to the UA marker, so it can carry
   the site's own rotation and colour. */
.post-toc-title::before {
  content: "";
  flex: 0 0 auto;
  width: 0.42rem;
  height: 0.42rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 200ms ease;
}

.post-toc-details[open] .post-toc-title::before {
  transform: rotate(45deg);
}

.post-toc-count {
  font-family: var(--font-mono);
  font-size: 0.9em;
  letter-spacing: 0;
  color: var(--chip-fg);
  background: var(--rule);
  padding: 0.05rem 0.4rem;
  border-radius: 1px;
}

.post-toc-details[open] .post-toc-list {
  margin-top: 0.7rem;
}

.post-toc-list {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: toc;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.post-toc-list li {
  counter-increment: toc;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.post-toc-list li::before {
  content: counter(toc, decimal-leading-zero);
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--muted-soft);
}

.post-toc-list a {
  color: var(--fg-soft);
  font-size: var(--fs-md);
  line-height: 1.4;
  border-bottom: 1px solid transparent;
  transition: border-color 160ms ease, color 160ms ease;
}

.post-toc-list a:hover {
  color: var(--fg);
  border-bottom-color: var(--rule);
}

.post-body {
  padding: clamp(1.75rem, 3.5vw, 2.5rem) 0 clamp(1.5rem, 3vw, 2rem);
  font-size: var(--fs-lg);
  line-height: 1.75;
  color: var(--fg);
}

.post-body > * + * {
  margin-top: 1rem;
}

.post-body h2 {
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  margin-top: 2.25rem;
  margin-bottom: 0.5rem;
  scroll-margin-top: 1.5rem;
}

.post-body h3 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.012em;
  line-height: 1.25;
  margin-top: 1.75rem;
  margin-bottom: 0.4rem;
  scroll-margin-top: 1.5rem;
}

.post-body p {
  font-size: var(--fs-lg);
  line-height: 1.75;
}

/* Un vrai soulignement, pas une bordure. Une bordure basse ne peut pas eviter
   les jambages et se pose plus loin du texte que le trait auquel elle sert de
   substitut ; `text-underline-offset` fait les deux. Il s'epaissit au survol
   plutot que de changer de couleur : le lien reste lisible en cours de geste. */
.post-body a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.post-body a:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* 🔴 Les bandes des schemas sont des <a> autour de texte SVG. L'ancienne
   bordure basse ne s'y dessinait pas — une bordure n'a pas de sens sur un
   element SVG — donc le passage au vrai soulignement a barre TOUTES les bandes
   du diagramme d'un coup. Vu a la capture, pas deduit. */
.post-body svg a,
.post-body svg a:hover {
  text-decoration: none;
}

.post-body strong {
  font-weight: 600;
}

.post-body em {
  font-style: italic;
}

.post-body ul,
.post-body ol {
  padding-left: 1.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.post-body ul {
  list-style: disc;
}

.post-body ol {
  list-style: decimal;
}

.post-body li::marker {
  color: var(--muted-soft);
}

/* .post-summary is a sibling of .post-body, not a descendant, so the In brief
   card needs naming here or a backtick in a summary renders as bare text. The
   blocks avoid symbols by editorial rule, which makes this dormant rather than
   unnecessary: render_inline_md still emits <code> for anyone who uses one. */
.post-body code,
.post-summary code {
  padding: 0.12rem 0.4rem;
  background: var(--rule);
  border-radius: 1px;
  font-family: var(--font-mono);
  font-size: 0.88em;
  /* hashes and symbol names offer no break opportunity and would otherwise
     push the whole page wider than the viewport */
  overflow-wrap: anywhere;
}

/* Inline diagram, currently only in the Apple internals index. It is inline SVG
   rather than an <img> so it reads the theme tokens directly and follows the
   theme toggle; an external image only ever sees the OS preference. The viewBox
   gives it its aspect ratio, so no height is reserved by hand. */
.post-body .stack-diagram {
  display: block;
  width: 100%;
  height: auto;
  margin: 1.5rem 0;
}

.post-body .stack-diagram text {
  font-family: inherit;
}

.post-body .stack-diagram .band {
  fill: var(--bg-card);
  stroke: var(--rule);
  stroke-width: 1;
}

.post-body .stack-diagram .spine {
  stroke: var(--rule);
  stroke-width: 2;
}

.post-body .stack-diagram .t {
  fill: var(--fg);
  font-size: 17px;
  font-weight: 600;
}

.post-body .stack-diagram .s {
  fill: var(--muted);
  font-size: 13.5px;
}

.post-body .stack-diagram .f {
  fill: var(--muted);
  font-size: 12.5px;
}

.post-body .stack-diagram .n {
  fill: var(--accent);
  font-size: 15px;
  font-weight: 600;
}

/* Each band links to the post that covers it. The post script leaves these
   alone, so they open in the tab like the internal links they are. */
.post-body .stack-diagram a {
  cursor: pointer;
}

.post-body .stack-diagram a .band,
.post-body .stack-diagram a .t {
  transition: stroke 160ms ease, fill 160ms ease;
}

.post-body .stack-diagram a:hover .band,
.post-body .stack-diagram a:focus-visible .band {
  stroke: var(--accent);
  stroke-width: 2;
}

.post-body .stack-diagram a:hover .t,
.post-body .stack-diagram a:focus-visible .t {
  fill: var(--accent);
}

/* Injected by the post script around every pre. It is the positioning context
   for the copy button: the pre itself scrolls horizontally, and anchoring the
   button there carries it off with the code. */
.post-body .code-block {
  position: relative;
}

.post-body pre,
.paper-bibtex {
  background: var(--fg);
  color: var(--bg);
  padding: 1rem 1.1rem;
  border-radius: var(--r-md);
  overflow-x: auto;
  font-size: var(--fs-sm);
  line-height: 1.55;
  margin-top: 1.25rem;
}

.post-body pre code {
  background: transparent;
  border: none;
  padding: 0;
  color: inherit;
  font-size: inherit;
  overflow-wrap: normal; /* pre scrolls horizontally rather than wrapping */
}

.code-copy {
  position: absolute;
  top: 0.5rem;
  right: 0.55rem;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: rgba(245, 241, 234, 0.55);
  background: transparent;
  border: 1px solid rgba(245, 241, 234, 0.18);
  border-radius: 1px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 160ms ease, color 160ms ease, border-color 160ms ease, background 160ms ease;
}

.code-copy svg {
  display: block;
}

.post-body .code-block:hover .code-copy,
.code-copy:focus-visible {
  opacity: 1;
}

@media (hover: none) {
  .post-body .code-copy {
    opacity: 1;
  }
  .post-body .code-block pre {
    padding-top: 2.6rem;
  }
}

.code-copy:hover {
  color: rgba(245, 241, 234, 0.95);
  border-color: rgba(245, 241, 234, 0.42);
  background: rgba(245, 241, 234, 0.06);
}

.code-copy.copied {
  color: rgba(245, 241, 234, 0.95);
  border-color: rgba(245, 241, 234, 0.42);
  opacity: 1;
}

.post-body blockquote {
  margin: 1.5rem 0;
  padding: 1rem 1.25rem;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  color: var(--muted);
}

.post-body blockquote code {
  color: var(--fg);
}

.post-body blockquote p {
  margin: 0;
}

/* ---------- Callouts (note / tip / warning / danger) ---------- */
.post-body .admonition {
  --callout-stripe: var(--muted-soft);

  margin: 1.5rem 0;
  padding: 0.85rem 1.1rem;
  background: color-mix(in srgb, var(--callout-stripe) var(--tint-callout), var(--bg-soft));
  border: 1px solid color-mix(in srgb, var(--callout-stripe) 30%, var(--rule));
  border-radius: var(--r-md);
  color: var(--fg-soft);
  font-size: var(--fs-md);
  line-height: 1.7;
}

.post-body .admonition > * + * {
  margin-top: 0.5rem;
}

.post-body .admonition p {
  margin: 0;
}

.post-body .admonition-title {
  margin: 0 0 0.15rem;
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: lowercase;
  color: var(--callout-stripe);
}

.post-body .admonition.note    { --callout-stripe: var(--status-slate); }
.post-body .admonition.tip     { --callout-stripe: var(--status-moss); }
.post-body .admonition.warning { --callout-stripe: var(--accent); }
.post-body .admonition.danger  { --callout-stripe: var(--status-brick); }

.post-body img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: var(--r-md);
  margin: 1.5rem auto;
}

.post-body .post-video {
  margin: 1.5rem 0;
}

.post-body video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-md);
  background: #000;
}

.post-body iframe {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
  border-radius: var(--r-md);
  margin: 1.75rem 0;
  background: #101216; /* letterbox : le fond sombre du systeme, jamais du noir pur */
}

.post-body hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 2rem 0;
}

/* A table that cannot fit scrolls inside itself, the way `pre` already does.
   Before this, the five-column table in sptm-txm-memory-tagging pushed the
   whole document 39px wider than a 390px phone, and a document wider than the
   screen slides out from under every position:fixed control on the page: the
   reading pill and the back-to-top button both stopped looking centred as soon
   as the reader scrolled sideways. display:block is what makes the overflow
   the table's own; width:max-content keeps its natural column widths, and
   max-width:100% lets it still fill the column when it does fit. */
.post-body table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  margin: 1.25rem 0;
}

.post-body th,
.post-body td {
  text-align: center;
  padding: 0.55rem 0.7rem;
  border-bottom: 1px solid var(--rule);
}

.post-body th {
  font-weight: 600;
  color: var(--fg);
}

/* Post CTA — a post ends on the offer it belongs to, not on a link back to the
   index. The hairline that used to open .post-footer now opens this block. */
/* The line that points a reverse-engineering or forensics post to the online
   course. It follows the article, outside it, so the narration never reads it. */
.post-course {
  max-width: var(--measure);
  font-size: var(--fs-md);
  line-height: 1.7;
  color: var(--muted);
}

.post-cta {
  border-top: 1px solid var(--rule);
  padding-top: clamp(1.5rem, 3vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.post-cta-line {
  font-size: var(--fs-md);
  color: var(--muted);
  max-width: var(--measure);
}

.post-cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.85rem 1.35rem;
}

.post-cta-service {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--muted);
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 160ms ease, border-color 160ms ease;
}

.post-cta-service:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

.post-cta-arrow {
  transition: transform 200ms ease;
}

.post-cta-service:hover .post-cta-arrow {
  transform: translateX(3px);
}

.post-footer {
  padding: clamp(1.5rem, 3vw, 2rem) 0 clamp(2.5rem, 5vw, 4rem);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.post-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.post-tag {
  border: 1px solid var(--rule);
  background: var(--bg-card);
  border-radius: 1px;
  padding: 0.3rem 0.75rem;
  font-size: var(--fs-xs);
  color: var(--muted);
  letter-spacing: 0.01em;
}

.back-to-blog {
  font-size: var(--fs-sm);
  color: var(--muted);
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  align-self: flex-start;
  transition: color 160ms ease, border-color 160ms ease;
}

.back-to-blog:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* ---------- Team ---------- */
/* Shorter than a window since it lost its talks: the hub's recipe, scoped to
   this page's body class, keeps the footer on the bottom edge. */
.page-team {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.page-team > .nav,
.page-team > main {
  width: 100%;
}

.page-team > main {
  flex: 1 0 auto;
}

.team-main {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

/* The page is its title and the people under it (the owner's call,
   2026-11-13): no lede and no rule between them, the card answers the title. */
.team-hero {
  padding: clamp(2rem, 4vw, 3rem) 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.7rem;
}

.team-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.team-meta {
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-top: 0.4rem;
}

.team-list {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(2.75rem, 5vw, 3.75rem);
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(3rem, 6vw, 4.5rem);
}

.team-empty {
  font-size: var(--fs-md);
  color: var(--muted);
  padding: 2rem 0;
  text-align: center;
}

.team-member {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: 100%;
  max-width: 420px;
}

.team-photo {
  position: relative;
  width: clamp(124px, 18vw, 156px);
  aspect-ratio: 1 / 1;
  container-type: inline-size;
  margin-bottom: 1.1rem;
}

.team-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(0.2) saturate(0.92);
  transition: filter 280ms ease, transform 380ms ease;
}

.team-member:hover .team-photo img {
  filter: none;
}

/* Le repli n'a de sens que sans photo. La photo etant detouree, son degrade
   se voyait au travers et refaisait le cadre qu'on venait d'enlever. */
.team-photo:has(img) .team-photo-fallback {
  display: none;
}

.team-photo-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(150deg, var(--bg-soft) 0%, var(--rule) 100%);
  color: var(--muted-soft);
  font-size: 26cqw; /* initials track the avatar circle, not the text ramp */
  font-weight: 600;
  letter-spacing: -0.02em;
  user-select: none;
}

.team-name {
  font-size: var(--fs-subhead);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
  margin: 0;
}

/* The title, under the name: the same words as the post bylines. */
.team-role {
  margin-top: 0.4rem;
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.55;
}

.team-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 1rem;
}

.team-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--rule);
  background: var(--bg-card);
  border-radius: 1px;
  font-size: var(--fs-xs);
  color: var(--muted);
  letter-spacing: 0.005em;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}

.team-link:hover {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.team-link-arrow {
  transition: transform 200ms ease;
}

.team-link:hover .team-link-arrow {
  transform: translateX(2px);
}

.team-divider {
  width: 32px;
  height: 1px;
  background: var(--rule);
  border: 0;
  margin: 0;
}

/* ---------- Advisories ---------- */
.adv-main {
  max-width: 880px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

.adv-hero {
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(1.25rem, 2.5vw, 1.75rem);
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.7rem;
}

.adv-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.adv-lede {
  font-size: var(--fs-lg);
  color: var(--muted);
  max-width: var(--measure);
  line-height: 1.65;
  /* Centred, so balanced: `pretty` left "write-up." alone on its line. */
  text-wrap: balance;
}

.adv-meta a {
  color: var(--fg);
  border-bottom: 1px solid var(--rule);
  transition: border-color 160ms ease;
}

.adv-meta a:hover {
  border-bottom-color: var(--fg);
}

.adv-meta {
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-top: 0.4rem;
}

.adv-list {
  display: flex;
  flex-direction: column;
  gap: clamp(2.5rem, 5vw, 3.5rem);
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(3rem, 6vw, 4.5rem);
}

.adv-empty {
  font-size: var(--fs-md);
  color: var(--muted);
  padding: 2rem 0;
  text-align: center;
}

/* ---- groups ---- */
.adv-group-head {
  margin-bottom: 1rem;
}

.adv-group-title {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  font-size: var(--fs-subhead);
  font-weight: 600;
  letter-spacing: -0.025em;
}

.adv-group-count {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--chip-fg);
  background: var(--rule);
  border-radius: 1px;
  padding: 0.1rem 0.5rem;
  font-variant-numeric: tabular-nums;
}

.adv-group-blurb {
  margin-top: 0.35rem;
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.5;
}

/* ---- table ---- */
.adv-table-wrap {
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--bg-card);
}

.adv-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}

.adv-table thead th {
  text-align: left;
  font-size: var(--fs-3xs);
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  font-weight: 600;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}

.adv-table thead th { padding: 0.7rem 0.75rem; }

.adv-table tbody td {
  padding: 0.8rem 0.75rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
  color: var(--fg-soft);
}

.adv-table tbody tr:last-child td {
  border-bottom: none;
}

.adv-row {
  transition: background 160ms ease;
}

.adv-row:hover {
  background: color-mix(in srgb, var(--fg) 4%, transparent);
}

.adv-table { table-layout: fixed; }

.adv-col-sev    { width: 5.5rem; }
.adv-col-class  { width: 8rem; }
.adv-col-credit { width: 7rem; }
.adv-col-status { width: 10rem; }   /* the mono CVE badge fills 9rem */
.adv-col-date   { width: 6.5rem; }

.adv-c-sev,
.adv-c-credit,
.adv-c-status,
.adv-c-date { white-space: nowrap; }

.adv-c-vuln { overflow-wrap: anywhere; }

.adv-c-date {
  font-variant-numeric: tabular-nums;
  color: var(--muted-soft);
}

.adv-row-title {
  display: block;
  font-weight: 600;
  font-size: var(--fs-md);
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--fg);
}

a.adv-row-title {
  transition: color 160ms ease;
}

a.adv-row-title:hover {
  color: var(--accent);
}

.adv-row-arrow {
  color: var(--muted);
  transition: transform 200ms ease, color 160ms ease;
}

a.adv-row-title:hover .adv-row-arrow {
  transform: translate(2px, -2px);
  color: var(--accent);
}

.adv-row-sub {
  display: block;
  margin-top: 0.2rem;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.adv-c-credit a {
  border-bottom: 1px solid transparent;
  transition: border-color 160ms ease;
}

.adv-c-credit a:hover {
  border-bottom-color: var(--fg);
}

.adv-dash { color: var(--muted-soft); }

/* ---- badges (shared) ---- */
.adv-status,
.adv-sev {
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 0.18rem 0.55rem;
  border-radius: 1px;
  border: 1px solid var(--adv-badge, var(--rule));
  color: var(--adv-badge, var(--muted));
  background: color-mix(in srgb, var(--adv-badge, var(--rule)) var(--tint-badge), transparent);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.adv-status-assigned { --adv-badge: var(--status-moss); font-family: var(--font-mono); letter-spacing: 0; }
.adv-status-pending  { --adv-badge: var(--status-amber); }
.adv-status-none     { --adv-badge: var(--status-slate); }

.adv-sev-low      { --adv-badge: var(--status-slate); }
.adv-sev-medium   { --adv-badge: var(--status-amber); }
.adv-sev-high     { --adv-badge: var(--status-orange); }
.adv-sev-critical { --adv-badge: var(--status-brick); }
html.dark .adv-sev-critical { --adv-badge: color-mix(in srgb, var(--status-brick) 88%, var(--fg)); }
@media (prefers-color-scheme: dark) {
  html:not(.light) .adv-sev-critical { --adv-badge: color-mix(in srgb, var(--status-brick) 88%, var(--fg)); }
}

.adv-class-name { display: block; }

.adv-cwe {
  display: inline-block;
  margin-top: 0.3rem;
  font-family: var(--font-mono);
  font-size: 1em;
  color: var(--chip-fg);
  background: var(--rule);
  padding: 0.05em 0.35em;
  border-radius: 1px;
  white-space: nowrap;
}

/* ---- responsive: stack rows into cards ---- */
@media (max-width: 720px) {
  .adv-table,
  .adv-table thead,
  .adv-table tbody,
  .adv-table tr,
  .adv-table td { display: block; width: auto; }

  .adv-table thead { display: none; }
  .adv-table colgroup { display: none; }

  .adv-table tbody tr {
    display: flex;
    flex-direction: column;
    padding: 1rem 1.1rem;
    border-bottom: 1px solid var(--rule);
  }

  .adv-table tbody tr:last-child { border-bottom: none; }

  .adv-table tbody td {
    border: none;
    padding: 0.16rem 0;
    white-space: normal;
    min-width: 0;
    display: flex;
    flex-wrap: wrap; /* a badge wider than its row at a large text size drops under its label, never clipped */
    gap: 0.25rem 0.75rem;
    align-items: baseline;
  }

  .adv-table tbody td::before {
    content: attr(data-label);
    flex: 0 0 4.5rem;
    font-size: var(--fs-3xs);
    letter-spacing: 0.01em;
    color: var(--muted-soft);
    font-weight: 600;
    padding-top: 0.15rem;
  }

  .adv-table tbody td.adv-c-vuln {
    display: block;
    order: -1;
    margin-bottom: 0.5rem;
  }

  .adv-table tbody td.adv-c-vuln::before { display: none; }
}

/* The embargoed findings, one line under the tables (build.py _embargo_line).
   A plain muted sentence under a hairline: a frame would read as a third,
   empty table, and the guide keeps dashes for a diagram's boundary. */
.adv-embargo-line {
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-md);
  line-height: 1.55;
  color: var(--muted);
  text-wrap: pretty;
}

/* ---------- Brand kit ---------- */
.brand-main {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

.brand-hero {
  padding: clamp(2rem, 4vw, 3rem) 0 clamp(1.5rem, 3vw, 2.25rem);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  align-items: flex-start;
}

/* Meme regle : tous les textes du hero sur la meme mesure, donc un seul bord. */
.brand-hero > p,
.brand-hero > h1 {
  max-width: var(--measure);
}

.brand-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.brand-lede {
  font-size: var(--fs-lg);
  color: var(--muted);
  line-height: 1.65;
}

.brand-lede a {
  color: var(--fg);
  border-bottom: 1px solid var(--rule);
  transition: border-color 160ms ease;
}

.brand-lede a:hover {
  border-bottom-color: var(--fg);
}

.brand-section {
  padding: clamp(2rem, 3.5vw, 3rem) 0;
  border-top: 1px solid var(--rule);
}

.brand-section-end {
  padding-bottom: clamp(3rem, 5vw, 4.5rem);
}

.brand-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 0.85rem;
}

.brand-grid-social {
  grid-template-columns: 1fr;
  grid-auto-rows: auto;
}

.logo-card {
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  overflow: hidden;
  height: 100%;
}

.logo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  height: 200px;
}

.logo-preview img {
  max-height: 96px;
  max-width: 80%;
  width: auto;
  height: auto;
}

.logo-preview-light {
  background: #eeefec;
}

.logo-preview-dark {
  background: #101216;
}

.logo-preview-wide {
  padding: 2rem 1.5rem;
}

.logo-preview-edge {
  padding: 0;
  height: auto;
}

.logo-preview-edge img {
  display: block;
  width: 100%;
  height: auto;
  max-width: none;
  max-height: none;
}

.logo-preview img {
  display: block;
  max-width: 100%;
  height: auto;
}

.logo-meta {
  padding: 1rem 1.1rem 1.15rem;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.logo-meta h3 {
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.012em;
}

.logo-meta > p {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.5;
}

.logo-files {
  margin-top: 0.5rem !important;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.logo-files a,
.type-files a {
  font-size: var(--fs-2xs);
  letter-spacing: 0.02em;
  padding: 0.28rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: 1px;
  color: var(--muted);
  background: var(--bg);
  font-variant-numeric: tabular-nums;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}

.logo-files a:hover,
.type-files a:hover {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.palette-themes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
}

.palette-theme {
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: 1.1rem 1.1rem 1.25rem;
}

.palette-theme-dark {
  background: #101216;
  border-color: #3a414b;
  color: #f7f7f5;
}

.palette-label {
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  font-weight: 600;
  margin-bottom: 0.85rem;
}

.palette-theme-dark .palette-label {
  color: #8b939f;
}

.swatches {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.swatch {
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 0.7rem;
  align-items: center;
}

.swatch-chip {
  display: block;
  width: 28px;
  height: 28px;
  border-radius: 1px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
}

.swatch-meta {
  display: grid;
  grid-template-columns: minmax(0, auto) 1fr;
  gap: 0.6rem 1rem;
  align-items: baseline;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

.swatch-hex {
  letter-spacing: 0.01em;
  font-weight: 500;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

.swatch-role {
  color: var(--muted);
  font-size: var(--fs-xs);
  text-align: right;
}

.palette-theme-dark .swatch-role {
  color: #9aa1ac;
}

.swatch-accent .swatch-hex {
  color: var(--accent);
}

.palette-theme-dark .swatch-accent .swatch-hex {
  color: #8aa9f0;
}

.type-specimens {
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
}

.type-specimen {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-bottom: 1.4rem;
  border-bottom: 1px solid var(--rule);
}

.type-specimen:last-child {
  border-bottom: 0;
}

.type-line {
  color: var(--fg);
  line-height: 1.15;
}

.type-display {
  font-size: var(--fs-display);
  font-weight: 600;
  letter-spacing: -0.03em; /* matches .hero-title: a specimen must be truthful */
  line-height: 1.05;
}

.type-h2 {
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.type-tagline {
  font-size: var(--fs-lede);
  font-weight: 500;
  letter-spacing: -0.012em;
}

.type-title {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.3;
}

.type-small {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.type-body {
  font-size: var(--fs-lg);
  font-weight: 400;
  line-height: 1.6;
  color: var(--fg-soft);
  max-width: var(--measure);
}

.type-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.1rem;
  font-size: var(--fs-2xs);
  letter-spacing: 0.04em;
  color: var(--muted-soft);
  font-variant-numeric: tabular-nums;
}

.type-meta span:first-child {
  color: var(--muted);
  font-weight: 600;
}

.type-files {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: 1.5rem;
}

.brand-rules {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure);
}

.brand-rules li {
  position: relative;
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.6;
  padding-left: 1.1rem;
}

.brand-rules li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--muted-soft);
}

.brand-rules strong {
  color: var(--fg);
  font-weight: 600;
}

.press-quote {
  margin: 0;
  padding: 1.25rem 1.4rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  max-width: 72ch;
}

.press-quote p {
  font-size: var(--fs-md);
  line-height: 1.65;
  color: var(--fg-soft);
}

/* ---------- Brand guide (/brand/) ----------
   The page documents the system and has to obey it: tokens only, the two text
   sizes, hairlines, 1px corners, no ornament. Live specimens reuse the site's
   own classes (tile, panel, buttons, .post-body for code, callouts, tables and
   figures, wrapped in .brand-demo); what follows is only what the page itself
   needs to lay that documentation out. */
.brand-section {
  scroll-margin-top: 1rem;
}

.brand-toc-nav {
  align-self: stretch;
}

.brand-toc {
  align-self: stretch;
  columns: 3 14rem;
  column-gap: 2rem;
  counter-reset: brand-toc;
  margin-top: 0.6rem;
}

.brand-toc li {
  counter-increment: brand-toc;
  break-inside: avoid;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.45rem 0;
  border-top: 1px solid var(--rule);
}

.brand-toc li::before {
  content: counter(brand-toc, decimal-leading-zero);
  flex: 0 0 1.5rem;
  font-size: var(--fs-sm);
  color: var(--muted-soft);
  font-variant-numeric: tabular-nums;
}

.brand-toc a {
  font-size: var(--fs-md);
  color: var(--fg-soft);
  border-bottom: 1px solid transparent;
  transition: color 160ms ease, border-color 160ms ease;
}

.brand-toc a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* One block of documentation inside a section: an h3 name, prose, then a table,
   a list or a specimen. */
.brand-sub {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
}

.section-head + .brand-sub,
.brand-section > .brand-sub:first-child {
  margin-top: 0;
}

.brand-h3 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.018em;
  line-height: 1.3;
}

.brand-text {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  max-width: var(--measure);
  font-size: var(--fs-md);
  line-height: 1.65;
  color: var(--fg-soft);
}

.brand-text a,
.brand-rules a,
.brand-table a,
.brand-dl a,
.brand-section .section-head p a {
  color: var(--fg);
  border-bottom: 1px solid var(--rule);
  transition: border-color 160ms ease;
}

.brand-text a:hover,
.brand-rules a:hover,
.brand-table a:hover,
.brand-dl a:hover,
.brand-section .section-head p a:hover {
  border-bottom-color: var(--fg);
}

.brand-text strong,
.brand-dl strong {
  font-weight: 600;
  color: var(--fg);
}

/* Inline code in the guide's own prose and tables. 0.875rem, not the posts'
   0.88em: inside a 14px table cell the em size fell to 12.3px, under the floor. */
.brand-text code,
.brand-rules code,
.brand-table code,
.brand-dl code,
.brand-section .section-head code {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--fg);
  background: var(--rule);
  padding: 0.05rem 0.35rem;
  border-radius: 1px;
  overflow-wrap: anywhere;
}

.brand-rules {
  max-width: var(--measure);
}

/* Reference tables. They follow the table rule the page states: text left,
   figures right in tabular numerals, one hairline under each row, no fill, no
   vertical rule, and a table wider than a phone scrolls inside itself. */
.brand-table-wrap {
  max-width: 100%;
  overflow-x: auto;
}

.brand-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  line-height: 1.45;
  font-variant-numeric: tabular-nums;
}

.brand-table-wide {
  min-width: 40rem;
}

.brand-table th,
.brand-table td {
  text-align: left;
  vertical-align: top;
  padding: 0.55rem 1rem 0.55rem 0;
  border-bottom: 1px solid var(--rule);
}

.brand-table th:last-child,
.brand-table td:last-child {
  padding-right: 0;
}

.brand-table thead th {
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.brand-table tbody th {
  font-weight: 600;
  color: var(--fg);
}

.brand-table td {
  color: var(--fg-soft);
}

.brand-table .num {
  text-align: right;
  white-space: nowrap;
}

/* In a cell, a code value breaks only when it cannot fit at all: "anywhere"
   would let the column shrink until "rect" split into "rec" and "t". */
.brand-table code {
  overflow-wrap: break-word;
}

.brand-table .nowrap {
  white-space: nowrap;
}

/* The hex value of a colour, next to its chip, inside a table cell. */
.brand-chip {
  display: inline-block;
  width: 0.875rem;
  height: 0.875rem;
  margin-right: 0.45rem;
  vertical-align: -0.12em;
  border-radius: 1px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 30%, transparent);
}

.brand-dl {
  display: grid;
  grid-template-columns: minmax(7rem, 13rem) minmax(0, 1fr);
  max-width: 100%;
}

.brand-dl dt,
.brand-dl dd {
  padding: 0.6rem 0;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-md);
  line-height: 1.55;
}

.brand-dl dt {
  padding-right: 1.25rem;
  font-weight: 600;
  color: var(--fg);
}

.brand-dl dd {
  color: var(--fg-soft);
}

/* Numbered steps: the brand-rules list with its figure in place of the square. */
.brand-steps {
  counter-reset: brand-step;
}

.brand-steps li {
  counter-increment: brand-step;
  padding-left: 1.75rem;
}

.brand-steps li::before {
  content: counter(brand-step);
  top: 0;
  width: auto;
  height: auto;
  background: none;
  color: var(--muted-soft);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The statement panel's heading, as an h3 inside a specimen. */
.panel .brand-panel-title {
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
  max-width: 22ch;
}

/* A figure column in the advisories table, for its specimen. */
.brand-section .adv-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* A specimen sits in a hairline frame on the page's own ground, so it reads
   as an exhibit and not as part of the guide's text. */
.brand-spec {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: clamp(1rem, 2.5vw, 1.5rem);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

.brand-spec-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.brand-spec-label {
  font-size: var(--fs-sm);
  color: var(--muted);
}

.brand-spec .grid {
  width: 100%;
}

/* The newsletter's button goes full width under 480px inside its own form;
   beside other buttons in a specimen it keeps its size. */
.brand-spec-row .nl-button {
  width: auto;
}

/* The site's post components, shown as they ship: .post-body without its
   column padding. */
.post-body.brand-demo {
  padding: 0;
  max-width: 100%;
}

.post-body.brand-demo > :first-child {
  margin-top: 0;
}

.post-body.brand-demo pre {
  margin-top: 0;
}

.post-body.brand-demo .admonition {
  margin: 0;
}

.post-body.brand-demo .admonition + .admonition {
  margin-top: 0.75rem;
}

.post-body.brand-demo table {
  margin: 0;
}

/* A field and its label, outside the newsletter form that owns .nl-row. */
.brand-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 100%;
  max-width: 26rem;
}

.brand-field label {
  font-size: var(--fs-md);
  font-weight: 500;
}

/* A field's edge is a control: the waitlist's 3:1 edge (--muted-soft) on the
   card fill, as on the course page, not the hairline of the newsletter dialog. */
.brand-field .nl-input {
  width: 100%;
  flex: none;
  background: var(--bg-card);
  border-color: var(--muted-soft);
}

.brand-field .nl-input:hover,
.brand-field .nl-input:focus {
  border-color: var(--fg);
}

.brand-field-hint {
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* The course platform's notice, reproduced for its specimen: learn.css is not
   loaded on the site. Same values as .notice / .notice-error there. */
.brand-notice {
  max-width: 26rem;
  padding: 0.85rem 1.1rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  font-size: var(--fs-md);
  line-height: 1.5;
  color: var(--fg);
}

.brand-notice-error {
  border-color: var(--status-brick);
}

/* Clear space around the mark: half its height on every side. */
.brand-clearspace {
  display: inline-flex;
  padding: 48px;
  background: var(--bg-soft);
  outline: 1px solid var(--rule);
  outline-offset: -1px;
}

.brand-clearspace img {
  width: 96px;
  height: 96px;
  outline: 1px solid var(--muted-soft);
}

.brand-minsize {
  display: flex;
  align-items: flex-end;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.brand-minsize figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  align-items: flex-start;
}

.brand-minsize figcaption {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Figures documented on the page. The viewBox is drawn narrow on purpose and
   the displayed width capped, so the smallest label never renders under 14px,
   on a phone or on a wide screen. */
.brand-figure {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0;
}

.brand-figure .post-body.brand-demo .stack-diagram {
  margin: 0;
}

.brand-figure figcaption {
  max-width: var(--measure);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
}

/* Chart and arrow vocabulary of the inline-SVG figure. Same contract as
   .band, .spine, .t, .s and .n: no colour inside the SVG, every colour a token,
   so a chart follows the theme toggle the way a diagram does. */
.post-body .stack-diagram .arrow {
  fill: none;
  stroke: var(--muted-soft);
  stroke-width: 1.5;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

.post-body .stack-diagram .axis {
  stroke: var(--rule);
  stroke-width: 1;
}

.post-body .stack-diagram .ref {
  stroke: var(--fg);
  stroke-width: 1;
}

/* Halfway between ink and page: #848486 in both themes, 3.5:1 on the light
   page and 5.0:1 on the dark one, so a bar is a graphic that passes 3:1. */
.post-body .stack-diagram .bar {
  fill: color-mix(in srgb, var(--fg) 50%, var(--bg));
}

.post-body .stack-diagram .bar-key {
  fill: var(--accent);
}

.post-body .stack-diagram .line {
  fill: none;
  stroke: color-mix(in srgb, var(--fg) 50%, var(--bg));
  stroke-width: 2;
}

.post-body .stack-diagram .line-key {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
}

.post-body .stack-diagram .v {
  fill: var(--fg);
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Architecture figures: a zone groups the boxes that live together (a
   machine, a process, the kernel), a boundary is a line the data crosses
   (user and kernel, the lab and the internet). Both are structure, so they
   stay quieter than the boxes they hold. */
.post-body .stack-diagram .zone {
  fill: var(--bg-soft);
  stroke: none;
}

.post-body .stack-diagram .boundary {
  fill: none;
  stroke: var(--muted-soft);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

/* A figure drawn about 300 units wide for the phone, shown at most 4/3 of
   that on a wide screen: 17-unit titles land at 22.7px, 13.5-unit details at
   18px, never louder than a heading. */
.post-body .stack-diagram.is-narrow {
  max-width: 25rem;
}

/* On a touch screen nothing hovers: the guide's copy buttons stay visible at
   44px, and the code steps down to leave them a band of their own. */
@media (hover: none) {
  .brand-main .code-copy {
    opacity: 1;
    width: 44px;
    height: 44px;
    top: 0.25rem;
    right: 0.25rem;
  }

  .brand-main .code-block pre {
    padding-top: calc(44px + 0.75rem);
  }
}

/* Previews of the slide and document templates. */
.brand-shots {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.1rem 0.85rem;
}

.brand-shot {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0;
}

.brand-shot img {
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  border-radius: 1px;
}

.brand-shot figcaption {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
}

.brand-shot figcaption strong {
  color: var(--fg);
  font-weight: 600;
}

.brand-shot-page {
  max-width: 22rem;
}

/* A transactional mail, as clients render it. Mails are light only
   (color-scheme: light only), so the specimen stays light in either theme. */
.brand-mail {
  padding: 24px 12px;
  background: #f7f7f5;
  border: 1px solid var(--rule);
  border-radius: 1px;
  color-scheme: light;
}

.brand-mail-card {
  max-width: 560px;
  margin: 0 auto;
  padding: 32px 28px;
  background: #ffffff;
  border: 1px solid #d6d8d4;
  color: #101216;
  font-size: 16px;
  line-height: 1.6;
}

.brand-mail-card p {
  margin: 0 0 16px;
}

.brand-mail-head {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  margin-bottom: 24px !important;
}

.brand-mail-head img {
  width: 24px;
  height: 24px;
}

.brand-mail-btn {
  display: inline-block;
  margin: 8px 0 16px;
  padding: 12px 20px;
  background: #101216;
  color: #f7f7f5;
  font-weight: 600;
  line-height: 1.2;
}

.brand-mail-small {
  font-size: 14px;
  line-height: 1.5;
  color: #666c76;
  overflow-wrap: anywhere;
}

.brand-mail-foot {
  margin-top: 32px !important;
  margin-bottom: 0 !important;
}

/* The public page (/brand/) sets its colour values in Archivo, as the One
   Family rule asks of a number; the full guide keeps the mono beside its
   tokens. Figures stay tabular from .swatch-meta. */
.palette-plain .swatch-hex {
  font-family: inherit;
}

/* Each card shows its theme whatever the reader's: the dark card is already
   pinned above, and the light one is pinned here so a dark-theme reader sees
   #1B3A8A written in #1B3A8A, on white, and not in the dark accent on a dark
   card. Scoped to the public page; the guide shows its tokens live. */
.palette-plain .palette-theme:not(.palette-theme-dark) {
  background: #ffffff;
  border-color: #d6d8d4;
  color: #101216;
}

.palette-plain .palette-theme:not(.palette-theme-dark) .palette-label,
.palette-plain .palette-theme:not(.palette-theme-dark) .swatch-role {
  color: #666c76;
}

.palette-plain .palette-theme:not(.palette-theme-dark) .swatch-accent .swatch-hex {
  color: #1b3a8a;
}

.brand-files {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
}

@media (max-width: 720px) {
  .brand-shots {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  .brand-dl {
    grid-template-columns: minmax(0, 1fr);
  }

  .brand-dl dd {
    padding-top: 0;
    border-top: 0;
  }

  .brand-clearspace {
    padding: 32px;
  }

  .brand-clearspace img {
    width: 64px;
    height: 64px;
  }
}

/* One column: each card takes its own height, not the tallest one's. */
@media (max-width: 720px) {
  .brand-grid {
    grid-auto-rows: auto;
  }
}

/* The download chips follow the header's touch rule: 44px tall where there is
   no pointer to hover with. */
@media (hover: none) {
  .logo-files a,
  .type-files a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
}

/* On paper a card stays on one page, and the grounds that carry a light mark
   or a colour (the inverse preview, the chips, the dark palette) print even
   with background graphics off, or the inverse mark prints white on white. */
@media print {
  .logo-card,
  .palette-theme,
  .press-quote,
  .type-specimen {
    break-inside: avoid;
  }

  .logo-preview,
  .swatch-chip,
  .palette-theme-dark,
  .brand-rules li::before {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}

@media (max-width: 720px) {
  .brand-grid,
  .palette-themes {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  /* align cards with the hero's left edge */
  .blog-card {
    padding: 1.3rem 0;
  }
  .blog-card-head {
    flex-wrap: wrap;
    gap: 0.25rem 0.8rem;
  }

  /* An atomic inline can start a line, so on a narrow column the arrow drops
     alone under a wrapped headline. Hidden here, as the research rows already
     do at this width. */
  .hero-proof-arrow {
    display: none;
  }
}

@media (hover: none) {
  .nav-links a,
  .nav-contact {
    padding-block: 0.7rem; /* 44px tall touch target */
  }
}

@media (max-width: 480px) {
  .brand-name {
    display: none;
  }

  .nav {
    gap: 0.7rem;
  }

  .nav-links {
    gap: 0.9rem;
  }
}

/* Measured 2026-11-13 with six links (Training added), in Chromium with the browser's default text size set to
   16, 20 and 24px: below 803px at 16px (824px on a touch screen, where the buttons are 44px) the full bar no
   longer fits, the brand name wraps and the search button is squeezed, then falls under a 24px target at 749px
   (760px on touch) and the page scrolls sideways at 711px (722px). At 20px the same point is 984px (1006px on
   touch), 49.2em and 50.3em. So the links move behind a hamburger from 52em down, 832px at the default size, in
   em so the threshold grows with the reader's text. The nav script reads the same query. At 24px the full bar
   never fits the 1120px column, whatever the width: above 52em (1248px) it shows with the brand name wrapped.
   Contact stays in the bar: it is the site's one action and should never be a tap away. */
@media (max-width: 52em) {
  html.js-nav .nav-toggle {
    display: inline-flex;
  }

  html.js-nav .nav-contact {
    margin-left: auto; /* group CTA, hamburger and theme toggle on the right */
  }

  html.js-nav .nav-links {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 120; /* above content and .reading-progress, below the skip link */
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin: 0;
    padding: 0 var(--pad-x);
    font-size: var(--fs-lg);
    background: var(--bg-card);
    /* --bg-card sits only a shade off the page, so the sheet is delimited by
       rules rather than by its fill: a hairline splitting it from the bar, and
       the strong rule closing it against the content it covers. */
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule-strong);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity 180ms ease, transform 200ms ease, visibility 0s linear 200ms;
  }

  html.js-nav .nav.is-open .nav-links {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 180ms ease, transform 200ms ease, visibility 0s;
  }

  /* Ruled rows, like the research list: the hairline is the row, and hover or
     the current page sharpens it to ink through the base .nav-links a rule. */
  html.js-nav .nav-links a {
    padding: 0.95rem 0;
    color: var(--fg);
    border-bottom: 1px solid var(--rule);
  }

  html.js-nav .nav-links a:last-child {
    border-bottom: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* the blanket rule zeroes durations but not delays; and a visibility still in transition refused the focus
     the menu moves to its first link, so the panel switches with no transition at all */
  html.js-nav .nav-links,
  html.js-nav .nav.is-open .nav-links {
    transition: none !important;
  }
}

/* No JS means the panel can never open, so the links stay in the bar and drop
   to a second tier instead. */
@media (max-width: 400px) {
  html:not(.js-nav) .nav {
    flex-wrap: wrap;
    row-gap: 0;
  }

  html:not(.js-nav) .nav-links {
    order: 3;
    flex-basis: 100%;
    flex-wrap: wrap; /* six links do not fit one row under 400px: the last ones take a second row */
    margin-left: 0;
    justify-content: space-between;
    gap: 0.5rem;
    padding-top: 0.55rem;
    margin-top: 0.35rem;
    border-top: 1px solid var(--rule);
  }

  html:not(.js-nav) .nav-contact {
    margin-left: auto;
  }

  /* the CTA plus the 44px copy target does not fit a 320px column at full size */
  .contact-cta {
    padding: 0.55rem 0.85rem;
    gap: 0.5rem;
  }
}

/* ---------- Dark mode ---------- */
html.dark {
  --bg: #101216;
  --bg-soft: #181b20;
  --bg-card: #1e222a;
  --fg: #f7f7f5;
  --fg-soft: #d9dce1;
  --muted: #9aa1ac;
  --muted-soft: #8b939f;
  --muted-inv: #5c6470;
  --rule: #2d323a;
  --rule-strong: #3a414b;
  --accent: #8aa9f0;
  --accent-inv: #1b3a8a;
  --chip-fg: #949ba6;

  --status-slate: #929ca8;
  --status-moss: #7fb088;
  --status-amber: #d6a44a;
  --status-orange: #d97c4f; /* oklch(68% 0.13 46): the light orange's hue, 6.2:1 on the dark page */
  --status-brick: #dc6a52;

  color-scheme: dark;
}

html.dark .panel {
  background: var(--fg);
  color: var(--bg);
  border: none;
}

html.dark .panel .kicker-light {
  color: var(--muted-inv);
}

html.dark .footer {
  background: var(--bg-soft);
  color: var(--fg);
  border-top: 1px solid var(--rule);
}

html.dark .footer-name {
  color: var(--fg);
}

html.dark .footer-meta,
html.dark .footer-nav {
  color: var(--muted);
}

html.dark .footer-nav a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

html.dark .footer-bottom {
  color: var(--muted-soft);
  border-top: 1px solid var(--rule);
}

html.dark .footer ::selection,
html.dark .post-body pre ::selection,
html.dark .paper-bibtex ::selection {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--fg);
}

html.dark .post-body pre,
html.dark .paper-bibtex {
  background: #0b0d10;
  color: #d9dce1;
  border: 1px solid var(--rule);
}

html.dark .tile:hover,
html.dark .blog-card:hover {
  box-shadow: 0 12px 28px -22px rgba(14, 13, 10, 0.65);
}

html.dark .team-photo img {
  filter: grayscale(0.28) saturate(0.88) brightness(0.95);
}

html.dark .team-member:hover .team-photo img {
  filter: brightness(0.98);
}

html.dark .team-member:hover .team-photo {
  box-shadow: 0 0 0 4px var(--bg), 0 0 0 5px var(--rule-strong);
}

html.dark .team-link {
  background: var(--bg-soft);
}

html.dark .team-link:hover {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}

/* The light variant shows unless the page is dark. The dark one hides in light,
   chosen or from the system, but not on html:not(.dark) alone: without JS there
   is no class at all, the fallback under prefers-color-scheme then hides the
   light variant, and hiding this one too left the header with no mark. */
html.light .brand-logo-dark {
  display: none;
}

@media not all and (prefers-color-scheme: dark) {
  html:not(.dark) .brand-logo-dark {
    display: none;
  }
}

html.dark .brand-logo-light {
  display: none;
}

html.dark .prev-logos img {
  filter: invert(1);
  opacity: 0.68;
}

html.dark .prev-logos li:hover img {
  opacity: 0.92;
}

/* ---------- Theme toggle ---------- */
.theme-toggle {
  width: var(--nav-btn);
  height: var(--nav-btn);
  flex: 0 0 var(--nav-btn);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rule);
  border-radius: 1px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  padding: 0;
  font: inherit;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease;
}

.theme-toggle:hover {
  color: var(--fg);
  border-color: var(--fg);
  background: var(--bg-card);
}

.theme-toggle svg {
  width: 16px;
  height: 16px;
  display: block;
}

.theme-toggle .theme-icon {
  display: none;
}

.theme-toggle[data-theme="auto"] .theme-icon-auto,
.theme-toggle[data-theme="light"] .theme-icon-light,
.theme-toggle[data-theme="dark"] .theme-icon-dark {
  display: block;
}

/* Touch sizing must come after the base rules above — same specificity, so
   source order decides. Keeping it earlier silently discarded it. */
@media (hover: none) {
  /* Le jeton monte a la cible de doigt, donc les TROIS boutons de la nav suivent.
     Cette regle ne portait que sur la bascule de theme : sur un telephone elle
     faisait 44 px a cote d'une loupe et d'un hamburger a 34, et c'est ce
     desaccord qui se voyait. */
  :root {
    --nav-btn: 44px;
  }

  .contact-copy {
    width: 44px;
    height: 44px;
  }
}

/* ---------- Responsive ---------- */
@media (max-width: 820px) {
  .hero {
    grid-template-columns: 1fr;
  }

  .hero-schema {
    order: 2;
    width: 100%;
    max-width: 420px;
    margin-inline: auto;
  }
}

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

  .nav {
    gap: 0.85rem;
  }

  .research-list a {
    flex-wrap: wrap;
    row-gap: 0.15rem;
  }

  .research-date {
    flex-basis: 100%;
  }

  .research-foot {
    flex-direction: column;
    align-items: flex-start;
  }

  .footer-inner {
    flex-direction: column;
    align-items: center;
    gap: 2rem;
  }

  .footer-brand {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.75rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------- Service pages ---------- */
.service-main {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

.service-hero {
  padding: clamp(2rem, 4vw, 3.25rem) 0 clamp(1.5rem, 3vw, 2.25rem);
  border-bottom: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.9rem;
}

/* Le filet de cloture appartient au CADRE, les textes a la colonne : deux bords,
   et deux intentions lisibles. Il y en avait quatre — le titre la ou ses mots
   s'arretaient, le chapeau a 682, le filet de la note a 778, la cloture a 960.
   Un cap sur le hero lui-meme a ete essaye et rejete : le filet de cloture
   s'arretait alors avant la grille de cartes en dessous et ne separait plus rien. */
.service-hero > p,
.service-hero > h1 {
  max-width: var(--measure);
}

.service-back {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: var(--fs-sm);
  color: var(--muted);
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 160ms ease, border-color 160ms ease;
}

.service-back:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

.service-back-arrow {
  transition: transform 200ms ease;
}

.service-back:hover .service-back-arrow {
  transform: translateX(-3px);
}

.service-hero .kicker {
  color: var(--muted-soft);
}

.service-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
}

.service-lede {
  font-size: var(--fs-lede);
  color: var(--fg-soft);
  line-height: 1.6;
}

/* Operational reassurance: the crisis reader's first questions (availability,
   admissibility, confidence), ruled off from the lede like a spec line. */
.service-reassure {
  margin-top: 0.2rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-md);
  color: var(--muted-soft);
  line-height: 1.55;
}

.service-section {
  padding: clamp(2.5rem, 4vw, 3.5rem) 0;
  border-bottom: 1px solid var(--rule);
}

.service-section-end {
  border-bottom: 0;
  padding-bottom: clamp(3rem, 5vw, 4.5rem);
}

/* Capability pillars */
.service-pillars {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
}

.pillar {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: clamp(1.4rem, 2.5vw, 1.9rem);
}

.pillar h3 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.02em;
}

.pillar > p {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.6;
}

.pillar-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.15rem;
}

.pillar-list li {
  position: relative;
  padding-left: 1.15rem;
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.5;
}

.pillar-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 5px;
  height: 5px;
  border-radius: 1px;
  background: var(--muted-soft);
}

.pillar-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.4rem;
}

.pillar-tag {
  font-size: var(--fs-2xs);
  letter-spacing: 0.03em;
  padding: 0.28rem 0.6rem;
  border: 1px solid var(--rule);
  border-radius: 1px;
  color: var(--muted);
  background: var(--bg);
  font-variant-numeric: tabular-nums;
}

/* Deliverable list inside the dark panel */
.panel-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.55rem 1.6rem;
}

.panel-list li {
  position: relative;
  padding-left: 1.35rem;
  font-size: var(--fs-md);
  color: color-mix(in srgb, currentColor 82%, transparent);
  line-height: 1.5;
}

.panel-list li::before {
  content: "\2192";
  content: "\2192" / "";  /* decorative: no "right arrow" read before each item */
  position: absolute;
  left: 0;
  top: 0;
  color: var(--muted-inv);
}

/* Scope note */
/* The measure cap belongs on the text, not on the frame: capping the box left
   it ~100px short of every other block on the page, which read as a misalign
   rather than as a decision. */
.service-note {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 1.15rem 1.3rem;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

.service-note-title {
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  color: var(--accent);
  font-weight: 600;
}

.service-note p {
  max-width: 74ch;
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.6;
}

.service-note a {
  color: var(--fg);
  border-bottom: 1px solid var(--muted-soft);
  transition: border-color 160ms ease;
}

.service-note a:hover {
  border-bottom-color: var(--fg);
}

.service-note strong {
  color: var(--fg);
  font-weight: 600;
}

/* How a service runs (2026-11-13): the steps are a figure, drawn with the brand guide's diagram method, where
   they used to be a table. Heading, scope note, then the figure across the column (the owner's call: the
   figure beside the note left an empty column under it). Two drawings of the same steps: a landscape one from
   1024px, where the section is 880px wide and its 820-unit viewBox sets the details at 14.5px, and the
   guide's vertical one below, where a landscape figure would shrink its text under the floor. The one not shown
   is display: none, so a screen reader meets one figure. */
.service-process {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  row-gap: 1.5rem;
}

.service-process > .section-head {
  margin-bottom: 0;
}

/* The figure borrows .post-body for the diagram classes only: none of the
   prose padding comes with it. */
.service-figure.post-body {
  margin: 0;
  padding: 0;
}

.service-figure .stack-diagram {
  margin: 0;
}

.service-figure.post-body .stack-diagram.is-narrow {
  max-width: 22rem;
}

.service-figure .stack-diagram.is-wide {
  display: none;
}

@media (min-width: 1024px) {
  .service-figure .stack-diagram.is-wide {
    display: block;
  }
  .service-figure .stack-diagram.is-narrow {
    display: none;
  }
}

/* Public proof: one to three rows, each a public piece of work and what it
   shows, in the hairline-row grammar of the home page's research list. */
.service-proof {
  border-top: 1px solid var(--rule);
}

.service-proof li {
  border-bottom: 1px solid var(--rule);
}

.service-proof a {
  display: flex;
  align-items: baseline;
  gap: 1.25rem;
  padding: 0.95rem 0.2rem;
  transition: background 160ms ease;
}

.service-proof a:hover {
  background: var(--bg-soft);
}

.service-proof-kind {
  flex: 0 0 6.5rem;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.service-proof-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.service-proof-title {
  font-size: var(--fs-md);
  font-weight: 500;
  letter-spacing: -0.005em;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease;
}

.service-proof a:hover .service-proof-title {
  text-decoration-color: var(--fg);
}

.service-proof-what {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.55;
}

@media (max-width: 560px) {
  .service-proof a {
    flex-direction: column;
    gap: 0.3rem;
  }

  .service-proof-kind {
    flex-basis: auto;
  }
}

/* Linkable service tile on the home grid */
a.tile {
  color: inherit;
}

.tile-more {
  margin-top: auto;
  padding-top: 0.5rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--muted);
  transition: color 160ms ease;
}

a.tile:hover .tile-more,
.training-offers .tile:hover .tile-more {
  color: var(--fg);
}

.tile-arrow {
  transition: transform 200ms ease;
}

a.tile:hover .tile-arrow,
.training-offers .tile:hover .tile-arrow {
  transform: translateX(3px);
}

/* The home page's Training tile holds two links: its heading's, stretched over
   the whole tile as the other tiles are links, and the online course's, in the
   sentence and above the stretched one, so the product is one click away. */
.tile-training .tile-link,
.training-offers .tile-link {
  color: inherit;
}

/* The /training/ hub's tiles the same way: their title's link covers the tile,
   so the link is named by the title alone (training/index.html). */
.tile-training .tile-link::after,
.training-offers .tile-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.tile-training .tile-link:focus-visible,
.training-offers .tile-link:focus-visible {
  outline: none;
}

.tile-training .tile-link:focus-visible::after,
.training-offers .tile-link:focus-visible::after {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: 1px;
}

.tile-training .tile-inline {
  position: relative;
  z-index: 1;
  color: var(--fg);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.tile-training .tile-inline:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* Training course */
/* A list, so a screen reader announces four figures rather than one run of
   text. It runs to the frame like the promise band under it: the hero keeps two
   right edges, the text measure and the frame, instead of three. */
.course-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: 1fr;   /* one height for the row of four; two by two on a phone, see below */
  gap: 0.85rem;
  list-style: none;
  margin: 0 0 0.4rem;
  padding: 0;
  align-self: stretch;   /* same width as the promises under it */
}

.course-hero-cta + .course-facts {
  margin-top: 0.85rem;
}

.course-fact {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1.2rem 1.3rem;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

.course-fact-value {
  font-size: var(--fs-heading);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.025em;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.course-fact-label {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-weight: 500;
  overflow-wrap: anywhere;
}

/* "mini-labs" held on one line: Archivo has no non-breaking hyphen, and the
   hub's card broke it after "15 mini-". */
.course-nowrap {
  white-space: nowrap;
}

/* What the purchase keeps giving: four promises under the figures, in a hairline-ruled band */
.course-promise {
  list-style: none;
  margin: 0 0 clamp(1.5rem, 2.5vw, 2.25rem);
  padding: 1.1rem 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem 1.75rem;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  align-self: stretch;
}

/* Last in the hero, the band's own bottom rule would sit a few pixels above
   the hero's closing rule: the closing rule does the job. */
.course-promise:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}

.course-promise li {
  font-size: var(--fs-md);
  line-height: 1.5;
  color: var(--muted);
}

.course-promise strong {
  display: block;
  color: var(--fg);
  font-weight: 600;
  margin-bottom: 0.2rem;
}

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

@media (max-width: 720px) {
  .course-promise { grid-template-columns: 1fr; }
}

/* Online course: the curriculum, one <details> per section, rendered by build.py.
   A ruled row list, like the platform's own course page and the site's row
   lists: rows divided by 1px rules, a hover fill, and one disclosure mark (the
   platform's hairline chevron) shared with the FAQ below. 19 bordered cards
   read as a static table and hid that every row opens. */
.curriculum {
  border-top: 1px solid var(--rule);
}

/* "Open all sections" (the course page's script injects it above the list): a
   text button at the list's right edge, drawn as the page's links are. */
.curriculum-toggle {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 0.5rem;
}

.curriculum-all {
  -webkit-appearance: none;
  appearance: none;
  padding: 0.35rem 0;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--fg);
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration-line: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.curriculum-all:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

.curriculum-section {
  border-bottom: 1px solid var(--rule);
}

.curriculum-section > summary,
.faq-item > summary {
  cursor: pointer;
  list-style: none;
  transition: background 160ms ease;
}

.curriculum-section > summary::-webkit-details-marker,
.faq-item p {
  overflow-wrap: anywhere;
}

.faq-item > summary::-webkit-details-marker {
  display: none;
}

/* Only where there is a hover: on a touch screen the tapped row kept the fill
   and the dark chevron after it was closed, and read as still selected. */
@media (hover: hover) {
  .curriculum-section > summary:hover,
  .faq-item > summary:hover {
    background: var(--bg-soft);
  }
}

.curriculum-section > summary::after,
.faq-item > summary::after {
  content: "";
  flex: 0 0 auto;
  justify-self: end;
  align-self: center;
  width: 0.5rem;
  height: 0.5rem;
  border: solid var(--muted);
  border-width: 0 1px 1px 0;
  transform: translateY(-25%) rotate(45deg);
  transition: transform 160ms ease, border-color 160ms ease;
}

.curriculum-section[open] > summary::after,
.faq-item[open] > summary::after {
  transform: translateY(25%) rotate(-135deg);
}

@media (hover: hover) {
  .curriculum-section > summary:hover::after,
  .faq-item > summary:hover::after {
    border-color: var(--fg);
  }
}

.curriculum-section > summary {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr) auto 0.5rem;
  gap: 0.75rem;
  align-items: baseline;
  padding: 0.95rem 0.6rem;
}

.curriculum-sec-num,
.curriculum-num {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-size: var(--fs-sm);
}

.curriculum-sec-title {
  font-weight: 600;
  color: var(--fg);
}

.curriculum-sec-meta,
.curriculum-time {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.curriculum-free {
  margin-left: 0.5rem;
  padding: 0.05rem 0.45rem;
  border-radius: var(--r-md);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--accent);
  border: 1px solid currentColor;
}

/* The final challenge's walkthroughs: listed, and said to wait for the report,
   on a quiet line under the title. A grey chip beside the title wrapped inside
   itself into two-line blocks on phones. A sheet row says on that same line
   which lesson's page holds it (.curriculum-where), a section's quiz what it
   draws and asks (.curriculum-pass). */
.curriculum-held,
.curriculum-where,
.curriculum-pass {
  grid-column: 2;
  grid-row: 2;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.curriculum-lesson > .curriculum-time {
  grid-row: 1;
  grid-column: 3;
}

/* Lessons sit under their section's title, so the section rules run the full
   width and the lesson rules start at the title: the nesting reads at a glance. */
.curriculum-lessons {
  list-style: none;
  margin: 0 0 0 3.6rem;
  padding: 0 0.6rem 0.7rem 0;
}

.curriculum-lesson {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr) auto;
  gap: 0.1rem 0.75rem;   /* the row gap only parts a title from its note (.curriculum-held) */
  align-items: baseline;
  padding: 0.5rem 0;
  font-size: var(--fs-md);
  color: var(--fg-soft);
  border-top: 1px solid var(--rule);
}

/* An exercise with no video of its own, under the lesson it goes with:
   unnumbered, and "mini-lab" or "lab" where a lesson shows its length; a
   section's quiz the same, last in its section, marked "quiz". */
.curriculum-sheet .curriculum-time,
.curriculum-quiz .curriculum-time {
  font-variant-numeric: normal;
}

.curriculum-free-cta {
  margin: 0 0.6rem 0 3.6rem;  /* its rule ends where the lesson rules do */
  padding: 0.6rem 0 0.95rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.curriculum-asof {
  margin-top: 0.85rem;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.course-waitlist .nl-form {
  margin-top: 0.75rem;
}

.course-waitlist .nl-form[hidden] {
  display: none;
}

.course-waitlist {
  display: grid;
  gap: 1rem;
}

/* The field sat on the grey pricing box at 1.24:1: give it the white card fill
   and a border that reaches 3:1 (--muted-soft, 4.6:1 on --bg-soft). */
.course-waitlist .nl-input {
  background: var(--bg-card);
  border-color: var(--muted-soft);
}

.course-waitlist .nl-input:hover,
.course-waitlist .nl-input:focus {
  border-color: var(--fg);
}

/* An address refused for itself (empty, malformed, refused by the server) is
   drawn as one: the field's border and the message in brick, 5.5:1 on the
   pricing box (5.2:1 in dark). newsletter.js sets aria-invalid only for those;
   a failure on our side (server, rate limit, network, timeout) colours the
   message and leaves a correct address looking correct. */
.course-waitlist .nl-input[aria-invalid="true"],
.course-waitlist .nl-input[aria-invalid="true"]:hover,
.course-waitlist .nl-input[aria-invalid="true"]:focus {
  border-color: var(--status-brick);
}

.course-waitlist .nl-status.is-error {
  color: var(--status-brick);
  font-weight: 500;
}

/* The same action as the hero's button, so the same solid button. */
.course-waitlist .nl-button {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.course-waitlist .nl-button:hover {
  color: var(--bg);
  background: var(--fg-soft);
  border-color: var(--fg-soft);
}

.course-waitlist .nl-success-text + .nl-success-text {
  margin-top: 0.4rem;
}

/* The confirmation names the address in bold. An address has no space and no
   break opportunity at '.' or '@', so an ordinary 35-character one widened the
   box, then the page, past a phone's edge: the text column may shrink
   (min-width: 0; Safari still counts a whole word in a column's minimum width,
   the same lesson as the paper's digest). newsletter.js writes the address in
   two halves, the name with its @ and the domain, each one holding together
   (an inline box no wider than the line): it breaks at the @, never mid-word as
   break-all on the whole address did ("student@exa" / "mple.com"). Only a half
   longer than a whole line breaks inside itself. */
.course-waitlist .nl-success > div {
  min-width: 0;
}

.course-waitlist .nl-address > span {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* The confirmation takes the focus once the form is gone (newsletter.js): its
   ring stands clear of the check box and of the lines' ends, where the site's
   3 px offset ran flush along them inside the price box. */
.course-waitlist .nl-success:focus-visible {
  outline-offset: 6px;
  border-radius: 0;   /* the 1 px corner grew with the offset into a rounded ring: square, as every box here */
}

/* The price stays put while the form beside it grows an error line or turns
   into the confirmation. */
.pricing.course-pricing {
  align-items: start;
}

/* One column on a phone: minmax(0, 1fr), not the shared 1fr, whose minimum is
   the content's own, so nothing in the box can push it past the screen. */
@media (max-width: 720px) {
  .pricing.course-pricing {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The course page's four boxed blocks start their text at one inset (the
   pricing box's), where they started at 22, 36, 37 and 44 px. The panel has no
   border, so it gets the border's pixel back in its padding. */
.course-main .service-note,
.course-main .lab-callout,
.course-main .pricing {
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.course-main .panel {
  padding: calc(clamp(1.5rem, 3vw, 2.25rem) + 1px);
}

/* "What you will be able to do": the paragraph runs as wide as the two-column list under it. Held to the
   measure, it broke well before the panel's edge and read as cut short (the owner, 2026-11-13). */
.course-main .panel p {
  max-width: none;
}

/* The lab callout's list is taller than its heading and paragraph: centred,
   the heading started some 95 px below the list beside it. One top line. */
.course-main .lab-callout {
  align-items: start;
}

/* Open sale: the buy button where the waitlist form stood. */
.course-buy {
  margin-top: 0.25rem;
}

.pricing-terms {
  margin-top: 0.35rem;
  font-size: var(--fs-sm);
}

.course-hero-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  margin-top: 0.25rem;
}

.course-cta {
  display: inline-flex;
  padding: calc(0.65rem - 1px) calc(1.2rem - 1px);
  border: 1px solid transparent;  /* drawn in forced colours, where the background is not */
  border-radius: var(--r-md);
  background: var(--fg);
  color: var(--bg);
  font-weight: 600;
  text-decoration: none;
}

.course-cta:hover,
.course-cta:focus-visible {
  background: var(--fg-soft);
  color: var(--bg);
}

/* The closing block is no flex column: the line under its buttons needs its own space. */
.course-closing .course-hero-cta + .course-hero-note {
  margin-top: 0.75rem;
}

/* The second action beside a solid one (section 00 free, or the waitlist in
   the closing block): the same box, outlined. Its border is --muted-soft, 3:1
   on the page, where a --rule hairline read as no edge at all. It inverts on
   hover only where there is a hover: a tap on a phone left it painted solid. */
.course-cta-alt,
.course-cta-alt:hover,
.course-cta-alt:focus-visible {
  background: var(--bg-card);
  color: var(--fg);
  border-color: var(--muted-soft);
}

@media (hover: hover) {
  .course-cta-alt:hover {
    background: var(--fg);
    color: var(--bg);
    border-color: var(--fg);
  }
}

/* Phones: each action is a full-width button, one above the other, as the
   waitlist's button is under its field. "Buy the course" stood at its own
   width in the price box, weaker than the button it replaces. */
@media (max-width: 560px) {
  .course-hero-cta {
    flex-direction: column;
    align-items: stretch;
    align-self: stretch;  /* the hero is a column of start-aligned items */
  }
  .course-hero-cta .course-cta,
  .course-buy .course-cta {
    display: flex;
    justify-content: center;
  }
}

.course-hero-note {
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* Links inside the course page's prose: links are colour-inherit site-wide, so
   without this they looked exactly like the words around them. Ink, an
   underline, thicker on hover. The underline is --muted-soft, not --rule: a
   --rule hairline measured 1.2 to 1.4:1 and read as no underline at all; this
   one is 4.6:1 on the pricing box and 6:1 on the dark page. */
.course-hero-note a,
.faq-item > p a,
.course-guarantee a,
.course-proof a,
.pricing-terms a,
.curriculum-free-cta a,
.course-waitlist .nl-success-text a {
  color: var(--fg);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.course-hero-note a:hover,
.faq-item > p a:hover,
.course-guarantee a:hover,
.course-proof a:hover,
.pricing-terms a:hover,
.curriculum-free-cta a:hover,
.course-waitlist .nl-success-text a:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* "More about him" uses the site's contact-note links, a --rule border: the
   same 3:1 line here, on the course page only. */
.course-main .contact-note a:not([class]) {
  border-bottom-color: var(--muted-soft);
}

.course-main .contact-note a:not([class]):hover {
  border-bottom-color: var(--fg);
}

/* The waitlist's fine print ("Privacy"): the shared .nl-fine link is the
   sentence's own grey with a --rule border, 1.2:1 on the pricing box, so the
   link could not be told apart. The same 3:1 line as above. */
.course-main .nl-fine a {
  border-bottom-color: var(--muted-soft);
}

.course-main .nl-fine a:hover {
  border-bottom-color: var(--fg);
}

/* The guarantee is a reason to buy, set next to the price: one line in ink,
   held to the reading measure, and what its conditions count in a disclosure
   under it. All of it in grey read as small print where the buyer decides. */
.course-guarantee {
  margin-top: 0.85rem;
  max-width: var(--measure);
  font-size: var(--fs-md);
  line-height: 1.55;
  color: var(--muted);
}

.course-guarantee-line {
  margin: 0;
  color: var(--fg);
}

.course-guarantee strong {
  color: var(--fg);
  font-weight: 600;
}

.course-guarantee-more {
  margin-top: 0.15rem;
}

/* The FAQ's disclosure mark, at the size of the line it closes. */
.course-guarantee-more > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  list-style: none;
  padding: 0.5rem 0;   /* a tap target a thumb finds, not a 21 px line */
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--fg-soft);
}

.course-guarantee-more > summary::-webkit-details-marker {
  display: none;
}

.course-guarantee-more > summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border: solid var(--muted);
  border-width: 0 1px 1px 0;
  transform: translateY(-25%) rotate(45deg);
  transition: transform 160ms ease, border-color 160ms ease;
}

.course-guarantee-more[open] > summary::after {
  transform: translateY(25%) rotate(-135deg);
}

@media (hover: hover) {
  .course-guarantee-more > summary:hover {
    color: var(--fg);
  }
  .course-guarantee-more > summary:hover::after {
    border-color: var(--fg);
  }
}

.course-guarantee-more > p {
  margin: 0.15rem 0 0;
}

@media (prefers-reduced-motion: reduce) {
  .course-guarantee-more > summary::after {
    transition: none;
  }
}

/* Who teaches it: the illustrated portrait /team/ publishes, beside the head.
   A cut-out with no frame, as on /team/ and /cv/. */
/* the certificate's specimen, in its own section of the course page: the document itself, on a hairline */
.course-cert {
  margin: 0.75rem 0 0;
  max-width: 46rem;
}

.course-cert img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}

.course-cert figcaption {
  margin-top: 0.75rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: color-mix(in srgb, currentColor 78%, transparent);
}

/* links inherit their colour site-wide: this one needs its underline to be seen */
.course-cert figcaption a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

/* The course's mark: MZ, the two bytes every Windows program starts with, the course's subject. Ink on
   paper by inversion, square, in the site's type; the bytes in hex under it, in mono since they are data.
   Shared by the site and the platform (learn serves this stylesheet). */
.course-mark {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 4.5rem;
  aspect-ratio: 1;
  background: var(--fg);
  color: var(--bg);
  border-radius: var(--r-md);
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
}

.course-mark-hex {
  margin-top: 0.4rem;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--muted-inv);
}

.course-mark-lg {
  width: 6.5rem;
  font-size: var(--fs-heading);
}

/* What the course covers, as a row of labels (build.py COURSE_TOPICS): the sales page's hero, the home page's
   course card, the /training/ card. Outlined like a quiet tile, never filled: they label, they do not link. */
.course-topics {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin: 0;
  padding: 0;
}

.course-topics li {
  padding: 0.3rem 0.7rem;
  font-size: var(--fs-sm);
  font-weight: 500;
  line-height: 1.3;
  color: var(--fg-soft);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

/* The course page's title beside its mark. */
.course-hero-head {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 1.5rem);
}

@media (max-width: 560px) {
  .course-hero-head {
    flex-direction: column;
    align-items: flex-start;
  }
  .course-hero-head .course-mark {
    width: 3.75rem;
    font-size: var(--fs-xl);
  }
}

/* Frames from the lessons (build:course-stills): the first wide, the others two by two. The slides are light
   in both themes, so each sits on a hairline, like the certificate. */
.course-stills {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 1.75rem) clamp(1rem, 2vw, 1.5rem);
  margin-top: 1.5rem;
}

.course-still {
  margin: 0;
}

.course-still-lead {
  grid-column: 1 / -1;
}

.course-still img {
  transition: border-color 160ms ease;
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}

.course-still figcaption {
  margin-top: 0.55rem;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--muted);
}

.course-still-num {
  margin-right: 0.3em;
  color: var(--fg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
  .course-stills {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The path to the certificate (build:course-path): one row per stage on a spine, each stage's length as a bar
   against the longest, then the report and the certificate, the conclusion in ink. Bars take the chart grey of
   the brand guide (halfway between ink and page, 3:1 in both themes). */
.course-path {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.path-stage {
  position: relative;
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr) minmax(9rem, 16rem);
  align-items: center;
  gap: 0.35rem 1.5rem;
  padding: 0.9rem 1rem 0.9rem 2rem;
  border-bottom: 1px solid var(--rule);
}

.path-stage::before {
  content: "";
  position: absolute;
  left: 0.55rem;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--rule);
}

.path-stage:first-child::before {
  top: 50%;
}

.path-stage:last-child::before {
  bottom: 50%;
}

.path-stage::after {
  content: "";
  position: absolute;
  left: calc(0.55rem - 0.3rem);
  top: 50%;
  width: 0.6rem;
  height: 0.6rem;
  margin-top: -0.3rem;
  background: var(--bg);
  border: 1px solid var(--fg);
}

.path-secs {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.path-what {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.path-name {
  font-weight: 600;
  color: var(--fg);
}

.path-titles {
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--muted);
}

.path-time {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.path-bar {
  display: block;
  height: 0.5rem;
  overflow: hidden;
  background: var(--bg-soft);
}

.path-bar > span {
  display: block;
  height: 100%;
  background: color-mix(in srgb, var(--fg) 50%, var(--bg));
}

.path-dur {
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.path-end {
  --focus-ring: var(--bg);
  background: var(--fg);
  color: var(--bg);
  border-bottom-color: var(--fg);
}

.path-end::before {
  background: var(--muted-inv);
}

.path-end::after {
  background: var(--bg);
  border-color: var(--bg);
}

.path-end .path-name {
  color: var(--bg);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

.path-end .path-secs,
.path-end .path-titles {
  color: var(--muted-inv);
}

@media (max-width: 720px) {
  .path-stage {
    grid-template-columns: minmax(0, 1fr);
    padding-left: 1.85rem;
  }
  /* stacked, each node faces its stage's first line rather than the middle of a tall row */
  .path-stage::after {
    top: 1.5rem;
  }
  .path-stage:first-child::before {
    top: 1.5rem;
  }
  .path-stage:last-child::before {
    bottom: calc(100% - 1.5rem);
  }
}

/* The certificate's section: the words beside the specimen, which takes the larger share. */
.course-cert-band {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3rem);
}

.course-cert-text {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}

.course-cert-text h2 {
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
  text-wrap: balance;
}

.course-cert-text p {
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--muted);
}

.course-cert-band .course-cert {
  margin: 0;
  max-width: none;
}

@media (max-width: 860px) {
  .course-cert-band {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* What a student needs, in three short columns ruled like the hero's promises. */
.course-needs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 1.5rem 0 0;
  border-top: 1px solid var(--rule);
}

.course-need {
  padding: 1rem 1.5rem 1.1rem 0;
}

.course-need + .course-need {
  padding-left: 1.5rem;
  border-left: 1px solid var(--rule);
}

.course-need dt {
  margin-bottom: 0.3rem;
  font-weight: 600;
  color: var(--fg);
}

.course-need dd {
  margin: 0;
  line-height: 1.55;
  color: var(--muted);
}

@media (max-width: 720px) {
  .course-needs {
    grid-template-columns: minmax(0, 1fr);
  }
  .course-need,
  .course-need + .course-need {
    padding: 0.9rem 0;
    border-left: 0;
  }
  .course-need + .course-need {
    border-top: 1px solid var(--rule);
  }
}

/* /training/: the one course as a large tile, its mark, name, topics and figures. */
.training-offers-one {
  grid-template-columns: minmax(0, 1fr);
}

.training-course-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.8rem;
  max-width: 42rem;
}

.training-offers .training-course h2 {
  font-size: var(--fs-2xl);
  letter-spacing: -0.02em;
  line-height: 1.2;
}

@media (max-width: 560px) {
  .training-course .course-mark-lg {
    width: 4.5rem;
    font-size: var(--fs-2xl);
  }
}

/* A still links to itself at full size; the frame shows it is a link on hover. */
.course-still-link {
  display: block;
}

.course-still-link:hover img {
  border-color: var(--fg);
}

.course-teacher {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2rem);
  align-items: start;
}

.course-teacher-photo {
  display: block;
  width: clamp(96px, 14vw, 132px);
  height: auto;
}

@media (max-width: 560px) {
  .course-teacher {
    grid-template-columns: 1fr;
    gap: 0.75rem;
  }
}

/* The head, the record and "More about him" share the column beside the photo:
   one left edge, where the record and its links started flush under the photo.
   The record follows its lede closely, as the rest of the same paragraph. */
.course-teacher-text > .section-head {
  margin-bottom: 1.25rem;
}

/* The instructor's record, as plain facts under the section head. */
.course-proof {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  max-width: var(--measure);
}

.course-proof li {
  font-size: var(--fs-md);
  line-height: 1.55;
  color: var(--fg-soft);
}

.faq {
  border-top: 1px solid var(--rule);
}

.faq-item {
  border-bottom: 1px solid var(--rule);
}

.faq-item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.95rem 0.6rem;
  font-weight: 600;
}

.faq-item > p {
  margin: 0;
  padding: 0 0.6rem 1.1rem;
  max-width: var(--measure);
  color: var(--fg-soft);
}

@media (prefers-reduced-motion: reduce) {
  .curriculum-section > summary::after,
  .faq-item > summary::after {
    transition: none;
  }
}

/* Phones: the section's length goes under its title, but a lesson's length
   (five characters at most) stays on the lesson's line. */
@media (max-width: 560px) {
  .curriculum-section > summary {
    grid-template-columns: 2rem minmax(0, 1fr) 0.5rem;
    column-gap: 0.6rem;
    row-gap: 0.15rem;
  }
  .curriculum-section > summary::after {
    grid-column: 3;
    grid-row: 1;
  }
  .curriculum-sec-meta {
    grid-column: 2;
  }
  .curriculum-lessons {
    margin-left: 2.6rem;
  }
  .curriculum-lesson {
    grid-template-columns: 2.4rem minmax(0, 1fr) auto;
    gap: 0.1rem 0.5rem;
  }
  /* Under the title and the length both, so it holds on one line. */
  .curriculum-held,
  .curriculum-where,
  .curriculum-pass {
    grid-column: 2 / 4;
  }
  .curriculum-free-cta {
    margin-left: 2.6rem;
  }
}

/* Lab / VM callout */
.lab-callout {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: clamp(1.2rem, 3vw, 2.25rem);
  align-items: center;
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.lab-callout-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.lab-callout-body h2 {
  font-size: var(--fs-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.lab-callout-body p {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.6;
  max-width: 52ch;
}

.lab-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.lab-list li {
  position: relative;
  padding-left: 1.4rem;
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.5;
}

.lab-list li::before {
  content: "\2192";
  content: "\2192" / "";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--muted-soft);
  font-weight: 600;
}

/* Pricing block */
.pricing {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  background: var(--bg-soft);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.pricing-head {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.pricing-amount {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.pricing-unit {
  font-size: var(--fs-sm);
  color: var(--muted);
}

.pricing-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem 1.4rem;
}

.pricing-list li {
  position: relative;
  padding-left: 1.35rem;
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.5;
}

.pricing-list li::before {
  content: "\2192";
  content: "\2192" / "";
  position: absolute;
  left: 0;
  top: 0;
  color: var(--muted-soft);
}

@media (max-width: 720px) {
  .lab-callout,
  .pricing {
    grid-template-columns: 1fr;
  }
  .pricing-list {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 680px) {
  .service-pillars,
  .panel-list {
    grid-template-columns: 1fr;
  }

  /* Two by two, each row as tall as its own tiles: one row height for all made
     every tile as tall as the longest label ("labs solved on video" runs to
     two lines here, three at 320 px) and left the others half empty. */
  .course-facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: auto;
  }
}

/* ---------- Papers ----------
   A paper page reuses the service-page shell above; only what a paper needs
   and a service page does not is declared here. The BibTeX block is absent on
   purpose: .paper-bibtex is folded into the .post-body pre selectors instead,
   so a citation and a code block cannot drift apart across the three theme
   states. */
/* The home research foot is space-between with two sides. Papers joins the
   right-hand side rather than becoming a third column, so the advisories tally
   keeps the left edge to itself. */
.research-foot-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1.25rem;
}

.paper-title {
  font-size: var(--fs-post);
  letter-spacing: -0.03em;
  line-height: 1.12;
  max-width: 28ch;
}

.paper-byline {
  font-size: var(--fs-md);
  color: var(--muted);
}

/* A download, not a link out: the arrow falls rather than advancing. */
.paper-download:hover .contact-arrow {
  transform: translateY(2px);
}

/* The document itself, displayed in the page. Its box takes the page's own
   proportions (US Letter, 612 x 792), capped to the viewport, so a laptop
   shows a full page and a phone shows exactly one page width, with no empty
   band under it; the width is the column's and never wider, because a
   document wider than the screen slides every position: fixed control out
   from under the reader. Without a PDF viewer the box holds the fallback
   below and grows to fit it (the automatic minimum of an aspect-ratio box). */
.paper-frame {
  display: block;
  width: 100%;
  aspect-ratio: 612 / 792;
  height: auto;
  max-height: min(82vh, 1000px);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--bg-card);
}

/* The fallback: page 1 as a picture, the whole of it one link to the PDF.
   The page is held under the frame's cap, so on a wide screen without a
   viewer it sits centred instead of running out of the box.
   Chromium clips an <object> to its content box, and an author rule cannot
   lift it (overflow and overflow-clip-margin both stay forced), so the
   site's ring, drawn 3px outside the link, was cut away. It is drawn inside
   instead, against the frame, with a 6px band of card between it and the
   page: on the white page itself the dark theme's light ring would not show. */
.paper-cover {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.paper-cover:focus-visible {
  outline-offset: -2px;
}

.paper-cover img {
  display: block;
  width: auto;
  height: auto;
  margin: 6px 6px 0;
  max-width: calc(100% - 12px);
  max-height: calc(min(82vh, 1000px) - 3.5rem - 6px);
}

.paper-cover-caption {
  align-self: stretch;
  padding: 0.8rem 1rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.paper-cover-action {
  color: var(--fg);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease;
}

.paper-cover:hover .paper-cover-action,
.paper-cover:focus-visible .paper-cover-action {
  text-decoration-color: var(--fg);
}

.paper-verify {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  margin: 0.9rem 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
}

.paper-verify-label {
  display: block;
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  color: var(--muted-soft);
  font-weight: 600;
}

/* A digest and a fingerprint are single unbreakable runs, and a document wider
   than the screen takes every fixed control with it. `overflow-wrap: anywhere`
   is the modern way to say this and it is not enough: Safari honours it for
   breaking the line but not for shrinking the element's min-content
   contribution, so on iOS the run still widened the whole page while Chromium
   measured clean. `word-break: break-all` is the one both engines apply to the
   intrinsic width, and a hex digest is the one kind of text that can be cut
   anywhere without being hurt. The filename keeps `anywhere`, so it breaks at
   its hyphens rather than mid-word. */
.paper-verify code {
  font-size: 1em;
  overflow-wrap: anywhere;
  word-break: break-all;
}

.paper-verify a {
  overflow-wrap: anywhere;
}

/* A flex item refuses to shrink below its content by default, which would put
   the width back however the text is allowed to break. */
.paper-verify li {
  min-width: 0;
}

.paper-verify-note {
  margin-top: 0.9rem;
  font-size: var(--fs-md);
  color: var(--muted);
  max-width: var(--measure);
}

/* Symbols in an abstract read as prose without this; the fill is the same one
   inline code carries in a post. Excludes the citation block, which paints its
   own surface. */
.service-main :not(pre) > code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--rule);
  border-radius: 1px;
  padding: 0.12em 0.35em;
}

/* The index row: the paper's first page, then its title, what it shows and
   its byline. The list's baseline alignment would pin the page to the title.
   The section's own rules already bound the list: its hairlines doubled them. */
.paper-list {
  border-top: 0;
}

.paper-list li:last-child {
  border-bottom: 0;
}

.paper-list a {
  align-items: flex-start;
  flex-wrap: nowrap;   /* the research list wraps under 600px; the page stays beside its title */
  padding-top: 1.1rem;
  padding-bottom: 1.1rem;
}

.paper-thumb {
  flex: 0 0 auto;
  display: block;
  width: 6rem;
  height: auto;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--bg-card);
}

.paper-entry {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
  max-width: var(--measure);
}

.paper-entry-summary {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.5;
}

.paper-entry-meta {
  font-size: var(--fs-sm);
  color: var(--muted-soft);
}

@media (max-width: 480px) {
  .paper-thumb { width: 4.5rem; }
  .paper-list a { gap: 1rem; }
}

/* ---------- 404 / fallout shelter ---------- */
.notfound-main {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 var(--pad-x);
  min-height: 52vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 1.1rem;
  padding-top: clamp(2.5rem, 6vw, 4.5rem);
  padding-bottom: clamp(2.5rem, 6vw, 4.5rem);
}

/* The instrument line under the button: the trefoil small and in ink, as the
   brand guide keeps it, the status in words, then the dosimeter atomic.js
   writes beside it. Nothing sits above the title. */
.notfound-reading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.35rem 0.6rem;
  margin-top: 1.25rem;
  font-size: var(--fs-sm);
  color: var(--muted-soft);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.notfound-mark {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  color: var(--fg);
}

/* The dosimeter (atomic.js). Fixed width digits, so the line does not
   shiver as the reading climbs. */
.notfound-dose {
  font-variant-numeric: tabular-nums;
}
.notfound-dose.is-pegged { color: var(--accent); }
.notfound-mark.is-count { opacity: 0.55; }

.notfound-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
}

.notfound-lede {
  font-size: var(--fs-lede);
  color: var(--muted);
  max-width: 48ch;
  line-height: 1.6;
}

.notfound-main .contact-cta {
  align-self: center;
  margin-top: 0.5rem;
}

/* Without the large mark above its title the page is shorter than a laptop
   screen, and the page colour showed as a band under the footer. On this page
   only, the body is a column whose middle takes what is left. The navigation
   and the main column centre themselves with auto margins, which a flex item
   reads as "shrink to fit", hence their explicit width. */
body:has(> .notfound-main) {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}

body:has(> .notfound-main) > .nav,
body:has(> .notfound-main) > .notfound-main {
  width: 100%;
}

body:has(> .notfound-main) > .notfound-main {
  flex: 1 0 auto;
}

/* ---------- Easter egg: irradiation (Konami) ---------- */
.irradiate-glow {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  background:
    radial-gradient(circle at 50% 118%, rgba(226, 106, 63, 0.55), rgba(226, 106, 63, 0) 62%),
    radial-gradient(circle at 50% -18%, rgba(226, 106, 63, 0.35), rgba(226, 106, 63, 0) 55%);
  mix-blend-mode: screen;
  animation: irradiate-pulse 1.6s ease-out forwards;
}

@keyframes irradiate-pulse {
  0% { opacity: 0; }
  10% { opacity: 1; }
  28% { opacity: 0.5; }
  44% { opacity: 0.95; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .irradiate-glow {
    animation: none;
    opacity: 0.4;
  }
}

/* ---------- Radiation emblem (footer mark) ---------- */
.rad-emblem {
  width: 22px;
  height: 22px;
  color: var(--muted-inv);
  flex-shrink: 0;
  transition: color 200ms ease;
}

html.dark .rad-emblem {
  color: var(--muted-soft);
}

.footer-bottom .rad-emblem:hover {
  color: var(--accent);
}

.rad-spin {
  transform-box: fill-box;
  transform-origin: center;
  animation: rad-spin 14s linear infinite;
}

@keyframes rad-spin {
  to { transform: rotate(360deg); }
}

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

/* ---------- Easter egg: capsule pop (Konami reward) ---------- */
.capsule-pop {
  position: fixed;
  left: 50%;
  top: 50%;
  width: clamp(120px, 22vw, 200px);
  z-index: 10000;
  pointer-events: none;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 10px 34px rgba(226, 106, 63, 0.45));
  animation: capsule-pop 1.6s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}

.capsule-pop svg { display: block; width: 100%; height: auto; }
.capsule-pop .cap-body,
.capsule-pop .cap-nucleus { fill: var(--accent); }
.capsule-pop .cap-label { fill: var(--bg); }
.capsule-pop .cap-orbits { fill: none; stroke: var(--accent); }

@keyframes capsule-pop {
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.3) rotate(-40deg); }
  18% { opacity: 1; transform: translate(-50%, -50%) scale(1.08) rotate(6deg); }
  32% { transform: translate(-50%, -50%) scale(1) rotate(0deg); }
  72% { opacity: 1; transform: translate(-50%, -50%) scale(1) rotate(0deg); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.05) rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
  .capsule-pop {
    animation: none;
    opacity: 1;
    transform: translate(-50%, -50%);
  }
}

/* ---------- Easter egg: criticality incident (footer emblem click) ---------- */
/* No pointer cursor on purpose: the mark must not look clickable. */
.footer-bottom .rad-emblem { border-radius: 50%; }
.footer-bottom .rad-emblem:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

.rad-emblem.is-warming {
  color: var(--accent);
  animation: rad-critical 4.05s cubic-bezier(.6,0,.9,.5) forwards;
}
.rad-emblem.is-warming .rad-spin { animation-duration: 1.2s; }
.rad-emblem.is-critical .rad-spin { animation-duration: 0.16s; }
.rad-emblem.is-spent { color: var(--accent); }
.rad-emblem.is-spent .rad-spin { animation-duration: 0.6s; }

@keyframes rad-critical {
  0% { transform: scale(1); filter: drop-shadow(0 0 0 transparent); }
  70% { transform: scale(1.5); filter: drop-shadow(0 0 10px var(--accent)); }
  100% { transform: scale(2.3); filter: drop-shadow(0 0 24px var(--accent)) brightness(1.8); }
}

.meltdown-layer {
  position: fixed;
  inset: 0;
  z-index: 10000;
  pointer-events: none;
  overflow: hidden;
}

.meltdown-flash {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at var(--gx) var(--gy), #fff 0, color-mix(in srgb, var(--accent) 70%, #fff) 6%,
      color-mix(in srgb, var(--accent) 35%, transparent) 30%, transparent 70%);
  animation: meltdown-flash 0.9s ease-out forwards;
}

@keyframes meltdown-flash {
  0% { opacity: 0; }
  8% { opacity: 1; }
  100% { opacity: 0; }
}

.meltdown-ring {
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 6px solid var(--accent);
  transform: translate(-50%, -50%) scale(0);
}

.meltdown-spark {
  position: absolute;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--accent);
}

/* A neutron in flight during the build-up. */
.meltdown-neutron {
  position: absolute;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 18%, var(--accent) 26%,
    color-mix(in srgb, var(--accent) 35%, transparent) 45%, transparent 70%);
}

/* A piece the chain has reached: it takes the accent and strains. */
.is-hot {
  color: var(--accent) !important;
  transition: color 300ms ease;
}

.meltdown-scorch {
  position: absolute;
  width: 360px;
  height: 360px;
  margin: -180px 0 0 -180px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  background: radial-gradient(circle, color-mix(in srgb, var(--fg) 45%, transparent) 0,
    color-mix(in srgb, var(--accent) 22%, transparent) 30%, transparent 68%);
  opacity: 0;
  animation: meltdown-scorch 2.4s ease-out forwards;
}

@keyframes meltdown-scorch {
  to { opacity: 0.8; }
}

html.melted {
  overflow-x: hidden; /* debris past the edge must not widen the page */
}

.meltdown-notice {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 1.25rem;
  width: max-content;
  max-width: calc(100% - 32px);
  margin-inline: auto;
  z-index: 10001;
  padding: 0.6rem 1rem;
  border-radius: 999px;
  background: var(--fg);
  color: var(--bg);
  font-size: var(--fs-md);
  font-weight: 500;
  text-align: center;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
  animation: meltdown-notice 0.5s 0.2s cubic-bezier(.2,.8,.2,1) both;
}

@keyframes meltdown-notice {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

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

/* ---------- Dark mode without JavaScript ---------- */
/* The theme is normally carried by the .dark class the pre-paint script sets,
   which means a reader with JS disabled and a dark OS was served the light
   theme. This block is the same dark section keyed off the OS preference
   instead of the class, and it stands down as soon as the reader has picked
   light explicitly (the script stamps html.light for exactly that purpose).
   INVARIANT: every rule above that starts with `html.dark` has a twin here.
   Change one, change the other. */
@media (prefers-color-scheme: dark) {
  html:not(.light) .footer {
    --focus-ring: var(--fg); /* dark footer sits on --bg-soft, not inverted */
  }

  html:not(.light) .post-body pre,
  html:not(.light) .paper-bibtex {
    --focus-ring: var(--fg); /* dark code blocks sit on their own ink, not inverted */
  }

  html:not(.light) {
    --bg: #101216;
    --bg-soft: #181b20;
    --bg-card: #1e222a;
    --fg: #f7f7f5;
    --fg-soft: #d9dce1;
    --muted: #9aa1ac;
    --muted-soft: #8b939f;
    --muted-inv: #5c6470;
    --rule: #2d323a;
    --rule-strong: #3a414b;
    --accent: #8aa9f0;
    --accent-inv: #1b3a8a;
    --chip-fg: #949ba6;

    --status-slate: #929ca8;
    --status-moss: #7fb088;
    --status-amber: #d6a44a;
    --status-orange: #d97c4f;
    --status-brick: #dc6a52;

    color-scheme: dark;
  }

  html:not(.light) .panel {
    background: var(--fg);
    color: var(--bg);
    border: none;
  }

  html:not(.light) .panel .kicker-light {
    color: var(--muted-inv);
  }

  html:not(.light) .footer {
    background: var(--bg-soft);
    color: var(--fg);
    border-top: 1px solid var(--rule);
  }

  html:not(.light) .footer-name {
    color: var(--fg);
  }

  html:not(.light) .footer-meta,
  html:not(.light) .footer-nav {
    color: var(--muted);
  }

  html:not(.light) .footer-nav a:hover {
    color: var(--fg);
    border-bottom-color: var(--fg);
  }

  html:not(.light) .footer-bottom {
    color: var(--muted-soft);
    border-top: 1px solid var(--rule);
  }

  html:not(.light) .footer ::selection,
  html:not(.light) .post-body pre ::selection,
  html:not(.light) .paper-bibtex ::selection {
    background: color-mix(in srgb, var(--accent) 22%, transparent);
    color: var(--fg);
  }

  html:not(.light) .post-body pre,
  html:not(.light) .paper-bibtex {
    background: #0b0d10;
    color: #d9dce1;
    border: 1px solid var(--rule);
  }

  html:not(.light) .tile:hover,
  html:not(.light) .blog-card:hover {
    box-shadow: 0 12px 28px -22px rgba(14, 13, 10, 0.65);
  }

  html:not(.light) .team-photo img {
    filter: grayscale(0.28) saturate(0.88) brightness(0.95);
  }

  html:not(.light) .team-member:hover .team-photo img {
    filter: brightness(0.98);
  }

  html:not(.light) .team-member:hover .team-photo {
    box-shadow: 0 0 0 4px var(--bg), 0 0 0 5px var(--rule-strong);
  }

  html:not(.light) .team-link {
    background: var(--bg-soft);
  }

  html:not(.light) .team-link:hover {
    background: var(--fg);
    color: var(--bg);
    border-color: var(--fg);
  }

  html:not(.light) .brand-logo-light {
    display: none;
  }

  html:not(.light) .prev-logos img {
    filter: invert(1);
    opacity: 0.68;
  }

  html:not(.light) .prev-logos li:hover img {
    opacity: 0.92;
  }

  html:not(.light) .rad-emblem {
    color: var(--muted-soft);
  }

}

/* ---------- Newsletter (nav modal + end-of-post signup) ----------
   Both surfaces share this block; the tokens flip with the theme already,
   so nothing here is dark-mode-specific. */
.nl-dialog {
  position: relative;
  width: min(420px, calc(100vw - 2.5rem));
  max-height: min(560px, calc(100vh - 2.5rem));
  overflow: auto;
  padding: clamp(1.5rem, 4vw, 2rem);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--bg-card);
  color: var(--fg);
}

.nl-dialog::backdrop {
  /* A scrim must darken behind the dialog in both themes. Keying it to --fg
     backfired in dark mode, where --fg is near-white and lit the page up
     instead; use a fixed dark wash. */
  background: rgb(0 0 0 / 0.55);
}

.nl-close {
  -webkit-appearance: none;
  appearance: none;
  position: absolute;
  top: 0.5rem;
  right: 0.6rem;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font: inherit;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  border-radius: 1px;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease;
}

.nl-close:hover {
  color: var(--fg);
  background: var(--bg-soft);
}

.nl-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.nl-dialog .nl-form {
  padding-top: 0.35rem; /* clears the close button */
}

.nl-label {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--fg);
}

.nl-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
}

.nl-input {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 220px;
  min-width: 0;
  padding: 0.55rem 0.9rem;
  font: inherit;
  font-size: var(--fs-md);
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  transition: border-color 160ms ease;
}

.nl-input::placeholder {
  color: var(--muted);
}

.nl-input:hover,
.nl-input:focus {
  border-color: var(--fg);
}

.nl-button {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 auto;
  padding: 0.55rem 1.25rem;
  font: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--fg);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
}

.nl-button:hover {
  background: var(--fg);
  border-color: var(--fg);
  color: var(--bg);
}

/* A request on its way: aria-disabled rather than disabled, so the button
   keeps the focus (newsletter.js refuses a second send meanwhile). */
.nl-button:disabled,
.nl-button[aria-disabled="true"] {
  cursor: default;
  opacity: 0.55;
}

.nl-status {
  font-size: var(--fs-sm);
  color: var(--fg);
}

/* "write to contact@sigreturn.com" in an error is a mailto link (newsletter.js):
   links inherit their colour site-wide, so it needs its underline to be seen. */
.nl-status a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

.nl-status a:hover {
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
}

/* Empty, the live region stays in the page but takes no room: display:none
   took it out of the accessibility tree, so the first message could appear
   in a region a screen reader had never registered, and go unannounced. */
.nl-status:empty {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.nl-fine {
  font-size: var(--fs-xs);
  color: var(--muted);
}

.nl-fine a {
  color: inherit;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: color 160ms ease, border-color 160ms ease;
}

.nl-fine a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* Honeypot: kept in normal flow and paintable by a bot's renderer, just
   pushed off the visible canvas — display:none/hidden is commonly ignored
   by the scripts this field exists to catch. */
.nl-hp {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.nl-post {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding: clamp(1.5rem, 3vw, 2rem);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--bg-card);
}

.nl-success {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
}

.nl-check {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 1px;
}

.nl-success-title {
  margin: 0 0 0.2rem;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--fg);
}

.nl-success-text {
  margin: 0;
  font-size: var(--fs-md);
  color: var(--muted);
}

/* "Wrong address? Enter it again" (newsletter.js puts the form back): a
   button, drawn as the link it reads as in its sentence. */
.nl-again {
  -webkit-appearance: none;
  appearance: none;
  padding: 0;
  font: inherit;
  font-weight: 500;
  color: var(--fg);
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration-line: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.nl-again:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

@media (max-width: 480px) {
  .nl-row {
    flex-direction: column;
    align-items: stretch;
  }
  /* In a column the flex-basis governs height, so the 220px basis would make
     the field 220px tall (and Safari stretches it further). Reset it. */
  .nl-input {
    flex: 0 0 auto;
    width: 100%;
  }
  .nl-button {
    width: 100%;
  }
}

/* ---------- Subscription confirmation page ----------
   A short "moment" page: too little content to fill the legal layout, so it
   centres itself and pins the footer to the bottom instead of stranding it
   mid-viewport over dead space. */
.page-confirm {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* In a flex column the nav's auto side margins beat stretch, so it shrank to
   its content and sat off-centre: give it the full width back, its max-width
   and margins then centre it like on every other page. */
.page-confirm > .nav {
  width: 100%;
}

.confirm-main {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(2rem, 8vh, 5rem) var(--pad-x);
}

.confirm-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin-bottom: 1.1rem;
  color: var(--accent);
  border: 1.5px solid var(--accent);
  border-radius: 1px;
}

/* /subscribe/ shows the check only once its script knows the link worked:
   without the script, or on a dead link, no glyph claims a success. */
.confirm-icon[hidden] {
  display: none;
}

.confirm-title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1.05;
  margin: 0;
}

.confirm-text {
  font-size: var(--fs-lg);
  color: var(--muted);
  max-width: 46ch;
  margin: 0.5rem 0 1rem;
}

/* The waitlist's thank-you links to the free section 00: ink and an underline
   that reaches 3:1, as the course page's prose links. */
.confirm-text a {
  color: var(--fg);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

.confirm-text a:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* The confirm and unsubscribe pages of the newsletter service (newsletter/app.py INTERSTITIAL): the way back
   sat 6 px under the button, a thumb aimed at one could take the other. /subscribe/ too, under its buttons. */
.confirm-main form + .confirm-back,
.sub-action:not([hidden]) ~ .confirm-back {
  margin-top: 1.25rem;
}

/* /subscribe/'s waitlist thank-you: its next step drawn as the course page's buttons.
   The row is a flex box, which would beat the hidden attribute until the script fills it. */
.sub-action[hidden] {
  display: none;
}

.confirm-back a {
  font-size: var(--fs-md);
  color: var(--muted);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: color 160ms ease, border-color 160ms ease;
}

.confirm-back a:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* /subscribe/ without its script: the blog and the course side by side, apart enough for a thumb, the second
   under the first on a phone too narrow for both */
.sub-backs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.75rem;
}

/* The page a waitlist confirmation link opens (newsletter/app.py): its one
   action puts the reader on the list, the step after the course page's solid
   "Join the waitlist", so it is the same solid button rather than the quiet
   outlined one, which read as secondary or disabled. */
.confirm-waitlist .nl-button {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.confirm-waitlist .nl-button:hover {
  color: var(--bg);
  background: var(--fg-soft);
  border-color: var(--fg-soft);
}

/* ---------- Post summary, post navigation, search and filtering ----------

   Four components added together because they share one job: getting a reader
   to the post they want. Everything here resolves through the tokens at the
   top of this sheet, so light and dark follow the toggle with no per-theme
   rules, apart from the two places noted inline. */

/* ---- "In brief": the TLDR card at the top of a post ---- */

.post-summary {
  /* Aligne sur la colonne de l'article : meme bord gauche et meme bord droit
     que .post-body, dont il est le frere. box-sizing: border-box est global,
     donc le padding interne ne deborde pas. */
  max-width: none;
  margin: clamp(1.25rem, 3vw, 1.75rem) 0 0;
  padding: clamp(1rem, 2.5vw, 1.35rem) clamp(1.1rem, 3vw, 1.5rem);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

/* Folded by default (the owner's call, 2026-11-13): the label opens it. */
.post-summary-kicker {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  color: var(--muted-soft);
  cursor: pointer;
  list-style: none;
}

.post-summary-kicker::-webkit-details-marker {
  display: none;
}

.post-summary-kicker::after {
  content: "";
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border: solid var(--muted);
  border-width: 0 1px 1px 0;
  transform: translateY(-25%) rotate(45deg);
  transition: transform 160ms ease;
}

.post-summary[open] > .post-summary-kicker {
  margin-bottom: 0.55rem;
}

.post-summary[open] > .post-summary-kicker::after {
  transform: translateY(25%) rotate(-135deg);
}

@media (prefers-reduced-motion: reduce) {
  .post-summary-kicker::after {
    transition: none;
  }
}

.post-summary-tldr {
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--fg-soft);
}

.post-summary-list {
  list-style: none;
  margin: 0.9rem 0 0;
  padding: 0.9rem 0 0;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.post-summary-list li {
  position: relative;
  padding-left: 1.1rem;
  font-size: var(--fs-md);
  line-height: 1.55;
  color: var(--muted);
}

/* The marker is the one accent in the block, per The Hazard-Mark Rule. */
.post-summary-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 0.4rem;
  height: 1px;
  background: var(--accent);
}

.post-summary code {
  font-size: 0.92em;
}

/* ---- Previous / next ---- */

/* No auto inline margin here, and no measure cap. Both were dead weight: the
   post column is already 720px and centred, and 72ch resolves to 756px so the
   cap never bound. The auto margin was worse than dead. An auto margin absorbs
   the free space of a grid item, which cancels the stretch, so the row sized
   itself to its two titles instead of its column: 615px on apple-security-stack,
   681px on zone-allocator, 552px where only one neighbour exists. The hairlines
   above and below came up short with it, which is what made the row look
   squashed on some posts and not others. */
.post-nav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

/* A missing neighbour leaves its column empty rather than letting the other
   slide across: "next" has to stay on the right to mean anything. */
.post-nav-prev {
  grid-column: 1;
}

.post-nav-next {
  grid-column: 2;
}

.post-nav-top {
  margin-top: clamp(1.25rem, 3vw, 1.75rem);
  padding: 0.7rem 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  gap: 1.5rem;
}

.post-nav-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  font-size: var(--fs-sm);
  color: var(--muted);
  transition: color 160ms ease;
}

.post-nav-next.post-nav-link {
  justify-content: flex-end;
  text-align: right;
}

.post-nav-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.post-nav-dir {
  flex: 0 0 auto;
  color: var(--muted-soft);
  transition: transform 200ms ease, color 160ms ease;
}

.post-nav-link:hover {
  color: var(--fg);
}

.post-nav-prev:hover .post-nav-dir {
  transform: translateX(-3px);
  color: var(--fg);
}

.post-nav-next:hover .post-nav-dir {
  transform: translateX(3px);
  color: var(--fg);
}

/* Same treatment as the top row, deliberately: two different presentations
   of the same pair of links read as two different features. Only the spacing
   above it differs, because it follows the article rather than the byline. */
.post-nav-bottom {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding: 0.7rem 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  gap: 1.5rem;
}

@media (max-width: 640px) {
  .post-nav {
    grid-template-columns: 1fr;
  }

  .post-nav-prev,
  .post-nav-next {
    grid-column: 1;
  }

  .post-nav-next.post-nav-link {
    text-align: left;
    justify-content: flex-start;
  }

  /* The arrow leads on both sides once they are stacked, so the row still
     reads as a pair rather than as two unrelated links. */
  .post-nav-next.post-nav-link {
    flex-direction: row-reverse;
    justify-content: flex-end;
  }
}

/* ---- Blog card: stretched link ----
   The card stopped being an <a> so its tag pills could become buttons; an
   anchor may not contain interactive content. The title's link is stretched
   over the card instead, and the pills sit above it in the stack. */

.blog-card {
  position: relative;
}

.blog-card-link {
  color: inherit;
}

.blog-card-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.blog-card:focus-within {
  background: var(--bg-soft);
}

.blog-card-link:focus-visible {
  outline: none;
}

.blog-card-link:focus-visible::after {
  outline: 2px solid var(--focus-ring);
  outline-offset: -3px;
  border-radius: 1px;
}

.blog-card:focus-within .blog-card-arrow {
  transform: translateX(3px);
  color: var(--fg);
}

/* ---- Tag pills as controls ---- */

.blog-card-tag,
.post-tag {
  position: relative;
  z-index: 1;
}

/* A pill must look exactly the same as a button as it did as a span, so this
   block only neutralises UA button chrome and declares nothing the .blog-card-tag
   and .post-tag rules already set. Careful with specificity: button.blog-card-tag
   is (0,1,1) and beats .blog-card-tag at (0,1,0), so any font-size, padding,
   background, colour or letter-spacing declared here would silently win. */
button.blog-card-tag,
button.post-tag {
  -webkit-appearance: none;
  appearance: none;
  line-height: inherit;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, border-color 160ms ease;
}

/* The card pill draws no border of its own: without this reset the UA button
   border survives and the 999px radius renders it as a dark ring. */
button.blog-card-tag {
  border: 0;
  font-family: var(--font-mono);
}

/* The post pill keeps its own hairline border, and takes the body font back
   from the UA button font, which .post-tag never overrides on its own. */
button.post-tag {
  font-family: inherit;
}

button.blog-card-tag:hover,
button.post-tag:hover {
  background: var(--fg);
  color: var(--bg);
  border-color: var(--fg);
}

/* ---- Active filter chip ---- */

.blog-filter-active {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: 1rem;
}

.blog-filter-active[hidden] {
  display: none;
}

.blog-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.25rem 0.7rem;
  font: inherit;
  font-size: var(--fs-xs);
  color: var(--muted);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}

.blog-filter-chip:hover {
  color: var(--fg);
  border-color: var(--fg);
}

.blog-filter-chip-x {
  font-size: 1.05em;
  line-height: 1;
  color: var(--muted-soft);
}

.blog-filter-chip:hover .blog-filter-chip-x {
  color: var(--accent);
}

/* ---- Search trigger in the nav ---- */

.search-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--nav-btn);
  height: var(--nav-btn);
  padding: 0;
  color: var(--muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}

.search-trigger:hover {
  color: var(--fg);
  border-color: var(--rule);
}

/* ---- Search modal ---- */

.search-dialog {
  position: fixed;
  width: min(640px, calc(100vw - 2.5rem));
  max-height: min(70vh, calc(100vh - 4rem));
  margin-top: clamp(3rem, 12vh, 8rem);
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--bg-card);
  color: var(--fg);
}

.search-dialog[open] {
  display: flex;
  flex-direction: column;
}

/* Same fixed dark wash as .nl-dialog::backdrop, for the same reason: keying a
   scrim to --fg lights the page up in dark mode instead of dimming it. The
   blur is the part that is new. */
.search-dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.search-form {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 1.1rem 1.35rem;
  border-bottom: 1px solid var(--rule);
}

.search-icon {
  display: inline-flex;
  color: var(--muted-soft);
  flex: 0 0 auto;
}

.search-input {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.25rem 0;
  font: inherit;
  font-size: var(--fs-lg);
  color: var(--fg);
  background: none;
  border: 0;
}

.search-input:focus {
  outline: none;
}

.search-input::placeholder {
  color: var(--muted-soft);
}

.search-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

.search-close {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  padding: 0;
  font: inherit;
  font-size: 1.2rem;
  line-height: 1;
  color: var(--muted-soft);
  background: none;
  border: 1px solid transparent;
  border-radius: 1px;
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}

.search-close:hover {
  color: var(--fg);
  border-color: var(--rule);
}

.search-status {
  padding: 1rem 1.35rem 0.35rem;
  font-size: var(--fs-xs);
  color: var(--muted-soft);
}

.search-results {
  overflow-y: auto;
  /* No horizontal padding: the rows carry it themselves, so their separators
     run the full width of the card instead of floating inset. */
  padding: 0 0 0.6rem;
}

/* Hairline rows rather than cards: a list of eight results has to stay
   readable, and eight bordered boxes read as a wall. The row is padded
   generously on purpose; a dense list of long technical titles is the thing
   this modal is worst at. */
.search-result {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 1.05rem 1.35rem;
  color: inherit;
}

/* Straight hairlines, edge to edge. A border-radius on the row makes its own
   border-top curve away at both ends, which reads as a rounded separator. */
.search-result + .search-result {
  border-top: 1px solid var(--rule);
}

.search-result:hover,
.search-result.is-active {
  background: var(--bg-soft);
}

/* The tint alone measured 1.15:1 against the card, which is not a selection
   indicator anyone can see. Hover keeps the tint; the arrow-key selection also
   draws a real outline. */
.search-result.is-active {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

.search-result-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--fs-2xs);
  color: var(--muted-soft);
}

.search-result-cat {
  color: var(--muted-soft);
}

.search-result-date {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.search-result-title {
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: 1.35;
}

.search-result-snippet {
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.search-result-snippet mark {
  padding: 0 0.1em;
  color: var(--fg);
  background: color-mix(in srgb, var(--accent) 24%, transparent);
  border-radius: 1px;
}

.search-hint {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding: 0.8rem 1.35rem 0.9rem;
  border-top: 1px solid var(--rule);
  font-size: var(--fs-2xs);
  color: var(--muted-soft);
}

.search-hint kbd {
  font-family: var(--font-mono);
  font-size: 0.95em;
  padding: 0.05rem 0.3rem;
  color: var(--chip-fg);
  background: var(--rule);
  border-radius: 1px;
}

@media (max-width: 560px) {
  .search-dialog {
    margin-top: 1.25rem;
    max-height: calc(100vh - 2.5rem);
  }

  .search-hint {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .post-nav-dir,
  .search-trigger,
  .blog-filter-chip,
  button.blog-card-tag,
  button.post-tag {
    transition: none;
  }

  .post-nav-prev:hover .post-nav-dir,
  .post-nav-next:hover .post-nav-dir {
    transform: none;
  }
}

/* ---- Back to top ----
   Floating, bottom right, injected by the post script. Follows the site's
   button language rather than inventing one: hairline card at rest per The
   Lift-On-Intent Rule, full ink inversion on hover per The Inversion Rule, and
   the arrow nudges the way every other arrow on the site does, upward here. */

.to-top {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: calc(clamp(1rem, 3vw, 2rem) + var(--to-top-lift, 0px));
  z-index: 40;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 0;
  color: var(--muted);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  cursor: pointer;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 200ms ease, transform 200ms ease, color 160ms ease,
    background 160ms ease, border-color 160ms ease;
}

.to-top[hidden] {
  display: none; /* the UA rule loses to display:inline-flex above */
}

.to-top.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.to-top:hover {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.to-top:hover svg {
  transform: translateY(-3px);
}

.to-top svg {
  transition: transform 200ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .to-top,
  .to-top svg {
    transition: none;
  }

  .to-top.is-visible {
    transform: none;
  }

  .to-top:hover svg {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .post-toc-title,
  .post-toc-title::before {
    transition: none;
  }
}

/* ---- Table of contents as a sticky rail (wide screens) ----
   Below this width the contents stay a collapsed <details> at the top of the
   post, which is the whole point of collapsing them: on a phone they are the
   tallest thing between the title and the first sentence. Above it there is
   room in the margin, and a contents list is only genuinely useful while you
   are reading, which a block at the top of the page cannot be.

   The post column keeps its 720px measure; the rail is added beside it, so the
   article shifts right by half the rail on these screens. That is the cost of
   the rail and it is deliberate. */
@media (min-width: 1320px) {
  /* Three symmetric columns, not two: with 1fr on each side the 720px measure
     stays centred in the viewport and the rail lives in the margin that was
     already there. A two-column grid pushed the article right by half the rail.

     The container is allowed past --max-w here, and the breakpoint sits at
     1320 rather than 1180, for one reason: at the site's own 1120 the gutter
     leaves about 150px of text and every entry broke over three or four lines.
     A rail that cramped is worse than no rail, so it only appears where there
     is honestly room for it. */
  .post-main {
    max-width: min(1400px, 100%);
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 720px) minmax(0, 1fr);
    row-gap: 0;
  }

  /* Everything stays in the centre column; only the contents move out. */
  .post-main > * {
    grid-column: 2;
  }

  /* The area has to span every row or the sticky element can only travel
     inside the first one. Twelve is above the ten blocks a post can have;
     empty implicit rows collapse to zero because row-gap is 0. */
  .post-toc {
    grid-column: 1;
    grid-row: 1 / span 12;
    justify-self: end;
    align-self: start;
    width: min(100%, 290px);
    position: sticky;
    top: 2.5rem;
    margin: clamp(2rem, 4vw, 3rem) 0 0;
    /* The right padding is the gap to the article and is deliberately large:
       a rail this close to the text reads as part of it. The few pixels above
       and below keep the summary's focus ring inside the scrolling box, which
       would otherwise clip it. */
    padding: 0.3rem 3rem 0.3rem 1.1rem;
    max-height: calc(100vh - 5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: none;
    border: 0;
    border-left: 1px solid var(--rule);
    border-radius: 0;
  }

  /* In the rail the list is always open, so the summary is a label and not a
     control: no pointer, no chevron, and nothing to click. JS sets the open
     attribute, because CSS cannot force a <details> open. */
  .post-toc .post-toc-title {
    cursor: default;
    pointer-events: none;
    margin-bottom: 0.75rem;
  }

  .post-toc .post-toc-title::before {
    display: none;
  }

  .post-toc-details[open] {
    padding-bottom: 0;
  }

  .post-toc-details[open] .post-toc-list {
    margin-top: 0;
  }

  .post-toc-list {
    gap: 0.55rem;
  }

  .post-toc-list a {
    font-size: var(--fs-sm);
    color: var(--muted);
  }
}

/* ---- Current section in the contents ----
   Set by the post script from the scroll position. The accent is a scarce
   mark per The Hazard-Mark Rule, and it is doing real work here: it is the
   only thing telling you where you are in a 25 KB post. */
.post-toc-list li.is-current::before {
  color: var(--accent);
}

.post-toc-list li.is-current a {
  color: var(--fg);
  font-weight: 500;
}

/* ---- Narration ----
   Injected by audio.js, and only on a post build.py found audio for, so none
   of this reaches a page where it would do nothing. The register follows the
   rest of the site: hairline card at rest per The Lift-On-Intent Rule, full
   ink inversion on intent per The Inversion Rule, and the accent kept scarce
   per The Hazard-Mark Rule. Here it earns its place twice, because it is the
   only thing telling a listener where the voice is on the page. */

:root {
  --tts-band: rgba(27, 58, 138, 0.075);
  --tts-word: rgba(27, 58, 138, 0.2);
}

html.dark {
  --tts-band: rgba(138, 169, 240, 0.11);
  --tts-word: rgba(138, 169, 240, 0.28);
}

/* -- the entry button, under the byline -- */

.tts-hero-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 1.4rem;
}

.tts-hero {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.5rem 0.95rem 0.5rem 0.75rem;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--fg);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}

.tts-hero:hover {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

.tts-hero[disabled] {
  cursor: default;
  color: var(--muted);
}

/* The triangle is drawn rather than fetched: one control, no icon request. */
.tts-hero-icon {
  width: 0;
  height: 0;
  margin-left: 0.15rem;
  border-style: solid;
  border-width: 5px 0 5px 8px;
  border-color: transparent transparent transparent var(--accent);
  transition: border-color 160ms ease;
}

.tts-hero.is-playing .tts-hero-icon {
  width: 8px;
  height: 10px;
  margin-left: 0.15rem;
  border: none;
  background: linear-gradient(to right, var(--accent) 0 3px,
    transparent 3px 5px, var(--accent) 5px 8px);
}

.tts-hero:hover .tts-hero-icon {
  border-left-color: var(--bg);
}

.tts-hero.is-playing:hover .tts-hero-icon {
  background: linear-gradient(to right, var(--bg) 0 3px,
    transparent 3px 5px, var(--bg) 5px 8px);
}

.tts-hero-restart {
  padding: 0;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--muted);
  background: none;
  border: none;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease;
}

.tts-hero-restart:hover {
  color: var(--fg);
  border-bottom-color: var(--fg);
}

/* The disclosure under the button: its own line, small and muted, so it reads
   as a caption of the control and not as a second one. */
.tts-hero-note {
  flex-basis: 100%;
  margin-top: -0.3rem;
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* -- the block being read -- */

/* A band rather than a left rule: post-body list items already own their
   ::before, and a spread shadow tints past the text box without moving a
   single character. */
.tts-current {
  background: var(--tts-band);
  box-shadow: 0 0 0 0.45rem var(--tts-band);
  border-radius: 1px;
}

::highlight(tts-word) {
  background-color: var(--tts-word);
}

/* -- start from any paragraph -- */

.tts-block {
  position: relative;
}

.tts-from-here {
  position: absolute;
  top: 0.2em;
  left: -1.9rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  padding: 0;
  color: var(--muted);
  background: none;
  border: none;
  border-radius: 1px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 140ms ease, color 140ms ease, background 140ms ease;
}

.tts-block:hover > .tts-from-here,
.tts-from-here:focus-visible {
  opacity: 1;
}

.tts-from-here:hover {
  color: var(--bg);
  background: var(--fg);
}

.tts-current > .tts-from-here {
  opacity: 1;
  color: var(--accent);
}

/* -- the floating control -- */

/* Centred with left/right:0 and an auto inline margin rather than left:50%
   plus a translate. The transform version measured dead centre in the
   emulator and still drifted on a phone, because it is anchored to the layout
   viewport and 100vw is not the width you can see once anything on the page
   overflows sideways. This version centres inside the containing block, wraps
   instead of overflowing, and needs no viewport unit at all. */
.tts-pill {
  position: fixed;
  left: 0;
  right: 0;
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: 45;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  width: max-content;
  max-width: calc(100% - 1.5rem);
  margin-inline: auto;
  padding: 0.35rem 0.45rem;
  color: var(--fg);
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: 1px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.13);
}

.tts-pill[hidden] {
  display: none; /* the UA rule loses to display:flex above */
}

.tts-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  height: 2rem;
  padding: 0 0.4rem;
  font: inherit;
  font-size: var(--fs-3xs);
  color: var(--muted);
  background: none;
  border: none;
  border-radius: 1px;
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease;
}

.tts-btn:hover {
  color: var(--bg);
  background: var(--fg);
}

.tts-play {
  color: var(--fg);
}

.tts-time {
  padding: 0 0.5rem;
  font-size: var(--fs-3xs);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}

/* The one control that appears mid-listen, when the reader has scrolled off
   on their own. It says so in words rather than an icon, because it is
   offering to move the page under them. */
.tts-follow {
  color: var(--accent);
  white-space: nowrap;
}

.tts-follow[hidden] {
  display: none;
}

.tts-close {
  margin-left: 0.1rem;
}

/* Two floating controls on a narrow screen would sit on each other. */
@media (max-width: 900px) {
  body.tts-on .to-top {
    bottom: calc(clamp(1rem, 3vw, 2rem) + 3.25rem);
  }

  .tts-from-here {
    left: -1.15rem;
    width: 1.1rem;
    height: 1.1rem;
  }

  .tts-time {
    padding: 0 0.3rem;
  }

  /* The words are what unbalanced the pill on a phone: five round icons and
     one long label read as a control cluster shoved to one side, however
     exactly the box was centred. The arrow says the same thing and keeps the
     row symmetrical. */
  .tts-follow {
    font-size: 0;
    min-width: 2rem;
    padding: 0;
  }

  .tts-follow::before {
    content: "\2193";
    font-size: 0.95rem;
    line-height: 1;
  }
}

@media (max-width: 420px) {
  .tts-time {
    display: none; /* the pill keeps its controls before its readout */
  }
}

@media (prefers-reduced-motion: reduce) {
  .tts-hero,
  .tts-hero-icon,
  .tts-hero-restart,
  .tts-from-here,
  .tts-btn {
    transition: none;
  }
}

/* ---------- Marque de lien sortant ----------
   Dessinee plutot que composee : aucune des polices du site ne couvre U+2197,
   qui retombait sur une fonte systeme, plus fine et mal alignee. */
.mark-keep {
  white-space: nowrap;
}

.team-link-arrow,
.talk-arrow,
.adv-row-arrow {
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-left: 0;
  vertical-align: -0.02em;
  font-size: inherit;
}

.team-link-arrow svg,
.talk-arrow svg,
.adv-row-arrow svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: square;
  stroke-linejoin: miter;
}

/* ===========================================================================
   /cv/ — le CV du fondateur
   ---------------------------------------------------------------------------
   Un document, pas un tableau de bord. Une colonne de lecture, le nom de
   chaque section dans la marge de gauche, les dates alignees a droite de la
   ligne d'en-tete. Cette disposition est celle d'un CV imprime depuis toujours
   parce qu'elle se parcourt a la verticale : l'oeil descend la colonne des
   dates sans lire, et s'arrete ou il veut.
   Le bloc @media print en fin de section est le seul rendu PDF : pas de
   seconde feuille a maintenir, donc pas de seconde feuille qui derive.
   =========================================================================== */

.cv-main {
  /* Largeur du document = marge des titres + colonne de lecture. La colonne
     reste sous --measure : le CV se lit, il ne se survole pas. */
  --cv-gutter: 9.5rem;
  --cv-gap: 2.5rem;
  max-width: calc(var(--cv-gutter) + var(--cv-gap) + 42rem);
  margin: 0 auto;
  padding: 0 var(--pad-x);
}

/* --- identite ------------------------------------------------------------ */

.cv-hero {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
  padding: clamp(2rem, 4vw, 3.25rem) 0 clamp(1.75rem, 3vw, 2.5rem);
}

.cv-hero-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.55rem;
}

.cv-name {
  font-size: var(--fs-title);
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

.cv-headline {
  font-size: var(--fs-lede);
  color: var(--fg-soft);
  margin: 0;
  font-weight: 500;
}

.cv-lede {
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.6;
  max-width: 38rem;
  margin: 0.35rem 0 0;
}

/* Les faits qui ne meritent pas une phrase : lieu, anciennete, diplome. */
.cv-facts {
  display: flex;
  flex-wrap: wrap;
  /* Un interligne non nul : sur telephone les trois faits passent sur deux
     rangees et se collaient, l'intitule de la seconde touchait la premiere. */
  gap: 0.8rem 2rem;
  margin: 0.9rem 0 0;
}

.cv-fact dt {
  font-size: var(--fs-3xs);
  color: var(--muted-soft);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.cv-fact dd {
  margin: 0.1rem 0 0;
  font-size: var(--fs-sm);
  color: var(--fg-soft);
}

.cv-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  margin-top: 1rem;
}

.cv-link {
  font-size: var(--fs-sm);
  color: var(--fg-soft);
  /* L'adresse, d'un seul mot, depassait l'ecran avec un texte agrandi. */
  overflow-wrap: anywhere;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: text-decoration-color 160ms ease, color 160ms ease;
}

.cv-link:hover {
  color: var(--fg);
  text-decoration-color: var(--accent);
}

.cv-actions:empty { display: none; }
.cv-actions { margin-top: 1.1rem; }

.cv-print {
  display: inline-block;
  text-decoration: none;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  padding: 0.5rem 0.95rem;
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}

.cv-print:hover {
  background: var(--fg);
  color: var(--bg);
}

.cv-portrait {
  width: clamp(116px, 20vw, 188px);
}

/* Le portrait est un DECOUPAGE : fond transparent, pas de plaque, pas de
   filet. Un cadre autour en ferait une vignette collee sur la page.
   Il tient sur les deux themes sans traitement, et ce n'est pas un hasard :
   sa trame porte 26 % de pixels tres clairs (14,6:1 sur l'encre du fond
   sombre) et 46 % de tres sombres (12,7:1 sur le papier). Un `invert` en
   sombre a ete essaye et rejete — sur un visage, ca donne un negatif. */
.cv-portrait img {
  display: block;
  width: 100%;
  height: auto;
}

/* --- ossature des sections ----------------------------------------------- */

.cv-section {
  display: grid;
  grid-template-columns: var(--cv-gutter) minmax(0, 1fr);
  gap: 0 var(--cv-gap);
  border-top: 1px solid var(--rule);
  padding: clamp(1.75rem, 3vw, 2.5rem) 0;
}

.cv-section-head {
  position: sticky;
  top: 1.25rem;
  align-self: start;
}

.cv-section-title {
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.005em;
  margin: 0;
  color: var(--fg);
  text-wrap: balance;
}

.cv-section-note {
  font-size: var(--fs-md);
  color: var(--muted-soft);
  margin: 0.4rem 0 0;
  line-height: 1.45;
}

.cv-section-body {
  display: flex;
  flex-direction: column;
  gap: clamp(1.6rem, 2.6vw, 2.1rem);
  min-width: 0;
}

/* --- une entree ----------------------------------------------------------- */

.cv-entry {
  break-inside: avoid;
  min-width: 0;
}

.cv-entry-head {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
}

.cv-entry-id {
  flex: 1 1 auto;
  min-width: 0;
}

.cv-mark {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  object-fit: contain;
  margin-top: 0.1rem;
}

.cv-mark-pair { display: block; }
.cv-mark-pair img { display: block; width: 100%; height: 100%; }

.cv-role {
  font-size: var(--fs-xl);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0;
  font-weight: 600;
  text-wrap: balance;
}

.cv-role-sm { font-size: var(--fs-lg); }

.cv-role a,
.cv-course a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease;
}

.cv-role a:hover,
.cv-course a:hover { text-decoration-color: var(--accent); }

/* The online course, one line at the head of the talks: it sits first in the
   column's flex gap, so it drops the description's own top margin. */
.cv-course { margin-top: 0; }

/* Le genre d'intervention, au-dessus du titre : une formation et une
   conference ne se lisent pas pareil, et c'est la premiere chose a savoir. */
.cv-role-kind {
  font-size: var(--fs-3xs);
  font-weight: 600;
  color: var(--accent);
  margin: 0 0 0.2rem;
  letter-spacing: 0.01em;
}

.cv-org {
  /* Pas de glyphe entre les elements : ni point median, ni barre, ni tiret.
     C'est l'ECART qui separe, et la couleur qui hierarchise — l'employeur se
     lit d'abord, le lieu et le secteur derriere. Une rangee flex plutot qu'une
     marge sur chaque element, pour que le retour a la ligne ne laisse pas un
     renfoncement parasite au debut de la seconde rangee. */
  /* Un element par ligne. L'espace seul ne separait pas assez : « CEPOL
     Training on Lawful Decryption Budapest, Hungary » se lisait comme une
     seule chaine. Le retour a la ligne separe sans rien ajouter entre les
     mots, ce qui evite aussi bien le point median que la barre. */
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0.25rem 0 0;
  line-height: 1.45;
}

.cv-org span:first-child {
  color: var(--fg-soft);
}

.cv-dates {
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  color: var(--muted);
  margin: 0.25rem 0 0;
  text-align: right;
  /* Une colonne de dates ne s'aligne qu'avec des chiffres de chasse fixe. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cv-entry[data-current="true"] .cv-dates { color: var(--accent); font-weight: 600; }

.cv-desc {
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.6;
  margin: 0.7rem 0 0;
  max-width: var(--measure);
  text-wrap: pretty;
}

.cv-points {
  margin: 0.7rem 0 0;
  padding: 0;
  list-style: none;
  max-width: var(--measure);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.cv-points li {
  position: relative;
  padding-left: 1.05rem;
  font-size: var(--fs-md);
  color: var(--muted);
  line-height: 1.55;
  text-wrap: pretty;
}

/* Un filet, pas une puce ronde : la page n'a pas d'autre forme ronde. */
.cv-points li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.65em;
  width: 0.5rem;
  height: 1px;
  background: var(--rule-strong);
}

.cv-credential {
  font-size: var(--fs-xs);
  color: var(--muted-soft);
  margin: 0.5rem 0 0;
}

.cv-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.8rem 0 0;
  padding: 0;
  list-style: none;
}

.cv-tag {
  font-size: var(--fs-3xs);
  color: var(--chip-fg);
  background: var(--rule);
  border-radius: var(--r-md);
  padding: 0.2rem 0.45rem;
  line-height: 1.4;
}

.cv-entry-tight .cv-role { font-weight: 500; }

/* Un BADGE accredite et une formation de trois jours ne se lisent pas au meme
   niveau : la section les separe sans changer de titre. */
.cv-subhead {
  font-size: var(--fs-3xs);
  font-weight: 600;
  color: var(--muted-soft);
  letter-spacing: 0.01em;
  margin: 0.5rem 0 -0.4rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}

/* --- conferences et formations ------------------------------------------- */

.cv-talk {
  display: grid;
  grid-template-columns: 104px minmax(0, 1fr);
  gap: 1.15rem;
  align-items: start;
}

.cv-talk-photo {
  width: 104px;
  height: 128px;
  object-fit: cover;
  object-position: center 32%;
  border-radius: var(--r-md);
  border: 1px solid var(--rule);
  /* La photo est une preuve, pas le sujet : elle recule d'un cran. */
  filter: grayscale(1);
  opacity: 0.88;
  transition: filter 220ms ease, opacity 220ms ease;
}

.cv-talk:hover .cv-talk-photo { filter: grayscale(0); opacity: 1; }
.cv-talk-text { min-width: 0; }

/* --- competences ---------------------------------------------------------- */

.cv-skills {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

.cv-skill-row {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: 0.35rem 1rem;
  align-items: baseline;
  break-inside: avoid;
}

.cv-skill-name {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted-soft);
  margin: 0;
}

.cv-skill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.55rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--fg-soft);
}

/* La ponctuation va APRES l'item, jamais avant le suivant : un separateur pose
   en ::before ouvre la ligne des que la liste passe a la ligne, ce qui s'est vu
   sur trois des six familles. Une virgule reste collee au mot qui la precede. */
.cv-skill-list li:not(:last-child)::after {
  content: ",";
  color: var(--muted-soft);
}

/* --- diplomes scannes ----------------------------------------------------- */

.cv-credential-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1.1rem;
}

.cv-credential-card {
  margin: 0;
  min-width: 0;
}

.cv-credential-card a {
  display: block;
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  overflow: hidden;
  background: #fff;
  transition: border-color 180ms ease;
}

.cv-credential-card a:hover { border-color: var(--rule-strong); }

.cv-credential-card img {
  display: block;
  width: 100%;
  height: 132px;
  object-fit: cover;
  object-position: top center;
}

.cv-credential-card figcaption {
  font-size: var(--fs-3xs);
  color: var(--muted);
  margin-top: 0.45rem;
  line-height: 1.45;
  text-wrap: pretty;
}

/* --- la marque d'un employeur suit le theme, comme la bande de l'accueil --- */

html.dark .cv-mark-keyed { filter: invert(1); opacity: 0.88; }

@media (prefers-color-scheme: dark) {
  html:not(.light) .cv-mark-keyed { filter: invert(1); opacity: 0.88; }
}

/* --- etroit --------------------------------------------------------------- */

@media (max-width: 860px) {
  .cv-section {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.1rem;
  }
  .cv-section-head { position: static; }
  .cv-section-title { color: var(--muted-soft); }
}

@media (max-width: 620px) {
  .cv-hero {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Le portrait passe AVANT le nom quand la colonne est unique : sur un
     telephone, l'ordre visuel d'un CV commence par le visage. */
  .cv-portrait { grid-row: 1; }
  .cv-entry-head { flex-wrap: wrap; }
  .cv-dates {
    order: 3;
    width: 100%;
    text-align: left;
    margin-top: 0.4rem;
  }
  .cv-talk {
    grid-template-columns: minmax(0, 1fr);
  }
  /* 🔴 Une photo PORTRAIT ne se met pas dans une boite PAYSAGE. La pleine
     largeur donnait 350x190 pour des originaux en 717x1000 : `cover` n'en
     montrait qu'une bande de 39 % de la hauteur, et les visages sautaient.
     Sur bureau la boite fait 104x128, presque le rapport d'origine, d'ou
     l'absence de defaut la-bas. On garde donc le meme rapport ici, en plus
     grand. Mesure : la rogne passe de 61 % a 11 % de la hauteur. */
  .cv-talk-photo { width: 132px; height: 164px; }
  .cv-skill-row { grid-template-columns: minmax(0, 1fr); }
}

/* --- impression ----------------------------------------------------------- */
/* Le PDF, c'est cette page. Rien d'autre a maintenir. */

@media print {
  /* Sur papier il n'y a pas de theme : on repose la palette claire, sinon un
     lecteur en mode sombre imprime une page noire. */
  :root, html.dark, html.light {
    --bg: #ffffff;
    --bg-soft: #ffffff;
    --bg-card: #ffffff;
    --fg: #000000;
    --fg-soft: #1a1a1a;
    --muted: #3d3d3d;
    --muted-soft: #555555;
    --rule: #c8c8c8;
    --rule-strong: #888888;
    --accent: #1b3a8a;
    --chip-fg: #333333;
    color-scheme: light;
  }

  @page {
    margin: 14mm 14mm 16mm;
  }

  html, body {
    background: #fff !important;
    color: #000;
  }

  /* La chrome du site n'a rien a faire sur un CV imprime. */
  .nav, .footer, .skip-link, .cv-actions, .newsletter-backdrop,
  .search-backdrop, #back-to-top, .cv-section-note { display: none !important; }

  /* Les scans sont une piece jointe, pas une page de CV : neuf images
     pleine largeur ajouteraient trois feuilles a un document de deux. */
  .cv-section[aria-labelledby="credentials"] { display: none !important; }

  /* Les photos de conference aussi : le texte porte l'information. */
  .cv-talk-photo { display: none !important; }
  .cv-talk { grid-template-columns: minmax(0, 1fr); }

  .cv-main {
    max-width: none;
    padding: 0;
    --cv-gutter: 7rem;
    --cv-gap: 1.5rem;
  }

  .cv-hero { padding: 0 0 0.9rem; break-after: avoid; }
  .cv-name { font-size: 22pt; }
  .cv-headline { font-size: 11pt; }
  .cv-lede { font-size: 9pt; color: #333; }
  .cv-portrait { width: 84px; }

  .cv-section {
    padding: 0.85rem 0;
    break-inside: auto;
  }
  .cv-section-head { position: static; }
  .cv-section-title { font-size: 9.5pt; break-after: avoid; }

  .cv-section-body { gap: 0.85rem; }
  .cv-role { font-size: 11pt; }
  .cv-role-sm { font-size: 10pt; }
  .cv-org, .cv-dates { font-size: 8.5pt; }
  .cv-org { line-height: 1.3; }
  .cv-desc { font-size: 9pt; margin-top: 0.35rem; }
  .cv-points { margin-top: 0.35rem; gap: 0.15rem; }
  .cv-points li { font-size: 8.5pt; line-height: 1.4; }
  .cv-facts { margin-top: 0.5rem; }
  .cv-links { margin-top: 0.5rem; }
  .cv-link { font-size: 8.5pt; color: #000; }

  /* Les pastilles de mots-cles sont un outil de survol a l'ecran. Sur papier
     elles remplissent des lignes sans rien apprendre a qui lit deja la liste
     des faits juste au-dessus. */
  .cv-tags { display: none !important; }

  .cv-entry, .cv-skill-row { break-inside: avoid; }
  .cv-mark { width: 24px; height: 24px; }

  /* 🔴 Le papier est blanc quel que soit le theme AFFICHE, et le theme survit a
     l'impression : la classe `dark` reste sur <html>. Sans ces trois lignes,
     `html.dark .brand-logo-light { display: none }` masquait la marque encre et
     laissait imprimer la variante BLANCHE — invisible sur la feuille. Trouve en
     produisant le PDF depuis la page en thème sombre, pas en la relisant. */
  .brand-logo-light { display: block !important; }
  .brand-logo-dark { display: none !important; }
  .cv-mark-keyed { filter: none !important; opacity: 1 !important; }

  a { text-decoration: none; color: inherit; }
}

/* --- le parcours, pleine largeur au-dessus de l'experience ---------------
   Le modele de la frise d'Adam (2026-11-13) : un axe des annees en haut, une
   rangee par organisation, ses periodes en barres a l'echelle ; les postes a
   l'encre, ce qui court a cote d'un poste en gris (le gris des graphiques de la
   charte, mi-chemin entre l'encre et la page), les diplomes en petits carres.
   build.py ecrit les positions en parts de l'axe (--a, --w, --x) et le nombre
   d'annees (--years) pour le quadrillage, une ligne toutes les deux annees, la
   ou l'axe met ses etiquettes. */
.cv-career {
  border-top: 1px solid var(--rule);
  padding: clamp(1.75rem, 3vw, 2.5rem) 0;
}

.cv-gantt {
  --name-w: 13rem;
  margin: 0;
}

.cv-gantt-axis,
.cv-gantt-row {
  display: grid;
  grid-template-columns: var(--name-w) minmax(0, 1fr);
  column-gap: 1rem;
  align-items: center;
}

.cv-gantt-scale {
  position: relative;
  height: 1.5rem;
}

.cv-gantt-scale span {
  position: absolute;
  left: var(--x);
  bottom: 0.3rem;
  transform: translateX(-50%);
  font-size: var(--fs-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.cv-gantt-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

.cv-gantt-row {
  min-height: 2.2rem;
}

.cv-gantt-name {
  padding: 0.3rem 0;
  font-size: var(--fs-md);
  line-height: 1.3;
  color: var(--fg);
}

.cv-gantt-plot {
  position: relative;
  align-self: stretch;
  background: linear-gradient(to right, var(--rule) 1px, transparent 1px) 0 0 / calc(200% / var(--years)) 100% repeat-x;
}

.cv-gantt-bar {
  position: absolute;
  top: 50%;
  left: var(--a);
  width: var(--w);
  height: 0.5rem;
  margin-top: -0.25rem;
  background: var(--fg);
  border-radius: var(--r-md);
}

.cv-gantt-row.is-side .cv-gantt-bar {
  background: color-mix(in srgb, var(--fg) 50%, var(--bg));
}

.cv-gantt-dot {
  position: absolute;
  top: 50%;
  left: var(--a);
  width: 0.6rem;
  height: 0.6rem;
  margin: -0.3rem 0 0 -0.3rem;
  background: var(--bg);
  border: 1px solid var(--fg);
  border-radius: var(--r-md);
}

.cv-gantt-caption {
  margin-top: 0.9rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
}

@media (max-width: 560px) {
  .cv-gantt {
    --name-w: 7.5rem;
  }
  .cv-gantt-axis,
  .cv-gantt-row {
    column-gap: 0.6rem;
  }
  .cv-gantt-name {
    font-size: var(--fs-sm);
  }
}

@media print {
  .cv-gantt-plot,
  .cv-gantt-bar,
  .cv-gantt-dot {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cv-career { break-inside: avoid; }
}

/* --- produits maison sur l'accueil ---------------------------------------
   Une entree par produit. Le bloc reprend la facture des piliers de services :
   fond de carte, filet d'un pixel, pas d'ombre. La seule couleur de la page
   qui ne vienne pas des jetons du site est le point azur de dotID, et c'est
   voulu : la marque du produit n'est pas celle du laboratoire. */

.product-list {
  list-style: none;
  margin: clamp(1.5rem, 2.5vw, 2rem) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

@media (max-width: 720px) {
  .product-list { grid-template-columns: 1fr; }
}

.product {
  background: var(--bg-card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: clamp(1.4rem, 2.5vw, 1.9rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.85rem;
}

.product-head {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.product-mark {
  height: 30px;
  width: auto;
  flex-shrink: 0;
}

/* Une icone d'app iOS est un carre plein cadre : elle garde son coin arrondi
   d'iOS, pas le coin carre du site. */
.product-mark-app { border-radius: 22%; }

.product-id { min-width: 0; }

.product-name {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

.product-role {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0.15rem 0 0;
}

.product-desc {
  font-size: var(--fs-md);
  color: var(--fg-soft);
  line-height: 1.6;
  margin: 0;
  max-width: var(--measure);
  text-wrap: pretty;
}

/* Deux cartes cote a cote : le lien descend en bas de la sienne, alignes. */
.product-link {
  margin-top: auto;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--fg);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  padding: 0.45rem 0.85rem;
  transition: background-color 160ms ease, color 160ms ease;
}

.product-link:hover {
  background: var(--fg);
  color: var(--bg);
}

/* The online course, first of the products and as wide as the two under it
   (build.py render_home_course): the card's head, role line, description and
   outline link like its neighbours, plus the course's topics, three figures,
   the certificate in words and a specimen of it beside the text. It was a band
   of its own under the services until 2026-11-13. */
.product-course {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(1.75rem, 4vw, 3rem);
  align-items: center;
}

.product-course-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.95rem;
  min-width: 0;
}

.product-course .product-head {
  gap: 1.1rem;
}

.product-course .product-name {
  font-size: var(--fs-2xl);
  letter-spacing: -0.015em;
  line-height: 1.2;
  text-wrap: balance;
}

/* Three across at every width: two by two, the third fact sat alone on a row. */
.course-facts.product-course-facts {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  margin: 0.1rem 0 0;
}

/* The link and the price on one row, the price wrapping under it on a phone. */
.product-course-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  margin-top: 0.15rem;
}

.product-course-foot .product-link {
  margin-top: 0;
}

.product-course-figure {
  margin: 0;
  min-width: 0;
}

/* A picture of a paper document. It stays paper in the dark theme, with the
   same edge as the course page's specimen (.course-cert img), so the two pages
   frame it alike. */
.product-course-figure img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}

.product-course-figure figcaption {
  margin-top: 0.75rem;
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.5;
}

/* The specimen's link to its public page: links inherit their colour
   site-wide, so it needs an underline to be seen as a link. */
.product-course-figure figcaption a {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--muted-soft);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: text-decoration-color 160ms ease, text-decoration-thickness 160ms ease;
}

.product-course-figure figcaption a:hover {
  text-decoration-color: var(--fg);
  text-decoration-thickness: 2px;
}

/* One column: the specimen follows the text, in source order. Its caption
   holds a link, and a figure moved up would put that link ahead of the course
   link on screen and after it in the tab order. */
@media (max-width: 820px) {
  .product-course {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.5rem;
  }
  .product-course-figure {
    max-width: 34rem;  /* a tablet's full column made the specimen the largest thing on the page */
  }
}

/* On a phone the mark goes above the name, as on the course page's hero: beside
   it, the name ran to four lines with the mark centred on them. */
@media (max-width: 560px) {
  .product-course .product-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.9rem;
  }
  .product-course .course-mark {
    width: 3.75rem;
    font-size: var(--fs-xl);
  }
  .course-facts.product-course-facts {
    gap: 0.5rem;
  }
  .product-course-facts .course-fact {
    padding: 0.85rem 0.8rem;
  }
}

/* ---------------------------------------------------------------------------
   Index de symboles (/reference/)
   Une page-outil : on la consulte, on ne la lit pas. D'où une typographie de
   liste dense, le symbole en tête de ligne comme dans un index de fin
   d'ouvrage, et la phrase de contexte en retrait pour qu'on balaie les
   symboles sans que la prose n'accroche l'œil.
   --------------------------------------------------------------------------- */

.ref-count {
  font-size: var(--fs-2xs);
  color: var(--muted);
  letter-spacing: 0.01em;
  margin-top: 0.5rem;
}

/* La barre colle sur toute la page parce qu'elle partage sa <section> avec
   les groupes (seule dans la sienne, elle défilait avec le premier écran).
   Elle tient sur une ligne, jamais deux : repliée (fenêtre de moins de
   900 px à la souris), elle dépassait la marge de défilement de 5rem et une
   ancre atterrissait sous elle, et la dernière ligne étirait X et Z sur un
   tiers de l'écran. Quand la place manque, la ligne défile de côté ; quand
   il y en a, les lettres se partagent la largeur. Au doigt, voir plus bas. */
.ref-jump {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: thin;
  gap: 0.25rem;
  position: sticky;
  /* Les encoches du téléphone ; 0 seul collerait la barre sous le capteur. */
  top: env(safe-area-inset-top, 0px);
  z-index: 2;
  padding: 0.6rem 0;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.ref-jump a {
  flex: 1 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  padding: 0.3rem 0.45rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xs);
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  text-decoration: none;
}

.ref-jump a:hover,
.ref-jump a:focus-visible {
  color: var(--bg);
  background: var(--fg);
  border-color: var(--fg);
}

/* Au doigt, chaque lettre fait 44 px et la barre reste sur une ligne, qu'on
   fait défiler de côté : collante sur trois lignes, elle prenait un septième
   d'un écran de téléphone. Sur un téléphone elle court d'un bord à l'autre,
   et la lettre coupée au bord dit qu'elle défile. */
@media (hover: none) {
  .ref-jump {
    scrollbar-width: none;
  }

  .ref-jump::-webkit-scrollbar {
    display: none;
  }

  .ref-jump a {
    flex: none;
    min-width: 44px;
    min-height: 44px;
  }
}

@media (max-width: 560px) {
  .ref-jump {
    margin-inline: calc(-1 * var(--pad-x));
    padding-inline: var(--pad-x);
  }
}

/* La section qui porte la barre et les groupes ne trace pas la règle de
   fin d'une .service-section : la dernière entrée a déjà la sienne, et la
   note qui suit ouvre sa propre section. */
.ref-body {
  padding-bottom: 0;
  border-bottom: 0;
}

.ref-group {
  margin-top: 2.4rem;
  /* La barre de lettres est collante : sans cette marge, une ancre atterrit
     sous elle et le lecteur croit que le lien n'a rien fait. 5rem couvre la
     barre au doigt (44 px plus ses marges). */
  scroll-margin-top: 5rem;
}

/* Un titre reste à l'encre et sans espacement de capitales (DESIGN.md) : la
   lettre était en bleu marine, espacée de 0.08em. */
.ref-letter {
  font-size: var(--fs-sm);
  letter-spacing: 0.01em;
  color: var(--fg);
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--rule);
}

.ref-list {
  margin: 0;
}

.ref-entry {
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--rule);
}

/* L'ancre d'un symbole est sur son <dt> : c'est lui qui doit s'arrêter sous la
   barre collante, en tête de l'écran. */
.ref-entry dt {
  margin: 0 0 0.3rem;
  scroll-margin-top: 5rem;
}

.ref-entry dt code {
  font-size: var(--fs-xs);
  word-break: break-all;      /* un symbole long ne doit pas élargir la page */
}

.ref-entry dd {
  margin: 0;
}

.ref-context {
  margin: 0 0 0.25rem;
  font-size: var(--fs-xs);
  color: var(--fg-soft);
  /* a context line can hold a hash or a path with no space: it wraps anywhere rather than widen the page
     (a 64-hex digest pushed the page 73 px past a 390 px phone); word-break too, for Safari's min-content */
  overflow-wrap: anywhere;
  word-break: break-word;
}

.ref-source {
  font-size: var(--fs-2xs);
  color: var(--muted);
}

@media (min-width: 46rem) {
  /* Deux colonnes seulement quand il y a la place : le symbole tient la
     gouttière de gauche, comme dans un index imprimé. */
  .ref-entry {
    display: grid;
    grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
    gap: 0 1.5rem;
    align-items: baseline;
  }
  .ref-entry dt { margin-bottom: 0; }
}

.pricing + .contact-note {
  margin-top: 0.85rem;
}

/* A post image links to its file (post template): no underline, no change at rest. */
.post-body a.post-img-link {
  display: inline-block;
  border-bottom: 0;
}
