/* ==========================================================================
   Koja Studio — feuille de style commune
   Variables de thème (clair/sombre), typographie, layout, composants partagés
   ========================================================================== */

/* --------------------------------------------------------------------------
   TYPOGRAPHIE — decision du 28/07/2026 : Inter remplacee.
     TITRES ... Cabinet Grotesk 500   (Fontshare, licence commerciale gratuite)
     TEXTE .... Public Sans 300/400   (SIL Open Font License)
     LOGO ..... Comfortaa 700         (logo uniquement, jamais ailleurs)
   Polices auto-hebergees dans assets/fonts (aucune dependance a un CDN
   exterieur : plus rapide, et le site continue de s'afficher correctement
   meme ouvert en local depuis le disque).
   Fichiers sources et .otf pour le poste de travail :
     01 - COMMUNICATION / 02 - IDENTITE VISUELLE / 02 - Charte graphique / Polices
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Cabinet Grotesk';
  src: url('../assets/fonts/CabinetGrotesk-Variable.woff2') format('woff2-variations'),
       url('../assets/fonts/CabinetGrotesk-Variable.woff2') format('woff2');
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Public Sans';
  src: url('../assets/fonts/PublicSans-Variable.woff2') format('woff2-variations'),
       url('../assets/fonts/PublicSans-Variable.woff2') format('woff2');
  font-weight: 100 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Public Sans';
  src: url('../assets/fonts/PublicSans-Italic-Variable.woff2') format('woff2-variations'),
       url('../assets/fonts/PublicSans-Italic-Variable.woff2') format('woff2');
  font-weight: 100 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'Comfortaa';
  src: url('../assets/fonts/Comfortaa-Bold.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  --bg: #fdfdfd;
  --bg-raised: #f3f3f3;
  --text: #1a1a1a;
  --text-soft: rgba(26, 26, 26, 0.62);
  --text-faint: rgba(26, 26, 26, 0.38);
  --border: rgba(26, 26, 26, 0.12);
  /* gris du mot "studio" dans le logo (remplace l'ancien orange), assombri
     ici pour rester lisible sur fond clair : en gris pur, contrairement à
     l'orange, il n'y a plus que la luminosité pour se détacher du fond. */
  --accent: #6e6e6e;
  --on-accent: #fff;
  --placeholder-bg: #e6e6e6;
  --placeholder-text: rgba(26, 26, 26, 0.32);
  --tag-bg: #1a1a1a;
  --tag-text: #fdfdfd;
  --header-h: 84px;
  --transition: 0.35s ease;
}

html[data-theme="dark"] {
  --bg: #111111;
  --bg-raised: #1a1a1a;
  --text: #f3f3f3;
  --text-soft: rgba(243, 243, 243, 0.62);
  --text-faint: rgba(243, 243, 243, 0.38);
  --border: rgba(243, 243, 243, 0.14);
  /* même logique, éclairci ici pour rester lisible sur fond sombre */
  --accent: #a6a6a6;
  --on-accent: #1a1a1a;
  --placeholder-bg: #232323;
  --placeholder-text: rgba(243, 243, 243, 0.32);
  --tag-bg: #f3f3f3;
  --tag-text: #111111;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
/* fond posé sur <html> (pas seulement <body>) : le corps démarre à
   opacity:0 (voir kojaPageIn plus bas) au chargement de chaque page — sans
   ce fond, ce court instant révèle le blanc par défaut du navigateur
   derrière, quel que soit le thème (flash blanc, y compris en thème
   sombre). Le thème est posé sur <html> avant le premier rendu (script
   dans le <head> de chaque page), donc var(--bg) est déjà la bonne
   couleur dès ce tout premier paint. */
/* pas de scroll-behavior:smooth global : sur Chrome, appliqué au défilement
   à la molette/trackpad (pas seulement aux ancres), ça produit un effet
   "lent puis qui accélère" assez désagréable (bug connu de Chrome, absent
   sur Firefox qui gère ça différemment) — le confort de défilement natif
   prime sur le petit plus des ancres qui glissent au lieu de sauter. */
html { overflow-x: hidden; background: var(--bg); }
/* Empêche le "pull-to-refresh" (glisser vers le bas en haut de page qui
   recharge le site) sur mobile — ça arrivait par erreur en manipulant le
   mur d'images tout en haut de la page. Limité aux petits écrans (touch)
   via la media query : sur PC, le rebond de la molette/trackpad reste
   inchangé. Demande de Guillaume le 30/07. */
@media (max-width: 860px) {
  html, body { overscroll-behavior-y: none; }
}
/* Pages projet : le paragraphe descriptif sous le titre (.mw-intro p.sub,
   présent sur les 35 pages projet via leur bloc <style> commun) est masqué
   sur mobile — demande de Guillaume le 30/07. Règle posée ici plutôt que
   dans chaque page pour n'avoir qu'un seul endroit à maintenir ; gardée
   dans une media query donc le PC ne bouge pas. */
@media (max-width: 860px) {
  .mw-intro p.sub { display: none; }
}
h1, h2, h3, h4 { text-wrap: balance; }
/* Titres : Cabinet Grotesk en graisse 500 (pas plus lourd : au-dessus, le
   dessin devient massif ; au-dessous, il perd sa presence). Interlettrage
   legerement negatif, comme dans la charte graphique. */
h1, h2, h3, h4,
.wall-topbar-manifesto p,
.wtile.ttile-welcome .tw-inner p {
  font-family: 'Cabinet Grotesk', 'Helvetica Neue', Arial, sans-serif;
  font-weight: 500;
  letter-spacing: -0.025em;
}
/* Titres posés sur les images du mur : la graisse 500 disparait sur une
   mosaique de photos, il faut monter a 600. Regle generale de la charte :
   500 sur fond uni, 600 des que le titre est sur une image. */
.hero-text h1,
.hero-screen h1,
.wall-topbar-manifesto p strong { font-weight: 600; }
p { text-wrap: pretty; }
body {
  font-family: 'Public Sans', 'Helvetica Neue', Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-weight: 300;
  transition: background var(--transition), color var(--transition);
  animation: kojaPageIn 0.45s ease both;
}

/* Transitions de page : le thème est posé avant le premier rendu (script
   dans le <head> de chaque page), donc plus de flash blanc/sombre ; la
   page arrive par un fondu discret et part par un fondu court au clic
   sur un lien interne (voir initPageTransitions dans main.js). */
@keyframes kojaPageIn { from { opacity: 0; } to { opacity: 1; } }
body.page-leave { opacity: 0; transition: opacity 0.16s ease; }

.has-custom-cursor, .has-custom-cursor * { cursor: none !important; }
/* Curseur "point + cote" : un point fin + une micro-annotation qui change
   selon ce qu'on survole (voir / ouvrir / glisser / lire), comme les cotes
   sur un plan d'architecte. */
.cursor-dot {
  position: fixed; top: 0; left: 0; width: 9px; height: 9px; border-radius: 50%;
  background: var(--text); z-index: 9999; pointer-events: none;
  transform: translate(-50%, -50%);
  opacity: 0; transition: opacity 0.2s ease, width 0.18s ease, height 0.18s ease;
}
.cursor-dot.is-visible { opacity: 1; }
.cursor-dot.is-down { width: 15px; height: 15px; }
.cursor-tag {
  position: fixed; top: 0; left: 0; z-index: 9999; pointer-events: none;
  color: var(--text);
  font-family: 'Public Sans', sans-serif; font-size: 10.5px; letter-spacing: 1.8px; text-transform: uppercase; font-weight: 500;
  padding-left: 17px; opacity: 0; transition: opacity 0.18s ease;
}
.cursor-tag::before {
  content: ""; position: absolute; left: 1px; top: 58%; width: 12px; height: 1px;
  background: var(--text); transform: rotate(-32deg); transform-origin: left center;
}
.cursor-tag.is-on { opacity: 1; }

header.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100; height: var(--header-h);
  display: flex; align-items: center; justify-content: space-between; padding: 0 44px;
  background: transparent;
  transition: background var(--transition), border-color var(--transition);
}
header.site-header.is-scrolled { background: var(--bg); border-bottom: 1px solid var(--border); }

