/* ==========================================================================
   WS-Audit — Components
   Every reusable UI part. Nothing here hard-codes a value; tokens only.
   ========================================================================== */

/* --- Button --------------------------------------------------------------
   Variants: primary, secondary, ghost, danger. Sizes: default, sm.
   One primary per view. Danger is always confirmed by a modal. */
.ws-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--ws-2);
  min-height: var(--ws-touch-min);
  padding: 0 var(--ws-4);
  border: 1px solid transparent;
  border-radius: var(--ws-radius-field);
  font: inherit;
  font-size: var(--ws-text-body);
  font-weight: var(--ws-weight-med);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--ws-motion), border-color var(--ws-motion);
}
.ws-btn svg { flex: none; margin-right: 4px !important; }
.ws-btn--icon svg { margin-right: 0 !important; }
.ws-btn--icon svg { margin-right: 0; }
.ws-btn--icon svg { margin-right: 0; }

.ws-btn--primary {
  background: var(--ws-blue);
  border-color: var(--ws-blue);
  color: #fff;
}
.ws-btn--primary:hover { background: var(--ws-blue-deep); border-color: var(--ws-blue-deep); }

.ws-btn--secondary {
  background: var(--ws-surface);
  border-color: var(--ws-line-control);
  color: var(--ws-ink);
}
.ws-btn--secondary:hover { background: var(--ws-row-hover); border-color: var(--ws-muted); }

.ws-btn--ghost { background: transparent; color: var(--ws-ink); }
.ws-btn--ghost:hover { background: var(--ws-row-hover); }

.ws-btn--danger {
  background: var(--ws-surface);
  border-color: var(--ws-danger);
  color: var(--ws-danger);
}
.ws-btn--danger:hover { background: var(--ws-danger-wash); }

/* The dense variant. 36px is below the 44px touch rule on purpose: it exists
   for mouse-driven, information-dense views (table rows, pagination, filter
   bars) where 44px everywhere would wreck the density. Touch pointers and
   tablet widths get the full 44px back — see the coarse-pointer block below. */
.ws-btn--sm { min-height: 36px; padding: 0 var(--ws-3); font-size: var(--ws-text-table); }
.ws-btn--icon { padding: 0; width: var(--ws-touch-min); }
.ws-btn--icon.ws-btn--sm { width: 36px; }

.ws-btn:disabled, .ws-btn[aria-disabled="true"] {
  opacity: .5; cursor: not-allowed; pointer-events: none;
}
/* Double-click guard: JS sets data-busy while a request is in flight. */
.ws-btn[data-busy] { pointer-events: none; opacity: .7; }
.ws-btn[data-busy] .ws-spinner { display: block; }
.ws-btn .ws-spinner { display: none; }

/* --- Field --------------------------------------------------------------- */
.ws-field { display: flex; flex-direction: column; gap: var(--ws-1); }

/* Stacked fields only. `+` cannot tell "below" from "beside": put two fields
   side by side in a grid and the second one silently gained 16px of top margin,
   so NPA sat 16px above Localité and Pays. Use .ws-field-row for a row — it
   spaces with gap and cancels this. */
.ws-field + .ws-field { margin-top: var(--ws-4); }

/* --- Field row -----------------------------------------------------------
   Fields side by side. Collapses to one column on narrow screens, so a form
   never needs an inline media query. */
