/* =============================================================
   Galerie client — Eloic Foucart
   Design tokens repris à l'identique du portfolio eloicfoucart.com
   ============================================================= */

/* ---- Fontes auto-hébergées ---- */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/sg-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/sg-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/jbm-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('../fonts/jbm-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Inter — le texte courant.
   ------------------------------------------------------------------------
   Space Grotesk est une fonte de TITRE : son œil large et ses terminaisons
   géométriques portent un nom de reportage, mais fatiguent sur une phrase de
   quinze mots. Elle reste aux titres ; le texte passe à Inter, dessinée pour
   l'écran et déjà livrée avec le site (aucun fichier de plus).
   Les deux découpes suivent celles des autres fontes : l'essentiel d'abord,
   les caractères rares seulement s'ils paraissent. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
                 U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF,
                 U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
                 U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---- Tokens ---- */
:root {
  color-scheme: light;

  /* Le papier est ivoire, et non blanc d'écran : c'est celui du site public
     (assets/site/pages.css) et de l'écran de chargement, et l'accent de
     l'administration est un terracotta — un papier froid sous un accent
     chaud, cela se voyait. Le décalage est faible et tous les contrastes
     tiennent : l'encre reste à 17,7:1, les gris à 5,2:1. */
  --paper-0:   #faf7f0;
  --paper-1:   #f1ece2;
  --paper-2:   #e6e0d3;
  --paper-ink: #0d1113;
  --muted:     #6b7280;
  --muted-2:   #9aa1a6;
  --line:      rgba(0,0,0,.09);

  --ink-0:     #0d1113;
  --ink-1:     #131719;
  --ink-2:     #1b2023;

  --accent:    #0d1113;
  --danger:    #b4442f;
  --warning:   #c98a2b;
  --success:   #2f7d5c;

  /* ---- Les états d'un dossier ------------------------------------------
     Six teintes dans le registre des trois précédentes : sourdes, terreuses,
     jamais vives. Elles ne classent pas par catégorie mais par **à qui revient
     le prochain geste** — c'est ce qu'on cherche en balayant une liste de
     projets un lundi matin.

       demande   froid, entrant      → vous devez répondre
       devis     ocre, attente       → la balle est chez la cliente
       confirme  vert, engagé        → la date est prise, rien à faire d'ici là
       realise   prune, en cours     → vous devez livrer
       livre     gris, terminé       → l'œil doit passer sans s'arrêter
       perdu     terre cuite, classé

     « Livré » est volontairement neutre : un dossier fini ne doit pas attirer
     autant qu'un dossier qui attend. Toutes tiennent au moins 4,5 de contraste
     sur le papier, mesuré — elles servent de texte, pas seulement de pastille. */
  /* ---- L'accent de l'administration ------------------------------------
     Le mobilier de l'interface — titres de carte, en-têtes de tableau, onglets,
     compteurs — était entièrement gris : 6 % des éléments seulement portaient
     une couleur, et uniquement sur les badges d'état.

     Cette terre de Sienne réchauffe l'ensemble. C'est la teinte la plus proche
     du terra cotta de « sans suite » — ΔE 21 en CIELAB, du même ordre que
     l'écart entre « livré » et « séance faite ». Les deux ne se rencontrent
     qu'au tableau des projets, où l'état porte en plus une pastille et un
     libellé : la couleur n'y est jamais seule à parler.

     La face cliente n'y touche pas — les photos doivent rester seules à colorer
     une galerie. */
  --accent-adm:      #8c4a2f;
  --accent-adm-vif:  #a1552f;
  --accent-adm-doux: #f3ece8;

  --etat-demande:  #3d6a86;
  /* Assombri de #9c6718 : sur le fond teinté du récapitulatif de devis il
     tombait à 4,25 de contraste, sous le seuil. Il passe maintenant sur les
     deux fonds — papier et papier teinté. Mesuré, pas estimé. */
  --etat-devis:    #8f5f16;
  --etat-confirme: #2f7d5c;
  --etat-realise:  #6b5c80;
  --etat-livre:    #5f6b66;
  --etat-perdu:    #b4442f;

  --font-display: 'Space Grotesk', 'Space Grotesk repli', 'Inter', system-ui, sans-serif;
  --font-body:    'Inter', 'Inter repli', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono:    'JetBrains Mono', 'JetBrains Mono repli', ui-monospace, 'SF Mono', Menlo, monospace;

  /* La fonte à empattements des galeries.
     Aucun fichier n'est téléchargé : c'est celle du système du lecteur, prise
     dans l'ordre des plus belles. Iowan Old Style et Palatino sont sur les
     Mac et les iPhone, Cambria et Constantia sur Windows, et Georgia partout
     depuis toujours. Une vraie fonte de livre sur chaque machine, pour zéro
     octet — là où en embarquer une coûterait 80 Ko avant la première photo.
     Le compromis assumé : deux clientes ne voient pas exactement la même. */
  --font-serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua',
                Cambria, Constantia, Georgia, 'Times New Roman', serif;

  --text-xs:   12px;
  --text-sm:   14px;
  --text-base: 16px;
  --text-md:   20px;

  /* L'espacement vit dans echelle.css — huit crans, un seul endroit,
     les trois faces. Chargée avant celle-ci. */

  --radius-sm:   6px;
  --radius-md:   12px;
  --radius-pill: 999px;

  /* Le mouvement, en trois temps et une courbe.
     ----------------------------------------------------------------------
     Il y avait vingt-huit durées différentes dans les feuilles : chacune
     choisie pour elle-même, aucune en rapport avec les autres. On ne voit pas
     la différence entre 220 et 240 ms, mais on sent que l'application n'a pas
     un rythme — elle en a vingt-huit.

       micro  ce qui répond au doigt : survol, appui, bascule
       ui     ce qui se déplace : un panneau, une barre, un repli
       enter  ce qui apparaît : une image qui arrive, une page qui entre

     Une seule courbe pour tout ce qui sort ; la symétrique est réservée aux
     allers-retours (un voile qui s'ouvre et se ferme). */
  --ease-out:    cubic-bezier(.2,.8,.2,1);
  --ease-in-out: cubic-bezier(.76,0,.24,1);
  --dur-micro:   120ms;
  --dur-ui:      220ms;
  --dur-enter:   400ms;

  --sat: env(safe-area-inset-top,    0px);
  --sar: env(safe-area-inset-right,  0px);
  /* Blanc posé sur une image ou un voile sombre : visionneuse, couverture,
     lecteur vidéo, pastilles. Il ne suit pas le thème — ces surfaces sont
     toujours sombres — mais il mérite un nom : « #fff » répété vingt-six fois
     ne dit pas pourquoi, et rend tout ajustement impossible en un point. */
  --sur-media: #ffffff;

  /* Le fond derrière une vidéo qui ne remplit pas son cadre. Noir absolu et
     non « --ink-0 » : les bandes doivent disparaître, pas former un cadre gris
     autour de l'image. */
  --letterbox: #000000;

  /* Le bandeau d'aperçu administrateur — jamais vu par un client. Il emprunte
     la couleur d'alerte, mais en fond plein : c'est un état, pas un message. */
  --bandeau-apercu: var(--danger);

  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left,   0px);
}

