/* =============================================================================
   Portfolio — Louenn Penanc'hoat

   Ecrit a la main, sans framework et sans etape de build. Aucune ressource
   n'est chargee depuis un CDN : la page s'affiche entierement hors ligne.

   Une seule police est embarquee, pour les titres et les chiffres. Le corps de
   texte reste en police systeme : rien a telecharger, rendu immediat et familier.
   ============================================================================= */

/* ---------------------------------------------------------------- police */

/* Bricolage Grotesque, sous-ensemble latin, embarquee dans le depot plutot
   que chargee depuis un CDN. Fichier variable : un seul telechargement couvre
   les graisses 400 a 800 et l'axe de taille optique.
   SIL Open Font License 1.1 — voir fonts/OFL-bricolage.txt */
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("../fonts/bricolage-grotesque.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  /* `swap` : le texte s'affiche tout de suite dans la police systeme, la
     substitution se fait a l'arrivee du fichier. Rien n'est jamais invisible. */
  font-display: swap;
}

/* Source Serif 4 pour le texte courant et la navigation. Une serif sous un
   display grotesque : les deux polices se distinguent vraiment, la ou un
   second sans aurait donne l'impression d'une seule police a deux tailles.
   Celle-ci est dessinee pour l'ecran — elle tient a 17px la ou une serif de
   labeur devient fragile. Variable aussi : graisses 400 a 700 et taille
   optique dans un seul fichier.
   SIL Open Font License 1.1 — voir fonts/OFL-source-serif.txt */