.ws-field-row {
  display: grid;
  gap: var(--ws-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  align-items: start;
}
.ws-field-row > .ws-field { margin-top: 0; }
.ws-field-row + .ws-field,
.ws-field + .ws-field-row,
.ws-field-row + .ws-field-row { margin-top: var(--ws-4); }

/* Named ratios, for the cases where auto-fit guesses wrong. */
.ws-field-row--2      { grid-template-columns: 1fr 1fr; }
.ws-field-row--3      { grid-template-columns: 1fr 1fr 1fr; }
.ws-field-row--4      { grid-template-columns: 1fr 1fr 1fr 1fr; }
.ws-field-row--zip    { grid-template-columns: 140px 1fr 180px; }   /* NPA · localité · pays */
.ws-field-row--narrow { grid-template-columns: 200px 1fr; }

@media (max-width: 720px) {
  .ws-field-row,
  .ws-field-row--2, .ws-field-row--3, .ws-field-row--4,
  .ws-field-row--zip, .ws-field-row--narrow { grid-template-columns: 1fr; }
}

.ws-label {
  display: flex;
  align-items: center;
  gap: var(--ws-1);
  font-size: var(--ws-text-table);
  font-weight: var(--ws-weight-med);
  color: var(--ws-ink);
}
.ws-label .ws-req { color: var(--ws-danger); margin-left: 2px; }

/* An icon in a label names the field's kind — phone, address, date. It sits in
   --ws-muted so the word stays the thing you read, and the icon is what your
   eye lands on when scanning. */
.ws-label > svg {
  flex: none;
  color: var(--ws-muted);
}
.ws-field[data-invalid] .ws-label > svg { color: var(--ws-danger); }

.ws-hint { font-size: var(--ws-text-meta); color: var(--ws-muted); }

.ws-input, .ws-select, .ws-textarea {
  width: 100%;
  min-height: var(--ws-touch-min);
  padding: var(--ws-2) var(--ws-3);
  border: 1px solid var(--ws-line-control);
  border-radius: var(--ws-radius-field);
  background: var(--ws-surface);
  color: var(--ws-ink);
  font: inherit;
  font-size: var(--ws-text-body);
  transition: border-color var(--ws-motion);
}
.ws-textarea { min-height: 96px; resize: vertical; line-height: var(--ws-lh-body); }
.ws-input:hover, .ws-select:hover, .ws-textarea:hover { border-color: var(--ws-muted); }
.ws-input::placeholder, .ws-textarea::placeholder { color: var(--ws-muted); }

.ws-select {
  appearance: none;
  padding-right: var(--ws-8);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235C6B77' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--ws-3) center;
}

.ws-input:disabled, .ws-select:disabled, .ws-textarea:disabled {
  background: var(--ws-bg); color: var(--ws-muted); cursor: not-allowed;
}

/* The open list of a <select>.
   ------------------------------------------------------------------
   The closed control above has always been ours. The list that drops out of it
   was not: the browser hands it to the operating system, which draws it with
   the system font, the system blue and the system scrollbar. Twenty categories
   of Windows highlight in the middle of the app.

   `appearance: base-select` is the only way to get it back without rebuilding a
   listbox in JavaScript — the element stays a real <select>, so keyboard
   navigation, type-ahead, screen readers and the mobile native picker keep
   working exactly as before. A browser that does not support it ignores this
   whole block and keeps the system list: nothing to detect, nothing to fall
   back to.

   The @supports is not decoration. Without it, a browser that parses
   `::picker(select)` but not `base-select` would apply half of these rules to a
   control it still draws itself.

   `base-select` only exists for a closed, single-value <select> — the popup it
   redraws is the dropdown list. A <select multiple size="6"> is not that: it
   has no closed state, it is always an inline listbox. Applying `display: flex`
   to it broke that listbox — options overlapped into one garbled line with a
   horizontal scrollbar (the library picker on an audit page, screenshot from
   Tiago 07.08.2026). `:not([multiple])` on every rule below keeps a multi-select
   on the plain `appearance: none` styling above, which was always correct for
   it. */
@supports (appearance: base-select) {
  .ws-select:not([multiple]),
  .ws-select:not([multiple])::picker(select) { appearance: base-select; }

  /* base-select redraws the control from scratch, so the chevron, the padding
     and the touch height have to be restated — they are not inherited from the
     appearance:none rule above. */
  .ws-select:not([multiple]) {
    display: flex;
    align-items: center;
    text-align: left;
    padding-right: var(--ws-8);
  }
  /* Our chevron is already the background image; the browser's own would sit
     next to it. */
  .ws-select:not([multiple])::picker-icon { display: none; }

  .ws-select:not([multiple])::picker(select) {
    border: 1px solid var(--ws-line);
    border-radius: var(--ws-radius-box);
    background: var(--ws-surface);
    box-shadow: var(--ws-shadow-overlay);
    padding: var(--ws-1);
    /* The question categories run to twenty entries. */
    max-height: 320px;
    overflow-y: auto;
    /* No transition: the list must be there the instant it is asked for. */
  }
  .ws-select:not([multiple])::picker(select):popover-open { margin-top: 2px; }

  .ws-select:not([multiple]) option {
    display: flex;
    align-items: center;
    gap: var(--ws-2);
    min-height: 36px;
    padding: var(--ws-1) var(--ws-2);
    border-radius: var(--ws-radius-field);
    color: var(--ws-ink);
    font-size: var(--ws-text-body);
  }
  .ws-select:not([multiple]) option:hover,
  .ws-select:not([multiple]) option:focus { background: var(--ws-row-hover); outline: none; }
  .ws-select:not([multiple]) option:checked {
    background: var(--ws-blue-wash);
    color: var(--ws-blue-deep);
    font-weight: var(--ws-weight-med);
  }
  /* The tick that marks the current option. Left where the eye scans, and it
     reserves its width on every row so the labels do not shift by 20px when
     the selection moves. */
  .ws-select:not([multiple]) option::checkmark {
    order: -1;
    flex: none;
    width: 14px;
    color: var(--ws-blue);
  }
  .ws-select:not([multiple]) option:disabled { color: var(--ws-muted); }

  /* Coarse pointer: the same 44px rule as every other control. */
  @media (pointer: coarse) {
    .ws-select:not([multiple]) option { min-height: var(--ws-touch-min); }
  }
}

/* Error state — colour is never the only signal; ws-error text carries it. */
.ws-field[data-invalid] .ws-input,
.ws-field[data-invalid] .ws-select,
.ws-field[data-invalid] .ws-textarea { border-color: var(--ws-danger); }

.ws-error {
  display: flex; align-items: center; gap: var(--ws-1);
  font-size: var(--ws-text-meta);
  font-weight: var(--ws-weight-med);
  color: var(--ws-danger);
}

/* File input.
   The one control a browser draws itself: `type="file"` renders a native
   "Choisir un fichier" button that ignores .ws-input entirely, so the Documents
   panel showed system chrome sitting inside our field. ::file-selector-button
   is the only hook, and it takes the secondary button's look so the pair reads
   as one control. Height matches .ws-input — the native button is taller than
   the text it replaces, hence the padding rather than min-height. */
.ws-file {
  width: 100%;
  padding: var(--ws-1) var(--ws-2);
  border: 1px solid var(--ws-line-control);
  border-radius: var(--ws-radius-field);
  background: var(--ws-surface);
  color: var(--ws-ink);
  font: inherit;
  font-size: var(--ws-text-body);
  line-height: 2;
  transition: border-color var(--ws-motion);
}
.ws-file:hover { border-color: var(--ws-muted); }
.ws-file::file-selector-button {
  margin-right: var(--ws-3);
  padding: var(--ws-1) var(--ws-3);
  border: 1px solid var(--ws-line-control);
  border-radius: var(--ws-radius-field);
  background: var(--ws-bg);
  color: var(--ws-ink);
  font: inherit;
  font-size: var(--ws-text-meta);
  font-weight: var(--ws-weight-med);
  cursor: pointer;
  transition: background var(--ws-motion), border-color var(--ws-motion);
}
.ws-file::file-selector-button:hover { background: var(--ws-row-hover); border-color: var(--ws-muted); }
.ws-file:disabled, .ws-file:disabled::file-selector-button {
  background: var(--ws-bg); color: var(--ws-muted); cursor: not-allowed;
}
.ws-field[data-invalid] .ws-file { border-color: var(--ws-danger); }

/* Checkbox / radio */
.ws-check { display: flex; align-items: flex-start; gap: var(--ws-2); min-height: var(--ws-touch-min); }
.ws-check input { width: 18px; height: 18px; margin: 11px 0 0; accent-color: var(--ws-blue); flex: none; }
.ws-check label { padding-top: 10px; font-size: var(--ws-text-body); cursor: pointer; }

/* Compact variant — dense rows (a filter bar, a long list of toggles) where the
   48px touch target would space the list out to nothing. Three views were
   hand-rolling this with inline min-height/padding-top/font-size overrides,
   each landing on a slightly different number. */
.ws-check--sm { min-height: 32px; }
.ws-check--sm input { width: 16px; height: 16px; margin: 6px 0 0; }
.ws-check--sm label { padding-top: 4px; font-size: var(--ws-text-table); }

/* Segmented control — yes/no, compliance, risk */
/* align-self: .ws-field is a column flex container and would stretch this to
   full width, leaving dead space after the last option. */
.ws-segment {
  display: inline-flex;
  align-self: flex-start;
  border: 1px solid var(--ws-line-control);
  border-radius: var(--ws-radius-field);
  overflow: hidden;
}
.ws-segment button {
  min-height: var(--ws-touch-min);
  padding: 0 var(--ws-4);
  border: 0; border-right: 1px solid var(--ws-line-control);
  background: var(--ws-surface);
  font: inherit; font-size: var(--ws-text-table); font-weight: var(--ws-weight-med);
  color: var(--ws-muted);
  cursor: pointer;
  transition: background var(--ws-motion), color var(--ws-motion);
}
.ws-segment button:last-child { border-right: 0; }
.ws-segment button:hover { background: var(--ws-row-hover); }
.ws-segment button[aria-pressed="true"] { background: var(--ws-blue-wash); color: var(--ws-blue-deep); }

/* --- Table ---------------------------------------------------------------
   Density is the point. Rules, not cards. Sticky head, no zebra. */
.ws-table-wrap { overflow-x: auto; }
.ws-table { width: 100%; border-collapse: collapse; font-size: var(--ws-text-table); }
.ws-table th, .ws-table td {
  padding: var(--ws-2) var(--ws-3);
  text-align: left;
  border-bottom: var(--ws-border);
  vertical-align: middle;
}
.ws-table th {
  position: sticky; top: 0; z-index: var(--ws-z-sticky);
  background: var(--ws-bg);
  font-weight: var(--ws-weight-semi);
  color: var(--ws-muted);
  white-space: nowrap;
}
.ws-table tbody tr { height: var(--ws-row-h); }
.ws-table tbody tr:hover { background: var(--ws-row-hover); }
.ws-table tbody tr:last-child td { border-bottom: 0; }
.ws-table td.ws-td-actions { text-align: right; white-space: nowrap; width: 1%; }
/* Un <form> est un bloc : posé à côté d'un lien, il passait à la ligne et la
   rangée d'actions se cassait en deux. Les enfants restent donc sur une ligne. */
.ws-table td.ws-td-actions > form { display: inline-flex; }
.ws-table td.ws-td-actions > * + * { margin-left: var(--ws-1); }
/* Numeric columns align right in both head and body, or the column reads broken. */
.ws-table th.ws-num, .ws-table td.ws-num { text-align: right; }

/* Tableau -> cartes, sous 900px.
   ------------------------------------------------------------------
   Un tableau de sept colonnes dans 375px ne défile pas : il se comprime.
   « Audit sauvegarde — Commune de Savièse » tombait sur cinq lignes et la
   référence se cassait en quatre morceaux. La ligne devient donc une carte,
   et chaque cellule une paire libellé / valeur.

   Le libellé vient de `data-label` sur le <td> : la CSS ne peut pas lire le
   <th> correspondant, et un tableau sans en-tête visible laisserait des
   valeurs nues. Une cellule sans `data-label` s'affiche pleine largeur — c'est
   ce qu'on veut pour la colonne titre et pour la colonne d'actions.

   Opt-in par `.ws-table.ws-table--cards` : le journal, lui, garde son tableau et son
   défilement horizontal, parce que ses lignes se lisent en colonnes. */
@media (max-width: 900px) {
  .ws-table.ws-table--cards,
  .ws-table.ws-table--cards tbody,
  .ws-table.ws-table--cards tr,
  .ws-table.ws-table--cards td { display: block; width: auto; }

  .ws-table.ws-table--cards thead { display: none; }

  .ws-table.ws-table--cards tbody tr {
    height: auto;
    padding: var(--ws-3);
    border: 1px solid var(--ws-line);
    border-radius: var(--ws-radius-box);
    background: var(--ws-surface);
  }
  .ws-table.ws-table--cards tbody tr + tr { margin-top: var(--ws-2); }

  .ws-table.ws-table--cards td {
    padding: var(--ws-1) 0;
    border: 0;
    text-align: left;
  }
  /* Paire libellé / valeur : le libellé garde une colonne fixe pour que les
     valeurs s'alignent d'une ligne à l'autre. */
  .ws-table.ws-table--cards td[data-label] {
    display: grid;
    grid-template-columns: 8.5rem minmax(0, 1fr);
    gap: var(--ws-2);
    align-items: baseline;
  }
  .ws-table.ws-table--cards td[data-label]::before {
    content: attr(data-label);
    color: var(--ws-muted);
    font-size: var(--ws-text-meta);
  }
  /* Une cellule vide n'a pas à occuper une ligne dans la carte. */
  .ws-table.ws-table--cards td:empty { display: none; }

  /* Un badge est une pastille, pas une barre. Dans une grille il s'étire à la
     largeur de sa colonne s'il ne dit pas le contraire ; le texte, lui, garde
     `stretch` pour pouvoir passer à la ligne. */
  .ws-table.ws-table--cards td[data-label] > .ws-badge,
  .ws-table.ws-table--cards td[data-label] > .ws-progress { justify-self: start; }

  /* La colonne d'actions passe en bas, alignée à gauche comme le reste. */
  .ws-table.ws-table--cards td.ws-td-actions {
    width: auto;
    white-space: normal;
    text-align: left;
    margin-top: var(--ws-2);
    padding-top: var(--ws-2);
    border-top: 1px solid var(--ws-line);
    display: flex;
    flex-wrap: wrap;
    gap: var(--ws-1);
  }
  /* La première cellule porte le nom de la chose : c'est le titre de la carte. */
  .ws-table.ws-table--cards td:first-child {
    font-size: var(--ws-text-body);
    font-weight: var(--ws-weight-med);
    padding-bottom: var(--ws-2);
  }

  /* Plus de tableau, donc plus rien à faire défiler latéralement. */
  .ws-table-wrap:has(.ws-table--cards) { overflow-x: visible; }

  /* Les tableaux restés en tableau — le journal — doivent défiler, pas se
     comprimer. Sans largeur minimale, les colonnes se réduisent jusqu'à casser
     chaque mot sur sa propre ligne, et le conteneur n'a plus rien à faire
     défiler : c'est la ligne qui grandit en hauteur. */
  .ws-table:not(.ws-table--cards) { min-width: 640px; }
}

/* Sortable header. min-height 24px = WCAG 2.5.8 AA target size. */
.ws-th-sort {
  display: inline-flex; align-items: center; gap: var(--ws-1);
  min-height: 24px;
  background: none; border: 0; padding: 0;
  font: inherit; font-weight: inherit; color: inherit;
  cursor: pointer;
}
.ws-th-sort svg { opacity: .35; }
.ws-th-sort[aria-sort="ascending"] svg,
.ws-th-sort[aria-sort="descending"] svg { opacity: 1; color: var(--ws-blue); }
.ws-th-sort[aria-sort="descending"] svg { transform: rotate(180deg); }

/* --- Filter bar ---------------------------------------------------------- */
.ws-filters {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--ws-2);
  padding: var(--ws-3) var(--ws-4);
  border-bottom: var(--ws-border);
}
.ws-filters .ws-search { flex: 1; min-width: 200px; max-width: 320px; }
.ws-search { position: relative; }
.ws-search svg {
  position: absolute; left: var(--ws-3); top: 50%; transform: translateY(-50%);
  color: var(--ws-muted); pointer-events: none;
}
.ws-search .ws-input { padding-left: var(--ws-8); }