/* ---- Surfaces : les composants s'expriment en --fg / --bg / --hairline ----
   Le second sélecteur sert au thème « auto » : le script du <head> pose
   data-theme sur <html> avant le premier rendu, et sa spécificité plus forte
   retourne la surface sombre rendue par le serveur, sans clignotement. */
.surface-paper,
html[data-theme="light"] body.surface-dark {
  color-scheme: light;
  --bg:       var(--paper-0);
  --bg-2:     var(--paper-1);
  --bg-3:     var(--paper-2);
  --fg:       var(--paper-ink);
  --fg-2:     rgba(13,17,19,.62);
  --fg-3:     rgba(13,17,19,.38);
  --fg-4:     rgba(13,17,19,.20);
  --hairline: rgba(0,0,0,.09);
  --hover:    rgba(0,0,0,.035);
  --btn-fg:   var(--paper-0);
  --btn-bg:   var(--paper-ink);
}

/* Pas de « html[data-theme="dark"] body.surface-paper » ici.
   Ce sélecteur promettait une administration en thème sombre qu'elle ne sait
   pas rendre : admin.css fixe 24 fonds sur la palette claire (--paper-*) au
   lieu des tokens de surface, si bien que les fonds restaient clairs pendant
   que --fg passait au blanc — du texte à 1,01 de contraste, invisible.
   Il était de toute façon inatteignable : le serveur choisit lui-même la
   classe de surface (Theme::surface()), et le script d'amorçage ne pose jamais
   que data-theme="light". La galerie cliente, elle, est bien thémée : app.css
   n'emploie aucune couleur figée. Rendre l'administration sombre demanderait
   de convertir ces 24 emplois — un chantier, pas un sélecteur. */
