@charset "UTF-8";
/* Preferred: Dart Sass module system. */
b {
  font-weight: 500;
}

.mb-n3 {
  margin-bottom: -1rem !important;
}

.btn {
  --bs-btn-padding-x: 0.65rem;
}

.btn-sm {
  --bs-btn-padding-x: 0.48rem;
}

.btn-xs {
  --bs-btn-padding-y: 0.125rem;
  --bs-btn-padding-x: 0.4rem;
  --bs-btn-font-size: 0.75rem;
  --bs-btn-border-radius: var(--bs-border-radius-sm);
}

.btn-medium {
  min-width: 160px;
}

.btns-medium .btn {
  min-width: 160px;
}

.gx-lg-6 {
  --bs-gutter-x: 4rem;
}

/* Petit fix pour une icône pas très bien foutue. */
i.bi.bi-list-task {
  display: inline-block;
  transform: translateY(5%) scale(1.2);
}

.bi.bi-lock {
  display: inline-block;
  transform: scaleX(1.15);
}

/** Utilitaires pour les icônes personnalisées (ex. https://tabler.io/icons?icon=scale) */
.ti {
  display: inline-block;
  width: 1em;
  height: 1em;
  background-color: currentColor;
  -webkit-mask: var(--ti) no-repeat center/contain;
  mask: var(--ti) no-repeat center/contain;
  transform: scale(1.2);
  transform-origin: top;
}

i.ti-scale {
  --ti: url("/static/images/tabler-icons/scale.svg");
}

i.ti-robot-face {
  --ti: url("/static/images/tabler-icons/robot-face.svg");
}

body.home-page .tracker-home-link {
  display: none;
}

/** Largeur de page : mode classique borné, mode max pleine largeur.
 *  Écran ≤ 1920px → 100% (exploite tout l'écran). Écran très large (4K) →
 *  1920px centré, pas d'étalement. Le toggle de mode (banner.html
 *  #toggle-layout-switch → html.layout-max) retire le plafond. */
body.page-shell {
  --page-shell-gutter: 1%;
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--page-shell-gutter);
  max-width: 1920px;
}

/** Toggle de mode d'affichage (banner.html) : icône reflète l'état courant.
 *  Classique → flèches d'expansion (cliquer = passer en max).
 *  Max → flèches de rétrécissement (cliquer = revenir en classique).
 *  Swap piloté par html.layout-max, sans JS — l'anti-FOUC pose la classe
 *  avant le premier paint, donc bonne icône dès le chargement. */
.toggle-layout .icon-max {
  display: none;
}

/** Largeur du contenu selon le mode (layout-classic / layout-max).
 *  Conteneur unique enveloppant le bloc content de page.html : la largeur se
 *  règle à un seul endroit, valable pour toutes les pages. Les blocs internes
 *  restent à 100% et remplissent ce conteneur.
 *  Classique → 100% jusqu'au plafond --content-max-width, centré.
 *  Max → 100%, plein conteneur (densité client).
 *  Besoin d'une largeur différente sur UN bloc : poser max-width /
 *  --content-max-width sur ce bloc précis (on redescend à la demande). */
.content-width {
  --content-max-width: clamp(60rem, 80%, 100rem);
  position: relative;
  width: 100%;
  margin-right: auto;
  max-width: var(--content-max-width);
}

/** Largeur locale du bloc content (≠ mode global layout-max).
 *  État = classe `content-width--wide` sur le conteneur, posée par le bouton
 *  #content-width-toggle (coin haut-droit) ou auto au boot selon le nombre de
 *  champs (custom.js). Le bloc passe en max-width: none, ce seul bloc.
 *  Icône reflète l'état : expansion → clic élargit ; rétrécissement une fois
 *  élargi → clic resserre. Swap piloté par la classe, sans JS. */
.content-width--wide {
  max-width: none;
}

.content-width-toggle {
  position: absolute;
  z-index: 1;
  top: 0;
  right: 0;
  border: 0;
  background: transparent;
  color: #999;
  line-height: 1;
  padding: 0.25rem;
}
.content-width-toggle:hover {
  color: var(--bs-body-color);
}
.content-width-toggle .icon-contract {
  display: none;
}
.content-width--wide .content-width-toggle .icon-expand {
  display: none;
}
.content-width--wide .content-width-toggle .icon-contract {
  display: inline-block;
}

/** Alignement des champs du fieldset sur les encadrés de section.
 *  Les widgets de champ sont des colonnes Bootstrap (col-12 / col) dont le
 *  padding de gouttière (0.75rem) n'est PAS compensé quand le champ est posé
 *  hors d'un .row (champs pleine largeur) → ils rentrent de 0.75rem de plus
 *  que les encadrés audit/commentaires (simple p-3). On annule la gouttière,
 *  restaurée seulement sur les rangées multi-colonnes (.row-composite) qui en
 *  ont besoin pour espacer leurs colonnes. */
.content-width fieldset {
  --bs-gutter-x: 0;
}

.content-width fieldset .row-composite {
  --bs-gutter-x: 1.5rem;
}

/** Form rows à label fixe (.col-form-label) — utilisés dans le partial
 *  issue.item.fieldset.html. Label posé en colonne gauche, largeur figée
 *  140px : empêche la 2ème ligne de pousser la row en hauteur (sinon les
 *  rows suivantes décalent). Wrap autorisé en débordement visuel.
 *  L'override .field-row:not(:first-child) recadre à droite quand
 *  le label n'est pas dans la 1ère colonne d'une rangée multi-colonnes. */
label.col-form-label {
  padding: 0 5px 0 0;
  overflow: visible;
  white-space: normal;
  font-size: 90%;
}

#issue-edit-form label.col-form-label {
  width: 125px;
}

#issue-edit-form .CodeMirror,
#add-comment-form .CodeMirror {
  height: auto;
  min-height: 4em;
}

#issue-edit-form .CodeMirror-scroll,
#add-comment-form .CodeMirror-scroll {
  min-height: 4em;
  max-height: 30em;
}

#issue-edit-form .CodeMirror .CodeMirror-code .cm-header-2,
.card-body.markdown-preview-body h2 {
  font-size: 20px;
  line-height: 100%;
  text-transform: none;
  margin: 0.5em 0 0 0;
}

#issue-edit-form .CodeMirror .CodeMirror-code .cm-header-3,
.card-body.markdown-preview-body h3 {
  font-size: 18px;
  line-height: 100%;
  margin: 0.5em 0 0 0;
}

/* INTENT: [2026-05-17 16-05-00] [extract-fieldset-inline-css] Sortir le
   <style> inline du partial issue.item.fieldset.html (lignes 45–76). Bloc
   label.col-form-label + override .field-row:not(:first-child)
   relève du layout des form rows (largeur figée 140px, alignement à droite
   hors 1ère col) — déposé ici. Le bloc dropzone part en parallèle dans
   _components.scss (composant UI).
   DONE:   [2026-05-17 16-05-00] [extract-fieldset-inline-css] Bloc
   label.col-form-label ajouté en tête de _layout.scss avec header comment.
   Override .field-row:not(:first-child) label.col-form-label gardé
   non-nested (sélecteur descendant cross-component, pas un BEM enfant).
   `height: calc(...)` commenté du source supprimé (mort, jamais activé).
   Pas d'autre règle touchée. */
/** Écartement vertical des form-rows. Variable ajustable globalement ou
 *  scopée (form { --form-row-gap: 0.75rem }). Source de vérité = les
 *  data-attributs data-field (champ unique) / data-fields (composite,
 *  plusieurs champs côte à côte). La marge est cancellée sur les enfants
 *  directs d'un data-fields — ils héritent du rythme du parent et doivent
 *  rester collés à leur voisin de ligne. */
:root {
  --form-row-gap: 0.6rem;
}

[data-field],
[data-fields] {
  margin-bottom: var(--form-row-gap);
}

[data-fields] > [data-field] {
  margin-bottom: 0;
}

div.navbar {
  padding: 0.5rem 0 1rem 0;
}

/** Rappel permanent du mode maintenance : liseré orange épais sous la navbar.
 *  body.is-maintenance posé par layout/page.html quand le drapeau est actif.
 *  Seul l'admin navigue normalement pendant la maintenance (les autres voient
 *  la page maintenance) — le liseré n'apparaît donc que pour lui. */