.logo { display: block; line-height: 0; }
/* Le fichier du logo "kojastudio" est recadré au plus juste (le "k" et le "j"
   touchent les bords), alors que l'ancien logo "koja" avait de larges marges
   internes : à hauteur CSS égale les lettres seraient 1,6x plus grosses. Les
   43/33 px ci-dessous redonnent donc exactement la taille de lettres d'avant
   (42 px du haut du "k" au bas du "j", comme les 68 px de l'ancien fichier). */
.logo img { height: 43px; display: block; }
@media (max-width: 640px) { .logo img { height: 33px; } }
.logo .logo-dark-img { display: none; }
html[data-theme="dark"] .logo .logo-light-img { display: none; }
html[data-theme="dark"] .logo .logo-dark-img { display: block; }

nav.main-nav { display: flex; align-items: center; }
nav.main-nav a {
  color: var(--text); text-decoration: none; font-size: 11px; letter-spacing: 2px;
  text-transform: uppercase; opacity: 0.65; margin-left: 32px; transition: opacity 0.2s ease;
  font-family: 'Comfortaa', cursive; font-weight: 700;
  position: relative; padding-bottom: 4px;
}
nav.main-nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: var(--accent); transition: width 0.25s ease;
}
nav.main-nav a:hover { opacity: 1; }
nav.main-nav a:hover::after { width: 100%; }
nav.main-nav a.is-disabled { opacity: 0.28; pointer-events: none; }

/* Sous-menu du nav : "Projets" déroule Images / Vidéos / Photos au survol.
   Pas de JS : :hover / :focus-within suffisent. Le panneau colle au bas du
   lien (top:100%) pour que la souris puisse y descendre sans trou. Pas de
   cadre (ni bordure ni fond) : le panneau flotte, aligné à droite sur
   "Projets" (right:0 du conteneur .nav-sub, qui a la largeur du lien). */
.nav-sub { position: relative; display: inline-flex; align-items: center; }
.nav-sub-panel {
  position: absolute; top: 100%; right: 0; min-width: 90px;
  display: flex; flex-direction: column; align-items: flex-end; text-align: right;
  padding: 14px 0 18px;
  opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s;
}
.nav-sub:hover .nav-sub-panel, .nav-sub:focus-within .nav-sub-panel {
  opacity: 1; visibility: visible; transform: translateY(0);
}
nav.main-nav .nav-sub-panel a { margin-left: 0; }
nav.main-nav .nav-sub-panel a + a { margin-top: 11px; }
/* Sous 860 px le panneau n'est plus masqué mais déplié dans le menu burger :
   voir la media query « nav mobile » plus bas. Il n'y a pas de survol sur
   téléphone, Images et Photos étaient donc inaccessibles (repère 03/08). */

.lang-toggle, .theme-toggle {
  background: none; border: none; font-family: inherit; color: var(--text);
  font-size: 10px; letter-spacing: 1px; opacity: 0.55; margin-left: 30px; cursor: pointer; white-space: nowrap;
}
.lang-toggle:hover, .theme-toggle:hover { opacity: 1; }