/* « .lb » n'est PLUS ici, volontairement.
   La visionneuse imposait ses propres jetons sombres — posés sur l'élément
   lui-même, ils l'emportaient donc sur ceux hérités du corps. Une galerie
   claire ouvrait ses photos sur du noir, ce qui se voyait comme un défaut
   parce que c'en était un : la visionneuse fait partie de la galerie, pas
   d'une application à part.
   Elle hérite maintenant. Sombre sur une galerie sombre, claire sur une
   galerie claire — et le film, lui, garde son fond noir par --letterbox,
   qui n'a rien à voir avec la surface. */
.surface-dark {
  color-scheme: dark;
  --bg:       var(--ink-0);
  --bg-2:     var(--ink-1);
  --bg-3:     var(--ink-2);
  --fg:       rgba(255,255,255,.92);
  --fg-2:     rgba(255,255,255,.48);
  --fg-3:     rgba(255,255,255,.28);
  --fg-4:     rgba(255,255,255,.14);
  --hairline: rgba(255,255,255,.10);
  --hover:    rgba(255,255,255,.04);
  --btn-fg:   var(--ink-0);
  --btn-bg:   rgba(255,255,255,.92);

  /* Les mêmes teintes, éclaircies : posées telles quelles sur l'encre, elles
     tombaient sous 3 de contraste. L'administration est sur papier, mais rien
     n'oblige un état à y rester — et un jeton qui ment sur une surface est un
     piège pour la prochaine page qui l'emploiera. */
  --etat-demande:  #7fa8c4;
  --etat-devis:    #d9a34a;
  --etat-confirme: #6fbf9a;
  --etat-realise:  #a394bd;
  --etat-livre:    #9aa8a2;
  --etat-perdu:    #e08a72;
}

/* ---- Reset & base ---- */
*, *::before, *::after { box-sizing: border-box; }
html { height: 100%; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
img, video { display: block; max-width: 100%; }
a { color: inherit; }
button, input, select, textarea { font-family: inherit; font-size: inherit; color: inherit; }
h1, h2, h3, h4 { font-weight: 400; margin: 0; }
p { margin: 0; }
:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 2px; }

/* L'attribut hidden doit toujours l'emporter : sans cela, un display: flex posé
   par une classe rendrait visible un bloc que le HTML déclare masqué. */
[hidden] { display: none !important; }

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

.wrap {
  max-width: 1480px;
  margin: 0 auto;
  padding-left:  max(clamp(20px, 3.4vw, 48px), var(--sal));
  padding-right: max(clamp(20px, 3.4vw, 48px), var(--sar));
}

/* ---- Keyframes partagées ---- */
@keyframes fadeUp  { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }
@keyframes fadeIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes panelIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@keyframes spin    { to { transform: rotate(360deg); } }

/* ---- Typographie ----
   ------------------------------------------------------------------------
   Six rôles, et rien d'autre : titre, section, texte, appui, étiquette,
   chiffre. Toute taille écrite ailleurs qu'ici est une septième voix.

   L'ÉTIQUETTE est la seule chose qui s'écrive en chasse fixe et en
   capitales : nom de carte, intitulé de champ, état, action courte. Elle
   avait six variantes — 9, 10, 11 et 12 px, trois interlettrages — qui ne se
   distinguaient pas à l'œil mais empêchaient tout d'aligner. Elle en a deux :
   10 px dans une carte, 11 px au-dessus d'un titre de page, où elle serait
   sinon trop maigre. Toutes deux à .16em, en capitales.

   Partout ailleurs, le texte courant reprend sa place (voir --font-body). */
:root {
  --etiq:       10px;
  --etiq-page:  11px;
  --etiq-chasse: .16em;
}

.kicker {
  font-family: var(--font-mono);
  font-size: var(--etiq-page);
  letter-spacing: var(--etiq-chasse);
  text-transform: uppercase;
  color: var(--fg-2);
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin: 0;
}
.kicker-dot { opacity: .35; }