body.is-maintenance div.navbar {
  border-bottom-width: 4px !important;
  border-bottom-color: var(--bs-orange, #fd7e14) !important;
}

/** Page layout — 2 colonnes leftnav / content de layout/page.html.
 *  - Leftnav : 260px préférée, rétrécit à 180px min sous la poussée du content (table large, widget).
 *  - Content : prend le reste.
 *  - Remplace col-3 / col-9, rigides : côté droit vide ou nav saturée à 25 %. */
.page-leftnav {
  flex: 0 1 360px;
  min-width: 280px;
  transition: flex-basis 0.2s ease, min-width 0.2s ease;
}

.page-content {
  flex: 1 1 0;
  min-width: 0;
  /** Padding horizontal piloté par variable — remplace les utilities ps-* responsive.
   *  clamp() fluide : confortable en grand écran, réduit en petit. Bornes à ajuster.
   *  Convention réutilisable (body, leftnav…) : un --<element>-padding-inline par élément,
   *  resserré sous html.layout-max (mode densité du client). */
  --page-content-padding-inline: clamp(0.75rem, 2vw, 3rem);
  padding-left: var(--page-content-padding-inline);
  padding-right: 1rem;
}

/** Repli de la barre latérale. Chevron #leftnav-toggle dans la colonne content (page.html).
 *  Classe html.leftnav-collapsed posée par l'anti-FOUC de page.html + le handler clic de custom.js.
 *  Barre repliée à 0, content reprend la place via son flex: 1 1 0 ci-dessus. */
.page-leftnav > * {
  transition: opacity 0.15s ease;
}
html.leftnav-collapsed .page-leftnav {
  flex-basis: 0;
  min-width: 0;
  padding-inline: 0;
  overflow: hidden;
}
html.leftnav-collapsed .page-leftnav > * {
  opacity: 0;
}

.leftnav-toggle {
  position: absolute;
  top: 8px;
  left: 0;
  background-color: var(--bs-body-bg);
  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
  transform: translateX(-100%);
  transition: transform 0.2s ease, left 0.2s ease;
}
.leftnav-toggle .leftnav-toggle-expand {
  display: none;
}
html.leftnav-collapsed .leftnav-toggle {
  transform: none;
  left: calc(-1 * var(--page-shell-gutter));
  box-shadow: none;
}
html.leftnav-collapsed .leftnav-toggle .leftnav-toggle-collapse {
  display: none;
}
html.leftnav-collapsed .leftnav-toggle .leftnav-toggle-expand {
  display: inline-block;
}

.sort-group-form {
  max-width: 1000px;
}

/* 
Mode max : réduction de divers espaces pour augmenter la quantité d'informations visible. 
*/
html.layout-max body.page-shell {
  max-width: none;
}
html.layout-max .content-width {
  max-width: none;
}
html.layout-max .toggle-layout .icon-classic {
  display: none;
}
html.layout-max .toggle-layout .icon-max {
  display: inline-block;
}
html.layout-max div.navbar {
  padding: 0.5rem 0 0 0;
}
html.layout-max .page-content {
  --page-content-padding-inline: clamp(0.25rem, 1vw, 1rem);
}

/* DONE: [2026-06-04] [rename-layout-modes] Renommage pleine-largeur → modes classique/compact.
   - Sélecteurs : html.layout-fluid → html.layout-compact (3 occurrences page-shell / icônes / page-content).
   - Wrapper .toggle-fullwidth → .toggle-layout ; icônes .icon-constrained → .icon-classic, .icon-fluid → .icon-compact.
   - layout-classic non encore stylé (défaut = base body.page-shell) ; posé sur html, réservé pour styles futurs. */
/* DONE: [2026-06-04] [rename-layout-modes] Classe d'état renommée layout-compact → layout-max (les autres « compact » inchangés). */
/* DONE: [2026-06-04] [rename-layout-modes] Alignement complet « compact » → « max » : sélecteur .icon-compact → .icon-max ; prose « mode compact » → « mode max ». */
/* DONE: [2026-06-05] [compose-immediate-mount] #add-comment-form ajouté aux règles min-height .CodeMirror (4em, plafond 30em) : compose box montée en immediate, plafonner la hauteur SimpleMDE par défaut (~300px). */
/* DONE: [2026-06-05] [content-toggle-zindex] z-index:1 sur .content-width-toggle : le form #issue-edit-form (position-relative, après le bouton dans le DOM) le couvrait, clic perdu. */
html {
  background-color: var(--c-html-bg);
}

/* Modales centrées verticalement par défaut (préférence projet). Réplique
   .modal-dialog-centered de Bootstrap sur toute .modal-dialog — plus besoin de
   poser la classe sur chaque modale. */
.modal-dialog {
  display: flex;
  align-items: center;
  min-height: calc(100% - var(--bs-modal-margin) * 2);
}

/* Un <form> enroulant .modal-content devient l'enfant flex direct (règle ci-dessus) et,
   sans width:100% — que Bootstrap ne pose que sur .modal-content —, se rétracte à son contenu :
   modale étriquée malgré modal-lg (ex. #transfer-filter-modal). Le forcer à remplir le dialog. */
.modal-dialog > form {
  width: 100%;
}

/* Padding des modales un cran plus généreux que le défaut Bootstrap (1rem, jugé chiche) — préférence projet.
   Posé sur --bs-modal-padding → header + body + footer suivent. */
.modal {
  --bs-modal-padding: 1.5rem;
}

/* DONE: [2026-06-03 16-40-00] [drop-iframe-modal] Règle .html-response-modal*
   retirée : l'iframe sandbox="" donnait une origine opaque (null) → polices
   bloquées CORS. ajax.js n'affiche plus de page d'erreur en overlay (redirection
   login ou nouvel onglet pour les tracebacks). Journaux iframe-fill retirés plus bas. */
.btn-medium {
  min-width: 10rem;
}

.navigation {
  border-right: none;
  box-shadow: inset -5px 0px 6px 0 rgba(0, 0, 0, 0.07);
}
.navigation .nav-header {
  font-size: 0.875rem;
  font-weight: 500;
  text-transform: uppercase;
  margin: 1em 0 1em 0;
  padding: 0 0.5rem;
  color: var(--bs-heading-color);
}
.navigation > ul > li > a {
  display: flex;
  align-items: flex-start;
  padding: 0.25rem 0.5rem;
  color: var(--bs-body-color);
  text-decoration: none;
  border-radius: 4px;
}
.navigation > ul > li > a:hover {
  color: #7ac;
}

/** Hover/focus option dans popup Tom Select ouvert.
 *  Redéclare la valeur vendor — pour stabilité face à un futur upgrade. */
.ts-dropdown .active {
  background-color: var(--bs-tertiary-bg);
  color: inherit;
}

/** Tom Select rend son .ts-dropdown dans <body> (dropdownParent, static/js/form_widgets.js).
 *  Dans une modale (z-index 1055), le dropdown passe dessous faute de z-index suffisant.
 *  Le remonter au-dessus de la modale (ex. sélecteur d'utilisateur de #transfer-filter-modal). */
.ts-dropdown {
  z-index: 1060;
}

/** Empêche un item long de pousser .ts-wrapper au-delà du parent.
 *  .ts-control est flex sans min-width:0 par défaut → l'item dicte la largeur
 *  et casse la grid Bootstrap. Ellipsis sur l'item, comme un <select> natif. */
.ts-wrapper {
  max-width: 100%;
  min-width: 0;
}

.ts-wrapper .ts-control {
  flex-wrap: nowrap;
  overflow: hidden;
}
.ts-wrapper .ts-control .item {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1 1 auto;
}

/** Plafond visuel du popup Tom Select. Au-delà, scroll interne au dropdown.
 *  ~14-16 options visibles selon le thème — augmenté du défaut vendor. */
.ts-dropdown-content {
  max-height: 24rem;
}

/** Méta-valeur "— any —" (option value="") grisée dans le control, cohérent
 *  avec le placeholder pâle des <input> de la même search row.
 *  - <select> natif : :has(option[value=""]:checked) cible le control entier.
 *  - Tom Select sélection vide : data-value="" recopié depuis value="" sur l'item rendu.
 *  - Tom Select sans sélection : placeholder dans l'<input> interne de .ts-control.
 *  Couleur partagée avec .form-control::placeholder via --placeholder-color. */
.ts-wrapper .ts-control input::-moz-placeholder {
  color: var(--placeholder-color);
}
.form-select:has(option[value=""]:checked),
.ts-wrapper .ts-control .item[data-value=""],
.ts-wrapper .ts-control input::placeholder {
  color: var(--placeholder-color);
}

/** Pour les select autocomplete (Tom Select), on veut que la saisie soit alignée à gauche et pas en centre. */
.ts-wrapper.tom-autocomplete .ts-control .item {
  flex: 0 0 auto;
}

/** Chips d'un multi-select Tom Select (`<select multiple>` → Tom Select pose la classe `.multi` sur le wrapper).
 *  Couvre data-widget="autocomplete-multi" (nosy) ET data-widget="tom-tags-input" (labels).
 *  - flex: 0 0 auto → chaque chip hug son texte (sinon flex: 1 1 auto hérité ci-dessus étirait « olivier × » sur toute la largeur du control).
 *  - background-color theme-aware → suit le swap light/dark sans override per-thème. */
.ts-wrapper.multi .ts-control .item {
  flex: 0 0 auto;
  background-color: var(--bs-tertiary-bg);
}

/** Propage le shrink jusqu'aux widgets enfants (Tom Select, <select> natif).
 *  Sans min-width:0, le .col Bootstrap s'étire pour matcher la largeur
 *  naturelle de l'item interne (option longue) et déforme la .row.
 *  Scopé aux wrappers de widgets du projet : .field-row (Tom Select)
 *  et [data-field] (toute row de champ rendue par m_widgets ou inline). */
.field-row .col,
[data-field] .col {
  min-width: 0;
}

/** Trigger BS collapse + chevron pivotant. Bootstrap bascule aria-expanded ;
 *  le CSS suit, pas de JS. Exemple :
 *    <label class="collapse-toggle" data-bs-toggle="collapse" data-bs-target="#X"
 *           aria-expanded="false">Texte<i class="bi bi-chevron-down ms-2"></i></label> */
.collapse-toggle {
  cursor: pointer;
}
.collapse-toggle .collapse-icon {
  display: inline-block;
  /* transform ne s'applique pas aux inline */
  transition: transform 0.2s ease;
}
.collapse-toggle[aria-expanded=true] .collapse-icon {
  transform: rotate(180deg);
}

/** Override de --bs-border-style seul : se combine avec .border Bootstrap
 *  du template (conserve color/width hérités du thème).
 *  Drop géré par dropzone_init.js ; click utilise le rôle natif <label for>. */
label.dropzone {
  --bs-border-style: dashed;
  min-height: 6rem;
  cursor: pointer;
  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
label.dropzone.is-dragover {
  background-color: var(--bs-tertiary-bg);
  border-color: var(--bs-primary) !important;
}

body.readonly #issue-edit-form input,
body.readonly #issue-edit-form select,
body.readonly #issue-edit-form textarea,
body.readonly #issue-edit-form .ts-wrapper {
  background-color: var(--readonly-bg);
}
body.readonly #issue-edit-form .form-select {
  background-image: none !important;
}
body.readonly #issue-edit-form .textarea-view {
  background-color: var(--readonly-bg);
  min-height: 4rem;
}