/* Bouton hamburger : n'existe visuellement qu'en dessous de 860px (voir la
   media query plus bas). Les 3 barres sont 3 <span> dans le bouton, animées
   en croix à l'ouverture. */
.nav-toggle {
  display: none; background: none; border: none; cursor: pointer; padding: 6px;
  width: 34px; height: 30px; position: relative; z-index: 210;
}
.nav-toggle span {
  display: block; position: absolute; left: 6px; right: 6px; height: 1.5px;
  background: var(--text); transition: transform 0.25s ease, opacity 0.25s ease;
}
.nav-toggle span:nth-child(1) { top: 10px; }
.nav-toggle span:nth-child(2) { top: 15px; }
.nav-toggle span:nth-child(3) { top: 20px; }
body.nav-open .nav-toggle span:nth-child(1) { transform: translateY(5px) rotate(45deg); }
body.nav-open .nav-toggle span:nth-child(2) { opacity: 0; }
body.nav-open .nav-toggle span:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 860px) {
  header.site-header { padding: 0 22px; }
  .nav-toggle { display: block; order: 3; }
  /* le nav lui-même devient le panneau plein écran — mêmes liens, mêmes
     <a>, juste un habillage différent : pas de markup dupliqué à
     maintenir sur les ~40 pages du site (repère 30/07, corrige le menu
     invisible sur mobile). */
  nav.main-nav {
    position: fixed; inset: 0; top: var(--header-h); z-index: 199;
    flex-direction: column; align-items: center; justify-content: center;
    background: var(--bg); gap: 4px;
    opacity: 0; visibility: hidden; transform: translateY(-14px);
    transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s;
  }
  body.nav-open nav.main-nav { opacity: 1; visibility: visible; transform: translateY(0); }
  nav.main-nav a:not(.lang-toggle):not(.theme-toggle) {
    display: block; margin-left: 0; padding: 14px 0; font-size: 16px; opacity: 0.85;
  }
  .nav-sub { flex-direction: column; }
  /* Sous-menu de « Projets » sur téléphone : pas de survol possible, donc il
     est déplié en permanence sous le lien, en plus petit et plus discret. */
  .nav-sub-panel {
    position: static; opacity: 1; visibility: visible; transform: none;
    min-width: 0; padding: 0 0 8px;
    align-items: center; text-align: center;
  }
  /* aucune transition ici : sinon le panneau semble « se dérouler » au
     moment où l'on touche « Projets », alors qu'il est déjà là */
  .nav-sub-panel { transition: none; }
  nav.main-nav .nav-sub-panel a:not(.lang-toggle):not(.theme-toggle) {
    padding: 10px 0; font-size: 14.5px; opacity: 0.72;
  }
  nav.main-nav .nav-sub-panel a + a { margin-top: 0; }
  nav.main-nav .lang-toggle, nav.main-nav .theme-toggle { margin-left: 0; margin-top: 22px; font-size: 12px; }
  body.nav-open { overflow: hidden; }
}

.ph {
  background: var(--placeholder-bg); color: var(--placeholder-text);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; letter-spacing: 1px; text-transform: uppercase; text-align: center;
  padding: 10px; width: 100%; height: 100%;
}

.drag-strip {
  display: flex; overflow-x: auto; scrollbar-width: none; cursor: grab;
  -webkit-overflow-scrolling: touch; user-select: none; touch-action: pan-y;
}
.drag-strip::-webkit-scrollbar { display: none; }
.drag-strip.is-dragging { cursor: grabbing; scroll-behavior: auto; }
.drag-strip img {
  -webkit-user-drag: none; user-drag: none; pointer-events: none;
  -webkit-touch-callout: none;
  box-shadow: none; border: none;
}

footer.site-footer {
  border-top: 1px solid var(--border); padding: 40px 44px; font-size: 12px; color: var(--text-soft);
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;
}

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 44px; }
.eyebrow { font-size: 11px; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); opacity: 0.95; font-weight: 600; }
@media (max-width: 640px) {
  .wrap { padding: 0 20px; }
  footer.site-footer { padding: 28px 20px; }
}

html[data-lang="fr"] .t-en { display: none !important; }
html[data-lang="en"] .t-fr { display: none !important; }

/* ==========================================================================
   Scroll cue (hero)
   ========================================================================== */
.scroll-cue {
  position: absolute; left: 50%; bottom: 34px; transform: translateX(-50%);
  z-index: 6; display: flex; flex-direction: column; align-items: center; gap: 12px;
  color: inherit; opacity: 0.55; text-decoration: none;
}
.scroll-cue span:not(.line) { font-size: 10px; letter-spacing: 3px; text-transform: uppercase; }
/* indicateur "molette de souris" — volontairement différent d'un simple
   trait avec un mot au-dessus (trop proche d'autres sites vus par Guillaume) */
.scroll-cue .line {
  width: 22px; height: 34px; border: 1.3px solid currentColor; border-radius: 12px;
  position: relative; background: transparent;
}
.scroll-cue .line::after {
  content: ""; position: absolute; left: 50%; top: 6px; width: 3px; height: 7px; border-radius: 2px;
  background: currentColor; transform: translate(-50%, 0);
  animation: scrollcue 1.7s ease-in-out infinite;
}
@keyframes scrollcue {
  0% { transform: translate(-50%, 0); opacity: 1; }
  70% { transform: translate(-50%, 13px); opacity: 0; }
  100% { transform: translate(-50%, 13px); opacity: 0; }
}

