/* Ce fichier ne contient que des imports, et l'ordre compte :
   1. tokens     — les variables, rien d'autre
   2. fonts      — déclaration des polices auto-hébergées
   3. base       — remise à zéro et éléments natifs
   4. layout     — structure générale de la page
   5. components — un fichier par bloc visuel */

/* ===== tokens.css ===== */
/* Toutes les couleurs, tailles, espacements et durées du site sont définis ici.
   Aucun autre fichier ne doit contenir une couleur ou une durée écrite en dur. */

:root {
  /* light-dark() choisit la première valeur en thème clair, la seconde en thème sombre.
     Cela évite de recopier toute la palette dans un second bloc : une seule ligne par
     rôle, les deux thèmes côte à côte.

     color-scheme vaut « dark » et non « light dark » : le site s'affiche en sombre pour
     tout le monde au premier chargement, même chez un visiteur dont le système est réglé
     en clair. C'est un choix assumé ; le bouton de l'en-tête permet de passer en clair,
     et ce choix est ensuite mémorisé. */
  color-scheme: dark;

  /* Couleurs — direction artistique « Blueprint d'atelier ».
     Contrastes vérifiés selon WCAG 2.1, tous supérieurs à 4.5:1 sur le fond. */
  --color-surface: light-dark(#f2efe9, #0d1826); /* papier / bleu de plan */
  --color-raised: light-dark(#fbfaf7, #142334); /* cartes, blocs en relief */
  --color-text: light-dark(#16283f, #dfe6ee); /* 12.98:1 / 14.20:1 */
  --color-muted: light-dark(#56616f, #9aa8b8); /*  5.49:1 /  7.37:1 */
  --color-accent: light-dark(#b3202b, #e5626b); /*  5.79:1 /  5.35:1 — annotations */
  --color-line: light-dark(#2c6e9b, #6fb3d9); /*  4.80:1 /  7.76:1 — traits techniques */
  --color-rule: light-dark(#c3ccd7, #2a3b4f); /* filets et séparateurs */
  --color-grid: light-dark(#dfe5ec, #14202e); /* quadrillage du papier millimétré */
  /* L'écran de l'ordinateur est toujours sombre, dans les deux thèmes : c'est un écran.
     Ses couleurs ne passent donc pas par light-dark(). */
  --color-screen: light-dark(#16283f, #050b12);
  --color-screen-text: light-dark(#f2efe9, #dfe6ee);
  --color-screen-dim: #8fa2b8; /* 5.5:1 sur l'écran clair, 7.4:1 sur le sombre */
  --color-screen-scan: rgb(255 255 255 / 3%); /* trame très faible de balayage */

  /* Typographie. Les polices auto-hébergées sont citées en premier ; tant qu'elles ne
     sont pas installées (voir assets/fonts/README.md), le navigateur prend la suivante. */
  --font-display: "Archivo", ui-sans-serif, "Segoe UI", system-ui, sans-serif;
  --font-body: "IBM Plex Sans", ui-sans-serif, "Segoe UI", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* Échelle typographique : tierce majeure (x1.25), fluide entre 320 px et 1440 px. */
  --step--1: clamp(0.83rem, 0.81rem + 0.10vw, 0.89rem);
  --step-0: clamp(1rem, 0.97rem + 0.15vw, 1.09rem);
  --step-1: clamp(1.25rem, 1.19rem + 0.29vw, 1.42rem);
  --step-3: clamp(1.95rem, 1.77rem + 0.88vw, 2.44rem);
  --step-4: clamp(2.44rem, 2.14rem + 1.46vw, 3.2rem);
  /* Hors échelle : réservé au seul nom de la page d'accueil. */
  --step-5: clamp(2.6rem, 1.6rem + 4.6vw, 4.75rem);

  --leading-tight: 1.15;
  --leading-body: 1.6;
  --measure: 62ch; /* largeur de lecture confortable */

  /* Espacements : tous multiples de 8 px, comme la grille du fond. */
  --grid-unit: 8px;
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2rem;
  --space-5: 3rem;
  --space-6: 4rem;
  --space-7: 6rem;
  --container: 72rem;

  /* Traits : un plan technique n'a ni coin arrondi ni ombre portée. */
  --border-hair: 1px;
  --border-thick: 2px;

  /* Mouvement. Aucune durée ne doit être écrite ailleurs que dans ce bloc. */
  --duration-instant: 1ms; /* apparition sèche, sans fondu */
  --duration-fast: 120ms; /* survol, changement d'état immédiat */
  --duration: 240ms; /* transition courante */
  --duration-slow: 600ms; /* apparition d'un bloc entier */
  --duration-entrance: 700ms; /* entrée en scène de la page d'accueil */
  --stagger: 100ms; /* décalage entre deux éléments qui entrent à la suite */
  --boot-start: 600ms; /* première ligne de la séquence de démarrage */
  --boot-step: 200ms; /* écart entre deux lignes de démarrage */
  --boot-end: 1500ms; /* fin de la séquence : le prompt apparaît */
  --type-step: 95ms; /* temps d'un caractère de la commande tapée à l'écran */
  --duration-blink: 1.1s; /* clignotement du curseur */
  --duration-bob: 1.8s; /* balancement de la flèche « faites défiler » */
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);

  /* Superpositions */
  --z-header: 10;
  --z-progress: 20;
  --z-skip: 30;
}

/* Les deux seuls effets du bouton clair/sombre : forcer le mode que light-dark() suivra. */
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

/* Repli pour les navigateurs antérieurs à 2024, qui ignorent light-dark() : ils
   recevraient des couleurs vides. On leur sert le thème sombre, celui par défaut. */
@supports not (color: light-dark(#000, #fff)) {
  :root {
    --color-surface: #0d1826;
    --color-raised: #142334;
    --color-text: #dfe6ee;
    --color-muted: #9aa8b8;
    --color-accent: #e5626b;
    --color-line: #6fb3d9;
    --color-rule: #2a3b4f;
    --color-grid: #14202e;
    --color-screen: #050b12;
    --color-screen-text: #dfe6ee;
  }
}

/* ===== fonts.css ===== */
/* Déclaration des polices auto-hébergées. Les fichiers vivent dans assets/fonts/,
   leurs licences aussi (voir assets/fonts/README.md).

   font-display: swap — le texte s'affiche tout de suite avec la police système, puis
   bascule sur la vraie police dès qu'elle est chargée. Sans cela, la page resterait
   blanche quelques centaines de millisecondes. */

@font-face {
  font-family: "Archivo";
  src: url("../assets/fonts/archivo-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../assets/fonts/ibm-plex-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../assets/fonts/ibm-plex-sans-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("../assets/fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ===== base.css ===== */
/* Remise à zéro minimale et mise en forme des éléments natifs. */

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

body,
h1,
h2,
h3,
h4,
p,
ul,
ol,
dl,
dd,
figure {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

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

/* Le défilement animé est une animation comme une autre : on la retire pour qui la refuse. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  min-height: 100vh;
  background-color: var(--color-surface);
  /* Papier millimétré : deux dégradés d'un pixel, sans aucune image à télécharger. */
  background-image:
    linear-gradient(to right, var(--color-grid) var(--border-hair), transparent var(--border-hair)),
    linear-gradient(to bottom, var(--color-grid) var(--border-hair), transparent var(--border-hair));
  background-size: calc(var(--grid-unit) * 4) calc(var(--grid-unit) * 4);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: var(--leading-body);
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--leading-tight);
  text-wrap: balance;
}

h1 {
  font-size: var(--step-4);
}

h2 {
  font-size: var(--step-3);
}

h3 {
  font-size: var(--step-1);
}

h4 {
  font-size: var(--step-0);
}

p {
  max-width: var(--measure);
  text-wrap: pretty;
}

strong,
b,
dt {
  font-weight: 600;
}

a {
  color: inherit;
  text-decoration-color: var(--color-line);
  text-underline-offset: 0.2em;
}

a:hover {
  text-decoration-color: var(--color-accent);
}

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

/* Un contour visible partout, mais seulement pour qui navigue au clavier. */
:focus-visible {
  outline: var(--border-thick) solid var(--color-accent);
  outline-offset: 3px;
}

::selection {
  background-color: var(--color-line);
  color: var(--color-surface);
}

/* Premier élément atteignable au clavier : il saute la navigation. */
.skip-link {
  position: absolute;
  z-index: var(--z-skip);
  top: var(--space-1);
  left: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border: var(--border-hair) solid var(--color-text);
  background-color: var(--color-raised);
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ===== layout.css ===== */
/* Structure de page : en-tête, sections, pied de page.
   Les blocs visuels ont leur propre fichier dans components/. */

.site-header {
  position: sticky;
  z-index: var(--z-header);
  top: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--border-hair) solid var(--color-rule);
  background-color: var(--color-surface);
}

.site-header__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.site-header__tools {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Une section = un repère numéroté en marge, un titre, un corps.
   Sur petit écran les trois se suivent ; à partir de 60rem le repère passe en marge
   gauche, comme la cote d'un plan technique. */
.section {
  display: grid;
  gap: var(--space-2);
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-7) var(--space-3);
  border-top: var(--border-hair) solid var(--color-rule);
}

.section__marker {
  color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
}

.section__title {
  text-transform: uppercase;
  letter-spacing: 0.01em;
}

.section__body {
  display: grid;
  min-width: 0;
  gap: var(--space-5);
  margin-top: var(--space-3);
}

/* Sur les très petits écrans, on récupère seize pixels de largeur utile. */
@media (max-width: 24rem) {
  .section,
  .site-header,
  .site-footer {
    padding-inline: var(--space-2);
  }
}

@media (min-width: 60rem) {
  .section {
    grid-template-columns: 5rem 1fr;
    column-gap: var(--space-4);
  }

  .section__marker {
    grid-row: 1;
    grid-column: 1;
    padding-top: 0.45em;
    text-align: right;
  }

  .section__title {
    grid-row: 1;
    grid-column: 2;
  }

  .section__body {
    grid-row: 2;
    grid-column: 2;
  }
}

.site-footer {
  display: grid;
  gap: var(--space-1);
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-5) var(--space-3);
  border-top: var(--border-hair) solid var(--color-rule);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

/* Page 404 : même grille que les sections, sans repère numéroté. */
.error-page {
  display: grid;
  gap: var(--space-3);
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-7) var(--space-3);
}

.error-page__code {
  color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: var(--step-4);
  line-height: 1;
}

/* ===== components/nav.css ===== */
/* En-tête du site : marque, navigation, sélecteur de langue, bouton de thème. */

.site-header__brand {
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.site-header__nav a {
  border-bottom: var(--border-hair) solid transparent;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.04em;
  text-decoration: none;
}

.site-header__nav a:hover,
.site-header__nav a:focus-visible {
  border-bottom-color: var(--color-accent);
}

/* Sélecteur de langue : un lien, pas un bouton — il fonctionne sans JavaScript. */
.lang-switch {
  padding: 0.1em 0.45em;
  border: var(--border-hair) solid var(--color-rule);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-decoration: none;
}

.lang-switch:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* Masqué par défaut : sans JavaScript ce bouton ne ferait rien, et un contrôle inerte
   est pire que pas de contrôle. La classe « js » est posée par le script du <head>,
   avant le premier rendu, donc le bouton n'apparaît jamais en sautillant. */
.theme-toggle {
  display: none;
}

.js .theme-toggle {
  display: grid;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: var(--border-hair) solid var(--color-rule);
  background-color: transparent;
  color: inherit;
  cursor: pointer;
  place-items: center;
}

.theme-toggle:hover {
  border-color: var(--color-accent);
}

/* Un disque à moitié rempli : la moitié pleine indique le thème actif. Aucun fichier
   d'icône n'est nécessaire, et la couleur suit automatiquement celle du texte. */
.theme-toggle__mark {
  width: 0.8rem;
  height: 0.8rem;
  border: var(--border-hair) solid currentColor;
  border-radius: 50%;
  background-image: linear-gradient(to right, currentColor 50%, transparent 50%);
}

/* ===== components/button.css ===== */
/* Bouton unique du site, réutilisé par le lien CV, la page 404 et l'ouverture de
   l'ordinateur. Un seul style, donc une seule chose à modifier. */

.button {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: var(--border-hair) solid currentColor;
  background-color: transparent;
  color: inherit;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.button:hover {
  background-color: var(--color-text);
  color: var(--color-surface);
}

@media (prefers-reduced-motion: reduce) {
  .button {
    transition: none;
  }
}

/* ===== components/chip.css ===== */
/* Étiquette encadrée, utilisée à la fois par les compétences et par les mots-clés du
   parcours. Un seul style pour les deux, donc une seule chose à changer. */

.chip {
  padding: 0.12em 0.6em;
  border: var(--border-hair) solid var(--color-rule);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.04em;
}

/* ===== components/hero.css ===== */
/* Page d'accueil : une planche de dessin technique plein écran.
   La scène de défilement (deux hauteurs d'écran) n'est activée que par laptop.js, et
   seulement sur grand écran : sans JavaScript, la section fait un écran et tout est lisible. */

.hero {
  position: relative;
}

.hero__sticky {
  position: relative;
  display: grid;
  min-height: calc(100svh - 3.5rem);
  padding: var(--space-6) var(--space-4) var(--space-6);
  align-content: center;
  justify-items: center;
  gap: var(--space-4);
}

@media (min-width: 48rem) {
  .hero.is-scrollable {
    min-height: 220svh;
  }

  .hero.is-scrollable .hero__sticky {
    position: sticky;
    top: 0;
    height: 100svh;
    min-height: 0;
  }
}

/* Cadre de planche : un filet fin, et des traits de coupe rouges aux quatre coins. */
.hero__frame {
  position: absolute;
  inset: var(--space-1);
  border: var(--border-hair) solid var(--color-rule);
  pointer-events: none;
}

.hero__tick {
  position: absolute;
  width: 1.2rem;
  height: 1.2rem;
  border: 0 solid var(--color-accent);
}

.hero__tick--tl {
  top: -1px;
  left: -1px;
  border-top-width: var(--border-thick);
  border-left-width: var(--border-thick);
}

.hero__tick--tr {
  top: -1px;
  right: -1px;
  border-top-width: var(--border-thick);
  border-right-width: var(--border-thick);
}

.hero__tick--bl {
  bottom: -1px;
  left: -1px;
  border-bottom-width: var(--border-thick);
  border-left-width: var(--border-thick);
}

.hero__tick--br {
  right: -1px;
  bottom: -1px;
  border-right-width: var(--border-thick);
  border-bottom-width: var(--border-thick);
}

.hero__identity {
  display: grid;
  justify-items: center;
  gap: var(--space-1);
  text-align: center;
}

.hero__eyebrow {
  color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.hero__name {
  font-size: var(--step-5);
  letter-spacing: -0.02em;
}

/* Ligne de cote : un filet, le lieu, un filet — comme la dimension d'une pièce. */
.hero__dimension {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: min(100%, 28rem);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero__dimension::before,
.hero__dimension::after {
  flex: 1;
  height: var(--border-hair);
  background-color: var(--color-rule);
  content: "";
}

.hero__headline {
  max-width: 44ch;
  color: var(--color-muted);
  font-size: var(--step-1);
  text-align: center;
}

.hero__controls {
  display: grid;
  justify-items: center;
  gap: var(--space-2);
}

.hero__hint {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
}

.hero__arrow {
  width: 0.55rem;
  height: 0.55rem;
  border-right: var(--border-hair) solid currentColor;
  border-bottom: var(--border-hair) solid currentColor;
  rotate: 45deg;
}

/* Cartouche du plan, en bas à droite. Masqué sous 60rem, faute de place. */
.hero__block {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  display: none;
  border: var(--border-hair) solid var(--color-rule);
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

.hero__block-row {
  display: grid;
  grid-template-columns: 6rem 10rem;
}

.hero__block-row + .hero__block-row {
  border-top: var(--border-hair) solid var(--color-rule);
}

.hero__block dt {
  padding: 0.3rem 0.6rem;
  border-right: var(--border-hair) solid var(--color-rule);
  color: var(--color-muted);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.hero__block dd {
  padding: 0.3rem 0.6rem;
}

@media (min-width: 60rem) {
  .hero__block {
    display: grid;
  }
}

/* Entrée en scène. Les éléments sont à leur état final par défaut : l'animation ne fait
   que les amener depuis un état masqué, donc en mouvement réduit tout est simplement
   déjà en place, sans une seule animation jouée. */
@media (prefers-reduced-motion: no-preference) {
  /* Le bloc entier est conditionné à l'interrupteur « heroEntrance » de
     src/js/config.js, que build.py inscrit sur la balise <html>. Le caractère & désigne
     le sélecteur parent : « & .hero__name » équivaut donc à
     « :root:not([data-anim-off~="heroEntrance"]) .hero__name ». Cela évite de répéter
     neuf fois la même condition. */
  :root:not([data-anim-off~="heroEntrance"]) {
    & .hero__frame {
      animation: hero-frame var(--duration-entrance) var(--ease-out) both;
    }

    & .hero__eyebrow {
      animation: hero-rise var(--duration-slow) var(--ease-out) var(--stagger) both;
    }

    & .hero__name {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 2)
        both;
    }

    & .hero__dimension {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 3)
        both;
    }

    & .hero__headline {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 4)
        both;
    }

    & .laptop {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 5)
        both;
    }

    & .hero__controls {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 7)
        both;
    }

    & .hero__block {
      animation: hero-rise var(--duration-entrance) var(--ease-out) calc(var(--stagger) * 8)
        both;
    }

    & .hero__arrow {
      animation: hero-bob var(--duration-bob) var(--ease-out) var(--boot-end) infinite;
    }
  }
}

@keyframes hero-frame {
  from {
    opacity: 0;
    transform: scale(0.985);
  }
}

@keyframes hero-rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}

@keyframes hero-bob {
  50% {
    transform: translateY(4px);
  }
}

/* ===== components/laptop.css ===== */
/* L'ordinateur portable, dessiné au trait — aucune image, aucun bitmap.
   Une seule variable pilote tout : --laptop-open, de 0 (fermé) à 1 (ouvert).
   Elle vaut 1 par défaut, donc sans JavaScript l'ordinateur est déjà ouvert et lisible. */

.laptop {
  --laptop-open: 1;

  display: grid;
  justify-items: center;
  width: min(100%, 34rem);
  /* Aucune perspective, volontairement : un plan technique se dessine en projection
     orthographique. Les arêtes parallèles le restent, rotateX ne fait qu'écraser la
     hauteur, et l'ordinateur ne peut donc jamais prendre une allure de trapèze. */
}

.laptop__lid {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  transform-origin: bottom center;
  /* -85deg : capot rabattu, réduit à une tranche fine de la même épaisseur apparente
     que la base — un ordinateur fermé. 0deg : écran vertical, ouvert.
     On s'arrête avant 90 degrés pile, où la tranche disparaîtrait complètement. */
  transform: rotateX(calc(-85deg + 85deg * var(--laptop-open)));
  /* Seul élément du site à porter will-change : c'est lui qui bouge à chaque image. */
  will-change: transform;
}

/* Dos du capot, visible seulement quand l'ordinateur est fermé. Un simple fondu croisé
   avec l'écran évite d'avoir à gérer les faces arrière en 3D. */
.laptop__shell {
  position: absolute;
  inset: 0;
  border: var(--border-thick) solid var(--color-line);
  background-color: var(--color-raised);
  opacity: calc(1 - var(--laptop-open));
}

.laptop__screen {
  position: absolute;
  display: grid;
  align-content: start;
  gap: 0.3em;
  padding: clamp(0.75rem, 2.2vw, 1.6rem);
  border: var(--border-thick) solid var(--color-line);
  background-color: var(--color-screen);
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0 3px,
    var(--color-screen-scan) 3px 4px
  );
  color: var(--color-screen-text);
  font-family: var(--font-mono);
  font-size: clamp(0.68rem, 1.5vw, 0.95rem);
  line-height: 1.7;
  inset: 0;
  opacity: var(--laptop-open);
  overflow: hidden;
}

.laptop__boot {
  display: grid;
  gap: 0.1em;
}

.laptop__boot-line {
  display: block;
  color: var(--color-screen-dim);
}

.laptop__prompt {
  margin-top: 0.6em;
  max-width: none;
}

.laptop__sigil {
  color: var(--color-accent);
}

/* La commande tapée. En police à chasse fixe, une unité ch vaut exactement un
   caractère : c'est ce qui permet de dévoiler le texte caractère par caractère et de
   faire suivre le curseur, sans jamais toucher à la largeur — donc sans recalcul de
   mise en page. */
.laptop__typed {
  display: inline-block;
  white-space: pre;
}

.laptop__cursor {
  display: inline-block;
  width: 1ch;
  height: 1em;
  background-color: currentColor;
  vertical-align: text-bottom;
}

/* Base : une plaque vue en perspective, avec le clavier et le pavé tactile en creux. */
.laptop__base {
  position: relative;
  width: 103%;
  height: clamp(2.2rem, 6vw, 3.6rem);
  border: var(--border-hair) solid var(--color-line);
  background-color: var(--color-raised);
  /* Pas de perspective() ici : le conteneur en applique déjà une. En cumuler deux
     écrase la base en coin de triangle. */
  transform: rotateX(58deg);
  transform-origin: top center;
}

.laptop__hinge {
  position: absolute;
  top: 0;
  right: 14%;
  left: 14%;
  height: var(--border-thick);
  background-color: var(--color-line);
}

.laptop__keyboard {
  position: absolute;
  border: var(--border-hair) solid var(--color-rule);
  inset: 20% 8% 42% 8%;
}

.laptop__trackpad {
  position: absolute;
  bottom: 8%;
  left: 50%;
  width: 22%;
  height: 26%;
  border: var(--border-hair) solid var(--color-rule);
  translate: -50% 0;
}

/* Séquence de démarrage : chaque ligne apparaît à son tour, la totalité tient en 1,5 s.
   Le rang de la ligne est fourni par build.py (--line), donc ajouter une ligne dans
   content/ui.json fonctionne sans toucher au CSS. */
@media (prefers-reduced-motion: no-preference) {
  /* Interrupteur « boot » de src/js/config.js. Surtout pas steps(1) ici : combiné à
     « both », il fige la valeur de départ au lieu de celle d'arrivée, et la ligne
     resterait invisible pour toujours. Une durée d'une milliseconde en linear donne la
     même apparition sèche, sans le piège. */
  :root:not([data-anim-off~="boot"]) .laptop__boot-line {
    animation: laptop-show var(--duration-instant) linear
      calc(var(--boot-start) + var(--line) * var(--boot-step)) both;
  }

  /* Interrupteur « typing ». Il couvre l'apparition du prompt, la frappe et le
     clignotement du curseur : les trois forment un seul effet, et le curseur porte deux
     animations dans une même déclaration, qu'on ne peut donc pas séparer proprement. */
  :root:not([data-anim-off~="typing"]) {
    & .laptop__prompt {
      animation: laptop-show var(--duration-instant) linear var(--boot-end) both;
    }

    /* Frappe : un pas de steps() par caractère, sur clip-path plutôt que sur la largeur. */
    & .laptop__typed {
      animation: laptop-type calc(var(--chars) * var(--type-step))
        steps(var(--chars)) var(--boot-end) both;
    }

    /* Le curseur part collé au « $ » et avance d'un caractère à chaque pas, en même temps
       que le texte se dévoile. Il ne se met à clignoter qu'une fois la frappe finie.
       steps(1) est ici exactement ce qu'il faut : pas de fondu, et l'animation est infinie
       donc sans état final à figer. */
    & .laptop__cursor {
      animation:
        laptop-caret calc(var(--chars) * var(--type-step))
          steps(var(--chars)) var(--boot-end) both,
        laptop-blink var(--duration-blink) steps(1)
          calc(var(--boot-end) + var(--chars) * var(--type-step)) infinite;
    }
  }
}

@keyframes laptop-type {
  from {
    clip-path: inset(0 100% 0 0);
  }

  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes laptop-caret {
  from {
    transform: translateX(calc(var(--chars) * -1ch));
  }

  to {
    transform: translateX(0);
  }
}

@keyframes laptop-show {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes laptop-blink {
  50% {
    opacity: 0;
  }
}

/* ===== components/terminal.css ===== */
/* Section « Qui suis-je » : un bloc terminal et la liste des compétences. */

.terminal {
  position: relative;
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
  border: var(--border-hair) solid var(--color-rule);
  background-color: var(--color-raised);
}

/* Équerre d'angle, comme un repère de découpe sur un plan. */
.terminal::after {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 0.8rem;
  height: 0.8rem;
  border-right: var(--border-thick) solid var(--color-line);
  border-bottom: var(--border-thick) solid var(--color-line);
  content: "";
}

.terminal__command {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}

.terminal__prompt {
  color: var(--color-accent);
}

.terminal__output {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.4rem var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

.terminal__output dt {
  color: var(--color-muted);
  font-weight: 400;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.terminal__bio {
  border-top: var(--border-hair) solid var(--color-rule);
  padding-top: var(--space-2);
}

.portrait {
  border: var(--border-hair) solid var(--color-rule);
}

.skills {
  display: grid;
  gap: var(--space-3);
  align-content: start;
}

.skills__title {
  font-size: var(--step-1);
}

.skills__group {
  display: grid;
  gap: var(--space-1);
  justify-items: start;
}

.skills__label {
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.skills__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* Deux colonnes à partir de 60rem : le terminal à gauche, les compétences à droite. */
@media (min-width: 60rem) {
  .section__body--split {
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    align-items: start;
    gap: var(--space-4);
  }
}

/* ===== components/timeline.css ===== */
/* Parcours : une ligne verticale qui descend, un nœud en losange par étape, et une
   boîte encadrée à droite. La ligne se trace et les boîtes montent quand elles entrent
   dans l'écran — voir src/js/components/timeline.js. */

.timeline {
  display: grid;
  gap: var(--space-4);
}

.timeline__item {
  position: relative;
  padding-left: 2.75rem;
}

/* Segment de ligne verticale. Chaque étape porte le sien : mis bout à bout, ils forment
   le fil conducteur, et il se trace du haut vers le bas au fil de la lecture. */
.timeline__item::before {
  position: absolute;
  top: 0.45rem;
  bottom: calc(-1 * var(--space-4));
  left: 0.5rem;
  width: var(--border-hair);
  background-color: var(--color-rule);
  transform-origin: top center;
  transition: transform var(--duration-slow) var(--ease-out);
  content: "";
}

.timeline__item:last-child::before {
  bottom: auto;
  height: 2rem;
}

.timeline__node {
  position: absolute;
  top: 0.45rem;
  left: 0.5rem;
  width: 0.6rem;
  height: 0.6rem;
  border: var(--border-thick) solid var(--color-line);
  background-color: var(--color-surface);
  rotate: 45deg;
  translate: -50% 0;
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out);
}

/* Une couleur de nœud par type d'étape. Ajouter un type = ajouter une ligne ici. */
.timeline__item--certification .timeline__node {
  border-color: var(--color-accent);
}

.timeline__item--projet .timeline__node {
  background-color: var(--color-line);
}

.timeline__item--emploi .timeline__node {
  rotate: 0deg;
}

.timeline__card {
  position: relative;
  display: grid;
  gap: var(--space-1);
  /* Sans cela, un mot-clé très long empêcherait la grille de rétrécir sous 320 px. */
  min-width: 0;
  overflow-wrap: break-word;
  padding: var(--space-3);
  border: var(--border-hair) solid var(--color-rule);
  background-color: var(--color-raised);
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out),
    border-color var(--duration) var(--ease-out);
}

/* Connecteur entre la ligne et la boîte. */
.timeline__card::before {
  position: absolute;
  top: 1.15rem;
  left: calc(-1 * var(--space-3));
  width: var(--space-3);
  height: var(--border-hair);
  background-color: var(--color-rule);
  content: "";
}

/* Équerre d'angle, le même repère que sur le bloc terminal. */
.timeline__card::after {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 0.8rem;
  height: 0.8rem;
  border-right: var(--border-thick) solid var(--color-line);
  border-bottom: var(--border-thick) solid var(--color-line);
  content: "";
}

.timeline__item:hover .timeline__card,
.timeline__item:focus-within .timeline__card {
  border-color: var(--color-line);
  transform: translateX(4px);
}

.timeline__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  align-items: baseline;
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

.timeline__kind {
  color: var(--color-accent);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.timeline__period {
  color: var(--color-muted);
  letter-spacing: 0.04em;
}

.timeline__title {
  font-size: var(--step-1);
}

.timeline__org {
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

.timeline__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-1);
}

.timeline__link {
  margin-top: var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--step--1);
}

/* État masqué avant l'entrée dans l'écran. Conditionné à la classe « js » : sans
   JavaScript, rien n'est jamais caché. */
.js .timeline__item:not(.is-visible)::before {
  transform: scaleY(0);
}

.js .timeline__item:not(.is-visible) .timeline__node {
  opacity: 0;
  transform: scale(0.2);
}

.js .timeline__item:not(.is-visible) .timeline__card {
  opacity: 0;
  transform: translateY(14px);
}

/* Le décalage entre le tracé de la ligne, l'allumage du nœud et la montée de la boîte
   donne l'impression que le fil alimente l'étape. */
.timeline__node {
  transition-delay: calc(var(--stagger) * 1.6);
}

.timeline__card {
  transition-delay: calc(var(--stagger) * 0.9);
}

@media (prefers-reduced-motion: reduce) {
  .timeline__item::before,
  .timeline__node,
  .timeline__card {
    transition: none;
  }
}

@media (min-width: 48rem) {
  .timeline__item {
    padding-left: 4rem;
  }

  .timeline__item::before,
  .timeline__node {
    left: 1rem;
  }

  .timeline__card::before {
    left: calc(-1 * var(--space-4));
    width: var(--space-4);
  }
}

/* ===== components/link-card.css ===== */
/* Grille de cartes vers les profils extérieurs. */

.link-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-2);
}

/* Repère de position pour la confirmation de copie. */
.link-grid__item {
  position: relative;
}

.link-card {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.15rem var(--space-2);
  height: 100%;
  padding: var(--space-3);
  border: var(--border-hair) solid var(--color-rule);
  background-color: var(--color-raised);
  text-decoration: none;
  transition:
    border-color var(--duration) var(--ease-out),
    transform var(--duration) var(--ease-out);
}

.link-card:hover,
.link-card:focus-visible {
  border-color: var(--color-line);
  transform: translateY(-3px);
}

/* Équerre d'angle, comme sur les autres blocs, révélée au survol. */
.link-card::after {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 0.8rem;
  height: 0.8rem;
  border-right: var(--border-thick) solid var(--color-line);
  border-bottom: var(--border-thick) solid var(--color-line);
  opacity: 0;
  transition: opacity var(--duration) var(--ease-out);
  content: "";
}

.link-card:hover::after,
.link-card:focus-visible::after {
  opacity: 1;
}

.link-card__icon {
  grid-row: 1 / span 2;
  align-self: start;
  color: var(--color-line);
}

.link-card__icon svg {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
}

.link-card__label {
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 700;
}

.link-card__description {
  color: var(--color-muted);
  font-size: var(--step--1);
}

/* Confirmation de copie. Elle n'a aucune classe d'état : le CSS se déclenche sur la
   présence de texte, que le JavaScript pose puis efface au bout de deux secondes. */
.link-card__feedback {
  position: absolute;
  z-index: 1;
  /* À cheval sur le bord haut de la carte : à l'intérieur, l'étiquette recouvrirait
     la fin de l'adresse. */
  top: -0.75em;
  right: var(--space-2);
  padding: 0.1em 0.55em;
  border: var(--border-hair) solid var(--color-line);
  background-color: var(--color-surface);
  color: var(--color-line);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration) var(--ease-out);
}

.link-card__feedback:not(:empty) {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .link-card,
  .link-card::after,
  .link-card__feedback {
    transition: none;
  }
}

/* ===== components/reveal.css ===== */
/* Apparition des blocs à leur entrée dans l'écran, et filet de progression de lecture.
   L'état masqué est conditionné à la classe « js » : sans JavaScript, rien n'est caché. */

.js [data-reveal="fade"] {
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out);
}

.js [data-reveal="fade"]:not(.is-visible) {
  opacity: 0;
  transform: translateY(10px);
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal="fade"] {
    transition: none;
  }
}

/* Filet de deux pixels tout en haut de la fenêtre. Purement indicatif, donc masqué aux
   lecteurs d'écran ; il n'anime qu'un scaleX, sans jamais recalculer la mise en page. */
.reading-progress {
  position: fixed;
  z-index: var(--z-progress);
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  pointer-events: none;
}

.reading-progress__bar {
  display: block;
  height: 100%;
  background-color: var(--color-accent);
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
}