@font-face {
  font-family: "Source Serif 4";
  src: url("../fonts/source-serif.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* Geist pour l'interface — la navigation et les boutons. Une grotesque
   neutre et un peu technique : elle ne cherche pas a etre lue longtemps, elle
   sert a reperer. Sous-ensemble latin, 16 Ko.
   SIL Open Font License 1.1 — voir fonts/OFL-geist.txt */
@font-face {
  font-family: "Geist";
  src: url("../fonts/geist.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------- jetons */

:root {
  /* Aligne les elements dessines par le navigateur — barres de defilement
     notamment — sur le theme affiche. */
  color-scheme: light;

  --bg: #faf9f7;
  --bg-sunk: #f2f0ec;
  --surface: #ffffff;
  --line: #e2ded7;
  --line-strong: #cdc7bd;
  --text: #14161a;
  --text-dim: #55585f;
  /* Verifie sur les deux fonds : 5,30:1 sur --bg et 4,90:1 sur --bg-sunk.
     Le seuil AA (4,5) s'applique aussi aux textes secondaires. La valeur
     d'origine, #86888f, n'etait qu'a 3,37. */
  --text-faint: #656870;
  --accent: #1f3ce8;
  --accent-hover: #1730b8;
  --accent-soft: #e8ebfd;
  --accent-fg: #ffffff;

  --halo-a: radial-gradient(circle, rgba(31, 60, 232, .17), transparent 68%);
  --halo-b: radial-gradient(circle, rgba(232, 108, 60, .15), transparent 68%);
  --grain-opacite: .34;
  --grain-melange: multiply;
  --ombre-projet: 0 26px 60px -30px rgba(20, 22, 26, .34);
  --ombre-projet-survol: 0 36px 80px -32px rgba(20, 22, 26, .42);

  --page: 1120px;
  --gap: clamp(72px, 9vw, 128px);

  --font-display: "Bricolage Grotesque", "Segoe UI", system-ui, sans-serif;
  --font-body: "Source Serif 4", Georgia, "Times New Roman", serif;
  --font-ui: "Geist", "Segoe UI", system-ui, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", SFMono-Regular, Consolas, monospace;
}

/* Le site est concu en clair : c'est un choix, la plupart des portfolios sont
   sombres. Mais un visiteur dont le systeme demande le sombre recoit cette
   variante plutot qu'une page eblouissante. Seuls les jetons changent, aucune
   forme ni aucun espacement.

   Chaque couple a ete verifie : le contraste le plus faible de cette palette
   est de 5,18:1, pour un seuil AA de 4,5. */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --bg: #12110f;
    --bg-sunk: #1a1917;
    --surface: #191816;
    --line: #2b2926;
    --line-strong: #3d3a35;
    --text: #f2f0ec;
    --text-dim: #b0aca5;
    --text-faint: #8f8b84;
    --accent: #8ea2ff;
    --accent-hover: #aab7ff;
    --accent-soft: #1c2340;
    --accent-fg: #0d0f1a;

    --halo-a: radial-gradient(circle, rgba(96, 122, 255, .20), transparent 68%);
    --halo-b: radial-gradient(circle, rgba(255, 138, 84, .13), transparent 68%);
    --grain-opacite: .20;
    --grain-melange: overlay;
    --ombre-projet: 0 26px 60px -28px rgba(0, 0, 0, .72);
    --ombre-projet-survol: 0 36px 84px -30px rgba(0, 0, 0, .85);
  }
}

/* ----------------------------------------------------------------- socle */

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

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 17px/1.6 var(--font-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, p, ul, ol, dl, dd, figure, table { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img { display: block; max-width: 100%; height: auto; }

a { color: inherit; }

/* La regle `[hidden] { display: none }` du navigateur vient de sa feuille par
   defaut : n'importe quelle regle d'auteur posant un `display` la supplante,
   quelle que soit sa specificite. Sans ce rappel, un element masque par
   l'attribut resterait visible. */
[hidden] { display: none !important; }

code {
  font-family: var(--font-mono);
  font-size: .88em;
  background: var(--bg-sunk);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: .08em .34em;
}

/* Lien d'evitement : premier element focusable, invisible tant qu'on ne
   navigue pas au clavier. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--text);
  color: var(--bg);
  padding: 12px 18px;
  text-decoration: none;
}
.skip:focus { left: 0; }

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

/* ------------------------------------------------------------ barre haute */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  /* Trois colonnes de largeur egale de part et d'autre : la navigation tombe
     au centre de la page quelle que soit la largeur de la marque ou du
     selecteur de langue. Un simple `space-between` la decalerait. */
  display: grid;
  grid-template-columns: 1fr minmax(0, auto) 1fr;
  align-items: center;
  gap: 24px;
  padding: 16px clamp(18px, 3vw, 40px);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid transparent;
  transition: border-color .25s, padding .25s;
}

/* Une fois le sommet quitte, la barre se detache et se resserre. */
.topbar.is-stuck {
  border-bottom-color: var(--line);
  padding-top: 11px;
  padding-bottom: 11px;
}

/* --- jauge de lecture */

.jauge {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  pointer-events: none;
}
.jauge span {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(var(--lu, 0));
  transform-origin: left;
  /* `scaleX` plutot qu'une largeur : la mise en page n'est jamais recalculee,
     le navigateur se contente de redessiner le calque. */
  will-change: transform;
}

/* --- marque */

/* Le pseudo GitHub plutot que le nom complet : le nom est deja ecrit en
   grand juste en dessous, et c'est sous ce pseudo que se trouvent les depots.
   Il porte la police des titres, pas celle de la navigation. */
.mark {
  display: inline-block;
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -.005em;
  font-size: 17.5px;
  white-space: nowrap;
}

/* --- navigation */

/* La colonne du milieu ne doit pas pousser la grille : sans `min-width: 0`,
   une liste plus large que l'ecran elargit la page entiere. */
.topbar nav { min-width: 0; }

.topbar nav ul {
  display: flex;
  gap: clamp(14px, 1.9vw, 24px);
  /* Sur un telephone, cinq entrees ne tiennent pas. Plutot que d'en cacher —
     il faudrait alors dupliquer des liens et le reperage de section perdrait
     sa cible — la liste defile. Le degrade a droite signale qu'il en reste. */
  overflow-x: auto;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.topbar nav ul::-webkit-scrollbar { display: none; }

@media (max-width: 860px) {
  .topbar nav ul {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
  }
}

.topbar nav a {
  display: block;
  padding: 4px 0;
  font-family: var(--font-ui);
  /* Reglages releves sur le site de reference : Geist 12px, graisse normale,
     0,24px d'interlettrage — soit .02em a cette taille. */
  font-size: 12px;
  font-weight: 400;
  letter-spacing: .02em;
  color: var(--text-dim);
  text-decoration: none;
  white-space: nowrap;
  transition: color .18s;
}

.topbar nav a:hover { color: var(--text); }

/* --- selecteur de langue */

.langues {
  display: flex;
  align-items: center;
  gap: 5px;
  justify-self: end;
}

.lng {
  padding: 4px 6px;
  border: 0;
  background: transparent;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: .06em;
  color: var(--text-faint);
  cursor: pointer;
  border-radius: 3px;
  transition: color .18s;
}
.lng:hover { color: var(--text); }

/* La langue active se distingue par la couleur d'accent, pas seulement par
   une graisse : `aria-pressed` porte deja l'information pour les lecteurs
   d'ecran, la couleur la rend visible. */
.lng[aria-pressed="true"] { color: var(--accent); }

.lng-sep { color: var(--line-strong); font-size: 11px; }

/* Section en cours de lecture, posee par js/main.js. La couleur est le seul
   marqueur visuel ; l'information reste portee par `aria-current`, donc
   annoncee aux lecteurs d'ecran sans dependre de la couleur. */
.topbar nav a[aria-current="true"] { color: var(--accent); }

/* --------------------------------------------------------------- accroche */

/* L'accroche sort de la colonne de contenu pour occuper toute la largeur et
   toute la hauteur de l'ecran. C'est la seule section a le faire : elle donne
   le ton, le reste du site revient a une mise en page sage. */
/* L'accroche est un panneau, pas une bande pleine largeur : le fond de page
   reste visible autour, ce qui la detache et donne un cadre au portrait.
   `overflow: hidden` fait que la photo vient mourir sur l'angle arrondi au
   lieu de deborder. */
.hero {
  position: relative;
  /* Gouttiere sur les petits ecrans, largeur de colonne au-dela. */
  width: calc(100% - 2 * clamp(12px, 3vw, 28px));
  max-width: var(--page);
  margin: 8px auto var(--gap);
  /* Hauteur d'ecran moins la barre et les marges, plafonnee : sur un tres
     grand ecran, un panneau plein cadre ne serait plus qu'un grand vide. */
  min-height: min(860px, calc(100svh - 104px));
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  /* En `vh` : sur un ecran bas, c'est la hauteur qui manque, pas la
     largeur — un remplissage calcule sur la largeur ne s'y adapte pas. */
  padding: clamp(34px, 4.4vh, 60px) 0 clamp(38px, 5vh, 64px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: clamp(18px, 2vw, 26px);
  box-shadow: 0 30px 70px -46px rgba(20, 22, 26, .5);
}

/* --- decor : deux halos et un grain, entierement dessines par le navigateur */

.hero-decor {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.halo {
  position: absolute;
  border-radius: 50%;
  /* Le flou est ce qui coute le plus cher a l'affichage : `will-change` sort
     ces deux elements dans leur propre calque, une fois pour toutes. */
  filter: blur(90px);
  will-change: transform;
  /* Deplacement suivant le pointeur, pose par js/main.js. Les valeurs par
     defaut valent zero : sans souris, les halos restent simplement immobiles. */
  transform: translate3d(calc(var(--px, 0) * 34px), calc(var(--py, 0) * 26px), 0);
  transition: transform .9s cubic-bezier(.22, .61, .36, 1);
}

.halo-a {
  width: 52vw;
  height: 52vw;
  max-width: 700px;
  max-height: 700px;
  top: -14vw;
  right: -8vw;
  background: var(--halo-a);
}

.halo-b {
  width: 42vw;
  height: 42vw;
  max-width: 560px;
  max-height: 560px;
  bottom: -12vw;
  left: -6vw;
  background: var(--halo-b);
  transform: translate3d(calc(var(--px, 0) * -22px), calc(var(--py, 0) * -18px), 0);
}

/* Grain : un bruit fractal genere par le navigateur, pas une image. Il casse
   le lisse des degrades, qui autrement se voit en bandes sur certains ecrans. */
.grain {
  position: absolute;
  inset: -50%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: var(--grain-opacite);
  mix-blend-mode: var(--grain-melange);
}

/* --- contenu */

.hero-inner {
  position: relative;
  z-index: 1;
  /* Occupe toute la hauteur utile du panneau : sans cela le bloc se centre,
     et le portrait cale « en bas » s'arrete au bas du bloc, pas au bord du
     panneau — il flottait alors, coupe net, au-dessus de 120px de vide. */
  flex: 1;
  width: 100%;
  margin: 0 auto;
  padding: 0 clamp(26px, 4.5vw, 60px);
}

/* Conteneur de requete : le titre se dimensionne alors sur la largeur de
   cette colonne, et non sur celle de la fenetre. C'est ce qui empeche
   « Penanc'hoat » — un mot insecable, qui ne peut pas se couper — de deborder
   sur le portrait quand la colonne se resserre. */
.hero-texte {
  min-width: 0;
  container-type: inline-size;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 18px;
}

h1 {
  font-family: var(--font-display);
  font-optical-sizing: auto;
  /* Repli pour les navigateurs sans unites de conteneur : calibre sur la
     fenetre, donc prudent. */
  font-size: clamp(52px, 9vw, 112px);
  /* `cqi` se lit sur la colonne de texte : le titre occupe toujours la meme
     part de la place disponible, quelle que soit la largeur prise par le
     portrait. Le `vh` le fait retomber sur un ecran bas, ou il mangerait
     sinon toute la hauteur. */
  font-size: clamp(52px, min(18cqi, 15vh), 140px);
  line-height: .84;
  letter-spacing: -.045em;
  font-weight: 700;
  margin-bottom: clamp(24px, 3.2vw, 44px);
}

.nom-ligne { display: block; }

/* La seconde ligne est decalee : le nom cesse d'etre un bloc et devient une
   composition. */
.nom-decale { margin-left: clamp(0px, 5cqi, 56px); }

.hero-bas {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(22px, 2.6vw, 34px);
  flex-wrap: wrap;
}

/* Assez large pour tenir en trois lignes plutot que quatre, sans venir
   toucher le portrait. */
.hero-dire { max-width: 54ch; }

/* --- portrait */

/* Cale en bas de la section, aligne a droite sur la meme colonne que le
   texte. `z-index` absent : l'element reste sous .hero-inner, qui est a 1,
   donc la deuxieme ligne du nom passe devant les cheveux. */
.hero-portrait {
  display: none;
  align-self: end;
  /* Le panneau a un remplissage bas ; cette marge negative le compense pour
     que la photo aille toucher le bord, ou l'angle arrondi la coupe. */
  margin-bottom: calc(-1 * clamp(38px, 5vh, 64px));
  pointer-events: none;
}

.hero-portrait img {
  width: auto;
  height: min(60vh, 560px);
  object-fit: contain;
  object-position: bottom;
  /* Le detourage laisse un bord net ; cette ombre tres etalee redonne un peu
     d'assise au sujet, qui autrement flotte sur le fond. */
  filter: drop-shadow(0 30px 50px rgba(20, 22, 26, .18));
}

/* Sur fond sombre, une ombre sombre ne separe plus rien : le fond se charge
   deja de detacher le sujet. */
@media (prefers-color-scheme: dark) {
  .hero-portrait img { filter: none; }
}

/* En dessous de cette largeur, le nom occupe deja toute la colonne : la photo
   n'aurait plus de place. La requete `media` sur la <source> evite en plus de
   la telecharger. */
@media (min-width: 1000px) {
  .hero-inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: stretch;
    column-gap: clamp(20px, 3vw, 44px);
  }
  /* Le texte se centre dans la hauteur, le portrait descend au bord. */
  .hero-texte { align-self: center; }
  .hero-portrait { align-self: end; }
  .hero-portrait { display: block; }
  .hero-bas { flex-direction: column; align-items: flex-start; }
}

.lede {
  font-size: clamp(17px, 1.55vw, 20px);
  line-height: 1.55;
  color: var(--text-dim);
}
.lede strong { color: var(--text); font-weight: 650; }

.seeking { margin-top: 18px; font-size: 17px; }
.seeking strong { color: var(--accent); font-weight: 650; }

.links { display: flex; flex-wrap: wrap; gap: 10px; }

.btn {
  display: inline-flex;
  align-items: center;
  height: 46px;
  padding: 0 22px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  /* Un bouton n'est pas du texte a lire : il porte la police d'interface,
     comme la navigation. */
  font-family: var(--font-ui);
  font-size: 14.5px;
  font-weight: 500;
  text-decoration: none;
  transition: border-color .18s, background .18s, color .18s, transform .18s;
}
.btn:hover { border-color: var(--text); transform: translateY(-2px); }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* --- invite a defiler */

/* Cale en bas du panneau plutot que place dans le flux : sa marge y occupait
   une centaine de pixels que le portrait ne pouvait pas franchir, et la photo
   se retrouvait coupee net au-dessus du bord. */
.cue {
  position: absolute;
  left: 50%;
  bottom: clamp(18px, 2.2vh, 28px);
  transform: translateX(-50%);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-faint);
  text-decoration: none;
  transition: color .18s;
}
.cue:hover { color: var(--accent); }

.cue svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: descendre 2.4s ease-in-out infinite;
}

@keyframes descendre {
  0%, 100% { transform: translateY(0); opacity: .75; }
  50%      { transform: translateY(5px); opacity: 1; }
}

/* --------------------------------------------------------------- sections */

.section {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 28px var(--gap);
}

.section-title {
  font-family: var(--font-display);
  font-size: clamp(30px, 4vw, 46px);
  line-height: 1.05;
  letter-spacing: -.022em;
  font-weight: 700;
  padding-bottom: 20px;
  margin-bottom: 44px;
  border-bottom: 1px solid var(--line);
}

.section-lede {
  font-size: 18px;
  color: var(--text-dim);
  max-width: 62ch;
  margin-bottom: 40px;
}
.section-lede a { color: var(--accent); text-underline-offset: 3px; }

/* ---------------------------------------------------------------- projets */

.project {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 78px);
  align-items: center;
  padding-bottom: clamp(64px, 8vw, 118px);
}

/* Un projet sur deux inverse ses colonnes : sans ca, deux cartes identiques
   se succedent et la section devient un formulaire. Le gabarit s'inverse
   avec elles : sans cette ligne, la colonne large restait a gauche et
   revenait donc au texte un projet sur deux — les captures n'avaient plus
   la meme largeur d'un projet a l'autre. */
.project:nth-of-type(even) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); }
.project:nth-of-type(even) .project-media { grid-column: 2; grid-row: 1; }
.project:nth-of-type(even) .project-body  { grid-column: 1; grid-row: 1; }

