Design system Hublot v3 · catalogue généré depuis les fichiers du dossier

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.

Couleurs et ombres (tokens.css)

TokenClairSombreBoucherieUsage
--surface#f3f5f8#0f141b
--surface-raised#ffffff#161d27Cartes, panneaux, menus, lignes de liste et de tableau : ce qui est posé sur `surface`.
--surface-sunken#eceff3#0c1117Zones en retrait : en-têtes de tableau, groupes de champs, commentaires, fond de code/PLU.
--surface-hover#f6f8fa#1b2430Survol d'une ligne, d'un champ éditable au repos, d'un bouton neutre.
--line#dde3ea#243040Filets et séparateurs de lignes. Décoratif, jamais porteur de sens seul.
--line-strong#8794a3#637386Bordure des champs de saisie, cases à cocher, boutons neutres : ≥ 3:1 sur `surface-raised`, un champ se voit avant d'être cliqué.
--ink#1a2230#e6ebf1Texte principal, titres, chiffres.
--ink-muted#5b6675#9aa7b6Texte secondaire, libellés, métadonnées, icônes neutres.
--ink-faint#8b95a3#6c7886Placeholders, texte désactivé, indices d'édition : jamais pour une information indispensable.
--poissonnerie#376374#8ac3d5Couleur principale Poissonnerie, le bleu pétrole (logo, site, barre latérale).
--poissonnerie-hover#2b515f#a6d3e1Survol et appui du bouton principal Poissonnerie.
--poissonnerie-soft#e1eef2#8ac3d524Fond teinté derrière du texte `poissonnerie` : ligne sélectionnée, onglet actif, badge info.
--on-poissonnerie#ffffff#0f141bTexte et icônes sur un fond `poissonnerie`.
--poissonnerie-accent#8ac3d5#8ac3d5Couleur 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#8ac3d51fBandeau ou carte teintée Poissonnerie.
--on-poissonnerie-accent#12313a#0f141bTexte sur un aplat `poissonnerie-accent`.
--poissonnerie-nav#2f5563#0c1419Fond de la barre latérale Poissonnerie : la couleur principale, un ton plus sombre, comme sur la tablette et magasin.
--poissonnerie-nav-active#a6d3e1#8ac3d5Texte, 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-shadergba(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-strongrgba(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#f28b95Couleur principale Boucherie, le rouge bordeaux.
--boucherie-hover#7d0314#f7aab2Survol et appui du bouton principal Boucherie.
--boucherie-soft#f9e3e6#e4435324Fond teinté derrière du texte `boucherie`.
--on-boucherie#ffffff#0f141bTexte et icônes sur un fond `boucherie`.
--boucherie-accent#e44353#e44353Couleur secondaire Boucherie, le rouge vif : remplissage, repère, série 1. Jamais comme texte sur fond clair.
--boucherie-accent-soft#fbe6e8#e443531fBandeau ou carte teintée Boucherie.
--on-boucherie-accent#2a0508#1b0305Texte 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#1b1014Fond de la barre latérale Boucherie.
--boucherie-nav-active#f59aa3#f59aa3Texte, icône et repère de l'entrée active sur `boucherie-nav`.
--boucherie-shadergba(64, 12, 22, 0.07)rgba(0, 0, 0, 0.42)Teinte des ombres en Boucherie : bordeaux sombre.
--boucherie-shade-strongrgba(64, 12, 22, 0.15)rgba(0, 0, 0, 0.62)Teinte appuyée des ombres en Boucherie.
--brandvar(--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-hovervar(--poissonnerie-hover)var(--poissonnerie-hover)var(--boucherie-hover)Survol et appui de `brand`.
--brand-softvar(--poissonnerie-soft)var(--poissonnerie-soft)var(--boucherie-soft)Fond teinté derrière du texte `brand`.
--on-brandvar(--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.
--accentvar(--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-softvar(--poissonnerie-accent-soft)var(--poissonnerie-accent-soft)var(--boucherie-accent-soft)Bandeau ou carte teintée du métier.
--on-accentvar(--on-poissonnerie-accent)var(--on-poissonnerie-accent)var(--on-boucherie-accent)Texte sur un aplat `accent`, jamais blanc.
--navvar(--poissonnerie-nav)var(--poissonnerie-nav)var(--boucherie-nav)Fond de la barre latérale, sombre dans les deux thèmes.
--shadevar(--poissonnerie-shade)var(--poissonnerie-shade)var(--boucherie-shade)Ombre courte du métier (contact avec la surface).
--shade-strongvar(--poissonnerie-shade-strong)var(--poissonnerie-shade-strong)var(--boucherie-shade-strong)Ombre portée du métier (élévation).
--nav-ink#ffffff#e6ebf1Libellés et icônes de navigation sur `nav`.
--nav-mutedrgba(255, 255, 255, 0.78)#c3ccd6Entrées inactives sur `nav`.
--nav-hoverrgba(255, 255, 255, 0.10)rgba(255, 255, 255, 0.08)Survol d'une entrée sur `nav`.
--nav-activevar(--poissonnerie-nav-active)var(--poissonnerie-nav-active)var(--boucherie-nav-active)Texte, icône et barre de l'entrée active.
--nav-active-bgrgba(255, 255, 255, 0.12)rgba(255, 255, 255, 0.10)Fond de l'entrée active.
--infovar(--brand)var(--brand)var(--brand)Information neutre : bandeau, aide contextuelle. Suit le métier.
--slate#535b7c#aab4c0Bleu ardoise des titres du site : bouton secondaire plein, série 2, badge neutre teinté.
--slate-soft#eaecf2#1c2531Fond teinté derrière du texte `slate`.
--on-slate#ffffff#0f141bTexte sur `slate`.
--success#1e7a4c#6fd39dTerminé, payé, en ligne, conforme. Toujours accompagné d'un mot ou d'une icône.
--success-soft#e0f3e8#2f9e6429Fond d'un badge ou d'une alerte `success`.
--on-success#ffffff#0f141bTexte sur un aplat `success`.
--warning#9a5200#f0b068À vérifier, expire bientôt, stock bas, DLC proche.
--warning-soft#fdf0dc#d9822b2eFond d'un badge ou d'une alerte `warning`.
--on-warning#ffffff#0f141bTexte sur un aplat `warning`.
--danger#b3261e#f28b8bErreur, impayé, hors ligne, action destructive. Un bouton destructif reste en contour, jamais en aplat rouge.
--danger-soft#fbe9e7#d645452eFond d'un badge ou d'une alerte `danger`.
--on-danger#ffffff#0f141bTexte sur un aplat `danger`.
--link#25667a#8ac3d5Liens dans le texte, souligné.
--focus-ring#1a8bad#5cc6f2Anneau de focus clavier : 2 px d'écart puis 2 px pleins.
--chart-1var(--accent)var(--accent)var(--accent)Série 1 des graphiques ECharts : l'accent du métier.
--chart-2var(--slate)Série 2.
--chart-3var(--warning)Série 3.
--chart-4var(--success)Série 4.
--chart-5var(--brand)var(--brand)var(--brand)Série 5 : le `brand` du métier.
--chart-gridvar(--line)Grille et axes des graphiques.
--shadow-sm0 1px 1px var(--shade), 0 1px 3px var(--shade)Niveau 1 : bouton, touche, puce. Juste assez pour décoller du fond.
--shadow-card0 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-md0 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-lg0 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.

Espacements, formes et texte (tokens.css)

TokenNormalCompactTouchUsage
--space-14pxÉcart icône/texte, marge interne d'un badge.
--space-28pxÉcart entre contrôles d'une même ligne, marge interne des puces.
--space-312pxMarge interne des champs et cellules (desktop).
--space-416pxMarge interne des cartes, gouttière de page mobile, marge interne des champs (tablette).
--space-524pxGouttière de page desktop, entre deux cartes.
--space-632pxEntre deux sections d'une page.
--space-748pxEntre deux sections du site public, état vide.
--space-864pxMarge de section du site public.
--radius-sm4pxBadges, cases à cocher, cellules surlignées.
--radius-md8pxBoutons, champs, onglets, alertes, menus.
--radius-lg10pxCartes, panneaux, tuiles, listes.
--radius-full999pxPuces, avatars, le disque du logo.
--control36px34px48pxHauteur des boutons, champs et lignes denses. Suit la densité : 34 px en compact, 48 px en touch.
--control-sm30px30px44pxPetit bouton, bouton d'alerte. 44 px en touch : la cible tactile minimale.
--control-touch48pxBoutons, champs, lignes de liste et cases à cocher sur tablette (classes `-touch`, valable quelle que soit la densité).
--control-touch-lg56pxBouton d'action principal en pied d'écran tablette.
--row44px40px56pxLigne de tableau ou de liste : une sous-ligne y tient encore.
--icon20px18px24pxIcône dans un bouton ou une ligne.
--touch-target44pxTaille minimale d'une cible tactile, sur toutes les plateformes.
--sidenav248pxLargeur de la barre latérale dépliée.
--sidenav-rail64pxLargeur du rail replié (tablette, CRM réduit).
--content-max1280pxLargeur maximale du contenu d'une page CRM.
--topbar-height56pxHauteur de la barre du haut (`v-app-bar`).
--font-display"Paytone One", Roboto, system-ui, sans-serifPaytone One (Google Fonts, une graisse) : l'accroche, le titre d'écran d'accueil, le mot-symbole « Hublot ».
--font-sansRoboto, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serifRoboto 400, 500, 600 : tout le reste.
--font-mono"Roboto Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospaceCodes PLU, EAN, identifiants, SQL.
--fs-body15px13.5px17pxTexte courant.
--fs-small13px12.5px15pxMétadonnées, aides, légendes.
--fs-label13px12.5px15pxLibellé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-h124px24px28pxTitre de page.
--fs-h220px18px22pxTitre de section.
--fs-h316px15px18pxSous-titre.
--fs-number-xl32px28px36pxChiffre clé d'une tuile.
--fs-display32px32px36pxAccroche en Paytone One. Jamais en dessous de 24 px.
--fs-clock24pxHorloge de l'en-tête tablette.
--lh-body22px20px24px
--lh-small20px18px22px
--lh-label18px16px20px
--lh-h132px32px36px
--lh-h228px26px30px
--lh-h324px22px26px
--lh-number-xl36px34px40px
--lh-display40px40px44px
--lh-clock28px
--fw-regular400
--fw-medium500
--fw-semibold600Styles de texte composés : font: var(--text-h1) ; letter-spacing: var(--ls-h1)
--text-display400 var(--fs-display)/var(--lh-display) var(--font-display)Accroche, titre d'écran d'accueil tablette, mot-symbole « Hublot ». Un seul par page.
--ls-display0
--text-h1600 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-h2600 var(--fs-h2)/var(--lh-h2) var(--font-sans)Titre de section, de carte ou de dialogue.
--ls-h2-0.2px
--text-h3600 var(--fs-h3)/var(--lh-h3) var(--font-sans)Sous-titre, titre d'une ligne de liste ou de tâche.
--ls-h30
--text-body400 var(--fs-body)/var(--lh-body) var(--font-sans)Texte courant : formulaires, cellules, paragraphes.
--ls-body0
--text-body-strong500 var(--fs-body)/var(--lh-body) var(--font-sans)Titre d'une ligne de liste, nom dans une cellule.
--ls-body-strong0
--text-body-tablet400 17px/24px var(--font-sans)Texte courant sur tablette en rayon, quelle que soit la densité (équivaut à `body` en densité touch).
--ls-body-tablet0
--text-small400 var(--fs-small)/var(--lh-small) var(--font-sans)Métadonnées, aide sous un champ, légende. En `ink-muted`.
--ls-small0
--text-label500 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-label0
--text-number-xl600 var(--fs-number-xl)/var(--lh-number-xl) var(--font-sans)Chiffre clé d'une tuile, chiffres tabulaires.
--ls-number-xl-0.02em
--text-number500 var(--fs-body)/var(--lh-body) var(--font-sans)Montant ou poids dans une cellule, aligné à droite, chiffres tabulaires.
--ls-number0
--text-clock600 var(--fs-clock)/var(--lh-clock) var(--font-sans)Horloge de l'en-tête tablette.
--ls-clock0
--text-mono400 var(--fs-small)/var(--lh-small) var(--font-mono)Codes PLU, EAN, identifiants, SQL. Sur `surface-sunken`.
--ls-mono0Mouvement, plans, points de rupture (informatifs : un @media ne lit pas une variable)
--duration-press120msEnfoncement d'un bouton, d'une touche, d'une case.
--duration-state180msChangement d'état : couleur, fond, filet, survol, focus.
--duration-tooltip150msInfobulle.
--duration-menu220msMenu, toast, accordéon, apparition d'une ligne.
--duration-drawer360msPanneau latéral, dialogue.
--ease-outcubic-bezier(.23, 1, .32, 1)Tout ce qui entre (ease-out fort).
--ease-movecubic-bezier(.77, 0, .175, 1)Ce qui se déplace à l'écran.
--ease-drawercubic-bezier(.32, .72, 0, 1)Panneaux latéraux.
--ease-springlinear(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-sticky10En-tête collant, barre d'outils.
--z-nav20Barre latérale, barre du haut.
--z-dropdown100Menus, popovers, sélecteurs de date.
--z-drawer200Panneau latéral.
--z-modal300Dialogues et leur voile.
--z-toast400Toasts, snackbars.
--z-tooltip500Infobulles, au-dessus de tout.
--bp-sm600pxTéléphone au-dessous ; tablette portrait au-dessus (Vuetify `sm`).
--bp-md960pxTablette paysage, CRM avec rail (Vuetify `md`).
--bp-lg1280pxDesktop, barre latérale dépliée (Vuetify 3 `lg` ; Vuetify 2 utilise 1264 px).
--bp-xl1920pxGrand écran (Vuetify 3 `xl` ; Vuetify 2 utilise 1904 px).
--hb-ease-outvar(--ease-out)
--hb-ease-movevar(--ease-move)
--hb-ease-drawervar(--ease-drawer)

Classes hb-* (bundle.css)

/* 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
/* 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
/* 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.
/* 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.
/* 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.
/* 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`.
/* 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.
/* 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
/* 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)
/* 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.
/* 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.
/* 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.
/* 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`.
/* 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é)
/* 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é
/* 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é
/* 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.
/* 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
/* 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
/* 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
/* 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
/* 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.
/* 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.
/* 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.
/* 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.
/* 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
/* 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.
/* 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.
/* 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.
/* 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.
/* 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>.
/* 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.
/* 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)
/* 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.
/* É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).
/* 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.
/* 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>)
/* 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)
/* 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
/* 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).
/* 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).
/* 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.
/* 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é.
/* 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é.
/* 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`).
/* 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.
/* 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.
/* 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.
/* 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.
/* 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).
/* 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.
/* 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.
/* 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.
/* 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.
/* 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é.
/* 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).
/* 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).
/* 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.
/* 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`.
/* 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.
/* 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.
/* 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`.
/* 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.
/* 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`.
/* 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.
/* 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).
/* 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).
/* 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.
/* 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.
/* 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.
/* 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.
/* 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.
/* 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.
/* 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.
/* 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; }
}

Habillage Vuetify 2 (vuetify.scss)

En-tête
// 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
// ---------- 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
// ---------- 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
// ---------- 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
// ---------- 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
// ---------- 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
// ---------- 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
// ---------- 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
// ---------- 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)
// ---------- 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)
// ---------- 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; }
  }
}

Habillage Vuetify 3 (vuetify3/overrides.scss)

vuetify3/overrides.scss
// 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; } } }

Helpers

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.

theme.js
/**
 * 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
  }
}
bundle.js
/* 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);
})();