/* ==========================================================================
   Qui sommes-nous / Notre spécificité — bloc alterné avec fil de progression
   ========================================================================== */
.about-split { position: relative; max-width: 1180px; margin: 40px auto; padding: 0 44px; }
.about-thread {
  position: absolute; top: 10px; bottom: 10px; left: 50%; width: 1px;
  background: var(--border); transform: translateX(-50%); z-index: 0;
}
.about-thread::after {
  content: ""; position: absolute; top: 0; left: 0; width: 100%; height: var(--thread-fill, 0%);
  background: var(--accent); transition: height 0.08s linear;
}
.about-block {
  position: relative; z-index: 1; max-width: 480px; padding: 70px 0;
  opacity: 0; transform: translateY(36px); transition: opacity 0.8s ease, transform 0.8s ease;
}
.about-block.is-in { opacity: 1; transform: translateY(0); }
.about-block.left { margin-right: auto; text-align: left; transform: translate(-24px, 36px); }
.about-block.left.is-in { transform: translate(0, 0); }
.about-block.right { margin-left: auto; text-align: right; transform: translate(24px, 36px); }
.about-block.right.is-in { transform: translate(0, 0); }
.about-block .eyebrow { display: block; margin-bottom: 16px; }
.about-block h2 { font-weight: 300; font-size: clamp(21px, 2.4vw, 28px); line-height: 1.4; margin-bottom: 16px; }
.about-block p { font-size: 15px; line-height: 1.8; color: var(--text-soft); }
.about-block .block-cta {
  display: inline-block; margin-top: 24px; font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
  padding: 14px 28px; border: 1px solid var(--accent); color: var(--accent); text-decoration: none;
  background: transparent; transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.about-block .block-cta:hover { background: var(--bg-raised); }
.about-block .dot {
  position: absolute; top: 78px; width: 9px; height: 9px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--accent); z-index: 2;
}
.about-block.left .dot { right: -49px; }
.about-block.right .dot { left: -49px; }
.about-block.is-in .dot { background: var(--accent); }
/* en theme sombre, le trait et les pastilles gagnent a ressortir davantage
   que le gris standard : blanc plein plutot que --border/--accent ici. */