.project + .project {
  border-top: 1px solid var(--line);
  padding-top: clamp(64px, 8vw, 118px);
}

/* --- le visuel */

.project-media {
  display: block;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: var(--ombre-projet);
  /* Legere mise en perspective : la capture cesse d'etre un rectangle plat
     colle a la page. Elle se redresse au survol. */
  transform: perspective(1400px) rotateY(3.5deg) scale(.985);
  transition: transform .55s cubic-bezier(.22, .61, .36, 1),
              box-shadow .55s cubic-bezier(.22, .61, .36, 1);
}

.project:nth-of-type(even) .project-media {
  transform: perspective(1400px) rotateY(-3.5deg) scale(.985);
}

.project-media:hover,
.project-media:focus-visible {
  transform: perspective(1400px) rotateY(0) scale(1);
  box-shadow: var(--ombre-projet-survol);
}

.project-media img { width: 100%; }

/* --- le texte */

.project-num {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .12em;
  color: var(--accent);
  margin-bottom: 14px;
}

.project h3 {
  font-family: var(--font-display);
  font-optical-sizing: auto;
  font-size: clamp(30px, 3.6vw, 50px);
  line-height: 1;
  letter-spacing: -.03em;
  font-weight: 700;
}

.project-role {
  font-size: 15px;
  color: var(--text-faint);
  margin: 10px 0 22px;
}