/* --- Badge / status ------------------------------------------------------
   Shape + text carry the meaning; colour reinforces. Never colour alone. */
.ws-badge {
  display: inline-flex; align-items: center; gap: var(--ws-1);
  padding: 2px var(--ws-2);
  border-radius: var(--ws-radius-field);
  font-size: var(--ws-text-meta);
  font-weight: var(--ws-weight-med);
  white-space: nowrap;
}
.ws-badge::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
.ws-badge--ok      { background: var(--ws-ok-wash);      color: var(--ws-ok); }
.ws-badge--warn    { background: var(--ws-warn-wash);    color: var(--ws-warn); }
.ws-badge--danger  { background: var(--ws-danger-wash);  color: var(--ws-danger); }
.ws-badge--neutral { background: var(--ws-neutral-wash); color: var(--ws-neutral); }
.ws-badge--info    { background: var(--ws-blue-wash);    color: var(--ws-blue-deep); }

/* --- Progress ------------------------------------------------------------ */
.ws-progress { display: flex; align-items: center; gap: var(--ws-2); }
.ws-progress-track {
  flex: 1; min-width: 60px; height: 4px;
  background: var(--ws-line); border-radius: 2px; overflow: hidden;
}
.ws-progress-bar { height: 100%; background: var(--ws-blue); transition: width var(--ws-motion); }
.ws-progress-label { font-size: var(--ws-text-meta); color: var(--ws-muted); min-width: 34px; text-align: right; }