/** Étoile rouge auto sur label d'un champ required.
 *  Source = `required` HTML5 sur input/select/textarea (généré depuis schema.py).
 *  Zéro classe à poser côté template. */
.row:has(:required) > .col-form-label::after {
  content: " *";
  color: var(--bs-danger);
}

/** Alignement auto des labels dans un row composite : premier = text-start (default Bootstrap),
 *  suivants = text-end. Évite de poser label_class manuellement sur chaque champ.
 *  Caller : ajouter la classe `row-composite` sur le `<div class="row">` qui groupe les colonnes. */
.row-composite > [class*=col-lg-]:not(:first-child) .col-form-label {
  text-align: end;
}

/** Input avec une icône cliquable à droite (bouton-overlay).
 *  Réserve la place côté padding et stylise le bouton frère (couleur secondaire, darken au hover).
 *  Le positionnement du bouton est porté par les utilities Bootstrap côté template :
 *  position-absolute top-50 end-0 translate-middle-y.
 *  Pattern : <div class="position-relative">
 *              <input class="form-control input-icon-end">
 *              <button class="btn ..."></button>
 *            </div> */
.input-icon-end {
  padding-inline-end: 2.5rem;
}
.input-icon-end ~ .btn {
  color: var(--bs-secondary-color);
}
.input-icon-end ~ .btn:hover {
  color: var(--bs-body-color);
}

/** Dual rendering des selects : <select.widget-edit> (édition) vs <input.widget-view> (visualisation).
 *  Permet la sélection de texte (copier-coller) sur les valeurs en mode readonly.
 *  Pas de JS — le toggle est piloté par la classe `.readonly` posée sur <body>. */
body.readonly .widget-edit {
  display: none;
}

body:not(.readonly) .widget-view {
  display: none;
}

/** Champ qui n'a aucun sens en mode readonly (ex. uploader fichier) : disparaît entièrement. */
body.readonly .edit-only {
  display: none !important;
}

/* !important pour battre .d-flex de Bootstrap (utility avec !important par design). */
/* Visibilité : on voit toujours un seul des deux boutons, celui qui permet
   de basculer vers l'AUTRE mode. JS toggle .readonly sur <body>. */
body.readonly .mode-view {
  display: none;
}

body:not(.readonly) .mode-edit {
  display: none;
}

/* INTENT: [2026-05-13 16-30-00] [tom-select-listbox-mode] Quand Tom Select
   tourne sur un <select size="N">, le mode dropdown classique de TS (control
   input + popup en dessous) ne reproduit pas le rendu listbox natif. Ce mode
   listbox-mode masque le control et sort le dropdown du popup pour qu'il
   rende dans le flux comme une listbox.
   DONE:   [2026-05-13 16-30-00] [tom-select-listbox-mode] Règle ajoutée à la
   fin de _components.scss. Activée par la classe .ts-listbox-mode posée par
   le JS d'init sur tom.wrapper quand element.size > 1. */
/* INTENT: [2026-05-13 17-45-00] [custom-listbox] Retrait définitif de la
   règle .ts-wrapper.ts-listbox-mode — Tom Select n'est pas adapté au cas
   listbox dépliée (cf INTENT [tom-select-listbox-revert] dans
   static/js/tom_select_init.js). Remplacé par un widget custom léger basé
   sur <ul role="listbox"><li role="option">. CSS .custom-listbox ajouté.
   DONE:   [2026-05-13 17-45-00] [custom-listbox] Bloc .ts-wrapper.ts-listbox-mode
   supprimé. Bloc .custom-listbox ajouté avec __list (border, max-height ~13em,
   focus ring BS5) et __option (hover, aria-selected="true" → background
   primary). */
/* INTENT: [2026-05-13 19-25-00] [listbox-option-empty] Modificateur
   .custom-listbox__option--empty pour la ligne décor rendue en fin de
   listbox quand la macro listbox_choices est appelée avec empty_row=True.
   But : neutraliser les affordances cliquables (cursor pointer + hover) tout
   en gardant la hauteur d'une ligne (héritée de &__option). Le contenu
   &#160; côté template préserve la line-height. La ligne n'a pas
   role="option" donc le handler clic JS l'ignore — pas de risque
   d'interaction.
   DONE:   [2026-05-13 19-25-00] [listbox-option-empty] Bloc &--empty ajouté
   dans &__option avec cursor:default et &:hover { background-color:
   transparent } pour écraser le hover hérité. Pas de override sur
   aria-selected — la ligne n'est jamais sélectionnable (pas de role="option",
   ignorée par le handler clic). */
/* INTENT: [2026-05-13 20-56-00] [collapse-toggle-chevron] Règle générique
   .collapse-toggle pour tout trigger Bootstrap collapse avec chevron pivotant.
   But : déclarer cursor:pointer sur le trigger et faire pivoter 180° l'icône
   .bi-chevron-down enfant quand aria-expanded="true" (toggled par Bootstrap
   lui-même — pas de JS). Pose unique en fin de fichier, opt-in côté template
   via class="collapse-toggle". Première utilisation : <label> de la live
   preview Markdown dans issue.item.fieldset.html. */
/* DONE:   [2026-05-13 20-56-00] [collapse-toggle-chevron] Bloc .collapse-toggle
   ajouté en toute fin de fichier (3 règles imbriquées en syntaxe SCSS nested :
   cursor:pointer sur le sélecteur racine, transition transform sur
   .bi-chevron-down enfant, transform:rotate(180deg) sous le sélecteur
   parent[aria-expanded="true"]). Pas de variable Sass — durée (0.2s) en
   littéral. La règle suffit à animer toute icône .bi-chevron-down placée
   comme enfant d'un .collapse-toggle. */
/* INTENT: [2026-05-17 15-50-16] [listbox-system-accent] Remplacer le bleu
   Bootstrap (var(--bs-primary)) sur .custom-listbox__option par les CSS system
   colors. Hover et selected utilisaient --bs-primary, qui se résout en #0d6efd
   (light) ou #7ac (dark) — bleu jugé horrible, en particulier en dark mode.
   Cible : aligner sur la palette d'accent du système d'exploitation de
   l'utilisateur (Windows/macOS/Linux) — couleur native, dark-mode-aware,
   sans hardcoding RGB ni override par mode. */
/* DONE:   [2026-05-17 15-50-16] [listbox-system-accent] V1 (revert dans V2) :
   AccentColor + color-mix utilisés sur hover et selected. Échec en Chrome
   stable : AccentColor reste flag-gated dans Chromium depuis ~Chrome 115
   (2023), feature disabled by default. Règles barrées dans DevTools, hover
   inopérant. Voir V2 ci-dessous. */
/* INTENT: [2026-05-17 15-59-35] [listbox-highlight] Repli V1 sur les system
   colors CSS 2.1 universellement supportées : Highlight et HighlightText
   (couleur de sélection système, dispo Chrome/Firefox/Safari/Edge depuis
   toujours, contrairement à AccentColor flag-gated dans Chromium). Hover
   garde var(--bs-tertiary-bg) (gris neutre Bootstrap) pour ne pas confondre
   hover et selected — distinction visuelle entre « je survole » et « c'est
   l'item courant ». */
/* DONE:   [2026-05-17 15-59-35] [listbox-highlight] V2 (revert dans V3) :
   hover en gris Bootstrap, selected en Highlight. Casse l'alignement avec
   les <select> natifs voisins, qui peignent leur hover d'option en bleu OS
   (= Highlight). Incohérence visuelle entre listbox custom et select natif
   dans le même formulaire. Voir V3 ci-dessous. */
/* INTENT: [2026-05-17 16-04-00] [listbox-native-alignment] But initial de
   la série : aligner le widget custom listbox sur le rendu des <option>
   d'un <select> natif. Sur la plupart des OS, le hover et le selected
   d'une option native sont peints avec la même couleur (Highlight). Donc
   appliquer Highlight aux deux états sur le widget custom = même couleur
   que le <select> natif voisin = cohérence visuelle dans le formulaire. */