.project-body p + p { margin-top: 15px; }
.project-body p { color: var(--text-dim); }
.project-body strong { color: var(--text); font-weight: 650; }

.stack {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 26px;
}
.stack li {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text-dim);
  background: var(--bg-sunk);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 12px;
}

.project-links { display: flex; gap: 10px; margin-top: 28px; flex-wrap: wrap; }

.project-links a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  padding: 0 18px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--text);
  font-size: 14.5px;
  font-weight: 550;
  text-decoration: none;
  transition: border-color .18s, color .18s, transform .18s;
}
.project-links a::after {
  content: "→";
  font-size: 15px;
  transition: transform .18s;
}
.project-links a:hover {
  border-color: var(--accent);
  color: var(--accent);
  transform: translateY(-2px);
}
.project-links a:hover::after { transform: translateX(3px); }

/* ------------------------------------------------------------ open source */

/* Les chiffres sont l'argument de la section : ils sortent des cartes qui les
   enfermaient pour occuper une bande, plus grands et plus aeres. */
.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: clamp(28px, 3.5vw, 56px);
  padding: clamp(34px, 4vw, 56px) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-bottom: clamp(52px, 6vw, 84px);
}

.figures li { display: flex; flex-direction: column; gap: 10px; }

.figure {
  font-family: var(--font-display);
  font-optical-sizing: auto;
  font-size: clamp(48px, 6.2vw, 86px);
  line-height: .9;
  letter-spacing: -.045em;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.figure-label {
  font-size: 14px;
  color: var(--text-dim);
  max-width: 20ch;
}

/* Le bloc principal porte 32 des 41 contributions : un filet d'accent le
   distingue des quatre depots secondaires, listes plus bas. */
.oss-principal {
  border-left: 2px solid var(--accent);
  padding-left: clamp(22px, 3vw, 40px);
  margin-bottom: clamp(52px, 6vw, 84px);
}

.oss-detail h3 {
  font-family: var(--font-display);
  font-size: clamp(21px, 2.2vw, 28px);
  letter-spacing: -.02em;
  font-weight: 700;
  margin-bottom: 22px;
}
.muted { color: var(--text-faint); font-weight: 400; font-size: .82em; }

.oss-principal h3 a {
  color: inherit;
  text-decoration: none;
  text-underline-offset: 4px;
  transition: color .18s;
}
.oss-principal h3 a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.oss-secondaire { margin-bottom: 24px; }

.bullets { display: flex; flex-direction: column; gap: 16px; max-width: 76ch; }
.bullets li {
  position: relative;
  padding-left: 24px;
  color: var(--text-dim);
}
.bullets li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .58em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
}
.bullets strong { color: var(--text); font-weight: 650; }

