/* xtn.css — bundle vanilla de la charte Onyx & Jade. GÉNÉRÉ par frontend/scripts/build_charte.py. */

/* Polices de la charte — Inter 4.1 (texte + Display via l'axe opsz) et JetBrains Mono 2.304.
   Auto-hébergées (CSP `font-src 'self'`), sous-ensembles faits depuis les releases officielles avec
   TOUTES les features conservées : Google Fonts et Fontsource amputent les `cv*` / `ss*` / `zero`, et
   c'est précisément le réglage (I à empattements, 1 à base, zéro barré) qui donne son caractère à l'app.
   Fichiers : frontend/public/fonts/ (licences OFL à côté). */

@font-face {
  font-family: "Inter";
  src: url("/fonts/InterVariable-4.1-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
    U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  src: url("/fonts/InterVariable-4.1-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-017F, U+0218-021B, U+0237, U+1E9E;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/JetBrainsMono-2.304-latin.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

/* Replis métriques (méthode next/font) : tant qu'Inter n'est pas là, la police système est mise à
   l'échelle d'Inter, donc rien ne saute quand elle arrive. Une famille par police locale : un
   `local()` introuvable fait échouer sa règle et le navigateur passe à la famille suivante. */
@font-face {
  font-family: "Inter Fallback HN";
  src: local("Helvetica Neue");
  size-adjust: 106.12%;
  ascent-override: 91.29%;
  descent-override: 22.73%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback Arial";
  src: local("Arial");
  size-adjust: 107.12%;
  ascent-override: 90.44%;
  descent-override: 22.52%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback Segoe";
  src: local("Segoe UI");
  size-adjust: 107.71%;
  ascent-override: 89.94%;
  descent-override: 22.39%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "JetBrains Mono Fallback";
  src: local("Menlo");
  size-adjust: 99.66%;
  ascent-override: 102.35%;
  descent-override: 30.1%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "JetBrains Mono Fallback Consolas";
  src: local("Consolas");
  size-adjust: 109.13%;
  ascent-override: 93.47%;
  descent-override: 27.49%;
  line-gap-override: 0%;
}


/* GÉNÉRÉ par frontend/scripts/check_palette.py — ne pas éditer à la main.
   Charte « Onyx & Jade » (docs/design/CHARTE.md). Dark par défaut, clair sur [data-theme="light"].
   Les valeurs brutes sont --xtn-* ; Tailwind les expose en --color-* (bg-bg, text-fg-muted, border-border…). */


:root {
  color-scheme: dark;
  --xtn-bg: #000000; /* oklch(0.000 0.000 0) */
  --xtn-surface: #0c0f0e; /* oklch(0.165 0.005 174) */
  --xtn-surface-2: #141816; /* oklch(0.204 0.007 164) */
  --xtn-surface-3: #1d221f; /* oklch(0.246 0.009 159) */
  --xtn-border: #272d2a; /* oklch(0.290 0.010 164) */
  --xtn-border-strong: #414744; /* oklch(0.391 0.009 165) */
  --xtn-fg: #ecefee; /* oklch(0.950 0.003 174) */
  --xtn-fg-muted: #b1b5b3; /* oklch(0.769 0.005 165) */
  --xtn-fg-faint: #7d827f; /* oklch(0.601 0.007 160) */
  --xtn-accent: #58d8ae; /* oklch(0.799 0.129 168) */
  --xtn-accent-hover: #66e6ba; /* oklch(0.841 0.131 168) */
  --xtn-accent-active: #4bcaa0; /* oklch(0.758 0.128 168) */
  --xtn-accent-fg: #000000; /* oklch(0.000 0.000 0) */
  --xtn-accent-text: #5fdfb4; /* oklch(0.820 0.130 168) */
  --xtn-accent-soft: #061f16; /* oklch(0.216 0.036 167) */
  --xtn-selection: #0f3a2c; /* oklch(0.314 0.054 168) */
  --xtn-ok: #81d174; /* oklch(0.787 0.149 141) */
  --xtn-ok-soft: #0b1d0d; /* oklch(0.210 0.039 146) */
  --xtn-warn: #fe9e38; /* oklch(0.781 0.158 63) */
  --xtn-warn-soft: #251404; /* oklch(0.211 0.040 63) */
  --xtn-danger: #ee8484; /* oklch(0.729 0.130 21) */
  --xtn-danger-soft: #28100f; /* oklch(0.209 0.040 24) */
  --xtn-info: #6a99fd; /* oklch(0.695 0.156 264) */
  --xtn-info-soft: #0e182a; /* oklch(0.210 0.039 262) */
  --xtn-domain-jour: #ffb9bd; /* oklch(0.852 0.081 15) */
  --xtn-domain-jour-text: #ffb9bd; /* oklch(0.852 0.081 15) */
  --xtn-domain-jour-soft: #281012; /* oklch(0.210 0.040 16) */
  --xtn-domain-moteur: #7fe6fa; /* oklch(0.870 0.100 212) */
  --xtn-domain-moteur-text: #7fe6fa; /* oklch(0.870 0.100 212) */
  --xtn-domain-moteur-soft: #001d22; /* oklch(0.212 0.037 210) */
  --xtn-domain-revenu: #fae8a2; /* oklch(0.930 0.090 95) */
  --xtn-domain-revenu-text: #fae8a2; /* oklch(0.930 0.090 95) */
  --xtn-domain-revenu-soft: #1e1802; /* oklch(0.210 0.039 95) */
  --xtn-domain-equipe: #cf6cb1; /* oklch(0.667 0.150 340) */
  --xtn-domain-equipe-text: #cc6faf; /* oklch(0.667 0.141 340) */
  --xtn-domain-equipe-soft: #24101e; /* oklch(0.208 0.041 339) */
  --xtn-domain-atelier: #bd9fe5; /* oklch(0.754 0.103 304) */
  --xtn-domain-atelier-text: #bd9fe5; /* oklch(0.754 0.103 304) */
  --xtn-domain-atelier-soft: #1c1327; /* oklch(0.209 0.040 304) */
  --xtn-branch-hybride: #aac3ff; /* oklch(0.820 0.089 267) */
  --xtn-app-corextn: #5fdfb4; /* oklch(0.820 0.130 168) */
  --xtn-app-scalextn: #d33850; /* oklch(0.581 0.190 17) */
  --xtn-app-growxtn: #f071aa; /* oklch(0.715 0.167 354) */
  --xtn-app-linkxtn: #4ab7fb; /* oklch(0.746 0.139 240) */
  --xtn-app-dm: #ffcf50; /* oklch(0.874 0.150 88) */
  --xtn-app-overlay: #4680f1; /* oklch(0.620 0.181 262) */
  --xtn-app-discord: #5658c8; /* oklch(0.521 0.170 278) */
  --xtn-app-infloww: #e66e42; /* oklch(0.674 0.160 40) */
  --xtn-chart-1: #58d8ae; /* oklch(0.799 0.129 168) */
  --xtn-chart-2: #7fe6fa; /* oklch(0.870 0.100 212) */
  --xtn-chart-3: #cf6cb1; /* oklch(0.667 0.150 340) */
  --xtn-chart-4: #bd9fe5; /* oklch(0.754 0.103 304) */
  --xtn-chart-5: #fae8a2; /* oklch(0.930 0.090 95) */
  --xtn-chart-6: #ffb9bd; /* oklch(0.852 0.081 15) */
  --xtn-chart-7: #6a99fd; /* oklch(0.695 0.156 264) */
  --xtn-chart-8: #767c79; /* oklch(0.580 0.008 165) */
  --xtn-div-neg: #ee8484; /* oklch(0.729 0.130 21) */
  --xtn-div-zero: #414744; /* oklch(0.391 0.009 165) */
  --xtn-div-pos: #81d174; /* oklch(0.787 0.149 141) */
  --xtn-skeleton-base: #141816; /* oklch(0.204 0.007 164) */
  --xtn-skeleton-shine: #222825; /* oklch(0.270 0.010 164) */
  --xtn-app-corextn-soft: #182721; /* oklch(0.257 0.023 168) */
  --xtn-app-scalextn-soft: #251717; /* oklch(0.223 0.023 19) */
  --xtn-app-growxtn-soft: #271c20; /* oklch(0.241 0.019 355) */
  --xtn-app-linkxtn-soft: #162329; /* oklch(0.247 0.021 228) */
  --xtn-app-dm-soft: #272519; /* oklch(0.263 0.021 100) */
  --xtn-app-overlay-soft: #141d28; /* oklch(0.227 0.025 254) */
  --xtn-app-discord-soft: #141924; /* oklch(0.214 0.023 266) */
  --xtn-app-infloww-soft: #261c16; /* oklch(0.237 0.019 52) */
  --xtn-seq-1: #003c24; /* oklch(0.314 0.072 160) */
  --xtn-seq-2: #005e43; /* oklch(0.427 0.089 165) */
  --xtn-seq-3: #228467; /* oklch(0.550 0.100 169) */
  --xtn-seq-4: #57b293; /* oklch(0.699 0.100 169) */
  --xtn-seq-5: #88e3c2; /* oklch(0.850 0.100 169) */
  --xtn-ring: #58d8ae80;
  --xtn-hybride-soft: #1e2429; /* oklch(0.256 0.013 243) */
  --xtn-shadow-1: 0 0 0 1px var(--xtn-border);
  --xtn-shadow-2: 0 8px 24px rgb(0 0 0 / 0.55), 0 0 0 1px var(--xtn-border);
  --xtn-shadow-3: 0 24px 60px rgb(0 0 0 / 0.7), inset 0 1px 0 rgb(255 255 255 / 0.04);
}

:root[data-theme="light"] {
  color-scheme: light;
  --xtn-bg: #ecf1ef; /* oklch(0.954 0.006 170) */
  --xtn-surface: #fafcfb; /* oklch(0.989 0.002 165) */
  --xtn-surface-2: #e1e8e5; /* oklch(0.925 0.008 169) */
  --xtn-surface-3: #d7dedb; /* oklch(0.894 0.009 169) */
  --xtn-border: #cbd3d0; /* oklch(0.860 0.010 172) */
  --xtn-border-strong: #abb3b0; /* oklch(0.760 0.010 172) */
  --xtn-fg: #131a17; /* oklch(0.209 0.012 167) */
  --xtn-fg-muted: #535a57; /* oklch(0.460 0.010 168) */
  --xtn-fg-faint: #7b827f; /* oklch(0.600 0.009 169) */
  --xtn-accent: #007c5d; /* oklch(0.521 0.106 168) */
  --xtn-accent-hover: #006b50; /* oklch(0.469 0.095 168) */
  --xtn-accent-active: #005c44; /* oklch(0.422 0.086 168) */
  --xtn-accent-fg: #fafcfb; /* oklch(0.989 0.002 165) */
  --xtn-accent-text: #007558; /* oklch(0.500 0.101 168) */
  --xtn-accent-soft: #d1f3e4; /* oklch(0.936 0.041 167) */
  --xtn-selection: #c5efe0; /* oklch(0.918 0.048 172) */
  --xtn-ok: #217937; /* oklch(0.509 0.130 148) */
  --xtn-ok-soft: #d8f1db; /* oklch(0.934 0.039 149) */
  --xtn-warn: #9c5600; /* oklch(0.525 0.123 60) */
  --xtn-warn-soft: #fee4cf; /* oklch(0.935 0.040 61) */
  --xtn-danger: #bd383a; /* oklch(0.539 0.170 24) */
  --xtn-danger-soft: #ffe2df; /* oklch(0.936 0.032 25) */
  --xtn-info: #3365c5; /* oklch(0.525 0.160 262) */
  --xtn-info-soft: #deeaff; /* oklch(0.934 0.031 262) */
  --xtn-domain-jour: #d07181; /* oklch(0.660 0.120 10) */
  --xtn-domain-jour-text: #a54a5c; /* oklch(0.530 0.121 10) */
  --xtn-domain-jour-soft: #ffe1e4; /* oklch(0.935 0.033 11) */
  --xtn-domain-moteur: #009db9; /* oklch(0.641 0.114 216) */
  --xtn-domain-moteur-text: #007489; /* oklch(0.515 0.092 216) */
  --xtn-domain-moteur-soft: #cdf1fb; /* oklch(0.936 0.040 216) */
  --xtn-domain-revenu: #d6b33a; /* oklch(0.777 0.140 92) */
  --xtn-domain-revenu-text: #7f6600; /* oklch(0.520 0.106 92) */
  --xtn-domain-revenu-soft: #f3e9cc; /* oklch(0.934 0.040 91) */
  --xtn-domain-equipe: #bd3986; /* oklch(0.561 0.184 349) */
  --xtn-domain-equipe-text: #a6467a; /* oklch(0.536 0.139 349) */
  --xtn-domain-equipe-soft: #ffdfee; /* oklch(0.934 0.040 348) */
  --xtn-domain-atelier: #9667e2; /* oklch(0.619 0.181 299) */
  --xtn-domain-atelier-text: #7957b2; /* oklch(0.534 0.140 299) */
  --xtn-domain-atelier-soft: #ece5ff; /* oklch(0.935 0.036 298) */
  --xtn-branch-hybride: #394c94; /* oklch(0.440 0.120 270) */
  --xtn-app-corextn: #007558; /* oklch(0.500 0.101 168) */
  --xtn-app-scalextn: #bd0025; /* oklch(0.504 0.203 23) */
  --xtn-app-growxtn: #de4f94; /* oklch(0.640 0.189 354) */
  --xtn-app-linkxtn: #1dabdc; /* oklch(0.694 0.130 228) */
  --xtn-app-dm: #cc9c00; /* oklch(0.718 0.147 86) */
  --xtn-app-overlay: #3879fa; /* oklch(0.607 0.204 262) */
  --xtn-app-discord: #4343c0; /* oklch(0.464 0.190 276) */
  --xtn-app-infloww: #d26717; /* oklch(0.631 0.158 50) */
  --xtn-chart-1: #007c5d; /* oklch(0.521 0.106 168) */
  --xtn-chart-2: #009db9; /* oklch(0.641 0.114 216) */
  --xtn-chart-3: #bd3986; /* oklch(0.561 0.184 349) */
  --xtn-chart-4: #9667e2; /* oklch(0.619 0.181 299) */
  --xtn-chart-5: #d6b33a; /* oklch(0.777 0.140 92) */
  --xtn-chart-6: #d07181; /* oklch(0.660 0.120 10) */
  --xtn-chart-7: #3365c5; /* oklch(0.525 0.160 262) */
  --xtn-chart-8: #47474e; /* oklch(0.401 0.012 286) */
  --xtn-div-neg: #bd383a; /* oklch(0.539 0.170 24) */
  --xtn-div-zero: #abb3b0; /* oklch(0.760 0.010 172) */
  --xtn-div-pos: #217937; /* oklch(0.509 0.130 148) */
  --xtn-skeleton-base: #e1e8e5; /* oklch(0.925 0.008 169) */
  --xtn-skeleton-shine: #f2f6f4; /* oklch(0.970 0.005 165) */
  --xtn-app-corextn-soft: #d9f7ea; /* oklch(0.951 0.036 168) */
  --xtn-app-scalextn-soft: #ffe6e4; /* oklch(0.945 0.028 23) */
  --xtn-app-growxtn-soft: #ffe5f0; /* oklch(0.946 0.032 350) */
  --xtn-app-linkxtn-soft: #d7f4ff; /* oklch(0.950 0.034 223) */
  --xtn-app-dm-soft: #f9edd5; /* oklch(0.949 0.034 85) */
  --xtn-app-overlay-soft: #e2efff; /* oklch(0.947 0.026 254) */
  --xtn-app-discord-soft: #e8edff; /* oklch(0.948 0.025 274) */
  --xtn-app-infloww-soft: #ffe8da; /* oklch(0.946 0.031 53) */
  --xtn-seq-1: #90f6d0; /* oklch(0.900 0.110 168) */
  --xtn-seq-2: #68ceaa; /* oklch(0.780 0.110 168) */
  --xtn-seq-3: #3fa885; /* oklch(0.661 0.110 168) */
  --xtn-seq-4: #088665; /* oklch(0.551 0.110 168) */
  --xtn-seq-5: #006849; /* oklch(0.458 0.097 164) */
  --xtn-ring: #007c5d80;
  --xtn-hybride-soft: #e5eaf1; /* oklch(0.935 0.011 257) */
  --xtn-shadow-1: 0 1px 2px oklch(0.13 0.012 165 / 0.06), 0 0 0 1px var(--xtn-border);
  --xtn-shadow-2: 0 8px 24px oklch(0.13 0.012 165 / 0.10), 0 0 0 1px var(--xtn-border);
  --xtn-shadow-3: 0 24px 60px oklch(0.13 0.012 165 / 0.16);
}

@theme inline {
  --color-bg: var(--xtn-bg);
  --color-surface: var(--xtn-surface);
  --color-surface-2: var(--xtn-surface-2);
  --color-surface-3: var(--xtn-surface-3);
  --color-border: var(--xtn-border);
  --color-border-strong: var(--xtn-border-strong);
  --color-fg: var(--xtn-fg);
  --color-fg-muted: var(--xtn-fg-muted);
  --color-fg-faint: var(--xtn-fg-faint);
  --color-accent: var(--xtn-accent);
  --color-accent-hover: var(--xtn-accent-hover);
  --color-accent-active: var(--xtn-accent-active);
  --color-accent-fg: var(--xtn-accent-fg);
  --color-accent-text: var(--xtn-accent-text);
  --color-accent-soft: var(--xtn-accent-soft);
  --color-selection: var(--xtn-selection);
  --color-ok: var(--xtn-ok);
  --color-ok-soft: var(--xtn-ok-soft);
  --color-warn: var(--xtn-warn);
  --color-warn-soft: var(--xtn-warn-soft);
  --color-danger: var(--xtn-danger);
  --color-danger-soft: var(--xtn-danger-soft);
  --color-info: var(--xtn-info);
  --color-info-soft: var(--xtn-info-soft);
  --color-domain-jour: var(--xtn-domain-jour);
  --color-domain-jour-text: var(--xtn-domain-jour-text);
  --color-domain-jour-soft: var(--xtn-domain-jour-soft);
  --color-domain-moteur: var(--xtn-domain-moteur);
  --color-domain-moteur-text: var(--xtn-domain-moteur-text);
  --color-domain-moteur-soft: var(--xtn-domain-moteur-soft);
  --color-domain-revenu: var(--xtn-domain-revenu);
  --color-domain-revenu-text: var(--xtn-domain-revenu-text);
  --color-domain-revenu-soft: var(--xtn-domain-revenu-soft);
  --color-domain-equipe: var(--xtn-domain-equipe);
  --color-domain-equipe-text: var(--xtn-domain-equipe-text);
  --color-domain-equipe-soft: var(--xtn-domain-equipe-soft);
  --color-domain-atelier: var(--xtn-domain-atelier);
  --color-domain-atelier-text: var(--xtn-domain-atelier-text);
  --color-domain-atelier-soft: var(--xtn-domain-atelier-soft);
  --color-branch-hybride: var(--xtn-branch-hybride);
  --color-app-corextn: var(--xtn-app-corextn);
  --color-app-scalextn: var(--xtn-app-scalextn);
  --color-app-growxtn: var(--xtn-app-growxtn);
  --color-app-linkxtn: var(--xtn-app-linkxtn);
  --color-app-dm: var(--xtn-app-dm);
  --color-app-overlay: var(--xtn-app-overlay);
  --color-app-discord: var(--xtn-app-discord);
  --color-app-infloww: var(--xtn-app-infloww);
  --color-chart-1: var(--xtn-chart-1);
  --color-chart-2: var(--xtn-chart-2);
  --color-chart-3: var(--xtn-chart-3);
  --color-chart-4: var(--xtn-chart-4);
  --color-chart-5: var(--xtn-chart-5);
  --color-chart-6: var(--xtn-chart-6);
  --color-chart-7: var(--xtn-chart-7);
  --color-chart-8: var(--xtn-chart-8);
  --color-div-neg: var(--xtn-div-neg);
  --color-div-zero: var(--xtn-div-zero);
  --color-div-pos: var(--xtn-div-pos);
  --color-skeleton-base: var(--xtn-skeleton-base);
  --color-skeleton-shine: var(--xtn-skeleton-shine);
  --color-app-corextn-soft: var(--xtn-app-corextn-soft);
  --color-app-scalextn-soft: var(--xtn-app-scalextn-soft);
  --color-app-growxtn-soft: var(--xtn-app-growxtn-soft);
  --color-app-linkxtn-soft: var(--xtn-app-linkxtn-soft);
  --color-app-dm-soft: var(--xtn-app-dm-soft);
  --color-app-overlay-soft: var(--xtn-app-overlay-soft);
  --color-app-discord-soft: var(--xtn-app-discord-soft);
  --color-app-infloww-soft: var(--xtn-app-infloww-soft);
  --color-seq-1: var(--xtn-seq-1);
  --color-seq-2: var(--xtn-seq-2);
  --color-seq-3: var(--xtn-seq-3);
  --color-seq-4: var(--xtn-seq-4);
  --color-seq-5: var(--xtn-seq-5);
  --color-ring: var(--xtn-ring);
  --color-hybride-soft: var(--xtn-hybride-soft);
  --shadow-1: var(--xtn-shadow-1);
  --shadow-2: var(--xtn-shadow-2);
  --shadow-3: var(--xtn-shadow-3);
}

:root {
  --color-bg: var(--xtn-bg);
  --color-surface: var(--xtn-surface);
  --color-surface-2: var(--xtn-surface-2);
  --color-surface-3: var(--xtn-surface-3);
  --color-border: var(--xtn-border);
  --color-border-strong: var(--xtn-border-strong);
  --color-fg: var(--xtn-fg);
  --color-fg-muted: var(--xtn-fg-muted);
  --color-fg-faint: var(--xtn-fg-faint);
  --color-accent: var(--xtn-accent);
  --color-accent-hover: var(--xtn-accent-hover);
  --color-accent-active: var(--xtn-accent-active);
  --color-accent-fg: var(--xtn-accent-fg);
  --color-accent-text: var(--xtn-accent-text);
  --color-accent-soft: var(--xtn-accent-soft);
  --color-selection: var(--xtn-selection);
  --color-ok: var(--xtn-ok);
  --color-ok-soft: var(--xtn-ok-soft);
  --color-warn: var(--xtn-warn);
  --color-warn-soft: var(--xtn-warn-soft);
  --color-danger: var(--xtn-danger);
  --color-danger-soft: var(--xtn-danger-soft);
  --color-info: var(--xtn-info);
  --color-info-soft: var(--xtn-info-soft);
  --color-domain-jour: var(--xtn-domain-jour);
  --color-domain-jour-text: var(--xtn-domain-jour-text);
  --color-domain-jour-soft: var(--xtn-domain-jour-soft);
  --color-domain-moteur: var(--xtn-domain-moteur);
  --color-domain-moteur-text: var(--xtn-domain-moteur-text);
  --color-domain-moteur-soft: var(--xtn-domain-moteur-soft);
  --color-domain-revenu: var(--xtn-domain-revenu);
  --color-domain-revenu-text: var(--xtn-domain-revenu-text);
  --color-domain-revenu-soft: var(--xtn-domain-revenu-soft);
  --color-domain-equipe: var(--xtn-domain-equipe);
  --color-domain-equipe-text: var(--xtn-domain-equipe-text);
  --color-domain-equipe-soft: var(--xtn-domain-equipe-soft);
  --color-domain-atelier: var(--xtn-domain-atelier);
  --color-domain-atelier-text: var(--xtn-domain-atelier-text);
  --color-domain-atelier-soft: var(--xtn-domain-atelier-soft);
  --color-branch-hybride: var(--xtn-branch-hybride);
  --color-app-corextn: var(--xtn-app-corextn);
  --color-app-scalextn: var(--xtn-app-scalextn);
  --color-app-growxtn: var(--xtn-app-growxtn);
  --color-app-linkxtn: var(--xtn-app-linkxtn);
  --color-app-dm: var(--xtn-app-dm);
  --color-app-overlay: var(--xtn-app-overlay);
  --color-app-discord: var(--xtn-app-discord);
  --color-app-infloww: var(--xtn-app-infloww);
  --color-chart-1: var(--xtn-chart-1);
  --color-chart-2: var(--xtn-chart-2);
  --color-chart-3: var(--xtn-chart-3);
  --color-chart-4: var(--xtn-chart-4);
  --color-chart-5: var(--xtn-chart-5);
  --color-chart-6: var(--xtn-chart-6);
  --color-chart-7: var(--xtn-chart-7);
  --color-chart-8: var(--xtn-chart-8);
  --color-div-neg: var(--xtn-div-neg);
  --color-div-zero: var(--xtn-div-zero);
  --color-div-pos: var(--xtn-div-pos);
  --color-skeleton-base: var(--xtn-skeleton-base);
  --color-skeleton-shine: var(--xtn-skeleton-shine);
  --color-app-corextn-soft: var(--xtn-app-corextn-soft);
  --color-app-scalextn-soft: var(--xtn-app-scalextn-soft);
  --color-app-growxtn-soft: var(--xtn-app-growxtn-soft);
  --color-app-linkxtn-soft: var(--xtn-app-linkxtn-soft);
  --color-app-dm-soft: var(--xtn-app-dm-soft);
  --color-app-overlay-soft: var(--xtn-app-overlay-soft);
  --color-app-discord-soft: var(--xtn-app-discord-soft);
  --color-app-infloww-soft: var(--xtn-app-infloww-soft);
  --color-seq-1: var(--xtn-seq-1);
  --color-seq-2: var(--xtn-seq-2);
  --color-seq-3: var(--xtn-seq-3);
  --color-seq-4: var(--xtn-seq-4);
  --color-seq-5: var(--xtn-seq-5);
  --color-ring: var(--xtn-ring);
  --color-hybride-soft: var(--xtn-hybride-soft);
  --shadow-1: var(--xtn-shadow-1);
  --shadow-2: var(--xtn-shadow-2);
  --shadow-3: var(--xtn-shadow-3);
  --color-muted: var(--xtn-fg-muted);
  --color-faint: var(--xtn-fg-faint);
  --color-bg-soft: var(--xtn-surface-2);
  --color-domain-dev: var(--xtn-domain-atelier);
  --color-domain-ops-ig: var(--xtn-domain-moteur);
  --color-domain-outreach: var(--xtn-domain-revenu);
  --color-domain-content: var(--xtn-domain-atelier);
  --color-domain-team: var(--xtn-domain-equipe);
  --color-domain-finance: var(--xtn-domain-revenu);
  --color-domain-admin: var(--xtn-domain-jour);
  --color-domain-dev-text: var(--xtn-domain-atelier-text);
  --color-domain-ops-ig-text: var(--xtn-domain-moteur-text);
  --color-domain-outreach-text: var(--xtn-domain-revenu-text);
  --color-domain-content-text: var(--xtn-domain-atelier-text);
  --color-domain-team-text: var(--xtn-domain-equipe-text);
  --color-domain-finance-text: var(--xtn-domain-revenu-text);
  --color-domain-admin-text: var(--xtn-domain-jour-text);
  --color-domain-dev-soft: var(--xtn-domain-atelier-soft);
  --color-domain-ops-ig-soft: var(--xtn-domain-moteur-soft);
  --color-domain-outreach-soft: var(--xtn-domain-revenu-soft);
  --color-domain-content-soft: var(--xtn-domain-atelier-soft);
  --color-domain-team-soft: var(--xtn-domain-equipe-soft);
  --color-domain-finance-soft: var(--xtn-domain-revenu-soft);
  --color-domain-admin-soft: var(--xtn-domain-jour-soft);
}


/* (vanilla) Tokens non chromatiques de la charte : polices, échelle typographique, rayons, profondeur, mouvement.
   Les couleurs vivent dans tokens.css (généré). Ici tout est écrit à la main et lu par Tailwind :
   `text-13`, `rounded-card`, `z-nav`, `duration-base`, `ease-out-xtn`… */

:root {
  --font-sans: "Inter", "Inter Fallback HN", "Inter Fallback Segoe", "Inter Fallback Arial", system-ui,
    -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", "JetBrains Mono Fallback", "JetBrains Mono Fallback Consolas", ui-monospace,
    Menlo, monospace;

  /* Échelle en px fixes : une interface dense se règle au pixel, pas au rem. Interligne et
     approche sont attachés au corps (docs/design/CHARTE.md §2.1). */
  --text-11: 11px;
  --text-11--line-height: 16px;
  --text-11--letter-spacing: 0.06em;
  --text-11--font-weight: 500;
  --text-12: 12px;
  --text-12--line-height: 16px;
  --text-13: 13px;
  --text-13--line-height: 18px;
  --text-14: 14px;
  --text-14--line-height: 20px;
  --text-15: 15px;
  --text-15--line-height: 22px;
  --text-16: 16px;
  --text-16--line-height: 24px;
  --text-18: 18px;
  --text-18--line-height: 24px;
  --text-18--letter-spacing: -0.01em;
  --text-18--font-weight: 600;
  --text-20: 20px;
  --text-20--line-height: 26px;
  --text-20--letter-spacing: -0.015em;
  --text-20--font-weight: 600;
  --text-24: 24px;
  --text-24--line-height: 30px;
  --text-24--letter-spacing: -0.02em;
  --text-24--font-weight: 600;
  --text-28: 28px;
  --text-28--line-height: 32px;
  --text-28--letter-spacing: -0.02em;
  --text-28--font-weight: 600;
  --text-32: 32px;
  --text-32--line-height: 36px;
  --text-32--letter-spacing: -0.025em;
  --text-32--font-weight: 600;
  --text-40: 40px;
  --text-40--line-height: 44px;
  --text-40--letter-spacing: -0.03em;
  --text-40--font-weight: 600;

  --radius-card: 12px;
  --radius-btn: 8px;
  --radius-input: 6px;
  --radius-tile: 10px;

  --z-subnav: 20;
  --z-nav: 30;
  --z-overlay: 50;
  --z-toast: 60;

  /* Trois durées, pas une de plus (docs/design/MOTION.md) : 120 ms pour un état (couleur, opacité,
     appui), 200 ms pour une entrée ou un petit déplacement, 320 ms pour ce qui se pose (sheet,
     palette, changement de page). Une entrée décélère (`out`), une sortie accélère (`exit`) — plus
     court, toujours. Jamais d'animation décorative. */
  --duration-fast: 120ms;
  --duration-base: 200ms;
  --duration-sheet: 320ms;
  --ease-out-xtn: cubic-bezier(0.2, 0, 0, 1);
  --ease-exit-xtn: cubic-bezier(0.4, 0, 1, 1);
  --ease-sheet: cubic-bezier(0.32, 0.72, 0, 1);
}


/* Sol de l'app : document, typographie de base, réglages de plateforme. Rien ici n'est un composant. */

/* 100% en chaîne (html → body → #root) vaut le viewport DU MOMENT : celui de Safari barre visible ou
   cachée, celui de l'app installée sans barre — exactement ce que `100dvh` promet, sans le bug de
   `100vh` (figé sur le grand viewport, qui passe sous la barre d'onglets). Aucun `100vh` n'est donc
   utilisé sur un chemin téléphone ; les sheets sont en `dvh`. */
html,
body,
#root {
  height: 100%;
}
/* Le fond sur html AUSSI : c'est lui qu'iOS montre pendant le rebond élastique en haut et en bas de
   page, et au lancement d'une app installée avant le premier rendu. Noir, comme `background_color`
   du manifest : pas de flash. */
html {
  background: var(--color-bg);
}
body {
  background: var(--color-bg);
  color: var(--color-fg);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Chiffres tabulaires partout : les colonnes d'argent et les compteurs ne doivent pas trembler quand
     ils changent. `cv08` = I à empattements, pour que « Il » ne se confonde jamais dans une table à 12 px. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "cv08";
  font-optical-sizing: auto;
  -webkit-tap-highlight-color: transparent;
}

/* Rôles typographiques (docs/design/CHARTE.md §2.1). Les tailles viennent de theme.css. */
.t-th {
  font-size: var(--text-11);
  line-height: var(--text-11--line-height);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-feature-settings: "cv08", "case";
  color: var(--color-fg-muted);
}
.t-cell {
  font-size: var(--text-13);
  line-height: var(--text-13--line-height);
}
/* Un nombre : tabulaire, 1 à base (`cv01`), aligné à droite par l'appelant. Argent = toujours Inter, jamais mono. */
.t-num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "cv08", "cv01", "tnum";
}
.t-kpi {
  font-size: var(--text-28);
  line-height: var(--text-28--line-height);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "cv08", "cv01", "tnum";
}
/* Ids, handles IG, IMEI, `#482` : si on le copie-colle, c'est du mono. Zéro barré, ligatures coupées. */
.t-code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  font-variant-numeric: slashed-zero tabular-nums;
  font-feature-settings: "zero", "calt" 0;
}
/* Prose : chiffres proportionnels, la seule exception au tabulaire global. */
.t-prose {
  font-variant-numeric: proportional-nums;
  line-height: 1.55;
}

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