.display {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(38px, 7vw, 92px);
  letter-spacing: -.035em;
  line-height: .95;
  color: var(--fg);
  margin: 0;
}
.display-sm {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(28px, 4vw, 46px);
  letter-spacing: -.03em;
  line-height: 1.02;
  margin: 0;
}
.lead {
  font-size: clamp(17px, 1.6vw, 21px);
  line-height: 1.55;
  letter-spacing: -.01em;
  color: var(--fg);
}
.text-body { font-size: clamp(14px, 1.1vw, 16px); line-height: 1.8; color: var(--fg-2); }
.mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .1em;
  color: var(--fg-2);
}
.muted { color: var(--fg-2); }

/* ---- Boutons ---- */
/* Un bouton dit ce qu'il fait, dans la langue du reste.
   ------------------------------------------------------------------------
   Il était en chasse fixe, capitales, .18em — la même voix que les états et
   les intitulés. Trois choses qui ne se ressemblent pas portaient le même
   costume, et l'action, qui doit se repérer d'un coup d'œil, se fondait dans
   l'étiquetage. Elle reprend donc la voix du texte, en demi-gras.

   La forme aussi : la pastille revient aux ÉTATS — un badge est une pastille
   par nature. Une action est un rectangle à coins doux, comme sur le site. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-3);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  padding: 12px 20px;
  min-height: 44px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out),
              opacity var(--dur-micro) var(--ease-out);
}
@media (hover: hover) { .btn:hover { background: var(--hover); border-color: var(--fg-3); } }
.btn[disabled], .btn.is-disabled { opacity: .4; pointer-events: none; }

.btn-solid { background: var(--btn-bg); color: var(--btn-fg); border-color: transparent; }
@media (hover: hover) { .btn-solid:hover { opacity: .82; background: var(--btn-bg); } }

.btn-sm { padding: 8px 14px; min-height: 36px; font-size: 13px; }
.btn-ghost { border-color: transparent; }
.btn-danger { color: var(--danger); border-color: rgba(180,68,47,.35); }

/* Le bouton qui attend.
   --------------------------------------------------------------------------
   Il reste lisible : .btn[disabled] tombe à 40 % d'opacité, ce qui convient à
   un bouton hors d'usage, pas à un bouton qui travaille — c'est justement le
   moment où l'on cherche à lire ce qu'il raconte.

   L'anneau tourne devant le texte. Pas de marge : .btn est un inline-flex avec
   un gap, qui espace déjà ses enfants — en ajouter une les éloignerait deux
   fois. */
.btn.est-occupe[disabled] { opacity: .78; pointer-events: none; cursor: progress; }
.btn.est-occupe::before {
  content: '';
  width: 1em; height: 1em;
  flex: 0 0 auto;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-tourne 700ms linear infinite;
}
@keyframes btn-tourne { to { transform: rotate(1turn); } }

/* Sans animation : un point fixe suffit à dire « en cours », et il ne tourne
   pas dans le champ de vision de quelqu'un que cela gêne. */
@media (prefers-reduced-motion: reduce) {
  .btn.est-occupe::before { animation: none; border-top-color: currentColor; opacity: .5; }
}

/* Un bouton rouge plein : réservé à l'action d'une confirmation, où elle doit
   être la plus visible de la boîte, pas la plus discrète. */
.btn-danger.btn-solid { background: var(--danger); color: #fff; border-color: transparent; }
@media (hover: hover) { .btn-danger.btn-solid:hover { opacity: .88; background: var(--danger); } }

/* ---------------------------------------------------------------------------
   La demande de confirmation
   ---------------------------------------------------------------------------
   À la place de la boîte du système, qui écrivait « 127.0.0.1 dit » au-dessus
   de la question et ne connaissait ni la police ni le thème.

   On s'appuie sur <dialog> : la couche supérieure, le piège à tabulation,
   Échap et le retour du focus sont au navigateur. Reste l'habillage.

   Le voile est flouté comme les panneaux du site : on doit sentir que la page
   est encore là, en attente, et non remplacée. */
.confirme {
  max-width: min(420px, calc(100vw - 2 * var(--space-4)));
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--bg);
  color: var(--fg);
  box-shadow: 0 24px 64px rgba(0,0,0,.28);
}
.confirme::backdrop {
  background: rgba(0,0,0,.42);
  backdrop-filter: blur(6px);
}
.confirme-corps {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
}
.confirme-titre {
  margin: 0;
  font-family: var(--font-display);
  font-size: 19px; font-weight: 600; line-height: 1.3;
  text-wrap: balance;
}
.confirme-dit {
  margin: 0;
  font-size: 14px; line-height: 1.6;
  color: var(--fg-2);
}
/* L'action à droite, là où va la main ; l'échappatoire à côté, pas en dessous :
   une annulation isolée en bas se lit comme une seconde question. */