.repos { width: 100%; border-collapse: collapse; font-size: 15px; }
.repos th, .repos td {
  text-align: left;
  padding: 16px 18px 16px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.repos thead th {
  font-family: var(--font-mono);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--text-faint);
  font-weight: 400;
  padding-bottom: 12px;
}
.repos tbody th { font-weight: 650; white-space: nowrap; }

/* Le nom du depot mene au depot. Le soulignement n'apparait qu'au survol :
   quatre noms soulignes d'affilee alourdiraient la colonne. */
.repos tbody th a {
  text-decoration: none;
  text-underline-offset: 3px;
  transition: color .18s;
}
.repos tbody th a:hover {
  color: var(--accent);
  text-decoration: underline;
}
.repos td { color: var(--text-dim); }
.repos td:nth-child(2) {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  width: 4rem;
}

/* --------------------------------------------------------------- parcours */

/* La date passe en colonne : l'oeil descend la chronologie d'un coup, sans
   avoir a la relire dans chaque bloc. */
.timeline { display: flex; flex-direction: column; }

.timeline li {
  display: grid;
  grid-template-columns: minmax(120px, 168px) 1fr;
  gap: clamp(20px, 3vw, 48px);
  padding: clamp(26px, 3vw, 38px) 0;
  border-top: 1px solid var(--line);
}
.timeline li:last-child { border-bottom: 1px solid var(--line); }
/* Le titre de section porte deja un filet ; celui du premier jalon en
   dessinait un second juste en dessous, avec une bande vide entre les deux. */