/* Barres de défilement fines : les tables denses défilent beaucoup. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border-strong) transparent;
}

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

/* Tout ce qui se tape ou se clique : pas de zoom au double-tap ni de délai de 300 ms (`manipulation`
   garde le pincement et le défilement), pas de voile gris d'iOS au toucher — chaque contrôle a son
   propre état `:active` (components.css), qui dit la même chose dans la couleur de la charte. */
a,
button,
[role="button"],
[role="checkbox"],
input,
select,
textarea,
summary,
label {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Zoom au focus sur iOS. Sous 16px, Safari zoome le viewport quand un champ prend le focus, et ne
   dézoome JAMAIS. HORS COUCHE, volontairement : une déclaration normale bat toutes les règles en
   couche, y compris un `text-sm` de Tailwind posé plus tard sur un input. 16px littéral et non 1rem :
   c'est le seuil de Safari, il ne doit pas bouger si la taille racine change. */
@media (hover: none) and (pointer: coarse) {
  .input,
  input:not([type="checkbox"]):not([type="radio"]),
  select,
  textarea {
    font-size: 16px;
  }
}

/* Un seul endroit qui sait de combien les bords ont besoin : la barre d'onglets et la zone sûre.
   Gauche et droite servent en paysage — l'encoche mange 47px d'un côté, l'indicateur de l'autre. */
:root {
  --xtn-safe-b: env(safe-area-inset-bottom, 0px);
  --xtn-safe-t: env(safe-area-inset-top, 0px);
  --xtn-safe-l: env(safe-area-inset-left, 0px);
  --xtn-safe-r: env(safe-area-inset-right, 0px);
}

/* 52px de barre + la zone sûre + de l'air : sans ça le dernier bouton de chaque écran passe dessous.
   HORS COUCHE parce que `<main>` porte aussi `p-5`, un utilitaire qui battrait une règle de composant. */
.xtn-main-pad {
  padding-bottom: calc(52px + var(--xtn-safe-b) + 1.25rem);
  padding-left: max(1.25rem, var(--xtn-safe-l));
  padding-right: max(1.25rem, var(--xtn-safe-r));
}
@media (min-width: 768px) {
  .xtn-main-pad {
    padding-bottom: 2rem;
    padding-left: 2rem;
    padding-right: 2rem;
  }
}

/* Une cible de 44px autour d'un petit contrôle (une pastille-lien de 18px, « supprimer » en 11px) sans
   changer son dessin : le pseudo-élément prend les coups. Tactile seulement — à la souris, une zone
   invisible qui déborde vole les clics des voisins. */
.tap-ext {
  position: relative;
}
@media (hover: none) and (pointer: coarse) {
  .tap-ext::before {
    content: "";
    position: absolute;
    inset: -12px -8px;
  }
}

/* Les longues listes (Fait, Comptes) : une ligne hors écran ne coûte ni layout ni peinture, et sa
   hauteur est réservée pour que la barre de défilement ne saute pas. 56px = une ligne téléphone. */
.cv-auto {
  content-visibility: auto;
  contain-intrinsic-size: auto 56px;
}

/* Rien ne bouge pour qui l'a demandé. Les états changent quand même, instantanément — les délais
   aussi tombent à zéro, sinon une entrée décalée (`.stagger`) resterait invisible pendant son délai. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
  }
}


/* Composants CSS de la charte — les noms que tout le front (et demain les apps sœurs) utilisent.
   Tout passe par les tokens : aucune couleur littérale ici. */

@layer components {
  /* ── Surfaces ─────────────────────────────────────────────────────────────────────────────── */
  .card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
  }
  .card-raised {
    box-shadow: var(--shadow-2);
  }

  /* ── Boutons ──────────────────────────────────────────────────────────────────────────────── */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    border-radius: var(--radius-btn);
    padding: 0.5rem 0.85rem;
    font-size: var(--text-14);
    line-height: var(--text-14--line-height);
    font-weight: 500;
    transition:
      background-color var(--duration-fast) var(--ease-out-xtn),
      border-color var(--duration-fast) var(--ease-out-xtn),
      color var(--duration-fast) var(--ease-out-xtn),
      transform var(--duration-fast) var(--ease-out-xtn);
  }
  .btn-primary {
    background: var(--color-accent);
    color: var(--color-accent-fg);
  }
  .btn-primary:active {
    background: var(--color-accent-active);
  }
  .btn-ghost {
    border: 1px solid var(--color-border);
    color: var(--color-fg);
  }
  .btn-danger {
    border: 1px solid var(--color-danger-soft);
    background: var(--color-danger-soft);
    color: var(--color-danger);
  }
  .btn-quiet {
    color: var(--color-fg-muted);
  }
  /* Le survol n'existe qu'à la souris : sur iOS, un `:hover` nu reste collé au bouton après le tap.
     Le doigt a `:active` à la place, un cran plus sombre (surface-3) pour être vu sous le pouce. */
  @media (hover: hover) {
    .btn-primary:hover {
      background: var(--color-accent-hover);
    }
    .btn-ghost:hover {
      background: var(--color-surface-2);
      border-color: var(--color-border-strong);
    }
    .btn-danger:hover {
      border-color: var(--color-danger);
    }
    .btn-quiet:hover {
      color: var(--color-fg);
      background: var(--color-surface-2);
    }
  }
  .btn-ghost:active,
  .btn-quiet:active {
    background: var(--color-surface-3);
    color: var(--color-fg);
  }
  .btn-danger:active {
    border-color: var(--color-danger);
  }

  /* ── Champs ───────────────────────────────────────────────────────────────────────────────── */
  .input {
    background: var(--color-surface-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-input);
    padding: 0.5rem 0.7rem;
    font-size: var(--text-14);
    line-height: var(--text-14--line-height);
    color: var(--color-fg);
    width: 100%;
    transition: border-color var(--duration-fast) var(--ease-out-xtn), box-shadow var(--duration-fast) var(--ease-out-xtn);
  }
  .input::placeholder {
    color: var(--color-fg-faint);
  }
  .input:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px var(--color-ring);
  }

  /* ── Pastilles ────────────────────────────────────────────────────────────────────────────── */
  .pill {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    border-radius: 9999px;
    padding: 0.1rem 0.55rem;
    font-size: var(--text-11);
    line-height: var(--text-11--line-height);
    font-weight: 500;
    letter-spacing: 0.01em;
    border: 1px solid var(--color-border);
    color: var(--color-fg-muted);
    white-space: nowrap;
  }
  /* Tons sémantiques : texte + pastille du même token, bordure = la pastille (la couleur ne crie pas). */
  .pill-ok {
    color: var(--color-ok);
    background: var(--color-ok-soft);
    border-color: var(--color-ok-soft);
  }
  .pill-warn {
    color: var(--color-warn);
    background: var(--color-warn-soft);
    border-color: var(--color-warn-soft);
  }
  .pill-danger {
    color: var(--color-danger);
    background: var(--color-danger-soft);
    border-color: var(--color-danger-soft);
  }
  .pill-info {
    color: var(--color-info);
    background: var(--color-info-soft);
    border-color: var(--color-info-soft);
  }
  .pill-neutral {
    color: var(--color-fg-muted);
    background: var(--color-surface-3);
    border-color: var(--color-surface-3);
  }
  .pill-accent {
    color: var(--color-accent-text);
    background: var(--color-accent-soft);
    border-color: var(--color-accent-soft);
  }
  /* Une couleur venue d'ailleurs (domaine, app) : le composant pose `--pc` (texte) et `--ps` (pastille)
     en style inline, ex. `--pc: var(--xtn-domain-moteur-text); --ps: var(--xtn-domain-moteur-soft)`. */
  .pill-var {
    color: var(--pc, var(--color-fg-muted));
    background: var(--ps, var(--color-surface-3));
    border-color: var(--ps, var(--color-surface-3));
  }
  .pill-hybride {
    color: var(--color-branch-hybride);
    background: var(--color-hybride-soft);
    border-color: var(--color-hybride-soft);
  }
  .dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 9999px;
    background: currentColor;
    flex: none;
  }

  /* Rail de domaine : le bord gauche coloré d'une ligne de tâche. */
  .rail {
    border-left: 3px solid var(--rail, var(--color-border-strong));
    border-radius: 0 var(--radius-btn) var(--radius-btn) 0;
  }

  /* ── Tuile KPI ────────────────────────────────────────────────────────────────────────────── */
  .stat {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    padding: 0.75rem 0.9rem;
    min-width: 0;
  }
  .stat-label {
    font-size: var(--text-11);
    line-height: var(--text-11--line-height);
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-feature-settings: "cv08", "case";
    color: var(--color-fg-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .stat-value {
    margin-top: 0.15rem;
    font-size: var(--text-24);
    line-height: var(--text-24--line-height);
    font-weight: 600;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "cv08", "cv01", "tnum";
  }
  @media (min-width: 768px) {
    .stat-value {
      font-size: var(--text-28);
      line-height: var(--text-28--line-height);
    }
  }
  .stat-hint {
    margin-top: 0.15rem;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-muted);
    font-variant-numeric: tabular-nums;
  }

  /* ── Skeleton ─────────────────────────────────────────────────────────────────────────────── */
  .skeleton {
    border-radius: var(--radius-btn);
    background: linear-gradient(
      90deg,
      var(--color-skeleton-base) 0%,
      var(--color-skeleton-shine) 50%,
      var(--color-skeleton-base) 100%
    );
    background-size: 200% 100%;
    animation: xtn-shimmer 1.6s ease-in-out infinite;
  }
  @keyframes xtn-shimmer {
    from {
      background-position: 200% 0;
    }
    to {
      background-position: -200% 0;
    }
  }

  /* ── Ticking a task off ───────────────────────────────────────────────────────────────────── */
  /* A 44px thumb target wrapped around an 18px box, sitting in its own column of the row. Nothing
     else on the row is clickable, so a thumb has to actually aim to complete something. */
  .tick {
    display: grid;
    place-items: center;
    height: 44px;
    width: 44px;
    flex: none;
    border-radius: var(--radius-btn);
    transition: background-color var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .tick:hover {
      background: var(--color-surface-2);
    }
    .tick:hover .tick-box {
      border-color: var(--color-accent);
    }
  }
  .tick:active {
    background: var(--color-surface-2);
  }
  .tick:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -3px;
  }
  /* Fills the instant it is tapped — the network answer arrives later, and may still take it back. */
  .tick-box {
    display: grid;
    place-items: center;
    height: 18px;
    width: 18px;
    border-radius: 5px;
    border: 1px solid var(--color-border-strong);
    color: transparent;
    transition:
      background-color var(--duration-fast) var(--ease-out-xtn),
      border-color var(--duration-fast) var(--ease-out-xtn),
      color var(--duration-fast) var(--ease-out-xtn);
  }
  .tick[aria-checked="true"] .tick-box {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-fg);
  }
  /* The check draws itself rather than popping: long enough to be seen, short enough to not be a show. */
  .tick-mark {
    stroke-dasharray: 12;
    stroke-dashoffset: 12;
    transition: stroke-dashoffset 160ms var(--ease-out-xtn);
  }
  .tick[aria-checked="true"] .tick-mark {
    stroke-dashoffset: 0;
  }

  /* 0fr ⇄ 1fr animates the height without measuring anything in JS. The slot owns the gap under it,
     so the space closes with the row instead of being left behind the way space-y would. */
  .task-slot {
    display: grid;
    grid-template-rows: 1fr;
    padding-bottom: 0.75rem;
    /* Une sortie : plus courte que l'entrée, et elle accélère. La hauteur est la seule animation de
       layout que la grammaire tolère (0fr ⇄ 1fr) ; la carte glisse aussi de 8px à droite (motion.css). */
    transition:
      grid-template-rows var(--duration-base) var(--ease-exit-xtn),
      padding-bottom var(--duration-base) var(--ease-exit-xtn),
      opacity var(--duration-fast) var(--ease-exit-xtn);
  }
  .task-slot:last-child {
    padding-bottom: 0;
  }
  /* The clipper. Keep it padding-free: with border-box, padding would floor the collapsed height. */
  .task-slot > * {
    overflow: hidden;
    min-height: 0;
  }
  .task-slot.is-leaving {
    grid-template-rows: 0fr;
    padding-bottom: 0;
    opacity: 0;
  }

  .toast {
    background: var(--color-surface-2);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-btn);
    box-shadow: var(--shadow-2);
  }

  /* ── Bottom sheet (components/TaskDetail, EntityForm) ─────────────────────────────────────── */
  /* Appended after `.card` on purpose: `.sheet-panel` refines it rather than repeating it, and with
     equal specificity source order is what decides. Phone-first — a sheet is the only way to read a
     long task on a small screen — and a centred panel from 640px up. */
  .sheet-backdrop {
    position: absolute;
    inset: 0;
    /* Un doigt qui glisse sur le fond ne doit pas faire défiler la page figée derrière. */
    touch-action: none;
    /* Le voile est le fond de la page à 60 % : noir en dark (comme avant), papier menthe en clair —
       un voile noir sur un thème clair dirait « autre app ». */
    background: color-mix(in oklab, var(--color-bg) 60%, transparent);
    opacity: 0;
    transition: opacity var(--duration-base) var(--ease-out-xtn);
  }
  .sheet-backdrop[data-state="open"] {
    opacity: 1;
  }
  .sheet-backdrop[data-state="closing"] {
    transition-timing-function: var(--ease-exit-xtn);
  }
  .sheet-panel {
    /* Top corners only: the sheet sits flush against the bottom edge of the phone. */
    border-radius: 16px 16px 0 0;
    border-bottom: 0;
    max-height: 92vh;
    max-height: 92dvh; /* the visual viewport, so the browser chrome never eats the actions */
    transform: translate3d(0, 100%, 0);
    /* 320 ms pour se poser (`--ease-sheet` : vite au départ, long à s'arrêter) ; le retour après un
       glissement lâché sous le seuil reprend cette même transition. La sortie, elle, accélère en 200. */
    transition: transform var(--duration-sheet) var(--ease-sheet);
    box-shadow: var(--shadow-3);
  }
  .sheet-panel[data-state="open"] {
    transform: none;
    /* Le seul `will-change` de l'app, et seulement tant que la sheet est ouverte : c'est elle que le
       doigt déplace en direct. */
    will-change: transform;
  }
  .sheet-panel[data-state="closing"] {
    transition: transform var(--duration-base) var(--ease-exit-xtn);
  }
  @media (min-width: 640px) {
    .sheet-panel {
      border-radius: var(--radius-card);
      border-bottom: 1px solid var(--color-border);
      max-height: 85vh;
      transform: translate3d(0, 8px, 0) scale(0.985);
      opacity: 0;
      transition: transform var(--duration-base) var(--ease-out-xtn), opacity var(--duration-base) var(--ease-out-xtn);
    }
    .sheet-panel[data-state="open"] {
      transform: none;
      opacity: 1;
    }
    .sheet-panel[data-state="closing"] {
      transition: transform var(--duration-fast) var(--ease-exit-xtn), opacity var(--duration-fast) var(--ease-exit-xtn);
    }
  }
  /* The grab area drags the sheet down, so the browser must not read the gesture as a scroll. */
  .sheet-grab {
    touch-action: none;
  }
  .sheet-grip {
    width: 36px;
    height: 4px;
    border-radius: 9999px;
    background: var(--color-border-strong);
  }
  .sheet-scroll {
    overflow-y: auto;
    /* A flick past the end of the body must not scroll the page underneath. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  /* Home-indicator clearance on iPhone, normal rhythm everywhere else. */
  .sheet-safe {
    padding-bottom: max(0.875rem, env(safe-area-inset-bottom));
  }
  /* 44px is the floor for a one-handed tap; `.btn` alone lands around 36. */
  .sheet-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
  }

  /* Dégagement clavier des sheets (formulaire ET fiche de tâche, qui a sa zone de note). Le clavier
     iOS se dessine PAR-DESSUS le viewport au lieu de le réduire, donc 92dvh mesure toujours la hauteur
     pleine et le pied collant finit sous les touches. `useSheet` écrit le recouvrement mesuré dans
     --sheet-kb et le panneau s'arrête d'autant plus haut. Vaut 0 quand aucun clavier n'est ouvert. */
  .sheet-kb-lift {
    --sheet-kb: 0px;
    margin-bottom: var(--sheet-kb);
    max-height: calc(92dvh - var(--sheet-kb));
  }
  @media (min-width: 640px) {
    .sheet-kb-lift {
      margin-bottom: 0;
      max-height: 85vh;
    }
  }

  /* ── Sol tactile ──────────────────────────────────────────────────────────────────────────── */
  /* Le déclencheur est une CAPACITÉ, pas une largeur. Un iPhone en paysage fait 844px : tous les
     breakpoints disent « ordinateur » alors que le périphérique est toujours un pouce. À l'inverse, un
     portable tactile de 1280px piloté à la souris rapporte `pointer: fine` et garde les contrôles denses. */
  .xtn-tap {
    display: inline-flex;
    align-items: center;
  }
  .xtn-tap-icon {
    display: grid;
    place-items: center;
  }
  @media (hover: none) and (pointer: coarse) {
    .btn {
      min-height: 44px;
    }
    /* EntityForm rend son interrupteur booléen en `btn w-full text-left`. Un conteneur flex ignore
       text-align : on rend son sens à l'utilitaire ici. (0,2,0) bat (0,1,0). */
    .btn.text-left {
      justify-content: flex-start;
    }
    .input {
      min-height: 44px;
    }
    .xtn-tap {
      min-height: 44px;
    }
    .xtn-tap-icon {
      min-height: 44px;
      min-width: 44px;
    }
  }

  /* ── Navigation ───────────────────────────────────────────────────────────────────────────── */
  /* Cinq sections tiennent exactement dans une barre d'onglets, donc rien n'est relégué derrière un
     tiroir. Chaque onglet porte la teinte de sa section quand il est actif : la couleur dit où on est. */
  .xtn-tabbar {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: var(--z-nav);
    border-top: 1px solid var(--color-border);
    /* Translucide : la liste qui passe dessous dit qu'il y a une suite — mais à 82 % les libellés se
       battaient avec le texte de la dernière ligne. 92 % garde le voile, rend l'onglet lisible. */
    background: color-mix(in oklab, var(--color-bg) 92%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    padding-bottom: var(--xtn-safe-b);
    padding-left: var(--xtn-safe-l);
    padding-right: var(--xtn-safe-r);
    user-select: none;
  }
  .xtn-tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: 52px;
    padding: 0 2px;
    font-size: var(--text-11);
    line-height: 1;
    font-weight: 500;
    color: var(--color-fg-faint);
    border-top: 2px solid transparent;
    margin-top: -1px;
    transition: color var(--duration-fast) var(--ease-out-xtn), transform var(--duration-fast) var(--ease-out-xtn);
  }
  .xtn-tab:active {
    color: var(--color-fg);
    background: var(--color-surface-2);
  }
  .xtn-tab[aria-current="page"] {
    color: var(--color-fg);
    border-top-color: var(--section, var(--color-accent));
  }
  .xtn-tab[aria-current="page"] .xtn-tab-mark {
    color: var(--section, var(--color-accent));
  }

  /* Topbar téléphone : 44px, la phase à gauche, la recherche à droite. Collante sous la zone sûre. */
  .xtn-topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-subnav);
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;
    margin: -1.25rem calc(-1 * max(1.25rem, var(--xtn-safe-r))) 0.75rem calc(-1 * max(1.25rem, var(--xtn-safe-l)));
    padding: var(--xtn-safe-t) max(0.75rem, var(--xtn-safe-r)) 0 max(0.75rem, var(--xtn-safe-l));
    background: color-mix(in oklab, var(--color-bg) 88%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  /* Collants : c'est ce qui manquait le plus. L'ancienne bande disparaissait en haut de toute page
     longue, donc changer d'écran voulait dire remonter d'abord. */
  .xtn-subtabs {
    position: sticky;
    top: 0;
    z-index: var(--z-subnav);
    margin: -0.75rem calc(-1 * max(1.25rem, var(--xtn-safe-r))) 1rem calc(-1 * max(1.25rem, var(--xtn-safe-l)));
    padding: 0.5rem max(1.25rem, var(--xtn-safe-r)) 0.5rem max(1.25rem, var(--xtn-safe-l));
    border-bottom: 1px solid var(--color-border);
    background: color-mix(in oklab, var(--color-bg) 88%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }
  .xtn-subtab {
    display: inline-flex;
    align-items: center;
    flex: none;
    border-radius: var(--radius-btn);
    border: 1px solid transparent;
    padding: 0 0.75rem;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    white-space: nowrap;
    color: var(--color-fg-muted);
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  .xtn-subtab:active {
    background: var(--color-surface-3);
    color: var(--color-fg);
  }
  .xtn-subtab[aria-current="page"] {
    color: var(--color-fg);
    background: var(--color-surface-2);
    border-color: var(--color-border);
  }

  /* Sidebar ordinateur : un item porte un petit carré de la teinte de sa section quand il est actif. */
  .xtn-navlink {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    white-space: nowrap;
    border-radius: var(--radius-btn);
    padding: 0.35rem 0.6rem;
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
    color: var(--color-fg-muted);
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .xtn-navlink:hover {
      background: var(--color-surface-2);
      color: var(--color-fg);
    }
  }
  .xtn-navlink[aria-current="page"] {
    background: var(--color-surface-2);
    color: var(--color-fg);
  }
  .xtn-navlink[aria-current="page"] .xtn-navdot {
    background: var(--section, var(--color-accent));
  }
  .xtn-navdot {
    width: 6px;
    height: 6px;
    border-radius: 2px;
    background: var(--color-border-strong);
    flex: none;
    transition: background-color var(--duration-fast) var(--ease-out-xtn);
  }

  /* Ligne d'inventaire sur téléphone (components/EntityTable). `:active` scopé à `button` : une ligne
     n'est une cible que si l'appelant a passé onRowClick. */
  .etbl-row {
    display: block;
    width: 100%;
    min-height: 44px;
    text-align: left;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
  }
  button.etbl-row {
    transition: background-color 120ms var(--ease-out-xtn);
  }
  button.etbl-row:active {
    background: var(--color-surface-2);
  }
  button.etbl-row:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
  }

  /* Tuile d'app (launcher). La teinte de l'app vit dans le glyphe et le filet du haut, jamais dans le fond. */
  .app-tile {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.45rem;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    padding: 0.85rem 0.5rem 0.7rem;
    text-align: center;
    color: var(--color-fg);
    transition:
      border-color var(--duration-fast) var(--ease-out-xtn),
      background-color var(--duration-fast) var(--ease-out-xtn),
      transform var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .app-tile:hover {
      border-color: var(--color-border-strong);
      background: var(--color-surface-2);
    }
  }
  /* Pendant l'appui long (`data-pressing`, AppGrid) et sous le doigt : la tuile s'enfonce d'un cran
     et se resserre d'un demi-pour-cent — le retour visuel qui tient lieu d'haptique. */
  .app-tile:active,
  .app-tile[data-pressing] {
    border-color: var(--color-border-strong);
    background: var(--color-surface-2);
  }
  .app-tile[data-pressing] {
    transform: scale(0.98);
  }

  /* Une carte-lien ou une carte-bouton (ligne d'inbox, brief, alerte, tâche faite, tuile KPI) dit
     qu'elle a été touchée : un cran plus clair, comme `.etbl-row`. Le titre d'une ligne de tâche
     (`.row-open`) aussi — c'est un bouton sans cadre, il lui faut un état. */
  a.card:active,
  button.card:active,
  a.stat:active,
  .row-open:active,
  .xtn-tap-icon:active {
    background: var(--color-surface-2);
  }
  /* ── Réglages (SettingsSheet ; page « Réglages » de chaque app sœur) ─────────────────────── */
  /* Un seul squelette pour toute la famille : un titre `.settings-title`, des sections `.card` dans
     un ordre fixe (Compte, Apparence, Notifications, Application, Intégrations), chacune un `.t-th`
     puis des lignes à filet de 44 px. Téléphone d'abord — cartes pleine largeur ; 640 px au plus
     au-delà (`.settings-page`, pour une page ; la sheet de corextn garde sa largeur). */
  .settings-page {
    width: 100%;
    max-width: 640px;
    margin-inline: auto;
    padding: 1rem;
    padding-bottom: max(1rem, env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
  }
  .settings-title {
    font-size: var(--text-20);
    line-height: var(--text-20--line-height);
    letter-spacing: var(--text-20--letter-spacing);
    font-weight: var(--text-20--font-weight);
    color: var(--color-fg);
  }
  .settings-section {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    overflow: hidden;
  }
  .settings-head {
    padding: 0.75rem 1rem 0.375rem;
  }
  /* Une ligne : libellé à gauche, valeur ou commande à droite, un filet entre deux. Un `button` ou
     un `a` en ligne dit qu'il a été touché comme une carte-bouton : un cran plus clair. */
  .settings-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    width: 100%;
    min-height: 44px;
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--color-border);
    text-align: left;
    color: var(--color-fg);
    transition: background-color var(--duration-fast) var(--ease-out-xtn);
  }
  .settings-head + .settings-row,
  .settings-section > .settings-row:first-child {
    border-top: 0;
  }
  button.settings-row:active,
  a.settings-row:active {
    background: var(--color-surface-2);
  }
  .settings-label {
    min-width: 0;
    flex: 1;
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
  }
  .settings-value {
    min-width: 0;
    max-width: 60%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: right;
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
    color: var(--color-fg-muted);
    font-variant-numeric: tabular-nums;
  }
  .settings-hint {
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-muted);
  }
  /* Le corps d'une section qui n'est pas une liste de lignes : un contrôle segmenté, un bouton
     plein, un paragraphe d'état. */
  .settings-body {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.375rem 1rem 0.875rem;
    border-top: 1px solid var(--color-border);
  }
  .settings-head + .settings-body {
    border-top: 0;
  }
  /* Le pied « ◉ corextn » des apps sœurs : la marque du hub, ton faible, retour au hub au tap. */
  .settings-footer {
    display: flex;
    justify-content: center;
    padding: 0.25rem 0;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-faint);
  }
  .settings-footer a {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 44px;
    padding: 0 0.75rem;
    border-radius: var(--radius-btn);
    color: var(--color-fg-muted);
    transition: color var(--duration-fast) var(--ease-out-xtn);
  }
  .settings-footer a:active {
    color: var(--color-app-corextn);
  }

}