.confirme-pied {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  margin-top: var(--space-1);
}

/* La boîte arrive, elle n'apparaît pas : le temps d'entrée, pas celui d'un
   survol, pour qu'on ait le réflexe de lire avant de cliquer. */
@media (prefers-reduced-motion: no-preference) {
  .confirme[open] { animation: confirme-entre var(--dur-enter) var(--ease-out); }
  .confirme[open]::backdrop { animation: confirme-voile var(--dur-ui) var(--ease-out); }
}
@keyframes confirme-entre {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
@keyframes confirme-voile { from { opacity: 0; } to { opacity: 1; } }

@media (max-width: 420px) {
  .confirme-pied { flex-direction: column-reverse; }
  .confirme-pied .btn { width: 100%; justify-content: center; }
}

/* Lien-CTA souligné, comme les CTA du portfolio */
/* Le même appel à l'action, qu'il soit écrit <a> ou <button>. Sans la remise
   à zéro ci-dessous, le navigateur habille les <button> de sa boîte grise :
   fond, bordure en relief, texte centré. Les deux ne se ressembleraient plus. */
.cta {
  display: inline-flex; align-items: center; gap: var(--space-3);
  font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--fg); text-decoration: none;
  appearance: none; -webkit-appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--fg-3);
  width: auto;
  padding: 0 0 3px;
  text-align: left;
  cursor: pointer;
  transition: opacity var(--dur-ui) var(--ease-out);
}
@media (hover: hover) { .cta:hover { opacity: .5; } }

/* Au doigt, le CTA ne mesurait que 20 px de haut. Lui imposer une hauteur
   minimale décollerait son soulignement du texte de vingt pixels : c'est le
   trait qui fait le CTA. On étend donc la seule zone de contact, centrée sur
   le texte, sans rien déplacer à l'écran. */
@media (pointer: coarse) {
  .cta { position: relative; }
  .cta::after {
    content: ''; position: absolute; left: 0; right: 0;
    top: 50%; transform: translateY(-50%); height: 44px;
  }
}

/* ---- Champs ---- */
.field { display: block; margin-bottom: var(--space-6); }
/* L'étiquette dit à quoi sert le champ : elle doit se lire. --fg-3 la laissait
   à 2,5 de contraste sur la surface claire, sous le minimum. */
.field-label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--etiq); letter-spacing: var(--etiq-chasse); text-transform: uppercase;
  color: var(--fg-2);
  margin-bottom: var(--space-2);
}
.field-input, .field-select, .field-textarea {
  width: 100%;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--hairline);
  padding: 10px 0;
  font-size: var(--text-base);
  letter-spacing: -.01em;
  color: var(--fg);
  border-radius: 0;
  transition: border-color var(--dur-micro) var(--ease-out);
}
.field-textarea { min-height: 110px; resize: vertical; line-height: 1.7; }

/* Un champ de fichier reste un contrôle natif : le navigateur y dessine son
   propre bouton gris, avec sa police système et son relief. Au milieu de
   boutons en mono majuscule, il détonne. On habille donc le bouton lui-même,
   sans rien changer au comportement. */