.timeline li:first-child { border-top: 0; padding-top: 0; }

.when {
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .06em;
  color: var(--text-faint);
  padding-top: 5px;
}

.timeline h3 {
  font-size: clamp(19px, 2vw, 24px);
  font-weight: 650;
  letter-spacing: -.016em;
}
.where { font-size: 15px; color: var(--text-dim); margin: 4px 0 10px; }
.jalon > p:last-child { color: var(--text-dim); max-width: 62ch; }

.skills { margin-top: clamp(44px, 5vw, 72px); }
.skills h3 {
  font-family: var(--font-display);
  font-size: clamp(21px, 2.2vw, 28px);
  letter-spacing: -.02em;
  font-weight: 700;
  margin-bottom: 26px;
}
.skills dl {
  display: grid;
  grid-template-columns: minmax(120px, 168px) 1fr;
  gap: 18px clamp(20px, 3vw, 48px);
}
.skills dt {
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: .06em;
  color: var(--text-faint);
  padding-top: 2px;
}
.skills dd { margin: 0; color: var(--text-dim); }

/* ---------------------------------------------------------------- contact */

/* Derniere section avant le pied : elle porte l'action qu'on attend du
   visiteur, elle a donc droit au meme traitement que l'accroche. */
.section-contact {
  position: relative;
  overflow: hidden;
  padding: clamp(88px, 11vw, 168px) 0 clamp(96px, 12vw, 180px);
}