/* ══ Shell & primitives (P0, couche shell : App.tsx, components/*) ═══════════════════════════════
   Ajouté à la fin, séparé de la charte au-dessus : les classes que la shell et les primitives ont
   demandées en plus. Toujours via les tokens, jamais une couleur littérale. */
@layer components {
  /* Le carré 6×6 au-dessus du libellé d'onglet. `.xtn-tab[aria-current="page"]` lui donne la teinte
     de section via `color` (plus haut), donc le fond suit currentColor. */
  .xtn-tab-mark {
    width: 6px;
    height: 6px;
    border-radius: 2px;
    background: currentColor;
    color: var(--color-border-strong);
    transition: color var(--duration-fast) var(--ease-out-xtn);
  }
  /* Le point d'un titre de groupe dans la sidebar est toujours allumé : c'est la légende du code couleur. */
  .xtn-navdot-on {
    background: var(--section, var(--color-accent));
  }
  /* Les sous-onglets se collent SOUS la topbar téléphone, pas par-dessus : les deux sont sticky. */
  .xtn-topbar + .xtn-subtabs {
    top: calc(44px + var(--xtn-safe-t));
  }
  /* Bande du haut sur ordinateur : recherche ⌘K (P1) et phase (P3). */
  .xtn-desktopbar {
    position: sticky;
    top: 0;
    z-index: var(--z-subnav);
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 2rem;
    border-bottom: 1px solid var(--color-border);
    background: color-mix(in oklab, var(--color-bg) 88%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  /* Contrôle segmenté (Recap) et chips filtrantes (Kanban) : components/Tabs. */
  .xtn-seg {
    display: inline-flex;
    gap: 2px;
    padding: 2px;
    border-radius: var(--radius-btn);
    border: 1px solid var(--color-border);
    background: var(--color-surface-2);
  }
  .xtn-seg-btn {
    border-radius: 6px;
    padding: 0.3rem 0.85rem;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    font-weight: 500;
    color: var(--color-fg-muted);
    white-space: nowrap;
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .xtn-seg-btn:hover {
      color: var(--color-fg);
    }
  }
  .xtn-seg-btn:active,
  .xtn-chip:active {
    background: var(--color-surface-3);
    color: var(--color-fg);
  }
  .xtn-seg-btn[aria-pressed="true"] {
    background: var(--color-surface);
    color: var(--color-fg);
    box-shadow: var(--shadow-1);
  }
  .xtn-chips {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .xtn-chips::-webkit-scrollbar {
    display: none;
  }
  .xtn-chip {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 0.35rem;
    border-radius: var(--radius-btn);
    border: 1px solid var(--color-border);
    padding: 0 0.75rem;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-muted);
    white-space: nowrap;
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  .xtn-chip[aria-pressed="true"] {
    border-color: var(--color-accent);
    background: var(--color-accent-soft);
    color: var(--color-fg);
  }

  /* Pile de toasts : au-dessus de la barre d'onglets et de la zone sûre ; en bas sur ordinateur. */
  .toast-host {
    position: fixed;
    inset-inline: 0;
    bottom: calc(52px + var(--xtn-safe-b) + 0.75rem);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0 1rem;
    pointer-events: none;
  }
  @media (min-width: 768px) {
    .toast-host {
      bottom: calc(1rem + var(--xtn-safe-b));
    }
  }

  /* Colonne de kanban : un filet discret dans la teinte de l'Atelier — le contenu est son domaine. */
  .kanban-col {
    border-top: 2px solid color-mix(in oklab, var(--color-domain-atelier) 60%, transparent);
    padding-top: 0.5rem;
  }
}

/* ══ P1 : cockpit, palette de commandes (components/CommandPalette, AppRow, Stat compact) ═══════════
   Toujours via les tokens, jamais une couleur littérale. */
@layer components {
  /* Tuile KPI trois de front sur téléphone : 20px, marges serrées, 24px à partir de md. */
  .stat-compact {
    padding: 0.6rem 0.7rem;
  }
  .stat-value-compact {
    font-size: var(--text-20);
    line-height: var(--text-20--line-height);
    letter-spacing: -0.015em;
  }
  @media (min-width: 768px) {
    .stat-value-compact {
      font-size: var(--text-24);
      line-height: var(--text-24--line-height);
      letter-spacing: -0.02em;
    }
  }

  /* Pastille d'app de la Home : le mark dans la teinte de l'app, le point de santé au coin. Huit de
     front dans 335px : 36px chacune, 6px d'écart. */
  .app-chip {
    position: relative;
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 0.4rem;
    padding: 6px;
    border-radius: 9999px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    color: var(--color-fg);
    transition:
      border-color var(--duration-fast) var(--ease-out-xtn),
      background-color var(--duration-fast) var(--ease-out-xtn),
      transform var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .app-chip:hover {
      border-color: var(--color-border-strong);
      background: var(--color-surface-2);
    }
  }
  .app-chip:active {
    border-color: var(--color-border-strong);
    background: var(--color-surface-2);
  }
  /* 36px de dessin, 44px de cible : le pseudo-élément déborde de 4px de chaque côté sur tactile.
     Deux voisines se chevauchent de 2px sur les 6px d'écart — invisible, et bien moins qu'un raté. */
  @media (hover: none) and (pointer: coarse) {
    .app-chip::before {
      content: "";
      position: absolute;
      inset: -4px;
      border-radius: 9999px;
    }
  }
  @media (min-width: 768px) {
    .app-chip {
      padding: 6px 12px 6px 6px;
    }
  }
  .app-chip-dot {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 9px;
    height: 9px;
    border-radius: 9999px;
    background: currentColor;
    box-shadow: 0 0 0 2px var(--color-surface);
  }

  /* Touche clavier : le ⌘K du champ de recherche, l'Échap de la palette. */
  .kbd {
    display: inline-flex;
    align-items: center;
    flex: none;
    min-height: 20px;
    padding: 0 0.35rem;
    border-radius: 5px;
    border: 1px solid var(--color-border);
    background: var(--color-surface-2);
    font-family: var(--font-sans);
    font-size: var(--text-11);
    line-height: 1;
    color: var(--color-fg-muted);
  }

  /* ── Palette de commandes ────────────────────────────────────────────────────────────────── */
  /* Téléphone d'abord : la feuille prend tout l'écran (le clavier prend la moitié du bas, la liste
     défile au-dessus). Ordinateur : panneau centré de 640px, à 14vh du haut, ombre 3. */
  .palette-root {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    justify-content: center;
    align-items: stretch;
  }
  .palette-backdrop {
    position: absolute;
    inset: 0;
    /* Même voile que `.sheet-backdrop` : le fond de la page à 60 %, dans les deux thèmes. */
    background: color-mix(in oklab, var(--color-bg) 60%, transparent);
  }
  .palette-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    min-height: 0;
    outline: none;
    border: 0;
    border-radius: 0;
    padding-top: var(--xtn-safe-t);
  }
  .palette-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 52px;
    padding: 0.4rem 0.75rem 0.4rem 1rem;
    border-bottom: 1px solid var(--color-border);
  }
  /* 16px littéral sur tactile (base.css l'impose aussi) : Safari zoomerait en dessous, et ne dézoome jamais. */
  .palette-input {
    flex: 1;
    min-width: 0;
    background: transparent;
    border: 0;
    outline: none;
    padding: 0.4rem 0;
    font-size: var(--text-16);
    line-height: var(--text-16--line-height);
    color: var(--color-fg);
  }
  .palette-input::placeholder {
    color: var(--color-fg-faint);
  }
  .palette-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding: 0.35rem;
    padding-bottom: max(0.5rem, var(--xtn-safe-b));
    scroll-padding-block: 0.5rem;
  }
  .palette-kicker {
    padding: 0.6rem 0.65rem 0.15rem;
    font-size: var(--text-11);
    line-height: var(--text-11--line-height);
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-fg-faint);
  }
  .palette-list [cmdk-group-heading] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.55rem 0.65rem 0.25rem;
    font-size: var(--text-11);
    line-height: var(--text-11--line-height);
    font-weight: 500;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-feature-settings: "cv08", "case";
    color: var(--color-fg-muted);
  }
  .palette-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 44px;
    padding: 0.4rem 0.65rem;
    border-radius: var(--radius-btn);
    color: var(--color-fg);
    cursor: default;
    user-select: none;
    scroll-margin-block: 0.35rem;
  }
  .palette-item[data-selected="true"] {
    background: var(--color-surface-2);
  }
  .palette-item:active {
    background: var(--color-surface-3);
  }
  .palette-item[data-disabled="true"] {
    color: var(--color-fg-faint);
  }
  .palette-item-icon {
    display: inline-flex;
    flex: none;
    width: 20px;
    justify-content: center;
    color: var(--color-fg-muted);
  }
  .palette-foot {
    align-items: center;
    gap: 1rem;
    padding: 0.45rem 1rem;
    border-top: 1px solid var(--color-border);
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-faint);
  }
  @media (min-width: 768px) {
    .palette-root {
      align-items: flex-start;
      padding-top: 14vh;
    }
    .palette-panel {
      width: 640px;
      max-height: 70vh;
      padding-top: 0;
      border: 1px solid var(--color-border);
      border-radius: var(--radius-card);
      box-shadow: var(--shadow-3);
    }
    .palette-input {
      font-size: var(--text-15);
      line-height: var(--text-15--line-height);
    }
    .palette-item {
      min-height: 40px;
    }
  }
}