.field-input[type="file"] {
  padding: 8px 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .04em;
  color: var(--fg-2);
  cursor: pointer;
}
.field-input[type="file"]::file-selector-button {
  margin-right: 14px;
  padding: 9px 18px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .18em;
  text-transform: uppercase;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
@media (hover: hover) {
  .field-input[type="file"]::file-selector-button:hover {
    background: var(--hover); border-color: var(--fg-3);
  }
}
/* À la mise au point, l'ourlet fonce ET s'épaissit.
   La couleur seule ne suffisait pas : sur un filet d'un pixel, passer de 38 %
   à 100 % de noir est un changement que tout le monde ne distingue pas. Le
   second trait vient d'une ombre portée, qui n'occupe aucune place et ne
   décale donc pas la ligne du champ. */
.field-input:focus, .field-select:focus, .field-textarea:focus {
  outline: none;
  border-bottom-color: var(--fg);
  box-shadow: 0 1px 0 0 var(--fg);
}
/* --fg-4 donnait 1,5 de contraste : « 1 200,00 » et « Reportage mariage —
   journée complète » ne se lisaient pas, alors qu'ils sont là pour montrer la
   forme attendue. --fg-3 reste plus faible qu'une vraie valeur, qu'on ne doit
   pas confondre avec un exemple. */
.field-input::placeholder, .field-textarea::placeholder { color: var(--fg-3); }
.field-hint { display: block; margin-top: 6px; font-size: 13px; color: var(--fg-2); line-height: 1.55; }
.field-error { color: var(--danger); font-size: 13px; margin-top: 6px; }
/* Interrupteur */
.switch { display: flex; align-items: center; gap: var(--space-3); cursor: pointer; padding: 10px 0; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch-track {
  width: 40px; height: 22px; flex: none;
  border-radius: var(--radius-pill);
  background: var(--fg-4);
  position: relative;
  transition: background var(--dur-micro) var(--ease-out);
}
.switch-track::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--bg);
  transition: transform var(--dur-micro) var(--ease-out);
}
.switch input:checked + .switch-track { background: var(--fg); }
.switch input:checked + .switch-track::after { transform: translateX(18px); }
.switch-text { font-size: var(--text-sm); line-height: 1.4; }
.switch-text small { display: block; color: var(--fg-2); font-size: 12px; margin-top: 2px; }

/* ---- Notifications ---- */
.toast-stack {
  /* Ancrée sur les deux bords plutôt que centrée par transform : posée à
     « left: 50% », la pile n'avait pour largeur disponible que la moitié droite
     de l'écran, et toutes les notifications s'y trouvaient bridées — visible
     surtout sur les messages longs, réduits à une colonne étroite. */
  position: fixed; left: 0; right: 0; bottom: calc(24px + var(--sab));
  z-index: 200; display: flex; flex-direction: column; gap: var(--space-2); align-items: center;
  padding: 0 12px;
  pointer-events: none;
}
/* Une notification peut porter une action — c'est là qu'on annule un geste
   qu'on vient de faire, pendant qu'on y pense encore. */
.toast { display: flex; align-items: center; gap: var(--space-4); }
.toast-action {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .16em; text-transform: uppercase;
  color: inherit; white-space: nowrap;
  border-bottom: 1px solid currentColor;
}
@media (hover: hover) { .toast-action:hover { opacity: .65; } }

.toast {
  background: var(--ink-0); color: #fff;
  border: 1px solid rgba(255,255,255,.12);
  padding: 12px 20px; border-radius: var(--radius-pill);
  font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: .06em;
  opacity: 0; transform: translateY(10px);
  transition: opacity var(--dur-ui) var(--ease-out), transform var(--dur-ui) var(--ease-out);
  max-width: min(90vw, 460px); text-align: center;
}
.toast.show { opacity: 1; transform: none; }
.toast.is-error { background: var(--danger); border-color: transparent; }

/* Notification longue : ce n'est plus une confirmation, c'est une explication.
   Elle se lit comme un paragraphe — alignée à gauche, dans la police de
   lecture, sans interlettrage étiré, et les retours à la ligne du message sont
   conservés. La pilule laisse place à un cadre : un rayon de 999 px sur six
   lignes de texte donne une tache ovale où plus rien ne s'accroche. */
.toast.is-long {
  display: block;
  max-width: min(94vw, 560px);
  padding: 16px 20px;
  border-radius: var(--radius-md);
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  letter-spacing: 0;
  line-height: 1.55;
  white-space: pre-line;
}

/* ---- Chargement ---- */
.spinner {
  width: 18px; height: 18px; flex: none;
  border: 1.5px solid var(--fg-4);
  border-top-color: var(--fg);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

/* ---- Barre de progression ---- */
.progress {
  height: 2px; background: var(--fg-4); border-radius: 2px; overflow: hidden;
}
.progress-fill {
  height: 100%; width: 0; background: var(--fg);
  transition: width var(--dur-ui) var(--ease-out);
}

/* ---- Badges ---- */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--etiq);
  letter-spacing: var(--etiq-chasse); text-transform: uppercase;
  padding: 5px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--hairline); color: var(--fg-2);
  white-space: nowrap;
}
/* Un état se porte en attribut : le même mécanisme sert aux projets, aux devis,
   aux contrats et aux messages, et l'œil n'apprend la convention qu'une fois.
   Texte et filet teintés, fond transparent — un aplat de couleur ferait une
   tache dans une page de papier et de filets. */