.contact-decor {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.halo-c {
  width: 62vw;
  height: 62vw;
  max-width: 820px;
  max-height: 820px;
  left: 50%;
  bottom: -34vw;
  translate: -50% 0;
  background: var(--halo-a);
}

.contact-inner {
  position: relative;
  z-index: 1;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 28px;
  text-align: center;
}

/* C'est le titre de la section, pas une etiquette : un lecteur d'ecran qui
   parcourt la page titre par titre doit tomber dessus. Il en garde
   l'apparence discrete — la phrase juste en dessous porte le poids visuel. */
.contact-sur {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 13px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 26px;
}

.contact-dire {
  font-family: var(--font-display);
  font-optical-sizing: auto;
  font-size: clamp(24px, 3.2vw, 42px);
  line-height: 1.18;
  letter-spacing: -.024em;
  font-weight: 700;
  color: var(--text-dim);
  max-width: 22ch;
  margin: 0 auto clamp(36px, 5vw, 60px);
}
.contact-dire strong { color: var(--text); font-weight: 700; }

.contact-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(22px, 2.8vw, 32px);
}

/* Ces trois liens sont desormais l'action principale de la page : ils sont
   dimensionnes en consequence, pas comme un rappel de pied de page. */
.contact-liens {
  display: flex;
  align-items: center;
  gap: clamp(18px, 2.6vw, 30px);
}

.contact-liens a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  color: var(--text-dim);
  transition: color .18s, border-color .18s, background .18s, transform .18s;
}
.contact-liens a:hover {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: var(--accent);
  transform: translateY(-3px);
}

/* Les icones prennent la couleur du texte : elles suivent le theme sans
   qu'aucune regle ne les cite dans la palette sombre. */
.contact-liens svg {
  width: 26px;
  height: 26px;
  fill: currentColor;
}

.copier {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 38px;
  padding: 0 18px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  font-family: var(--font-ui);
  font-size: 13.5px;
  cursor: pointer;
  transition: color .18s, border-color .18s, background .18s;
}
.copier:hover { color: var(--text); border-color: var(--text); }

/* Le retour de la copie passe par le texte du bouton, pas par une bulle
   flottante : il est ainsi annonce par les lecteurs d'ecran sans region
   `live` supplementaire, et disparait tout seul. */
.copier.est-copie {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.contact-cadre {
  margin-top: clamp(38px, 5vw, 58px);
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: .04em;
  color: var(--text-faint);
}

/* -------------------------------------------------------------- responsive */

@media (max-width: 900px) {
  .project, .project + .project { grid-template-columns: 1fr; }
  .project:nth-of-type(even) .project-media,
  .project:nth-of-type(even) .project-body { grid-column: 1; grid-row: auto; }
  .project-media,
  .project:nth-of-type(even) .project-media { transform: none; }
  .skills dl { grid-template-columns: 1fr; gap: 4px 0; }
  .skills dt { margin-top: 12px; }
}

@media (max-width: 620px) {
  body { font-size: 16px; }
  .topbar { padding: 14px 18px; }
  .topbar nav ul { gap: 16px; }
  .topbar nav a { font-size: 12px; }
  .section { padding-left: 18px; padding-right: 18px; }
  .timeline li, .skills dl { grid-template-columns: 1fr; gap: 6px; }
  .when { padding-top: 0; }
  .skills dt { margin-top: 14px; }
  .oss-principal { padding-left: 18px; }
  .repos thead { display: none; }
  .repos tbody th, .repos td { display: block; border: 0; padding: 2px 0; }
  .repos tbody tr { display: block; padding: 14px 0; border-bottom: 1px solid var(--line); }
  .repos td:nth-child(2)::before { content: "PR : "; color: var(--text-faint); }
}

/* Respecte le reglage systeme : ni defilement anime, ni transition. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}

/* Ombre discrete sous la barre haute une fois qu'on a quitte le sommet
   (classe posee par js/main.js). */
.topbar.is-stuck { border-bottom: 1px solid var(--line); }