/* DONE:   [2026-05-17 16-04-00] [listbox-native-alignment] Bloc &__option
   simplifié : &:hover et &[aria-selected="true"] fusionnés sur la même
   règle (background Highlight, color HighlightText). Pas de distinction
   visuelle entre les deux états — c'est exactement le comportement du
   <select> natif déplié (size > 1). */
/* INTENT: [2026-05-17 16-05-00] [extract-fieldset-inline-css] Sortir le
   bloc dropzone du <style> inline du partial issue.item.fieldset.html
   (lignes 62–75). Composant UI à part entière (cible drag&drop pour les
   fichiers d'une issue) — convention déjà respectée par les autres
   composants du fichier (.html-response-modal, .custom-listbox,
   .collapse-toggle).
   DONE:   [2026-05-17 16-05-00] [extract-fieldset-inline-css] Bloc
   label.dropzone ajouté après .collapse-toggle, syntaxe nested SCSS
   (&.is-dragover imbriqué). Commentaires inline justifiant l'override de
   --bs-border-style et le !important sur border-color (écrasement de la
   --bs-border-color héritée de .border) déplacés dans le header comment du
   bloc — pas dupliqués sur les lignes. Pas de réécriture de propriétés. */
/* INTENT: [2026-05-17 16-39-06] [ts-wrapper-noexpand] Bug rapporté : quand
   l'option sélectionnée est longue (ex. label Severity traduit verbeux),
   Tom Select élargit .ts-wrapper au-delà de la largeur du conteneur
   parent (td, col Bootstrap), ce qui casse la grid. Cause : .ts-control
   est en flex sans min-width:0, donc l'item sélectionné impose sa largeur
   naturelle. Fix : max-width:100% + min-width:0 sur .ts-wrapper, et
   ellipsis sur .ts-control .item (comportement équivalent à un <select>
   natif dont la largeur est figée et qui clippe son label). */
/* DONE:   [2026-05-17 16-39-06] [ts-wrapper-noexpand] Bloc .ts-wrapper
   ajouté juste après .ts-dropdown .active : max-width:100%, min-width:0.
   Bloc .ts-wrapper .ts-control : flex-wrap:nowrap, overflow:hidden ;
   .item nested : overflow:hidden, text-overflow:ellipsis, white-space:
   nowrap, min-width:0, flex:1 1 auto. Pas de touche aux widgets custom
   listbox ni au .ts-dropdown.
   V2 : insuffisant. .ts-wrapper{max-width:100%} se résout à 100% du parent
   .col Bootstrap qui lui-même s'étire pour matcher la width naturelle du
   contenu (default flex item sans min-width:0). Cascade circulaire :
   l'item long pousse .ts-control → .ts-wrapper → .col. Ajout d'une règle
   pour casser la cascade au niveau du parent .col.
   V3 : V2 utilisait .col:has(>.ts-wrapper){min-width:0} — sur-précis et
   inhabituel ici. Le projet a déjà l'idiome .field-row (classe posée
   sur les rows de champ du formulaire). Utiliser .field-row .col
   {min-width:0} — plus simple, plus stable, et conforme aux conventions
   internes. Pas de souci d'impact : tous les .col internes à un
   .field-row ont vocation à respecter la largeur du conteneur. */
/* INTENT: [2026-05-17 16-16-32] [ts-dropdown-highlight] Override de la
   couleur d'option active dans le dropdown Tom Select. Le vendor pose
   var(--bs-tertiary-bg) (gris pâle peu visible) sur .ts-dropdown .active.
   V1 : Highlight/HighlightText pour aligner sur la V3 du widget custom
   listbox. Mauvais raisonnement — Highlight varie selon l'OS de chaque
   utilisateur (gris Windows, bleu macOS, etc.) → incohérence inter-users.
   V2 : revenir à var(--bs-primary)/var(--bs-light) (paire historique du
   widget custom listbox, en place depuis le 13 mai, jamais contestée).
   Cohérence cross-machine garantie.
   V3 : Tom Select utilise .active pour le HOVER (option survolée pointer
   ou focus clavier), pas pour le SELECTED. Le hover du widget custom
   listbox est var(--bs-tertiary-bg) (gris) depuis le 13 mai. Mettre du
   bleu sur le hover Tom Select crée une incohérence visible avec le
   hover gris du widget custom. Aligner Tom Select sur le gris custom :
   var(--bs-tertiary-bg) + color:inherit. */
/* DONE:   [2026-05-17 16-16-32] [ts-dropdown-highlight] V1 (Highlight) puis
   V2 (var(--bs-primary)/var(--bs-light)) puis V3 (var(--bs-tertiary-bg) +
   color:inherit) appliquées en cascade. Règle .ts-dropdown .active ajoutée
   juste après .custom-listbox. Spécificité égale au vendor, gagne par
   ordre de cascade. La V3 redéclare en pratique la valeur vendor — règle
   conservée explicite pour résister à un changement futur du vendor et
   pour montrer l'intention dans le SCSS. Le widget custom listbox
   (.custom-listbox__option) reste à son état pré-17/05 : hover
   var(--bs-tertiary-bg), selected var(--bs-primary) + var(--bs-light). */
/* INTENT: [2026-05-17 19-13-00] [terse-comments] Réécrire les commentaires
   d'en-tête de bloc qui paraphrasent la mécanique CSS/JS au lieu d'exposer
   le WHY non-évident. Cibles : .html-response-modal, .custom-listbox,
   &__list "pas de max-height", &__option--empty, .ts-dropdown .active,
   .ts-wrapper, .field-row .col, .collapse-toggle, .collapse-icon inline,
   label.dropzone.
   DONE:   [2026-05-17 19-15-00] [terse-comments] 9 blocs réécrits. Garde
   le WHY non-dérivable du code (raison, contrainte externe, hook JS) ;
   supprime la paraphrase de mécanique HTML/CSS/JS. Aucune règle CSS
   touchée — uniquement les commentaires. */
/* INTENT: [2026-05-19 05-26-00] [navbar-control-border] Aligner la bordure
   des widgets navbar (search input + bouton submit, toggle-theme switch)
   sur celle du bouton login (.btn-outline-secondary). Les boutons (search
   submit, lang selector) prennent .btn-outline-secondary inline dans le
   template — adaptation light/dark gérée par _theme.scss. L'input de
   recherche (.form-control) et le switch (.form-check-input) n'ont pas
   de classe BS qui descend jusqu'à --bs-secondary : on pose leur
   border-color sur la var custom --c-dim-border (cf _theme.scss),
   #6c757d en light et #adb5bd en dark. Cohérence visuelle des 4 widgets
   dans les 2 modes via une seule source de vérité.
   DONE:   [2026-05-19 05-26-00] [navbar-control-border] Bloc .navbar
   ajouté ci-dessous avec deux règles imbriquées : .navbar-search
   .form-control et .toggle-theme .form-check-input, toutes deux pointant
   sur var(--c-dim-border). */
/* INTENT: [2026-05-20 20-40-00] [unified-min-height-compact-form]
   - Aligner min-height .form-control / .form-select / .ts-wrapper sur la padding compacte.
   - .ts-wrapper garde son 38px vendor.
   - Inputs ~30px → écart visible 8px. */
/* DONE:   [2026-05-20 09-14-00]
 Rendre le tableau un peu plus compact parce qu'il contient de nombreux champs. */
form[name=itemSynopsis] th {
  font-weight: semi-bold;
}
form[name=itemSynopsis] th,
form[name=itemSynopsis] td {
  padding: 0.2rem 0.5rem;
}
form[name=itemSynopsis] input,
form[name=itemSynopsis] .ts-wrapper .ts-control {
  padding: 0.1rem 0.5rem;
}
form[name=itemSynopsis] input[type=checkbox],
form[name=itemSynopsis] input[type=radio] {
  padding: 0.1rem;
}
form[name=itemSynopsis] .form-control,
form[name=itemSynopsis] .form-select,
form[name=itemSynopsis] .ts-wrapper {
  min-height: calc(1.5em + 0.2rem + var(--bs-border-width) * 2);
}
form[name=itemSynopsis] .form-select {
  min-width: 0;
}

[data-is-custom] label {
  text-decoration-line: underline;
  text-decoration-color: rgba(0, 0, 0, 0.03);
  text-decoration-thickness: 3px;
  text-underline-offset: 3px;
}

/* DONE: [2026-05-20 20-40-00] [unified-min-height-compact-form]
   - min-height: calc(1.5em + 0.2rem + var(--bs-border-width) * 2).
   - Cible : .form-control, .form-select, .ts-wrapper.
   - 0.2rem = padding compacte top+bottom. À resync si la padding change.
   - Override 38px vendor par spécificité + ordre cascade. */
/** Tables sur fond carte — language visuel uniforme pour les <table> qui s'affichent
 *  comme des cartes (bordure + radius + th non-gras + alternance de gris) :
 *   - .history : audit log d'une issue (HTML généré par Roundup HTMLItem.history()) ;
 *   - table.striped-card : liste d'issues (template partagé html/issue.index.html).
 *  border-collapse:separate (défaut <table>=collapse) requis pour que border-radius
 *  rende les coins ; overflow:hidden = sécurité contre les contenus débordants. */