/* Le fond porte la couleur, le texte passe à l'encre.
   Teinter le fond ET le texte de la même teinte les rapproche : au mieux 4,29
   de contraste, sous le seuil quelle que soit l'opacité — mesuré sur les six.
   Avec l'encre on monte à 14, et la couleur reste entièrement lisible puisque
   c'est le fond qui la porte.

   20 % : assez pour que l'état saute aux yeux dans un tableau, assez peu pour
   que six pastilles colorées à l'écran ne fassent pas une guirlande. */
.badge[data-etat] {
  color: var(--fg);
  background: color-mix(in srgb, var(--teinte) 20%, transparent);
  border-color: color-mix(in srgb, var(--teinte) 34%, transparent);
}
.badge[data-etat]::before {
  content: ''; flex: none;
  width: 5px; height: 5px; border-radius: 50%;
  /* La teinte, et non currentColor : le texte est passé à l'encre, et une
     pastille noire ne dirait plus rien. */
  background: var(--teinte);
}

/* Sans color-mix — Safari d'avant 16.2 — le fond reste transparent et la
   couleur revient au texte, comme avant. Rien ne casse. */
@supports not (background: color-mix(in srgb, red 20%, transparent)) {
  .badge[data-etat] { color: var(--teinte); }
  .badge[data-etat]::before { background: currentColor; }
}
.badge[data-etat="demande"]  { --teinte: var(--etat-demande); }
.badge[data-etat="devis"]    { --teinte: var(--etat-devis); }
.badge[data-etat="confirme"] { --teinte: var(--etat-confirme); }
.badge[data-etat="realise"]  { --teinte: var(--etat-realise); }
.badge[data-etat="livre"]    { --teinte: var(--etat-livre); }
.badge[data-etat="perdu"]    { --teinte: var(--etat-perdu); }

/* Les documents empruntent les mêmes teintes : un devis en attente est jaune
   comme un projet en attente. Une seconde échelle de couleurs demanderait de
   retenir deux conventions. */
.badge[data-etat="brouillon"] { --teinte: var(--etat-livre); }
.badge[data-etat="envoye"]    { --teinte: var(--etat-devis); }
.badge[data-etat="accepte"],
.badge[data-etat="signe"],
.badge[data-etat="parti"]     { --teinte: var(--etat-confirme); }
.badge[data-etat="refuse"],
.badge[data-etat="echoue"]    { --teinte: var(--etat-perdu); }
.badge[data-etat="expire"]    { --teinte: var(--etat-livre); }

/* Sans color-mix — Safari d'avant 16.2 — le filet reste le hairline d'origine
   et seule la couleur du texte porte l'information. Rien ne casse. */

/* Les statuts de galerie rejoignent la même échelle. Les valeurs ne changent
   pas — --success et --danger sont déjà les teintes « confirmé » et « perdu » —
   mais elles portent désormais leur pastille, comme les états d'un dossier.
   Une seule convention à retenir dans toute l'administration. */
.badge-live    { --teinte: var(--etat-confirme); }
.badge-draft   { --teinte: var(--etat-livre); }   /* un statut se lit : --fg-3 tombait à 2,5 de contraste */
.badge-expired { --teinte: var(--etat-perdu); }

.badge-live, .badge-draft, .badge-expired {
  color: var(--teinte);
  border-color: color-mix(in srgb, var(--teinte) 34%, transparent);
}
.badge-live::before, .badge-draft::before, .badge-expired::before {
  content: ''; flex: none;
  width: 5px; height: 5px; border-radius: 50%;
  background: currentColor;
}

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

/* ---- Polices de repli aux mêmes métriques ----
   Sans cela, le texte saute au moment où Space Grotesk arrive. Ces valeurs
   alignent la police système sur la nôtre le temps du chargement. */
