Source unique : tokens/hublot.tokens.json (W3C DTCG), compilée par node scripts/build.mjs vers tokens.css et dist/ (SCSS, Vuetify 2 et 3, Tailwind, Android, iOS, Claude Design). La vitrine review.html montre les composants en situation ; ce catalogue liste tout ce que contiennent les fichiers. Régénéré par node catalogue.mjs.
| Token | Clair | Sombre | Boucherie | Usage |
|---|---|---|---|---|
--surface | #f3f5f8 | #0f141b | | |
--surface-raised | #ffffff | #161d27 | | Cartes, panneaux, menus, lignes de liste et de tableau : ce qui est posé sur `surface`. |
--surface-sunken | #eceff3 | #0c1117 | | Zones en retrait : en-têtes de tableau, groupes de champs, commentaires, fond de code/PLU. |
--surface-hover | #f6f8fa | #1b2430 | | Survol d'une ligne, d'un champ éditable au repos, d'un bouton neutre. |
--line | #dde3ea | #243040 | | Filets et séparateurs de lignes. Décoratif, jamais porteur de sens seul. |
--line-strong | #8794a3 | #637386 | | Bordure des champs de saisie, cases à cocher, boutons neutres : ≥ 3:1 sur `surface-raised`, un champ se voit avant d'être cliqué. |
--ink | #1a2230 | #e6ebf1 | | Texte principal, titres, chiffres. |
--ink-muted | #5b6675 | #9aa7b6 | | Texte secondaire, libellés, métadonnées, icônes neutres. |
--ink-faint | #8b95a3 | #6c7886 | | Placeholders, texte désactivé, indices d'édition : jamais pour une information indispensable. |
--poissonnerie | #376374 | #8ac3d5 | | Couleur principale Poissonnerie, le bleu pétrole (logo, site, barre latérale). |
--poissonnerie-hover | #2b515f | #a6d3e1 | | Survol et appui du bouton principal Poissonnerie. |
--poissonnerie-soft | #e1eef2 | #8ac3d524 | | Fond teinté derrière du texte `poissonnerie` : ligne sélectionnée, onglet actif, badge info. |
--on-poissonnerie | #ffffff | #0f141b | | Texte et icônes sur un fond `poissonnerie`. |
--poissonnerie-accent | #8ac3d5 | #8ac3d5 | | Couleur secondaire Poissonnerie, le bleu clair : remplissage, repère de navigation, série 1 des graphiques. Jamais comme texte sur fond clair. |
--poissonnerie-accent-soft | #e8f4f7 | #8ac3d51f | | Bandeau ou carte teintée Poissonnerie. |
--on-poissonnerie-accent | #12313a | #0f141b | | Texte sur un aplat `poissonnerie-accent`. |
--poissonnerie-nav | #2f5563 | #0c1419 | | Fond de la barre latérale Poissonnerie : la couleur principale, un ton plus sombre, comme sur la tablette et magasin. |
--poissonnerie-nav-active | #a6d3e1 | #8ac3d5 | | Texte, icône et repère de l'entrée active sur `poissonnerie-nav` (v3 : éclairci de #8ac3d5 à #a6d3e1 pour passer 5:1 sur la barre). |
--poissonnerie-shade | rgba(16, 52, 66, 0.07) | rgba(0, 0, 0, 0.42) | | Teinte des ombres en Poissonnerie : bleu pétrole, jamais un gris neutre. Utilisée par les tokens d'ombre, pas directement. |
--poissonnerie-shade-strong | rgba(16, 52, 66, 0.15) | rgba(0, 0, 0, 0.62) | | Teinte appuyée des ombres en Poissonnerie (portée longue des surfaces flottantes). |
--boucherie | #9d0419 | #f28b95 | | Couleur principale Boucherie, le rouge bordeaux. |
--boucherie-hover | #7d0314 | #f7aab2 | | Survol et appui du bouton principal Boucherie. |
--boucherie-soft | #f9e3e6 | #e4435324 | | Fond teinté derrière du texte `boucherie`. |
--on-boucherie | #ffffff | #0f141b | | Texte et icônes sur un fond `boucherie`. |
--boucherie-accent | #e44353 | #e44353 | | Couleur secondaire Boucherie, le rouge vif : remplissage, repère, série 1. Jamais comme texte sur fond clair. |
--boucherie-accent-soft | #fbe6e8 | #e443531f | | Bandeau ou carte teintée Boucherie. |
--on-boucherie-accent | #2a0508 | #1b0305 | | Texte sur un aplat `boucherie-accent` : sombre dans les deux thèmes (le blanc ne passait que 4:1 sur le rouge vif). |
--boucherie-nav | #7a0a1c | #1b1014 | | Fond de la barre latérale Boucherie. |
--boucherie-nav-active | #f59aa3 | #f59aa3 | | Texte, icône et repère de l'entrée active sur `boucherie-nav`. |
--boucherie-shade | rgba(64, 12, 22, 0.07) | rgba(0, 0, 0, 0.42) | | Teinte des ombres en Boucherie : bordeaux sombre. |
--boucherie-shade-strong | rgba(64, 12, 22, 0.15) | rgba(0, 0, 0, 0.62) | | Teinte appuyée des ombres en Boucherie. |
--brand | var(--poissonnerie) | var(--poissonnerie) | var(--boucherie) | Emplacement métier : l'action principale, le lien actif, l'onglet courant, les icônes d'action. Les composants n'utilisent que les emplacements, jamais une couleur métier directement. |
--brand-hover | var(--poissonnerie-hover) | var(--poissonnerie-hover) | var(--boucherie-hover) | Survol et appui de `brand`. |
--brand-soft | var(--poissonnerie-soft) | var(--poissonnerie-soft) | var(--boucherie-soft) | Fond teinté derrière du texte `brand`. |
--on-brand | var(--on-poissonnerie) | var(--on-poissonnerie) | var(--on-boucherie) | Texte et icônes sur `brand`. Jamais du blanc en dur : `brand` s'éclaircit dans le thème sombre. |
--accent | var(--poissonnerie-accent) | var(--poissonnerie-accent) | var(--boucherie-accent) | Emplacement métier : remplissage (icône, repère, série 1 des graphiques, bandeau). Jamais comme texte sur fond clair. |
--accent-soft | var(--poissonnerie-accent-soft) | var(--poissonnerie-accent-soft) | var(--boucherie-accent-soft) | Bandeau ou carte teintée du métier. |
--on-accent | var(--on-poissonnerie-accent) | var(--on-poissonnerie-accent) | var(--on-boucherie-accent) | Texte sur un aplat `accent`, jamais blanc. |
--nav | var(--poissonnerie-nav) | var(--poissonnerie-nav) | var(--boucherie-nav) | Fond de la barre latérale, sombre dans les deux thèmes. |
--shade | var(--poissonnerie-shade) | var(--poissonnerie-shade) | var(--boucherie-shade) | Ombre courte du métier (contact avec la surface). |
--shade-strong | var(--poissonnerie-shade-strong) | var(--poissonnerie-shade-strong) | var(--boucherie-shade-strong) | Ombre portée du métier (élévation). |
--nav-ink | #ffffff | #e6ebf1 | | Libellés et icônes de navigation sur `nav`. |
--nav-muted | rgba(255, 255, 255, 0.78) | #c3ccd6 | | Entrées inactives sur `nav`. |
--nav-hover | rgba(255, 255, 255, 0.10) | rgba(255, 255, 255, 0.08) | | Survol d'une entrée sur `nav`. |
--nav-active | var(--poissonnerie-nav-active) | var(--poissonnerie-nav-active) | var(--boucherie-nav-active) | Texte, icône et barre de l'entrée active. |
--nav-active-bg | rgba(255, 255, 255, 0.12) | rgba(255, 255, 255, 0.10) | | Fond de l'entrée active. |
--info | var(--brand) | var(--brand) | var(--brand) | Information neutre : bandeau, aide contextuelle. Suit le métier. |
--slate | #535b7c | #aab4c0 | | Bleu ardoise des titres du site : bouton secondaire plein, série 2, badge neutre teinté. |
--slate-soft | #eaecf2 | #1c2531 | | Fond teinté derrière du texte `slate`. |
--on-slate | #ffffff | #0f141b | | Texte sur `slate`. |
--success | #1e7a4c | #6fd39d | | Terminé, payé, en ligne, conforme. Toujours accompagné d'un mot ou d'une icône. |
--success-soft | #e0f3e8 | #2f9e6429 | | Fond d'un badge ou d'une alerte `success`. |
--on-success | #ffffff | #0f141b | | Texte sur un aplat `success`. |
--warning | #9a5200 | #f0b068 | | À vérifier, expire bientôt, stock bas, DLC proche. |
--warning-soft | #fdf0dc | #d9822b2e | | Fond d'un badge ou d'une alerte `warning`. |
--on-warning | #ffffff | #0f141b | | Texte sur un aplat `warning`. |
--danger | #b3261e | #f28b8b | | Erreur, impayé, hors ligne, action destructive. Un bouton destructif reste en contour, jamais en aplat rouge. |
--danger-soft | #fbe9e7 | #d645452e | | Fond d'un badge ou d'une alerte `danger`. |
--on-danger | #ffffff | #0f141b | | Texte sur un aplat `danger`. |
--link | #25667a | #8ac3d5 | | Liens dans le texte, souligné. |
--focus-ring | #1a8bad | #5cc6f2 | | Anneau de focus clavier : 2 px d'écart puis 2 px pleins. |
--chart-1 | var(--accent) | var(--accent) | var(--accent) | Série 1 des graphiques ECharts : l'accent du métier. |
--chart-2 | var(--slate) | | | Série 2. |
--chart-3 | var(--warning) | | | Série 3. |
--chart-4 | var(--success) | | | Série 4. |
--chart-5 | var(--brand) | var(--brand) | var(--brand) | Série 5 : le `brand` du métier. |
--chart-grid | var(--line) | | | Grille et axes des graphiques. |
--shadow-sm | 0 1px 1px var(--shade), 0 1px 3px var(--shade) | | | Niveau 1 : bouton, touche, puce. Juste assez pour décoller du fond. |
--shadow-card | 0 1px 2px var(--shade), 0 6px 16px -6px var(--shade-strong) | inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 1px 2px var(--shade), 0 6px 16px -6px var(--shade-strong) | | Niveau 2 : carte, liste, tableau posés sur la page grisée. |
--shadow-md | 0 2px 4px var(--shade), 0 12px 28px -8px var(--shade-strong) | inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 4px var(--shade), 0 12px 28px -8px var(--shade-strong) | | Niveau 3 : menu, infobulle riche, barre collée, bouton flottant. |
--shadow-lg | 0 4px 8px var(--shade), 0 24px 48px -12px var(--shade-strong) | inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 4px 8px var(--shade), 0 24px 48px -12px var(--shade-strong) | | Niveau 4 : dialogue, panneau latéral. Le seul niveau qui masque ce qu'il recouvre. |
| Token | Normal | Compact | Touch | Usage |
|---|---|---|---|---|
--space-1 | 4px | | | Écart icône/texte, marge interne d'un badge. |
--space-2 | 8px | | | Écart entre contrôles d'une même ligne, marge interne des puces. |
--space-3 | 12px | | | Marge interne des champs et cellules (desktop). |
--space-4 | 16px | | | Marge interne des cartes, gouttière de page mobile, marge interne des champs (tablette). |
--space-5 | 24px | | | Gouttière de page desktop, entre deux cartes. |
--space-6 | 32px | | | Entre deux sections d'une page. |
--space-7 | 48px | | | Entre deux sections du site public, état vide. |
--space-8 | 64px | | | Marge de section du site public. |
--radius-sm | 4px | | | Badges, cases à cocher, cellules surlignées. |
--radius-md | 8px | | | Boutons, champs, onglets, alertes, menus. |
--radius-lg | 10px | | | Cartes, panneaux, tuiles, listes. |
--radius-full | 999px | | | Puces, avatars, le disque du logo. |
--control | 36px | 34px | 48px | Hauteur des boutons, champs et lignes denses. Suit la densité : 34 px en compact, 48 px en touch. |
--control-sm | 30px | 30px | 44px | Petit bouton, bouton d'alerte. 44 px en touch : la cible tactile minimale. |
--control-touch | 48px | | | Boutons, champs, lignes de liste et cases à cocher sur tablette (classes `-touch`, valable quelle que soit la densité). |
--control-touch-lg | 56px | | | Bouton d'action principal en pied d'écran tablette. |
--row | 44px | 40px | 56px | Ligne de tableau ou de liste : une sous-ligne y tient encore. |
--icon | 20px | 18px | 24px | Icône dans un bouton ou une ligne. |
--touch-target | 44px | | | Taille minimale d'une cible tactile, sur toutes les plateformes. |
--sidenav | 248px | | | Largeur de la barre latérale dépliée. |
--sidenav-rail | 64px | | | Largeur du rail replié (tablette, CRM réduit). |
--content-max | 1280px | | | Largeur maximale du contenu d'une page CRM. |
--topbar-height | 56px | | | Hauteur de la barre du haut (`v-app-bar`). |
--font-display | "Paytone One", Roboto, system-ui, sans-serif | | | Paytone One (Google Fonts, une graisse) : l'accroche, le titre d'écran d'accueil, le mot-symbole « Hublot ». |
--font-sans | Roboto, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif | | | Roboto 400, 500, 600 : tout le reste. |
--font-mono | "Roboto Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace | | | Codes PLU, EAN, identifiants, SQL. |
--fs-body | 15px | 13.5px | 17px | Texte courant. |
--fs-small | 13px | 12.5px | 15px | Métadonnées, aides, légendes. |
--fs-label | 13px | 12.5px | 15px | Libellés de champ, en-têtes de colonne. Même taille que `small` : le libellé se distingue de l’aide par la graisse et la couleur, jamais par la taille. |
--fs-h1 | 24px | 24px | 28px | Titre de page. |
--fs-h2 | 20px | 18px | 22px | Titre de section. |
--fs-h3 | 16px | 15px | 18px | Sous-titre. |
--fs-number-xl | 32px | 28px | 36px | Chiffre clé d'une tuile. |
--fs-display | 32px | 32px | 36px | Accroche en Paytone One. Jamais en dessous de 24 px. |
--fs-clock | 24px | | | Horloge de l'en-tête tablette. |
--lh-body | 22px | 20px | 24px | |
--lh-small | 20px | 18px | 22px | |
--lh-label | 18px | 16px | 20px | |
--lh-h1 | 32px | 32px | 36px | |
--lh-h2 | 28px | 26px | 30px | |
--lh-h3 | 24px | 22px | 26px | |
--lh-number-xl | 36px | 34px | 40px | |
--lh-display | 40px | 40px | 44px | |
--lh-clock | 28px | | | |
--fw-regular | 400 | | | |
--fw-medium | 500 | | | |
--fw-semibold | 600 | | | Styles de texte composés : font: var(--text-h1) ; letter-spacing: var(--ls-h1) |
--text-display | 400 var(--fs-display)/var(--lh-display) var(--font-display) | | | Accroche, titre d'écran d'accueil tablette, mot-symbole « Hublot ». Un seul par page. |
--ls-display | 0 | | | |
--text-h1 | 600 var(--fs-h1)/var(--lh-h1) var(--font-sans) | | | Titre de page, en couleur `brand`. Paytone One reste au mot-symbole. |
--ls-h1 | -0.3px | | | |
--text-h2 | 600 var(--fs-h2)/var(--lh-h2) var(--font-sans) | | | Titre de section, de carte ou de dialogue. |
--ls-h2 | -0.2px | | | |
--text-h3 | 600 var(--fs-h3)/var(--lh-h3) var(--font-sans) | | | Sous-titre, titre d'une ligne de liste ou de tâche. |
--ls-h3 | 0 | | | |
--text-body | 400 var(--fs-body)/var(--lh-body) var(--font-sans) | | | Texte courant : formulaires, cellules, paragraphes. |
--ls-body | 0 | | | |
--text-body-strong | 500 var(--fs-body)/var(--lh-body) var(--font-sans) | | | Titre d'une ligne de liste, nom dans une cellule. |
--ls-body-strong | 0 | | | |
--text-body-tablet | 400 17px/24px var(--font-sans) | | | Texte courant sur tablette en rayon, quelle que soit la densité (équivaut à `body` en densité touch). |
--ls-body-tablet | 0 | | | |
--text-small | 400 var(--fs-small)/var(--lh-small) var(--font-sans) | | | Métadonnées, aide sous un champ, légende. En `ink-muted`. |
--ls-small | 0 | | | |
--text-label | 500 var(--fs-label)/var(--lh-label) var(--font-sans) | | | Libellé de champ, en-tête de colonne, titre de section d'une fiche. Casse de phrase (plus de capitales depuis la v2). |
--ls-label | 0 | | | |
--text-number-xl | 600 var(--fs-number-xl)/var(--lh-number-xl) var(--font-sans) | | | Chiffre clé d'une tuile, chiffres tabulaires. |
--ls-number-xl | -0.02em | | | |
--text-number | 500 var(--fs-body)/var(--lh-body) var(--font-sans) | | | Montant ou poids dans une cellule, aligné à droite, chiffres tabulaires. |
--ls-number | 0 | | | |
--text-clock | 600 var(--fs-clock)/var(--lh-clock) var(--font-sans) | | | Horloge de l'en-tête tablette. |
--ls-clock | 0 | | | |
--text-mono | 400 var(--fs-small)/var(--lh-small) var(--font-mono) | | | Codes PLU, EAN, identifiants, SQL. Sur `surface-sunken`. |
--ls-mono | 0 | | | Mouvement, plans, points de rupture (informatifs : un @media ne lit pas une variable) |
--duration-press | 120ms | | | Enfoncement d'un bouton, d'une touche, d'une case. |
--duration-state | 180ms | | | Changement d'état : couleur, fond, filet, survol, focus. |
--duration-tooltip | 150ms | | | Infobulle. |
--duration-menu | 220ms | | | Menu, toast, accordéon, apparition d'une ligne. |
--duration-drawer | 360ms | | | Panneau latéral, dialogue. |
--ease-out | cubic-bezier(.23, 1, .32, 1) | | | Tout ce qui entre (ease-out fort). |
--ease-move | cubic-bezier(.77, 0, .175, 1) | | | Ce qui se déplace à l'écran. |
--ease-drawer | cubic-bezier(.32, .72, 0, 1) | | | Panneaux latéraux. |
--ease-spring | linear(0, 0.009, 0.035 2.1%, 0.141 4.4%, 0.723 12.9%, 0.938 16.7%, 1.017 20.6%, 1.061 24.5%, 1.077 28.3%, 1.071 32.3%, 1.035 41.4%, 1.008 50.5%, 0.997 60.4%, 1) | | | Ressort léger (7 % de dépassement) : coche, interrupteur, toast, menu qui s'ouvre. Réponse à une action seulement. Repli : ease sur les navigateurs sans linear(). |
--z-sticky | 10 | | | En-tête collant, barre d'outils. |
--z-nav | 20 | | | Barre latérale, barre du haut. |
--z-dropdown | 100 | | | Menus, popovers, sélecteurs de date. |
--z-drawer | 200 | | | Panneau latéral. |
--z-modal | 300 | | | Dialogues et leur voile. |
--z-toast | 400 | | | Toasts, snackbars. |
--z-tooltip | 500 | | | Infobulles, au-dessus de tout. |
--bp-sm | 600px | | | Téléphone au-dessous ; tablette portrait au-dessus (Vuetify `sm`). |
--bp-md | 960px | | | Tablette paysage, CRM avec rail (Vuetify `md`). |
--bp-lg | 1280px | | | Desktop, barre latérale dépliée (Vuetify 3 `lg` ; Vuetify 2 utilise 1264 px). |
--bp-xl | 1920px | | | Grand écran (Vuetify 3 `xl` ; Vuetify 2 utilise 1904 px). |
--hb-ease-out | var(--ease-out) | | | |
--hb-ease-move | var(--ease-move) | | | |
--hb-ease-drawer | var(--ease-drawer) | | |
/* Hublot — design system v3.3, composants `hb-*`. Chargé après tokens.css. Toutes les couleurs et tailles viennent des tokens ; aucun hexadécimal ici. Depuis la v3, le rebranchement métier `[data-activity="boucherie"]`, les courbes `--ease-*` (alias `--hb-ease-*`) et la densité `[data-density="touch"]` sont dans tokens.css : les classes `-touch` restent valables pour forcer la taille tactile d'un élément hors densité touch. */
/* Base */
body { margin: 0; font: var(--text-body); background: var(--surface); color: var(--ink); -webkit-font-smoothing: antialiased; }
.hb-display { font: var(--text-display); letter-spacing: 0; }
.hb-h1 { font: var(--text-h1); letter-spacing: -.3px; color: var(--brand); margin: 0; }
.hb-h2 { font: var(--text-h2); letter-spacing: -.2px; color: var(--ink); margin: 0; }
.hb-h3 { font: var(--text-h3); letter-spacing: 0; color: var(--ink); margin: 0; }
.hb-small { font: var(--text-small); color: var(--ink-muted); }
.hb-muted { color: var(--ink-muted); }
.hb-num { font-variant-numeric: tabular-nums; }
.hb-row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.hb-stack { display: flex; flex-direction: column; gap: var(--space-3); }
a.hb-link { color: var(--link); text-decoration: underline; }/* Page : gouttières, en-tête titre + actions */
.hb-page { padding: var(--space-5) var(--space-6) var(--space-6); display: flex; flex-direction: column; gap: var(--space-5); }
.hb-page--narrow { max-width: var(--content-max); margin: 0 auto; }
.hb-page-title { font: var(--text-h1); letter-spacing: -.3px; margin: 0; color: var(--brand); }
.hb-page-sub { font: var(--text-small); color: var(--ink-muted); margin: 2px 0 0; }
.hb-head { display: flex; align-items: flex-end; gap: var(--space-4); flex-wrap: wrap; }
.hb-head__actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }/* Bouton. Un seul `hb-btn-primary` par écran ; le neutre porte un filet visible ; le destructif reste en contour. */
.hb-btn { font: var(--text-body); font-weight: 500; height: var(--control); --hb-btn-pad: var(--space-4); padding: 0 var(--hb-btn-pad); border-radius: var(--radius-md); border: 1px solid var(--line-strong); background: var(--surface-raised); color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; gap: var(--space-2); white-space: nowrap; box-shadow: var(--shadow-sm); transition: transform 140ms var(--hb-ease-out), background-color 120ms ease, box-shadow 120ms ease; }
.hb-btn:active { transform: scale(.97); box-shadow: none; }
.hb-btn .hb-ico, .hb-btn .v-icon { color: var(--brand); font-size: var(--icon); width: var(--icon); height: var(--icon); }
.hb-btn-primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.hb-btn-primary .hb-ico, .hb-btn-primary .v-icon { color: var(--on-brand); }
.hb-btn-secondary { background: var(--slate); border-color: var(--slate); color: var(--on-slate); }
.hb-btn-secondary .hb-ico, .hb-btn-secondary .v-icon { color: var(--on-slate); }
.hb-btn-danger { color: var(--danger); border-color: var(--danger); }
.hb-btn-danger .hb-ico, .hb-btn-danger .v-icon { color: var(--danger); }
.hb-btn-quiet { border-color: transparent; background: transparent; box-shadow: none; color: var(--brand); }
.hb-btn-sm { height: var(--control-sm); --hb-btn-pad: var(--space-3); padding: 0 var(--hb-btn-pad); font-size: var(--fs-small); }
.hb-btn-touch { height: var(--control-touch); --hb-btn-pad: var(--space-5); padding: 0 var(--hb-btn-pad); font-size: 17px; }
[data-density="touch"] .hb-btn { --hb-btn-pad: var(--space-5); padding: 0 var(--hb-btn-pad); }
[data-density="touch"] .hb-btn-icon { padding: 0; }
.hb-btn-touch-lg { height: var(--control-touch-lg); --hb-btn-pad: var(--space-6); padding: 0 var(--hb-btn-pad); font-size: 17px; width: 100%; justify-content: center; }
.hb-btn[disabled] { opacity: .5; cursor: not-allowed; box-shadow: none; }/* Bouton à icône seule : carré, neutre au repos, prend la couleur métier au survol ; la variante danger ne rougit qu'au survol. Toujours une infobulle. */
.hb-btn-icon { position: relative; width: var(--control); height: var(--control); padding: 0; justify-content: center; border-color: transparent; background: transparent; box-shadow: none; color: var(--ink-muted); }
.hb-btn-icon .hb-ico, .hb-btn-icon .v-icon { color: inherit; }
.hb-btn:focus-visible, .hb-field:focus-visible, .hb-editable:focus-visible, .hb-tab:focus-visible, .hb-nav a:focus-visible, .hb-check:focus-visible, .hb-chip:focus-visible, .hb-radio:focus-visible, .hb-switch:focus-visible, .hb-segmented > button:focus-visible, .hb-stepper > button:focus-visible, .hb-key:focus-visible, .hb-listing-item:focus-visible, .hb-pagination-pages > button:focus-visible, .hb-details > summary:focus-visible, .hb-tip:focus-visible, .hb-fab:focus-visible, .hb-bottom-nav > a:focus-visible, .hb-bottom-nav > button:focus-visible, .hb-tree summary:focus-visible, .hb-tree-leaf:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
.hb-btn:hover { background: var(--surface-hover); border-color: var(--ink-muted); }
.hb-btn-primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); }
.hb-btn-secondary:hover { filter: brightness(.94); border-color: var(--slate); }
.hb-btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }
.hb-btn-quiet:hover { background: var(--brand-soft); border-color: transparent; }
.hb-btn-icon:hover { background: var(--surface-sunken); border-color: transparent; color: var(--brand); }
.hb-btn-icon.hb-btn-icon-danger:hover { background: var(--danger-soft); color: var(--danger); }
}/* Champ : filet visible au repos, plus foncé au survol, couleur métier au focus. */
.hb-label { display: block; font: var(--text-label); color: var(--ink); margin-bottom: var(--space-1); }
.hb-field { font: var(--text-body); height: var(--control); width: 100%; box-sizing: border-box; padding: 0 var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--line-strong); background: var(--surface-raised); color: var(--ink); transition: border-color 120ms ease, box-shadow 120ms ease; }
.hb-field::placeholder { color: var(--ink-faint); }
.hb-field:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.hb-field:disabled { background: var(--surface-sunken); color: var(--ink-muted); cursor: default; }
textarea.hb-field { height: auto; padding: var(--space-2) var(--space-3); resize: vertical; }
.hb-field-touch { height: var(--control-touch); font-size: 17px; padding: 0 var(--space-4); }
.hb-field-num { text-align: right; font-variant-numeric: tabular-nums; }
.hb-field-mono { font: var(--text-mono); background: var(--surface-sunken); }
.hb-field[aria-invalid="true"] { border-color: var(--danger); }
.hb-hint { font: var(--text-small); color: var(--ink-muted); margin-top: var(--space-1); }
.hb-hint-error { color: var(--danger); }
.hb-group { display: flex; flex-direction: column; min-width: 200px; }/* Champ composé (unité, stepper) : le cadre porte le filet et le focus, le champ intérieur n'a ni filet ni fond. L'unité suit la valeur, même taille, en `ink-muted`. */
.hb-suffix, .hb-stepper { display: flex; align-items: center; height: var(--control); box-sizing: border-box; border: 1px solid var(--line-strong); border-radius: var(--radius-md); background: var(--surface-raised); overflow: hidden; transition: border-color 120ms ease, box-shadow 120ms ease; }
.hb-suffix > .hb-field, .hb-stepper > .hb-field { height: 100%; border: 0; border-radius: 0; background: transparent; }
.hb-suffix > .hb-field:focus, .hb-stepper > .hb-field:focus { box-shadow: none; }
.hb-suffix:focus-within, .hb-stepper:focus-within { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.hb-suffix:has(> .hb-field[aria-invalid="true"]), .hb-stepper:has(> .hb-field[aria-invalid="true"]) { border-color: var(--danger); }
.hb-suffix:has(> .hb-field:disabled), .hb-stepper:has(> .hb-field:disabled) { background: var(--surface-sunken); }
.hb-suffix > .hb-field { flex: 1; min-width: 0; padding-right: var(--space-1); }
.hb-suffix > span { flex: none; padding-right: var(--space-3); color: var(--ink-muted); font-variant-numeric: tabular-nums; pointer-events: none; }
@media (hover: hover) and (pointer: fine) {
.hb-field:hover:not(:disabled):not(:focus), :is(.hb-suffix, .hb-stepper):hover:not(:focus-within):not(:has(> .hb-field:disabled)) { border-color: var(--ink-muted); }
.hb-group > .hb-time:hover:not([aria-expanded="true"]):not(:disabled):not(:focus-visible) { border-color: var(--ink-muted); background: var(--surface-raised); box-shadow: none; }
}/* Texte éditable en place (titre, description) : se lit comme du texte, se reconnaît comme un champ.
Au repos : filet discret et crayon `hb-editable-hint` posé dans le champ, à droite ; au survol : filet plein et fond ; au focus : couleur métier, le crayon disparaît.
Le groupe prend la largeur disponible : l'appelant la borne (`max-width`) pour que le champ ne s'étire pas sur toute la page. */
.hb-editable-group { position: relative; display: flex; align-items: center; min-width: 0; max-width: 100%; }
.hb-editable-group--multiline { align-items: flex-start; }
.hb-editable { font: inherit; color: inherit; width: calc(100% + 2 * var(--space-2)); box-sizing: border-box; margin: 0 calc(-1 * var(--space-2)); padding: var(--space-1) calc(var(--icon) + 2 * var(--space-2)) var(--space-1) var(--space-2); background: transparent; border: 1px dashed var(--line); border-radius: var(--radius-md); transition: border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease; }
.hb-editable::placeholder { color: var(--ink-faint); font-style: italic; }
.hb-editable:focus { outline: none; background: var(--surface-raised); border: 1px solid var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.hb-editable:focus ~ .hb-editable-hint { opacity: 0; }
.hb-editable:disabled { border-color: transparent; cursor: default; }
.hb-editable:disabled ~ .hb-editable-hint { display: none; }
textarea.hb-editable { resize: none; }
.hb-editable-hint { position: absolute; right: var(--space-2); top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: var(--icon); width: var(--icon); height: var(--icon); line-height: 1; pointer-events: none; transition: opacity 120ms ease, color 120ms ease; }
.hb-editable-group--multiline .hb-editable-hint { top: calc(var(--space-1) + 1px + var(--lh-body) / 2); }
@media (hover: hover) and (pointer: fine) {
.hb-editable:hover:not(:disabled):not(:focus) { border: 1px solid var(--line-strong); background: var(--surface-raised); }
.hb-editable-group:hover .hb-editable-hint { color: var(--brand); }
}/* Case à cocher */
.hb-check { appearance: none; width: 22px; height: 22px; border-radius: var(--radius-sm); border: 2px solid var(--line-strong); background: var(--surface-raised); margin: 0; flex: none; cursor: pointer; display: grid; place-items: center; transition: transform 120ms var(--hb-ease-out), background-color 120ms ease, border-color 120ms ease; }
.hb-check:active { transform: scale(.9); }
.hb-check:checked { background: var(--brand); border-color: var(--brand); }
.hb-check:checked::after { content: ""; width: 5px; height: 10px; border: solid var(--on-brand); border-width: 0 2.5px 2.5px 0; transform: translateY(-1px) rotate(45deg); }
.hb-check:disabled { opacity: .5; cursor: default; }
.hb-check-touch, [data-density="touch"] .hb-check { width: 28px; height: 28px; }
.hb-check-touch:checked::after, [data-density="touch"] .hb-check:checked::after { width: 7px; height: 13px; }
@media (hover: hover) and (pointer: fine) { .hb-check:hover:not(:checked):not(:disabled) { border-color: var(--brand); } }
.hb-check-label { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; color: var(--ink); }/* Badge d'état (point + mot) et puce (avatar + nom) */
.hb-badge { display: inline-flex; align-items: center; gap: var(--space-1); font: var(--text-label); line-height: 20px; padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--surface-sunken); color: var(--ink); white-space: nowrap; }
.hb-badge::before { content: ""; width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; flex: none; }
.hb-badge-success { background: var(--success-soft); color: var(--success); }
.hb-badge-warning { background: var(--warning-soft); color: var(--warning); }
.hb-badge-danger { background: var(--danger-soft); color: var(--danger); }
.hb-badge-info { background: var(--brand-soft); color: var(--brand); }
.hb-badge-slate { background: var(--slate-soft); color: var(--slate); }
.hb-badge-plain::before { display: none; }
.hb-chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 var(--space-2) 0 3px; border-radius: var(--radius-full); border: 1px solid var(--line); background: var(--surface-raised); color: var(--ink); font: var(--text-small); font-weight: 500; white-space: nowrap; max-width: 100%; cursor: default; }
.hb-chip > span { overflow: hidden; text-overflow: ellipsis; }
.hb-chip-action { cursor: pointer; }
.hb-chip-more { padding: 0 var(--space-2); color: var(--ink-muted); background: var(--surface-sunken); border-color: transparent; }
.hb-avatar { width: 20px; height: 20px; border-radius: var(--radius-full); background: var(--slate); color: var(--on-slate); font: 600 10px/20px var(--font-sans); text-align: center; flex: none; letter-spacing: .02em; }
.hb-chip .hb-avatar { width: 18px; height: 18px; line-height: 18px; font-size: 9px; }
.hb-avatar-group { display: inline-flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
@media (hover: hover) and (pointer: fine) { .hb-chip-action:hover { border-color: var(--brand); background: var(--brand-soft); color: var(--brand); } }/* Carte : blanche, filet et ombre, posée sur la page grisée. `hb-card-h` en-tête avec filet sous lui. */
.hb-card { background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); }
.hb-card-h { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--line); }
.hb-card-h h2, .hb-card-h__title { margin: 0; font: var(--text-h3); letter-spacing: 0; color: var(--ink); }
.hb-card-h__sub { font: var(--text-small); color: var(--ink-muted); }
.hb-card-h__icon { color: var(--brand); display: inline-flex; }
.hb-card-h__actions { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
.hb-card-body { padding: var(--space-4) var(--space-5); }/* Barre d'actions (pied de carte, dialogue, formulaire) : Annuler / Fermer en premier dans le DOM donc à gauche, l'action principale en dernier donc à droite, toujours un espace entre les deux. */
.hb-card-foot, .hb-actions { padding: var(--space-3) var(--space-5); border-top: 1px solid var(--line); display: flex; align-items: center; gap: var(--space-2); background: var(--surface-hover); border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
.hb-card-foot > :first-child, .hb-actions > :first-child, .hb-dialog-actions > :first-child { margin-right: auto; }
.hb-card-foot > :only-child, .hb-actions > :only-child, .hb-dialog-actions > :only-child { margin-right: 0; margin-left: auto; }/* Bouton discret en tête de pied : son texte s'aligne sur le contenu de la carte, le fond de survol déborde dans la marge. */
.hb-card-foot > .hb-btn-quiet:first-child:not(:only-child), .hb-actions > .hb-btn-quiet:first-child:not(:only-child), .hb-dialog-actions > .hb-btn-quiet:first-child:not(:only-child) { margin-left: calc(-1 * var(--hb-btn-pad)); }/* Bandeau teinté en tête de carte (titre d'une check-list, fiche) : la couleur métier en fond doux, le titre en `brand`. */
.hb-card-banner { padding: var(--space-4) var(--space-5); background: var(--brand-soft); border-bottom: 1px solid var(--line); border-radius: var(--radius-lg) var(--radius-lg) 0 0; }/* Zone en retrait dans une carte (groupe de champs, encadré) */
.hb-sunken { background: var(--surface-sunken); border-radius: var(--radius-md); }/* Tuile de chiffre clé */
.hb-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); }
.hb-tile { background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: var(--space-4) var(--space-5); min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); border-top: 3px solid var(--accent); }
.hb-tile-label { font: var(--text-label); font-weight: 400; color: var(--ink-muted); }
.hb-tile-value { font: var(--text-number-xl); letter-spacing: -.02em; font-variant-numeric: tabular-nums; color: var(--ink); }
.hb-tile-delta { font: var(--text-small); color: var(--ink-muted); display: flex; align-items: center; gap: var(--space-1); }
.hb-tile-delta b { font-weight: 500; }
.hb-tile-delta.up b { color: var(--success); }
.hb-tile-delta.down b { color: var(--danger); }
.hb-tile--text .hb-tile-value { font: var(--text-h3); letter-spacing: 0; }
.hb-spark { display: block; width: 100%; height: 28px; margin-top: var(--space-2); }
.hb-spark .fill { fill: var(--accent-soft); }
.hb-spark .stroke { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; }
.hb-spark .dot { fill: var(--brand); }/* Tableau : en-tête en retrait, lignes séparées par un filet, survol teinté */
.hb-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface-raised); box-shadow: var(--shadow-card); }
.hb-table { width: 100%; border-collapse: collapse; font: var(--text-body); }
.hb-table th { text-align: left; font: var(--text-label); color: var(--ink-muted); background: var(--surface-sunken); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); white-space: nowrap; }
.hb-table td { padding: 0 var(--space-3); height: var(--row); border-bottom: 1px solid var(--line); vertical-align: middle; }
.hb-table tr:last-child td { border-bottom: 0; }
.hb-table .num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; }
.hb-table .mono { font: var(--text-mono); color: var(--ink-muted); }
.hb-table tr[aria-selected="true"] td { background: var(--brand-soft); }
@media (hover: hover) and (pointer: fine) { .hb-table tbody tr:hover td { background: var(--surface-hover); } }
.hb-table-touch td { height: var(--control-touch); font-size: 17px; }
.hb-table-foot { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-3); padding: var(--space-2) var(--space-4); border-top: 1px solid var(--line); font: var(--text-small); color: var(--ink-muted); }/* Tableau, suite : en-tête triable (`aria-sort` sur le th), en-tête collant, colonne de sélection, ligne de totaux, variante dense. */
.hb-table th[aria-sort] { cursor: pointer; user-select: none; }
.hb-table th[aria-sort]::after { content: "\2195"; margin-left: 4px; color: var(--ink-faint); }
.hb-table th[aria-sort="ascending"]::after { content: "\2191"; color: var(--brand); }
.hb-table th[aria-sort="descending"]::after { content: "\2193"; color: var(--brand); }
.hb-table th[aria-sort="ascending"], .hb-table th[aria-sort="descending"] { color: var(--brand); }
.hb-table--sticky th { position: sticky; top: 0; z-index: 1; }
.hb-table .sel { width: 22px; padding-right: 0; }
.hb-table tfoot td { font-weight: 500; background: var(--surface-sunken); border-top: 1px solid var(--line-strong); border-bottom: 0; }
.hb-table--dense td { height: var(--control-sm); }
.hb-cell-title { font-weight: 500; color: var(--ink); }
.hb-cell-title--empty { font-weight: 400; color: var(--ink-muted); }
.hb-cell-sub { font: var(--text-small); color: var(--ink-muted); margin-top: 1px; white-space: nowrap; }
.hb-cell-sub > span + span::before { content: '·'; margin: 0 6px; }/* Fiche : liste clé / valeur, bloc de code */
.hb-kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px var(--space-4); margin: 0; }
.hb-kv dt { font: var(--text-small); color: var(--ink-muted); }
.hb-kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.hb-code { font: var(--text-mono); background: var(--surface-sunken); border: 1px solid var(--line); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; overflow: auto; max-height: 60vh; color: var(--ink); }
.mono { font: var(--text-mono); background: var(--surface-sunken); color: var(--ink); border-radius: var(--radius-sm); padding: 0 4px; }
.mono--plain, .hb-cell-sub .mono { background: transparent; padding: 0; color: inherit; }/* Panneau latéral */
.hb-drawer-h { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--line); background: var(--surface-raised); position: sticky; top: 0; z-index: 1; }
.hb-drawer-h__text { flex: 1; min-width: 0; }
.hb-drawer-h h2, .hb-drawer-h__title { margin: 0; font: var(--text-h2); letter-spacing: -.2px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-drawer-h__sub { font: var(--text-small); color: var(--ink-muted); margin-top: 2px; }
.hb-drawer-h__meta { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
.hb-drawer-body { padding: var(--space-4) var(--space-5); display: flex; flex-direction: column; gap: var(--space-5); }
.hb-drawer-section > h3 { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); font: var(--text-label); color: var(--ink-muted); }
.hb-drawer-section > h3 .spacer { flex: 1; }/* Navigation latérale : fond sombre métier, entrée active en couleur d'accent avec barre */
.hb-nav { width: var(--sidenav); background: var(--nav); color: var(--nav-ink); display: flex; flex-direction: column; gap: 20px; padding: 20px var(--space-3); box-sizing: border-box; min-height: 100%; }
.hb-nav-brand { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-2); font: var(--text-display); font-size: 22px; line-height: 28px; }
.hb-nav-brand i { width: 24px; height: 24px; border-radius: var(--radius-full); background: var(--accent); box-shadow: inset 0 0 0 4px var(--nav); outline: 3px solid var(--accent); outline-offset: -1px; flex: none; }
.hb-nav-sections { display: flex; flex-direction: column; gap: var(--space-3); flex: 1; }
.hb-nav-section { display: flex; flex-direction: column; gap: 2px; }
.hb-nav-group { display: flex; align-items: center; gap: var(--space-1); width: 100%; margin: 0; border: 0; background: none; font: var(--text-label); text-transform: uppercase; letter-spacing: .04em; text-align: left; color: var(--nav-muted); padding: 0 10px 4px; cursor: pointer; }
.hb-nav-group span { flex: 1; }
.hb-nav-group .chev { color: inherit; opacity: .6; transition: transform 200ms var(--hb-ease-out); }
.hb-nav-group[aria-expanded="true"] .chev { transform: rotate(90deg); }
.hb-nav-items { display: flex; flex-direction: column; gap: 2px; }
.hb-nav a, .v-application .hb-nav a { display: flex; align-items: center; gap: 10px; min-height: 36px; margin: 0; padding: 0 10px; border-radius: var(--radius-md); color: var(--nav-muted); text-decoration: none; font: var(--text-body); position: relative; box-sizing: border-box; }
.hb-nav a .ico, .hb-nav a .v-icon, .hb-nav a .material-icons { width: 22px; height: 22px; font-size: 22px; color: currentColor; flex: none; }
.hb-nav a .ico { border-radius: var(--radius-sm); background: currentColor; opacity: .55; }
.hb-nav a[aria-current="page"], .v-application .hb-nav a[aria-current="page"] { color: var(--nav-active); background: var(--nav-active-bg); font-weight: 500; }
.hb-nav a[aria-current="page"]::before { content: ""; position: absolute; left: -12px; top: 8px; bottom: 8px; width: 3px; border-radius: 0 2px 2px 0; background: var(--nav-active); }
.hb-nav a[aria-current="page"] .ico { opacity: 1; }
@media (hover: hover) and (pointer: fine) { .hb-nav a:hover, .v-application .hb-nav a:hover { color: var(--nav-ink); background: var(--nav-hover); } .hb-nav-group:hover { color: var(--nav-ink); } }
@media (prefers-reduced-motion: reduce) { .hb-nav-group .chev { transition: none; } }
@media (max-width: 599.98px) { .hb-nav a { min-height: 44px; } }
.hb-nav-rail { width: var(--sidenav-rail); }
.hb-nav-rail a { justify-content: center; padding: 0; }
.hb-nav-rail .hb-nav-group, .hb-nav-rail a span, .hb-nav-rail .hb-nav-brand span { display: none; }
.hb-nav-rail .hb-nav-brand { justify-content: center; padding-left: 0; padding-right: 0; }/* Onglets : filet sous la rangée, l'actif en couleur métier avec barre de 3 px */
.hb-tabs { display: flex; gap: var(--space-1); border-bottom: 1px solid var(--line); padding: 0; overflow-x: auto; }
.hb-tab { font: var(--text-body); font-weight: 500; background: none; border: 0; border-bottom: 3px solid transparent; margin-bottom: -1px; padding: var(--space-3) var(--space-4); color: var(--ink-muted); cursor: pointer; display: inline-flex; align-items: center; gap: var(--space-2); border-radius: var(--radius-md) var(--radius-md) 0 0; white-space: nowrap; }
.hb-tab[aria-selected="true"] { color: var(--brand); border-bottom-color: var(--brand); background: var(--brand-soft); }
.hb-tab .count, .hb-count { font: var(--text-label); line-height: 18px; padding: 0 6px; border-radius: var(--radius-full); background: var(--surface-sunken); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.hb-tab[aria-selected="true"] .count { background: var(--surface-raised); color: var(--brand); }
@media (hover: hover) and (pointer: fine) { .hb-tab:hover { color: var(--ink); background: var(--surface-hover); } }/* Barre d'outils : boutons à icône seule alignés, sur fond de page, de carte ou de bandeau teinté ; le menu ⋮ regroupe le secondaire et le destructif. */
.hb-toolbar { display: flex; align-items: center; gap: var(--space-1); }
.hb-toolbar .hb-btn-icon { background: transparent; }
.hb-toolbar-sep { width: 1px; height: 20px; background: var(--line-strong); opacity: .5; margin: 0 var(--space-1); }
@media (hover: hover) and (pointer: fine) { .hb-card-banner .hb-btn-icon:hover, .hb-toolbar .hb-btn-icon:hover { background: var(--surface-raised); box-shadow: var(--shadow-sm); } }/* Puce à bascule (filtre) : neutre au repos, teinte métier quand elle est active. */
.hb-chip-toggle { height: var(--control-sm); padding: 0 var(--space-3) 0 var(--space-2); gap: var(--space-1); font: var(--text-small); font-weight: 500; cursor: pointer; border: 1px solid var(--line-strong); background: var(--surface-raised); color: var(--ink-muted); }
.hb-chip-toggle .material-icons, .hb-chip-toggle .v-icon { font-size: 18px; color: inherit; }
.hb-chip-toggle[aria-pressed="true"] { background: var(--brand-soft); border-color: var(--brand); color: var(--brand); }/* Onglet « + » : crée un nouvel onglet, en bout de rangée, icône seule avec infobulle. */
.hb-tab-add { flex: none; align-self: center; width: 32px; height: 32px; padding: 0; justify-content: center; margin: 0 0 2px var(--space-1); border: 1px dashed var(--line-strong); border-radius: var(--radius-md); background: transparent; color: var(--ink-muted); cursor: pointer; display: inline-flex; align-items: center; }
.hb-tab-add .material-icons, .hb-tab-add .v-icon { font-size: 20px; color: inherit; }
@media (hover: hover) and (pointer: fine) { .hb-tab-add:hover { border-style: solid; border-color: var(--brand); color: var(--brand); background: var(--brand-soft); } .hb-chip-toggle:hover { border-color: var(--brand); color: var(--brand); } }/* Bandeau : titre et actions sur une même ligne de base, la toolbar calée en haut à droite. */
.hb-card-banner .hb-head { align-items: flex-start; }
.hb-card-banner .hb-head__actions { margin-top: 2px; }/* Alerte */
.hb-alert { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--line); border-left-width: 4px; background: var(--surface-raised); color: var(--ink); }
.hb-alert .ico { width: 20px; height: 20px; border-radius: var(--radius-full); flex: none; margin-top: 1px; background: currentColor; }
.hb-alert b { font-weight: 500; }
.hb-alert p { margin: 0; }
.hb-alert-info { background: var(--brand-soft); border-color: var(--brand); }
.hb-alert-info .ico, .hb-alert-info b { color: var(--brand); }
.hb-alert-success { background: var(--success-soft); border-color: var(--success); }
.hb-alert-success .ico, .hb-alert-success b { color: var(--success); }
.hb-alert-warning { background: var(--warning-soft); border-color: var(--warning); }
.hb-alert-warning .ico, .hb-alert-warning b { color: var(--warning); }
.hb-alert-danger { background: var(--danger-soft); border-color: var(--danger); }
.hb-alert-danger .ico, .hb-alert-danger b { color: var(--danger); }
.hb-alert .hb-btn, .hb-alert .v-btn { margin-left: auto; height: var(--control-sm); padding: 0 var(--space-3); }/* Liste de lignes (tâches, commentaires) : carte à filet et ombre, une ligne par entrée sur une grille fixe.
Colonnes : poignée · case · contenu · colonne droite (date, personnes, actions) à largeurs fixes, pour que tout s'aligne d'une ligne à l'autre.
Au repos, une ligne ne montre que du texte ; le survol révèle la poignée, le lien d'ajout et les actions ; le clic sur un texte l'édite. */
.hb-list { border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface-raised); box-shadow: var(--shadow-card); }
.hb-list-row { display: grid; grid-template-columns: 20px 22px minmax(0, 1fr) auto; column-gap: var(--space-3); align-items: start; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line); background: var(--surface-raised); color: var(--ink); transition: background-color 120ms ease; }
.hb-list-row:last-child { border-bottom: 0; }
.hb-list-row--done .hb-list-title, .hb-list-row--done .hb-inline-title { color: var(--ink-muted); text-decoration: line-through; }
.hb-list-row--comment { background: var(--surface-sunken); box-shadow: inset 3px 0 0 var(--accent); }
.hb-list-row--comment .hb-inline-title { font-weight: 400; }
.hb-list-handle { display: flex; align-items: center; justify-content: center; width: 20px; height: 22px; margin-top: 1px; opacity: 0; transition: opacity 120ms ease; }
.hb-list-check { margin-top: 1px; }
.hb-list-icon { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin-top: 1px; color: var(--brand); }
.hb-list-icon .material-icons, .hb-list-icon .v-icon { font-size: 20px; color: inherit; }
.hb-list-body { min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); padding-top: 1px; }
.hb-list-title { font: var(--text-body-strong); color: var(--ink); }
.hb-list-desc { font: var(--text-small); color: var(--ink-muted); }
.hb-list-meta { font: var(--text-small); color: var(--ink-muted); display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }/* Texte éditable d'une ligne : nu au repos ; au survol de la ligne, seul le texte sous le pointeur prend la forme d'un champ (les deux à la fois feraient deux boîtes collées) ; couleur métier au focus. */
.hb-inline { font: inherit; color: inherit; width: 100%; box-sizing: border-box; display: block; margin: -2px calc(-1 * var(--space-2)); padding: 1px var(--space-2); background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm); outline: none; transition: background-color 120ms ease, border-color 120ms ease; }
.hb-inline::placeholder { color: var(--ink-faint); }
.hb-inline-title { font: var(--text-body-strong); color: var(--ink); }
.hb-inline-desc { font: var(--text-small); color: var(--ink-muted); resize: none; }
.hb-inline:focus { background: var(--surface-raised); border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); color: var(--ink); }
.hb-inline:disabled { cursor: default; }/* Lien discret d'ajout (description, détail) : visible au survol de la ligne seulement. */
.hb-list-add { align-self: flex-start; display: inline-flex; align-items: center; gap: var(--space-1); border: 0; padding: 0; margin: 0 0 0 calc(-1 * var(--space-2)); padding: 0 var(--space-2); background: none; font: var(--text-small); color: var(--ink-faint); cursor: pointer; border-radius: var(--radius-sm); opacity: 0; transition: opacity 120ms ease, color 120ms ease; }
.hb-list-add .material-icons, .hb-list-add .v-icon { font-size: 16px; color: inherit; }/* Colonne droite : date, personnes, actions, chacune à largeur fixe. */
.hb-list-side { display: grid; grid-template-columns: 112px 104px 64px; column-gap: var(--space-3); align-items: center; height: 24px; margin-top: 0; }
.hb-list-side--comment { grid-template-columns: 64px; }
.hb-list-date { display: inline-flex; align-items: center; gap: var(--space-1); height: 26px; padding: 0 var(--space-2); margin-left: calc(-1 * var(--space-2)); border: 0; border-radius: var(--radius-sm); background: none; font: var(--text-small); color: var(--ink-muted); white-space: nowrap; cursor: pointer; }
.hb-list-date .material-icons, .hb-list-date .v-icon { font-size: 18px; color: inherit; }
.hb-list-date:disabled { cursor: default; }
.hb-list-date--today { color: var(--brand); font-weight: 500; }
.hb-list-date--late { color: var(--danger); font-weight: 500; }
.hb-list-actions { display: flex; align-items: center; justify-content: flex-end; gap: 0; opacity: 0; transition: opacity 120ms ease; }
.hb-list-actions .hb-btn-icon { width: var(--control-sm); height: var(--control-sm); }
.hb-list-actions .hb-btn-icon .material-icons, .hb-list-actions .hb-btn-icon .v-icon { font-size: 18px; }/* Personnes : avatars empilés, noms en infobulle ; vide = un avatar pointillé « + ». Peut être un <button>. */
.hb-avatar-stack { border: 0; padding: 0; background: none; font: inherit; }
.hb-avatar-stack:disabled { cursor: default; }
.hb-avatar-stack { display: inline-flex; align-items: center; height: 24px; cursor: pointer; border-radius: var(--radius-full); }
.hb-avatar-stack .hb-avatar { width: 24px; height: 24px; line-height: 24px; font-size: 10px; box-shadow: 0 0 0 2px var(--surface-raised); }
.hb-avatar-stack .hb-avatar + .hb-avatar, .hb-avatar-stack .hb-avatar + .hb-avatar-more { margin-left: -6px; }
.hb-avatar-more { width: 24px; height: 24px; border-radius: var(--radius-full); background: var(--surface-sunken); color: var(--ink-muted); font: 500 11px/24px var(--font-sans); text-align: center; box-shadow: 0 0 0 2px var(--surface-raised); }
.hb-avatar-add { width: 24px; height: 24px; box-sizing: border-box; border-radius: var(--radius-full); border: 1px dashed var(--line-strong); color: var(--ink-faint); display: inline-flex; align-items: center; justify-content: center; }
.hb-avatar-add .material-icons, .hb-avatar-add .v-icon { font-size: 16px; color: inherit; }
.hb-list-row--comment .hb-list-body { padding-top: 1px; }
@media (hover: hover) and (pointer: fine) {
.hb-list-row:hover { background: var(--surface-hover); }
.hb-list-row--comment:hover { background: var(--surface-sunken); }
.hb-list-row:hover .hb-list-handle, .hb-list-row:hover .hb-list-actions, .hb-list-row:hover .hb-list-add, .hb-list-row:focus-within .hb-list-actions { opacity: 1; }
.hb-list-row:hover .hb-inline:not(:focus):not(:disabled) { cursor: text; }
.hb-list-row:hover .hb-inline:hover:not(:focus):not(:disabled) { background: var(--surface-raised); border-color: var(--line-strong); }
.hb-list-add:hover, .hb-avatar-stack:hover .hb-avatar-add { color: var(--brand); border-color: var(--brand); }
.hb-list-date:hover { background: var(--surface-sunken); color: var(--ink); }
}
@media (hover: none) { .hb-list-handle, .hb-list-actions, .hb-list-add { opacity: 1; } }/* Glisser-déposer : réordonner une liste par sa poignée. Les classes suivent les options de Sortable.js (vuedraggable) : `handle: '.hb-handle'`, `ghostClass: 'hb-drag-ghost'` (l'emplacement d'arrivée dans la liste), `dragClass: 'hb-drag-lifted'` (la copie qui suit le pointeur, avec `forceFallback: true`). `hb-list-handle` et `hb-list-row--ghost` de la liste de tâches en sont les variantes. */
:is(.hb-handle, .hb-list-handle) { position: relative; padding: 0; border: 0; border-radius: var(--radius-sm); background: transparent; font: inherit; color: var(--ink-faint); cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
.hb-handle { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 20px; height: 22px; }
:is(.hb-handle, .hb-list-handle) :is(.material-icons, .v-icon) { font-size: 20px; color: inherit; }
:is(.hb-handle, .hb-list-handle):active { cursor: grabbing; }
:is(.hb-handle, .hb-list-handle):focus-visible { opacity: 1; outline: 2px solid var(--focus-ring); outline-offset: 2px; }
:is(.hb-drag-ghost, .hb-list-row--ghost) { opacity: 1; background: var(--brand-soft); outline: 1px dashed var(--brand); outline-offset: -1px; box-shadow: none; }
:is(.hb-drag-ghost, .hb-list-row--ghost) > * { visibility: hidden; }
.hb-drag-lifted { opacity: 1 !important; background: var(--surface-raised); border-radius: var(--radius-md); box-shadow: var(--shadow-md); scale: 1.02; cursor: grabbing; }
.hb-drag-lifted :is(.hb-handle, .hb-list-handle) { opacity: 1; color: var(--brand); cursor: grabbing; }
.hb-drop-over { background: var(--brand-soft); outline: 2px dashed var(--brand); outline-offset: -2px; }
@media (hover: hover) and (pointer: fine) { :is(.hb-handle, .hb-list-handle):hover { color: var(--ink-muted); background: var(--surface-sunken); } }
@media (pointer: coarse) { :is(.hb-handle, .hb-list-handle)::before { content: ""; position: absolute; inset: calc((22px - var(--touch-target)) / 2) calc((20px - var(--touch-target)) / 2); } }/* Ligne de tâche compacte (tablette) */
.hb-task { display: flex; align-items: center; gap: var(--space-4); height: var(--control-touch); padding: 0 var(--space-4); border-bottom: 1px solid var(--line); background: var(--surface-raised); font-size: 17px; }
[data-density="touch"] .hb-task { height: var(--row); }
.hb-task:last-child { border-bottom: 0; }
.hb-task .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-task input:checked ~ .title { color: var(--ink-muted); text-decoration: line-through; }
.hb-task .time { font-variant-numeric: tabular-nums; color: var(--ink-muted); font-size: 15px; }
.hb-task .hb-field { width: 112px; }
.hb-task a { color: var(--link); font-size: 15px; }/* État vide. Dans une liste, l'état vide est une ligne squelette qui garde la forme d'une tâche (`hb-list-row--empty`) ; le bloc centré pointillé (`hb-empty`) sert aux pages sans liste. */
.hb-list-row--empty { align-items: center; color: var(--ink-faint); border-bottom: 1px dashed var(--line-strong); background: transparent; }
.hb-list-row--empty .hb-list-handle { opacity: 1; }
.hb-list-row--empty .hb-check { pointer-events: none; border-style: dashed; background: transparent; }
.hb-list-row--empty .hb-list-title { color: var(--ink-faint); font-weight: 400; font-style: italic; }
.hb-list-row--empty .hb-skeleton { flex: 1; max-width: 240px; }
.hb-skeleton { display: block; height: 10px; border-radius: var(--radius-full); background: var(--surface-sunken); }
.hb-skeleton + .hb-skeleton { margin-top: var(--space-2); width: 60%; }
.hb-empty { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-2); padding: var(--space-7) var(--space-5); border: 1px dashed var(--line-strong); border-radius: var(--radius-lg); color: var(--ink-muted); max-width: 440px; margin: 0 auto; }
.hb-empty .ring { width: 48px; height: 48px; border-radius: var(--radius-full); border: 6px solid var(--accent-soft); box-shadow: inset 0 0 0 3px var(--accent); margin-bottom: var(--space-2); }
.hb-empty h3 { margin: 0; font: var(--text-h3); color: var(--ink); }
.hb-empty p { margin: 0; font: var(--text-body); }
.hb-empty .hb-btn { margin-top: var(--space-2); }
.hb-empty--flat { border: 0; padding: var(--space-6) var(--space-4); }/* Toast : confirmation après une action (« Cuisson enregistrée »). Posé en bas au centre, sur fond `ink`, une seule ligne, disparaît seul ; jamais porteur d'une erreur bloquante (une erreur reste dans une alerte). */
.hb-toast { position: fixed; left: 50%; bottom: var(--space-5); transform: translateX(-50%); z-index: var(--z-toast); display: inline-flex; align-items: center; gap: var(--space-3); max-width: min(560px, calc(100% - 2 * var(--space-4))); padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background: var(--ink); color: var(--surface-raised); font: var(--text-body); box-shadow: var(--shadow-md); }
.hb-toast .ico { width: 20px; height: 20px; border-radius: var(--radius-full); background: var(--success); flex: none; }
.hb-toast-danger .ico { background: var(--danger); }
.hb-toast .hb-btn { height: var(--control-sm); border-color: transparent; background: transparent; color: var(--accent); box-shadow: none; margin-left: var(--space-2); }
.hb-toast-enter { animation: hb-toast-in var(--duration-menu) var(--ease-out); }
@keyframes hb-toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }/* Accessibilité : texte lu par le lecteur d'écran seulement ; lien d'évitement visible au focus. */
.hb-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; }
.hb-skip-link { position: absolute; left: var(--space-4); top: -100px; z-index: var(--z-tooltip); padding: var(--space-2) var(--space-4); border-radius: var(--radius-md); background: var(--brand); color: var(--on-brand); font: var(--text-body-strong); }
.hb-skip-link:focus { top: var(--space-4); outline: 2px solid var(--focus-ring); outline-offset: 2px; }/* Dialogue (contenu d'un v-dialog ou d'un <dialog>) */
.hb-dialog { background: var(--surface-raised); color: var(--ink); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); display: flex; flex-direction: column; }
.hb-dialog-h { padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--line); font: var(--text-h2); letter-spacing: -.2px; }
.hb-dialog-body { padding: var(--space-4) var(--space-5); font: var(--text-body); }
.hb-dialog-actions { padding: var(--space-3) var(--space-5); border-top: 1px solid var(--line); background: var(--surface-hover); display: flex; align-items: center; gap: var(--space-2); border-radius: 0 0 var(--radius-lg) var(--radius-lg); }/* Menu flottant (contenu d'un v-menu ou d'un popover maison) */
.hb-menu { background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-md); padding: var(--space-1) 0; min-width: 200px; }
.hb-menu-item { display: flex; align-items: center; gap: var(--space-3); min-height: var(--control); padding: 0 var(--space-4); font: var(--text-body); color: var(--ink); cursor: pointer; white-space: nowrap; }
.hb-menu-item[aria-selected="true"] { background: var(--brand-soft); color: var(--brand); }
.hb-menu-sep { height: 1px; background: var(--line); margin: var(--space-1) 0; }
@media (hover: hover) and (pointer: fine) { .hb-menu-item:hover { background: var(--surface-hover); } }/* Filtres en tête de carte */
.hb-filters { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.hb-filters > .v-input { flex: 0 0 220px; max-width: 220px; }
.hb-filter-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--space-3); align-items: center; }
.hb-filter-grid > * { min-width: 0; }
.hb-filter-grid__actions { display: flex; align-items: center; gap: var(--space-1); }/* Bouton radio et interrupteur : même filet et même couleur métier que la case à cocher ; 28 px en densité touch. `hb-choices` empile les options (`--row` les aligne). */
.hb-radio { appearance: none; width: 22px; height: 22px; border-radius: var(--radius-full); border: 2px solid var(--line-strong); background: var(--surface-raised); margin: 0; flex: none; cursor: pointer; display: grid; place-items: center; box-sizing: border-box; transition: border-color 120ms ease; }
.hb-radio:checked { border-color: var(--brand); }
.hb-radio:checked::after { content: ""; width: 10px; height: 10px; border-radius: var(--radius-full); background: var(--brand); }
.hb-radio:disabled { opacity: .5; cursor: default; }
[data-density="touch"] .hb-radio { width: 28px; height: 28px; }
[data-density="touch"] .hb-radio:checked::after { width: 14px; height: 14px; }
.hb-switch { appearance: none; width: 40px; height: 22px; border-radius: var(--radius-full); border: 2px solid var(--line-strong); background: var(--surface-sunken); margin: 0; flex: none; cursor: pointer; position: relative; box-sizing: border-box; transition: background-color 160ms ease, border-color 160ms ease; }
.hb-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: var(--radius-full); background: var(--ink-muted); transition: transform 160ms var(--ease-out), background-color 160ms ease; }
.hb-switch:checked { background: var(--brand); border-color: var(--brand); }
.hb-switch:checked::after { transform: translateX(18px); background: var(--on-brand); }
.hb-switch:disabled { opacity: .5; cursor: default; }
[data-density="touch"] .hb-switch { width: 52px; height: 28px; }
[data-density="touch"] .hb-switch::after { width: 20px; height: 20px; }
[data-density="touch"] .hb-switch:checked::after { transform: translateX(24px); }
.hb-choices { display: flex; flex-direction: column; gap: var(--space-2); }
.hb-choices--row { flex-direction: row; flex-wrap: wrap; gap: var(--space-4); }
.hb-check-label > .hb-small { margin-left: auto; }
@media (hover: hover) and (pointer: fine) { .hb-radio:hover:not(:checked):not(:disabled), .hb-switch:hover:not(:checked):not(:disabled) { border-color: var(--brand); } }/* Liste déroulante native : la boîte d'un champ, un chevron en bout ; pour un choix court sans recherche (sinon v-select / v-autocomplete). */
.hb-select { position: relative; display: block; }
.hb-select > .hb-field { appearance: none; padding-right: calc(var(--space-3) + 24px); cursor: pointer; }
.hb-select::after { content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; border: solid var(--ink-muted); border-width: 0 2px 2px 0; transform: translateY(-70%) rotate(45deg); pointer-events: none; }
.hb-select:focus-within::after { border-color: var(--brand); }/* Icône en tête de champ (recherche, téléphone) et bouton d'effacement en bout. */
.hb-prefix { position: relative; }
.hb-prefix > .material-icons, .hb-prefix > .v-icon, .hb-prefix > .ico { position: absolute; left: var(--space-3); top: 50%; transform: translateY(-50%); font-size: var(--icon); width: var(--icon); height: var(--icon); color: var(--ink-muted); pointer-events: none; }
.hb-prefix > .hb-field { padding-left: calc(var(--space-3) + var(--icon) + var(--space-2)); }
.hb-prefix:focus-within > .material-icons, .hb-prefix:focus-within > .v-icon, .hb-prefix:focus-within > .ico { color: var(--brand); }
.hb-prefix > .hb-clear { position: absolute; right: 3px; top: 50%; transform: translateY(-50%); width: calc(var(--control) - 8px); height: calc(var(--control) - 8px); }
.hb-prefix.has-clear > .hb-field { padding-right: var(--control); }/* Sélecteur de quantité : « − champ + » ; le champ reste saisissable, les boutons ajustent par pas. Prend la hauteur de contrôle de la densité. */
.hb-stepper { display: inline-flex; align-items: stretch; }
.hb-group > .hb-stepper { display: flex; }
.hb-stepper > button { flex: none; width: calc(var(--control) - 2px); border: 0; background: var(--surface-sunken); color: var(--brand); font: var(--text-h3); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
.hb-stepper > button:disabled { color: var(--ink-faint); cursor: default; }
.hb-stepper > .hb-field { width: 72px; text-align: center; font-variant-numeric: tabular-nums; padding: 0 var(--space-1); }
.hb-group > .hb-stepper > .hb-field { flex: 1; min-width: 0; }
@media (hover: hover) and (pointer: fine) { .hb-stepper > button:hover:not(:disabled) { background: var(--brand-soft); } }/* Formulaire : grille de champs à deux colonnes (une seule sur téléphone), groupe avec légende, marque « requis » sur le libellé. */
.hb-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); }
.hb-form-full { grid-column: 1 / -1; }
.hb-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.hb-fieldset > legend, .hb-legend { padding: 0; margin: 0 0 var(--space-1); font: var(--text-label); color: var(--ink); }
.hb-label-required::after { content: " *"; color: var(--danger); }/* Contrôle segmenté : un choix exclusif parmi deux à cinq (Jour / Semaine / Mois), un seul bouton pressé (`aria-pressed`). */
.hb-segmented { display: inline-flex; padding: 2px; border-radius: var(--radius-md); background: var(--surface-sunken); border: 1px solid var(--line); box-sizing: border-box; }
.hb-segmented > button { border: 0; background: transparent; height: calc(var(--control) - 6px); padding: 0 var(--space-3); border-radius: calc(var(--radius-md) - 2px); font: var(--text-small); font-weight: 500; color: var(--ink-muted); cursor: pointer; display: inline-flex; align-items: center; gap: var(--space-1); white-space: nowrap; }
.hb-segmented > button .material-icons, .hb-segmented > button .v-icon { font-size: 18px; color: inherit; }
.hb-segmented > button[aria-pressed="true"] { background: var(--surface-raised); color: var(--brand); box-shadow: var(--shadow-sm); }
@media (hover: hover) and (pointer: fine) { .hb-segmented > button:hover:not([aria-pressed="true"]) { color: var(--ink); } }/* Progression : barre (`--value` en %, `hb-progress--busy` sans valeur), anneau tournant, surcouche de chargement, étapes numérotées. */
.hb-progress { height: 6px; border-radius: var(--radius-full); background: var(--surface-sunken); overflow: hidden; }
.hb-progress > i { display: block; height: 100%; width: var(--value, 0%); border-radius: inherit; background: var(--brand); transition: width 240ms var(--ease-out); }
.hb-progress-success > i { background: var(--success); }
.hb-progress-warning > i { background: var(--warning); }
.hb-progress-danger > i { background: var(--danger); }
.hb-progress--busy > i { width: 40%; animation: hb-busy 1.2s var(--ease-move) infinite; }
.hb-progress-label { display: flex; justify-content: space-between; gap: var(--space-3); font: var(--text-small); color: var(--ink-muted); margin-bottom: var(--space-1); font-variant-numeric: tabular-nums; }
@keyframes hb-busy { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
.hb-spinner { display: inline-block; width: var(--icon); height: var(--icon); box-sizing: border-box; border-radius: var(--radius-full); border: 2px solid var(--brand-soft); border-top-color: var(--brand); animation: hb-spin .8s linear infinite; flex: none; }
.hb-spinner-lg { width: 40px; height: 40px; border-width: 3px; }
@keyframes hb-spin { to { transform: rotate(360deg); } }
.hb-loading { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-3); color: var(--ink-muted); font: var(--text-small); border-radius: inherit; }
.hb-loading::before { content: ""; position: absolute; inset: 0; background: var(--surface-raised); opacity: .8; border-radius: inherit; }
.hb-loading > * { position: relative; }
.hb-steps { display: flex; gap: var(--space-2); margin: 0; padding: 0; list-style: none; counter-reset: step; }
.hb-steps > li { flex: 1; display: flex; align-items: center; gap: var(--space-2); font: var(--text-small); color: var(--ink-muted); min-width: 0; white-space: nowrap; }
.hb-steps > li::before { counter-increment: step; content: counter(step); width: 26px; height: 26px; box-sizing: border-box; border-radius: var(--radius-full); border: 2px solid var(--line-strong); background: var(--surface-raised); color: var(--ink-muted); font: var(--text-label); display: inline-flex; align-items: center; justify-content: center; flex: none; }
.hb-steps > li::after { content: ""; flex: 1; height: 2px; background: var(--line); min-width: var(--space-4); }
.hb-steps > li:last-child { flex: none; }
.hb-steps > li:last-child::after { display: none; }
.hb-steps > li[aria-current="step"] { color: var(--brand); font-weight: 500; }
.hb-steps > li[aria-current="step"]::before { border-color: var(--brand); color: var(--brand); }
.hb-steps > li.done { color: var(--ink); }
.hb-steps > li.done::before { content: "\2713"; background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.hb-steps > li.done::after { background: var(--brand); }/* Pagination : le compte « 1–25 sur 312 » à gauche, les pages à droite, la courante en couleur métier, les flèches en boutons à icône. */
.hb-pagination { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; font: var(--text-small); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.hb-pagination-pages { display: inline-flex; gap: 2px; margin-left: auto; }
.hb-pagination-pages > button { min-width: var(--control-sm); height: var(--control-sm); padding: 0 var(--space-2); border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; font: var(--text-small); font-weight: 500; font-variant-numeric: tabular-nums; color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.hb-pagination-pages > button .material-icons, .hb-pagination-pages > button .v-icon { font-size: 18px; color: inherit; }
.hb-pagination-pages > button[aria-current="page"] { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.hb-pagination-pages > button:disabled { color: var(--ink-faint); cursor: default; }
.hb-pagination-pages > span { min-width: var(--control-sm); text-align: center; line-height: var(--control-sm); color: var(--ink-faint); }
@media (hover: hover) and (pointer: fine) { .hb-pagination-pages > button:hover:not([aria-current="page"]):not(:disabled) { background: var(--surface-hover); border-color: var(--line); } }/* Fil d'Ariane : au-dessus du titre de page, la page courante sans lien. */
.hb-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1); margin: 0; padding: 0; list-style: none; font: var(--text-small); color: var(--ink-muted); }
.hb-breadcrumb a { color: var(--ink-muted); text-decoration: none; }
.hb-breadcrumb li + li::before { content: "\203A"; margin-right: var(--space-1); color: var(--ink-faint); }
.hb-breadcrumb [aria-current="page"] { color: var(--ink); font-weight: 500; }
@media (hover: hover) and (pointer: fine) { .hb-breadcrumb a:hover { color: var(--brand); text-decoration: underline; } }/* Accordéon (`<details>`) : titre avec chevron, contenu en retrait ; un seul ou plusieurs à la suite. Séparateur : filet seul ou avec un mot au milieu. */
.hb-details { border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface-raised); }
.hb-details + .hb-details { margin-top: var(--space-2); }
.hb-details > summary { list-style: none; display: flex; align-items: center; gap: var(--space-2); min-height: var(--control); padding: 0 var(--space-4); font: var(--text-body-strong); color: var(--ink); cursor: pointer; border-radius: inherit; }
.hb-details > summary::-webkit-details-marker { display: none; }
.hb-details > summary::before { content: ""; width: 7px; height: 7px; border: solid var(--ink-muted); border-width: 0 2px 2px 0; transform: rotate(-45deg); margin-right: var(--space-1); flex: none; transition: transform 160ms var(--ease-out); }
.hb-details[open] > summary::before { transform: rotate(45deg); }
.hb-details[open] > summary { border-bottom: 1px solid var(--line); border-radius: var(--radius-md) var(--radius-md) 0 0; }
.hb-details > summary > .hb-small, .hb-details > summary > .hb-badge, .hb-details > summary > .hb-count { margin-left: auto; }
.hb-details-body { padding: var(--space-3) var(--space-4); }
.hb-divider { display: flex; align-items: center; gap: var(--space-3); font: var(--text-label); color: var(--ink-muted); margin: var(--space-2) 0; }
.hb-divider::before, .hb-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
@media (hover: hover) and (pointer: fine) { .hb-details > summary:hover { background: var(--surface-hover); } }/* Chronologie (historique d'une fiche, journal de synchro) : heure en tabulaire à gauche, point sur un trait, texte à droite ; le point prend la couleur de l'état (`tone-success`, `tone-warning`, `tone-danger`, `tone-brand` : pas `success` seul, que Vuetify peint en fond). */
.hb-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.hb-timeline > li { position: relative; display: grid; grid-template-columns: 64px 12px minmax(0, 1fr); column-gap: var(--space-3); padding-bottom: var(--space-4); }
.hb-timeline > li::before { content: ""; grid-column: 2; grid-row: 1; width: 12px; height: 12px; box-sizing: border-box; border-radius: var(--radius-full); background: var(--surface-raised); border: 3px solid var(--line-strong); margin-top: 4px; position: relative; z-index: 1; }
.hb-timeline > li::after { content: ""; position: absolute; left: calc(64px + var(--space-3) + 5px); top: 16px; bottom: -4px; width: 2px; background: var(--line); }
.hb-timeline > li:last-child { padding-bottom: 0; }
.hb-timeline > li:last-child::after { display: none; }
.hb-timeline > li.tone-success::before { border-color: var(--success); }
.hb-timeline > li.tone-warning::before { border-color: var(--warning); }
.hb-timeline > li.tone-danger::before { border-color: var(--danger); }
.hb-timeline > li.tone-brand::before { border-color: var(--brand); background: var(--brand); }
.hb-timeline-time { grid-column: 1; grid-row: 1; font: var(--text-small); color: var(--ink-muted); font-variant-numeric: tabular-nums; text-align: right; padding-top: 1px; }
.hb-timeline-body { grid-column: 3; grid-row: 1; min-width: 0; font: var(--text-body); color: var(--ink); }
.hb-timeline-body b { font-weight: 500; }
.hb-timeline-body p { margin: 2px 0 0; font: var(--text-small); color: var(--ink-muted); }/* Infobulle sans Vuetify (`data-tip`) : au survol et au focus, sous l'élément. Dans une app Vuetify, préférer v-tooltip. */
.hb-tip { position: relative; }
.hb-tip::after { content: attr(data-tip); position: absolute; left: 50%; top: calc(100% + 6px); transform: translate(-50%, -2px); z-index: var(--z-tooltip); padding: 4px var(--space-2); border-radius: var(--radius-sm); background: var(--ink); color: var(--surface-raised); font: var(--text-small); white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity var(--duration-tooltip) ease, transform var(--duration-tooltip) var(--ease-out); }
.hb-tip:hover::after, .hb-tip:focus-visible::after { opacity: 1; transform: translate(-50%, 0); }/* Pavé numérique (tablette : poids, prix, PLU) : touches de 64 px, le montant saisi au-dessus en `hb-amount`. Le « C » efface, la coche valide en couleur métier. */
.hb-amount { display: flex; align-items: baseline; justify-content: flex-end; gap: var(--space-2); font: var(--text-number-xl); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--ink); padding: var(--space-3) var(--space-4); background: var(--surface-sunken); border-radius: var(--radius-md); }
.hb-amount > small { font: var(--text-h3); color: var(--ink-muted); }
.hb-amount--placeholder { color: var(--ink-faint); }
.hb-keypad { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-2); }
.hb-key { height: 64px; border: 1px solid var(--line-strong); border-radius: var(--radius-md); background: var(--surface-raised); color: var(--ink); font: var(--text-h2); font-variant-numeric: tabular-nums; cursor: pointer; box-shadow: var(--shadow-sm); display: inline-flex; align-items: center; justify-content: center; padding: 0; transition: transform var(--duration-press) var(--ease-out), background-color 120ms ease; -webkit-tap-highlight-color: transparent; }
.hb-key:active { transform: scale(.96); background: var(--surface-sunken); box-shadow: none; }
.hb-key .material-icons, .hb-key .v-icon { font-size: 28px; color: inherit; }
.hb-key-muted { background: var(--surface-sunken); color: var(--ink-muted); font: var(--text-h3); }
.hb-key-primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
.hb-key-wide { grid-column: span 2; }
@media (hover: hover) and (pointer: fine) { .hb-key:hover { border-color: var(--ink-muted); } .hb-key-primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); } }/* Barre de validation collée (tablette, formulaire long) : résumé ou total à gauche, actions à droite, au-dessus du contenu qui défile. */
.hb-footbar { position: sticky; bottom: 0; z-index: var(--z-sticky); display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-5); background: var(--surface-raised); border-top: 1px solid var(--line); box-shadow: var(--shadow-md); }
.hb-footbar > :first-child { margin-right: auto; }
.hb-footbar-total { font: var(--text-number); font-variant-numeric: tabular-nums; color: var(--ink); }
.hb-footbar-total > small { font: var(--text-small); color: var(--ink-muted); margin-right: var(--space-2); }/* Liste simple (réglages, menus de tablette, résultats) : icône, libellé et sous-texte, valeur ou chevron à droite ; chaque entrée fait au moins une ligne. */
.hb-listing { margin: 0; padding: 0; list-style: none; background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); overflow: hidden; }
.hb-listing-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; min-height: var(--row); padding: var(--space-2) var(--space-4); box-sizing: border-box; border: 0; border-bottom: 1px solid var(--line); background: var(--surface-raised); color: var(--ink); font: var(--text-body); text-align: left; text-decoration: none; cursor: pointer; }
.hb-listing > li:last-child > .hb-listing-item, .hb-listing > .hb-listing-item:last-child { border-bottom: 0; }
.hb-listing-item > .material-icons, .hb-listing-item > .v-icon, .hb-listing-item > .ico { color: var(--brand); font-size: var(--icon); width: var(--icon); height: var(--icon); flex: none; }
.hb-listing-item > .text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.hb-listing-item > .text > small { font: var(--text-small); color: var(--ink-muted); }
.hb-listing-item > .value { font: var(--text-small); color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.hb-listing-item > .chev { width: 7px; height: 7px; border: solid var(--ink-faint); border-width: 2px 2px 0 0; transform: rotate(45deg); margin-right: 4px; flex: none; }
.hb-listing-item[aria-current="true"] { background: var(--brand-soft); color: var(--brand); }
@media (hover: hover) and (pointer: fine) { .hb-listing-item:hover { background: var(--surface-hover); } }/* Zone de dépôt de fichier : pointillé, icône métier, texte ; couleur métier quand un fichier la survole (`hb-dropzone--over`). L'`<input type="file">` reste dedans, caché. */
.hb-dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-6) var(--space-4); border: 2px dashed var(--line-strong); border-radius: var(--radius-lg); background: var(--surface-sunken); color: var(--ink-muted); font: var(--text-small); text-align: center; cursor: pointer; transition: border-color 120ms ease, background-color 120ms ease; }
.hb-dropzone > .material-icons, .hb-dropzone > .v-icon { font-size: 32px; color: var(--brand); }
.hb-dropzone b { font: var(--text-body-strong); color: var(--ink); }
.hb-dropzone input[type="file"] { display: none; }
.hb-dropzone--over { border-color: var(--brand); background: var(--brand-soft); }
@media (hover: hover) and (pointer: fine) { .hb-dropzone:hover { border-color: var(--brand); } }/* Squelette de chargement, suite : ligne de texte, bloc, cercle ; `hb-skeleton-pulse` sur le squelette ou son conteneur y fait passer un balayage (voir Mouvement). */
.hb-skeleton--text { height: 12px; }
.hb-skeleton--block { height: 80px; border-radius: var(--radius-md); }
.hb-skeleton--circle { width: 40px; height: 40px; border-radius: var(--radius-full); }
.hb-skeleton + .hb-skeleton--block { width: auto; }
.hb-skeleton + .hb-skeleton--circle { width: 40px; }/* Point d'état seul (dans un onglet, à côté d'un nom) et pastille sur un bouton à icône (notification, compte). */
.hb-dot { display: inline-block; width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--ink-muted); flex: none; vertical-align: middle; }
.hb-dot-success { background: var(--success); }
.hb-dot-warning { background: var(--warning); }
.hb-dot-danger { background: var(--danger); }
.hb-dot-brand { background: var(--brand); }
.hb-btn-icon > .hb-dot { position: absolute; top: 6px; right: 6px; box-shadow: 0 0 0 2px var(--surface-raised); }
.hb-btn-icon > .hb-count { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; line-height: 16px; padding: 0 4px; font-size: 10px; box-sizing: border-box; background: var(--danger); color: var(--on-danger); box-shadow: 0 0 0 2px var(--surface-raised); }/* Touche clavier dans une aide (`<kbd>`), avatars en trois tailles, avatar image. */
.hb-kbd { font: var(--text-mono); font-size: 12px; line-height: 18px; padding: 0 6px; border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: var(--radius-sm); background: var(--surface-raised); color: var(--ink); }
.hb-avatar-md { width: 32px; height: 32px; line-height: 32px; font-size: 13px; }
.hb-avatar-lg { width: 48px; height: 48px; line-height: 48px; font-size: 18px; }
.hb-avatar > img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; display: block; }/* Curseur (`<input type="range">`) : piste fine, poignée en couleur métier, 28 px de poignée en densité touch ; la valeur s'affiche à côté dans `hb-range-value`. */
.hb-range { display: flex; align-items: center; gap: var(--space-3); }
.hb-range > input[type="range"] { appearance: none; flex: 1; height: 22px; margin: 0; background: transparent; cursor: pointer; }
.hb-range > input[type="range"]::-webkit-slider-runnable-track { height: 6px; border-radius: var(--radius-full); background: var(--surface-sunken); border: 1px solid var(--line); }
.hb-range > input[type="range"]::-moz-range-track { height: 6px; border-radius: var(--radius-full); background: var(--surface-sunken); border: 1px solid var(--line); }
.hb-range > input[type="range"]::-webkit-slider-thumb { appearance: none; width: 22px; height: 22px; margin-top: -9px; border-radius: var(--radius-full); background: var(--brand); border: 3px solid var(--surface-raised); box-shadow: 0 0 0 1px var(--brand); }
.hb-range > input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: var(--radius-full); background: var(--brand); border: 3px solid var(--surface-raised); box-shadow: 0 0 0 1px var(--brand); }
.hb-range > input[type="range"]:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-full); }
.hb-range > input[type="range"]:disabled { opacity: .5; cursor: default; }
.hb-range-value { min-width: 56px; text-align: right; font: var(--text-body-strong); font-variant-numeric: tabular-nums; }
[data-density="touch"] .hb-range > input[type="range"] { height: 28px; }
[data-density="touch"] .hb-range > input[type="range"]::-webkit-slider-thumb { width: 28px; height: 28px; margin-top: -12px; }/* Barre de navigation basse (tablette, téléphone) : trois à cinq entrées, icône au-dessus du libellé, l'active en couleur métier. Collée en bas, au-dessus du contenu. */
.hb-bottom-nav { position: sticky; bottom: 0; z-index: var(--z-nav); display: flex; background: var(--surface-raised); border-top: 1px solid var(--line); box-shadow: var(--shadow-md); }
.hb-bottom-nav > a, .hb-bottom-nav > button { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-height: var(--control-touch-lg); padding: var(--space-1) var(--space-2); border: 0; background: none; color: var(--ink-muted); font: var(--text-label); text-decoration: none; cursor: pointer; position: relative; -webkit-tap-highlight-color: transparent; }
.hb-bottom-nav .material-icons, .hb-bottom-nav .v-icon { font-size: 24px; color: inherit; }
.hb-bottom-nav > [aria-current="page"] { color: var(--brand); }
.hb-bottom-nav > [aria-current="page"]::before { content: ""; position: absolute; top: 0; left: 25%; right: 25%; height: 3px; border-radius: 0 0 2px 2px; background: var(--brand); }
.hb-bottom-nav .hb-count { position: absolute; top: 6px; left: calc(50% + 6px); background: var(--danger); color: var(--on-danger); line-height: 16px; min-width: 16px; height: 16px; font-size: 10px; padding: 0 4px; box-sizing: border-box; }/* Bouton flottant : l'action principale d'un écran tactile quand la barre de validation ne convient pas (créer, scanner). Un seul par écran, en bas à droite. */
.hb-fab { position: fixed; right: var(--space-5); bottom: var(--space-5); z-index: var(--z-sticky); display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: 56px; height: 56px; padding: 0 var(--space-4); border: 0; border-radius: var(--radius-full); background: var(--brand); color: var(--on-brand); font: var(--text-body-strong); box-shadow: var(--shadow-md); cursor: pointer; transition: transform var(--duration-press) var(--ease-out), background-color 120ms ease; -webkit-tap-highlight-color: transparent; }
.hb-fab .material-icons, .hb-fab .v-icon { font-size: 24px; color: inherit; }
.hb-fab:active { transform: scale(.95); }
.hb-fab--icon { padding: 0; width: 56px; }
.hb-fab--static { position: static; }
@media (hover: hover) and (pointer: fine) { .hb-fab:hover { background: var(--brand-hover); } }/* Bannière de page : message qui concerne toute la page (hors ligne, version à mettre à jour, période de clôture), sous la barre du haut, fermable. Pour un message local à une carte : `hb-alert`. */
.hb-banner { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-5); background: var(--brand-soft); color: var(--ink); font: var(--text-body); border-bottom: 1px solid var(--line); }
.hb-banner > .material-icons, .hb-banner > .v-icon { color: var(--brand); font-size: var(--icon); flex: none; }
.hb-banner > p { margin: 0; flex: 1; min-width: 0; }
.hb-banner b { font-weight: 500; }
.hb-banner-warning { background: var(--warning-soft); }
.hb-banner-warning > .material-icons, .hb-banner-warning > .v-icon { color: var(--warning); }
.hb-banner-danger { background: var(--danger-soft); }
.hb-banner-danger > .material-icons, .hb-banner-danger > .v-icon { color: var(--danger); }
.hb-banner .hb-btn { height: var(--control-sm); }/* Arborescence : `<details>` imbriqués (rayons, familles, produits). Chaque niveau décale de 20 px ; une feuille est un lien ou un bouton sans chevron. */
.hb-tree { margin: 0; padding: 0; list-style: none; font: var(--text-body); }
.hb-tree ul { margin: 0; padding: 0 0 0 20px; list-style: none; }
.hb-tree summary, .hb-tree-leaf { list-style: none; display: flex; align-items: center; gap: var(--space-2); min-height: var(--control-sm); padding: 0 var(--space-2); border-radius: var(--radius-md); color: var(--ink); cursor: pointer; text-decoration: none; border: 0; background: none; width: 100%; box-sizing: border-box; text-align: left; font: inherit; }
.hb-tree summary::-webkit-details-marker { display: none; }
.hb-tree summary::before { content: ""; width: 6px; height: 6px; border: solid var(--ink-muted); border-width: 0 2px 2px 0; transform: rotate(-45deg); margin: 0 4px 0 2px; flex: none; transition: transform 160ms var(--ease-out); }
.hb-tree details[open] > summary::before { transform: rotate(45deg); }
.hb-tree-leaf { padding-left: calc(var(--space-2) + 16px); }
.hb-tree-leaf[aria-current="true"] { background: var(--brand-soft); color: var(--brand); font-weight: 500; }
.hb-tree .hb-count, .hb-tree .hb-badge { margin-left: auto; }
@media (hover: hover) and (pointer: fine) { .hb-tree summary:hover, .hb-tree-leaf:hover { background: var(--surface-hover); } }/* Erreur de page : la page entière n'a pas pu se charger (réseau, droits, introuvable). Centré, icône, titre, cause en une phrase, une action pour réessayer ou revenir. Pour une erreur de champ : `hb-hint-error` ; pour une erreur locale : `hb-alert-danger`. */
.hb-error { display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-2); padding: var(--space-8) var(--space-5); max-width: 480px; margin: 0 auto; color: var(--ink-muted); }
.hb-error > .material-icons, .hb-error > .v-icon { font-size: 48px; color: var(--danger); margin-bottom: var(--space-2); }
.hb-error h2 { margin: 0; font: var(--text-h2); color: var(--ink); }
.hb-error p { margin: 0; font: var(--text-body); }
.hb-error .hb-code { text-align: left; width: 100%; max-height: 160px; font-size: 12px; }
.hb-error .hb-row { margin-top: var(--space-3); justify-content: center; }/* Prix et poids : une valeur et son unité, alignés sur la ligne de base, chiffres tabulaires ; trois tailles. `hb-measure--muted` pour un prix barré ou secondaire. */
.hb-measure { display: inline-flex; align-items: baseline; gap: 3px; font: var(--text-number); font-variant-numeric: tabular-nums; letter-spacing: var(--ls-number); color: var(--ink); white-space: nowrap; }
.hb-measure > small { font: var(--text-small); color: var(--ink-muted); }
.hb-measure-lg { font: var(--text-h2); letter-spacing: 0; }
.hb-measure-lg > small { font: var(--text-body); }
.hb-measure-xl { font: var(--text-number-xl); letter-spacing: -.02em; }
.hb-measure-xl > small { font: var(--text-h3); }
.hb-measure--muted { color: var(--ink-muted); }
.hb-measure--strike { color: var(--ink-faint); text-decoration: line-through; }
.hb-measure--up { color: var(--success); }
.hb-measure--down { color: var(--danger); }/* Panneau à onglets dans un panneau latéral ou une carte : `hb-tabs` au-dessus, un panneau visible à la fois (`hidden` sur les autres). */
.hb-tabpanel { padding: var(--space-4) 0 0; }
.hb-tabpanel[hidden] { display: none; }/* Sélecteur d'heure à la manière d'iOS (`Hublot.timePicker`, `Hublot.timeField`, script dans bundle.js).
Deux roues en boucle, heures et minutes ; la ligne au centre est la valeur, posée sur une bande grise arrondie ; les autres tournent sur un cylindre et s'effacent.
On fait défiler au doigt, à la molette ou au clavier (flèches, Page, Début, Fin) ; un appui sur une ligne l'amène au centre ; la roue s'arrête net sur une valeur.
`hb-time` est la pastille d'heure d'iOS : un appui ouvre les roues dans une bulle (`hb-popover`), la valeur change en direct. */
.hb-timepicker { --wheel-item: 36px; --wheel-rows: 5; position: relative; display: inline-flex; justify-content: center; gap: 2px; padding: 0 var(--space-2); user-select: none; -webkit-user-select: none; }
[data-density="touch"] .hb-timepicker { --wheel-item: 44px; }
[data-density="compact"] .hb-timepicker { --wheel-item: 32px; }
.hb-timepicker::before { content: ""; position: absolute; left: var(--space-1); right: var(--space-1); top: 50%; height: var(--wheel-item); transform: translateY(-50%); border-radius: var(--radius-md); background: var(--surface-sunken); pointer-events: none; transition: box-shadow var(--duration-state) ease; }
.hb-timepicker:has(.hb-wheel:focus-visible)::before { box-shadow: 0 0 0 2px var(--focus-ring); }
.hb-wheel { position: relative; width: 72px; height: calc(var(--wheel-item) * var(--wheel-rows)); overflow-y: auto; overflow-x: hidden; scroll-snap-type: y mandatory; overscroll-behavior: contain; scrollbar-width: none; perspective: 520px; outline: none; touch-action: pan-y; -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: transparent;
-webkit-mask-image: linear-gradient(to bottom, transparent, black 30%, black 70%, transparent); mask-image: linear-gradient(to bottom, transparent, black 30%, black 70%, transparent); }
.hb-wheel::-webkit-scrollbar { display: none; }
.hb-wheel-track { padding: calc(var(--wheel-item) * (var(--wheel-rows) - 1) / 2) 0; transform-style: preserve-3d; }
.hb-wheel-item { height: var(--wheel-item); display: flex; align-items: center; justify-content: center; scroll-snap-align: center; scroll-snap-stop: normal; font: 400 22px/1 var(--font-sans); font-variant-numeric: tabular-nums; letter-spacing: .01em; color: var(--ink); cursor: pointer; backface-visibility: hidden; will-change: transform, opacity; }
[data-density="touch"] .hb-wheel-item { font-size: 24px; }
[data-density="compact"] .hb-wheel-item { font-size: 19px; }
.hb-wheel-item.is-selected { font-weight: 500; }/* Pastille d'heure : fond gris arrondi, chiffres tabulaires ; en couleur métier tant que la bulle est ouverte. */
.hb-time { display: inline-flex; align-items: center; justify-content: center; min-width: 76px; height: var(--control); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: var(--surface-sunken); color: var(--ink); font: var(--text-body); font-variant-numeric: tabular-nums; cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: color var(--duration-state) ease, background-color var(--duration-state) ease, transform var(--duration-press) var(--ease-out); }
.hb-time[aria-expanded="true"] { color: var(--brand); background: var(--brand-soft); }/* Dans un formulaire, l'heure prend le cadre d'un champ : même filet, même fond, valeur à gauche. */
.hb-group > .hb-time { width: 100%; justify-content: flex-start; border: 1px solid var(--line-strong); background: var(--surface-raised); }
.hb-group > .hb-time[aria-expanded="true"], .hb-group > .hb-time:focus-visible { outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); color: var(--ink); background: var(--surface-raised); }
.hb-time:active { transform: scale(.97); }
.hb-time:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.hb-time:disabled { opacity: .5; cursor: default; }
@media (hover: hover) and (pointer: fine) { .hb-time:hover:not([aria-expanded="true"]):not(:disabled) { background: var(--surface-hover); box-shadow: inset 0 0 0 1px var(--line); } }/* Bulle flottante (ouverte par un script : sélecteur d'heure, petit réglage) : niveau 4, s'ouvre en ressort depuis la pastille. */
.hb-popover { position: fixed; z-index: var(--z-dropdown); padding: var(--space-2) var(--space-1); background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-md); animation: hb-pop-in var(--duration-menu) var(--ease-spring); }
@media (prefers-reduced-motion: reduce) { .hb-popover { animation: hb-fade-in var(--duration-state) ease; } .hb-time, .hb-timepicker::before { transition: none; } }/* Plateformes : un même balisage sur poste, tablette Android, iPhone et iPad.
– Thème natif : les contrôles du système (sélecteurs, barres de défilement, champs date) suivent le thème ; `accent-color` les met à la couleur métier.
– Tactile (`pointer: coarse`) : pas de surbrillance grise au toucher, pas de délai de double-tap, et les champs à 16 px au moins (sinon Safari iOS zoome à la saisie).
– Zones de sécurité (encoche, barre d'accueil, iPad en multitâche) : pages, barres collées, bouton flottant et toast s'écartent de `env(safe-area-inset-*)` ; l'application pose `viewport-fit=cover` dans la balise viewport.
– Hauteur d'écran : un conteneur plein écran prend `100dvh` (la barre d'adresse de Safari iOS va et vient), avec `100vh` en repli. */
:root { color-scheme: light; accent-color: var(--brand); -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
[data-theme="dark"] { color-scheme: dark; }
@media (prefers-color-scheme: dark) { [data-theme="auto"] { color-scheme: dark; } }
.hb-btn, .hb-time, .hb-chip, .hb-tab, .hb-tab-add, .hb-check, .hb-radio, .hb-switch, .hb-key, .hb-listing-item, .hb-list-date, .hb-avatar-stack, .hb-segmented > button, .hb-stepper > button, .hb-pagination-pages > button, .hb-bottom-nav > a, .hb-bottom-nav > button, .hb-fab, .hb-nav a, .hb-tree summary, .hb-tree-leaf, .hb-details > summary { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
@media (pointer: coarse) {
.hb-field, .hb-editable, .hb-inline, .hb-stepper > .hb-field, .hb-select > .hb-field, .hb-suffix > span, .hb-time { font-size: max(16px, 1em); }
}
.hb-page { padding-left: max(var(--space-6), env(safe-area-inset-left)); padding-right: max(var(--space-6), env(safe-area-inset-right)); }
.hb-bottom-nav { padding-bottom: env(safe-area-inset-bottom); }
.hb-footbar { padding-bottom: max(var(--space-3), env(safe-area-inset-bottom)); }
.hb-fab { right: max(var(--space-5), calc(env(safe-area-inset-right) + var(--space-3))); bottom: max(var(--space-5), calc(env(safe-area-inset-bottom) + var(--space-3))); }
.hb-toast { bottom: max(var(--space-5), calc(env(safe-area-inset-bottom) + var(--space-3))); }
.hb-nav { padding-top: max(20px, env(safe-area-inset-top)); padding-bottom: max(20px, env(safe-area-inset-bottom)); }/* Contraste renforcé (réglage d'accessibilité iOS, Android, macOS, Windows) : filets au niveau des bordures de champ, texte secondaire en encre, anneau de focus de 3 px. */
@media (prefers-contrast: more) {
:root { --line: var(--line-strong); --ink-muted: var(--ink); --ink-faint: var(--ink-muted); }
.hb-btn:focus-visible, .hb-field:focus-visible, .hb-check:focus-visible, .hb-radio:focus-visible, .hb-switch:focus-visible, .hb-tab:focus-visible, .hb-chip:focus-visible { outline-width: 3px; }
.hb-card, .hb-list, .hb-table-wrap, .hb-tile, .hb-menu, .hb-dialog { border-color: var(--line-strong); }
}/* Mouvement : une réponse à chaque action, rien qui bouge seul hors chargement.
Ressort (`--ease-spring`) pour ce qui confirme (coche, interrupteur, toast, menu qui s'ouvre) ; sortie rapide (`--ease-out`) pour ce qui change d'état ; balayage pour ce qui charge.
Tout est coupé ou ramené à un fondu sous `prefers-reduced-motion`. */
.hb-btn { transition: transform var(--duration-press) var(--ease-out), background-color var(--duration-state) ease, border-color var(--duration-state) ease, box-shadow var(--duration-state) ease, color var(--duration-state) ease; }
.hb-btn:focus-visible, .hb-field:focus-visible, .hb-check:focus-visible, .hb-radio:focus-visible, .hb-switch:focus-visible, .hb-tab:focus-visible, .hb-chip:focus-visible, .hb-key:focus-visible { animation: hb-focus-in var(--duration-state) var(--ease-out); }
@keyframes hb-focus-in { from { outline-offset: 5px; outline-color: transparent; } }
.hb-check:checked::after { animation: hb-check-in var(--duration-menu) var(--ease-spring); }
@keyframes hb-check-in { from { opacity: 0; transform: translateY(-1px) rotate(45deg) scale(.3); } }
.hb-radio:checked::after { animation: hb-dot-in var(--duration-menu) var(--ease-spring); }
@keyframes hb-dot-in { from { transform: scale(0); } }
.hb-switch::after { transition: transform var(--duration-menu) var(--ease-spring), width var(--duration-press) var(--ease-out), background-color var(--duration-state) ease; }
.hb-switch:active:not(:disabled)::after { width: 18px; }
.hb-switch:checked:active:not(:disabled)::after { transform: translateX(14px); }
[data-density="touch"] .hb-switch:active:not(:disabled)::after { width: 26px; }
[data-density="touch"] .hb-switch:checked:active:not(:disabled)::after { transform: translateX(18px); }
.hb-tab, .hb-chip-toggle, .hb-segmented > button, .hb-listing-item, .hb-list-row, .hb-nav a, .hb-pagination-pages > button { transition: background-color var(--duration-state) ease, color var(--duration-state) ease, border-color var(--duration-state) ease, box-shadow var(--duration-state) ease; }
.hb-menu, .hb-dialog { animation: hb-pop-in var(--duration-menu) var(--ease-spring); transform-origin: top center; }
.hb-dialog { animation-duration: var(--duration-drawer); transform-origin: center; }
@keyframes hb-pop-in { from { opacity: 0; transform: translateY(-4px) scale(.97); } }
.hb-toast-enter { animation: hb-toast-in var(--duration-drawer) var(--ease-spring); }/* Ligne ajoutée (tâche, commentaire, relevé) : un éclat métier qui s'éteint, pour retrouver d'un coup d'œil ce qui vient d'arriver. */
.hb-list-row--new, .hb-listing-item.is-new, .hb-table tr.is-new td { animation: hb-flash 1.6s var(--ease-out); }
@keyframes hb-flash { from { background-color: var(--brand-soft); box-shadow: inset 3px 0 0 var(--brand); } }/* Squelette : un balayage lumineux de gauche à droite plutôt qu'un clignotement. */
.hb-skeleton-pulse .hb-skeleton, .hb-skeleton.hb-skeleton-pulse { position: relative; overflow: hidden; animation: none; }
.hb-skeleton-pulse .hb-skeleton::after, .hb-skeleton.hb-skeleton-pulse::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, var(--surface-raised), transparent); opacity: .7; transform: translateX(-100%); animation: hb-sweep 1.4s var(--ease-move) infinite; }
@keyframes hb-sweep { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
.hb-btn, .hb-switch::after, .hb-tab, .hb-chip-toggle, .hb-segmented > button, .hb-listing-item, .hb-list-row, .hb-nav a, .hb-pagination-pages > button { transition: none; }
.hb-check:checked::after, .hb-radio:checked::after, .hb-btn:focus-visible, .hb-field:focus-visible, .hb-check:focus-visible, .hb-radio:focus-visible, .hb-switch:focus-visible, .hb-tab:focus-visible, .hb-chip:focus-visible, .hb-key:focus-visible, .hb-skeleton-pulse .hb-skeleton::after, .hb-skeleton.hb-skeleton-pulse::after { animation: none; }
.hb-menu, .hb-dialog, .hb-toast-enter, .hb-list-row--new, .hb-listing-item.is-new, .hb-table tr.is-new td { animation: hb-fade-in var(--duration-state) ease; }
.hb-switch:active:not(:disabled)::after { width: auto; }
}
@keyframes hb-fade-in { from { opacity: 0; } }
@media (max-width: 599.98px) {
.hb-page { padding: var(--space-4) max(var(--space-4), env(safe-area-inset-right)) var(--space-4) max(var(--space-4), env(safe-area-inset-left)); gap: var(--space-4); }
.hb-head > :first-child { flex: 1 1 100%; min-width: 0; }
.hb-head__actions { margin-left: 0; flex-wrap: wrap; max-width: 100%; }
.hb-card-h { flex-wrap: wrap; padding: var(--space-3) var(--space-4); row-gap: var(--space-2); }
.hb-card-h__actions { flex-wrap: wrap; max-width: 100%; }
.hb-tiles { grid-template-columns: minmax(0, 1fr); }
.hb-cell-sub { white-space: normal; }
.hb-kv { grid-template-columns: 1fr; gap: 2px var(--space-4); }
.hb-kv dd { margin-bottom: var(--space-2); }
.hb-drawer-h { flex-wrap: wrap; }
.hb-drawer-h h2, .hb-drawer-h .hb-drawer-h__title { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: break-word; }
.hb-alert { flex-wrap: wrap; }
.hb-alert .hb-btn, .hb-alert .v-btn { margin-left: 0; }
.hb-list-row { grid-template-columns: 20px 22px minmax(0, 1fr); }
.hb-list-side { grid-column: 3; grid-template-columns: auto auto 1fr; justify-items: start; margin-top: var(--space-2); }
.hb-list-actions, .hb-list-add, .hb-list-handle { opacity: 1; }
.hb-filters .v-input { max-width: none !important; flex: 1 1 100%; }
.hb-form { grid-template-columns: minmax(0, 1fr); }
.hb-steps > li:not([aria-current="step"]) > span { display: none; }
.hb-timeline > li { grid-template-columns: 44px 12px minmax(0, 1fr); }
.hb-timeline > li::after { left: calc(44px + var(--space-3) + 5px); }
.hb-footbar { padding: var(--space-3) var(--space-4); flex-wrap: wrap; }
.hb-pagination-pages { margin-left: 0; }
.hb-banner { padding: var(--space-2) var(--space-4); flex-wrap: wrap; }
}
@media (min-width: 600px) and (max-width: 959.98px) { .hb-page { padding: var(--space-5); } }
@media (prefers-reduced-motion: reduce) {
.hb-btn, .hb-check, .hb-field, .hb-editable, .hb-list-row, .hb-switch, .hb-switch::after, .hb-progress > i, .hb-key, .hb-details > summary::before, .hb-tip::after, .hb-fab, .hb-tree summary::before { transition: none; }
.hb-toast-enter, .hb-progress--busy > i { animation: none; }
.hb-spinner { animation-duration: 1.6s; }
.hb-btn:active, .hb-check:active, .hb-key:active, .hb-fab:active { transform: none; }
}// Hublot — design system v2, habillage Vuetify 2. Chargé après vuetify.min.css, tokens.css et bundle.css. // Les composants Vuetify (menus, sélecteurs, dialogues, tableaux, puces) restent ceux de Vuetify : ils prennent // seulement les tokens. Règle : les couleurs viennent des tokens (--brand, --surface, …), jamais d'un hexadécimal. // Le métier se bascule par [data-activity] sur <html>, le thème par [data-theme], la densité par [data-density] (theme.ts).
// ---------- Base ----------
.v-application {
font-family: var(--font-sans) !important;
font-size: var(--fs-body);
line-height: var(--lh-body);
color: var(--ink);
background: var(--surface) !important;
font-variant-numeric: tabular-nums;
.v-application--wrap { background: var(--surface); }
a { color: var(--link); }
// Typographie : les classes Vuetify gardent leur nom, le rendu suit le système
.text-h4, .text-h5, .page-title { font: var(--text-h1); letter-spacing: -.3px; color: var(--brand); }
.text-h6, .text-subtitle-1.font-weight-bold { font: var(--text-h3); letter-spacing: 0; color: var(--ink); }
.text-body-1, .text-subtitle-1 { font: var(--text-body); letter-spacing: 0; }
.text-subtitle-1 { font-weight: 500; }
.text-body-2, .text-caption { font: var(--text-small); letter-spacing: 0; }
.text-subtitle-2 { font: var(--text-small); font-weight: 500; letter-spacing: 0; }
.text-overline { font: var(--text-label); letter-spacing: 0; text-transform: none; color: var(--ink-muted); }
.grey--text, .hb-muted { color: var(--ink-muted) !important; }
.success--text, .green--text, .hb-text-success { color: var(--success) !important; }
.warning--text, .orange--text, .amber--text, .hb-text-warning { color: var(--warning) !important; }
.error--text, .red--text, .hb-text-danger { color: var(--danger) !important; }
.primary--text, .hb-text-brand { color: var(--brand) !important; }
code, kbd { font: var(--text-mono); background: var(--surface-sunken); color: var(--ink); border-radius: var(--radius-sm); padding: 0 4px; box-shadow: none; }
// Icônes : neutres en `ink-muted`, couleur métier dès qu'elles portent une action ; jamais de gris Material ni de rouge au repos.
// Un composant posé en `dark` (tiroir de navigation sombre) garde ses icônes blanches.
.v-icon:not(.theme--dark) { color: var(--ink-muted); }
.v-icon.primary--text, .v-btn--icon:hover .v-icon, .v-list-item--link:hover .v-icon { color: var(--brand) !important; }
.v-btn .v-icon { font-size: var(--icon); }
.hb-btn .v-icon { color: inherit; font-size: var(--icon); }
.hb-btn-icon .v-icon { color: inherit !important; }
}// ---------- Surfaces : cartes, menus, dialogues ----------
.v-application {
// Carte posée sur la page grisée : blanche, filet et ombre (tokens --shadow-card)
.v-sheet.v-card:not(.v-card--flat):not(.v-picker), .hb-card {
background: var(--surface-raised);
border: 1px solid var(--line);
border-radius: var(--radius-lg) !important;
box-shadow: var(--shadow-card) !important;
}
.v-card--flat { background: var(--surface-raised); border-radius: var(--radius-lg) !important; }
.v-card > .v-card__title {
font: var(--text-h2); letter-spacing: -.2px; color: var(--ink);
padding: var(--space-4) var(--space-5);
word-break: normal; overflow-wrap: break-word;
}
.v-card > .v-card__subtitle { font: var(--text-small); color: var(--ink-muted); padding: 0 var(--space-5) var(--space-3); }
.v-card > .v-card__text { color: var(--ink); font: var(--text-body); padding: var(--space-4) var(--space-5); }
// Actions de carte et de dialogue : Annuler / Fermer en premier dans le DOM donc à gauche, l'action principale en dernier donc à droite, toujours un espace entre les deux (sans v-spacer à poser).
.v-card > .v-card__actions { padding: var(--space-3) var(--space-5); gap: var(--space-2); border-top: 1px solid var(--line); background: var(--surface-hover); }
.v-card > .v-card__actions > :first-child:not(.v-spacer) { margin-right: auto; }
.v-card > .v-card__actions > :only-child { margin-right: 0; margin-left: auto; }
.v-card > .v-card__actions > .v-btn + .v-btn { margin-left: 0; }
.v-dialog > .v-card, .v-dialog > .v-sheet.v-card { box-shadow: var(--shadow-md) !important; border: 0; }
.v-dialog > .v-card > .v-card__title { border-bottom: 1px solid var(--line); }
.v-dialog:not(.v-dialog--fullscreen) {
display: flex; flex-direction: column;
> .v-card:not(.v-picker) {
flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; overflow-y: auto;
> * { flex: 0 0 auto; }
> .v-card__text { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
}
}
.v-dialog, .v-dialog > .v-card, .v-dialog > .v-card > .v-card__text,
.v-navigation-drawer--temporary > .v-navigation-drawer__content { overscroll-behavior: contain; }
// Menus Vuetify (v-menu, v-select, v-autocomplete, sélecteur de date) : ils flottent, donc filet et ombre `md`
.v-menu__content, .v-autocomplete__content {
border-radius: var(--radius-md) !important;
border: 1px solid var(--line);
box-shadow: var(--shadow-md) !important;
background: var(--surface-raised);
}
.v-divider:not(.theme--dark) { border-color: var(--line) !important; }
.v-expansion-panel { background: var(--surface-raised); box-shadow: none !important; &::after { border-color: var(--line) !important; } }
.v-expansion-panels:not(.v-expansion-panels--accordion) > .v-expansion-panel { background: transparent; border-radius: 0 !important; &::before { box-shadow: none; } }
.v-expansion-panels--accordion > .v-expansion-panel { border-bottom: 1px solid var(--line); &:last-child { border-bottom: 0; } }
.v-expansion-panel-header {
min-height: 48px; padding: var(--space-3) var(--space-4); font: var(--text-h3); color: var(--ink);
justify-content: flex-start; gap: var(--space-2); text-align: left;
> .v-icon { flex: none; margin: 0; color: var(--brand); }
> span { flex: 1 1 auto; min-width: 0; }
.v-expansion-panel-header__icon { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-2); .v-icon { color: var(--brand); } }
}
.v-expansion-panels:not(.v-expansion-panels--accordion) > .v-expansion-panel > .v-expansion-panel-header,
.v-expansion-panel--active > .v-expansion-panel-header { border-bottom: 2px solid var(--accent); }
.v-expansion-panel-content .v-expansion-panel-header, .v-dialog .v-expansion-panel-header {
background: var(--surface-sunken);
> .v-icon, .v-expansion-panel-header__icon .v-icon { color: var(--ink-muted); }
}
.v-expansion-panel-content__wrap { padding: var(--space-3) var(--space-4) var(--space-4); }
.v-skeleton-loader__bone { background: var(--surface-sunken); }
}// ---------- Boutons ----------
.v-application {
.v-btn {
text-transform: none; letter-spacing: 0; font-weight: 500;
border-radius: var(--radius-md);
transition: transform 140ms var(--hb-ease-out), background-color 120ms ease, box-shadow 120ms ease;
&:not(.v-btn--round).v-size--default { height: var(--control); padding: 0 var(--space-4); font-size: var(--fs-body); }
&:not(.v-btn--round).v-size--small { height: var(--control-sm); padding: 0 var(--space-3); font-size: var(--fs-small); }
&:active { transform: scale(.97); }
&.v-btn--is-elevated { box-shadow: var(--shadow-sm); }
&.v-btn--outlined { border-color: var(--line-strong); background: var(--surface-raised); color: var(--ink); }
&.v-btn--outlined .v-icon { color: var(--brand); }
&.v-btn--text.primary--text, &.v-btn--outlined.primary--text { color: var(--brand) !important; }
&.v-btn--icon.v-size--default { width: var(--control); height: var(--control); }
&.v-btn--icon .v-icon { color: var(--ink-muted); }
&.v-btn--icon.error--text .v-icon, &.v-btn--icon.error--text:hover .v-icon { color: var(--danger) !important; }
&:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
}
.v-btn.v-btn--has-bg.primary { background: var(--brand) !important; color: var(--on-brand) !important; .v-icon { color: var(--on-brand); } }
.v-btn.v-btn--has-bg.secondary { background: var(--slate) !important; color: var(--on-slate) !important; .v-icon { color: var(--on-slate); } }
.v-btn.v-btn--has-bg.error { background: var(--danger) !important; color: var(--on-danger) !important; }
.v-btn.v-btn--has-bg.white, .v-btn.v-btn--has-bg.theme--light:not(.primary):not(.secondary):not(.error):not(.success):not(.warning) { background: var(--surface-raised) !important; color: var(--ink) !important; border: 1px solid var(--line-strong); box-shadow: var(--shadow-sm); }
@media (hover: hover) and (pointer: fine) {
.v-btn.v-btn--has-bg.primary:hover { background: var(--brand-hover) !important; }
.v-btn.v-btn--outlined:hover { background: var(--surface-hover); border-color: var(--ink-muted); }
.v-btn.v-btn--icon:hover { background: var(--surface-sunken); }
}
.v-btn--fab { border-radius: var(--radius-full); }
.v-btn-toggle { border-radius: var(--radius-md); border: 1px solid var(--line-strong); box-shadow: none !important; background: var(--surface-raised);
> .v-btn.v-btn { border: 0; opacity: 1; color: var(--ink-muted); height: var(--control-sm); }
> .v-btn.v-btn--active { background: var(--brand-soft) !important; color: var(--brand) !important; &::before { opacity: 0; } }
}
.v-stepper { box-shadow: none !important; border: 1px solid var(--line); border-radius: var(--radius-lg);
.v-stepper__header { box-shadow: none; border-bottom: 1px solid var(--line); }
.v-stepper__step--active .v-stepper__step__step, .v-stepper__step--complete .v-stepper__step__step { background: var(--brand) !important; color: var(--on-brand); }
.v-stepper__label { font: var(--text-body); color: var(--ink-muted); }
.v-stepper__step--active .v-stepper__label { color: var(--ink); font-weight: 500; }
.v-stepper__content { padding: var(--space-4); }
}
.v-speed-dial .v-btn--fab { box-shadow: var(--shadow-md); }
}// ---------- Champs : filet visible au repos, foncé au survol, couleur métier au focus ----------
.v-application {
.v-text-field--outlined, .v-select--outlined {
fieldset { border-color: var(--line-strong); border-radius: var(--radius-md); transition: border-color 120ms ease; }
&:hover:not(.v-input--is-focused):not(.v-input--is-disabled) fieldset { border-color: var(--ink-muted); }
&.v-input--is-focused fieldset { border: 2px solid var(--brand); }
&.error--text fieldset { border-color: var(--danger); }
.v-label { color: var(--ink-muted); font-size: var(--fs-body); }
.v-label--active { color: var(--brand); }
input, textarea, .v-select__selection { color: var(--ink); font-size: var(--fs-body); }
input::placeholder { color: var(--ink-faint); opacity: 1; }
&.v-input--is-disabled fieldset { border-color: var(--line); }
&.v-input--is-disabled .v-input__slot { background: var(--surface-sunken); }
}
.v-text-field--outlined.v-input--dense .v-input__control > .v-input__slot { min-height: var(--control); }
.v-input__icon .v-icon { color: var(--ink-muted); }
.v-select.v-input--is-focused .v-input__icon .v-icon { color: var(--brand); }
.v-messages__message { font: var(--text-small); color: var(--ink-muted); }
.error--text .v-messages__message { color: var(--danger); }
// Cases, radios, interrupteurs
.v-input--checkbox .v-icon, .v-input--radio-group .v-icon { color: var(--line-strong); }
.v-input--checkbox:hover .v-icon { color: var(--brand); }
.v-input--is-label-active.v-input--checkbox .v-icon, .v-input--is-label-active .v-icon.primary--text, .v-input--selection-controls__input .v-icon.primary--text { color: var(--brand) !important; }
.v-input--switch .v-input--switch__track.primary--text, .v-input--switch .v-input--switch__thumb.primary--text { color: var(--brand) !important; }
.v-input--selection-controls .v-label { color: var(--ink); font-size: var(--fs-body); line-height: var(--lh-body); }
// Sélecteur de date : jour choisi en couleur métier, aujourd'hui cerclé
.v-picker { border-radius: var(--radius-md); }
.v-picker__body { background: var(--surface-raised); }
.v-date-picker-header__value button, .v-date-picker-table th { color: var(--ink); font-weight: 600; }
.v-date-picker-table .v-btn { color: var(--ink); font-weight: 400; }
.v-date-picker-table .v-btn.v-btn--active { background: var(--brand) !important; color: var(--on-brand) !important; }
.v-date-picker-table .v-btn.v-btn--outlined { border-color: var(--brand); }
}// ---------- Tableaux ----------
.v-application {
.v-data-table {
background: var(--surface-raised); color: var(--ink); border-radius: var(--radius-lg);
> .v-data-table__wrapper > table {
> thead > tr > th {
font: var(--text-label); letter-spacing: 0; text-transform: none; color: var(--ink-muted) !important;
background: var(--surface-sunken); height: 36px; border-bottom: 1px solid var(--line) !important; white-space: nowrap;
&.sortable:hover, &.active { color: var(--brand) !important; }
.v-data-table-header__icon { color: var(--brand); }
}
> tbody > tr > td {
height: var(--row); font-size: var(--fs-body); line-height: var(--lh-body); border-bottom: 1px solid var(--line) !important;
&.text-end, &.num { font-variant-numeric: tabular-nums; font-weight: 500; }
}
> tbody > tr:last-child > td { border-bottom: 0 !important; }
> tbody > tr.v-data-table__selected > td, > tbody > tr[aria-selected="true"] > td { background: var(--brand-soft) !important; }
}
&.v-data-table--dense > .v-data-table__wrapper > table > tbody > tr > td { height: var(--control); font-size: var(--fs-small); }
.v-data-footer { border-top: 1px solid var(--line); font: var(--text-small); color: var(--ink-muted); }
}
.v-data-table.hb-clickable > .v-data-table__wrapper > table > tbody > tr:not(.v-data-table__empty-wrapper) { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) > td { background: var(--surface-hover) !important; }
}
.v-data-table--fixed-header > .v-data-table__wrapper > table > thead > tr > th { background: var(--surface-sunken); box-shadow: inset 0 -1px 0 var(--line); }
}// ---------- Listes, puces, onglets, alertes ----------
.v-application {
// Une liste posée en `dark` (tiroir de navigation sombre d'une application) garde le rendu Vuetify : seules les listes claires prennent les tokens.
.v-list:not(.theme--dark) {
background: var(--surface-raised); color: var(--ink); padding: var(--space-1) 0;
// Ligne de liste et de menu : 16 px de gouttière, 12 px entre la case, l'avatar ou l'icône et le texte, tout centré verticalement
.v-list-item { min-height: var(--control); padding: 0 var(--space-4); }
&.v-list--dense .v-list-item { min-height: var(--control); }
.v-list-item__content { padding: var(--space-2) 0; }
.v-list-item__action, .v-list-item__avatar, .v-list-item__icon { margin: 0 var(--space-3) 0 0 !important; min-width: 0; align-self: center; }
.v-list-item__action:last-child:not(:first-child), .v-list-item__icon:last-child:not(:first-child) { margin: 0 0 0 var(--space-3) !important; }
.v-list-item__action .v-input--selection-controls__input { margin-right: 0; }
.v-list-item__action .v-input--selection-controls { margin: 0; padding: 0; }
.v-list-item__avatar.v-avatar { height: 24px !important; min-width: 24px !important; width: 24px !important; }
.v-list-item__title { font-size: var(--fs-body); line-height: var(--lh-body); }
.v-list-item__subtitle { font: var(--text-small); color: var(--ink-muted) !important; }
.v-list-item--active:not(.nav-item) { color: var(--brand) !important; &::before { background: var(--brand); } }
.v-list-item__icon .v-icon, .v-list-item__action .v-icon { color: var(--ink-muted); }
@media (hover: hover) and (pointer: fine) { .v-list-item--link:hover { background: var(--surface-hover); } }
}
// Puces : coins doux, une pilule pour les personnes (avatar + nom), jamais de texte noir ou blanc sur un aplat vif
.v-chip { border-radius: var(--radius-full); font-weight: 500; font-size: var(--fs-small); letter-spacing: 0; background: var(--surface-sunken); color: var(--ink); }
.v-chip.v-size--default { height: 28px; padding: 0 var(--space-3); }
.v-chip.v-size--small { height: 24px; padding: 0 var(--space-2); }
.v-chip.v-size--x-small { height: 20px; padding: 0 6px; font-size: 11.5px; }
.v-chip.v-chip--pill .v-avatar { height: 22px !important; min-width: 22px !important; width: 22px !important; margin-left: -8px; margin-right: 6px; font: 600 10px/22px var(--font-sans); color: var(--on-slate); }
.v-chip.v-chip--outlined { background: var(--surface-raised); border-color: var(--line); }
.v-chip.success:not(.v-chip--outlined) { background: var(--success-soft) !important; color: var(--success) !important; }
.v-chip.warning:not(.v-chip--outlined) { background: var(--warning-soft) !important; color: var(--warning) !important; }
.v-chip.error:not(.v-chip--outlined) { background: var(--danger-soft) !important; color: var(--danger) !important; }
.v-chip.primary:not(.v-chip--outlined), .v-chip.info:not(.v-chip--outlined) { background: var(--brand-soft) !important; color: var(--brand) !important; }
.v-chip.secondary:not(.v-chip--outlined) { background: var(--slate-soft) !important; color: var(--slate) !important; }
.v-chip--outlined.primary--text { border-color: var(--brand); color: var(--brand) !important; }
.v-chip:not(.v-chip--outlined) {
&.blue, &.light-blue, &.indigo, &.cyan, &.purple, &.deep-purple { background: var(--brand-soft) !important; color: var(--brand) !important; }
&.green, &.teal, &.light-green, &.lime { background: var(--success-soft) !important; color: var(--success) !important; }
&.orange, &.amber, &.yellow, &.deep-orange { background: var(--warning-soft) !important; color: var(--warning) !important; }
&.red, &.pink { background: var(--danger-soft) !important; color: var(--danger) !important; }
&.grey, &.blue-grey, &.brown { background: var(--surface-sunken) !important; color: var(--ink-muted) !important; }
&.blue, &.light-blue, &.indigo, &.cyan, &.purple, &.deep-purple, &.green, &.teal, &.light-green, &.lime, &.orange, &.amber, &.yellow, &.deep-orange, &.red, &.pink, &.grey, &.blue-grey, &.brown { .v-icon { color: inherit !important; } }
}
@media (hover: hover) and (pointer: fine) { .v-chip--clickable:hover { border-color: var(--brand); background: var(--brand-soft); color: var(--brand); } }
// Onglets : filet sous la rangée, l'actif en couleur métier sur fond doux, barre de 3 px
.v-tabs-bar { background: transparent !important; border-bottom: 1px solid var(--line); height: auto; }
.v-tab { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: var(--fs-body); color: var(--ink-muted) !important; min-width: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md) var(--radius-md) 0 0; }
.v-tab--active { color: var(--brand) !important; background: var(--brand-soft); }
.v-tab:hover { color: var(--ink) !important; }
.v-tabs-slider { background: var(--brand); height: 3px; }
// Rangée d'onglets avec un « + » en bout et des filtres à droite : le filet court sur toute la rangée, les onglets ne prennent que leur largeur
.hb-tabs-row { display: flex; align-items: flex-end; gap: var(--space-3); border-bottom: 1px solid var(--line); }
.hb-tabs-row > .v-tabs { width: auto; flex: 0 1 auto; min-width: 0; }
.hb-tabs-row > .v-tabs .v-tabs-bar { border-bottom: 0; }
.hb-tabs-row .hb-tab-add { margin-bottom: 6px; }
.hb-tabs-row .hb-head__actions { margin-bottom: 6px; }
.v-alert { border-radius: var(--radius-md); font: var(--text-body); border-left: 4px solid currentColor; }
.v-alert.v-alert--text.success, .v-alert.success { background: var(--success-soft) !important; color: var(--success) !important; }
.v-alert.v-alert--text.warning, .v-alert.warning { background: var(--warning-soft) !important; color: var(--warning) !important; }
.v-alert.v-alert--text.error, .v-alert.error { background: var(--danger-soft) !important; color: var(--danger) !important; }
.v-alert.v-alert--text.info, .v-alert.info { background: var(--brand-soft) !important; color: var(--brand) !important; }
.v-tooltip__content { background: var(--ink); color: var(--surface-raised); font: var(--text-small); border-radius: var(--radius-sm); padding: 4px 8px; opacity: 1 !important; }
.v-snack__wrapper { box-shadow: var(--shadow-md) !important; border-radius: var(--radius-md); }
.v-progress-circular.primary--text, .v-progress-linear .primary { color: var(--brand) !important; }
.v-badge__badge { font: var(--text-label); }
}// ---------- Barre du haut et barre latérale ----------
// Crochets : `hb-topbar` sur le v-app-bar, `hb-nav-drawer` sur le v-navigation-drawer de l'application.
.v-application {
.v-app-bar.hb-topbar {
background: var(--surface-raised) !important; color: var(--ink);
box-shadow: var(--shadow-sm) !important; border-bottom: 1px solid var(--line);
.v-toolbar__title { font: var(--text-h2); color: var(--brand); }
.v-icon { color: var(--ink-muted); }
}
.hb-nav-drawer.v-navigation-drawer {
background: var(--nav) !important; background-image: none !important; border-right: 0; color: var(--nav-ink);
.v-list { background: transparent; }
.v-list-item, .v-list-item .v-icon { color: var(--nav-muted) !important; }
.v-list-item--active, .v-list-item--active .v-icon { color: var(--nav-active) !important; }
.v-list-item--active { background: var(--nav-active-bg); &::before { opacity: 0; } }
.v-list-item:hover { background: var(--nav-hover); color: var(--nav-ink) !important; }
}
.v-navigation-drawer--temporary.v-navigation-drawer--right { box-shadow: var(--shadow-md); max-width: 100%; }
.hb-drawer-nav ~ .hb-drawer-h { position: static; }
// Dialogue ou tiroir ouvert : voile clair et page floutée derrière
.v-overlay--active:not(.v-overlay--absolute) {
-webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
> .v-overlay__scrim { background-color: var(--ink) !important; opacity: .18 !important; will-change: auto; }
}
.v-bottom-sheet.v-dialog { overflow-y: auto; }
.v-bottom-sheet.v-dialog > .v-card, .v-bottom-sheet.v-dialog > .v-sheet { border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important; padding-bottom: env(safe-area-inset-bottom); }
}
@media (max-width: 959.98px) {
html:has(.v-dialog--active, .v-navigation-drawer--temporary.v-navigation-drawer--open) { overflow-y: hidden; }
}
.v-application .echarts { max-width: 100%; }// ---------- Mouvement des menus et dialogues ----------
.v-menu-transition-enter-active { transition: opacity 160ms cubic-bezier(.22, 1, .36, 1), transform 160ms cubic-bezier(.22, 1, .36, 1) !important; }
.v-menu-transition-leave-active { transition: opacity 90ms cubic-bezier(.65, 0, .35, 1), transform 90ms cubic-bezier(.65, 0, .35, 1) !important; }
.v-menu-transition-enter, .v-menu-transition-leave-to { opacity: 0; transform: translateY(-4px) !important; }
.v-menu-transition-enter-to, .v-menu-transition-leave { opacity: 1; transform: translateY(0) !important; }
.dialog-transition-enter-active, .dialog-transition-leave-active { transition: opacity 240ms cubic-bezier(.22, 1, .36, 1), transform 240ms cubic-bezier(.22, 1, .36, 1) !important; }
.dialog-transition-enter, .dialog-transition-leave-to { opacity: 0; transform: scale(.96) !important; }
.dialog-transition-enter-to, .dialog-transition-leave { opacity: 1; transform: scale(1) !important; }
@media (prefers-reduced-motion: reduce) {
.v-application .v-btn { transition: none; &:active { transform: none; } }
.v-menu-transition-enter-active, .v-menu-transition-leave-active, .dialog-transition-enter-active, .dialog-transition-leave-active { transition: none !important; }
}// ---------- Tableaux en cartes (moins de 600 px) ----------
.v-application .v-data-table--mobile {
> .v-data-table__wrapper tbody { gap: var(--space-3); padding: var(--space-3); }
.v-data-table__mobile-table-row {
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2) var(--space-3); padding: var(--space-3) var(--space-4);
background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-card);
}
.v-data-table__mobile-row {
display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; min-height: 0; min-width: 0; gap: 2px; text-align: left;
height: auto !important; padding: 0 !important; border-bottom: 0 !important;
}
.v-data-table__mobile-row__header { font: var(--text-label); color: var(--ink-muted); padding: 0; &:empty { display: none; } }
.v-data-table__mobile-row__cell { text-align: left; min-width: 0; overflow-wrap: break-word; }
&.hb-table-lead .v-data-table__mobile-row:first-child { grid-column: 1 / -1; }
&.hb-table-lead .v-data-table__mobile-row:first-child .v-data-table__mobile-row__header { display: none; }
&.hb-table-actions .v-data-table__mobile-row:first-child { grid-column: 1; grid-row: 1; }
&.hb-table-actions .v-data-table__mobile-row:last-child { grid-column: 2; grid-row: 1; justify-self: end; }
&.hb-table-lead.hb-table-actions .v-data-table__mobile-table-row { position: relative; }
&.hb-table-lead.hb-table-actions .v-data-table__mobile-row:first-child { grid-column: 1 / -1; padding-right: 44px !important; }
&.hb-table-lead.hb-table-actions .v-data-table__mobile-row:last-child { position: absolute; top: var(--space-2); right: var(--space-2); grid-column: auto; grid-row: auto; }
.v-data-table-header-mobile th { height: auto; padding: 0; border-bottom: 0 !important; }
.v-data-table-header-mobile__wrapper:not(:empty) { padding: var(--space-2) var(--space-3); }
.v-data-table-header-mobile__wrapper:empty { display: none; }
.v-data-table__expanded__row { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.v-data-table__expanded__content {
display: block; margin-top: calc(-1 * var(--space-3)); border: 1px solid var(--line); border-top: 0; border-radius: 0 0 var(--radius-lg) var(--radius-lg); overflow: hidden;
> td { display: block; width: 100%; }
}
}// ---------- Téléphone (moins de 600 px) ----------
@media (max-width: 599.98px) {
.v-application {
.v-card > .v-card__title { padding: var(--space-3) var(--space-4); flex-wrap: wrap; gap: var(--space-2); }
.v-card > .v-card__text { padding: var(--space-3) var(--space-4); }
.v-card > .v-card__actions { flex-wrap: wrap; }
.v-data-footer { flex-wrap: wrap; justify-content: center; padding: var(--space-2) var(--space-3); row-gap: var(--space-1); }
.v-data-footer__select { margin-left: 0; margin-right: var(--space-3); }
.v-data-footer__pagination { margin: 0 var(--space-3); }
.v-list-item__title, .v-list-item__subtitle { white-space: normal; overflow-wrap: break-word; }
.v-btn:not(.v-btn--round).v-size--default { height: var(--control-touch); }
.v-btn:not(.v-btn--round).v-size--small { height: 40px; }
.v-btn:not(.v-btn--round).v-size--x-small { height: 36px; padding: 0 var(--space-3); font-size: var(--fs-small); }
.v-btn.v-btn--icon.v-size--default { width: var(--control-touch); height: var(--control-touch); }
.v-btn.v-btn--icon.v-size--small { width: 40px; height: 40px; }
.v-btn.v-btn--icon.v-size--x-small { width: 36px; height: 36px; }
.v-text-field--outlined .v-input__control > .v-input__slot { min-height: var(--control-touch); }
.v-text-field--outlined .v-label { top: 14px; }
.v-text-field--outlined .v-label--active { transform: translateY(-20px) scale(.75); }
.v-text-field--enclosed.v-text-field--outlined:not(.v-text-field--solo) .v-input__prepend-inner,
.v-text-field--enclosed.v-text-field--outlined:not(.v-text-field--solo) .v-input__append-inner,
.v-text-field--enclosed.v-text-field--outlined:not(.v-text-field--solo) .v-input__prepend-outer,
.v-text-field--enclosed.v-text-field--outlined:not(.v-text-field--solo) .v-input__append-outer { margin-top: 12px; }
.v-chip.v-size--small, .v-chip.v-size--x-small { height: 26px; font-size: 13px; }
.v-list-item { min-height: var(--control-touch); }
.v-tab { padding: 0 var(--space-2); font-size: 14px; }
.v-dialog:not(.v-dialog--fullscreen):not(.v-bottom-sheet) {
margin: 0 !important; width: 100% !important; max-width: none !important; height: 100%; max-height: 100% !important; border-radius: 0 !important;
> .v-card { min-height: 100%; border-radius: 0 !important; }
> .v-card > .v-card__text { max-height: none !important; }
> .v-card > .v-card__actions {
position: sticky; bottom: 0; z-index: 1; flex-direction: column-reverse; align-items: stretch; gap: var(--space-2);
padding: var(--space-3) var(--space-4); background: var(--surface-raised); border-top: 1px solid var(--line);
.v-btn { margin: 0 !important; width: 100%; }
.spacer, .v-spacer { display: none; }
}
}
.v-navigation-drawer--temporary.v-navigation-drawer--right { width: 100% !important; }
.v-dialog:has(> .v-picker) { width: auto !important; height: auto; max-height: 90% !important; margin: var(--space-5) !important; border-radius: var(--radius-lg) !important; }
}
}// Hublot — design system v3. Habillage Vuetify 3 par les tokens (équivalent de vuetify.scss pour Vuetify 2).
// Chargé après les styles Vuetify, tokens.css et bundle.css. Vuetify 3 expose ses couleurs en variables `--v-theme-*`
// (triplets RGB) : les thèmes de dist/vuetify3/themes.ts les alimentent avec les mêmes valeurs que les tokens ;
// ce fichier aligne les formes (hauteurs, rayons, filets, surfaces) qui ne passent pas par le thème.
// Volontairement plus court que vuetify.scss : Vuetify 3 lit déjà la police, les rayons et les hauteurs dans settings.scss.
.v-application {
font-family: var(--font-sans);
font-size: var(--fs-body);
line-height: var(--lh-body);
color: var(--ink);
background: var(--surface);
font-variant-numeric: tabular-nums;
a { color: var(--link); }
// Typographie : les classes Vuetify gardent leur nom, le rendu suit le système
.text-h4, .text-h5 { font: var(--text-h1); letter-spacing: var(--ls-h1); color: var(--brand); }
.text-h6 { font: var(--text-h3); letter-spacing: 0; }
.text-body-1, .text-subtitle-1 { font: var(--text-body); letter-spacing: 0; }
.text-body-2, .text-caption { font: var(--text-small); letter-spacing: 0; }
.text-overline { font: var(--text-label); letter-spacing: 0; text-transform: none; color: var(--ink-muted); }
// Surfaces : carte posée sur la page grisée (filet + shadow-card), ce qui flotte (menus, dialogues) en shadow-md
.v-card:not(.v-card--flat) { background: var(--surface-raised); border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); }
.v-card-title { font: var(--text-h2); letter-spacing: var(--ls-h2); padding: var(--space-4) var(--space-5); }
.v-card-subtitle { font: var(--text-small); color: var(--ink-muted); padding: 0 var(--space-5) var(--space-3); opacity: 1; }
.v-card-text { font: var(--text-body); padding: var(--space-4) var(--space-5); }
.v-card-actions { padding: var(--space-3) var(--space-5); gap: var(--space-2); border-top: 1px solid var(--line); background: var(--surface-hover);
> :first-child:not(.v-spacer) { margin-right: auto; }
> :only-child { margin-right: 0; margin-left: auto; }
}
.v-overlay__content > .v-card { box-shadow: var(--shadow-md); border: 0; }
.v-menu > .v-overlay__content, .v-select__content, .v-autocomplete__content { border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-md); background: var(--surface-raised); }
.v-overlay__scrim { background: var(--ink); opacity: .18; }
// Boutons : hauteur et police suivent la densité ; un seul `color="primary"` par vue
.v-btn { text-transform: none; letter-spacing: 0; font-weight: 500; border-radius: var(--radius-md); transition: transform var(--duration-press) var(--ease-out), background-color 120ms ease;
&.v-btn--size-default { height: var(--control); padding: 0 var(--space-4); font-size: var(--fs-body); }
&.v-btn--size-small { height: var(--control-sm); padding: 0 var(--space-3); font-size: var(--fs-small); }
&.v-btn--icon.v-btn--size-default { width: var(--control); height: var(--control); }
&:active { transform: scale(.97); }
&:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
&.v-btn--variant-outlined { border-color: var(--line-strong); background: var(--surface-raised); color: var(--ink); .v-icon { color: var(--brand); } }
&.v-btn--variant-elevated.bg-primary { box-shadow: var(--shadow-sm); }
.v-icon { font-size: var(--icon); }
}
.v-btn--icon .v-icon { color: var(--ink-muted); }
@media (hover: hover) and (pointer: fine) {
.v-btn--variant-outlined:hover { background: var(--surface-hover); border-color: var(--ink-muted); }
.v-btn--icon:hover { background: var(--surface-sunken); .v-icon { color: var(--brand); } }
}
// Champs : filet visible au repos, foncé au survol, couleur métier au focus
.v-field--variant-outlined .v-field__outline { --v-field-border-width: 1px; --v-field-border-opacity: 1; color: var(--line-strong); }
.v-field--variant-outlined:hover .v-field__outline { color: var(--ink-muted); }
.v-field--variant-outlined.v-field--focused .v-field__outline { --v-field-border-width: 2px; color: var(--brand); }
.v-field--error .v-field__outline { color: var(--danger); }
.v-field { border-radius: var(--radius-md); font-size: var(--fs-body); }
.v-field__input { min-height: var(--control); padding-top: 0; padding-bottom: 0; color: var(--ink); }
.v-field__input::placeholder { color: var(--ink-faint); opacity: 1; }
.v-label { color: var(--ink-muted); opacity: 1; }
.v-field--focused .v-label { color: var(--brand); }
.v-messages { font: var(--text-small); color: var(--ink-muted); opacity: 1; }
.v-input--error .v-messages { color: var(--danger); }
.v-field--disabled .v-field__overlay { background: var(--surface-sunken); opacity: 1; }
// Tableaux : en-tête en retrait, lignes séparées par un filet, survol teinté
.v-table { background: var(--surface-raised); color: var(--ink); border-radius: var(--radius-lg);
> .v-table__wrapper > table {
> thead > tr > th { font: var(--text-label); color: var(--ink-muted); background: var(--surface-sunken); height: 36px; border-bottom: 1px solid var(--line); white-space: nowrap; }
> tbody > tr > td { height: var(--row); font-size: var(--fs-body); border-bottom: 1px solid var(--line); &.text-end { font-variant-numeric: tabular-nums; font-weight: 500; } }
> tbody > tr:last-child > td { border-bottom: 0; }
> tbody > tr.v-data-table__selected > td { background: var(--brand-soft); }
}
&.v-table--density-compact > .v-table__wrapper > table > tbody > tr > td { height: var(--control); font-size: var(--fs-small); }
}
.v-data-table-footer { border-top: 1px solid var(--line); font: var(--text-small); color: var(--ink-muted); }
@media (hover: hover) and (pointer: fine) { .v-table > .v-table__wrapper > table > tbody > tr:hover > td { background: var(--surface-hover); } }
// Listes, puces, onglets, alertes
.v-list { background: var(--surface-raised); color: var(--ink); padding: var(--space-1) 0; }
.v-list-item { min-height: var(--control); padding: 0 var(--space-4); }
.v-list-item--active { color: var(--brand); }
.v-list-item-subtitle { font: var(--text-small); color: var(--ink-muted); opacity: 1; }
.v-chip { border-radius: var(--radius-full); font-weight: 500; font-size: var(--fs-small); letter-spacing: 0; }
.v-chip.bg-success { background: var(--success-soft) !important; color: var(--success) !important; }
.v-chip.bg-warning { background: var(--warning-soft) !important; color: var(--warning) !important; }
.v-chip.bg-error { background: var(--danger-soft) !important; color: var(--danger) !important; }
.v-chip.bg-primary, .v-chip.bg-info { background: var(--brand-soft) !important; color: var(--brand) !important; }
.v-chip.bg-secondary { background: var(--slate-soft) !important; color: var(--slate) !important; }
.v-tabs { border-bottom: 1px solid var(--line); }
.v-tab { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: var(--fs-body); color: var(--ink-muted); min-width: 0; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md) var(--radius-md) 0 0; }
.v-tab--selected { color: var(--brand); background: var(--brand-soft); }
.v-tab__slider { background: var(--brand); height: 3px; }
.v-alert { border-radius: var(--radius-md); font: var(--text-body); border-left: 4px solid currentColor; }
.v-alert.bg-success, .v-alert.text-success { background: var(--success-soft) !important; color: var(--success) !important; }
.v-alert.bg-warning, .v-alert.text-warning { background: var(--warning-soft) !important; color: var(--warning) !important; }
.v-alert.bg-error, .v-alert.text-error { background: var(--danger-soft) !important; color: var(--danger) !important; }
.v-alert.bg-info, .v-alert.text-info { background: var(--brand-soft) !important; color: var(--brand) !important; }
.v-tooltip > .v-overlay__content { background: var(--ink); color: var(--surface-raised); font: var(--text-small); border-radius: var(--radius-sm); padding: 4px 8px; }
.v-snackbar__wrapper { box-shadow: var(--shadow-md); border-radius: var(--radius-md); }
// Barre du haut et barre latérale : mêmes crochets qu'en Vuetify 2 (`hb-topbar`, `hb-nav-drawer`)
.v-app-bar.hb-topbar { background: var(--surface-raised); color: var(--ink); box-shadow: var(--shadow-sm); border-bottom: 1px solid var(--line); .v-toolbar-title { font: var(--text-h2); color: var(--brand); } }
.hb-nav-drawer.v-navigation-drawer { background: var(--nav); color: var(--nav-ink); border-right: 0;
.v-list { background: transparent; }
.v-list-item, .v-list-item .v-icon { color: var(--nav-muted); }
.v-list-item--active, .v-list-item--active .v-icon { color: var(--nav-active); }
.v-list-item--active { background: var(--nav-active-bg); .v-list-item__overlay { opacity: 0; } }
.v-list-item:hover { background: var(--nav-hover); color: var(--nav-ink); }
}
}
// Téléphone (moins de 600 px) : tout passe en cible tactile, les dialogues plein écran
@media (max-width: 599.98px) {
.v-application {
.v-btn.v-btn--size-default { height: var(--control-touch); }
.v-btn--icon.v-btn--size-default { width: var(--control-touch); height: var(--control-touch); }
.v-field__input { min-height: var(--control-touch); }
.v-list-item { min-height: var(--control-touch); }
.v-dialog:not(.v-dialog--fullscreen) > .v-overlay__content { margin: 0; width: 100%; max-width: none; height: 100%; max-height: 100%; > .v-card { min-height: 100%; border-radius: 0; } }
}
}
@media (prefers-reduced-motion: reduce) { .v-application .v-btn { transition: none; &:active { transform: none; } } }theme.js (applications Vuetify) : token, syncVuetifyColors, applyActivity, applyTheme, initialTheme, applyDensity, chartPalette, watchSystemTheme, HublotVuetify2.
bundle.js (window.Hublot) : formatEUR, formatKg, formatPct, formatDate, formatTime, isoWeek, chartPalette, deviceDensity, setMode, getMode, savedTheme, token, badgeTone.
/**
* Thème Hublot pour une application Vue 2 + Vuetify 2 (crm-front, magasin). Design system v3.
*
* Le design system expose des emplacements CSS (--brand, --accent, --nav…) qui pointent sur
* Poissonnerie par défaut et sur Boucherie quand <html data-activity="boucherie"> est posé (tokens.css).
* Vuetify 2 lit ses couleurs dans son thème JS : on aligne les deux ici, à partir des mêmes tokens,
* pour que « primary » et --brand soient toujours la même couleur.
*
* En JavaScript (et non TypeScript) pour être importable tel quel par un projet vue-cli ; les types sont dans theme.d.ts.
* Les attributs sur <html> sont posés par window.Hublot.setMode (bundle.js, chargé avant), commun à toutes les stacks.
*
* import vuetify from './plugins/vuetify'
* import { HublotVuetify2 } from '@hublot-io/core-ui/assets/design-system/theme'
* Vue.use(HublotVuetify2, { vuetify, activity: 'poissonnerie', density: 'compact' }) // puis this.$hublot.setActivity('boucherie')
*
* ou, sans plugin : applyActivity(vuetify, 'boucherie'), applyTheme(vuetify, initialTheme()), applyDensity('compact').
*/
const root = () => document.documentElement
const H = () => { if (!window.Hublot || !window.Hublot.setMode) throw new Error('theme.js : charger bundle.js avant theme.js'); return window.Hublot }
/** Valeur calculée d'un token (--brand, --accent…) une fois [data-activity] et [data-theme] appliqués. */
export const token = name => H().token(name)
/** Les deux thèmes Vuetify lisent les mêmes tokens : « primary » et --brand restent une seule couleur. */
export const syncVuetifyColors = vuetify => {
const colors = {
primary: token('brand'),
secondary: token('slate'),
accent: token('accent'),
info: token('info'),
success: token('success'),
warning: token('warning'),
error: token('danger'),
anchor: token('link')
}
Object.assign(vuetify.theme.themes.light, colors)
Object.assign(vuetify.theme.themes.dark, colors)
return colors
}
/** Applique le métier ('poissonnerie' | 'boucherie') : attribut sur <html> (CSS) puis thème Vuetify (JS). */
export const applyActivity = (vuetify, activity) => {
H().setMode({ activity })
syncVuetifyColors(vuetify)
return activity
}
/** Applique le thème ('light' | 'dark' | 'auto') : attribut sur <html> (tokens.css) puis mode sombre Vuetify, mémorisé pour la prochaine ouverture. */
export const applyTheme = (vuetify, theme) => {
const mode = H().setMode({ theme })
vuetify.theme.dark = mode.theme === 'dark'
syncVuetifyColors(vuetify)
return theme
}
/** Thème mémorisé par l'utilisateur, sinon 'auto' (le thème du système, suivi en direct). */
export const initialTheme = () => H().savedTheme()
/** Densité ('normal' | 'compact' | 'touch') : compact pour le CRM interne (crm-front), touch pour la tablette en rayon, normal ailleurs. */
export const applyDensity = density => { H().setMode({ density }); return density }
/** Palette ECharts dans l'ordre chart-1 … chart-5 du design system, à recalculer au changement de thème ou de métier. */
export const chartPalette = () => H().chartPalette()
/** Suit le thème système quand le thème est 'auto'. Retourne une fonction d'arrêt. */
export const watchSystemTheme = vuetify => {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const on = () => { if (root().getAttribute('data-theme') === 'auto') { vuetify.theme.dark = mq.matches; syncVuetifyColors(vuetify) } }
mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on)
return () => (mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on))
}
/**
* Plugin Vue 2 : Vue.use(HublotVuetify2, { vuetify, activity, density, theme }).
* Expose this.$hublot (réactif) : { activity, theme, density, setActivity(), setTheme(), setDensity(), chartPalette() }.
*/
export const HublotVuetify2 = {
install (Vue, options = {}) {
const { vuetify } = options
if (!vuetify) throw new Error('HublotVuetify2 : passer l’instance Vuetify dans les options')
const state = Vue.observable({ activity: options.activity || 'poissonnerie', theme: options.theme || initialTheme(), density: options.density || 'normal' })
const api = {
get activity () { return state.activity },
get theme () { return state.theme },
get density () { return state.density },
setActivity (a) { state.activity = applyActivity(vuetify, a) },
setTheme (t) { state.theme = applyTheme(vuetify, t) },
setDensity (d) { state.density = applyDensity(d) },
chartPalette
}
applyDensity(state.density); applyActivity(vuetify, state.activity); applyTheme(vuetify, state.theme); watchSystemTheme(vuetify)
Vue.prototype.$hublot = api
Vue.hublot = api
}
}
/* Hublot — design system v3.3, helpers window.Hublot (formats, modes, mouvement). Script classique sans dépendance. */
/* @ds-bundle: {"format":4,"namespace":"Hublot","components":[{"name":"Button"},{"name":"TextField"},{"name":"Badge"},{"name":"StatTile"},{"name":"DataTable"},{"name":"SideNav"},{"name":"Tabs"},{"name":"Alert"},{"name":"TaskRow"},{"name":"EmptyState"}]} */
(function () {
var NBSP = ' ', NNBSP = ' ';
function group(intPart) { return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, NNBSP); }
function fixed(n, d) { var s = Math.abs(n).toFixed(d); var p = s.split('.'); return (n < 0 ? '-' : '') + group(p[0]) + (d ? ',' + p[1] : ''); }
var H = {
/** 1250 → "1 250,00 €" ; { ht:true } → "1 250,00 € HT" ; { perKg:true } → "18,90 €/kg" */
formatEUR: function (n, o) { o = o || {}; var s = fixed(Number(n) || 0, o.decimals == null ? 2 : o.decimals) + NBSP + '€'; if (o.perKg) s += '/kg'; if (o.ht) s += NBSP + 'HT'; return s; },
/** 1.25 → "1,250 kg" ; { decimals:1 } → "1,3 kg" */
formatKg: function (n, o) { o = o || {}; return fixed(Number(n) || 0, o.decimals == null ? 3 : o.decimals) + NBSP + 'kg'; },
/** 12.5 → "12,5 %" */
formatPct: function (n, o) { o = o || {}; return fixed(Number(n) || 0, o.decimals == null ? 1 : o.decimals) + NBSP + '%'; },
/** Date → "12/05/2022" ; { style:'list' } → "jeu. 12 mai" ; { style:'header' } → "12/05/2022 · Semaine N° 19" */
formatDate: function (d, o) {
o = o || {}; d = d instanceof Date ? d : new Date(d);
var days = ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'];
var months = ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'];
var dd = String(d.getDate()).padStart(2, '0'), mm = String(d.getMonth() + 1).padStart(2, '0'), yyyy = d.getFullYear();
if (o.style === 'list') return days[d.getDay()] + ' ' + d.getDate() + ' ' + months[d.getMonth()];
if (o.style === 'header') return dd + '/' + mm + '/' + yyyy + ' · Semaine N°' + NBSP + H.isoWeek(d);
return dd + '/' + mm + '/' + yyyy;
},
/** "16:03" */
formatTime: function (d) { d = d instanceof Date ? d : new Date(d); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); },
/** Numéro de semaine ISO 8601 */
isoWeek: function (d) { d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); var day = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - day); var y0 = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil(((d - y0) / 864e5 + 1) / 7); },
/** Palette ECharts dans l'ordre chart-1…chart-5, lue depuis les tokens du document */
chartPalette: function (el) { var cs = getComputedStyle(el || document.documentElement); return ['--chart-1', '--chart-2', '--chart-3', '--chart-4', '--chart-5'].map(function (v) { return cs.getPropertyValue(v).trim(); }); },
/** Courbes et durées de mouvement, pour Vue <transition> ou WAAPI */
motion: { easeOut: 'cubic-bezier(.23,1,.32,1)', easeMove: 'cubic-bezier(.77,0,.175,1)', easeDrawer: 'cubic-bezier(.32,.72,0,1)', easeSpring: 'linear(0, 0.009, 0.035 2.1%, 0.141 4.4%, 0.723 12.9%, 0.938 16.7%, 1.017 20.6%, 1.061 24.5%, 1.077 28.3%, 1.071 32.3%, 1.035 41.4%, 1.008 50.5%, 0.997 60.4%, 1)', press: 120, state: 180, tooltip: 150, menu: 220, drawer: 360 },
/** Modes du design system, posés sur <html> (ou l'élément donné) : { activity, theme, density }. Sans Vuetify ; theme.js s'en sert et synchronise Vuetify 2. */
MODES: { activity: ['poissonnerie', 'boucherie'], theme: ['light', 'dark', 'auto'], density: ['normal', 'compact', 'touch', 'auto'] },
/** Densité de l'appareil : 'touch' quand le pointeur principal est le doigt (tablette, iPhone, iPad sans clavier), sinon 'normal'. */
deviceDensity: function () { return window.matchMedia && window.matchMedia('(pointer: coarse)').matches ? 'touch' : 'normal'; },
/** Pose les modes sur <html> (ou l'élément donné). density 'auto' → 'touch' ou 'normal' selon le pointeur, et suit le changement (iPad qui reçoit un clavier à pavé).
Un changement de thème passe par un fondu enchaîné (View Transitions) quand le navigateur le permet et que l'utilisateur n'a pas réduit les animations. */
setMode: function (mode, el) {
el = el || document.documentElement; mode = mode || {};
['activity', 'theme', 'density'].forEach(function (k) { if (mode[k] != null && H.MODES[k].indexOf(mode[k]) < 0) throw new Error('Hublot.setMode: ' + k + ' inconnu : ' + mode[k]); });
var apply = function () {
if (mode.activity != null) el.setAttribute('data-activity', mode.activity);
if (mode.theme != null) el.setAttribute('data-theme', mode.theme);
if (mode.density != null) el.setAttribute('data-density', mode.density === 'auto' ? H.deviceDensity() : mode.density);
};
var calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var themeChanges = mode.theme != null && mode.theme !== el.getAttribute('data-theme');
if (themeChanges && !calm && document.startViewTransition) document.startViewTransition(apply); else apply();
if (mode.density === 'auto' && window.matchMedia && !el.__hbDensityWatch) {
el.__hbDensityWatch = true;
window.matchMedia('(pointer: coarse)').addEventListener('change', function () { if (el.__hbDensityAuto) el.setAttribute('data-density', H.deviceDensity()); });
}
if (mode.density != null) el.__hbDensityAuto = mode.density === 'auto';
if (mode.theme != null) { try { localStorage.setItem('hublot-theme', mode.theme); } catch (e) {} }
return H.getMode(el);
},
/** Modes effectifs : lit les attributs, puis 'auto' → le thème système. */
getMode: function (el) {
el = el || document.documentElement;
var theme = el.getAttribute('data-theme') || 'light';
if (theme === 'auto') theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return { activity: el.getAttribute('data-activity') || 'poissonnerie', theme: theme, density: el.getAttribute('data-density') || 'normal' };
},
/** Thème mémorisé par l'utilisateur ('light' | 'dark' | 'auto'), sinon 'auto'. */
savedTheme: function () { try { var s = localStorage.getItem('hublot-theme'); return s === 'light' || s === 'dark' || s === 'auto' ? s : 'auto'; } catch (e) { return 'auto'; } },
/** Valeur calculée d'un token (--brand, --control…) sur l'élément donné : suit thème, métier et densité. */
token: function (name, el) { return getComputedStyle(el || document.documentElement).getPropertyValue('--' + name).trim(); },
/** Statut métier → variante de badge */
badgeTone: function (status) {
var s = String(status || '').toLowerCase();
if (/actif|payé|en ligne|conforme|terminé|signé/.test(s)) return 'success';
if (/vérifier|expire|bas|proche|attente|brouillon/.test(s)) return 'warning';
if (/impayé|hors ligne|erreur|non conforme|expiré|résilié/.test(s)) return 'danger';
if (/info|nouveau/.test(s)) return 'info';
return 'neutral';
}
};
/* Roue à la manière d'iOS : une colonne qui défile au doigt, à la molette ou au clavier, s'arrête net sur une valeur, en boucle si demandé.
Le DOM est construit ici ; l'apparence est dans bundle.css (hb-wheel, hb-timepicker, hb-time, hb-popover). */
const REPEAT = 9;
const pad2 = n => String(n).padStart(2, '0');
const range = (n, step) => Array.from({ length: Math.ceil(n / step) }, (_, i) => i * step);
const calm = () => window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/** H.wheel(el, { values, value, loop, label, format, onChange }) → { get value(), set(v), destroy() }.
values : tableau de valeurs ; format(v) : texte affiché ; onChange(v) appelé quand la roue s'arrête sur une nouvelle valeur. */
H.wheel = function (el, opts) {
const values = opts.values;
const n = values.length;
const loop = !!opts.loop;
const format = opts.format || String;
const copies = loop ? REPEAT : 1;
const middle = loop ? Math.floor(REPEAT / 2) * n : 0;
el.classList.add('hb-wheel');
el.setAttribute('role', 'spinbutton');
el.setAttribute('tabindex', '0');
el.setAttribute('aria-label', opts.label || '');
const track = document.createElement('div');
track.className = 'hb-wheel-track';
track.setAttribute('aria-hidden', 'true');
track.innerHTML = Array.from({ length: n * copies }, (_, i) => '<div class="hb-wheel-item" data-i="' + i + '">' + format(values[i % n]) + '</div>').join('');
el.textContent = '';
el.appendChild(track);
const items = Array.from(track.children);
const state = { h: 0, index: 0, last: -1, frame: 0, settle: 0, glide: 0 };
// Hauteur de mise en page (offsetHeight) : insensible aux transformations, dont l'animation d'ouverture de la bulle.
const itemHeight = () => items[0].offsetHeight || 36;
const indexOf = v => Math.max(0, values.indexOf(v));
const announce = () => {
const v = values[((state.index % n) + n) % n];
el.setAttribute('aria-valuenow', String(indexOf(v)));
el.setAttribute('aria-valuetext', format(v) + (opts.unit ? ' ' + opts.unit : ''));
return v;
};
// Cylindre : chaque ligne tourne et s'efface selon sa distance au centre.
const paint = () => {
state.frame = 0;
const center = el.scrollTop / state.h;
const from = Math.max(0, Math.floor(center) - 4);
const to = Math.min(items.length - 1, Math.ceil(center) + 4);
items.slice(from, to + 1).forEach((item, k) => {
const d = from + k - center;
const a = Math.min(Math.abs(d), 3);
item.style.transform = 'rotateX(' + (-d * 20).toFixed(2) + 'deg) scale(' + (1 - a * 0.045).toFixed(3) + ')';
item.style.opacity = (1 - a * 0.26).toFixed(3);
item.classList.toggle('is-selected', Math.abs(d) < 0.5);
});
const i = Math.round(center);
if (i !== state.last) {
if (state.last !== -1 && navigator.vibrate) navigator.vibrate(3);
state.last = i;
}
};
// Déplacement programmé (appui sur une ligne, clavier, set) : animé ici plutôt que par scroll-behavior smooth, inégal selon les WebView Android.
// L'aimantation est suspendue pendant le trajet, sinon le navigateur ramène la roue sur une ligne à chaque image.
const scrollToIndex = (i, smooth) => {
const target = i * state.h;
cancelAnimationFrame(state.glide);
if (!smooth || calm() || Math.abs(target - el.scrollTop) < 1) { el.scrollTop = target; return; }
const from = el.scrollTop, start = performance.now(), duration = Math.min(420, 160 + Math.abs(target - from) * 1.2);
el.style.scrollSnapType = 'none';
const step = now => {
const t = Math.min(1, (now - start) / duration);
el.scrollTop = from + (target - from) * (1 - Math.pow(1 - t, 3));
if (t < 1) state.glide = requestAnimationFrame(step);
else { el.style.scrollSnapType = ''; el.scrollTop = target; }
};
state.glide = requestAnimationFrame(step);
};
// À l'arrêt : retient la valeur et, en boucle, revient discrètement sur la copie du milieu.
const settle = () => {
state.settle = 0;
const i = Math.round(el.scrollTop / state.h);
const rel = ((i % n) + n) % n;
if (loop && (i < 2 * n || i > (REPEAT - 2) * n)) scrollToIndex(middle + rel, false);
const before = values[((state.index % n) + n) % n];
state.index = loop ? middle + rel : i;
const v = announce();
if (v !== before && opts.onChange) opts.onChange(v);
};
const onScroll = () => {
if (!state.frame) state.frame = requestAnimationFrame(paint);
clearTimeout(state.settle);
state.settle = setTimeout(settle, 110);
};
const onClick = e => { const item = e.target.closest('.hb-wheel-item'); if (item) scrollToIndex(Number(item.dataset.i), true); };
const onKey = e => {
const steps = { ArrowUp: -1, ArrowDown: 1, PageUp: -5, PageDown: 5 }[e.key];
const i = Math.round(el.scrollTop / state.h);
if (steps) { e.preventDefault(); scrollToIndex(i + steps, true); }
if (e.key === 'Home' || e.key === 'End') { e.preventDefault(); scrollToIndex(i - (((i % n) + n) % n) + (e.key === 'End' ? n - 1 : 0), true); }
};
const place = v => { state.h = itemHeight(); state.index = middle + indexOf(v); scrollToIndex(state.index, false); announce(); paint(); };
const resize = new ResizeObserver(() => place(values[((state.index % n) + n) % n]));
el.addEventListener('scroll', onScroll, { passive: true });
el.addEventListener('click', onClick);
el.addEventListener('keydown', onKey);
resize.observe(items[0]);
place(opts.value != null ? opts.value : values[0]);
return {
get value () { return values[((state.index % n) + n) % n]; },
set (v) { state.index = middle + indexOf(v); scrollToIndex(state.index, true); },
destroy () { cancelAnimationFrame(state.glide); resize.disconnect(); el.removeEventListener('scroll', onScroll); el.removeEventListener('click', onClick); el.removeEventListener('keydown', onKey); }
};
};
/** H.timePicker(el, { value: '14:30', step: 5, onChange }) : deux roues heures / minutes en boucle, comme l'horloge d'iOS. 24 h.
Émet aussi un évènement 'change' sur el, detail = 'HH:MM'. */
H.timePicker = function (el, opts) {
opts = opts || {};
const step = opts.step || 1;
const parts = String(opts.value || '00:00').split(':').map(Number);
const minute = Math.round((parts[1] || 0) / step) * step % 60;
const state = { h: parts[0] || 0, m: minute };
const emit = () => {
const value = pad2(state.h) + ':' + pad2(state.m);
el.dataset.value = value;
if (opts.onChange) opts.onChange(value);
el.dispatchEvent(new CustomEvent('change', { detail: value, bubbles: true }));
};
el.classList.add('hb-timepicker');
el.setAttribute('role', 'group');
el.setAttribute('aria-label', opts.label || 'Heure');
el.textContent = '';
el.appendChild(document.createElement('div'));
el.appendChild(document.createElement('div'));
const hours = H.wheel(el.children[0], { values: range(24, 1), value: state.h, loop: true, label: 'Heures', unit: 'h', format: pad2, onChange: v => { state.h = v; emit(); } });
const minutes = H.wheel(el.children[1], { values: range(60, step), value: state.m, loop: true, label: 'Minutes', unit: 'min', format: pad2, onChange: v => { state.m = v; emit(); } });
el.dataset.value = pad2(state.h) + ':' + pad2(state.m);
return {
get value () { return el.dataset.value; },
set (v) { const p = String(v).split(':').map(Number); state.h = p[0]; state.m = Math.round(p[1] / step) * step % 60; hours.set(state.h); minutes.set(state.m); el.dataset.value = pad2(state.h) + ':' + pad2(state.m); },
destroy () { hours.destroy(); minutes.destroy(); }
};
};
/** H.timeField(button, { value, step, onChange }) : la pastille d'heure d'iOS. Un appui ouvre les roues dans une bulle sous la pastille
(au-dessus si la place manque), la valeur change en direct ; un appui hors de la bulle, Échap, ou le défilement de la page la referme. */
H.timeField = function (button, opts) {
opts = opts || {};
const state = { value: opts.value || '00:00', pop: null, picker: null };
button.classList.add('hb-time');
button.setAttribute('aria-haspopup', 'dialog');
button.setAttribute('aria-expanded', 'false');
button.textContent = state.value;
const close = () => {
if (!state.pop) return;
state.picker.destroy();
state.pop.remove();
state.pop = null;
button.setAttribute('aria-expanded', 'false');
document.removeEventListener('pointerdown', outside, true);
document.removeEventListener('keydown', escape, true);
window.removeEventListener('scroll', close);
window.removeEventListener('resize', close);
button.focus({ preventScroll: true });
};
const outside = e => { if (state.pop && !state.pop.contains(e.target) && e.target !== button) close(); };
const escape = e => { if (e.key === 'Escape') { e.preventDefault(); close(); } };
const open = () => {
const pop = document.createElement('div');
pop.className = 'hb-popover';
pop.setAttribute('role', 'dialog');
pop.setAttribute('aria-label', opts.label || 'Choisir une heure');
const host = document.createElement('div');
pop.appendChild(host);
const scope = button.closest('[data-activity]');
if (scope && scope !== document.documentElement) pop.dataset.activity = scope.dataset.activity;
document.body.appendChild(pop);
state.pop = pop;
state.picker = H.timePicker(host, { value: state.value, step: opts.step, label: opts.label, onChange: v => {
state.value = v; button.textContent = v;
if (opts.onChange) opts.onChange(v);
button.dispatchEvent(new CustomEvent('change', { detail: v, bubbles: true }));
} });
const r = button.getBoundingClientRect();
const w = pop.offsetWidth, h = pop.offsetHeight, gap = 8;
const left = Math.min(Math.max(r.left + r.width / 2 - w / 2, gap), window.innerWidth - w - gap);
const below = r.bottom + gap + h <= window.innerHeight - gap;
pop.style.left = left + 'px';
pop.style.top = (below ? r.bottom + gap : Math.max(gap, r.top - gap - h)) + 'px';
pop.style.transformOrigin = (r.left + r.width / 2 - left) + 'px ' + (below ? 'top' : 'bottom');
button.setAttribute('aria-expanded', 'true');
document.addEventListener('pointerdown', outside, true);
document.addEventListener('keydown', escape, true);
window.addEventListener('scroll', close, { passive: true });
window.addEventListener('resize', close);
pop.querySelector('.hb-wheel').focus({ preventScroll: true });
};
const toggle = () => { if (state.pop) close(); else open(); };
button.addEventListener('click', toggle);
return {
get value () { return state.value; },
set (v) { state.value = v; button.textContent = v; if (state.picker) state.picker.set(v); },
close,
destroy () { close(); button.removeEventListener('click', toggle); }
};
};
window.Hublot = Object.assign(window.Hublot || {}, H);
})();