/* --- Alert --------------------------------------------------------------- */
.ws-alert {
  display: flex; align-items: flex-start; gap: var(--ws-2);
  padding: var(--ws-3);
  border: 1px solid;
  border-radius: var(--ws-radius-field);
  font-size: var(--ws-text-body);
}
.ws-alert svg { flex: none; margin-top: 2px; margin-right: 6px !important; }
.ws-alert-body { flex: 1; min-width: 0; }
.ws-alert-title { font-weight: var(--ws-weight-semi); }
.ws-alert--ok     { background: var(--ws-ok-wash);      border-color: var(--ws-ok);      color: var(--ws-ok); }
.ws-alert--warn   { background: var(--ws-warn-wash);    border-color: var(--ws-warn);    color: var(--ws-warn); }
.ws-alert--danger { background: var(--ws-danger-wash);  border-color: var(--ws-danger);  color: var(--ws-danger); }
.ws-alert--info   { background: var(--ws-blue-wash);    border-color: var(--ws-blue);    color: var(--ws-blue-deep); }
.ws-alert p { color: var(--ws-ink); }

/* --- Modal — native <dialog>. The only place the shadow is allowed. ------ */
.ws-modal {
  /* 100% et non 100vw : `vw` compte la barre de défilement, donc la modale
     dépassait de sa largeur sur un desktop qui en affiche une. Un <dialog>
     est déjà dimensionné par rapport au viewport. */
  width: min(520px, calc(100% - var(--ws-8)));

  /* Sans hauteur maximale, une modale longue — la suppression qui demande de
     retaper le titre, une liste de non-conformités — poussait son pied hors de
     l'écran : plus de bouton Annuler, plus de bouton Confirmer. Le corps
     défile, l'en-tête et le pied restent. */
  max-height: min(90dvh, 90vh);

  padding: 0;
  border: var(--ws-border);
  border-radius: var(--ws-radius-box);
  background: var(--ws-surface);
  color: var(--ws-ink);
  box-shadow: var(--ws-shadow-overlay);
}
/* La colonne flex ne vaut QUE pour la modale ouverte. Posée sur .ws-modal tout
   court, elle écrasait le `display: none` que le navigateur applique à un
   <dialog> fermé : toutes les modales de l'application s'affichaient en
   permanence, dans le flux de la page, sans fond ni centrage. */