@font-face {
  font-family: 'Space Grotesk repli';
  src: local('Helvetica Neue'), local('Arial'), local('Roboto');
  size-adjust: 97%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}
@font-face {
  font-family: 'JetBrains Mono repli';
  src: local('SF Mono'), local('Menlo'), local('Consolas'), local('monospace');
  size-adjust: 98%;
  ascent-override: 102%;
  descent-override: 30%;
  line-gap-override: 0%;
}
/* Inter est très proche des grotesques du système : le repli n'a presque rien
   à corriger, et c'est tant mieux — le texte courant est ce qui saute le plus
   visiblement quand la fonte arrive après lui. */
@font-face {
  font-family: 'Inter repli';
  src: local('Helvetica Neue'), local('Segoe UI'), local('Roboto'), local('Arial');
  size-adjust: 100%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}

/* ═══ Display P3 — généré par bin/couleurs-p3.php, ne pas modifier à la main ═══ */
@supports (color: color(display-p3 0 0 0)) {
  @media (color-gamut: p3) {
    :root {
      --paper-0: color(display-p3 0.9783 0.969 0.9439);
      --paper-1: color(display-p3 0.9417 0.9261 0.8902);
      --paper-2: color(display-p3 0.8978 0.8792 0.8326);
      --paper-ink: color(display-p3 0.054 0.0662 0.0736);
      --muted: color(display-p3 0.4246 0.4462 0.497);
      --muted-2: color(display-p3 0.6089 0.6305 0.6488);
      --line: color(display-p3 0 0 0 / 0.09);
      --ink-0: color(display-p3 0.054 0.0662 0.0736);
      --ink-1: color(display-p3 0.0775 0.0897 0.0971);
      --ink-2: color(display-p3 0.1096 0.1249 0.136);
      --accent: color(display-p3 0.054 0.0662 0.0736);
      --danger: color(display-p3 0.6543 0.2955 0.2147);
      --warning: color(display-p3 0.7519 0.5517 0.2487);
      --success: color(display-p3 0.2712 0.4836 0.37);
      --accent-adm: color(display-p3 0.5146 0.3034 0.207);
      --accent-adm-vif: color(display-p3 0.592 0.3486 0.2167);
      --accent-adm-doux: color(display-p3 0.9481 0.9264 0.9117);
      --etat-demande: color(display-p3 0.2805 0.4113 0.5151);
      --etat-devis: color(display-p3 0.5334 0.3807 0.1529);
      --etat-confirme: color(display-p3 0.2712 0.4836 0.37);
      --etat-realise: color(display-p3 0.4099 0.3629 0.4921);
      --etat-livre: color(display-p3 0.3814 0.4181 0.401);
      --etat-perdu: color(display-p3 0.6543 0.2955 0.2147);
      --sur-media: color(display-p3 1 1 1);
      --letterbox: color(display-p3 0 0 0);
    }
    .surface-paper, html[data-theme="light"] body.surface-dark {
      --fg-2: color(display-p3 0.054 0.0662 0.0736 / 0.62);
      --fg-3: color(display-p3 0.054 0.0662 0.0736 / 0.38);
      --fg-4: color(display-p3 0.054 0.0662 0.0736 / 0.2);
      --hairline: color(display-p3 0 0 0 / 0.09);
      --hover: color(display-p3 0 0 0 / 0.035);
    }
    .surface-dark {
      --fg: color(display-p3 1 1 1 / 0.92);
      --fg-2: color(display-p3 1 1 1 / 0.48);
      --fg-3: color(display-p3 1 1 1 / 0.28);
      --fg-4: color(display-p3 1 1 1 / 0.14);
      --hairline: color(display-p3 1 1 1 / 0.1);
      --hover: color(display-p3 1 1 1 / 0.04);
      --btn-bg: color(display-p3 1 1 1 / 0.92);
      --etat-demande: color(display-p3 0.5312 0.6543 0.7577);
      --etat-devis: color(display-p3 0.8185 0.6477 0.3504);
      --etat-confirme: color(display-p3 0.5098 0.7412 0.6136);
      --etat-realise: color(display-p3 0.6293 0.5825 0.7294);
      --etat-livre: color(display-p3 0.6141 0.6571 0.6365);
      --etat-perdu: color(display-p3 0.8313 0.5568 0.4665);
    }
  }
}
/* ═══ fin du bloc Display P3 ═══ */
