/* * Système de tokens : le langage visuel de l'app s'inspire des étiquettes * d'inventaire et du geste du scanner code-barres (la ligne "laser" sous * le champ de scan, les rayures de code-barres en motif de rupture). * * Pas de police externe (Google Fonts, etc.) : le poste peut tourner sur * le réseau interne de la clinique, potentiellement sans accès internet. * Le "caractère" typographique vient du poids, du tracking et de * l'échelle plutôt que d'une police téléchargée. */ :root { --papier: #f5f3ec; /* fond, blanc "étiquette" chaud */ --papier-creux: #ebe8de; /* zones en creux (input, table stripe) */ --encre: #14181c; /* texte principal, quasi noir */ --acier: #5b6470; /* texte secondaire, bordures */ --acier-clair: #d8dbdf; /* bordures discrètes */ --brique: #b8402a; /* alerte stock bas */ --labo: #2e7d6b; /* stock ok / validation */ --ambre: #c67a17; /* accent focus / scan actif */ --police-corps: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; --police-donnee: ui-monospace, "SF Mono", "Cascadia Code", "Consolas", "Liberation Mono", monospace; } * { box-sizing: border-box; } body { font-family: var(--police-corps); max-width: 900px; margin: 0 auto 4rem; padding: 0 1rem; color: var(--encre); background: var(--papier); } h1, h2 { font-family: var(--police-corps); letter-spacing: -0.01em; } /* Focus clavier visible partout, indépendamment du style propre à chaque contrôle (accessibilité : ne jamais s'appuyer sur la seule couleur). */ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--ambre); outline-offset: 2px; } /* Motif de rupture "code-barres", réutilisé en petite touche (nav, en-tête du scanner, étiquette de résultat) : des barres de largeurs irrégulières en dégradé répété, sans image ni police à charger. */ .rayures-code-barre { height: 6px; background-image: repeating-linear-gradient( 90deg, var(--encre) 0px, var(--encre) 2px, transparent 2px, transparent 5px, var(--encre) 5px, var(--encre) 6px, transparent 6px, transparent 12px, var(--encre) 12px, var(--encre) 15px, transparent 15px, transparent 19px ); opacity: 0.85; } nav { display: flex; gap: 1.5rem; align-items: center; margin: 0 -1rem 2.5rem; padding: 1rem 1rem 0.85rem; background: var(--encre); } nav a { text-decoration: none; color: var(--papier); font-weight: 600; font-size: 0.85rem; letter-spacing: 0.06em; text-transform: uppercase; } nav a:hover { color: var(--ambre); } /* * Disclosure native (
/) : utilisée partout pour replier * par défaut ce qui n'est pas immédiatement utile (branches de * l'arborescence, formulaire d'édition d'un matériel). Zéro JavaScript, * et ça évite d'afficher d'un coup des centaines de champs de formulaire * quand une liste devient longue. */ details > summary { list-style: none; cursor: pointer; } details > summary::-webkit-details-marker { display: none; } .noeud-chevron { flex: none; width: 0.55em; height: 0.55em; border-right: 2px solid var(--acier); border-bottom: 2px solid var(--acier); transform: rotate(-45deg); transition: transform 0.15s ease; } details[open] > summary .noeud-chevron { transform: rotate(45deg); } .barre-recherche { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.25rem; } .barre-recherche input[type="search"] { flex: 1; max-width: 360px; background: var(--papier-creux); border: none; } .lien-effacer { font-size: 0.85rem; color: var(--acier); white-space: nowrap; } .liste-vide { color: var(--acier); font-style: italic; padding: 1rem 0.25rem; } /* Liste plate (résultats de recherche de catégories, destinataires). */ .liste-resultats { list-style: none; margin: 0 0 1.5rem; padding: 0; border-top: 1px solid var(--acier-clair); } .liste-resultats li { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.6rem 0.25rem; border-bottom: 1px solid var(--acier-clair); } .chemin-categorie { font-family: var(--police-donnee); font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* --------------------------------------------------------------------- Arborescence de catégories : profondeur illimitée, repliée par défaut. ------------------------------------------------------------------ */ .arbre-categories, .arbre-categories ul { list-style: none; margin: 0; padding: 0; } .arbre-categories ul { margin-left: 0.9rem; padding-left: 0.85rem; border-left: 1px solid var(--acier-clair); } .noeud-categorie { margin: 0.1rem 0; } .noeud-categorie > details > summary { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0.5rem; border-radius: 4px; } .noeud-categorie > details > summary:hover { background: var(--papier-creux); } .noeud-nom { font-weight: 600; } .noeud-compte { font-family: var(--police-donnee); font-size: 0.75rem; color: var(--acier); } .noeud-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; padding: 0.4rem 0.5rem 0.6rem 1.9rem; } .formulaire-racine { margin-bottom: 1.5rem; } /* --------------------------------------------------------------------- Liste des matériels : lignes compactes en lecture seule, le formulaire d'édition complet ne s'affiche qu'une fois la ligne dépliée. C'est ce qui évite le débordement horizontal d'un tableau avec un par colonne sur chaque ligne, y compris avec une longue liste. ------------------------------------------------------------------ */ .liste-materiels { list-style: none; margin: 0 0 2rem; padding: 0; border-top: 1px solid var(--acier-clair); } .liste-materiels > li { border-bottom: 1px solid var(--acier-clair); } .ligne-materiel > summary { display: grid; grid-template-columns: auto minmax(0, 2fr) minmax(0, 1.3fr) minmax(0, 1fr) auto; align-items: center; gap: 0.75rem; padding: 0.6rem 0.5rem; } .ligne-materiel > summary:hover { background: var(--papier-creux); } .materiel-nom, .materiel-categorie, .materiel-code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .materiel-nom { font-weight: 600; } .materiel-categorie, .materiel-code { font-size: 0.8rem; color: var(--acier); } .materiel-code { font-family: var(--police-donnee); } .materiel-quantite { font-family: var(--police-donnee); font-weight: 700; text-align: right; min-width: 2.5ch; } .materiel-quantite.quantite-basse { color: var(--brique); } .formulaire-edition { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.75rem; padding: 0.25rem 0.5rem 1.25rem 1.9rem; } .formulaire-edition label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.8rem; color: var(--acier); } .formulaire-actions { display: flex; gap: 0.5rem; align-items: end; } .formulaire-inline { display: inline-flex; gap: 0.5rem; align-items: center; } .formulaire-grille { display: grid; grid-template-columns: 1fr; gap: 0.75rem; max-width: 400px; } .bouton-danger { background: color-mix(in srgb, var(--brique) 12%, white); color: var(--brique); border: 1px solid color-mix(in srgb, var(--brique) 40%, white); } .message-erreur { color: var(--brique); } input, select, button { font-family: inherit; font-size: 1rem; padding: 0.5rem 0.6rem; border: 1px solid var(--acier-clair); border-radius: 4px; } button { cursor: pointer; border-color: var(--encre); background: white; } button:hover { background: var(--encre); color: var(--papier); } /* --------------------------------------------------------------------- Écran de scan : le seul endroit où on prend un vrai risque visuel. Le reste de l'app reste sobre pour ne pas diluer cette signature. ------------------------------------------------------------------ */ .scanner { max-width: 480px; margin: 0 auto; } .scanner-entete { text-align: center; margin-bottom: 2rem; } .scanner-eyebrow { font-family: var(--police-donnee); font-size: 0.75rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--acier); margin: 0 0 0.25rem; } .scanner-entete h1 { font-size: 1.75rem; font-weight: 800; margin: 0 0 1rem; } .scanner-formulaire { position: relative; margin-bottom: 1.5rem; } .scanner-label { display: block; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--acier); margin-bottom: 0.4rem; } .scanner-champ { width: 100%; font-family: var(--police-donnee); font-size: 1.5rem; letter-spacing: 0.05em; padding: 0.9rem 1rem; background: var(--papier-creux); border: none; border-radius: 4px 4px 0 0; } .scanner-champ::placeholder { color: var(--acier); opacity: 0.7; } /* * La "ligne laser" : un rappel direct du geste physique du scan (le * faisceau rouge d'une douchette code-barres). Immobile au repos, elle ne * s'anime que quand le champ a le focus, via le sélecteur adjacent * input:focus + .laser-ligne (pas besoin de JavaScript). */ .laser-ligne { height: 3px; background: var(--acier-clair); border-radius: 0 0 4px 4px; overflow: hidden; } .scanner-champ:focus + .laser-ligne { background: var(--ambre); animation: balayage-laser 1.6s ease-in-out infinite; } @keyframes balayage-laser { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } } .etiquette-erreur { color: var(--brique); font-weight: 600; text-align: center; margin-bottom: 1.5rem; } /* * L'étiquette de résultat : pensée comme une vraie étiquette d'inventaire * qu'on viendrait de scanner, avec les rayures de code-barres en tête et * une courte animation d'apparition ("tampon" qui vient de s'imprimer). */ .etiquette-resultat { background: white; border: 1px solid var(--acier-clair); border-radius: 4px; overflow: hidden; animation: apparition-etiquette 0.25s ease-out; } @keyframes apparition-etiquette { from { opacity: 0; transform: scale(0.97) translateY(4px); } to { opacity: 1; transform: scale(1) translateY(0); } } .etiquette-rayures { height: 10px; } .etiquette-resultat .etiquette-fil, .etiquette-resultat .etiquette-nom, .etiquette-resultat .etiquette-quantite, .etiquette-resultat .etiquette-statut { padding-left: 1.25rem; padding-right: 1.25rem; } .etiquette-fil { font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--acier); margin: 1rem 0 0.15rem; } .etiquette-nom { font-size: 1.3rem; font-weight: 800; margin: 0 0 1rem; } .etiquette-quantite { display: flex; align-items: baseline; gap: 0.5rem; } .etiquette-nombre { font-family: var(--police-donnee); font-size: 3rem; font-weight: 700; line-height: 1; } .etiquette-unite { color: var(--acier); } .etiquette-statut { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--acier); margin: 0.6rem 0 0; } .statut-puce { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .statut-ok { background: var(--labo); } .statut-bas { background: var(--brique); } .etiquette-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--acier-clair); margin-top: 1.25rem; } .etiquette-actions form { display: contents; } .bouton-scan { width: 100%; border: none; border-radius: 0; padding: 0.9rem; font-weight: 700; letter-spacing: 0.03em; background: white; } .bouton-scan:hover { background: var(--papier-creux); color: var(--encre); } .bouton-scan:active { transform: translateY(1px); } .bouton-reception { color: var(--labo); } .bouton-sortie { color: var(--brique); } /* Respecte la préférence système : pas d'animation pour qui la désactive. */ @media (prefers-reduced-motion: reduce) { .scanner-champ:focus + .laser-ligne { animation: none; } .etiquette-resultat { animation: none; } }