.history,
table.table-card,
table.striped-card {
  width: 100%;
  margin: 0;
  background: transparent;
  border: var(--bs-border-width) solid var(--c-table-border);
  border-radius: var(--bs-border-radius);
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
  --bs-table-striped-bg: transparent;
}
.history th,
table.table-card th,
table.striped-card th {
  font-weight: 500;
  padding: 0.5rem 0.75rem;
  border-color: var(--c-table-border);
}
.history td,
table.table-card td,
table.striped-card td {
  padding: 0.5rem 0.75rem;
  border-color: var(--c-table-border);
}
.history tr:last-child > *,
table.table-card tr:last-child > *,
table.striped-card tr:last-child > * {
  border-bottom: 0;
}

table.bg-dim {
  background: var(--c-bg-dim);
}

table.striped-card tr:nth-of-type(even) td {
  background: var(--c-bg-dim-alt);
}

table.striped-card.table-sm th,
table.striped-card.table-sm td {
  padding: 0.3rem 0.6rem;
  font-size: 0.875rem;
}

table.table.vertical-dense th {
  padding: 0.15rem 0.75rem !important;
  height: 2.1rem !important;
  vertical-align: middle !important;
}
table.table.vertical-dense td {
  padding: 0.05rem 0.75rem !important;
  height: 2rem !important;
  vertical-align: middle !important;
}

table.table thead tr th {
  background-color: var(--c-bg-thead);
  vertical-align: middle;
}

table.connections-table,
table.history-table {
  table-layout: fixed;
}

table.striped-card tr.group-spacer td {
  height: 1rem;
  background: transparent;
  border: none;
  padding: 0;
}

table.card-custom {
  width: 100%;
  background: transparent;
  border: var(--bs-border-width) solid var(--c-table-border);
  border-radius: var(--bs-border-radius);
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
}
table.card-custom tbody tr:first-child > * {
  box-shadow: inset 0 6px 6px -6px rgba(0, 0, 0, 0.5);
}
table.card-custom tbody tr:last-child > * {
  border-bottom: 0;
}
table.card-custom td,
table.card-custom th {
  padding: 0.5rem 0.75rem;
}
table.card-custom th {
  background: var(--bg-body);
}

table.table-with-groups td {
  background: var(--bs-body-bg);
  border-color: var(--c-strong-border);
}
table.table-with-groups td[data-fieldname=title] {
  padding-left: 1.5rem;
}
table.table-with-groups tr.group-separator th {
  border-top: 2px solid var(--bs-border-color);
  font-weight: 500;
  text-align: left;
  padding: 0.6rem 0.75rem 0.3rem 0.75rem;
  border-top: none;
  border-bottom: none;
  background: var(--c-bg-elevated);
}
table.table-with-groups tr.group-separator + tr td {
  box-shadow: inset 0 6px 6px -6px rgba(0, 0, 0, 0.25);
}

[data-fieldname=severity] {
  display: var(--grouped-severity-display, table-cell);
}

[data-fieldname=actor] {
  display: var(--grouped-actor-display, table-cell);
}

[data-fieldname=keyword] {
  display: var(--grouped-keyword-display, table-cell);
}

[data-fieldname=title] {
  display: var(--grouped-title-display, table-cell);
}

[data-fieldname=status] {
  display: var(--grouped-status-display, table-cell);
}

[data-fieldname=component] {
  display: var(--grouped-component-display, table-cell);
}

[data-fieldname=version] {
  display: var(--grouped-version-display, table-cell);
}

[data-fieldname=hardware] {
  display: var(--grouped-hardware-display, table-cell);
}

[data-fieldname=operating_system] {
  display: var(--grouped-operating_system-display, table-cell);
}

[data-fieldname=creator] {
  display: var(--grouped-creator-display, table-cell);
}

[data-fieldname=assignedto] {
  display: var(--grouped-assignedto-display, table-cell);
}

[data-fieldname=activity] {
  display: var(--grouped-activity-display, table-cell);
}

[data-fieldname=id] {
  display: var(--grouped-id-display, table-cell);
}

.sort-indicator {
  margin-left: 0.25rem;
}

.sort-indicator-secondary {
  opacity: 0.5;
}

/* INTENT: [2026-05-28 19-09-00] [sort-indicator-on-header] Styler l'indicateur de tri rendu par html/issue.index.html sur les <th> triés.
   DONE:   [2026-05-28 19-09-00] [sort-indicator-on-header] Règles .sort-indicator (margin-left) + .sort-indicator-secondary (opacity .5) ajoutées après le bloc @each de masquage des colonnes groupées. Pas de display:none — l'icône est rendue conditionnellement côté template, pas masquée par CSS. */
.sort-group-form .btn-check:checked + label .dir-asc,
.sort-group-form .btn-check:not(:checked) + label .dir-desc {
  display: none;
}
.sort-group-form .btn-check:checked + label {
  --bs-btn-active-color: var(--bs-body-color);
  --bs-btn-active-bg: var(--bs-tertiary-bg);
  --bs-btn-active-border-color: var(--bs-tertiary-bg);
}

.not-owned,
.not-owned td {
  color: var(--bs-secondary-color) !important;
}

.not-owned a:not(.btn) {
  color: inherit;
}

tr[data-archived] td {
  color: var(--bs-secondary-color);
  font-style: italic;
}

table th {
  font-weight: 500;
}

.history tr:has(th[colspan]) {
  display: none;
}

/* INTENT: [2026-05-21 11-00-00] [history-restyle] Re-style de la <table> rendue
   par context.history() pour adopter le langage visuel de la section Messages
   (wrapper border rounded + h5 côté template, ici on traite l'intérieur).
   Le HTML stock n'est pas accessible (génération côté Python dans templating.py)
   donc on cible par classe : .history pour le scoping.
   DONE:   [2026-05-21 11-00-00] [history-restyle] Bloc .history ajouté en fin
   de _components.scss. tr:has(th[colspan]) display:none pour cacher le titre
   redondant ; th font-weight:normal ; td sur fond c-bg-dim, tr:nth-of-type(odd)
   sur c-bg-dim-alt pour l'alternance. --bs-table-striped-bg=transparent
   neutralise le striped Bootstrap (sinon double-stripe). */
/* INTENT: [2026-05-21 12-00-00] [striped-card-factor] Étendre le langage visuel
   .history (table-comme-carte) à d'autres tables non générées par Roundup, en
   commençant par la liste d'issues (html/issue.index.html). Factoriser les
   règles communes sous un sélecteur conjoint (.history, table.striped-card) ;
   garder la règle spécifique tr:has(th[colspan])=display:none uniquement sur
   .history (titre redondant rendu par templating.py).
   DONE:   [2026-05-21 12-00-00] [striped-card-factor] Bloc .history refactoré
   en sélecteur conjoint avec table.striped-card. Règle spécifique .history
   pour cacher le titre rendue à part. Classe .striped-card à poser sur
   <table> dans les templates qui veulent ce rendu. */
/* DONE: [2026-05-23 18-15-00] [meta-value-grey-native-select] Règle "— any — grisé"
   étendue aux <select> natifs via `.form-select:has(option[value=""]:checked)`. Avant,
   seul Tom Select griserait son item value="" ; les selects natifs (priority/status/etc.
   sur issue.search.html) affichaient "— tous —" en noir comme une valeur ordinaire. */
/* DONE: [2026-05-27 17-30-00] [placeholder-color-share] Couleur de la règle ci-dessus
   passée de var(--bs-secondary-color) (gris moyen) à var(--placeholder-color) (#999
   défini dans _theme.scss :root). Alignée avec le placeholder des <input> de la même
   search row qui était plus clair — incohérence visuelle relevée sur issue.search.html. */
/* DONE: [2026-05-23 05-44-00] [drop-ro-namespace] Règle `.ro-select .col, .ro-input .col`
   remplacée par `[data-field] .col` (les wrappers de champ portent tous data-field, plus
   besoin du namespace `ro-*`). Ajout règle `form.readonly .edit-only { display: none }`
   pour les champs sans variante view (uploader fichier). */
/* DONE: [2026-05-28 06-45-00] [striped-card-sm] Variante `table.striped-card.table-sm` : padding 0.3rem 0.6rem + font-size 0.875rem. Bootstrap `.table-sm` seul écrasé par les paddings hardcodés du sélecteur conjoint. */
/* INTENT: [2026-05-28 06-32-00] [group-separator-style] Rendre le séparateur de groupe (tr.group-separator dans html/issue.index.html) clairement distinct des data rows. */
/* DONE:   [2026-05-28 06-32-00] [group-separator-style] Règle `table.striped-card tr.group-separator th` ajoutée après thead.
   - background c-bg-thead (cohérent avec le thead).
   - font-weight 600, text-align left (en-tête de section, pas centré).
   - padding 0.6rem 0.75rem, border-top 2px solid border-color (coupure visuelle avec le groupe précédent). */