html[data-theme="dark"] .about-thread,
html[data-theme="dark"] .about-thread::after { background: #ffffff; }
html[data-theme="dark"] .about-block .dot { border-color: #ffffff; }
html[data-theme="dark"] .about-block.is-in .dot { background: #ffffff; }
@media (max-width: 760px) {
  .about-thread { left: 14px; }
  .about-block, .about-block.left, .about-block.right { max-width: 100%; text-align: left !important; margin: 0 0 0 40px; transform: translateY(36px) !important; }
  .about-block.is-in { transform: translateY(0) !important; }
  .about-block.left .dot, .about-block.right .dot { left: -33px; right: auto; }
}

/* ==========================================================================
   CTA — bandeau "on en discute ?"
   ========================================================================== */
.cta-section { max-width: 900px; margin: 150px auto 140px; padding: 0 44px; text-align: center; }
.cta-section h2 { font-weight: 300; font-size: clamp(22px, 3vw, 32px); margin-bottom: 34px; }
.cta-row { display: flex; justify-content: center; gap: 18px; flex-wrap: wrap; }
.cta-row a {
  display: inline-block; font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
  padding: 16px 32px; text-decoration: none; transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
}
/* Aucun bouton plein sur le site : tous les boutons (primaire ou "ghost")
   partagent le même traitement, trait fin gris (comme "Découvrez notre
   travail" sous le hero de l'accueil), jamais de fond rempli. */
.cta-row a.cta-primary,
.cta-row a.cta-ghost { background: transparent; border: 1px solid var(--accent); color: var(--accent); }
.cta-row a.cta-primary:hover,
.cta-row a.cta-ghost:hover { background: var(--bg-raised); }
.cta-section .cta-row { flex-direction: column; align-items: center; gap: 14px; }
.cta-section .cta-row a { width: 100%; max-width: 320px; text-align: center; }

/* ==========================================================================
   Footer commun
   ========================================================================== */
footer.site-footer { display: block; padding: 70px 44px 26px; }
.footer-grid {
  max-width: 1180px; margin: 0 auto; display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 40px;
  padding-bottom: 40px;
}
.footer-col h4 {
  font-size: 10px; letter-spacing: 2px; text-transform: uppercase; opacity: 0.5;
  margin-bottom: 16px; font-weight: 500; color: var(--text);
}
.footer-col p { line-height: 1.85; color: var(--text-soft); font-size: 13px; }
.footer-col a { line-height: 2; color: var(--text-soft); text-decoration: none; font-size: 13px; display: block; }
.footer-col a:hover { color: var(--accent); }
.footer-social { display: flex; gap: 16px; margin-top: 12px; }
.footer-social a { display: inline-flex; opacity: 0.65; }
.footer-social a:hover { opacity: 1; }
.footer-social svg { width: 17px; height: 17px; stroke: currentColor; fill: none; }
.footer-bottom {
  max-width: 1180px; margin: 0 auto; padding-top: 22px; border-top: 1px solid var(--border);
  display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px;
  font-size: 11px; letter-spacing: 0.5px; opacity: 0.55;
}
/* le lien « Mentions légales » se lit comme les autres mentions du bas de
   page : meme couleur, meme taille, pas de soulignement ni de bleu par
   defaut du navigateur. Seul le survol le distingue. */
.footer-bottom a { color: inherit; text-decoration: none; font-size: inherit; letter-spacing: inherit; }
.footer-bottom a:hover { color: var(--accent); }
@media (max-width: 760px) { .footer-grid { grid-template-columns: 1fr; gap: 30px; } }

/* ==========================================================================
   Galerie Projets — mur infini avec révélation
   Au chargement (ou en arrivant depuis "Découvrez notre travail"), le mur
   passe directement en pleine lumière (plus de texte d'intro à effacer au
   préalable) et devient mobile : on le fait glisser à la souris, fluide et
   sans fin dans toutes les directions (les images reviennent en boucle),
   sans jamais recadrer les images. Un clic sur une image ouvre la page du
   projet.
   ========================================================================== */
.wall-stage {
  position: relative; height: 100vh; min-height: 580px; overflow: hidden;
  /* même habillage que le hero de l'accueil : le mur de la page Projets est
     la continuité directe du mur d'esquisses agrandi par la transition */
  background: #0a0a0a;
}
html[data-theme="light"] .wall-stage { background: #f4f4f2; }

.wall-viewport { position: absolute; inset: 0; cursor: grab; touch-action: none; z-index: 2; }
.wall-viewport.dragging { cursor: grabbing; }
.wall-board {
  position: absolute; inset: 0;
  filter: grayscale(1) brightness(0.6); opacity: 0.4; transform: scale(0.985);
  transition: filter 1.1s ease, opacity 1.1s ease, transform 1.1s ease;
}
/* filter:none et pas grayscale(0) brightness(1) : un filtre « neutre » n'est
   pas gratuit — le navigateur refiltre toute la surface du mur à chaque frame
   de déplacement, pour un résultat identique à l'image d'origine. none coupe
   le filtre une fois le mur révélé ; la transition grayscale→none s'interpole
   quand même, l'effet d'entrée gris→couleur est inchangé. */
.wall-stage.is-revealed .wall-board { filter: none; opacity: 1; transform: scale(1); }
.wall-stage:not(.is-revealed) .wall-viewport { pointer-events: none; }
/* une cellule = le motif complet d'images, répété en mosaïque pour l'infini */
.wall-cell { position: absolute; top: 0; left: 0; will-change: transform; }

/* tuiles du mur : mêmes proportions que le marquee de l'accueil (thumb 235px,
   version haute 285px), pour que la page Projets soit la continuité exacte
   de la fin de la transition. Le drag doit marcher PARTOUT, y compris en
   attrapant une image : rien n'est en pointer-events:none, mais le
   glisser-déposer natif des liens/images est désactivé. */
/* PAS de will-change ici — mesuré (Chromium bridé ×4, comparaison A/B) : avec
   51 projets le mur compte ~330 tuiles ; les promouvoir toutes en calques
   graphiques coûte plus cher que de laisser les cellules se redessiner
   (63 ms par frame contre 41). Les tuiles sont peintes dans le calque de leur
   cellule — .wall-cell, qui garde SON will-change — et seules les 4 à 6
   cellules bougent d'une frame à l'autre. Ne pas réintroduire sans mesurer. */
.wtile {
  position: absolute; top: 0; left: 0; display: block; width: 320px; text-decoration: none;
  transform-origin: 0 0; overflow: hidden;
  user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
}
/* images du mur (tuiles projet) — aucune retouche : couleurs et contraste
   restent strictement ceux du fichier d'origine, pas de filtre CSS dessus. */
.wtile img {
  width: 100%; height: 227px; object-fit: cover; display: block;
  background: #111;
  -webkit-user-drag: none; user-select: none;
  transition: transform 0.4s ease;
}
.wtile.tall img { height: 388px; }

/* survol : léger zoom + assombrissement franc de l'image (fond plat porté
   par un pseudo-élément directement sur la tuile, plutôt qu'un span
   séparé — moins de pièces mobiles, plus fiable), pour que le titre du
   projet affiché par-dessus reste toujours parfaitement lisible. */
.wtile::after {
  content: ""; position: absolute; inset: 0; opacity: 0; pointer-events: none;
  transition: opacity 0.35s ease; background: rgba(0, 0, 0, 0.46); z-index: 1;
}
.wtile:hover img { transform: scale(1.055); }
.wtile:hover::after { opacity: 1; }
.wtile .wcap {
  position: absolute; inset: 0; z-index: 2; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 20px;
  font-size: 11px; letter-spacing: 2.5px; text-transform: uppercase; color: #fff;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75), 0 0 6px rgba(0, 0, 0, 0.6);
  opacity: 0; transition: opacity 0.35s ease; pointer-events: none;
}
.wtile:hover .wcap { opacity: 1; }

/* tuile "bienvenue" : remplace une image du motif (voir WELCOME_IDX dans
   main.js). Sur le mur du hero, avant déploiement, une vraie image de
   projet (même mécanique srcset que les autres tuiles) la fait se fondre
   dans la mosaïque comme n'importe quelle autre tuile. Au déploiement
   (body.wall-zoom), l'image disparaît complètement en fondu : il ne reste
   que le texte sur fond plein (#0a0a0a / #f4f4f2), comme à l'origine — pas
   d'image assombrie derrière (repère 30/07). Sur /projets.html (pas de
   mode "hero"), l'image reste cachée en permanence : texte sur fond plein
   dès le chargement, comme avant. */
.wtile.ttile-welcome {
  /* hauteur forcée au-delà du gabarit "tall" (388px) : sans ça, la boîte
     ne fait que la hauteur de son texte et flotte, trop petite, dans son
     emplacement — ici elle rejoint le haut de l'image suivante dans la
     même colonne (388 + l'écart de 49px entre deux tuiles). */
  /* couleur en dur (pas var(--bg), légèrement différent) pour matcher
     EXACTEMENT le fond du mur/hero (#0a0a0a / #f4f4f2). */
  height: 437px;
  background: #0a0a0a;
  display: flex; align-items: center; justify-content: center; padding: 26px;
  cursor: default;
}
html[data-theme="light"] .wtile.ttile-welcome { background: #f4f4f2; }
.wtile.ttile-welcome img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;
  opacity: 0; transition: opacity 0.4s ease;
}
body.home-page .wtile.ttile-welcome img { opacity: 1; }
body.home-page.wall-zoom .wtile.ttile-welcome img { opacity: 0; }
.wtile.ttile-welcome:hover img { transform: none; }
.wtile.ttile-welcome .tw-inner { position: relative; z-index: 2; }
.wtile.ttile-welcome .tw-inner p {
  font-size: 19px; line-height: 1.65; color: var(--text); text-align: center;
  opacity: 1; transition: opacity 0.4s ease;
}
body.home-page .wtile.ttile-welcome .tw-inner p { opacity: 0; }
body.home-page.wall-zoom .wtile.ttile-welcome .tw-inner p { opacity: 1; }
.wtile.ttile-welcome .tw-inner p strong { font-weight: 700; }

/* bande noire pleine largeur en haut du mur : juste la phrase manifeste de
   l'agence (Cabinet Grotesk, police des titres du site — pas Comfortaa,
   reservee au logo),
   sur une seule ligne. Plus d'eyebrow "Koja Studio" (le logo est déjà dans
   le header juste au-dessus) ni de texte de bienvenue ici — ce texte vit
   maintenant dans une vraie tuile au milieu du mur (voir .ttile-welcome,
   window.kojaInfiniteWall dans main.js), qui se déplace avec les images
   plutôt que de rester figée en haut. */
.wall-topbar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 3;
  min-height: calc(var(--header-h) + 90px);
  display: flex; align-items: flex-start; justify-content: center;
  text-align: center; padding: 16px 44px; padding-top: calc(var(--header-h) + 2px);
  color: #eee; pointer-events: none;
  transition: color var(--transition);
}
html[data-theme="light"] .wall-topbar { color: #1a1a1a; }
/* fond en fondu (pas un aplat à bord franc) : les tuiles du mur qui
   passent sous la bande s'estompent progressivement au lieu d'être
   coupées net par une ligne droite. Le texte tombe à peu près au centre
   de la bande : la zone pleine doit donc s'étendre largement au-delà de
   ce point pour rester parfaitement lisible, le fondu ne servant qu'à
   estomper la toute fin de la bande. */
.wall-topbar::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(to bottom, #0a0a0a 80%, rgba(10, 10, 10, 0) 100%);
}
html[data-theme="light"] .wall-topbar::before {
  background: linear-gradient(to bottom, #f4f4f2 80%, rgba(244, 244, 242, 0) 100%);
}
.wall-topbar-manifesto p {
  font-weight: 300; font-size: clamp(15px, 1.7vw, 21px); line-height: 1.5;
  white-space: nowrap; margin: 0 auto;
}
.wall-topbar-manifesto p strong { font-weight: 600; }
@media (max-width: 760px) {
  .wall-topbar-manifesto p { white-space: normal; max-width: 90%; margin: 0 auto; }
}
@media (max-width: 640px) {
  .wall-topbar { min-height: calc(var(--header-h) + 70px); padding: 14px; padding-top: calc(var(--header-h) + 4px); }
}

/* sur l'accueil, ce même bandeau est réutilisé au sommet du mur une fois
   qu'il est déployé (voir index.html) — masqué tant que le mur n'est pas
   interactif, révélé en fondu avec le reste (header, indice) au moment
   où body.wall-live est posé. Fondu court et immédiat (pas de délai) pour
   que la bande apparaisse rapidement plutôt qu'avec une pause perceptible. */
.hero .wall-topbar { opacity: 0; transition: opacity 0.35s ease; }
body.wall-live .hero .wall-topbar { opacity: 1; }

.wall-drag-hint { display: none; }

/* page fixe : le mur ne défile jamais, quitte à ne pas voir le footer. */
body.wall-page { height: 100vh; overflow: hidden; }
/* accueil : une fois le mur du hero déployé, même verrou. */
body.wall-live { height: 100vh; overflow: hidden; }
/* ATTENTION — le verrou doit AUSSI porter sur <html>. Comme <html> a
   `overflow-x: hidden` (voir en haut de ce fichier), son overflow-y calculé
   vaut `auto` : l'overflow du <body> ne se propage donc plus au viewport, et
   c'est <html> qui reste le vrai conteneur de défilement. Sans ces deux
   règles, la molette faisait descendre la page alors que le mur est censé
   occuper tout l'écran. Corrigé le 02/08/2026 — ne pas retirer.
   (L'accueil ajoute en plus la classe `wall-locked` sur <html> dès le clic,
   voir index.html, parce que la classe `wall-live` n'arrive qu'à la fin de
   l'animation d'ouverture.) */
html:has(body.wall-page),
html:has(body.wall-live) { height: 100%; overflow: hidden; overscroll-behavior: none; }

@media (max-width: 760px) {
  .wall-stage { height: 100vh; }
}

/* indice de défilement vertical générique (molette, même logique que
   .scroll-cue) — réutilisé sur les pages projet pour inviter à descendre
   voir les images finales après la bande d'esquisses. */
.scroll-hint { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.scroll-hint .lbl { font-size: 10.5px; letter-spacing: 1.8px; text-transform: uppercase; opacity: 0.45; text-align: center; }
.scroll-hint-pill { width: 22px; height: 34px; border: 1.3px solid var(--text); border-radius: 12px; opacity: 0.55; position: relative; }
.scroll-hint-pill::after {
  content: ""; position: absolute; left: 50%; top: 6px; width: 3px; height: 7px; border-radius: 2px;
  background: var(--accent); transform: translate(-50%, 0); animation: scrollcue 1.7s ease-in-out infinite;
}

/* ==========================================================================
   Mur figé en arrière-plan du PREMIER ÉCRAN d'une page projet (window.
   kojaFrozenWallBackdrop, js/main.js) : reconstruit le mur, non interactif,
   exactement à la position qu'il avait au moment du clic — donne
   l'impression que la page s'ouvre littéralement à cet endroit du mur.
   Habillage IDENTIQUE au hero de l'accueil (mosaïque en pleine couleur +
   voile radial). Confiné à la hauteur du premier écran de la page projet —
   la section qui accueille #frozenWall porte la classe .fw-host (position
   relative + overflow hidden) : passé cet écran, la page retrouve le fond
   uni normal du site (var(--bg)), via le fondu ci-dessous qui ramène le
   mur à var(--bg) en bas de son cadre, sans coupure visible.
   ========================================================================== */
.fw-host { overflow: hidden; }
.frozen-wall {
  position: absolute; inset: 0; z-index: -1; overflow: hidden;
  /* fond derrière la mosaïque — comme .hero/.wall-stage de l'accueil : les
     tuiles sont posées à opacity:0.78 (ci-dessous), donc leurs interstices/
     transparence doivent voir ce même noir/blanc, pas le fond de page. */
  background: #0a0a0a;
  transition: transform 0.65s ease;
}
html[data-theme="light"] .frozen-wall { background: #f4f4f2; }
.frozen-wall .wall-viewport { pointer-events: none; }
/* le mur figé est toujours en pleine lumière (jamais "à révéler" comme sur
   l'accueil/Projets, où le mur passe du gris au couleur à l'arrivée).
   Opacité volontairement plus basse que le 0.78 par défaut de l'accueil
   (.hero-wall-board) : demande explicite de Guillaume d'assombrir encore,
   pour que le texte du premier écran (toujours superposé ici, contrairement
   à l'accueil où le mot "galaxie" mis à part le texte reste au centre sur
   un fond assez neutre) reste lisible par-dessus une mosaïque de photos. */
.frozen-wall .wall-board {
  /* none plutôt qu'un filtre neutre — voir .wall-stage.is-revealed plus haut */
  filter: none !important; opacity: 0.6 !important; transform: scale(1) !important;
}
/* voile — reproduit EXACTEMENT .hero-overlay + .hw-fade.top de l'accueil
   (mêmes valeurs de radial-gradient), avec le fondu haut converti en
   pourcentage de la hauteur totale de la boîte : sur l'accueil, .hw-fade
   est un élément à part de 26vh de haut avec un dégradé plein jusqu'à 42%
   de SA hauteur (≈ 11vh, donc ≈ 11% d'une boîte de 100vh) puis fondu vers
   transparent jusqu'à 26vh (≈ 26% d'une boîte de 100vh) — .fw-host fait
   ≈ 100vh sur toutes les pages projet concernées, d'où ces pourcentages.
   z-index:3 (pas 1) : .wall-viewport (partagé avec le mur interactif) a
   z-index:2 — sans passer au-dessus, ce voile restait caché SOUS les
   tuiles et ne s'affichait quasiment pas (cause du mur trop clair/saturé
   repéré sur screenshot, la vraie cause n'était pas les valeurs de
   dégradé mais cet ordre d'empilement). */
.frozen-wall::before {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background:
    linear-gradient(to bottom, #0a0a0a 0%, #0a0a0a 14%, rgba(10, 10, 10, 0) 32%),
    radial-gradient(ellipse at center, rgba(10, 10, 10, 0.6) 0%, rgba(10, 10, 10, 0.85) 65%, rgba(10, 10, 10, 0.95) 100%);
  transition: opacity 0.65s ease;
}
html[data-theme="light"] .frozen-wall::before {
  background:
    linear-gradient(to bottom, #f4f4f2 0%, #f4f4f2 14%, rgba(244, 244, 242, 0) 32%),
    radial-gradient(ellipse at center, rgba(244, 244, 242, 0.5) 0%, rgba(244, 244, 242, 0.83) 65%, rgba(244, 244, 242, 0.95) 100%);
}
/* .hw-fade.bottom de l'accueil, même conversion — un simple élément 26vh
   de haut posé en bas, dégradé plein jusqu'à 42% de SA hauteur puis fondu
   vers transparent. Pas de raccord supplémentaire vers var(--bg) : sur
   l'accueil non plus, le bas du mur ne se fond pas dans la section
   suivante (léger écart de teinte toléré, déjà le cas sur l'accueil entre
   #f4f4f2 et var(--bg) #fdfdfd en clair). */
.frozen-wall::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 32%; z-index: 3; pointer-events: none;
  background: linear-gradient(to top, #0a0a0a 0%, #0a0a0a 42%, rgba(10, 10, 10, 0) 100%);
}
html[data-theme="light"] .frozen-wall::after {
  background: linear-gradient(to top, #f4f4f2 0%, #f4f4f2 42%, rgba(244, 244, 242, 0) 100%);
}

/* texte du premier écran, par-dessus le mur figé : couleurs figées comme
   sur le hero de l'accueil (.hero-text — blanc plein par défaut, sombre en
   thème clair) plutôt que les jetons var(--text)/var(--text-soft)/
   var(--accent) habituels, pensés pour un aplat de couleur. Le mur est une
   mosaïque de photos pleinement visible (voir correctif "images cassées"
   plus haut) : le texte a besoin d'un contraste franc et fixe, pas d'une
   couleur qui varie avec le thème du site. Scopé à .fw-host uniquement —
   le reste de chaque page projet (images finales, footer...) garde les
   couleurs de thème normales sur fond uni. */
.fw-host .eyebrow,
.fw-host h1,
.fw-host p.sub,
.fw-host .sketches-head p,
.fw-host .scroll-hint .lbl,
.fw-host .scroll-cue {
  color: #fff;
}
.fw-host .eyebrow { opacity: 0.92; }
.fw-host p.sub,
.fw-host .sketches-head p,
.fw-host .scroll-hint .lbl { opacity: 0.72; }
.fw-host .scroll-hint-pill { border-color: #fff; }
html[data-theme="light"] .fw-host .eyebrow,
html[data-theme="light"] .fw-host h1,
html[data-theme="light"] .fw-host p.sub,
html[data-theme="light"] .fw-host .sketches-head p,
html[data-theme="light"] .fw-host .scroll-hint .lbl,
html[data-theme="light"] .fw-host .scroll-cue {
  color: #1a1a1a;
}
html[data-theme="light"] .fw-host .scroll-hint-pill { border-color: #1a1a1a; }

/* "fermeture" du mur figé (bouton de retour en fin de page projet) :
   mouvement inverse de l'ouverture — le voile s'efface et le mur revient
   en pleine lumière pendant que tout le reste du contenu de la page
   s'estompe (y compris le texte encore présent dans la section qui
   héberge le mur), donnant l'impression que le mur se referme sur la
   page avant de naviguer. */
body.wall-closing .frozen-wall::before { opacity: 0; }
body.wall-closing .frozen-wall { transform: scale(1.045); }
body.wall-closing > *:not(.fw-host) {
  opacity: 0; transition: opacity 0.5s ease; pointer-events: none;
}
body.wall-closing .fw-host > *:not(.frozen-wall) {
  opacity: 0; transition: opacity 0.5s ease; pointer-events: none;
}

/* ==========================================================================
   Méthode — "Ils nous ont fait confiance"
   ========================================================================== */
.clients { border-top: 1px solid var(--border); padding: 110px 44px 130px; }
.clients-inner { max-width: 1180px; margin: 0 auto; }
.clients-head { max-width: 720px; margin-bottom: 70px; }
.clients-head .eyebrow { display: block; margin-bottom: 20px; }
.clients-head h2 { font-weight: 300; font-size: clamp(23px, 2.8vw, 34px); line-height: 1.45; margin-bottom: 18px; }
.clients-head p { font-size: 15px; line-height: 1.8; color: var(--text-soft); }
.clients-grid { display: grid; grid-template-columns: 1fr 2fr; gap: 70px; align-items: start; }
.typo-list h3, .client-list h3 {
  font-size: 10px; letter-spacing: 2.4px; text-transform: uppercase;
  color: var(--accent); font-weight: 700; margin-bottom: 22px;
}
.typo-list ul { list-style: none; }
.typo-list li { font-size: 15.5px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.typo-list li:first-of-type { border-top: 1px solid var(--border); }
.client-list ul { list-style: none; columns: 2; column-gap: 50px; }
.client-list li { font-size: 13.5px; line-height: 2.15; color: var(--text-soft); break-inside: avoid; }
.client-list li em { font-style: normal; color: var(--text-faint); font-size: 12px; }
@media (max-width: 860px) {
  .clients-grid { grid-template-columns: 1fr; gap: 50px; }
  .client-list ul { columns: 1; }
}
@media (max-width: 640px) { .clients { padding: 80px 20px 90px; } }

/* --------------------------------------------------------------------------
   TITRE LU MAIS PAS VU
   La page Projets est un mur plein écran : elle n'a aucun titre à l'écran, et
   ne doit pas en avoir — ce serait un bandeau de texte au milieu des images.
   Or une page sans titre principal (<h1>) est une page dont Google ne sait pas
   dire de quoi elle parle, et qu'un lecteur d'écran ouvre sans annoncer où
   l'on vient d'arriver.
   Cette classe place le titre hors du champ de vision sans le retirer de la
   page : il reste lu par Google et par l'assistance vocale. Le texte doit
   correspondre au contenu réel de la page — un titre caché qui raconte autre
   chose que la page serait, lui, sanctionné.
   -------------------------------------------------------------------------- */
.titre-lu {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   .liste-lue — la liste des projets lisible sans JavaScript
   --------------------------------------------------------------------------
   Meme technique que .titre-lu ci-dessus : l'element reste dans le document
   et dans l'arbre d'accessibilite, mais n'occupe aucune place a l'ecran.

   Le mur d'images est construit par js/main.js apres le chargement. Sans
   cette liste, le HTML brut de l'accueil et de /projets.html ne contient
   AUCUN lien vers les 35 pages projet : Bing, LinkedIn et surtout les robots
   des moteurs IA (GPTBot, ClaudeBot, PerplexityBot, Applebot) ne les
   trouvaient pas. La liste est generee par outils/index-lisible.js entre les
   marqueurs KOJA-INDEX — ne pas l'ecrire a la main.

   Elle sert aussi de repli reel : navigation au clavier, lecteur d'ecran, et
   visiteur sans JavaScript. Son contenu correspond exactement a ce que le
   mur affiche une fois le JS execute — ne jamais y mettre autre chose.
   -------------------------------------------------------------------------- */
.liste-lue {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
}