.ws-modal[open] { display: flex; flex-direction: column; }
.ws-modal::backdrop { background: rgba(22, 33, 43, .45); }
.ws-modal-head {
  flex: none;
  display: flex; align-items: center; gap: var(--ws-3);
  padding: var(--ws-4);
  border-bottom: var(--ws-border);
}
.ws-modal-head .ws-title-section { flex: 1; min-width: 0; }
.ws-modal-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ws-4);
  overflow-wrap: anywhere;   /* un titre sans espaces ne casse pas la modale */
}
.ws-modal-foot {
  flex: none;
  display: flex; justify-content: flex-end; gap: var(--ws-2);
  padding: var(--ws-3) var(--ws-4);
  padding-bottom: calc(var(--ws-3) + env(safe-area-inset-bottom));
  border-top: var(--ws-border);
  background: var(--ws-bg);
  border-radius: 0 0 var(--ws-radius-box) var(--ws-radius-box);
}

@media (max-width: 480px) {
  /* Au pouce, deux boutons côte à côte dans 300px sont deux cibles étroites.
     Le pied passe en colonne, l'action principale au-dessus. */
  .ws-modal-foot { flex-direction: column-reverse; }
  .ws-modal-foot > * { width: 100%; }
}

/* --- Toast --------------------------------------------------------------- */
.ws-toasts {
  position: fixed; right: var(--ws-4); bottom: var(--ws-4);
  z-index: var(--ws-z-overlay);
  display: flex; flex-direction: column; gap: var(--ws-2);
}
.ws-toast {
  display: flex; align-items: flex-start; gap: var(--ws-2);
  min-width: 280px; max-width: 380px;
  padding: var(--ws-3);
  background: var(--ws-surface);
  border: var(--ws-border);
  border-left: 3px solid var(--ws-blue);
  border-radius: var(--ws-radius-field);
  box-shadow: var(--ws-shadow-overlay);
  font-size: var(--ws-text-body);
}
.ws-toast--ok     { border-left-color: var(--ws-ok); }
.ws-toast--danger { border-left-color: var(--ws-danger); }
.ws-toast-body { flex: 1; min-width: 0; }