/* INTENT: [2026-05-28 06-40-00] [hide-grouped-column] Masquer la colonne dont le champ sert au groupement (premier niveau) : info redondante avec le séparateur. */
/* DONE:   [2026-05-28 06-40-00] [hide-grouped-column] Boucle @each sur les fields de la table issue.index ajoutée après le bloc group-separator. Chaque [data-fieldname="X"] récupère display via var(--grouped-X-display, table-cell). Variable posée par html/issue.index.html sur le <table> quand request.group est présent. */
/* INTENT: [2026-05-28 07-00-00] [group-separator-color] Bandeau séparateur passé de gris pâle (c-bg-thead) à bleu-gris foncé avec texte blanc — démarcation forte. */
/* DONE:   [2026-05-28 07-00-00] [group-separator-color] background passé de c-bg-thead à c-group-separator-bg (nouvelle var dans _theme.scss : #546e7a light, #37474f dark). color #fff ajouté. */
/* INTENT: [2026-05-28 07-10-00] [group-spacer] Aérer la grille avec une ligne vide avant chaque séparateur de groupe. */
/* DONE:   [2026-05-28 07-10-00] [group-spacer] Règle `table.striped-card tr.group-spacer td` ajoutée après group-separator (height 1rem, background transparent, border/padding annulés). Row insérée côté template html/issue.index.html. */
/* DONE:   [2026-05-29 20-30-00] [ts-multi-chip] Règle `.ts-wrapper.multi .ts-control .item` ajoutée : `flex: 0 0 auto` (chip hug content au lieu d'étirer plein-largeur via le flex: 1 1 auto du parent) + `background-color: var(--bs-tertiary-bg)` (theme-aware light/dark). Couvre nosy (autocomplete-multi) ET labels (tom-tags-input). */
.search-page-header-title [data-mirror-from=queryname]:empty {
  display: none;
}
.search-page-header-title:has([data-mirror-from=queryname]:empty) .filter-label-named {
  display: none;
}
.search-page-header-title:has([data-mirror-from=queryname]:not(:empty)) .filter-label-unnamed {
  display: none;
}

.criterion-row th[scope=row] {
  font-weight: normal;
}
.criterion-row:has(input[name="@columns"]:checked) th[scope=row] {
  font-weight: 500;
}

.row-drag-handle {
  cursor: move;
  color: var(--bs-secondary-color);
}

.criterion-row-ghost > * {
  background-color: var(--bs-primary-bg-subtle);
  opacity: 0.5;
}

.criterion-row-drag {
  box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.25);
}

.criterion-toggle {
  padding: 0;
  border: 0;
  background: none;
  line-height: 1;
  cursor: pointer;
  color: var(--bs-body-color);
}
.criterion-toggle .bi {
  display: none;
  font-size: 1.1rem;
}
.criterion-toggle[data-state=off] .criterion-icon-off {
  display: inline;
  opacity: 0.35;
}
.criterion-toggle[data-state=asc] .criterion-icon-asc {
  display: inline;
}
.criterion-toggle[data-state=desc] .criterion-icon-desc {
  display: inline;
}
.criterion-toggle:hover .criterion-icon-off {
  opacity: 0.7;
}

.exclude-toggle {
  padding: 0;
  border: 0;
  background: none;
  line-height: 1;
  cursor: pointer;
  font-size: 1.1rem;
  color: var(--bs-body-color);
}
.exclude-toggle span {
  display: none;
}
.exclude-toggle[data-state=off] .exclude-icon-off {
  display: inline;
  opacity: 0.35;
}
.exclude-toggle[data-state=off]:hover .exclude-icon-off {
  opacity: 0.7;
}
.exclude-toggle[data-state=on] .exclude-icon-on {
  display: inline;
  opacity: 1;
  font-weight: 700;
}

.criterion-shake {
  animation: criterion-shake 0.3s;
}

@keyframes criterion-shake {
  0%, 100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-3px);
  }
  75% {
    transform: translateX(3px);
  }
}
.status-toggle .badge {
  align-self: center;
  vertical-align: middle;
}

#issues-table tr[data-status=closed] {
  opacity: 0.4;
}

.views-th {
  width: 1%;
  white-space: nowrap;
}

.views-cell {
  white-space: nowrap;
}
.views-cell .views-glyph + .views-glyph {
  margin-left: 0.4rem;
}
.views-cell .views-never,
.views-cell .views-stale {
  color: #999;
}
.views-cell .views-fresh {
  color: var(--bs-success);
}

/* Image preview — popover hover (~25rem) + lightbox modal (viewport-fit). */
.image-preview-popover {
  --bs-popover-max-width: 25rem;
}
.image-preview-popover .popover-body {
  padding: 0.5rem;
}
.image-preview-popover .image-preview-img {
  display: block;
  max-width: 100%;
  max-height: 20rem;
  -o-object-fit: contain;
     object-fit: contain;
}

.image-preview-modal-img {
  display: block;
  max-width: 100%;
  max-height: 80vh;
  margin: 0 auto;
  -o-object-fit: contain;
     object-fit: contain;
}

/* Mode fullscreen : header en overlay translucide + body sans padding, l'image peut tirer jusqu'à 100vh. */
.modal-fullscreen .modal-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1;
  background-color: rgba(var(--bs-body-bg-rgb), 0.6);
  backdrop-filter: blur(8px);
  border-bottom: 0;
  transition: opacity 0.3s;
}

/* Auto-hide après 1s (classe posée par JS). Fade lent (1s) jusqu'à 10% — hint visuel discret que la zone existe. Hover → fade-in snappy via transition override. */
.modal-fullscreen .modal-header.auto-hide {
  opacity: 0.1;
  transition: opacity 1s;
}

.modal-fullscreen .modal-header.auto-hide:hover {
  opacity: 1;
  transition: opacity 0.3s;
}

.modal-fullscreen .modal-body {
  padding: 0;
}

.modal-fullscreen .image-preview-modal-img {
  max-height: 100vh;
}

/* Dialog se cale sur la largeur intrinsèque de l'image. Petite image → petite modale, plafonné par modal-xl (1140px) ou viewport (fullscreen). */
#image-preview-modal .modal-dialog {
  width: -moz-max-content;
  width: max-content;
}

/* Override BS5 .modal-fullscreen { width: 100vw; height: 100%; margin: 0 } pour que le dialog se cale aussi sur l'image en fullscreen, capé par viewport et centré horizontalement. */
#image-preview-modal .modal-fullscreen {
  max-width: 100vw;
  height: auto;
  margin: 0 auto;
}

#image-preview-modal .modal-fullscreen .modal-content {
  height: auto;
}

/* Nom de fichier long ne doit pas élargir le dialog au-delà de la largeur de l'image. */
#image-preview-modal .modal-title {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* INTENT: [2026-05-29 22-00-00] [image-preview-fit] Ajouter width: max-content sur .modal-xl du #image-preview-modal pour que le dialog se cale sur la largeur intrinsèque de l'image. Ellipsis sur modal-title pour qu'un nom de fichier long n'inflate pas le dialog. */
/* DONE:   [2026-05-29 22-00-00] [image-preview-fit] 2 blocs ajoutés. */
/* INTENT: [2026-05-29 22-30-00] [image-preview-fit-fullscreen] Étendre le bornage au mode fullscreen (override width 100vw / height 100% de BS5 → max-content / auto, capé viewport). Élargir le sélecteur de max-content à .modal-dialog (couvre xl + fullscreen). */
/* DONE:   [2026-05-29 22-30-00] [image-preview-fit-fullscreen] Bloc .modal-fullscreen ajouté ; sélecteur de width: max-content passé de .modal-xl à .modal-dialog. */
/* INTENT: [2026-05-29 22-50-00] [fullscreen-header-overlay] En fullscreen, transformer le modal-header en overlay translucide + retirer le padding du modal-body pour que l'image utilise jusqu'à 100vh. */
/* DONE:   [2026-05-29 22-50-00] [fullscreen-header-overlay] 3 règles ajoutées sur le bloc .modal-fullscreen : header position: absolute + backdrop-filter blur, body padding: 0, image max-height: 100vh (remplace calc(100vh - 6rem)). */
/* INTENT: [2026-05-29 23-10-00] [fullscreen-center-and-auto-hide-header] (1) Centrer horizontalement le dialog en fullscreen (margin: 0 auto override de BS5 margin: 0). (2) Header overlay auto-hide après 5s via classe .auto-hide posée par JS, fade-in au hover sur la zone (toujours présente au layout). */
/* DONE:   [2026-05-29 23-10-00] [fullscreen-center-and-auto-hide-header] margin: 0 auto sur #image-preview-modal .modal-fullscreen. transition opacity 0.3s + règles .auto-hide / .auto-hide:hover sur le header. JS schedule_header_auto_hide / clear_header_auto_hide câblés dans set_image_preview_fullscreen. */
.scale-110 {
  transform: scale(1.1);
  display: inline-block;
}

.scale-125 {
  transform: scale(1.25);
  display: inline-block;
}

a.no-decoration {
  text-decoration: none;
}

i.bi.reverse-h {
  display: inline-block;
}

.reverse-h {
  transform: scaleX(-1);
}

[data-show-when=dirty] {
  display: none;
}

form.is-dirty [data-show-when=dirty] {
  display: revert;
}

ul.dash-list {
  list-style-type: "–  ";
}

.supplementary-field {
  position: absolute;
  left: -9999px;
}

/* Disclosure chevron animé — utilitaire posable sur n'importe quel <summary>.
   Remplace le marker natif (▶) par un chevron Bootstrap Icons à gauche, qui
   pivote à l'ouverture du <details>. */
summary.marker-arrow {
  list-style: none;
}
summary.marker-arrow::-webkit-details-marker {
  display: none;
}
summary.marker-arrow {
  display: flex;
  align-items: center;
  cursor: pointer;
}
summary.marker-arrow::before {
  font-family: "bootstrap-icons";
  content: "\f285";
  margin-right: 0.5rem;
  transition: transform 0.2s ease;
}