/* ══ iPhone : tirer-pour-rafraîchir, balayer-pour-cocher (components/PullToRefresh, lib/useSwipeDone) ═
   Toujours via les tokens, jamais une couleur littérale. */
@layer components {
  /* Le repère sous la topbar : un anneau Nucleus (`.ptr-ring`, motion.css) dans une bande dont la
     hauteur suit le doigt (`--ptr`, écrite par le JS). Pendant le tirage, pas de transition — elle
     suit ; au relâchement, la bande se replie. `armed` = le relâchement va recharger : l'anneau
     passe en jade. */
  .ptr {
    position: fixed;
    inset-inline: 0;
    top: calc(44px + var(--xtn-safe-t));
    z-index: calc(var(--z-subnav) - 1);
    display: flex;
    align-items: flex-end;
    justify-content: center;
    height: var(--ptr, 0px);
    overflow: hidden;
    pointer-events: none;
    transition: height var(--duration-base) var(--ease-out-xtn);
  }
  .ptr[data-state="pull"],
  .ptr[data-state="armed"] {
    transition: none;
  }
  @media (min-width: 768px) {
    .ptr {
      top: var(--xtn-safe-t);
    }
  }

  /* L'enveloppe coupe ce qui dépasse ; la zone jade attend derrière ; la carte glisse devant.
     `pan-y` : le défilement vertical reste au navigateur, l'horizontal nous revient. */
  .swipe {
    position: relative;
    border-radius: var(--radius-card);
  }
  /* Ne rogne que pendant le geste (`data-swiping`, posé par le hook) : au repos les anneaux de focus
     des boutons de la ligne peuvent déborder de la carte. */
  .swipe[data-swiping="true"] {
    overflow: hidden;
  }
  .swipe-face {
    position: relative;
    touch-action: pan-y;
    transition: transform var(--duration-base) var(--ease-out-xtn);
  }
  @media (hover: none) and (pointer: coarse) {
    .swipe-face {
      user-select: none;
      -webkit-user-select: none;
      -webkit-touch-callout: none;
    }
  }
  .swipe-zone {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    padding-left: 1rem;
    border-radius: var(--radius-card);
    background: var(--color-accent-soft);
    color: var(--color-accent-text);
    font-size: var(--text-14);
    font-weight: 500;
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  .swipe[data-swiping="true"] .swipe-zone {
    display: flex;
  }
  /* Au-delà du seuil : la zone passe en jade plein — le relâchement va cocher. */
  .swipe[data-armed="true"] .swipe-zone {
    background: var(--color-accent);
    color: var(--color-accent-fg);
  }
}

/* ══ P2 : fiches d'entité (components/EntitySheet), inbox persistante, réglages ═══════════════════
   Toujours via les tokens, jamais une couleur littérale. */
@layer components {
  /* Panneau latéral (ordinateur, ≥ md) : la fiche se pose à droite, 420px, pleine hauteur, et glisse
     depuis le bord droit avec la même courbe que la sheet sur téléphone (320 `--ease-sheet`, sortie
     200 accélérée). Le voile reste : Échap, un clic dessus ou le geste retour ferment. En dessous de
     md, c'est la bottom sheet de TaskDetail — et entre sm et md, son panneau centré. */
  /* La géométrie du panneau vit ici, pas en utilitaires : `w-full` (couche utilities) battrait la
     largeur de 420px posée dans la couche components. Sous sm : toute la largeur (bottom sheet) ;
     entre sm et md : le panneau centré de 32rem des autres sheets. */
  .sheet-panel.sheet-side {
    width: 100%;
  }
  @media (min-width: 640px) {
    .sheet-panel.sheet-side {
      max-width: 32rem;
    }
  }
  @media (min-width: 768px) {
    .sheet-panel.sheet-side {
      width: 420px;
      max-width: 100%;
      height: 100%;
      max-height: none;
      border-radius: 0;
      border-top: 0;
      border-right: 0;
      border-bottom: 0;
      border-left: 1px solid var(--color-border);
      box-shadow: var(--shadow-3);
      opacity: 1;
      transform: translate3d(100%, 0, 0);
      transition: transform var(--duration-sheet) var(--ease-sheet);
    }
    .sheet-panel.sheet-side[data-state="open"] {
      transform: none;
    }
    .sheet-panel.sheet-side[data-state="closing"] {
      transform: translate3d(100%, 0, 0);
      transition: transform var(--duration-base) var(--ease-exit-xtn);
    }
  }

  /* Pastille de compte (non-lus) : 16px, jade, sur l'onglet Jour et le lien Inbox de la sidebar. */
  .badge-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 9999px;
    background: var(--color-accent);
    color: var(--color-accent-fg);
    font-size: var(--text-11);
    line-height: 1;
    font-weight: 600;
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
  }
  /* Sur l'onglet, la pastille se pose à droite du carré, sans déplacer le libellé. */
  .xtn-tab {
    position: relative;
  }
  .xtn-tab .badge-count {
    position: absolute;
    top: 4px;
    left: calc(50% + 6px);
  }

  /* Chip de relation (fiche) : un point dans le ton du statut, le titre. Un bouton quand la relation
     a un écran (téléphone, compte, personne…), un simple span sinon (livrable). */
  .chip-rel {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    max-width: 100%;
    min-height: 32px;
    padding: 0 0.7rem;
    border-radius: 9999px;
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    color: var(--color-fg);
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
    text-align: left;
    transition:
      background-color var(--duration-fast) var(--ease-out-xtn),
      border-color var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    button.chip-rel:hover {
      border-color: var(--color-border-strong);
      background: var(--color-surface-2);
    }
  }
  button.chip-rel:active {
    background: var(--color-surface-3);
  }
  @media (hover: none) and (pointer: coarse) {
    button.chip-rel {
      min-height: 44px;
    }
  }

  /* Balayer vers la gauche (Inbox : marquer lu) : la zone attend à droite. Le recul du « clic » est
     dans motion.css. */
  .swipe[data-dir="left"] .swipe-zone {
    justify-content: flex-end;
    padding-left: 0;
    padding-right: 1rem;
  }

  /* Le chevron d'une carte repliable (Inbox › État) : il tourne d'un quart quand elle s'ouvre. */
  .chevron {
    display: inline-block;
    transition: transform var(--duration-fast) var(--ease-out-xtn);
  }
  [aria-expanded="true"] > .chevron {
    transform: rotate(90deg);
  }

  /* Ligne de notification : non lue = texte fg sur surface-2, lue = fg-muted sur la surface. */
  .notif {
    display: flex;
    width: 100%;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.7rem 0.75rem;
    text-align: left;
    color: var(--color-fg-muted);
    transition: background-color var(--duration-fast) var(--ease-out-xtn), color var(--duration-fast) var(--ease-out-xtn);
  }
  .notif[data-unread] {
    color: var(--color-fg);
    background: var(--color-surface-2);
  }
  @media (hover: hover) {
    button.notif:hover {
      background: var(--color-surface-2);
      color: var(--color-fg);
    }
  }
  button.notif:active {
    background: var(--color-surface-3);
  }

  /* ── Connexion ────────────────────────────────────────────────────────────────────────────── */
  /* L'écran de connexion de la famille, le même dans chaque app (pages/Login.tsx de corextn est la
     référence) : une carte de 360 px, le mark de l'app à 40 px, le wordmark à 18 px et une ligne de
     sous-titre, des libellés `.t-th` sur des `.input`, « Entrer », l'erreur sous le formulaire, et
     sous la carte « ◉ corextn » chez les sœurs. Téléphone : la carte est posée à ~22 % du haut pour
     que le clavier ne recouvre jamais le bouton ; ordinateur : centrée. Elle entre comme une page
     (fondu + 6 px, `xtn-rise` de motion.css) ; rien d'autre ne bouge, sauf le tour du satellite chez
     corextn. Les 16 px des champs sur tactile viennent de base.css. */
  .login-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100%;
    padding: 22vh 1.5rem 1.5rem;
    padding-top: 22svh;
    background: var(--color-bg);
  }
  @media (min-width: 640px) and (min-height: 600px) {
    .login-shell {
      justify-content: center;
      padding-top: 1.5rem;
    }
  }
  .login-card {
    width: 100%;
    max-width: 360px;
    padding: 28px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-2);
    animation: xtn-rise var(--duration-base) var(--ease-out-xtn) backwards;
  }
  .login-brand {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
  }
  .login-brand > svg {
    flex: none;
  }
  /* Le wordmark à 18 px — la règle de `Wordmark` (Mark.tsx) : 600, −2 %, « xtn » en fg-muted. */
  .login-brand b {
    display: block;
    font-size: var(--text-18);
    line-height: 1;
    font-weight: 600;
    letter-spacing: -0.02em;
    white-space: nowrap;
    color: var(--color-fg);
  }
  .login-brand b > span {
    color: var(--color-fg-muted);
  }
  .login-sub {
    margin-top: 0.25rem;
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
    color: var(--color-fg-muted);
  }
  .login-form {
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
    margin-top: 1.5rem;
  }
  .login-form label {
    display: block;
    margin-bottom: 0.375rem;
  }
  .login-form .btn {
    margin-top: 0.25rem;
  }
  .login-error {
    margin-top: 0.75rem;
    font-size: var(--text-13);
    line-height: var(--text-13--line-height);
    color: var(--color-danger);
  }
  .login-foot {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 1.25rem;
    font-size: var(--text-12);
    line-height: var(--text-12--line-height);
    color: var(--color-fg-faint);
  }
  .login-foot > svg {
    flex: none;
  }
}