/* --- Empty state --------------------------------------------------------- */
.ws-empty {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--ws-3);
  padding: var(--ws-12) var(--ws-4);
  text-align: center;
}
.ws-empty svg { color: var(--ws-line); }
.ws-empty-title { font-size: var(--ws-text-sub); font-weight: var(--ws-weight-semi); }
.ws-empty p { color: var(--ws-muted); max-width: 46ch; }

/* --- Loading ------------------------------------------------------------- */
.ws-spinner {
  width: 16px; height: 16px; flex: none;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: ws-spin 600ms linear infinite;
}
@keyframes ws-spin { to { transform: rotate(360deg); } }

.ws-skeleton {
  background: var(--ws-line);
  border-radius: var(--ws-radius-field);
  animation: ws-pulse 1.4s ease-in-out infinite;
}
@keyframes ws-pulse { 50% { opacity: .45; } }

@media (prefers-reduced-motion: reduce) {
  .ws-spinner { animation-duration: 2s; }
  .ws-skeleton { animation: none; opacity: .6; }
}

/* --- Save indicator — audit screen. Text carries state, not colour. ------ */
.ws-save {
  display: inline-flex; align-items: center; gap: var(--ws-1);
  font-size: var(--ws-text-meta);
  color: var(--ws-muted);
}
.ws-save[data-state="saving"] { color: var(--ws-muted); }
.ws-save[data-state="saved"]  { color: var(--ws-ok); }
.ws-save[data-state="error"]  { color: var(--ws-danger); font-weight: var(--ws-weight-med); }