details[open] > summary.marker-arrow::before {
  transform: rotate(90deg);
}

:root {
  --bs-border-width: 1px;
  --bs-body-bg: #f4f4f4;
  --c-html-bg: #dadada;
  --c-bg-dim: var(--bs-light);
  --c-bg-dim-alt: #f4f4f4;
  --c-editor-bg: #fff;
  --c-field-bg: #fff;
  --c-editor-fg: #212529;
  --c-editor-toolbar-bg: #f8f9fa;
  --c-editor-border: #dee2e6;
  --c-editor-selection: rgba(var(--bs-primary-rgb), 0.25);
  --c-dim-border: #e6e6e6;
  --c-strong-border: rgba(127,127,127,0.5);
  --c-table-border: #ccc;
  /* Aligné sur .btn-outline-secondary light (Bootstrap default). */
  --readonly-bg: rgba(0, 0, 0, 0.05);
  --c-bg-thead: #e0e0e0;
  --c-bg-elevated: #fff;
  /* Bandeau séparateur de groupe sur table.striped-card (cf html/issue.index.html).
     Bleu-gris foncé avec texte blanc — démarcation forte vis-à-vis des data rows. */
  --c-group-separator-bg: #aaa;
  /* "— any —" placeholder (text input + native select + Tom Select item).
     Gris neutre, lisible sur fond clair et sombre. */
  --placeholder-color: #999;
  /* Coloriage du texte selon data-severity (cf. issue.index.html — cellules table + spans de group separator).
     Valeurs tunées pour fond clair ; la variante dark redéfinit ces mêmes vars dans html.theme-dark. */
  --c-severity-improvement: #198754;
  --c-severity-feature-request: #0dcaf0;
  --c-severity-annoyance: #ffcc34;
  --c-severity-slows-production: #fd7e14;
  --c-severity-blocks-formula: #dc4335;
  --c-severity-blocks-all-production: #7c2d12;
  --c-severity-low: #198754;
  --c-severity-medium: #ffc107;
  --c-severity-high: #dc3545;
  /* .text-warning / .text-danger lisent le triplet `-rgb`, pas --bs-warning/danger.
     Orange ambré lisible sur fond clair (défaut BS #ffc107 = jaune illisible).
     Rouge poussé vers l'orange (défaut BS #dc3545). */
  --bs-warning-rgb: 230, 140, 6;
  --bs-danger-rgb: 224, 50, 30;
}

/* Densité fractionnaire (DPR 1.25/1.5/1.75, ex. 4K à 150% de scaling Windows) :
   1px CSS snappe à 1px device = trait trop fin sur tous les composants Bootstrap.
   DPR=1 (moniteur 1080p) et DPR=2 (Retina/4K à 200%) rendent 1px net → exclus.
   dppx = densité effective (matériel × zoom) ; l'émulation DevTools FHD ne matche pas. */
@media (min-resolution: 1.01dppx) and (max-resolution: 1.99dppx) {
  :root {
    --bs-border-width: 1.5px;
  }
}
a {
  color: inherit;
  text-decoration-color: rgba(var(--bs-link-color-rgb), 0.35);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
  transition: text-decoration-color 0.15s ease-in-out;
}
a:hover {
  text-decoration-color: rgba(var(--bs-link-color-rgb), 0.8);
}

.c-bg-dim {
  background-color: var(--c-bg-dim) !important;
}

/* DROPDOWN — box-shadow discrète, applicable aux 2 modes. rgba(0,0,0,.25)
   = trait gris léger sur fond clair, halo noir visible mais sobre sur fond
   #383838. Propriété écrite directement (pas via --bs-dropdown-box-shadow
   qui n'est pas relié à `box-shadow` dans le BS5 vendoré). */
.dropdown-menu {
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.25);
}

/* Toggle-theme : identity primary dans les 2 modes (light = checked, dark = unchecked).
   Sans ça la pill BS5 retombe sur body-bg en non-coché → invisible en dark, incohérent
   avec le bleu de light. Handle blanc forcé (BS5 ne le pose qu'au :checked). */
.toggle-theme .form-check-input {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
  --bs-form-switch-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='%23fff'/></svg>");
}

/** Toggle-theme (banner.html) : icône reflète le mode courant, swap sans JS.
 *  Light → soleil, dark → lune. Piloté par html.theme-dark (posée par
 *  l'anti-FOUC avant le premier paint → bonne icône dès le chargement). */
.toggle-theme .icon-dark {
  display: none;
}

html.theme-dark .toggle-theme .icon-light {
  display: none;
}

html.theme-dark .toggle-theme .icon-dark {
  display: inline-block;
}

[data-severity=improvement] {
  color: var(--c-severity-improvement) !important;
}

[data-severity=feature-request] {
  color: var(--c-severity-feature-request) !important;
}

[data-severity=annoyance] {
  color: var(--c-severity-annoyance) !important;
}

[data-severity=slows-production] {
  color: var(--c-severity-slows-production) !important;
}

[data-severity=blocks-formula] {
  color: var(--c-severity-blocks-formula) !important;
}

[data-severity=blocks-all-production] {
  color: var(--c-severity-blocks-all-production) !important;
}

[data-severity=low] {
  color: var(--c-severity-low) !important;
}

[data-severity=medium] {
  color: var(--c-severity-medium) !important;
}

[data-severity=high] {
  color: var(--c-severity-high) !important;
}

/* SimpleMDE editor surfaces — vendor CSS pré-buildé (static/simplemde.min.css), sans variables CSS, donc override à la main.
   Wrapper `html` : SimpleMDE écrit `.editor-toolbar a { color:#2c3e50!important }`, chargé APRÈS custom.css via {% block extracss %} de issue.item.html. Spécificité égale + !important des deux côtés = cascade tranche, vendor gagne. `html` (+1 en spécificité) suffit à le battre, sans !important.
   SimpleMDE utilise <a> pour les boutons toolbar, pas <button>. Couleurs portées par les --c-editor-* qui swappent dans html.theme-dark. */
html .form-control,
html .form-select,
html .input-group-text,
html .form-check-input,
html .c-border {
  border-width: 1.5px;
}
html .form-control,
html .form-select,
html .input-group-text,
html .form-check-input,
html .c-outline-secondary {
  --bs-border-color: #ccc;
}
html {
  /* Fond clair des champs de saisie : se détache du fond de page gris (--bs-body-bg). */
}
html .form-control,
html .form-select,
html .ts-wrapper .ts-control {
  background-color: var(--c-field-bg);
}
html {
  /* Placeholder Bootstrap par défaut = rgba(33,37,41,0.75) — quasi-noir, confondu avec une vraie valeur. Couleur partagée avec les <select>/Tom Select à option vide (cf. --placeholder-color dans :root). */
}
html .form-control::-moz-placeholder {
  color: var(--placeholder-color);
}
html .form-control::placeholder {
  color: var(--placeholder-color);
}
html .btn-outline-secondary {
  border-color: #ccc;
}
html .table {
  --bs-table-border-color: var(--c-table-border);
}
html .CodeMirror {
  background: var(--c-editor-bg);
  color: var(--c-editor-fg);
  border-color: var(--c-editor-border);
}
html .CodeMirror-cursor {
  border-left-color: var(--c-editor-fg);
}
html .CodeMirror .CodeMirror-selected,
html .CodeMirror-focused .CodeMirror-selected {
  background: var(--c-editor-selection);
}
html .CodeMirror-gutters {
  background: var(--c-editor-bg);
  border-right-color: var(--c-editor-border);
}
html .CodeMirror-linenumber {
  color: var(--bs-secondary-color);
}
html .editor-toolbar {
  background: var(--c-editor-toolbar-bg);
  border-color: var(--c-editor-border);
}
html .editor-toolbar a {
  color: var(--c-editor-fg) !important;
}
html .editor-toolbar a:hover, html .editor-toolbar a.active {
  background: var(--c-editor-border);
  border-color: var(--c-editor-border);
}
html .editor-statusbar {
  background: var(--c-editor-toolbar-bg);
  color: var(--c-editor-fg);
  border-color: var(--c-editor-border);
}

