Files
mabibli/MaBibli.Client/wwwroot/css/app.css
T
mathieuandClaude Opus 5 7d2f3dc5d9 Rassemble au catalogue les tomes qui racontent la même histoire
Les livres d'une même série se suivaient au hasard de l'alphabet, et rien
ne disait qu'ils allaient ensemble. Ils tiennent maintenant sous le nom de
leur série, à la place qu'occupait le premier d'entre eux — l'ordre général
ne bouge donc pas — et ce nom mène à la série, seul écran qui montre aussi
les tomes qui manquent.

Le regroupement ne cache ni ne duplique rien : une bascule le défait, et
un livre rattaché à deux séries ne paraît qu'une fois, toujours sous la
même. La règle est une fonction pure, avec ses tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 00:16:52 +02:00

1713 lines
43 KiB
CSS

html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: absolute;
display: block;
width: 8rem;
height: 8rem;
inset: 20vh 0 auto 0;
margin: 0 auto 0 auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
/* ------------------------------------------------------------------
MaBibli — interface du catalogue (phase 3)
Conçue mobile d'abord : une seule colonne, cibles tactiles d'au moins
2,75 rem, aucune règle de largeur en dessous du point de rupture.
Le PC n'ajoute qu'une largeur maximale et une grille de cartes.
------------------------------------------------------------------ */
:root {
--mb-accent: #1b3a5c;
--mb-bord: #dcdcdc;
--mb-fond-doux: #f4f5f7;
--mb-texte-doux: #5c6470;
--mb-danger: #a4242c;
}
body {
background: #fff;
color: #1d2129;
}
.titre-page {
font-size: 1.4rem;
margin: 0 0 0.75rem;
}
/*
Ligne d'en-tête d'une fiche : le texte à gauche, les actions groupées à DROITE (lot T).
C'est la disposition déjà tenue par la bibliographie et la liste des auteurs. Reléguées en
bas de l'écran, les actions d'une saga de vingt tomes ne se découvraient qu'après avoir
déroulé toute la liste.
⚠️ `min-width: 0` sur la colonne de texte : sans lui, un titre long refuse de rétrécir et
pousse les actions hors de l'écran — le piège déjà rencontré au lot M1.
*/
.entete-fiche {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
}
.entete-fiche-texte {
flex: 1 1 12rem;
min-width: 0;
}
.entete-fiche-actions {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 0.4rem;
}
.message-discret {
color: var(--mb-texte-doux);
font-size: 0.9rem;
}
.message-erreur {
background: #fdecec;
border-left: 3px solid var(--mb-danger);
color: #7a1a20;
padding: 0.6rem 0.75rem;
border-radius: 4px;
margin: 0.75rem 0;
}
.message-avertissement {
background: #fff8e1;
border-left: 3px solid #c99700;
color: #6b5200;
padding: 0.6rem 0.75rem;
border-radius: 4px;
margin: 0.5rem 0;
font-size: 0.9rem;
}
.compteur {
color: var(--mb-texte-doux);
font-size: 0.85rem;
margin: 0.75rem 0 0.25rem;
}
/* --- Champs de saisie --- */
.barre-recherche {
margin-bottom: 0.75rem;
}
.champ {
display: block;
margin-bottom: 0.9rem;
border: 0;
padding: 0;
}
.champ-libelle {
display: block;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.3rem;
}
.champ-aide,
.obligatoire {
font-weight: 400;
color: var(--mb-texte-doux);
}
.champ-saisie {
width: 100%;
/* 16px minimum : en dessous, iOS zoome au focus. */
font-size: 1rem;
padding: 0.7rem 0.75rem;
border: 1px solid var(--mb-bord);
border-radius: 8px;
background: #fff;
}
.champ-saisie:focus {
outline: 2px solid var(--mb-accent);
outline-offset: -1px;
}
/* --- Segments (filtres, format, statut) --- */
.segments {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.segment {
flex: 1 1 auto;
min-height: 2.5rem;
padding: 0.4rem 0.7rem;
font-size: 0.85rem;
border: 1px solid var(--mb-bord);
border-radius: 999px;
background: #fff;
color: #1d2129;
cursor: pointer;
}
.segment-actif {
background: var(--mb-accent);
border-color: var(--mb-accent);
color: #fff;
}
.segments-compacts .segment {
min-height: 2.2rem;
font-size: 0.8rem;
}
.segment.statut-a-lire.segment-actif { background: #6b4fa8; border-color: #6b4fa8; }
.segment.statut-en-cours.segment-actif { background: #b06b00; border-color: #b06b00; }
.segment.statut-lu.segment-actif { background: #1f7a44; border-color: #1f7a44; }
/* Type de document : même vocabulaire que les pastilles de statut, deux couleurs distinctes
des trois du statut et de celle du prêt, pour qu'on reconnaisse ce qu'on filtre (A2, IDEES.md). */
/* Le segment « Prêtés » reprend la couleur de l'étiquette de prêt, comme les segments de
statut reprennent celle de leur pastille : le filtre et la carte doivent se répondre. */
.segment.segment-prete.segment-actif { background: #8a5a00; border-color: #8a5a00; }
.segment.type-roman.segment-actif { background: #2a6f97; border-color: #2a6f97; }
.segment.type-bd.segment-actif { background: #a13d63; border-color: #a13d63; }
/* --- Boutons --- */
.bouton {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.75rem;
padding: 0.6rem 1rem;
border-radius: 8px;
border: 1px solid var(--mb-bord);
background: #fff;
color: #1d2129;
font-size: 0.95rem;
text-decoration: none;
cursor: pointer;
}
.bouton:disabled {
opacity: 0.55;
cursor: default;
}
.bouton-principal {
background: var(--mb-accent);
border-color: var(--mb-accent);
color: #fff;
}
.bouton-discret {
background: var(--mb-fond-doux);
}
.bouton-danger {
background: var(--mb-danger);
border-color: var(--mb-danger);
color: #fff;
}
.actions-formulaire {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1rem 0;
}
/* Cette barre ne porte que des ACTIONS : la navigation est dans le menu du bandeau.
Depuis que ce menu est passé en haut (2026-08-20), plus rien ne l'accompagne en bas de
l'écran : elle s'y pose donc directement, sans décalage à réserver. */
.actions-flottantes {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.6rem 1rem calc(0.6rem + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
border-top: 1px solid var(--mb-bord);
}
/* Base de 8rem : au-delà de trois entrées la barre passe sur deux lignes plutôt que de
comprimer chaque libellé sur trois lignes de texte. Mesuré sur 375 px de large. */
.actions-flottantes .bouton {
flex: 1 1 8rem;
}
/* --- Couvertures --- */
.couverture {
position: relative;
flex: 0 0 auto;
overflow: hidden;
border-radius: 6px;
background: var(--mb-fond-doux);
border: 1px solid var(--mb-bord);
}
.couverture-petite { width: 3.5rem; height: 5.25rem; }
.couverture-moyenne { width: 6rem; height: 9rem; }
.couverture-substitut {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: #9aa2ad;
}
.couverture-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Posée par l'attribut onerror : les couvertures OpenLibrary renvoient
des 502 intermittents, l'image cassée laisse place au substitut. */
.couverture-image.image-cassee {
display: none;
}
/* --- Liste de livres --- */
.liste-livres,
.liste-candidats {
list-style: none;
margin: 0;
padding: 0;
}
/* --- Livres d'une même série, rassemblés au catalogue (2026-08-22) ---
Le bloc se pose là où son premier tome serait tombé dans l'ordre du catalogue : il ne
déplace donc rien, il rassemble. Le trait à gauche est ce qui fait lire les cartes comme
« dedans » plutôt que comme la suite de la liste. */
.groupe-serie {
margin: 0.5rem 0;
border-left: 3px solid var(--mb-accent);
padding-left: 0.6rem;
}
/* Le nom mène à la série : c'est le seul endroit du catalogue qui montre aussi ce qui manque. */
.groupe-serie-titre {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.4rem;
padding: 0.35rem 0;
color: var(--mb-texte);
text-decoration: none;
font-weight: 600;
}
.groupe-serie-titre:hover .groupe-serie-nom,
.groupe-serie-titre:focus-visible .groupe-serie-nom {
text-decoration: underline;
}
.groupe-serie-nom {
min-width: 0;
overflow-wrap: anywhere;
}
.groupe-serie-compte {
color: var(--mb-texte-doux);
font-size: 0.85rem;
font-weight: 400;
}
/* Une case à cocher parmi des rangées de segments : elle doit se lire comme un réglage, pas
comme un filtre de plus. */
.bascule {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.25rem 0;
font-size: 0.9rem;
}
.carte-livre,
.carte-candidat {
display: flex;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid var(--mb-bord);
}
.carte-corps {
min-width: 0;
flex: 1;
}
.carte-titre {
display: block;
font-weight: 600;
color: #1d2129;
text-decoration: none;
margin: 0 0 0.15rem;
}
.carte-auteur {
margin: 0 0 0.25rem;
color: var(--mb-texte-doux);
font-size: 0.9rem;
}
.carte-details {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
align-items: center;
margin: 0 0 0.5rem;
font-size: 0.8rem;
color: var(--mb-texte-doux);
}
.etiquette {
background: var(--mb-fond-doux);
border-radius: 4px;
padding: 0.1rem 0.4rem;
}
/* --- Choix entre plusieurs notices --- */
/* Les candidats partagent titre et auteur : ce sont l'éditeur et l'année
qui les départagent, donc ils sont mis en avant visuellement. */
.candidat-distinction {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0 0 0.4rem;
font-size: 0.9rem;
}
.candidat-editeur {
font-weight: 600;
}
.candidat-annee {
background: #eef2f7;
border-radius: 4px;
padding: 0.05rem 0.4rem;
}
/* --- Formulaire --- */
.formulaire-apercu {
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.zone-danger {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--mb-bord);
}
/* --- PC : simple élargissement, la structure mobile ne change pas. --- */
@media (min-width: 48rem) {
.actions-flottantes {
position: static;
padding: 0;
margin-top: 1.5rem;
background: none;
border-top: 0;
}
.actions-flottantes .bouton {
flex: 0 0 auto;
}
}
/* --- Prêts --- */
/* Le prêt est une information du foyer, pas de l'utilisateur : sa couleur est
volontairement différente de celles des statuts de lecture, qui sont personnels. */
.etiquette-prete {
background: #fdecd8;
color: #7a4a00;
font-weight: 600;
}
.bloc-prets {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--mb-bord);
}
.titre-section {
font-size: 1.1rem;
margin: 0 0 0.5rem;
}
.titre-sous-section {
font-size: 0.95rem;
margin: 1.25rem 0 0.35rem;
color: var(--mb-texte-doux);
}
.bandeau-pret {
background: #fff8ef;
border-left: 3px solid #c98a2e;
border-radius: 4px;
padding: 0.6rem 0.75rem;
margin: 0.5rem 0;
font-size: 0.95rem;
}
.pret-duree {
color: var(--mb-texte-doux);
font-size: 0.85rem;
white-space: nowrap;
}
.avertissement-fort {
display: block;
margin-top: 0.35rem;
font-weight: 600;
}
/* L'historique se lit, il ne se manipule pas : rien n'y est cliquable. */
.liste-historique {
list-style: none;
margin: 0;
padding: 0;
font-size: 0.9rem;
}
.liste-historique li {
padding: 0.4rem 0;
border-bottom: 1px solid var(--mb-bord);
}
.historique-dates {
display: block;
color: var(--mb-texte-doux);
font-size: 0.85rem;
}
/* Bouton d'action au sein d'une carte : il ne doit pas peser autant qu'un bouton de page,
tout en gardant une cible tactile utilisable. */
.bouton-compact {
min-height: 2.25rem;
padding: 0.35rem 0.8rem;
font-size: 0.85rem;
}
/* --- Scan du code-barres --- */
.scanner-cadre {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 4 / 3;
}
.scanner-video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Le viseur matérialise la bande réellement décodée : viser ailleurs ne sert à rien. */
.scanner-viseur {
position: absolute;
left: 5%;
right: 5%;
top: 27.5%;
height: 45%;
border: 2px solid rgba(255, 255, 255, 0.85);
border-radius: 6px;
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
}
.scanner-mesure {
font-variant-numeric: tabular-nums;
}
/* --- Agrandissement de la couverture --- */
/* Le déclencheur recouvre exactement la vignette. Il n'existe que quand il y a
une image : sans couverture, le substitut à initiale reste un décor inerte. */
.couverture-declencheur {
position: absolute;
inset: 0;
padding: 0;
border: 0;
background: none;
cursor: zoom-in;
}
/* Image cassée (404/502 intermittent) : le onerror du <img> désactive ce bouton.
Curseur neutre, pour ne plus promettre un agrandissement qui n'ouvrirait rien. */
.couverture-declencheur:disabled {
cursor: default;
}
.calque-couverture {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
background: rgba(0, 0, 0, 0.82);
cursor: zoom-out;
}
.calque-couverture:focus {
outline: none;
}
/* Bornée par la fenêtre : sur mobile la couverture occupe la largeur,
sur PC elle ne dépasse pas la hauteur utile. */
.calque-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 6px;
background: #fff;
cursor: default;
}
.calque-image.image-cassee {
display: none;
}
.calque-fermer {
position: absolute;
top: calc(0.5rem + env(safe-area-inset-top));
right: 0.5rem;
min-width: 2.75rem;
min-height: 2.75rem;
border: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.15);
color: #fff;
font-size: 1.1rem;
cursor: pointer;
}
/* --- Statut de lecture en lecture seule --- */
/* Pastille pleine, aux couleurs des segments actifs, pour ne pas se confondre avec
l'étiquette pâle du prêt : le statut est personnel, le prêt est celui du foyer. */
.etiquette-statut {
color: #fff;
font-weight: 600;
}
.etiquette-statut.statut-a-lire { background: #6b4fa8; }
.etiquette-statut.statut-en-cours { background: #b06b00; }
.etiquette-statut.statut-lu { background: #1f7a44; }
/* Pastille du type de document sur la carte du catalogue : mêmes couleurs que le
filtre (A2, IDEES.md), pour reconnaître d'un coup d'œil ce qui a été filtré. */
.etiquette-type {
color: #fff;
font-weight: 600;
}
.etiquette-type.type-roman { background: #2a6f97; }
.etiquette-type.type-bd { background: #a13d63; }
/* --- Fiche du livre en lecture seule --- */
.couverture-grande { width: 8rem; height: 12rem; }
.fiche-entete {
display: flex;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1rem;
}
.fiche-infos {
min-width: 0;
flex: 1;
}
.fiche-titre {
font-size: 1.15rem;
margin: 0 0 0.25rem;
}
.fiche-auteurs {
margin: 0 0 0.5rem;
color: var(--mb-texte-doux);
}
/* Deux colonnes « libellé / valeur » qui restent lisibles à 320 px. */
.fiche-champs {
margin: 0;
font-size: 0.9rem;
}
.fiche-champs div {
display: flex;
gap: 0.5rem;
padding: 0.15rem 0;
}
.fiche-champs dt {
flex: 0 0 5rem;
color: var(--mb-texte-doux);
}
.fiche-champs dd {
margin: 0;
min-width: 0;
word-break: break-word;
}
/* Le statut reste manipulable hors édition : c'est le geste le plus fréquent
de l'application, l'enfouir derrière « Éditer » serait un recul. */
.bloc-statut {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--mb-bord);
}
/* --- Auteurs --- */
/* Le nom d'un auteur est un point d'entrée, pas une décoration : il doit se voir comme
cliquable, et rester une cible atteignable au pouce dans une liste dense. */
.lien-auteur {
color: var(--mb-accent);
text-decoration: none;
padding: 0.1rem 0;
}
.lien-auteur:hover,
.lien-auteur:focus {
text-decoration: underline;
}
.liste-auteurs,
.liste-rapprochements {
list-style: none;
margin: 0;
padding: 0;
}
.carte-auteur-ligne {
display: flex;
gap: 0.5rem;
align-items: center;
min-height: 2.75rem;
padding: 0.4rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/*
Trois colonnes, dont une seule est élastique.
⚠️ C'est la largeur FIGÉE des deux colonnes extrêmes qui fait tout l'intérêt : sans elle,
chaque ligne plaçait son compteur et ses boutons à un endroit différent, au gré de la
longueur du nom, et l'œil devait relire chaque ligne entière. Trois chiffres suffisent au
compteur — personne ne possède mille livres d'un même auteur.
*/
.auteur-compte {
flex: 0 0 2.5rem;
text-align: right;
font-variant-numeric: tabular-nums;
font-size: 0.85rem;
font-weight: 600;
color: var(--mb-texte-doux);
}
.auteur-nom {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.auteur-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.4rem;
}
/* Le champ de renommage s'insère dans la colonne d'actions : il doit pouvoir rétrécir, sinon
il pousse « Enregistrer » hors de l'écran sur un téléphone. */
.champ-auteur-renomme {
min-width: 0;
max-width: 10rem;
}
.rapprochements {
margin-bottom: 1.5rem;
padding: 0.75rem;
border: 1px solid var(--mb-bord);
border-radius: 8px;
background: var(--mb-fond-doux);
}
.carte-rapprochement {
padding: 0.5rem 0;
border-top: 1px solid var(--mb-bord);
}
.rapprochement-noms {
margin: 0;
font-size: 0.95rem;
}
/* ─────────────────────────────────────────────────────────────────────────────
Liste d'envies et bibliographie par auteur.
Mobile d'abord : une colonne, l'action à droite du texte, rien qui déborde.
───────────────────────────────────────────────────────────────────────────── */
.bloc-souhait-ajout {
background: var(--mb-fond-doux);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
}
/* Le champ ISBN et son bouton sur une ligne : le bouton ne doit pas se rétrécir
au point de devenir intouchable au pouce quand le champ est large. */
.ligne-isbn {
display: flex;
gap: 0.5rem;
}
.ligne-isbn .champ-saisie {
flex: 1;
min-width: 0;
}
.ligne-isbn .bouton {
flex-shrink: 0;
}
.actions-export {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.liste-souhaits,
.liste-bibliographie {
list-style: none;
margin: 0;
padding: 0;
}
.carte-souhait,
.carte-oeuvre {
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 0.75rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/* ⚠️ La ligne de bibliographie, elle, est centrée : la case à cocher est à gauche du titre
et les boutons à droite, sur la même ligne. Alignés en haut, la case flottait au-dessus du
texte et les boutons pendaient sous lui — c'est ce qui rendait la liste illisible à coup
d'œil, alors qu'on la parcourt précisément pour cocher. */
.carte-oeuvre {
align-items: center;
}
/* La carte partagée d'un livre possédé sert de corps à la ligne : elle porte déjà sa propre
bordure et son propre rembourrage, qu'il faut neutraliser ici pour ne pas doubler le trait. */
.carte-oeuvre > .carte-livre {
flex: 1;
min-width: 0;
padding: 0;
border-bottom: 0;
}
.oeuvre-case {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
/* Cible tactile : la case native fait 1rem, on double la zone cliquable autour. */
padding: 0.4rem;
margin: -0.4rem 0;
cursor: pointer;
}
.oeuvre-case input {
width: 1.1rem;
height: 1.1rem;
cursor: pointer;
}
.oeuvre-actions {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
gap: 0.4rem;
}
/*
Barre d'actions de la sélection, EN HAUT de la liste et collante : on coche en descendant,
et une barre en bas d'écran obligeait soit à redescendre pour valider, soit — pire —
recouvrait les dernières lignes qu'on cherchait justement à cocher.
*/
.barre-selection {
position: sticky;
top: 0;
z-index: 5;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin: 0.75rem 0;
padding: 0.6rem 0.75rem;
background: var(--mb-fond-doux);
border: 1px solid var(--mb-bord);
border-radius: 6px;
}
.selection-compte {
font-size: 0.85rem;
font-weight: 600;
color: var(--mb-texte-doux);
}
/* Libellé réservé aux lecteurs d'écran : la case à cocher n'a plus de texte visible à côté
d'elle, mais elle doit continuer à dire ce qu'elle sélectionne. */
.lecture-ecran {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Le corps prend la place restante et « min-width: 0 » l'autorise à rétrécir :
sans lui, un titre long pousse le bouton hors de l'écran sur mobile. */
.carte-souhait .carte-corps,
.carte-oeuvre .carte-corps {
flex: 1;
min-width: 0;
}
.carte-souhait .bouton,
.carte-oeuvre .bouton {
flex-shrink: 0;
align-self: center;
}
/* Les deux actions d'une envie, empilées : « Éditer » au-dessus de « Retirer ».
Elles ne s'étalent pas en largeur — sur un téléphone, la colonne d'actions doit rester
étroite pour laisser le titre respirer. */
.souhait-actions {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-self: center;
gap: 0.35rem;
}
.souhait-note {
margin: 0.25rem 0 0;
color: var(--mb-texte-doux);
font-size: 0.9rem;
font-style: italic;
}
/* Le grisage des livres déjà possédés : c'est l'information principale de l'écran
de bibliographie. L'opacité seule suffirait visuellement, mais elle ne dit rien
à un lecteur d'écran — d'où l'étiquette « Déjà chez vous » qui l'accompagne
toujours dans le balisage. */
.oeuvre-possedee {
opacity: 0.55;
}
.oeuvre-possedee .carte-titre {
font-weight: 400;
}
.oeuvre-titre-bouton {
width: 100%;
padding: 0;
border: 0;
background: transparent;
text-align: left;
cursor: pointer;
}
.detail-oeuvre {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.5rem;
margin: 0.25rem 0 0;
}
.etiquette-possede {
background: #e3ece3;
color: #2f5d34;
font-weight: 600;
}
.etiquette-souhaite {
background: #e6e6f5;
color: #3b3b7a;
font-weight: 600;
}
/* --- Mise à jour de l'application --- */
/* Injecté par js/mise-a-jour.js, hors composants Blazor : ce bandeau doit pouvoir s'afficher
même si l'application WebAssembly n'a pas démarré. D'où une règle globale et non scopée.
Il existe parce que les empreintes des assets WASM sont désactivées (CLAUDE.md) : le service
worker est le seul cache-busting du projet, et sa mise à jour attendrait sinon la fermeture
de tous les onglets. */
#mb-maj {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1100;
display: flex;
gap: 0.75rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
padding: 0.7rem 1rem;
background: #1b3a5c;
color: #fff;
font-size: 0.9rem;
}
#mb-maj button {
min-height: 2.5rem;
padding: 0.4rem 0.9rem;
border: 0;
border-radius: 6px;
background: #f5d76e;
color: #4a3800;
font-weight: 600;
cursor: pointer;
}
/* Un ISBN se relit chiffre à chiffre : une chasse fixe évite de confondre 1 et l,
0 et O, et rend les tranches séparées par des tirets alignées d'une ligne à l'autre. */
/* ⚠️ PAS de « white-space: nowrap » ici, et ce n'est pas un oubli. Un ISBN à tirets mesure
147 px en monospace, là où la colonne des valeurs de la fiche n'en offre que 111 à 375 px et
56 à 320 px : « nowrap » le faisait déborder, et avec lui TOUTE la page — 395 px de contenu
pour un écran de 320, mesuré le 2026-08-21. Il annulait au passage le « word-break » dont
« .fiche-champs dd » est justement muni pour cela.
Le défaut de CSS est exactement ce qu'on veut : le code tient sur une seule ligne quand il y
a la place, et ne se coupe qu'à un tiret quand il n'y en a pas — le tiret étant le seul
endroit où une coupure se relit. */
.code-isbn {
font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
}
/* Titre d'écran et son bouton de réglages sur la même ligne : le bouton « Filtrer » du
catalogue appartient au titre, pas au contenu. */
.entete-page {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.entete-page .titre-page {
margin-bottom: 0.5rem;
}
/* --- Liste d'envies : ordre choisi --- */
/* Colonne de déplacement à gauche de chaque envie. Les flèches sont larges à dessein :
c'est le seul moyen de réordonner au doigt, le glisser-déposer HTML5 n'existant pas
sur mobile. */
.rang-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
flex: 0 0 auto;
}
.rang-actions .bouton {
padding: 0.15rem 0.5rem;
line-height: 1.1;
}
.rang-numero {
font-size: 0.75rem;
color: var(--mb-texte-doux);
font-variant-numeric: tabular-nums;
}
/* La carte saisie s'efface pendant le glissé : sans ce repère, on ne sait plus laquelle
on déplace une fois le curseur loin de son point de départ. */
.carte-glissee {
opacity: 0.4;
}
/* Trois manières de chercher, sur une ligne : réutilise l'apparence des segments du
catalogue plutôt que d'inventer un troisième style de bascule. */
.onglets-secondaires {
display: flex;
gap: 0.25rem;
margin: 0.75rem 0;
}
.onglets-secondaires .segment {
flex: 1 1 0;
}
/* --- Sagas, cycles et séries --- */
.liste-series,
.liste-tomes,
/*
Arbre des séries : une simple imbrication de listes, indentée par un liseré vertical.
⚠️ Volontairement dépouillé — pas de carte, pas de couverture, pas de tome. L'arbre répond
à « qui contient quoi », et l'écran d'une série répond au reste. La version précédente
dépliait la liste complète des livres de chaque nœud, ce qui noyait la hiérarchie qu'elle
était censée montrer.
*/
.arbre,
.arbre-enfants {
list-style: none;
margin: 0;
padding: 0;
}
.arbre-enfants {
/* Le liseré remplace les traits d'un arbre dessiné : il suffit à rattacher visuellement
une descendance à son parent, sans pseudo-éléments à positionner. */
margin-left: 0.7rem;
padding-left: 0.8rem;
border-left: 2px solid var(--mb-bord);
}
.arbre-ligne {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 2.25rem;
padding: 0.2rem 0;
/* ⚠️ Même piège qu'au lot M : la ligne est elle-même un élément de flex (son <li> l'est),
et « min-width: auto » lui interdit de descendre sous sa largeur intrinsèque. Sans cette
ligne, un titre de sous-série indenté pousse le compte « 0 sur 2 tomes » hors de l'écran
et fait défiler la page horizontalement (mesuré à 320 px : 328 px de large). */
min-width: 0;
}
.arbre-plier,
.arbre-puce {
flex: 0 0 1.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
height: 1.5rem;
padding: 0;
font-size: 0.9rem;
color: var(--mb-texte-doux);
background: transparent;
border: 0;
}
.arbre-plier {
cursor: pointer;
}
.arbre-titre {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
color: #1d2129;
text-decoration: none;
}
.arbre-titre:hover,
.arbre-titre:focus-visible {
text-decoration: underline;
}
.arbre-compte {
flex: 0 0 auto;
font-size: 0.8rem;
color: var(--mb-texte-doux);
white-space: nowrap;
}
.arbre-actions {
flex: 0 0 auto;
display: flex;
gap: 0.25rem;
}
.liste-sous-series {
list-style: none;
margin: 0.5rem 0;
padding: 0;
}
.carte-serie {
padding: 0.7rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/* Ligne entièrement cliquable (lot V) : le curseur doit le dire, sans quoi seul le titre a
l'air de mener quelque part. Le survol marque la ligne visée, utile à la souris comme au
clavier lorsque le lien de titre prend le focus. */
.carte-cliquable {
cursor: pointer;
}
.carte-cliquable:hover,
.carte-cliquable:focus-within {
background: var(--mb-fond-doux);
}
.liste-sous-series {
margin: 0.35rem 0 0 1rem;
font-size: 0.9rem;
}
.liste-sous-series li {
padding: 0.2rem 0;
display: flex;
gap: 0.5rem;
align-items: baseline;
flex-wrap: wrap;
}
.carte-tome {
display: flex;
gap: 0.6rem;
align-items: flex-start;
padding: 0.6rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/* Le tome absent est estompé, comme une œuvre déjà possédée l'est dans la bibliographie —
mais l'opacité ne dit rien à un lecteur d'écran, d'où l'étiquette qui l'accompagne. */
.tome-manquant .carte-titre {
font-weight: 400;
color: var(--mb-texte-doux);
}
.etiquette-manquant {
background: #f3e2e2;
color: #7a2f2f;
font-weight: 600;
}
.actions-tome {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.35rem;
}
/* Rattacher un livre à une place : la recherche s'ouvre SOUS le tome concerné, pour qu'on voie
ce qu'on est en train de compléter. */
.recherche-livre {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: var(--mb-fond-doux);
border-radius: 6px;
}
.recherche-livre .bouton {
text-align: left;
}
.fiche-serie {
margin: 0.2rem 0;
font-size: 0.9rem;
color: var(--mb-texte-doux);
}
/* --- Revues et numéros --- */
.liste-numeros {
list-style: none;
margin: 0.5rem 0;
padding: 0;
}
.carte-numero {
display: flex;
gap: 0.6rem;
align-items: flex-start;
justify-content: space-between;
padding: 0.6rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/* Numéro, date et note tiennent sur une ligne dès qu'il y a la place, et se replient sinon :
la saisie d'un numéro est répétitive, on ne veut pas la faire défiler. */
.champs-numero {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.champs-numero .champ {
flex: 1 1 8rem;
}
/* --- Rôles des auteurs (scénario, dessin, couleurs) --- */
.role-auteur {
color: var(--mb-texte-doux);
font-size: 0.85em;
}
/* ------------------------------------------------------------------
Menu principal (bandeau)
⚠️ Ces règles sont ICI, en feuille GLOBALE, et non dans
`Layout/MainLayout.razor.css`. Deux raisons :
1. `NavLink` est un composant : l'isolation CSS ne pose son attribut de
portée que sur le balisage du composant courant, jamais sur ce que rend
un composant enfant. Déclarés en feuille scopée, `.menu-lien` n'atteignait
AUCUN des six liens — le `<nav>` prenait son fond sombre, les liens
restaient en bleu souligné par défaut.
2. La navigation ne doit dépendre ni du bundle d'isolation ni de son cache.
Le repli sans style reste une suite de liens lisibles les uns sous les
autres : c'est tout l'intérêt du gabarit retenu le 2026-08-20.
------------------------------------------------------------------ */
/*
Bandeau et menu collent ENSEMBLE en haut de l'écran (lot R) : sur PC, la rangée des six
destinations défilait avec la page et devenait inatteignable dès qu'on descendait dans une
longue liste.
⚠️ Un seul conteneur collant, et non deux éléments collants superposés : ceux-ci auraient
obligé à écrire en dur la hauteur du bandeau dans le `top` du menu, hauteur qui varie avec
la pastille hors-ligne et le nom d'utilisateur.
*/
.entete {
position: sticky;
top: 0;
z-index: 10;
}
.menu {
display: none;
flex-direction: column;
background: #16304c;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
/*
Téléphone : le menu déployé occupe l'écran ENTIER.
Déployé sous le bandeau, il partageait l'écran avec la liste qu'on quittait, et l'on
choisissait sa destination au milieu d'autre chose. En plein écran, les six destinations se
lisent d'un coup d'œil.
⚠️ Le calque garde une porte de sortie explicite — la croix ci-dessous, Échap, et le clic
hors des liens (posés dans MainLayout.razor), en plus de la fermeture sur LocationChanged.
Sans elles, un plein écran est un piège.
*/
.menu-ouvert {
display: flex;
position: fixed;
inset: 0;
z-index: 50;
overflow-y: auto;
padding-bottom: 2rem;
/* Le calque prend le focus à l'ouverture : sans cela Échap ne lui parviendrait pas. Le
contour du navigateur n'apprend rien ici, la croix étant visible. */
outline: none;
}
.menu-entete {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.menu-titre {
color: #e8edf3;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* Cible tactile de 2.25rem, comme les boutons du bandeau : on la touche au pouce. */
.menu-fermer {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
padding: 0;
font-size: 1.15rem;
line-height: 1;
color: #fff;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 6px;
cursor: pointer;
}
.menu-fermer:hover,
.menu-fermer:focus-visible {
background: rgba(255, 255, 255, 0.15);
}
.menu-lien {
display: block;
padding: 0.7rem 1rem;
color: #e8edf3;
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
border-left: 4px solid transparent;
}
.menu-lien:hover,
.menu-lien:focus-visible {
background: rgba(255, 255, 255, 0.1);
color: #fff;
text-decoration: none;
}
/* NavLink pose « active » tout seul ; « Catalogue » utilise NavLinkMatch.All,
sans quoi il resterait allumé sur toutes les pages — son href est la racine. */
.menu-lien.active {
color: #fff;
font-weight: 600;
background: rgba(255, 255, 255, 0.16);
border-left-color: #f5d76e;
}
/*
« À propos » est une annexe, pas une septième destination : un filet la sépare des six
entrées de la bibliothèque, et elle se porte en retrait. Elle reste un lien de plein droit —
même cible tactile, même état actif — parce qu'on la touche au pouce comme les autres.
*/
.menu-lien-annexe {
margin-top: 0.35rem;
border-top: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(232, 237, 243, 0.8);
font-weight: 400;
font-size: 0.9rem;
}
/*
À partir de 40rem la rangée tient sans se comprimer, et la bascule n'a plus
de raison d'être. Le point de rupture est répété dans `MainLayout.razor.css`
(qui y masque la bascule) — les deux doivent rester identiques.
*/
@media (min-width: 40rem) {
.menu {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 0.25rem;
padding: 0 0.5rem;
}
/*
⚠️ Le plein écran est ANNULÉ ici : sans cela, un menu ouvert au doigt puis un
agrandissement de la fenêtre laisserait un calque plein écran sur un PC, sans bascule
pour le refermer (elle est masquée à partir de cette largeur).
*/
.menu-ouvert {
position: static;
overflow-y: visible;
padding-bottom: 0;
}
.menu-entete {
display: none;
}
.menu-lien {
padding: 0.6rem 0.9rem;
border-left: 0;
border-bottom: 3px solid transparent;
}
.menu-lien.active {
background: rgba(255, 255, 255, 0.12);
border-bottom-color: #f5d76e;
}
/* En rangée, un filet au-dessus ne séparerait rien : le trait passe à GAUCHE, et la marge
supérieure disparaît sous peine de désaligner l'entrée du reste de la rangée. */
.menu-lien-annexe {
margin-top: 0;
margin-left: 0.4rem;
padding-left: 0.9rem;
border-top: 0;
border-left: 1px solid rgba(255, 255, 255, 0.25);
}
}
/* --- Attente (lot L1) -------------------------------------------------
Un appel lent doit se voir : une recherche BnF coûte ~1 s par page, et
jusqu'à dix pages pour les nouveautés. Sans indicateur, l'écran se lit
comme planté et l'on reclique, ce qui relance l'appel.
------------------------------------------------------------------- */
.attente {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--mb-texte-doux);
font-size: 0.9rem;
margin: 0.75rem 0;
}
.attente-rondelle {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
border: 2px solid var(--mb-bord);
border-top-color: var(--mb-accent);
border-radius: 50%;
animation: attente-tourne 0.8s linear infinite;
}
@keyframes attente-tourne {
to { transform: rotate(360deg); }
}
/* Une animation perpétuelle est précisément ce que ce réglage demande d'éviter.
L'indicateur reste visible — c'est lui qui porte l'information, pas sa rotation. */
@media (prefers-reduced-motion: reduce) {
.attente-rondelle {
animation: none;
border-top-color: var(--mb-accent);
border-right-color: var(--mb-accent);
}
}
/* --- Articles à la une d'un numéro (lot O2) ---------------------------
Une couleur à part, parce que ce n'est pas un thème : un titre d'article
est unique à sa parution et ne se réutilise jamais, là où un thème est un
vocabulaire commun au catalogue.
------------------------------------------------------------------- */
.etiquette-une {
background: #eef2f7;
color: #24405e;
font-style: italic;
}
/* Saisie d'une liste (thèmes, auteurs, articles à la une) : une valeur par ligne, son texte
à gauche et ses deux actions à droite.
⚠️ min-width: 0 sur la valeur est ce qui fait tenir la ligne à 320 px — un titre d'article
est long, et sans lui il pousserait « Supprimer » hors de l'écran (même leçon que la ligne
de création du lot M1, et que l'arbre des séries). */
.liste-saisie {
list-style: none;
margin: 0.25rem 0 0;
padding: 0;
}
.ligne-saisie {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
padding: 0.35rem 0;
border-bottom: 1px solid var(--mb-bord);
}
.ligne-saisie-valeur {
flex: 1 1 8rem;
min-width: 0;
overflow-wrap: anywhere;
}
.ligne-saisie .actions-tome {
flex: 0 0 auto;
margin-top: 0;
}
/* Les rôles d'un auteur passent à la ligne sous son nom dès que la place manque : trois
segments et deux boutons ne tiennent pas sur une ligne de téléphone. */
.segments-role {
flex: 1 1 100%;
margin: 0;
}
/* --- Créer depuis une liste : champ + bouton sur une ligne (lot M1) ----
⚠️ min-width: 0 sur le champ est ce qui fait tenir la ligne à 320 px : sans
lui, un élément de formulaire refuse de rétrécir sous sa largeur intrinsèque
et pousse le bouton hors de l'écran. Le bouton, lui, ne rétrécit pas et son
libellé ne se coupe pas — un bouton tronqué ne se lit plus.
------------------------------------------------------------------- */
.ligne-creation {
display: flex;
gap: 0.5rem;
align-items: stretch;
margin-bottom: 0.75rem;
}
.ligne-creation .champ-saisie {
flex: 1 1 auto;
min-width: 0;
}
.ligne-creation .bouton {
flex: 0 0 auto;
white-space: nowrap;
}