/* Mouvement — la grammaire est dans docs/design/MOTION.md ; ce fichier l'applique.
   Trois durées (`--duration-fast` 120 · `--duration-base` 200 · `--duration-sheet` 320), deux courbes
   (`--ease-out-xtn` pour entrer, `--ease-exit-xtn` pour sortir) et `--ease-sheet` pour ce qui se pose.
   Seuls `transform` et `opacity` bougent — sauf le repli `.task-slot` (components.css). Rien ne
   boucle, hormis le shimmer du skeleton et l'anneau du tirer-pour-rafraîchir. Toujours via les tokens. */

/* ── Keyframes (hors couche : un nom d'animation n'a pas de spécificité) ─────────────────────────── */
/* Entrer : apparaître en montant de 6px. Sert à la page, aux listes, aux résultats de la palette. */
@keyframes xtn-rise {
  from {
    opacity: 0;
    transform: translate3d(0, 6px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes xtn-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes xtn-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
/* Un panneau centré (palette sur ordinateur) : il se déploie de 98 % à 100 %, jamais d'un point. */
@keyframes xtn-pop-in {
  from {
    opacity: 0;
    transform: scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes xtn-pop-out {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: scale(0.98);
  }
}
/* Une feuille plein écran (palette sur téléphone) : elle monte du bord bas et se pose. */
@keyframes xtn-sheet-up {
  from {
    transform: translate3d(0, 100%, 0);
  }
  to {
    transform: none;
  }
}
@keyframes xtn-sheet-down {
  from {
    transform: none;
  }
  to {
    transform: translate3d(0, 100%, 0);
  }
}
@keyframes xtn-toast-in {
  from {
    opacity: 0;
    transform: translate3d(0, 12px, 0);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes xtn-toast-out {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translate3d(0, 4px, 0);
  }
}
/* La case qui se coche : un battement, 1 → 1,08 → 1. */
@keyframes xtn-pop {
  0% {
    transform: none;
  }
  50% {
    transform: scale(1.08);
  }
  100% {
    transform: none;
  }
}
/* Le « clic » d'un balayage qui commet : un recul de 3px, aller-retour. Tient lieu d'haptique. */
@keyframes xtn-snap {
  0% {
    transform: none;
  }
  50% {
    transform: translate3d(-3px, 0, 0);
  }
  100% {
    transform: none;
  }
}
/* Le même « clic », miroir, pour un balayage vers la gauche (Inbox : marquer lu). */
@keyframes xtn-snap-r {
  0% {
    transform: none;
  }
  50% {
    transform: translate3d(3px, 0, 0);
  }
  100% {
    transform: none;
  }
}
/* Le compte à rebours d'un toast : une ligne qui se vide de gauche à droite. */
@keyframes xtn-drain {
  from {
    transform: scaleX(1);
  }
  to {
    transform: scaleX(0);
  }
}
/* L'anneau du tirer-pour-rafraîchir : repart de l'angle où le doigt l'a laissé (`--ptr-turn`). */
@keyframes xtn-turn {
  from {
    transform: rotate(var(--ptr-turn, 0deg));
  }
  to {
    transform: rotate(calc(var(--ptr-turn, 0deg) + 360deg));
  }
}

@layer components {
  /* ── Page ────────────────────────────────────────────────────────────────────────────────── */
  /* À chaque changement de chemin, le contenu de <main> monte de 6px en apparaissant (App :
     `<PageEnter key={pathname}>`). `backwards` et non `both` : après 200 ms l'élément n'a plus de
     transform, donc un `position: fixed` à l'intérieur retrouve le viewport (les sheets sont de toute
     façon portées sur <body>). Quand une view transition joue déjà le fondu (`data-vt`), pas deux fois. */
  .page-enter {
    animation: xtn-rise var(--duration-base) var(--ease-out-xtn) backwards;
  }
  .page-enter[data-vt] {
    animation: none;
  }

  /* ── Listes ──────────────────────────────────────────────────────────────────────────────── */
  /* Premier rendu d'une liste : chaque enfant monte de 6px, 30 ms après le précédent, six au plus —
     le reste arrive ensemble derrière le sixième. L'index est posé en `--i` (lib/motion `stagger`) ;
     `data-animated` (lib/motion `useStagger`) est écrit une fois l'entrée jouée : un re-rendu, un
     rechargement, une ligne ajoutée plus tard n'animent plus rien. */
  .stagger > * {
    animation: xtn-rise var(--duration-base) var(--ease-out-xtn) backwards;
    animation-delay: calc(min(var(--i, 0), 6) * 30ms);
  }
  .stagger[data-animated] > * {
    animation: none;
  }
  /* Un bloc qui remplace son skeleton fond en 120 ms au lieu d'apparaître d'un coup. */
  .fade-swap {
    animation: xtn-fade-in var(--duration-fast) var(--ease-out-xtn) backwards;
  }

  /* ── Cocher ──────────────────────────────────────────────────────────────────────────────── */
  /* La case bat une fois quand elle se remplit ; la coche se dessine par-dessus (components.css). */
  .tick[aria-checked="true"] .tick-box {
    animation: xtn-pop 160ms var(--ease-out-xtn);
  }
  /* La ligne qui part glisse de 8px vers la droite pendant qu'elle se replie — le sens du balayage. */
  .task-slot .swipe {
    transition: transform var(--duration-base) var(--ease-exit-xtn);
  }
  .task-slot.is-leaving .swipe {
    transform: translate3d(8px, 0, 0);
  }

  /* ── Balayer pour cocher ─────────────────────────────────────────────────────────────────── */
  /* Le ✓ de la zone jade attend en retrait (60 %) et prend sa taille quand le geste est armé. */
  .swipe-zone-check {
    display: inline-block;
    transform: scale(0.6);
    opacity: 0.5;
    transition:
      transform var(--duration-fast) var(--ease-out-xtn),
      opacity var(--duration-fast) var(--ease-out-xtn);
  }
  .swipe[data-armed="true"] .swipe-zone-check {
    transform: none;
    opacity: 1;
  }
  /* Le relâchement qui commet : la carte accélère hors champ et l'enveloppe recule de 3 px (60 ms). */
  .swipe[data-commit] {
    animation: xtn-snap 60ms var(--ease-out-xtn);
  }
  .swipe[data-commit] .swipe-face {
    transition-timing-function: var(--ease-exit-xtn);
  }
  /* Vers la gauche, l'enveloppe recule vers la droite : le recul va toujours contre le geste. */
  .swipe[data-dir="left"][data-commit] {
    animation-name: xtn-snap-r;
  }

  /* ── Sheets ──────────────────────────────────────────────────────────────────────────────── */
  /* Le panneau monte (components.css) ; son contenu fond 80 ms plus tard, en 120 ms — l'œil suit le
     bord, puis lit. En sortie, rien : le panneau part avec tout dedans. */
  .sheet-panel > * {
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-out-xtn) 80ms;
  }
  .sheet-panel[data-state="open"] > *,
  .sheet-panel[data-state="closing"] > * {
    opacity: 1;
  }
  .sheet-panel[data-state="closing"] > * {
    transition: none;
  }

  /* ── Palette ─────────────────────────────────────────────────────────────────────────────── */
  /* Téléphone : la feuille monte en 320 ms et se pose ; ordinateur : le panneau se déploie de 98 % à
     100 % en 200 ms. Le fond fond en 200. Sortie (`data-state="closing"`, lib/palette) : plus court,
     accélérée, et rien n'est cliquable pendant. */
  .palette-backdrop {
    animation: xtn-fade-in var(--duration-base) var(--ease-out-xtn) backwards;
  }
  .palette-panel {
    animation: xtn-sheet-up var(--duration-sheet) var(--ease-sheet) backwards;
  }
  .palette-root[data-state="closing"] {
    pointer-events: none;
  }
  .palette-root[data-state="closing"] .palette-backdrop {
    animation: xtn-fade-out var(--duration-base) var(--ease-exit-xtn) forwards;
  }
  .palette-root[data-state="closing"] .palette-panel {
    animation: xtn-sheet-down var(--duration-base) var(--ease-exit-xtn) forwards;
  }
  @media (min-width: 768px) {
    .palette-panel {
      animation: xtn-pop-in var(--duration-base) var(--ease-out-xtn) backwards;
    }
    .palette-root[data-state="closing"] .palette-panel {
      animation: xtn-pop-out var(--duration-fast) var(--ease-exit-xtn) forwards;
    }
  }
  /* Les résultats se réordonnent à chaque requête : ils rentrent en cascade, six au plus
     (PaletteSheet pose `--i` et une clé par requête). */
  .palette-item {
    animation: xtn-rise var(--duration-base) var(--ease-out-xtn) backwards;
    animation-delay: calc(min(var(--i, 0), 6) * 30ms);
  }

  /* ── Toast ───────────────────────────────────────────────────────────────────────────────── */
  .toast {
    position: relative;
    animation: xtn-toast-in var(--duration-base) var(--ease-out-xtn) backwards;
  }
  .toast[data-leaving] {
    animation: xtn-toast-out var(--duration-fast) var(--ease-exit-xtn) forwards;
  }
  /* Le temps qu'il reste pour « Annuler » : une ligne de 2 px en jade qui se vide en `--ttl`, linéaire
     (un compte à rebours ne décélère pas). `scaleX` seulement, sur un pseudo-élément : rien à peindre. */
  .toast[data-timed]::after {
    content: "";
    position: absolute;
    left: 10px;
    right: 10px;
    bottom: 0;
    height: 2px;
    border-radius: 1px;
    background: var(--color-accent);
    transform-origin: left center;
    animation: xtn-drain var(--ttl, 7000ms) linear forwards;
  }

  /* ── La marque, instrument ───────────────────────────────────────────────────────────────── */
  /* Le satellite des marks de la shell lit `--sat` (posé sur <html> par lib/motion `pointSatellite`
     quand une pastille ou une tuile d'app est survolée) : la marque prend la teinte de l'app visée. */
  .mark-sat {
    fill: var(--sat, var(--color-accent));
    transition: fill var(--duration-fast) var(--ease-out-xtn);
  }
  @media (hover: hover) {
    .app-chip > svg,
    .app-tile > a > svg {
      transition: transform var(--duration-fast) var(--ease-out-xtn);
    }
    .app-chip:hover > svg,
    .app-tile:hover > a > svg {
      transform: translate3d(0, -1px, 0);
    }
  }

  /* ── Appuis ──────────────────────────────────────────────────────────────────────────────── */
  /* Sous le doigt ou la souris, un contrôle se resserre de 2 % (120 ms) — avec, pas à la place, de
     son changement de couleur (components.css). Jamais quand il est inactif. */
  .btn:active:not(:disabled),
  .app-tile:active,
  .app-chip:active,
  .xtn-tab:active {
    transform: scale(0.98);
  }

  /* ── Tirer pour rafraîchir ───────────────────────────────────────────────────────────────── */
  /* Un anneau Nucleus sans satellite. Pendant le tirage il suit le doigt : `--ptr-o` (opacité) et
     `--ptr-turn` (rotation, 180° au seuil) sont écrits par le JS, sans transition. Armé : jade.
     En rechargement : un tour par 700 ms, linéaire — la seule boucle de l'app avec le shimmer. */
  .ptr-ring {
    display: inline-flex;
    margin-bottom: 8px;
    color: var(--color-fg-muted);
    opacity: var(--ptr-o, 0);
    transform: rotate(var(--ptr-turn, 0deg));
    transition: color var(--duration-fast) var(--ease-out-xtn);
  }
  .ptr[data-state="idle"] .ptr-ring {
    transition:
      color var(--duration-fast) var(--ease-out-xtn),
      opacity var(--duration-fast) var(--ease-exit-xtn),
      transform var(--duration-base) var(--ease-out-xtn);
  }
  .ptr[data-state="armed"] .ptr-ring,
  .ptr[data-state="refreshing"] .ptr-ring {
    color: var(--color-accent);
  }
  .ptr[data-state="refreshing"] .ptr-ring {
    opacity: 1;
    animation: xtn-turn 700ms linear infinite;
  }
}

/* ── Changement de page : view transitions ─────────────────────────────────────────────────────── */
/* Quand le navigateur sait (Safari 18, Chrome), lib/viewTransition enveloppe la navigation dans
   `document.startViewTransition`. Les barres (topbar, sous-onglets, barre d'onglets, sidebar, toasts)
   sont nommées pour rester en place ; ce qui reste dans `root` — le contenu — fond en sortant (120 ms,
   accéléré) et monte de 6px en entrant (200 ms). Le carré de l'onglet actif et le point de la sidebar
   partagent un nom : ils glissent d'un onglet à l'autre au lieu de clignoter. Hors couche : les
   pseudo-éléments `::view-transition-*` ne se battent avec rien. */
@media (prefers-reduced-motion: no-preference) {
  .xtn-topbar {
    view-transition-name: topbar;
  }
  .xtn-subtabs {
    view-transition-name: subtabs;
  }
  .xtn-tabbar {
    view-transition-name: tabbar;
  }
  .xtn-sidebar {
    view-transition-name: sidebar;
  }
  .xtn-desktopbar {
    view-transition-name: desktopbar;
  }
  .toast-host {
    view-transition-name: toasts;
  }
  .xtn-tab[aria-current="page"] .xtn-tab-mark,
  .xtn-navlink[aria-current="page"] .xtn-navdot {
    view-transition-name: tab-mark;
  }
}
::view-transition-group(*) {
  animation-duration: var(--duration-base);
  animation-timing-function: var(--ease-out-xtn);
}
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: var(--duration-base);
  animation-timing-function: var(--ease-out-xtn);
}
::view-transition-old(root) {
  animation: xtn-fade-out var(--duration-fast) var(--ease-exit-xtn) both;
}
::view-transition-new(root) {
  animation: xtn-rise var(--duration-base) var(--ease-out-xtn) both;
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