html.theme-dark {
  color-scheme: dark;
  --bs-dark: #222;
  --bs-body-bg: var(--bs-dark);
  --bs-body-color: rgb(190, 190, 190);
  --bs-body-color-rgb: 190, 190, 190;
  --bs-heading-color: var(--bs-light);
  --bs-emphasis-color: var(--bs-light);
  --bs-emphasis-color-rgb: 255, 255, 255;
  --bs-primary: #7ac;
  --bs-primary-rgb: 119, 170, 204;
  --bs-primary-color: #7ac;
  --bs-secondary-color: #999;
  --bs-tertiary-bg: #555;
  --bs-tertiary-bg-rgb: 85, 85, 85;
  --bs-link-color: #64c8ff;
  --bs-link-color-rgb: 100, 200, 255;
  --bs-border-color: #777;
  --readonly-bg: rgba(255, 255, 255, 0.1);
  /* CUSTOM */
  --c-html-bg: #000;
  --c-dark-less: #444;
  --c-bg-dim: var(--c-dark-less);
  --c-bg-dim-alt: #333;
  --c-editor-bg: var(--bs-body-bg);
  --c-editor-fg: var(--bs-body-color);
  --c-editor-toolbar-bg: var(--c-dark-less);
  --c-editor-border: #555;
  --c-editor-selection: rgba(var(--bs-primary-rgb), 0.40);
  --c-table-border: #777;
  --c-bg-thead: #555;
  --c-group-separator-bg: #37474f;
  --c-bg-elevated: #484848;
  --c-field-bg: #484848;
  /* Coloriage du texte selon data-severity — variants éclaircis pour fond sombre. */
  --c-severity-improvement: #48a54b;
  --c-severity-feature-request: #259cd3;
  --c-severity-annoyance: #ce9f28;
  --c-severity-slows-production: #ff9800;
  --c-severity-blocks-formula: #ef7a50;
  --c-severity-blocks-all-production: #de2f2f;
  --c-severity-low: #4caf50;
  --c-severity-medium: #fbc02d;
  --c-severity-high: #ef5350;
}
html.theme-dark .form-control,
html.theme-dark .form-select,
html.theme-dark .input-group-text,
html.theme-dark .form-check-input,
html.theme-dark .c-outline-secondary {
  --bs-border-color: #777;
}
html.theme-dark .btn-outline-secondary,
html.theme-dark .c-outline-secondary {
  border-color: #777;
}
html.theme-dark {
  /* Logo banner — éclaircir en dark. Commenter/décommenter pour A/B. */
}
html.theme-dark #navbar-logo {
  filter: invert(1) hue-rotate(180deg);
  filter: brightness(2.5) saturate(0.75);
}
html.theme-dark {
  /* Buttons — chaque .btn-<variant> redéfinit ses propres --bs-btn-* sur lui-même
     (cf bootstrap.min.css). Pour modifier la couleur en dark, on ré-écrase ces
     variables sur le même sélecteur, à l'intérieur de html.theme-dark (gagne
     par spécificité). Les variants non listés gardent leur couleur Bootstrap. */
  /* État disabled : Bootstrap défaut 0.65 reste trop proche de l'enabled en dark
     (bg déjà sombre + petit fade = peu distinct). 0.4 → bouton visiblement inerte. */
}
html.theme-dark .btn {
  --bs-btn-disabled-opacity: 0.4;
}
html.theme-dark .btn-primary {
  --bs-btn-bg: #469;
  --bs-btn-border-color: #469;
  --bs-btn-hover-bg: #3d7ce6;
  --bs-btn-hover-border-color: #3d7ce6;
  --bs-btn-disabled-bg: #469;
  --bs-btn-disabled-border-color: #469;
}
html.theme-dark .btn-secondary {
  --bs-btn-bg: #6c757d;
  --bs-btn-border-color: #6c757d;
  --bs-btn-hover-bg: #828990;
  --bs-btn-hover-border-color: #828990;
  --bs-btn-disabled-bg: #6c757d;
  --bs-btn-disabled-border-color: #6c757d;
}
html.theme-dark .btn-success {
  --bs-btn-bg: #2da866;
  --bs-btn-border-color: #2da866;
  --bs-btn-hover-bg: #259357;
  --bs-btn-hover-border-color: #259357;
  --bs-btn-disabled-bg: #2da866;
  --bs-btn-disabled-border-color: #2da866;
}
html.theme-dark .btn-danger {
  --bs-btn-bg: #e04555;
  --bs-btn-border-color: #e04555;
  --bs-btn-hover-bg: #c63a48;
  --bs-btn-hover-border-color: #c63a48;
  --bs-btn-disabled-bg: #e04555;
  --bs-btn-disabled-border-color: #e04555;
}
html.theme-dark .btn-info {
  --bs-btn-color: #000;
  --bs-btn-bg: #3dd5f3;
  --bs-btn-border-color: #3dd5f3;
  --bs-btn-hover-color: #000;
  --bs-btn-hover-bg: #25cff2;
  --bs-btn-hover-border-color: #25cff2;
  --bs-btn-disabled-color: #000;
  --bs-btn-disabled-bg: #3dd5f3;
  --bs-btn-disabled-border-color: #3dd5f3;
}
html.theme-dark {
  /* Outline : trait + texte coloré sur fond transparent, fond plein au hover.
     Les vars à toucher sont différentes : --bs-btn-color (couleur trait/texte)
     et tout le bloc hover. */
}
html.theme-dark .btn-outline-primary {
  --bs-btn-color: #4d8bff;
  --bs-btn-border-color: #4d8bff;
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #4d8bff;
  --bs-btn-hover-border-color: #4d8bff;
}
html.theme-dark .btn-outline-secondary {
  --bs-btn-color: #adb5bd;
  --bs-btn-border-color: #adb5bd;
  --bs-btn-hover-color: #212529;
  --bs-btn-hover-bg: #adb5bd;
  --bs-btn-hover-border-color: #adb5bd;
}
html.theme-dark {
  --c-dim-border: #444;
}
html.theme-dark .c-bg-dim {
  background-color: var(--c-dark-less) !important;
}
html.theme-dark {
  /* Bootstrap form-select : la flèche du dropdown est un SVG embarqué dans background-image
     avec stroke codé en dur en #343a40 — invisible sur fond sombre. Override en dark mode
     avec le même SVG mais stroke en #f8f9fa (= --bs-body-color clair). */
}
html.theme-dark .form-select {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='%23f8f9fa' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/></svg>");
}
html.theme-dark .form-check-input:checked {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}
html.theme-dark {
  /* PAGINATION — override uniquement les vars dont la cible reste claire (--bs-border-color, --bs-secondary-bg gardent leur valeur light Bootstrap) ou hardcodée bleu Bootstrap (#0d6efd, rgba 13,110,253). Les autres vars pointent sur --bs-link-color / --bs-tertiary-bg / --bs-secondary-color déjà ré-écrits en dark plus haut. */
}
html.theme-dark .pagination {
  --bs-pagination-bg: transparent;
  --bs-pagination-border-color: var(--c-dark-less);
  --bs-pagination-hover-border-color: var(--c-dark-less);
  --bs-pagination-focus-bg: var(--bs-tertiary-bg);
  --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(100, 200, 255, 0.25);
  --bs-pagination-active-bg: #469;
  --bs-pagination-active-border-color: #469;
  --bs-pagination-disabled-bg: transparent;
  --bs-pagination-disabled-border-color: var(--c-dark-less);
}
html.theme-dark {
  /* CARD — body-bg en dark = #383838 collerait la card sur le fond ; border-color-translucent (rgba 0,0,0,.175) invisible ; cap-bg avec body-color-rgb (jamais redéfini en dark) trop sombre. Les vars vides supprimées : color/title/subtitle/box-shadow/height fall-back par défaut. */
}
html.theme-dark .card {
  --bs-card-bg: var(--c-dark-less);
  --bs-card-border-color: rgba(255, 255, 255, 0.15);
  --bs-card-cap-bg: rgba(255, 255, 255, 0.05);
}
html.theme-dark {
  /* MODAL — bg dark = body-bg #222 collerait à l'arrière-plan ; remonter sur --c-bg-elevated (#484848) pour l'effet de surface. */
}
html.theme-dark .modal {
  --bs-modal-bg: var(--c-bg-elevated);
}
html.theme-dark {
  /* BTN-CLOSE — BS5 5.3 applique `filter: var(--bs-btn-close-white-filter)` sous [data-bs-theme=dark] ; le projet utilise html.theme-dark donc cette règle ne matche pas. Filtre inliné en dur (var pas garantie exposée dans le BS5 minifié vendoré). Couvre toutes les croix (modale, alerts, banners) en un point. */
}
html.theme-dark .btn-close {
  filter: invert(1) grayscale(100%) brightness(200%);
}
html.theme-dark {
  /* Les .alert gardent leur fond clair BS en dark (jamais re-thémées) ; la croix doit donc rester sombre, pas suivre le filtre blanc global ci-dessus. */
}
html.theme-dark .alert .btn-close {
  filter: none;
}
html.theme-dark {
  /* DROPDOWN — bg/color/link-hover-bg auto-adaptent (pointent sur --bs-body-bg, --bs-body-color, --bs-tertiary-bg déjà ré-écrits). Border-color-translucent (rgba 0,0,0,.175) et divider-bg invisibles sur fond #383838 ; link-active-bg #0d6efd hardcodé bleu BS, trop saturé en dark → assorti à .btn-primary (#469). */
}
html.theme-dark .dropdown-menu {
  --bs-dropdown-border-color: rgba(255, 255, 255, 0.15);
  --bs-dropdown-divider-bg: rgba(255, 255, 255, 0.15);
  --bs-dropdown-link-active-bg: #469;
}
html.theme-dark .tooltip {
  opacity: 1;
  --bs-tooltip-bg: #f8f9fa;
  --bs-tooltip-color: #212529;
  box-shadow: 0 4px 8px rgb(0, 0, 0);
}
html.theme-dark {
  /* TABLE — BS5 lie --bs-table-color à --bs-emphasis-color (#f8f9fa en dark) → blanc dur. Re-router sur --bs-body-color pour aligner sur la teinte body. */
}
html.theme-dark .table {
  --bs-table-color: var(--bs-body-color);
}

/* End of main entry — partials hold the implementation. *//*# sourceMappingURL=custom.css.map */