/* --- Touch --------------------------------------------------------------
   Density is a mouse affordance. As soon as the pointer is a finger — or the
   viewport is tablet-sized — every dense control returns to the 44px rule.
   Both conditions are listed because a tablet reports coarse but a touchscreen
   laptop at desktop width should get it too. */
@media (max-width: 1024px), (pointer: coarse) {
  .ws-btn--sm { min-height: var(--ws-touch-min); padding: 0 var(--ws-4); }
  .ws-btn--icon.ws-btn--sm { width: var(--ws-touch-min); padding: 0; }
  .ws-th-sort { min-height: var(--ws-touch-min); }
  .ws-check input { width: 24px; height: 24px; margin-top: 8px; }
  .ws-check label { padding-top: 9px; }
  .ws-table tbody tr { height: var(--ws-touch-min); }
  .ws-table th, .ws-table td { padding: var(--ws-2); }
  /* Row links: stretch the hit area to the full cell height. */
  .ws-table td > a { display: inline-flex; align-items: center; min-height: var(--ws-touch-min); }
}

/* --- Stat — dashboard. Rules and type, not floating cards. --------------- */
/* Each cell draws its own right + bottom rule; the container clips the ones
   that land on its edge. Two alternatives were wrong: nth-child borders break
   when auto-fit rewraps, and a gap over a tinted container background paints
   any empty track at the end of the last row as a solid block. A cell that
   does not exist draws nothing. */
.ws-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  border: var(--ws-border);
  border-radius: var(--ws-radius-box);
  background: var(--ws-surface);
  overflow: hidden;
}
.ws-stat {
  padding: var(--ws-4);
  background: var(--ws-surface);
  box-shadow: 1px 0 0 0 var(--ws-line), 0 1px 0 0 var(--ws-line);
}
.ws-stat-value {
  font-size: var(--ws-text-page);
  font-weight: var(--ws-weight-semi);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.ws-stat-label { font-size: var(--ws-text-table); color: var(--ws-muted); margin-top: var(--ws-1); }

/* Questions marked "Hors PDF" need to be visually distinct so the auditor
   notices before filling 20 of them and discovering they won't appear. */
.qwrap[data-in-report="0"] {
  background: var(--ws-neutral-wash);
  border-left: 2px solid var(--ws-neutral) !important;
}
.qwrap[data-in-report="0"] .qblock > div:first-child .ws-label::after {
  content: " — hors rapport";
  font-size: var(--ws-text-meta);
  color: var(--ws-neutral);
  font-weight: var(--ws-weight-reg);
}

