/* Auto-generated by css:bundle — do not edit manually */

/* fulgore: auth/mfa_challenge_form.css */
.auth-mfa-challenge-form {
  max-width: 28rem;
  margin: var(--space-16) auto;
  display: grid;
  gap: var(--space-6);
}

.auth-mfa-challenge-form h2 {
  margin: 0;
}

.auth-mfa-challenge-form form {
  display: grid;
  gap: var(--space-5);
}


/* fulgore: auth/mfa_disable_modal.css */
.mfa-disable-modal__icon {
  text-align: center;
  font-size: 2rem;
  margin-bottom: var(--space-2);
}

.mfa-disable-modal__warning {
  color: var(--color-text-subtle);
  text-align: center;
  line-height: var(--line-height-loose);
}


/* fulgore: auth/mfa_setup_form.css */
/* LE QR A BESOIN D'UN FOND CLAIR, SINON IL NE SE SCANNE PAS.
   RQRCode le peint en `#000` et ne pose aucun fond (Mfa::Enroll, `color: "000"`).
   Un consumer dont le gabarit d'accès est sombre obtient donc du noir sur noir :
   mesuré au rendu WebKit sur Fatima le 2026-08-31, 1,03:1 de contraste. Le QR
   était bien là, bien centré, bien dimensionné — et parfaitement inscannable,
   donc personne ne s'enrôlait et personne n'entrait dans l'admin.

   Aucun spec ne pouvait le voir : la couleur des modules vit dans les octets du
   SVG, pas dans un style calculé. C'est la capture qui l'a sorti.

   La marge n'est pas décorative : les lecteurs de QR attendent une zone de
   silence autour du motif pour l'accrocher. */
.auth-mfa-setup-form {
  .mfa-qr-code {
    display: flex;
    justify-content: center;
    margin-block: var(--space-4);
    margin-inline: auto;
    width: fit-content;
    background: #fff;
    padding: var(--space-4);
    border-radius: var(--radius-md);
  }
}


/* fulgore: shared/errors/base.css */
.shared-errors-not-found,
.shared-errors-internal-server {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  text-align: center;
  gap: var(--space-6);
  padding: var(--space-10);
}

.shared-errors-not-found__code,
.shared-errors-internal-server__code {
  display: block;
  font-size: 8rem;
  font-weight: var(--font-weight-bold);
  color: var(--color-text-subtle);
  line-height: 1;
}

.shared-errors-not-found__title,
.shared-errors-internal-server__title {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

.shared-errors-not-found__description,
.shared-errors-internal-server__description {
  color: var(--color-text-subtle);
  margin: 0;
}

/* Raccourcis facultatifs sous le bouton de retour. Ils s'enroulent plutôt que
   de déborder : sur un écran étroit, quatre libellés ne tiennent pas sur une
   ligne. */
.shared-errors-not-found__links,
.shared-errors-internal-server__links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) var(--space-4);
  max-width: 100%;
}


/* fulgore: shared/flash.css */
@keyframes flash-enter {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes flash-exit {
  from {
    opacity: 1;
    transform: translateY(0);
  }

  to {
    opacity: 0;
    transform: translateY(-4px);
  }
}

.flash-container {
  
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  position: fixed;
  top: calc(var(--navbar-height) + var(--space-4));
  right: var(--space-6);
  z-index: 1000;

  
  min-width: 280px;
  max-width: 400px;
}

.flash-item {
  
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: var(--space-3);

  .svg {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }

  
  min-width: 280px;
  max-width: 400px;
  padding: var(--space-4);
  margin-bottom: var(--space-2);
  border-left: 3px solid;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.12));

  
  animation: flash-enter var(--duration-normal) var(--easing-default) both;

  &:last-child {
    margin-bottom: 0;
  }

  &.flash-item--exiting {
    animation: flash-exit var(--duration-fast) var(--easing-default) forwards;
  }
}

.flash-notice {
  background: var(--color-success-surface);
  color: var(--color-success);
  border-color: var(--color-success);
}

.flash-alert {
  background: var(--color-danger-surface);
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.flash-warning {
  background: var(--color-warning-surface);
  color: var(--color-warning);
  border-color: var(--color-warning);
}

.flash-info {
  background: var(--color-info-surface);
  color: var(--color-info);
  border-color: var(--color-info);
}

.flash-icon {
  
  flex-shrink: 0;

  
  width: var(--icon-size-sm);
  height: var(--icon-size-sm);
  margin-top: 1px;
}

.flash-body {
  
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--space-1);
}

.flash-message {
  
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1.4;

  flex: 1;
}

.flash-dismiss {
  
  flex-shrink: 0;

  
  padding: 0;
  border: none;

  
  line-height: 1;

  
  color: inherit;
  background: none;

  
  cursor: pointer;
  opacity: 0.6;
  transition: opacity var(--duration-fast) var(--easing-default);

  &:hover {
    opacity: 1;
  }

  .svg {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }

  svg {
    display: block;
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }
}


/* fulgore: surveys/base_completion_screen.css */
.surveys-base-completion-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-4);
  margin-block-start: var(--space-8);
}

.surveys-base-completion-screen__heading {
  font-size: var(--font-size-4xl);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
  margin: 0;
}

.surveys-base-completion-screen__description {
  font-size: var(--font-size-xl);
  color: var(--color-text-subtle);
  margin: 0;
  white-space: pre-line;
}

.surveys-base-completion-screen__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
  margin-block-end: var(--space-8);
}

.surveys-base-completion-screen__footer {
  align-self: flex-start;
}


/* fulgore: surveys/context_block.css */
.surveys-context-block {
  display: flex;
  flex-direction: column;
}

.surveys-context-block > * {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* ── Heading ── */

.surveys-context-block h2 {
  margin: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-brand);
}

/* ── Description ── */

.surveys-context-block p {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-normal);
  line-height: var(--line-height-loose);
  color: var(--color-text-subtle);
}

/* ── Label / Value pairs ── */

.surveys-context-block div > span:first-child {
  display: block;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-brand-light);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--space-1);
}

.surveys-context-block div > span:last-child {
  display: block;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-normal);
  color: var(--color-text-subtle);
}

/* ── Lists (steps, units) ── */

.surveys-context-block ol,
.surveys-context-block ul {
  margin: 0;
  padding-left: var(--space-5);
}

.surveys-context-block li {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-normal);
  line-height: var(--line-height-loose);
  color: var(--color-text-subtle);
  margin-bottom: var(--space-1);
}

.surveys-context-block ol li::marker {
  color: var(--color-brand-light);
  font-weight: var(--font-weight-semibold);
}


/* fulgore: surveys/inputs/base.css */
.surveys-inputs-base {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}


/* fulgore: surveys/inputs/boolean_card.css */
.ui-form:has(.surveys-inputs-boolean-card) {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-3);
}

.ui-form:has(.surveys-inputs-boolean-card) .form-actions,
.ui-form:has(.surveys-inputs-boolean-card) .ui-input {
  grid-column: 1 / -1;
}

.surveys-inputs-boolean-card__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-3);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);
  cursor: pointer;
  transition: border-color 200ms ease-out, box-shadow 200ms ease-out, transform 150ms ease-out;
  text-align: center;
}

.surveys-inputs-boolean-card__card:hover {
  border-color: var(--color-brand-light);
  box-shadow: 0 0 0 3px var(--color-brand-a10);
  transform: translateY(-2px);
}

.surveys-inputs-boolean-card__card:active {
  transform: translateY(0);
  box-shadow: none;
}

.surveys-inputs-boolean-card__checkbox:checked + .surveys-inputs-boolean-card__card {
  border-color: var(--color-brand);
  box-shadow: 0 0 0 3px var(--color-brand-a20);
}

.surveys-inputs-boolean-card__checkbox {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
  overflow: hidden;
}

.surveys-inputs-boolean-card__icon {
  font-size: var(--font-size-2xl);
  line-height: 1;
}

.surveys-inputs-boolean-card__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

@media (max-width: 767px) {
  .ui-form:has(.surveys-inputs-boolean-card) {
    grid-template-columns: repeat(2, 1fr);
  }
}


/* fulgore: surveys/inputs/choice_card.css */
.surveys-inputs-choice-card__form {
  display: contents;
}

.surveys-inputs-choice-card__button {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);

  padding: var(--space-5) var(--space-4);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);

  cursor: pointer;
  transition: border-color 200ms ease-out, box-shadow 200ms ease-out, transform 150ms ease-out;
}

.surveys-inputs-choice-card__button:hover {
  border-color: var(--color-brand-light);
  box-shadow: 0 0 0 3px var(--color-brand-a10);
  transform: translateY(-2px);
}

.surveys-inputs-choice-card__button:active {
  transform: translateY(0);
  box-shadow: none;
}

.surveys-inputs-choice-card__icon {
  font-size: var(--font-size-2xl);
  line-height: 1;
}

.surveys-inputs-choice-card__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  text-align: center;
}


/* fulgore: surveys/inputs/descriptive_choice.css */
.surveys-inputs-descriptive-choice {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
}

.surveys-inputs-descriptive-choice__form {
  display: contents;
}

.surveys-inputs-descriptive-choice__card {
  flex: 0 1 240px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-3);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);
  cursor: pointer;
  transition: border-color 200ms ease-out, box-shadow 200ms ease-out, transform 150ms ease-out;
  text-align: center;
}

.surveys-inputs-descriptive-choice__card:hover {
  border-color: var(--color-brand-light);
  box-shadow: 0 0 0 3px var(--color-brand-a10);
  transform: translateY(-2px);
}

.surveys-inputs-descriptive-choice__card:active {
  transform: translateY(0);
  box-shadow: none;
}

.surveys-inputs-descriptive-choice__card--selected {
  border-color: var(--color-brand);
  box-shadow: 0 0 0 3px var(--color-brand-a20);
}

.surveys-inputs-descriptive-choice__icon {
  font-size: var(--font-size-2xl);
  line-height: 1;
}

.surveys-inputs-descriptive-choice__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.surveys-inputs-descriptive-choice__description {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  line-height: var(--line-height-loose);
}



/* fulgore: surveys/map.css */
.surveys-map {
  --stop-height: 40px;
  --badge-size: 24px;

  position: relative;
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface-subtle);
}

.surveys-map__list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* ── SVG paths overlay ── */

.surveys-map__stops-wrapper {
  position: relative;
}

.surveys-map__paths {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--badge-size);
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 1;
}

.surveys-map__list {
  position: relative;
  z-index: 2;
}

/* ── Collapsible body ── */

.surveys-map__details {
  position: relative;
  z-index: 20;
  margin-top: var(--space-3);
}

.surveys-map__toggle {
  display: none;
}

.surveys-map__body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 400ms ease-out, opacity 400ms ease-out;
  opacity: 1;
}

.surveys-map__details:not([open]) .surveys-map__body {
  grid-template-rows: 0fr;
  opacity: 0;
}

.surveys-map__body > * {
  overflow: visible;
}

/* ── SVG path draw animation ── */

.surveys-map__segment {
  stroke-dasharray: var(--segment-length);
  stroke-dashoffset: var(--segment-length);
  animation: map-draw 500ms ease-out forwards;
}

.surveys-map__arc-segment {
  stroke-dasharray: var(--segment-length);
  stroke-dashoffset: var(--segment-length);
  animation: map-draw 800ms ease-in-out forwards;
  animation-delay: 200ms;
}

@keyframes map-draw {
  to { stroke-dashoffset: 0; }
}

/* Mobile — collapsed by default, toggle visible */
@media (max-width: 767px) {
  .surveys-map__toggle {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-brand);
    cursor: pointer;
    list-style: none;
    margin-bottom: var(--space-3);
  }

  .surveys-map__toggle::-webkit-details-marker {
    display: none;
  }

  .surveys-map__toggle::before {
    content: "▸ ";
  }

  .surveys-map__details[open] > .surveys-map__toggle::before {
    content: "▾ ";
  }
}


/* fulgore: surveys/map/arc.css */
/* Arc — loop-back indicator connecting a later stop back to an earlier one.
   Rendered as a left bracket with an arrow pointing up at the target. */

.surveys-map-arc {
  position: absolute;

  /* Vertical span: from target badge center to source badge center */
  top: calc(var(--space-6) + var(--space-3) + (var(--arc-to) + 0.5) * var(--stop-height));
  height: calc((var(--arc-from) - var(--arc-to)) * var(--stop-height));

  /* Horizontal: dedicated rail left of forks.
     Arc sits in the --arc-rail zone, forks sit in the --arc-indent zone. */
  left: 6px;
  right: calc(100% - var(--arc-rail) - var(--arc-indent) - var(--space-6) - var(--badge-size) / 2);

  /* Thinner than forks (2px vs 3px) to reduce visual noise */
  border: 2px solid var(--color-border);
  border-right: none;
  border-radius: var(--radius-md) 0 0 var(--radius-md);

  z-index: var(--arc-z);
  pointer-events: none;
}

/* Arrow pointing UP at the target stop */
.surveys-map-arc::before {
  content: "";
  position: absolute;
  top: -5px;
  right: -6px;

  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 6px solid var(--color-border);
}

/* Brand fill overlay — animates top-to-bottom (visual: "going back up") when loop is active */
.surveys-map-arc--active {
  border-color: var(--color-brand);
}

.surveys-map-arc--active::before {
  border-bottom-color: var(--color-brand);
}

.surveys-map-arc--active::after {
  content: "";
  position: absolute;
  top: -2px;
  left: -2px;
  bottom: -2px;
  right: 0;

  border: 2px solid var(--color-brand);
  border-right: none;
  border-radius: var(--radius-md) 0 0 var(--radius-md);

  clip-path: inset(100% 0 0 0);
  animation: arc-fill 800ms ease-out forwards;
  animation-delay: 400ms;
}

@keyframes arc-fill {
  0% { clip-path: inset(100% 0 0 0); }
  100% { clip-path: inset(0 0 0 0); }
}

/* Hide when map is collapsed on mobile */
@media (max-width: 767px) {
  .surveys-map__details:not([open]) ~ .surveys-map-arc {
    display: none;
  }
}


/* fulgore: surveys/map/fork.css */
/* Fork — left bracket connecting two badges (skip path) */

.surveys-map-fork {
  position: absolute;

  /* Top edge at fork-from badge center, bottom edge at fork-to badge center */
  top: calc(var(--space-6) + var(--space-3) + (var(--fork-from) + 0.5) * var(--stop-height));
  height: calc((var(--fork-to) - var(--fork-from)) * var(--stop-height));

  /* Left bracket in the arc-indent gutter (after the arc rail), right edge at badge center */
  left: calc(var(--arc-rail) + var(--space-6));
  right: calc(100% - var(--arc-rail) - var(--arc-indent) - var(--space-6) - var(--badge-size) / 2);

  border: 3px solid var(--color-border);
  border-right: none;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  z-index: 10;
}

/* Brand fill overlay — progressive top-to-bottom reveal when taken */
.surveys-map-fork--taken::after {
  content: "";
  position: absolute;
  top: -3px;
  left: -3px;
  bottom: -3px;
  right: 0;

  border: 3px solid var(--color-brand);
  border-right: none;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);

  clip-path: inset(0 0 100% 0);
  animation: fork-fill 1200ms ease-in-out forwards;
  animation-delay: 300ms;
}

@keyframes fork-fill {
  0% { clip-path: inset(0 0 100% 0); }
  100% { clip-path: inset(0 0 0 0); }
}

/* Hide when map is collapsed on mobile */
@media (max-width: 767px) {
  .surveys-map__details:not([open]) ~ .surveys-map-fork {
    display: none;
  }
}


/* fulgore: surveys/map/progress.css */
.surveys-map-progress {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);

  padding: var(--space-2) var(--space-3);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--color-brand);
}


/* fulgore: surveys/map/stop.css */
.surveys-map-stop {
  --badge-size: 24px;

  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: var(--stop-height);
  position: relative;
}

/* ── Badge ── */

.surveys-map-stop__badge {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;

  width: var(--badge-size);
  height: var(--badge-size);
  border-radius: var(--radius-full);

  position: relative;
  z-index: 20;

  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1;
}

/* ── Label ── */

.surveys-map-stop__label {
  font-size: var(--font-size-sm);
  line-height: var(--line-height-tight);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ── States ── */

/* Completed */
.surveys-map-stop--completed .surveys-map-stop__badge {
  background: var(--color-brand);
  color: var(--color-text-inverted);
}

.surveys-map-stop--completed .surveys-map-stop__label {
  color: var(--color-text-subtle);
}

.surveys-map-stop--completed .surveys-map-stop__badge .ui-icon {
  width: 14px;
  height: 14px;
}

/* Animated — pulse + check reveal (only on just-completed stop) */
.surveys-map-stop--animate .surveys-map-stop__badge {
  animation: badge-complete-pulse 250ms ease-out;
  animation-delay: calc(var(--stop-index, 0) * 150ms);
}

.surveys-map-stop--animate .surveys-map-stop__badge .ui-icon {
  opacity: 0;
  clip-path: inset(0 100% 0 0);
  animation: check-reveal 800ms ease-out forwards;
  animation-delay: calc(var(--stop-index, 0) * 150ms + 400ms);
}

@keyframes badge-complete-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.3); }
}

@keyframes check-reveal {
  0% {
    opacity: 0;
    clip-path: inset(0 100% 0 0);
  }
  20% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    clip-path: inset(0 0 0 0);
  }
}

/* Current */
.surveys-map-stop--current .surveys-map-stop__badge {
  background: var(--color-brand);
  color: var(--color-text-inverted);

  animation-name: badge-activate, survey-map-pulse;
  animation-duration: 300ms, 2s;
  animation-timing-function: ease-out, ease-in-out;
  animation-delay:
    calc(var(--stop-index, 0) * 150ms + 600ms),
    calc(var(--stop-index, 0) * 150ms + 900ms);
  animation-iteration-count: 1, infinite;
  animation-fill-mode: backwards, none;
}

@keyframes badge-activate {
  from {
    background: var(--color-surface-subtle);
    outline: 2px solid var(--color-border);
    outline-offset: -2px;
    color: transparent;
    box-shadow: none;
  }
  to {
    background: var(--color-brand);
    outline: 2px solid transparent;
    outline-offset: -2px;
    color: var(--color-text-inverted);
    box-shadow: none;
  }
}

.surveys-map-stop--current .surveys-map-stop__label {
  color: var(--color-text);
  font-weight: var(--font-weight-semibold);
}

@keyframes survey-map-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 var(--color-brand-a20);
  }
  50% {
    box-shadow: 0 0 0 6px var(--color-brand-a20);
  }
}

/* Upcoming */
.surveys-map-stop--upcoming .surveys-map-stop__badge {
  background: var(--color-surface-subtle);
  outline: 2px solid var(--color-border);
  outline-offset: -2px;
}

.surveys-map-stop--upcoming .surveys-map-stop__label {
  color: var(--color-text-disabled);
}

/* Skipped */
.surveys-map-stop--skipped .surveys-map-stop__badge {
  background: var(--color-surface-subtle);
  outline: 2px dashed var(--color-border);
  outline-offset: -2px;
}

.surveys-map-stop--skipped .surveys-map-stop__label {
  color: var(--color-text-disabled);
  text-decoration: line-through;
}

/* ── End stop ── */

.surveys-map-stop--end .surveys-map-stop__badge {
  width: calc(var(--badge-size) * 0.6);
  height: calc(var(--badge-size) * 0.6);
  margin-left: calc(var(--badge-size) * 0.2);
}

.surveys-map-stop--end.surveys-map-stop--completed .surveys-map-stop__badge {
  width: var(--badge-size);
  height: var(--badge-size);
  margin-left: 0;
  background: var(--color-success-surface);
  outline: none;
  font-size: var(--font-size-base);
  animation: tada-pop 600ms ease-out;
}

@keyframes tada-pop {
  0% { transform: scale(0); opacity: 0; }
  50% { transform: scale(1.4); opacity: 1; }
  70% { transform: scale(0.9); }
  100% { transform: scale(1); opacity: 1; }
}

.surveys-map-stop--end .surveys-map-stop__label {
  font-style: italic;
}

/* ── Clickable wrapper (jump_path active) ── */

.surveys-map-stop__form {
  display: contents;
}

.surveys-map-stop__button {
  all: unset;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  cursor: pointer;
}

.surveys-map-stop__button:hover .surveys-map-stop__label {
  color: var(--color-text);
}


/* fulgore: surveys/question_forms/footer.css */
.surveys-question-forms-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--space-6);
}

.surveys-question-forms-footer__submit {
  margin-left: auto;
}


/* fulgore: surveys/question_label.css */
.surveys-question-label {
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);

  color: var(--color-text);

  margin-bottom: var(--space-8);
}


/* fulgore: surveys/question_layout.css */
.surveys-question-layout {
  display: grid;
  /* start : la map et le bloc contexte gardent la hauteur de leur contenu
     au lieu de s'étirer à la hauteur de la colonne principale */
  align-items: start;
  gap: var(--space-6);
}

/* Le frame survey n'est pas un formulaire simple : lever le plafond 48rem
   posé par record-view sur son enfant unique, sinon la grille multi-colonnes
   ci-dessous s'applique dans 768px et écrase la colonne principale. */
.ui-bases-record-view__body > turbo-frame:only-child:has(.surveys-question-layout) {
  max-width: none;
}

.surveys-question-layout__context-block {
  min-height: var(--space-16);
  padding: var(--space-5) var(--space-6);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);
}

.surveys-question-layout__main {
  position: relative;
  z-index: 1;
}

/* ── Transitions ── */

.surveys-question-layout__main {
  opacity: 1;
}

@keyframes surveys-question-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.surveys-question-layout__main {
  animation: surveys-question-enter 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .surveys-question-layout__main {
    animation: none;
  }
}

@media (min-width: 768px) {
  .surveys-question-layout {
    grid-template-columns: 3fr 2fr;
    /* 3e rangée 1fr : absorbe l'excédent de hauteur de la colonne
       principale (section dépliée), sinon il se répartit sur la rangée 1
       et le bloc contexte tombe en bas de page. */
    grid-template-rows: auto auto 1fr;
    gap: var(--space-8);
  }

  .surveys-question-layout__main {
    grid-column: 1;
    grid-row: 1 / 4;
  }

  .surveys-map {
    grid-column: 2;
  }

  .surveys-question-layout__context-block {
    grid-column: 2;
  }
}

@media (min-width: 1440px) {
  .surveys-question-layout {
    grid-template-columns: 1fr 280px 280px;
    grid-template-rows: none;
  }

  .surveys-question-layout__main {
    grid-row: auto;
  }

  .surveys-map {
    grid-column: auto;
  }

  .surveys-question-layout__context-block {
    grid-column: auto;
  }
}


/* fulgore: ui/bases/button.css */
.ui-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-5);
  border-radius: var(--radius-md);
  font-family: var(--font-family-base);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  cursor: pointer;
  border: 1.5px solid transparent;
  text-decoration: none;
  white-space: nowrap;
  user-select: none;
  transition:
    background var(--duration-fast) var(--easing-default),
    color var(--duration-fast) var(--easing-default),
    border-color var(--duration-fast) var(--easing-default),
    box-shadow var(--duration-fast) var(--easing-default),
    opacity var(--duration-fast) var(--easing-default);

  .svg {
    height: var(--space-4);
    fill: currentColor;
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }

  

  &.ui-button--primary {
    background: var(--color-brand);
    color: var(--color-text-inverted);
    border-color: var(--color-brand);

    &:hover:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-brand-dark);
      border-color: var(--color-brand-dark);
    }

    &:active:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-brand-dark);
      box-shadow: none;
    }
  }

  &.ui-button--secondary {
    background: transparent;
    color: var(--color-brand);
    border-color: var(--color-brand);

    &:hover:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-brand-surface);
    }
  }

  &.ui-button--danger {
    background: var(--color-danger);
    color: var(--color-text-inverted);
    border-color: var(--color-danger);

    &:hover:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-danger-dark);
      border-color: var(--color-danger-dark);
    }

    &:active:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-danger-dark);
      box-shadow: none;
    }

    &:focus-visible {
      box-shadow: var(--focus-ring-danger);
    }
  }

  &.ui-button--ghost {
    background: transparent;
    color: var(--color-text-subtle);
    border-color: transparent;

    &:hover:not(:disabled):not(.ui-button--disabled) {
      background: var(--color-surface-raised);
      color: var(--color-text);
    }

    &:focus-visible {
      box-shadow: var(--focus-ring-neutral);
    }
  }

  

  &.ui-button--sm {
    font-size: var(--font-size-xs);
    padding: var(--space-1) var(--space-3);
  }

  &.ui-button--lg {
    font-size: var(--font-size-base);
    padding: var(--space-3) var(--space-6);
  }

  &.ui-button--icon-only {
    padding: var(--space-2);

    /* Shield the icon from consumer resets like `svg { max-width: 100% }`:
       100% of a shrink-to-fit content box computes to 0 width and hides
       the icon entirely — same trap as the sortable grip (cf. row.css). */
    .svg {
      width: var(--space-4);
      min-width: var(--space-4);
      max-width: none;
    }

    &.ui-button--sm {
      padding: var(--space-1);
    }

    &.ui-button--lg {
      padding: var(--space-3);
    }
  }



  /* L'ÉTAT DÉSACTIVÉ SE LIT « INACTIF », PAS « CASSÉ ». `opacity: 0.45` délave
     la couleur de marque du consumer sans rien garantir : sur le terracotta de
     FBordes, le libellé blanc tombait à ~2,6:1 (contre 6,44:1 à l'état actif) —
     sous les deux planchers WCAG. Et c'est le PREMIER état que voit un visiteur
     d'un écran de connexion, dont `form-validation` désactive le bouton tant que
     les champs sont vides.
     Des couleurs franches le disent sans ambiguïté et ne dépendent plus de la
     teinte de marque : mesuré 5,69:1 au navigateur le 2026-08-03, dans les deux
     moteurs et aux deux tailles d'écran.
     ⚠️ CE SIDECAR EST LA COPIE SERVIE — voir la note jumelle dans
     `ui/inputs/password.css`. `app/assets/stylesheets/fulgore/button.css` porte
     une seconde définition qui n'atteint aucun consumer. */
  &:disabled,
  &.ui-button--disabled {
    background: var(--color-surface-raised);
    border-color: var(--color-border);
    color: var(--color-text-subtle);
    cursor: not-allowed;
    pointer-events: none;
  }
}


/* fulgore: ui/bases/card.css */
.ui-bases-card {
  box-sizing: border-box;
  margin-inline: auto;
  padding: var(--space-8);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  background: var(--color-surface);
}

.ui-bases-card__heading {
  display: grid;
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  grid-template-columns: min-content auto;
  align-items: end;
}

.ui-bases-card__title {
  font: inherit;
}

.ui-bases-card__details {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--space-2) var(--space-6);
}

@media (max-width: 480px) {
  .ui-bases-card {
    padding: var(--space-4);
  }

  .ui-bases-card__details {
    grid-template-columns: 1fr;
  }
}


/* fulgore: ui/bases/card_row.css */
.ui-bases-card-row__dt {
  grid-column: 1;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  font-weight: var(--font-weight-medium);
}

.ui-bases-card-row__dd {
  grid-column: 2;
  margin: 0;
  font-size: var(--font-size-sm);
}

@media (max-width: 480px) {
  .ui-bases-card-row__dt {
    grid-column: auto;
  }

  .ui-bases-card-row__dd {
    grid-column: auto;
  }
}


/* fulgore: ui/bases/context_block.css */
.ui-bases-context-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  border: 1.5px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  background: var(--color-brand-surface);

  .ui-bases-context-block__heading {
    font-size: var(--font-size-base);
    font-weight: var(--font-weight-semibold);
    color: var(--color-brand);
  }

  .ui-bases-context-block__description {
    font-size: var(--font-size-sm);
    color: var(--color-text-subtle);
  }

  .ui-bases-context-block__list {
    margin: 0;
    padding-left: var(--space-5);

    & li {
      font-size: var(--font-size-sm);
      color: var(--color-text-subtle);
      margin-bottom: var(--space-1);
    }
  }
}

.ui-bases-context-block--warning {
  background: var(--color-warning-surface);
  border-color: var(--color-warning-a10);
  margin-bottom: var(--space-6);

  .ui-bases-context-block__heading {
    color: var(--color-warning);
  }
}


/* fulgore: ui/bases/edit.css */
.ui-bases-edit__header {
  margin-block-end: var(--space-6);
}

.ui-bases-edit__body {
}


/* fulgore: ui/bases/filter_bar.css */
/* `flex-start` et non `center` : dès qu'une barre porte des champs de hauteurs
   différentes (un multi-select à huit options est bien plus haut qu'une
   recherche), le centrage aligne chaque champ sur le milieu de sa ligne et
   dessine un escalier. Sur une barre à champs de même hauteur — le cas de tous
   les consumers jusqu'ici — les deux valeurs rendent à l'identique. */
.ui-filter-bar__fields {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.filter-field__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-subtle);
  margin-right: var(--space-2);
}

/* ⚠️ `flex` ET NON `inline-flex`, et c'est le contraire d'un détail. Un
   `inline-flex` se dimensionne sur son CONTENU : sa largeur est « indéfinie »
   au sens de CSS, et tout `width: %` d'un enfant s'y résout alors contre rien
   — l'input retombe sur sa taille intrinsèque quelle que soit la place qu'on
   lui offre. Mesuré le 2026-08-03 : 302 px pris dans un conteneur de 328,
   et 302 px dans une piste de grille de 416 chez RatsAndFrogs, où la barre
   paraissait décentrée alors que la GRILLE était juste.
   Le passage en `flex` (donc de niveau bloc) ne change RIEN aux barres à
   plusieurs champs : `.filter-field--search` y est un élément flex de
   `.ui-filter-bar__fields`, dimensionné sur son `max-content` — vérifié au
   rendu avant/après, à 1280 et 390 px, sur `UI::Bases::ExampleFilterBar`. */
.filter-search-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.filter-search {
  padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  background: var(--color-surface);
  color: var(--color-text);
  min-width: 16rem;

  /* ⚠️ `flex: 1` EST LA SECONDE MOITIÉ DU CORRECTIF, et sans elle la première
     ne sert à rien : passer le conteneur en `flex` lui donne une largeur
     définie, mais l'input reste `flex: 0 1 auto`, donc dimensionné sur son
     contenu. Mesuré le 2026-08-03 par injection : conteneur porté à 600 px,
     input toujours à 302. Les deux règles se tiennent — ne pas en retirer une
     en croyant l'autre suffisante.
     ⚠️⚠️ `flex: 1` ET NON `width: 100%`, essayé d'abord et REVERTÉ : un `100%`
     oblige à déclarer `box-sizing: border-box` (la gem n'a aucune remise à
     zéro globale — chaque composant le déclare chez lui), et ce border-box
     fait alors compter les 46 px de padding et de bordure DANS le `min-width:
     16rem` ci-dessus, qui a toujours été écrit en largeur de contenu. Mesuré :
     les champs des barres passaient de 302 à 256 px, soit 46 px perdus dans
     les dix-sept barres de la flotte. Le flex, lui, dimensionne sur la boîte
     de bordure quel que soit le `box-sizing` — rien à compenser. */
  flex: 1;

  &:focus {
    outline: none;
    border-color: var(--color-brand);
  }
}

.filter-search-clear {
  position: absolute;
  right: var(--space-2);
  display: none;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  background: var(--color-neutral-300);
  color: var(--color-neutral-700);
  border: none;
  border-radius: 999px;
  font-size: var(--font-size-xs);
  line-height: 1;
  cursor: pointer;
  padding: 0;

  &:hover {
    background: var(--color-neutral-400);
  }
}

.filter-search:not(:placeholder-shown) ~ .filter-search-clear {
  display: flex;
}

/* `min-width: 0` est le remède au piège WebKit : un item flex garde par défaut
   `min-width: auto` et refuse de se rétrécir sous son `min-content` — un
   multi-select à beaucoup d'options débordait alors sa case et se dessinait
   PAR-DESSUS le champ voisin. Mesuré en WebKit : recouvrement du champ de
   recherche par les options de type, ramené à zéro par cette règle. */
.filter-field--multi-select {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-width: 0;
  max-width: 100%;
}

.filter-multi-select {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.filter-multi-select__option {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-subtle);
  cursor: pointer;
  transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);

  input[type="checkbox"] {
    display: none;
  }

  &:hover {
    border-color: var(--color-brand);
    color: var(--color-brand);
  }

  &.is-active {
    background: var(--color-brand-surface);
    border-color: var(--color-brand);
    color: var(--color-brand);
    font-weight: var(--font-weight-semibold);
  }

  /* Le badge se teinte par `currentColor` plutôt qu'en dur : l'option prend
     l'une des 12 teintes de `UI::Pill` quand elle est active, et le compteur
     suit sans qu'on redéclare douze règles. */
  .filter-multi-select__count {
    margin-left: var(--space-2);
    min-width: 1.25em;
    padding: 0 var(--space-1);
    border-radius: 999px;
    background: color-mix(in srgb, currentColor 15%, transparent);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    line-height: 1.5;
    text-align: center;
    font-variant-numeric: tabular-nums;
  }
}

.filter-multi-select__option.is-active.ui-pill--violet {
  background: var(--color-violet-100);
  color: var(--color-violet-700);
  border-color: var(--color-violet-100);
}

.filter-multi-select__option.is-active.ui-pill--blue {
  background: var(--color-blue-100);
  color: var(--color-blue-700);
  border-color: var(--color-blue-100);
}

.filter-multi-select__option.is-active.ui-pill--green {
  background: var(--color-green-100);
  color: var(--color-green-700);
  border-color: var(--color-green-100);
}

.filter-multi-select__option.is-active.ui-pill--red {
  background: var(--color-red-100);
  color: var(--color-red-700);
  border-color: var(--color-red-100);
}

.filter-field--switch {
  display: flex;
  align-items: center;
}

.filter-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  cursor: pointer;

  input[type="checkbox"] {
    width: 1rem;
    height: 1rem;
    accent-color: var(--color-brand);
    cursor: pointer;
  }
}


/* fulgore: ui/bases/form.css */
.ui-bases-form {
  form {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
  }
}

.ui-form {
  display: grid;

  .ui-input + .ui-input {
    margin-top: var(--space-6);
  }
}

.ui-form__footer {
  margin: var(--space-4) 0 0;
  text-align: center;
  font-size: var(--font-size-sm);

  a {
    color: var(--color-text-subtle);
    transition: color var(--duration-fast) var(--easing-default);

    &:hover {
      color: var(--color-brand);
    }
  }
}

.form-error-summary {
  padding: var(--space-3) var(--space-4);
  background: var(--color-danger-a10);
  border: 1.5px solid var(--color-danger);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);

  .form-error-summary__title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-danger);
    margin-bottom: var(--space-2);
  }

  ul {
    margin: 0;
    padding-left: var(--space-4);

    li {
      font-size: var(--font-size-sm);
      color: var(--color-danger);
    }
  }
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
  justify-content: flex-end;
}


/* fulgore: ui/bases/input.css */
.ui-input {
  display: grid;
  gap: var(--space-2);

  label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* gap : espace minimal entre le texte et le tooltip quand le label est
       à largeur de contenu (checkbox) — space-between n'y suffit pas */
    gap: var(--space-2);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
  }

  input,
  textarea {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    color: var(--color-text);
    background: var(--color-surface);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    outline: none;
    box-sizing: border-box;
    transition:
      border-color var(--duration-fast) var(--easing-default),
      box-shadow var(--duration-fast) var(--easing-default);

    &::placeholder {
      color: var(--color-text-disabled);
    }

    &:focus {
      border-color: var(--color-brand);
      box-shadow: var(--focus-ring);
    }
  }

  textarea {
    resize: vertical;
    min-height: 6rem;
    line-height: var(--line-height-normal);
  }

  .ui-input__required {
    color: var(--color-danger);
    margin-left: var(--space-1);
  }

  .ui-input__hint {
    font-size: var(--font-size-xs);
    color: var(--color-text-subtle);
  }

  .ui-input__error {
    font-size: var(--font-size-xs);
    color: var(--color-danger);
  }

  

  &.ui-input--error {
    label {
      color: var(--color-danger);
    }

    input,
    textarea {
      border-color: var(--color-danger);

      &:focus {
        box-shadow: 0 0 0 3px var(--color-danger-a10);
      }
    }
  }

  

  &.ui-input--disabled {
    opacity: 0.5;
    cursor: not-allowed;

    input,
    textarea {
      cursor: not-allowed;
    }
  }
}


/* fulgore: ui/bases/list.css */
.ui-bases-list {
  padding: var(--space-8);
  display: grid;
  gap: var(--space-6);

  h1 {
    margin: 0;
  }
}

.ui-list-sort-disabled-notice {
  margin: var(--space-2) 0 0;
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
}

.ui-list-table-wrapper {
  overflow-x: auto;
}

.ui-list-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;

  th,
  td {
    padding: var(--space-3) var(--space-4);
    text-align: left;
    vertical-align: middle;
    border-bottom: 1px solid var(--color-border);
    font-size: var(--font-size-sm);
  }

  th {
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-subtle);
    font-size: var(--font-size-sm);
  }

  /* ⚠️ MONTÉES ICI LE 2026-09-02, ET ELLES CHANGENT L'ASPECT DE TOUTES LES
     LISTES DU PARC. C'étaient les deux seules choses qu'une table de la gem ne
     donnait pas, et deux sidecars de Fatima les recopiaient donc à l'identique.
     Les laisser dehors condamnait chaque consumer à les réécrire pour obtenir
     un tableau d'apparence ordinaire.
     Sous 768 px, `thead` est masqué par la bascule en cartes : la première
     règle y est inerte, la seconde teinte la carte survolée. */
  thead {
    background-color: var(--color-surface-subtle);
  }

  tbody tr:hover {
    background-color: var(--color-surface-subtle);
  }

  .ui-list-table__th--sortable {
    padding: 0;
  }

  .ui-col--spacer {
    width: 100%;
  }

  .ui-col--thumb {
    width: 3rem;
  }

  /* ⚠️ `white-space: nowrap` A CESSÉ D'ÊTRE LE DÉFAUT DE TOUTE CELLULE — NE PAS
     LE REMETTRE SUR `th, td`. Il l'a été jusqu'au 2026-08-02, et il fabriquait
     un débordement là où il n'y avait aucune raison d'en avoir un : la largeur
     du tableau devenait la somme de ses contenus les plus longs tenus sur UNE
     ligne. Mesuré en production sur un tableau de cinq colonnes dans 880 px de
     large — 1010 px de contenu, donc 130 px cachés, à cause d'une seule cellule
     de texte libre (« Me contacter (différents packs proposés) »).

     Et ce débordement n'était pas qu'un défilement de plus : il décalait la
     colonne d'actions collée à droite, qui RECOUVRAIT alors la dernière colonne
     de données. Chez le consumer c'était « Publiée », le champ qui dit si une
     fiche est en ligne — 7 en-têtes dans le DOM, 6 visibles à l'écran, dans les
     deux moteurs. Rien ne rougissait : le `<th>` est bien rendu, il n'est ni
     clippé ni hors cadre, il est simplement PEINT DESSOUS.

     La colonne collée n'était donc pas le défaut, elle en était le symptôme.
     Le `nowrap` général était la cause, et il aurait rejoué sur le prochain
     tableau du prochain site.

     Il reste juste là où couper serait pire que replier : un libellé d'en-tête,
     une vignette, la poignée, le rang, et la cellule d'actions — tous courts ou
     atomiques. Pour une cellule de données qui ne doit jamais se couper (une
     date, un montant, une référence), l'opt-in est `ui-col--nowrap`, dans la
     même famille que `ui-col--optional` / `--important` / `--thumb`.

     Borné à partir de 769 px : en dessous, le tableau devient une pile de
     cartes et TOUT doit se replier (cf. le bloc `max-width: 768px`). */
  @media (min-width: 769px) {
    th,
    .ui-col--nowrap,
    .ui-col--thumb,
    .ui-bases-row__grip,
    .ui-bases-row__position,
    .ui-list-table__td--actions,
    .ui-bases-row__actions {
      white-space: nowrap;
    }
  }

  .ui-list-table__th--right {
    text-align: right;
  }

  .sort-link {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    color: var(--color-text-subtle);
    text-decoration: none;
    white-space: nowrap;

    &:hover {
      color: var(--color-brand);
    }

    &.sort-link--active {
      color: var(--color-brand);
    }
  }

  .sort-arrow {
    font-size: var(--font-size-xs);
    opacity: 0.5;
  }

  .sort-link--active .sort-arrow {
    opacity: 1;
  }

  .ui-list-table__th--actions,
  .ui-list-table__td--actions,
  .ui-bases-row__actions {
    position: sticky;
    right: 0;
    background: var(--color-surface);
    box-shadow: -4px 0 8px -2px rgb(0 0 0 / 0.04);
  }

  /* `position: sticky` ALWAYS creates a stacking context, so every Actions
     cell is its own layer painted in DOM order — and a RowActionsMenu's
     `z-index: var(--z-dropdown)` only ever ranks INSIDE its own cell. Result:
     an open kebab menu is painted over by the next row's Actions cell (33% of
     the panel covered, measured WebKit + Chromium). Raising just the cell that
     holds an open menu lifts its whole layer above its siblings, which are all
     at `z-index: auto`. Floating mode (`position: fixed`, dropdown_controller)
     escapes CLIPPING, not stacking — it does not help here.
     Keep this rule INSIDE the `.ui-list-table` block: flattened out it drops
     from (0,2,0) to (0,1,0) and is silently outranked. */
  .ui-list-table__td--actions:has(.ui-row-actions-menu.is-open),
  .ui-bases-row__actions:has(.ui-row-actions-menu.is-open) {
    z-index: 1;
  }

}

/* ⚠️ ICI VIVAIT UNE RÈGLE QUI ÉTEIGNAIT L'OMBRE DE LA COLONNE COLLÉE AU-DESSUS
   DE 1025 px — NE PAS LA REMETTRE. L'ombre est le SEUL indice qui dise que
   cette colonne flotte au-dessus du reste et qu'il reste du contenu à sa
   gauche. La couper aux largeurs de bureau, c'est la couper exactement là où
   on regarde : le 2026-08-02, en production à 1280, une colonne de données
   entière était recouverte sans qu'aucun signe ne l'indique — et la barre de
   défilement du cadre mesure 0 px de haut (barre superposée macOS, invisible
   tant qu'on ne fait pas le geste). Aucun indice, donc, d'aucune sorte.

   Le pari de cette règle était « à cette largeur le tableau tient, l'ombre
   serait du bruit ». Il ne tient pas : un tableau déborde en fonction de son
   CONTENU, pas de la largeur de l'écran. Et le bruit est faible — l'ombre vaut
   4 % d'opacité, invisible quand la colonne est à sa place naturelle. */

@media (max-width: 1024px) {
  .ui-list-table .ui-col--optional {
    display: none;
  }
}

@media (max-width: 768px) {
  .ui-list-table {
    display: block;
    width: 100%;

    thead {
      display: none;
    }

    tbody {
      display: block;
      width: 100%;
    }

    .ui-col--optional,
    .ui-col--important {
      display: flex;
    }

    tbody tr {
      display: block;
      width: 100%;
      box-sizing: border-box;
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      margin-bottom: var(--space-3);
      padding: var(--space-2);
      box-shadow: var(--shadow-md);
    }

    td {
      display: flex;
      width: 100%;
      box-sizing: border-box;
      justify-content: space-between;
      align-items: center;
      border-bottom: 1px solid var(--color-border);
      white-space: normal;
    }


    td:last-child {
      border-bottom: none;
    }

    .ui-col--spacer {
      display: none;
    }

    .ui-col--card-hidden {
      display: none;
    }

    td::before {
      content: attr(data-label);
      font-weight: var(--font-weight-semibold);
      color: var(--color-text-subtle);
      font-size: var(--font-size-sm);
      flex-shrink: 0;
      margin-right: var(--space-3);
    }

    .ui-list-table__td--actions {
      position: static;
      box-shadow: none;
    }
  }
}

@media (max-width: 480px) {
  .ui-bases-list {
    padding: var(--space-3);
  }
}


/* fulgore: ui/bases/modal.css */
.ui-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  align-items: center;
  justify-content: center;

  &.is-open {
    display: flex;

    .ui-modal__backdrop {
      animation: fade-in var(--duration-normal) var(--easing-out) both;
    }

    .ui-modal__dialog {
      animation: scale-in var(--duration-normal) var(--easing-spring) both;
    }
  }
}

.ui-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.ui-modal__dialog {
  position: relative;
  white-space: normal;
  background: var(--color-surface);
  border-radius: var(--radius-xl);
  padding: var(--space-8);
  width: 100%;
  max-width: 32rem;
  max-height: calc(100vh - var(--space-8) * 2);
  margin: var(--space-4);
  box-shadow: var(--shadow-xl);
  overflow-x: hidden;
  overflow-y: auto;
}

@media (max-width: 1023px) {
  .ui-modal {
    padding-top: var(--navbar-height);
  }

  .ui-modal__dialog {
    max-height: calc(100vh - var(--navbar-height) - var(--space-8));
  }
}

.ui-modal__dialog > .ui-button {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
}

.ui-modal__body {
  max-width: 100%;
  overflow-wrap: break-word;
  text-align: center;
}

.ui-modal__header {
  margin-bottom: var(--space-4);
  text-align: center;
}

.ui-modal__footer {
  margin-top: var(--space-6);
}

.ui-modal__footer .form-actions {
  justify-content: center;
}

.ui-modal__footer .form-actions:has(> :nth-child(2)) {
  justify-content: space-between;
}

@media (max-width: 480px) {
  .ui-modal__dialog {
    padding: var(--space-5);
    margin: var(--space-2);
  }
}


/* fulgore: ui/bases/navbar.css */
.ui-bases-navbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--space-4);
  height: var(--navbar-height);
  padding: var(--space-1) var(--space-4);
  box-sizing: border-box;
  align-items: center;
  background: var(--color-brand);
  color: var(--color-text-inverted);
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}

.ui-bases-navbar a {
  display: block;
  color: inherit;
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--easing-default);
}

.ui-bases-navbar a:hover {
  background: rgba(255, 255, 255, 0.15);
}

/* ── Left section ── */

.navbar-left {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.navbar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: inherit;
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--easing-default);
  width: fit-content;
}

.navbar-brand:hover {
  background: rgba(255, 255, 255, 0.15);
}

.navbar-brand svg {
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
}

/* ── Hamburger ── */

.navbar-hamburger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: none;
  background: transparent;
  border-radius: var(--radius-md);
  color: var(--color-text-inverted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--easing-default);
}

.navbar-hamburger:hover {
  background: rgba(255, 255, 255, 0.15);
}

.navbar-hamburger:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ── Center section ── */

.navbar-nav {
  display: flex;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.navbar-link {
  display: block;
  color: inherit;
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  transition: background var(--duration-fast) var(--easing-default);
}

.navbar-link:hover {
  background: rgba(255, 255, 255, 0.15);
}

.navbar-link--active {
  background: rgba(255, 255, 255, 0.25);
  font-weight: var(--font-weight-semibold);
}

/* ── Right section ── */

.navbar-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

/* ── Responsive ── */

@media (max-width: 1023px) {
  .ui-bases-navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
  }
}


/* fulgore: ui/bases/new.css */
.ui-bases-new__header {
  margin-block-end: var(--space-6);
}

.ui-bases-new__body {
}


/* fulgore: ui/bases/page_header.css */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.page-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.page-header--size-md .page-title {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  margin: 0;
}

.page-header--size-sm .page-title {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  margin: 0;
}

@media (max-width: 480px) {
  .page-header {
    flex-direction: column;
    gap: var(--space-3);
  }
}


/* fulgore: ui/bases/progress_bar.css */
.ui-bases-progress-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ui-bases-progress-bar__label {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-subtle);
}

.ui-bases-progress-bar__track {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.ui-bases-progress-bar__bar {
  flex: 1;
  height: var(--space-2);
  border-radius: var(--radius-full);
  background-color: var(--color-surface-subtle);
  overflow: hidden;
}

.ui-bases-progress-bar__fill {
  height: 100%;
  border-radius: var(--radius-full);
  background-color: var(--color-text-subtle);
  transition: width var(--duration-normal) var(--easing-default);
}

.ui-bases-progress-bar--brand .ui-bases-progress-bar__fill {
  background-color: var(--color-brand);
}

.ui-bases-progress-bar--accent .ui-bases-progress-bar__fill {
  background-color: var(--color-accent);
}

.ui-bases-progress-bar--sm .ui-bases-progress-bar__bar {
  height: var(--space-1);
}

.ui-bases-progress-bar--sm .ui-bases-progress-bar__percentage {
  font-size: var(--font-size-xs);
}

.ui-bases-progress-bar__percentage {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  font-variant-numeric: tabular-nums;
  min-width: 3ch;
  text-align: right;
}


/* fulgore: ui/bases/record_view.css */
.ui-bases-record-view__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* wrap + min-width 0 : sans eux, une zone d'actions plus large que
     l'espace restant déborde du viewport mobile (flex min-width:auto). */
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.ui-bases-record-view__header > * {
  min-width: 0;
}

.ui-bases-record-view__title-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin: var(--space-6) 0 var(--space-8) 0;
}

.ui-bases-record-view__body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
  align-items: start;
}

.ui-bases-record-view__body > :only-child {
  grid-column: 1 / -1;
  max-width: 48rem;
  margin-inline: auto;
  width: 100%;
}

.ui-bases-record-view__body > .ui-bases-record-view__layout {
  max-width: none;
  margin-inline: 0;
}

/* ── Layout (used by Show/Edit body_content) ── */

.ui-bases-record-view__layout {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* ── Row (2-column grid) ── */

.ui-bases-record-view__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
  align-items: start;
}

@media (max-width: 768px) {
  .ui-bases-record-view__body {
    grid-template-columns: 1fr;
  }

  .ui-bases-record-view__row {
    grid-template-columns: 1fr;
  }
}


/* fulgore: ui/bases/record_view_title.css */
.ui-bases-record-view-title {
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);
  color: var(--color-text);
  margin: 0;
}


/* fulgore: ui/bases/row.css */
.ui-bases-row__actions-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

@media (max-width: 480px) {
  .ui-bases-row__actions-inner {
    justify-content: flex-end;
    flex-wrap: wrap;
  }
}


@keyframes position-flip {
  0%   { transform: perspective(120px) rotateX(0deg);   opacity: 1; }
  45%  { transform: perspective(120px) rotateX(-90deg); opacity: 0; }
  55%  { transform: perspective(120px) rotateX(-90deg); opacity: 0; }
  100% { transform: perspective(120px) rotateX(0deg);   opacity: 1; }
}

.ui-bases-row__position {
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  user-select: none;
}

.ui-bases-row__position.is-flipping {
  animation: position-flip 300ms ease-in-out;
  will-change: transform;
}

/* td-qualified: must outrank `.ui-list-table td` (list.css) or its padding
   collapses the cell content box to 0 and hides the handle. */
td.ui-bases-row__grip {
  width: 1.25rem;
  padding-right: 0;
  color: var(--color-text-subtle);
  user-select: none;
  cursor: grab;

  &:active {
    cursor: grabbing;
  }

  /* Shield the handle from consumer resets like `svg { max-width: 100% }`
     (100% of a zero-width content box renders the handle invisible). */
  & svg {
    width: 10px;
    min-width: 10px;
    max-width: none;
  }
}

/* The handle is a real button so it can be reached by Tab and driven by the
   arrow keys — mouse dragging is not the only way to reorder. It must not look
   like one: no native chrome, it inherits the cell's colour and grab cursor.
   The focus ring is the one thing it keeps, and it must stay visible — it is
   the only cue telling a keyboard user which row the arrows will move. */
button.ui-bases-row__grip-button {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: inherit;
  font: inherit;
  line-height: 0;

  /* Même anneau que partout ailleurs dans la gem (bouton, menu d'actions,
     lien de barre latérale) : `--focus-ring`, jamais un outline maison. */
  &:focus-visible {
    outline: none;
    border-radius: var(--radius-sm);
    box-shadow: var(--focus-ring);
  }
}

tr[draggable="true"] {
  cursor: grab;

  
  & td:not(:has(a, button)) {
    cursor: grab;
  }

  &:active,
  &:active td:not(:has(a, button)) {
    cursor: ns-resize;
  }
}

tr.sortable--dragging,
tr.sortable--dragging td {
  cursor: ns-resize;
}

tr.sortable--dragging {
  opacity: 0.7;
  background: var(--color-surface-raised);
  outline: 1.5px dashed var(--color-border-strong);
  outline-offset: -1px;
  box-shadow: var(--shadow-md);
}


/* fulgore: ui/bases/show.css */
.ui-bases-show__header {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  margin-block-end: var(--space-6);

  .ui-links-back {
    margin-block: 0;
  }
}

.ui-bases-show__body {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: var(--space-8);
}

.ui-bases-show__list {
  width: 100%;
  margin-block-start: var(--space-8);
}

.ui-bases-show__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-block-start: var(--space-8);
}

@media (max-width: 767px) {
  .ui-bases-show__body {
    grid-template-columns: 1fr;
  }

  .ui-bases-show__header {
    grid-template-columns: 1fr;
  }
}


/* fulgore: ui/bases/sidebar.css */
/* Le panneau reçoit le focus à l'ouverture en mobile (cf. nav_sidebar_controller
   #enableFocusTrap) : ce n'est pas un contrôle, aucun anneau ne doit s'y
   dessiner. Sélecteur d'attribut et non de classe — la règle doit valoir pour
   toute sidebar branchée sur le contrôleur, y compris les panneaux propres aux
   consumers qui n'utilisent pas .sidebar. */
[data-nav-sidebar-target="sidebar"]:focus {
  outline: none;
}

.sidebar {
  position: fixed;
  top: var(--navbar-height);
  left: 0;
  bottom: 0;
  width: var(--sidebar-width);
  background: var(--color-surface);
  border-right: 1px solid var(--color-border);
  padding: var(--space-4) 0;
  overflow-y: auto;
  z-index: var(--z-raised);
  box-sizing: border-box;
}

.sidebar nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.sidebar-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  color: var(--color-text-subtle);
  text-decoration: none;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  transition: background var(--duration-fast), color var(--duration-fast);
  border-left: 3px solid transparent;
}

.sidebar-link:hover {
  background: var(--color-surface-subtle);
  color: var(--color-text);
}

.sidebar-link:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.sidebar-link svg {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}

.sidebar-link--active {
  color: var(--color-brand);
  background: var(--color-brand-surface);
  border-left-color: var(--color-brand);
  font-weight: var(--font-weight-semibold);
}

/* ── Responsive ── */

@media (max-width: 1023px) {
  .sidebar {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar-link {
    transition: none;
  }
}


/* fulgore: ui/bases/stat_card.css */
.ui-stat-card {
  padding: var(--space-8);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  background: var(--color-surface);
}

.ui-stat-card__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.ui-stat-card__title {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  line-height: 1.2;
  margin: 0;
}


/* fulgore: ui/bases/view_actions.css */
.ui-bases-view-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}


/* fulgore: ui/buttons/switch.css */
.ui-buttons-switch {
  display: inline-flex;
  cursor: pointer;
  align-items: center;

  form,
  button {
    display: contents;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
  }
}

.ui-buttons-switch__track {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 2.75rem;
  height: 1.5rem;
  border-radius: var(--radius-full);
  background-color: var(--color-border);
  transition: background-color var(--duration-slow) var(--easing-default);
  padding: 0 var(--space-1);
  box-sizing: border-box;
  box-shadow:
    inset 0 2px 4px rgba(0, 0, 0, 0.18),
    inset 0 1px 2px rgba(0, 0, 0, 0.10);
}

.ui-buttons-switch--checked .ui-buttons-switch__track {
  background-color: var(--color-brand);
}

.ui-buttons-switch__thumb {
  position: absolute;
  left: var(--space-1);
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--radius-full);
  background: radial-gradient(circle at 35% 30%, #ffffff, #c8c8c8);
  box-shadow:
    0 2px 4px rgba(0, 0, 0, 0.22),
    0 1px 2px rgba(0, 0, 0, 0.14),
    0 0 0 1px rgba(0, 0, 0, 0.06);
  transition:
    transform var(--duration-slow) var(--easing-spring),
    width var(--duration-fast) var(--easing-out);
}

.ui-buttons-switch--checked .ui-buttons-switch__thumb {
  transform: translateX(1rem);
}

.ui-buttons-switch:active .ui-buttons-switch__thumb {
  width: 1.5rem;
}


/* fulgore: ui/chat/bubble.css */
.ui-chat-bubble {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: 75%;
}

.ui-chat-bubble--sent {
  align-self: flex-end;
  align-items: flex-end;
}

.ui-chat-bubble--received {
  align-self: flex-start;
  align-items: flex-start;
}

.ui-chat-bubble__author {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.ui-chat-bubble__meta {
  display: flex;
  justify-content: flex-end;
}

.ui-chat-bubble__time {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.ui-chat-bubble__body {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-loose);
  word-break: break-word;
}

.ui-chat-bubble--sent .ui-chat-bubble__body {
  background: var(--color-brand-surface);
  color: var(--color-text);
  border-bottom-right-radius: var(--radius-sm);
}

.ui-chat-bubble--received .ui-chat-bubble__body {
  background: var(--color-surface-subtle);
  color: var(--color-text);
  border-bottom-left-radius: var(--radius-sm);
}


/* fulgore: ui/chat/conversation_item.css */
.ui-chat-conversation-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  color: var(--color-text);
  border-radius: var(--radius-md);
  transition: background var(--duration-fast) var(--easing-default);

  &:hover {
    background: var(--color-surface-raised);
  }

  &--unread {
    background: var(--color-brand-surface);

    &:hover {
      background: var(--color-brand-a10);
    }

    .ui-chat-conversation-item__names {
      font-weight: var(--font-weight-semibold);
    }

    .ui-chat-conversation-item__preview {
      color: var(--color-text);
    }
  }
}

/* --- Avatars --- */

.ui-chat-conversation-item__avatars {
  display: flex;
  flex-direction: row;
  align-items: center;
  align-self: center;
}

.ui-chat-conversation-item__avatar {
  margin-left: -6px;

  &:first-child {
    margin-left: 0;
  }

  .ui-user-badge {
    gap: 0;
  }

  .ui-user-badge__text {
    display: none;
  }

  .ui-user-badge__circle {
    width: 1.75rem !important;
    height: 1.75rem !important;
    font-size: 0.6875rem !important;
    border: 2px solid var(--color-surface);
  }
}

/* --- Content --- */

.ui-chat-conversation-item__content {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ui-chat-conversation-item__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
}

.ui-chat-conversation-item__names {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-chat-conversation-item__time {
  font-size: 0.625rem;
  color: var(--color-text-subtle);
  white-space: nowrap;
  flex-shrink: 0;
}

.ui-chat-conversation-item__subject {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-chat-conversation-item__preview {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Unread badge --- */

.ui-chat-conversation-item__unread-badge {
  display: inline-block;
  min-width: 1.25rem;
  height: 1.25rem;
  line-height: 1.25rem;
  padding-inline: 5px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: var(--color-text-inverted);
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 0.6875rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
  align-self: center;
}


/* fulgore: ui/chat/empty_state.css */
.ui-chat-empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-8);
  color: var(--color-text-subtle);

  .svg {
    width: var(--icon-size-lg);
    height: var(--icon-size-lg);
    opacity: 0.4;
  }
}

.ui-chat-empty-state__text {
  margin: 0;
  font-size: var(--font-size-sm);
  text-align: center;
}


/* fulgore: ui/chat/input.css */
.ui-chat-input {
  flex-shrink: 0;
  border-top: 1px solid var(--color-border);
  padding: var(--space-3) var(--space-4);
}

.ui-chat-input__form {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.ui-chat-input__textarea {
  flex: 1;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  font-size: 16px;
  font-family: inherit;
  resize: none;
  outline: none;
  transition: border-color var(--duration-fast) var(--easing-default);

  &:focus {
    border-color: var(--color-brand);
    box-shadow: var(--focus-ring);
  }
}

.ui-chat-input__submit {
  padding: var(--space-2) var(--space-4);
  border: none;
  border-radius: var(--radius-lg);
  background: var(--color-brand);
  color: var(--color-text-inverted);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-fast) var(--easing-default);

  &:hover {
    background: var(--color-brand-dark);
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}


/* fulgore: ui/chat/list.css */
.ui-chat-list {
  flex: 1;
  min-height: 20rem;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.ui-chat-list[data-full-height="true"] {
  min-height: 0;
  max-height: none;
}


/* fulgore: ui/directions_dropdown.css */
/* "Directions" dropdown — anchors the absolute menu under the trigger.
   Stimulus `dropdown` toggles .is-open on this root element, not the menu. */

.ui-directions-dropdown {
  position: relative;
  display: inline-block;
}

.ui-directions-dropdown__toggle::after {
  content: "▾";
  margin-left: var(--space-1);
  font-size: 0.75em;
}

.ui-directions-dropdown__menu {
  display: none;
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  z-index: var(--z-dropdown);
  min-width: 12rem;
  padding: var(--space-1) 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.ui-directions-dropdown.is-open .ui-directions-dropdown__menu {
  display: block;
}

.ui-directions-dropdown__item {
  display: block;
  padding: var(--space-2) var(--space-4);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;
}

.ui-directions-dropdown__item:hover,
.ui-directions-dropdown__item:focus {
  background: var(--color-surface-alt);
}


/* fulgore: ui/display/code.css */
.code-container {
  display: grid;
  user-select: text;
}

/* ── Toggle button ── */

.code-container .collapse-toggle {
  background-color: var(--color-neutral-900);
  border: 1px solid var(--color-neutral-900);
  border-radius: var(--radius-lg);
  color: var(--color-text-inverted);
  cursor: pointer;
  font-size: var(--font-size-2xl);
  margin: 0 var(--space-4) 0 auto;
  padding: var(--space-2);
  position: relative;
  top: var(--space-6);
  transition: background-color var(--duration-normal) var(--easing-default),
              border-color var(--duration-normal) var(--easing-default);
  width: fit-content;
  z-index: var(--z-raised);
}

.code-container .collapse-toggle:hover {
  background-color: var(--color-surface);
  border-color: var(--color-neutral-900);
  color: var(--color-text);
}

/* ── Code block ── */

.code-container .code {
  background-color: var(--color-neutral-900);
  color: var(--color-neutral-0);
  overflow: scroll;
  padding: var(--space-2);
  transition: height var(--duration-normal) var(--easing-default);
  z-index: 1;
}

.code-container pre.collapsed {
  height: 5rem;
  overflow: hidden;
  cursor: pointer;
  padding: 0;
}

.code-container pre.expanded {
  height: 100%;
  overflow: scroll;
}


/* fulgore: ui/display/counter.css */
.ui-display-counter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 2px var(--space-3);
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);
}

.ui-display-counter .ui-icon {
  width: 0.875rem;
  height: 0.875rem;
}


/* fulgore: ui/display/datetime.css */
.ui-display-datetime {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  white-space: nowrap;
}


/* fulgore: ui/display/distribution_bar.css */
.ui-display-distribution-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.ui-display-distribution-bar {
  min-width: 0;
}

.ui-display-distribution-bar__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto minmax(40px, 80px);
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.ui-display-distribution-bar__emoji {
  font-size: var(--font-size-base);
  line-height: 1;
}

.ui-display-distribution-bar__label {
  font-size: var(--font-size-sm);
  color: var(--color-text);
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-display-distribution-bar__count {
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-subtle);
  text-align: right;
}

.ui-display-distribution-bar__track {
  height: 8px;
  background-color: var(--color-surface-subtle);
  border-radius: 999px;
  overflow: hidden;
}

.ui-display-distribution-bar__fill {
  height: 100%;
  background-color: var(--color-brand);
  transition: width 200ms ease;
}


/* fulgore: ui/display/iban.css */
.ui-display-iban {
  font-variant-numeric: tabular-nums;
}


/* fulgore: ui/display/image_skeleton.css */
/* .ui-image-skeleton — classe utility skeleton image partagée.
 * Pose un shimmer en background + un spinner via ::before sur l'élément porteur.
 * Réutilise @keyframes skeleton-shimmer et @keyframes spin (fulgore/animations.css).
 * Le wrapper doit définir ses propres dimensions (aspect-ratio, width, height,
 * border-radius, overflow).
 *
 * Deux usages possibles (selon la présence d'un wrapper) :
 *
 *   1. Sur un wrapper <div> qui contient un <picture> rendu par UI::Display::Picture
 *      → le shimmer + spinner sont sur le wrapper, l'image enfant le masque
 *        une fois chargée (z-index:1 sur le picture/img).
 *
 *   2. Directement sur le <picture> lui-même (via css_class de Picture, pour les
 *      cas sans wrapper parent — ex. miniatures en ligne dans un Row)
 *      → le ::before n'aurait pas d'effet sur un <img> seul (replaced element),
 *        mais le <picture> n'est PAS replaced, donc ::before fonctionne ;
 *        le <img> enfant masque le skeleton via z-index.
 *
 * Caveat asset transparent : le contract suppose un asset opaque (JPEG/WebP en
 * object-fit:cover). Pour un SVG/PNG transparent avec object-fit:contain,
 * les zones transparentes laissent voir le shimmer en permanence — préférer
 * dans ce cas un wrapper neutre sans skeleton (cf. show.css logo).
 */

.ui-image-skeleton {
  position: relative;
  overflow: hidden;
  background: linear-gradient(
    90deg,
    var(--color-surface-raised) 25%,
    var(--color-surface-subtle) 50%,
    var(--color-surface-raised) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s ease-in-out infinite;
}

.ui-image-skeleton::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-brand);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  z-index: 0;
}

.ui-image-skeleton > picture,
.ui-image-skeleton > picture > img,
picture.ui-image-skeleton,
picture.ui-image-skeleton > img {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}


/* fulgore: ui/display/money.css */
.ui-display-money {
  display: block;
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: right;
}

.ui-display-money--default {
  color: var(--color-brand);
}

.ui-display-money--positive {
  color: var(--color-success);
}

.ui-display-money--negative {
  color: var(--color-danger);
}


/* fulgore: ui/display/range_bar.css */
.ui-display-range-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.ui-display-range-bar__labels {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--font-size-xs);
  font-weight: 500;
  color: var(--color-text-subtle);
}

.ui-display-range-bar__min,
.ui-display-range-bar__max {
  margin: 0;
  min-width: 0;
  font-variant-numeric: tabular-nums;
}

.ui-display-range-bar__max {
  text-align: right;
}

.ui-display-range-bar__track {
  position: relative;
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--color-surface-subtle) 0%,
    var(--color-brand-surface) 50%,
    var(--color-surface-subtle) 100%
  );
  overflow: hidden;
}

.ui-display-range-bar__marker {
  position: absolute;
  top: 50%;
  width: 12px;
  height: 12px;
  background-color: var(--color-brand);
  border-radius: 50%;
  transform: translateY(-50%) translateX(-50%);
}

.ui-display-range-bar__marker-label {
  position: absolute;
  top: 100%;
  white-space: nowrap;
  margin-top: var(--space-1);
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  transform: translateX(-50%);
}


/* fulgore: ui/display/rna.css */
.ui-display-rna {
  font-variant-numeric: tabular-nums;
}


/* fulgore: ui/display/simple_format.css */
.simple-format {
  white-space: pre-line;
}

.simple-format p {
  margin: 0 0 1em;
}

.simple-format br {
  display: inline;
}


/* fulgore: ui/display/siret.css */
.ui-display-siret {
  font-variant-numeric: tabular-nums;
}


/* fulgore: ui/display/stat_block.css */
.ui-display-stat-block {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  min-width: 0;
  text-align: center;
}

/* Large variant — default */
.ui-display-stat-block--large {
  .ui-display-stat-block__value {
    font-size: var(--font-size-3xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
    margin: 0;
  }

  .ui-display-stat-block__label {
    font-size: var(--font-size-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-subtle);
    margin: 0;
  }
}

/* Compact variant */
.ui-display-stat-block--compact {
  min-width: 0;

  .ui-display-stat-block__value {
    font-size: var(--font-size-lg);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .ui-display-stat-block__label {
    font-size: var(--font-size-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-subtle);
    margin: 0;
  }
}


/* fulgore: ui/display/thumbnail.css */
.ui-display-thumbnail {
  width: 40px;
  height: 40px;
  overflow: hidden;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

.ui-display-thumbnail picture,
.ui-display-thumbnail .ui-display-thumbnail__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  aspect-ratio: 1;
}


/* fulgore: ui/empty_state.css */
.ui-empty-state {
  text-align: center;
  padding: var(--space-12) var(--space-8);
  color: var(--color-text-subtle);

  .empty-state-icon {
    margin-bottom: var(--space-3);

    .svg {
      width: var(--space-10);
      height: var(--space-10);
      fill: currentColor;
      opacity: 0.4;
      /* Consumer resets may force `svg { display: block }`, which escapes
         the text-align: center of the root — margin-inline keeps a block
         svg centered and is a no-op on an inline one. */
      margin-inline: auto;
    }
  }

  .empty-state-title {
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-medium);
    margin: 0 0 var(--space-2) 0;
  }

  .empty-state-description {
    font-size: var(--font-size-base);
    margin: 0;
  }

  .empty-state-cta {
    margin-top: var(--space-5);
  }
}


/* fulgore: ui/gauge.css */
.ui-gauge {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-4) var(--space-3);
  background-color: var(--color-brand-surface);
  border: 1px solid var(--color-brand-a10);
  border-radius: var(--radius-lg);
  text-align: center;
}

.ui-gauge__icon {
  font-size: var(--font-size-2xl);
  line-height: 1;
}

.ui-gauge__label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-brand);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.ui-gauge .ui-bases-progress-bar {
  width: 100%;
}

.ui-gauge .ui-bases-progress-bar__track {
  flex-direction: column;
  gap: var(--space-2);
}

.ui-gauge .ui-bases-progress-bar__bar {
  flex: none;
  width: 100%;
  height: var(--space-1);
}

.ui-gauge .ui-bases-progress-bar__percentage {
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--color-brand);
  text-align: center;
  min-width: unset;
}


/* fulgore: ui/ghost_row.css */
.ui-ghost-row {
  
  & .ui-ghost-row__cell {
    
    border: 3px dashed var(--color-brand);
    background-color: var(--color-brand-a20);
    padding-block: var(--space-1);
  }
}


/* fulgore: ui/help_tooltip.css */
.ui-help-tooltip {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.ui-help-tooltip__trigger {
  position: relative;
  width: 1.1rem;
  height: 1.1rem;
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);
  font-size: var(--font-size-xs);
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  cursor: pointer;
  line-height: 1;
  padding: 0;
}

/* Le rond visible fait 17,6 px (1.1rem, bordure comprise) ; le minimum
   WCAG 2.5.8 (AA) est 24 px. On étend la zone touchable AUTOUR du rond au lieu
   de grossir le rond : l'aspect ne bouge dans aucun consumer, et la cible passe
   la norme.
   ⚠️ Le débord se choisit à la mesure, pas au calcul : 3 px donnent 23,6 px,
   sous la barre. Mesuré au navigateur (WebKit ET Chromium, 390×844, sonde
   elementFromPoint), 0.25rem rend 24,4 px — ça passe, mais 0,4 px de marge se
   perd au premier arrondi d'appareil. 0.3rem rend 26 px. */
.ui-help-tooltip__trigger::before {
  content: "";
  position: absolute;
  inset: -0.3rem;
  border-radius: var(--radius-full);
}

.ui-help-tooltip__trigger:hover {
  background: var(--color-surface);
  color: var(--color-text);
}

.ui-help-tooltip__trigger:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.ui-help-tooltip__panel {
  --ui-tooltip-shift: 0;

  position: absolute;
  top: 50%;
  right: calc(100% + var(--space-2));
  transform: translateY(-50%);
  /* Rattrapage horizontal posé par tooltip_controller quand la bulle dépasse
     encore un bord après bascule. `translate` est une propriété distincte de
     `transform` : elle se compose avec le centrage ci-dessus ET avec les
     `transform: none` que certains consumers posent en repositionnant. */
  translate: var(--ui-tooltip-shift);
  background: var(--color-surface);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  box-shadow: var(--shadow-lg);
  min-width: 220px;
  max-width: 320px;
  z-index: var(--z-dropdown);
  white-space: normal;
}

/* Bascule posée par tooltip_controller quand la bulle sortirait du bord
   gauche du viewport : elle s'ouvre alors à droite du déclencheur. */
.ui-help-tooltip__panel[data-flipped] {
  right: auto;
  left: calc(100% + var(--space-2));
}


/* fulgore: ui/icon.css */
.svg {
  width: var(--icon-size-md);
  height: var(--icon-size-md);
  flex-shrink: 0;
  fill: currentColor;
}


/* fulgore: ui/icon_label.css */
.ui-icon-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.ui-icon-label__icon {
  flex-shrink: 0;
  font-size: var(--font-size-base);
  line-height: 1;
}

.ui-icon-label__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}


/* fulgore: ui/inputs/checkbox.css */
.ui-input--checkbox {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-3);

  label {
    cursor: pointer;
  }

  input[type="checkbox"] {
    width: 1.125rem;
    height: 1.125rem;
    accent-color: var(--color-brand);
    cursor: pointer;
  }

  input[type="hidden"] {
    display: none;
  }
}


/* fulgore: ui/inputs/checkbox_card.css */
.ui-inputs-checkbox-card {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--easing-default),
    background var(--duration-fast) var(--easing-default);

  &:hover {
    border-color: var(--color-brand);
    background: var(--color-brand-a10);
  }

  &.is-checked {
    border-color: var(--color-brand);
    background: var(--color-brand-a10);
  }
}

.ui-inputs-checkbox-card__checkbox {
  width: 1.25rem;
  height: 1.25rem;
  accent-color: var(--color-brand);
  cursor: pointer;
  transition: transform var(--duration-fast) var(--easing-bounce, cubic-bezier(0.34, 1.56, 0.64, 1));

  .no-transition & {
    transition: none;
  }
}

.ui-inputs-checkbox-card.is-checked .ui-inputs-checkbox-card__checkbox {
  animation: checkbox-pop var(--duration-normal, 250ms) var(--easing-bounce, cubic-bezier(0.34, 1.56, 0.64, 1));
}

.ui-inputs-checkbox-card.no-transition .ui-inputs-checkbox-card__checkbox {
  animation: none;
}

@keyframes checkbox-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.35); }
  100% { transform: scale(1); }
}

.ui-inputs-checkbox-card__text {
  position: relative;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.ui-inputs-checkbox-card__label {
  display: block;
  transition: opacity var(--duration-fast) var(--easing-default);
}

.ui-inputs-checkbox-card__label--unchecked {
  opacity: 1;
}

.ui-inputs-checkbox-card__label--checked {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.ui-inputs-checkbox-card.is-checked .ui-inputs-checkbox-card__label--unchecked {
  opacity: 0;
}

.ui-inputs-checkbox-card.is-checked .ui-inputs-checkbox-card__label--checked {
  opacity: 1;
}


/* fulgore: ui/inputs/checkbox_select.css */
.ui-input {
  .ui-checkbox-select {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  .ui-checkbox-select__option {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-2);
    cursor: pointer;
    font-size: var(--font-size-sm);
    color: var(--color-text);

    input[type="checkbox"] {
      width: auto;
      flex: none;
      accent-color: var(--color-brand);
      cursor: pointer;
    }
  }
}


/* fulgore: ui/inputs/color.css */
.ui-input {
  [data-controller="color-picker"] {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
  }

  [data-color-picker-target="swatch"] {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
    padding: var(--space-1);
    border: 1.5px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    cursor: pointer;
    transition:
      border-color var(--duration-fast) var(--easing-default),
      box-shadow var(--duration-fast) var(--easing-default);

    &::-webkit-color-swatch-wrapper {
      padding: 0;
    }

    &::-webkit-color-swatch {
      border: none;
      border-radius: var(--radius-sm);
    }

    &::-moz-color-swatch {
      border: none;
      border-radius: var(--radius-sm);
    }

    &:focus {
      border-color: var(--color-brand);
      box-shadow: var(--focus-ring);
    }
  }

  [data-color-picker-target="hex"] {
    flex: 1;
    width: auto;
    font-family: var(--font-family-mono);
  }

  &.ui-input--error {
    [data-color-picker-target="swatch"] {
      border-color: var(--color-danger);

      &:focus {
        box-shadow: 0 0 0 3px var(--color-danger-a10);
      }
    }
  }
}


/* fulgore: ui/inputs/date_range.css */
.filter-field--date-range {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.filter-date-range__field {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.filter-date-range__label {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

.filter-field--date-range input[type="date"] {
  padding: var(--space-1) var(--space-2);
  font-size: var(--font-size-sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}


/* fulgore: ui/inputs/file_dropzone.css */
.ui-inputs-file-dropzone__zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-lg);
  background-color: var(--color-surface);
  cursor: pointer;
  transition: border-color 150ms ease, background-color 150ms ease;
}

.ui-inputs-file-dropzone__zone:hover {
  border-color: var(--color-brand);
  background-color: var(--color-brand-surface);
}

.ui-inputs-file-dropzone__zone.is-dragover {
  border-color: var(--color-brand);
  background-color: var(--color-brand-surface);
}

.ui-inputs-file-dropzone__icon {
  font-size: var(--font-size-2xl);
}

.ui-inputs-file-dropzone__label {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.ui-inputs-file-dropzone__hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.ui-inputs-file-dropzone__zone.is-uploading {
  border-color: var(--color-brand);
  background-color: var(--color-brand-surface);
  pointer-events: none;
  cursor: default;
}

.ui-inputs-file-dropzone__zone.is-uploading .ui-inputs-file-dropzone__icon,
.ui-inputs-file-dropzone__zone.is-uploading .ui-inputs-file-dropzone__label,
.ui-inputs-file-dropzone__zone.is-uploading .ui-inputs-file-dropzone__hint {
  display: none;
}

.ui-inputs-file-dropzone__zone.is-uploading::after {
  content: "Envoi en cours…";
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-brand);
}

.ui-inputs-file-dropzone__zone.is-uploading::before {
  content: "";
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--color-brand);
  border-right-color: transparent;
  border-radius: var(--radius-full);
  animation: dropzone-spin 600ms linear infinite;
}

@keyframes dropzone-spin {
  to { transform: rotate(360deg); }
}

.ui-inputs-file-dropzone__input {
  display: none;
}


/* fulgore: ui/inputs/file_zone.css */
.ui-inputs-file-zone__zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-5) var(--space-4);
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-lg);
  background-color: var(--color-surface);
  cursor: pointer;
  transition: border-color var(--duration-normal) var(--easing-default),
    background-color var(--duration-normal) var(--easing-default);
}

.ui-inputs-file-zone__zone:hover {
  border-color: var(--color-brand);
  background-color: var(--color-brand-surface);
}

.ui-inputs-file-zone__zone.is-dragover {
  border-color: var(--color-brand);
  background-color: var(--color-brand-surface);
}

.ui-inputs-file-zone__current {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
}

.ui-inputs-file-zone__current-thumb {
  max-height: 7.5rem;
  border-radius: var(--radius-md);
  object-fit: contain;
}

.ui-inputs-file-zone__current-name {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-inputs-file-zone__replace {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.ui-inputs-file-zone__zone.has-file .ui-inputs-file-zone__current {
  display: none !important;
}

.ui-inputs-file-zone__preview {
  max-height: 7.5rem;
  margin-bottom: var(--space-2);
  border-radius: var(--radius-md);
  object-fit: contain;
}

.ui-inputs-file-zone__preview[hidden] {
  display: none !important;
}

.ui-inputs-file-zone__icon {
  font-size: var(--font-size-2xl);
}

.ui-inputs-file-zone__invite {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.ui-inputs-file-zone__filename {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-inputs-file-zone__filename[hidden] {
  display: none !important;
}

.ui-inputs-file-zone__formats {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.ui-inputs-file-zone__zone.has-file .ui-inputs-file-zone__invite {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-normal);
  color: var(--color-text-subtle);
}


/* fulgore: ui/inputs/money.css */
.ui-inputs-money {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  width: 100%;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition:
    border-color var(--duration-fast) var(--easing-default),
    box-shadow var(--duration-fast) var(--easing-default);
}

.ui-inputs-money:focus-within {
  border-color: var(--color-brand);
  box-shadow: var(--focus-ring);
}

.ui-inputs-money input[type="text"] {
  flex: 1;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

.ui-inputs-money input[type="text"]:focus {
  border-color: transparent;
  box-shadow: none;
}

.ui-inputs-money__currency {
  display: flex;
  align-items: center;
  padding: 0 var(--space-3);
  border-left: 1px solid var(--color-border);
  background: var(--color-surface-subtle);
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  white-space: nowrap;
}

.ui-input--error .ui-inputs-money {
  border-color: var(--color-danger);
}

.ui-input--error .ui-inputs-money:focus-within {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px var(--color-danger-a10);
}


/* fulgore: ui/inputs/password.css */
.ui-input {
  .password-wrapper {
    position: relative;
  }

  .password-wrapper input {
    padding-right: var(--space-10);
  }

  /* LA ZONE TACTILE, PAS L'ICÔNE. La pastille mesurait 16×16 — la taille de son
     icône, avec `padding: 0` autour. Le minimum WCAG 2.5.8 est 24×24 et Apple
     recommande 44 : sur un téléphone, 16px se rate. On agrandit la ZONE et on
     centre l'icône dedans, qui reste à 1rem : l'apparence ne change pas, la
     cible oui. `right` passe à `--space-1` pour que le carré de 44px reste dans
     le champ, qui fait 45px de haut.
     Mesuré au navigateur sur les écrans d'accès de FBordes, le 2026-08-03.
     ⚠️ CE SIDECAR EST LA COPIE SERVIE. `app/assets/stylesheets/fulgore/
     input.css` porte une seconde définition de `.password-toggle` qui n'est
     chargée nulle part chez les consumers — corriger celle-là seule ne change
     rien à l'écran, contre-épreuve faite le 2026-08-03. Les deux sont tenues
     alignées en attendant qu'on tranche laquelle disparaît. */
  .password-toggle {
    position: absolute;
    right: var(--space-1);
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    width: 44px;
    height: 44px;
    padding: 0;
    cursor: pointer;
    color: var(--color-text-subtle);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: color var(--duration-fast) var(--easing-default);

    &:hover { color: var(--color-text); }

    .svg {
      display: block;
      width: 1rem;
      height: 1rem;
    }
  }
}


/* fulgore: ui/inputs/percentage.css */
.ui-inputs-percentage {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  width: 100%;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition:
    border-color var(--duration-fast) var(--easing-default),
    box-shadow var(--duration-fast) var(--easing-default);
}

.ui-inputs-percentage:focus-within {
  border-color: var(--color-brand);
  box-shadow: var(--focus-ring);
}

.ui-inputs-percentage input[type="number"] {
  flex: 1;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

.ui-inputs-percentage input[type="number"]:focus {
  border-color: transparent;
  box-shadow: none;
}

.ui-inputs-percentage__suffix {
  display: flex;
  align-items: center;
  padding: 0 var(--space-3);
  border-left: 1px solid var(--color-border);
  background: var(--color-surface-subtle);
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  white-space: nowrap;
}

.ui-input--error .ui-inputs-percentage {
  border-color: var(--color-danger);
}

.ui-input--error .ui-inputs-percentage:focus-within {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px var(--color-danger-a10);
}


/* fulgore: ui/inputs/pill_select.css */
.pill-select {
  position: relative;
  display: inline-block;
  width: 100%;
}

.pill-select__trigger {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  text-align: left;
}

.pill-select__caret {
  margin-left: auto;
  color: var(--color-text-subtle);
  font-size: var(--font-size-xs);
}

.pill-select__dropdown {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  z-index: var(--z-dropdown);
  min-width: 100%;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.pill-select__dropdown[hidden] {
  display: none;
}

.pill-select__option {
  display: flex;
  align-items: center;
  padding: var(--space-1) var(--space-2);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  width: 100%;
  text-align: left;
}

.pill-select__option:hover {
  background: var(--color-surface-subtle);
}


/* fulgore: ui/inputs/range_slider.css */
.filter-field--range-slider {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
  min-width: 180px;
}

.filter-range-slider__labels {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-subtle);
  min-height: 1.2em;
  word-break: break-word;
}

.filter-range-slider__label {
  font-weight: var(--font-weight-semibold);
  color: var(--color-brand-dark);
}

.filter-range-slider__separator {
  color: var(--color-text-subtle);
  flex-shrink: 0;
}

.filter-range-slider__track {
  position: relative;
  height: 28px;
  display: flex;
  align-items: center;
}

.filter-range-slider__track::before {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 4px;
  margin-top: -2px;
  background: var(--color-border);
  border-radius: var(--radius-full);
  pointer-events: none;
}

/* Les deux `input[type=range]` sont superposés en plein cadre et transparents :
   seules leurs poignées reçoivent le pointeur, ce qui laisse les deux
   attrapables sur toute la longueur de la piste. */
.filter-range-slider__thumb {
  position: absolute;
  inset: 0 0 0 0;
  width: 100%;
  margin: 0;
  padding: 0;
  background: transparent;
  pointer-events: none;
  -webkit-appearance: none;
  appearance: none;
}

.filter-range-slider__thumb:focus-visible {
  outline: none;
}

.filter-range-slider__thumb::-webkit-slider-runnable-track {
  background: transparent;
  height: 28px;
  border: none;
}

.filter-range-slider__thumb::-moz-range-track {
  background: transparent;
  height: 28px;
  border: none;
}

.filter-range-slider__thumb::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: all;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-brand);
  border: 2px solid var(--color-surface);
  box-shadow: 0 0 0 1px var(--color-brand-dark);
  cursor: grab;
  transition: transform 100ms ease;
}

.filter-range-slider__thumb::-webkit-slider-thumb:active {
  cursor: grabbing;
  transform: scale(1.1);
}

.filter-range-slider__thumb::-moz-range-thumb {
  pointer-events: all;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-brand);
  border: 2px solid var(--color-surface);
  box-shadow: 0 0 0 1px var(--color-brand-dark);
  cursor: grab;
  transition: transform 100ms ease;
}

.filter-range-slider__thumb::-moz-range-thumb:active {
  cursor: grabbing;
  transform: scale(1.1);
}

.filter-range-slider__thumb:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--color-brand-surface);
}

.filter-range-slider__thumb:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--color-brand-surface);
}

.filter-range-slider__thumb--min {
  z-index: 1;
}

.filter-range-slider__thumb--max {
  z-index: 2;
}


/* fulgore: ui/inputs/rich_text.css */
.ui-input__rich-text {
  min-height: 200px;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--font-size-base);
  line-height: 1.5;
  background-color: var(--color-surface);
}

.ui-input__rich-text:focus {
  outline: none;
  border-color: var(--color-brand);
  box-shadow: var(--focus-ring);
}

.ui-input__rich-text:disabled {
  background-color: var(--color-surface-subtle);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}


/* fulgore: ui/inputs/select.css */
.ui-select {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

/* ============================================================
   Mode recherche (UI::Inputs::Select searchable: true)
   Le <select> natif garde .ui-select ci-dessus et reste la valeur
   soumise ; ce bloc n'habille que le déclencheur et le panneau que
   search-select_controller révèle par-dessus lui. `.is-open` est
   posé par dropdown_controller sur la racine .ui-search-select,
   jamais sur le panneau — même convention que row_actions_menu.css
   / directions_dropdown.css (racine.is-open racine__menu).
   ============================================================ */

.ui-search-select {
  position: relative;
  display: block;
  width: 100%;
}

/* Doit rendre comme .ui-select ci-dessus (même bordure, rayon,
   fond, taille de texte) pour qu'un formulaire mixant les deux
   modes reste homogène visuellement. */
.ui-search-select__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;

  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);

  font-size: var(--font-size-sm);
  text-align: left;
  color: var(--color-text);

  background-color: var(--color-surface);

  cursor: pointer;

  /* Le libellé peut déborder (valeur longue) : il se coupe
     proprement, jamais le chevron qui reste visible à droite. */
  span {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    min-width: 0;
  }

  &::after {
    content: "▾";
    flex-shrink: 0;
    font-size: 0.75em; /* pas de token pour une taille relative au glyphe — même valeur que directions_dropdown.css */
    color: var(--color-text-subtle);
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }
}

.ui-search-select__panel {
  display: none;
  flex-direction: column;
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  right: 0;
  z-index: var(--z-dropdown);

  /* Pas de token de hauteur dédié : borne dérivée de l'espacement
     (~15rem) pour garder ~6-7 options visibles avant de défiler. */
  max-height: calc(var(--space-10) * 6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;

  /* L'écart intérieur est porté PAR LE PANNEAU, et aucun de ses deux enfants
     ne porte de marge horizontale. C'est délibéré : un enfant de conteneur
     flex étiré prend la largeur du contenu du parent SANS en déduire sa
     propre marge — mesuré ici à 9px à gauche et −7px à droite, un débordement
     que le `overflow: hidden` ci-dessus rogne, donc invisible à l'œil et
     visible seulement à la mesure. Reprendre l'écart en marge sur le champ
     réintroduirait exactement ce défaut. */
  padding: var(--space-2);

  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.ui-search-select.is-open .ui-search-select__panel {
  display: flex;
}

.ui-search-select__filter {
  flex-shrink: 0;
  box-sizing: border-box;
  /* Aucune marge horizontale : l'écart vient du padding du panneau (cf. son
     commentaire). Seul l'écart SOUS le champ lui appartient. */
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);

  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);

  font-size: var(--font-size-sm);
  color: var(--color-text);

  background: var(--color-surface);

  &:focus {
    outline: none;
    border-color: var(--color-brand);
    box-shadow: var(--focus-ring);
  }
}

/* Liste bornée + défilante : c'est tout l'intérêt du panneau
   recherche. Le champ de filtre ci-dessus reste fixe (flex-shrink: 0)
   pendant que la liste défile dans l'espace restant. */
.ui-search-select__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;

  margin: 0;
  padding: var(--space-1) 0;
  list-style: none;
}

.ui-search-select__option {
  padding: var(--space-2) var(--space-3);

  font-size: var(--font-size-sm);
  color: var(--color-text);

  cursor: pointer;

  /* Survol souris et .is-active (option courante au clavier)
     doivent rester visuellement distincts : les deux peuvent être
     vrais en même temps sur deux lignes différentes. */
  &:hover {
    background: var(--color-surface-alt);
  }

  &.is-active {
    background: var(--color-brand-a10);
    color: var(--color-brand-dark);
  }
}

.ui-search-select__empty {
  margin: 0;
  padding: var(--space-4);

  font-size: var(--font-size-sm);
  text-align: center;
  color: var(--color-text-subtle);
}


/* fulgore: ui/inputs/vat_breakdown.css */
.ui-inputs-vat-breakdown {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}


/* fulgore: ui/inputs/verification_code.css */
.ui-verification-code__boxes {
  display: flex;
  flex-direction: row;
  gap: var(--space-2);
}

.ui-verification-code__box {
  width: 3rem;
  height: 3.5rem;
  text-align: center;
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  color: var(--color-text);
  transition: border-color var(--duration-fast) var(--easing-default),
              box-shadow var(--duration-fast) var(--easing-default);
  appearance: none;
  -moz-appearance: textfield;
}

.ui-verification-code__box::-webkit-outer-spin-button,
.ui-verification-code__box::-webkit-inner-spin-button {
  appearance: none;
}

.ui-verification-code__box:focus {
  outline: none;
  border-color: var(--color-brand);
  box-shadow: var(--focus-ring);
}

.ui-input--error .ui-verification-code__box {
  border-color: var(--color-danger);
}

.ui-input--error .ui-verification-code__box:focus {
  box-shadow: 0 0 0 3px var(--color-danger-a10);
}

.ui-verification-code__box:disabled {
  background-color: var(--color-surface-raised);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}


/* fulgore: ui/label_with_tooltip.css */
.ui-label-with-tooltip {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}

.ui-label-with-tooltip__text {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}


/* fulgore: ui/links/back.css */
.ui-links-back {
  display: flex;
  align-items: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  margin-block: var(--space-6);
}

.ui-links-back a {
  color: inherit;
  text-decoration: none;
  transition: color var(--duration-fast) var(--easing-default);

  &:hover {
    color: var(--color-brand);
  }
}


/* fulgore: ui/links/edit.css */
.ui-button--sm .svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}


/* fulgore: ui/links/plain.css */
.ui-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: opacity var(--duration-fast) var(--easing-default);

  &:hover {
    opacity: 0.7;
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-sm);
  }
}


/* fulgore: ui/locale_flag_switch.css */
/* Drapeau SVG du switch de langue. PAS de règle `fill` : les couleurs
   viennent du fichier SVG (contrairement à .svg de UI::Icon qui force
   currentColor). Ratio 4:3 (viewBox 640x480), hauteur fixe pour aligner. */
.ui-locale-flag {
  display: block;
  height: 1.25rem;
  width: auto;
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
}


/* fulgore: ui/locale_switch.css */
.ui-locale-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.ui-locale-switch__link {
  text-transform: uppercase;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
}

/* Sous `include_current`, la langue courante est affichée à côté des autres.
   Sans marque visuelle, deux drapeaux côte à côte ne disent pas lequel est
   actif — le sélecteur redevient muet, ce que l'option cherchait à corriger.
   Ce sont donc les AUTRES qui s'effacent, et le `:has()` limite la règle aux
   switchers qui affichent réellement la langue courante : un switcher sans
   `--current` (le régime par défaut) garde ses liens à pleine opacité. */
.ui-locale-switch:has(.ui-locale-switch__link--current)
  .ui-locale-switch__link:not(.ui-locale-switch__link--current) {
  opacity: 0.55;
}

/* Le survol doit ANNULER l'estompage, pas le retour au survol de `.ui-link`
   (`opacity: 0.7`, plain.css). Un simple `.ui-locale-switch__link:hover` a la
   même spécificité que lui et passe après dans le bundle : il l'aurait écrasé
   partout, y compris sur les switchers qui n'utilisent pas `include_current`.
   Le sélecteur reste donc calqué sur celui de l'estompage.
   La valeur est `1` et non `0.7` : depuis un repos à 0.55, un survol à 0.7 ne
   laisserait que 0.15 d'écart, un signal qu'on ne voit plus. Annuler pour de
   bon est aussi ce que le premier mot de ce commentaire promet. */
.ui-locale-switch:has(.ui-locale-switch__link--current)
  .ui-locale-switch__link:not(.ui-locale-switch__link--current):hover {
  opacity: 1;
}


/* fulgore: ui/map.css */
.ui-map {
  width: 100%;
  height: 20rem;
  border-radius: var(--radius-md);
  overflow: hidden;
  z-index: 0;
}


/* fulgore: ui/menu_toggle.css */
.ui-menu-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: none;
  color: var(--color-text);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--easing-default),
              background-color var(--duration-fast) var(--easing-default);
}

.ui-menu-toggle:hover {
  border-color: var(--color-text-soft);
  background-color: var(--color-surface-alt);
}

.ui-menu-toggle:focus-visible {
  outline: none;
  border-color: var(--color-text-soft);
  box-shadow: var(--focus-ring);
}


/* fulgore: ui/metric_card.css */
.ui-metric-card {
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.ui-metric-card__label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  font-weight: var(--font-weight-medium);
  margin-bottom: var(--space-1);
  position: relative;
}

.ui-metric-card__value {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* ── Tooltip repositioning inside card ── */

.ui-metric-card .ui-help-tooltip__panel {
  top: calc(100% + var(--space-1));
  right: auto;
  left: 0;
  transform: none;
  min-width: 12rem;
}

/* ── Comparison delta (M/M-1, etc.) ── */

.ui-metric-card__comparison {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-1);
  font-size: var(--font-size-xs);
}

.ui-metric-card__delta-value {
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
}

.ui-metric-card__delta-label {
  color: var(--color-text-subtle);
}

.ui-metric-card__comparison--positive .ui-metric-card__delta-value {
  color: var(--color-success);
}

.ui-metric-card__comparison--negative .ui-metric-card__delta-value {
  color: var(--color-danger);
}

.ui-metric-card__comparison--neutral .ui-metric-card__delta-value {
  color: var(--color-text-subtle);
}


/* fulgore: ui/notification/badge.css */
.ui-notification-badge {
  display: inline-block;
  min-width: 1.25rem;
  height: 1.25rem;
  line-height: 1.25rem;
  padding-inline: 5px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: var(--color-text-inverted);
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 0.6875rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
  box-sizing: border-box;
}


/* fulgore: ui/notification/bell.css */
.ui-notification-bell {
  position: relative;
}

.ui-notification-bell__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text-inverted);
  cursor: pointer;
  position: relative;
  transition: background var(--duration-fast) var(--easing-default);

  .svg {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
  }

  &:hover {
    background: rgba(255, 255, 255, 0.15);
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }
}

.ui-notification-bell__badge {
  position: absolute;
  top: 2px;
  right: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 1rem;
  height: 1rem;
  padding: 0 3px;
  border-radius: var(--radius-full);
  background: var(--color-danger);
  color: var(--color-text-inverted);
  font-size: 0.625rem;
  font-weight: var(--font-weight-semibold);
  line-height: 1;
}

.ui-notification-bell__badge--hidden {
  display: none;
}

.ui-notification-bell__dropdown {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: var(--space-2);
  width: 22rem;
  max-width: calc(100vw - 2rem);
  max-height: 28rem;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  z-index: var(--z-dropdown);

  &.is-open {
    display: block;
  }
}

.ui-notification-bell__dropdown-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.ui-notification-bell__dropdown-title {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-notification-bell__mark-all {
  border: none;
  background: none;
  padding: 0;
  font-size: var(--font-size-xs);
  color: var(--color-brand);
  cursor: pointer;

  &:hover {
    text-decoration: underline;
  }
}

.ui-notification-bell__list {
  display: flex;
  flex-direction: column;
}

.ui-notification-bell__empty {
  padding: var(--space-6);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  margin: 0;
}


/* fulgore: ui/notification/item.css */
.ui-notification-item {
  display: grid;
  grid-template-columns: 1.75rem 1fr;
  gap: var(--space-2);
  align-items: start;
  padding: var(--space-2) var(--space-4);
  text-decoration: none;
  color: var(--color-text);

  &:not(:last-child) {
    border-bottom: 1px solid var(--color-border);
  }

  &:hover {
    background: var(--color-surface-raised);
  }

  &--unread {
    background: var(--color-brand-surface);

    &:hover {
      background: var(--color-brand-a10);
    }
  }
}

.ui-notification-item__avatar {
  flex-shrink: 0;

  .ui-user-badge {
    gap: 0;
  }

  .ui-user-badge__text {
    display: none;
  }

  .ui-user-badge__circle {
    width: 1.75rem !important;
    height: 1.75rem !important;
    font-size: 0.625rem !important;
  }
}

.ui-notification-item__content {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.ui-notification-item__title-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1);
}

.ui-notification-item__actor-name {
  font-size: var(--font-size-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ui-notification-item .ui-pill {
  font-size: 0.5625rem;
  padding: 0 4px;
  line-height: 1.125rem;
  flex-shrink: 0;
}

.ui-notification-item__body {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-notification-item__meta {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-top: 1px;
}

.ui-notification-item__time,
.ui-notification-item__context {
  font-size: 0.625rem;
  color: var(--color-text-disabled);
}


/* fulgore: ui/pagination.css */
.ui-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}

.ui-pagination__arrow,
.ui-pagination__page,
.ui-pagination__gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--space-8);
  height: var(--space-8);
  padding: 0 var(--space-2);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  text-decoration: none;
}

.ui-pagination__arrow {
  color: var(--color-text-subtle);

  &:hover {
    background: var(--color-surface-subtle);
    color: var(--color-text);
  }
}

.ui-pagination__arrow--disabled {
  color: var(--color-text-disabled);
  pointer-events: none;
}

.ui-pagination__page {
  color: var(--color-text-subtle);

  &:hover {
    background: var(--color-surface-subtle);
    color: var(--color-text);
  }
}

.ui-pagination__page--current {
  background: var(--color-brand);
  color: var(--color-text-inverted);
  border: 1px solid var(--color-brand);
  font-weight: var(--font-weight-medium);
  pointer-events: none;
}

.ui-pagination__gap {
  color: var(--color-text-disabled);
  pointer-events: none;
}


/* fulgore: ui/pill.css */
.ui-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-3);
  border-radius: 999px;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
}

.ui-pill--brand {
  background: var(--color-violet-100);
  color: var(--color-violet-700);
}

.ui-pill--info {
  background: var(--color-blue-100);
  color: var(--color-blue-700);
}

.ui-pill--success {
  background: var(--color-green-100);
  color: var(--color-green-700);
}

.ui-pill--danger {
  background: var(--color-red-100);
  color: var(--color-red-700);
}

.ui-pill--warning {
  background: var(--color-amber-100);
  color: var(--color-amber-700);
}

.ui-pill--neutral {
  background: var(--color-neutral-100);
  color: var(--color-neutral-700);
}

/* Teintes nommées — la couleur est choisie par l'utilisateur d'un consumer,
   le nom ne porte donc aucun sens métier. Les cinq premières doublent
   volontairement une variante sémantique (brand/info/success/danger/warning) :
   « ce que ça veut dire » et « de quelle couleur c'est » sont deux vocabulaires
   distincts, et seul le second est configurable. */
.ui-pill--violet { background: var(--color-violet-100); color: var(--color-violet-700); }
.ui-pill--blue   { background: var(--color-blue-100);   color: var(--color-blue-700); }
.ui-pill--green  { background: var(--color-green-100);  color: var(--color-green-700); }
.ui-pill--red    { background: var(--color-red-100);    color: var(--color-red-700); }
.ui-pill--amber  { background: var(--color-amber-100);  color: var(--color-amber-700); }
.ui-pill--indigo { background: var(--color-indigo-100); color: var(--color-indigo-700); }
.ui-pill--teal   { background: var(--color-teal-100);   color: var(--color-teal-700); }
.ui-pill--lime   { background: var(--color-lime-100);   color: var(--color-lime-700); }
.ui-pill--orange { background: var(--color-orange-100); color: var(--color-orange-700); }
.ui-pill--pink   { background: var(--color-pink-100);   color: var(--color-pink-700); }
.ui-pill--brown  { background: var(--color-brown-100);  color: var(--color-brown-700); }

.ui-pill--draft {
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);
}

.ui-pill--in_progress {
  background: var(--color-success-surface);
  color: var(--color-success);
}

.ui-pill--review {
  background: var(--color-warning-surface);
  color: var(--color-warning);
}

.ui-pill--completed {
  background: var(--color-info-surface);
  color: var(--color-info);
}

.ui-pill--archived {
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);
}

.ui-pill--admin {
  background: var(--color-violet-100);
  color: var(--color-violet-700);
}

.ui-pill--accountant {
  background: var(--color-blue-100);
  color: var(--color-blue-700);
}

.ui-pill--active {
  background: var(--color-success-surface);
  color: var(--color-success);
}

.ui-pill--suspended {
  background: var(--color-red-100);
  color: var(--color-red-700);
}

.ui-pill--pending {
  background: var(--color-warning-surface);
  color: var(--color-warning);
}

.ui-pill--read {
  background: var(--color-blue-100);
  color: var(--color-blue-700);
}

.ui-pill--lmnp {
  background: var(--color-brand-surface);
  color: var(--color-brand);
}

.ui-pill--lmnp_vat {
  background: var(--color-info-surface);
  color: var(--color-info);
}

.ui-pill--lg {
  padding: var(--space-2) var(--space-4);
  font-size: var(--font-size-sm);
}

.ui-pill--strong {
  padding: var(--space-1) var(--space-3);
  font-weight: var(--font-weight-bold);
  box-shadow: var(--shadow-pill-strong);
}

.ui-pill--strong.ui-pill--brand   { background: var(--color-violet-600); color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--info    { background: var(--color-blue-700);   color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--success { background: var(--color-green-500);  color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--warning { background: var(--color-amber-500);  color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--danger  { background: var(--color-red-500);    color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--neutral { background: var(--color-neutral-700); color: var(--color-neutral-0); }

.ui-pill--strong.ui-pill--violet { background: var(--color-violet-600); color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--blue   { background: var(--color-blue-700);   color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--green  { background: var(--color-green-500);  color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--red    { background: var(--color-red-500);    color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--amber  { background: var(--color-amber-500);  color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--indigo { background: var(--color-indigo-700); color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--teal   { background: var(--color-teal-700);   color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--lime   { background: var(--color-lime-700);   color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--orange { background: var(--color-orange-700); color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--pink   { background: var(--color-pink-700);   color: var(--color-neutral-0); }
.ui-pill--strong.ui-pill--brown  { background: var(--color-brown-700);  color: var(--color-neutral-0); }


/* fulgore: ui/progress_ring.css */
.ui-progress-ring {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-8);
  background-color: var(--color-brand-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}

.ui-progress-ring__ring {
  flex-shrink: 0;
  width: 5rem;
  height: 5rem;
}

.ui-progress-ring__svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.ui-progress-ring__track {
  fill: none;
  stroke: var(--color-border);
  stroke-width: 8;
}

.ui-progress-ring__fill {
  fill: none;
  stroke: var(--color-brand);
  stroke-width: 8;
  stroke-linecap: round;
  transition: stroke-dashoffset 1.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.ui-progress-ring__ratio {
  transform: rotate(90deg);
  transform-origin: 60px 60px;
  text-anchor: middle;
  dominant-baseline: central;
  font-family: var(--font-family-display);
  font-size: 1.25rem;
  font-weight: var(--font-weight-bold);
  fill: var(--color-text);
}

.ui-progress-ring__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.ui-progress-ring__title {
  font-family: var(--font-family-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

.ui-progress-ring__subtitle {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  margin: 0;
}


/* fulgore: ui/row_actions_menu.css */
/* RowActionsMenu — kebab (⋮) dropdown wrapping already-rendered action
   components (UI::Links::* / UI::Buttons::*) as flat, full-width menu rows.
   Stimulus `dropdown` toggles .is-open on this root element, not the menu
   (cf. directions_dropdown.css / dropdown_controller). */

.ui-row-actions-menu {
  position: relative;
  display: inline-block;
}

.ui-row-actions-menu__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  border-radius: var(--radius-md);

  color: var(--color-text-subtle);
  background: transparent;

  cursor: pointer;
  transition: background var(--duration-fast) var(--easing-default),
    color var(--duration-fast) var(--easing-default);

  .svg {
    width: 1.25rem;
    height: 1.25rem;
  }

  &:hover:not(:disabled):not(.ui-row-actions-menu__toggle--disabled) {
    background: var(--color-surface-subtle);
    color: var(--color-text);
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }
}

.ui-row-actions-menu__menu {
  display: none;
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: var(--z-dropdown);
  min-width: 12rem;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;

  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.ui-row-actions-menu.is-open .ui-row-actions-menu__menu {
  display: block;
}

.ui-row-actions-menu__item {
  display: block;
}

/* Neutralize the wrapped action components (ui-link, ui-button — primary/
   secondary/danger, all sizes) back into flat, full-width text menu rows.
   Targets the real classes emitted by UI::Links::Show/Edit/New (<a
   class="ui-button ui-button--{primary,secondary} ui-button--sm">) and
   UI::Buttons::Destroy (<form> root wrapping <button class="ui-button
   ui-button--danger ui-button--sm">). */
.ui-row-actions-menu__item a,
.ui-row-actions-menu__item button,
.ui-row-actions-menu__item form {
  display: block;
  width: 100%;
  box-sizing: border-box;

  margin: 0;
  padding: var(--space-2) var(--space-4);
  border: none;
  border-radius: 0;

  font-size: var(--font-size-sm);
  text-align: left;
  color: var(--color-text);
  text-decoration: none;
  white-space: nowrap;

  background: transparent;
  box-shadow: none;

  cursor: pointer;
}

.ui-row-actions-menu__item a:hover,
.ui-row-actions-menu__item button:hover,
.ui-row-actions-menu__item form:hover {
  background: var(--color-surface-alt);
}

.ui-row-actions-menu__item a:focus-visible,
.ui-row-actions-menu__item button:focus-visible {
  outline: none;
  box-shadow: none;
  background: var(--color-surface-alt);
}

/* The base variant rules (.ui-button.ui-button--primary/secondary/danger,
   specificity 0,2,0) outrank the plain-element resets above (0,1,1) and
   win regardless of concat order — so background/color/hover-background
   from the wrapped button chrome survive unless matched at equal-or-higher
   specificity here. Re-neutralize per variant explicitly. */
.ui-row-actions-menu__item .ui-button--primary,
.ui-row-actions-menu__item .ui-button--secondary,
.ui-row-actions-menu__item .ui-button--danger {
  color: var(--color-text);
  background: transparent;
  border-color: transparent;
}

.ui-row-actions-menu__item .ui-button--primary:hover,
.ui-row-actions-menu__item .ui-button--secondary:hover,
.ui-row-actions-menu__item .ui-button--danger:hover {
  background: var(--color-surface-alt);
  border-color: transparent;
}

/* The wrapped icon (pen-to-square, plus…) rides along the text baseline
   instead of the button's centered flex layout. `display: inline-block`
   defends against a consumer global reset (`svg { display: block }`) that
   would otherwise stack the icon above its label. */
.ui-row-actions-menu__item .svg {
  display: inline-block;
  width: var(--font-size-sm);
  height: var(--font-size-sm);
  margin-right: var(--space-2);
  vertical-align: -0.15em;
}

/* Destroy's <form> is the item's direct child and must not add its own
   padding on top of the <button> it wraps, or add block spacing. */
.ui-row-actions-menu__item form {
  padding: 0;
}

.ui-row-actions-menu__item form button {
  width: 100%;
}

/* Danger row (UI::Buttons::Destroy → .ui-button--danger) keeps a red
   label, overriding the neutral text-color reset above. Source order
   (after the variant reset block) settles the equal-specificity (0,2,0)
   tie in its favor — hover background already comes from the reset
   block above, so only color needs restating here. */
.ui-row-actions-menu__item .ui-button--danger {
  color: var(--color-danger);
}

@media (prefers-reduced-motion: reduce) {
  .ui-row-actions-menu__toggle {
    transition: none;
  }
}


/* fulgore: ui/section.css */
.ui-section {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
}

/* ── Header ── */

.ui-section__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-4);
}

.ui-section__header[data-action] {
  cursor: pointer;
  user-select: none;
}

.ui-section__icon {
  flex-shrink: 0;
  font-size: var(--font-size-lg);
  line-height: 1;
}

.ui-section__title {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

.ui-section__counter {
  font-weight: var(--font-weight-normal);
  color: var(--color-text-subtle);
}

.ui-section__action {
  margin-left: auto;
  flex-shrink: 0;
}

.ui-section__toggle {
  margin-left: auto;
  background: none;
  border: none;
  padding: var(--space-1);
  cursor: pointer;
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  transition: transform var(--duration-fast);
}

.ui-section__action + .ui-section__toggle {
  margin-left: 0;
}

/* ── Body ── */

.ui-section__body {
  min-height: 0;
}

/* ── Footer ── */

.ui-section__footer {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-4);
}


/* fulgore: ui/section_header.css */
.ui-section-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-4);
}

.ui-section-header__icon {
  flex-shrink: 0;
  font-size: var(--font-size-lg);
  line-height: 1;
}

.ui-section-header__title {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

.ui-section-header__action {
  margin-left: auto;
  flex-shrink: 0;
}


/* fulgore: ui/shell/navbar.css */
.ui-shell-navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  height: var(--navbar-height);
  padding: var(--space-1) var(--space-4);
  box-sizing: border-box;
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: var(--shell-navbar-bg);
  color: var(--shell-navbar-fg);

  .navbar-hamburger { display: none; }
}
@media (max-width: 1023px) {
  .ui-shell-navbar .navbar-hamburger { display: flex; }
}
.navbar-sidebar-toggle {
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: none;
  border-radius: var(--radius-md);
  color: inherit;
  background: transparent;
  cursor: pointer;
  transition: background var(--duration-fast) var(--easing-default);
  .svg {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
    transition: transform var(--duration-fast) var(--easing-default);
  }
  &[aria-expanded="false"] .svg { transform: rotate(180deg); }
  &:hover { background: rgba(255, 255, 255, 0.15); }
  &:focus-visible { outline: none; box-shadow: var(--focus-ring); }
}
.ui-shell-navbar .navbar-sidebar-toggle { display: flex; }
@media (max-width: 1023px) {
  .ui-shell-navbar .navbar-sidebar-toggle { display: none; }
}
.ui-shell-navbar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  color: inherit;
}
.ui-shell-navbar__brand-logo { height: 28px; width: auto; }
.ui-shell-navbar__brand-name {
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
}
.navbar-session-email {
  font-size: var(--font-size-sm);
  color: color-mix(in srgb, var(--shell-navbar-fg) 85%, transparent);
  white-space: nowrap;
}
@media (max-width: 1023px) {
  .navbar-session-email { display: none; }
}
.navbar-logout { margin: 0; }
.ui-shell-navbar .navbar-logout .ui-button {
  display: flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  border: 1px solid color-mix(in srgb, var(--shell-navbar-fg) 30%, transparent);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: inherit;
  color: var(--shell-navbar-fg);
  background: transparent;
  box-shadow: none;
  &:hover:not(:disabled) {
    background: color-mix(in srgb, var(--shell-navbar-fg) 15%, transparent);
    color: var(--shell-navbar-fg);
  }
  &:focus-visible { outline: none; box-shadow: var(--focus-ring); }
}


/* fulgore: ui/shell/sidebar.css */
.ui-shell-sidebar {
  position: fixed;
  top: var(--navbar-height);
  left: 0;
  bottom: 0;
  z-index: var(--z-raised);
  width: var(--sidebar-width);
  max-width: 80vw;
  box-sizing: border-box;
  overflow-x: hidden;
  overflow-y: auto;
  padding: var(--space-6) var(--space-4);
  border-right: 1px solid var(--color-border);
  box-shadow: var(--shadow-lg);
  background: var(--color-surface-subtle);
  transform: translateX(-100%);
  transition: transform var(--duration-slow) var(--easing-out);
  nav { display: flex; flex-direction: column; gap: var(--space-2); }
  &.is-compact, &.is-expanded { transform: translateX(0); }
}
@media (min-width: 1024px) {
  .ui-shell-sidebar {
    position: sticky;
    top: var(--navbar-height);
    left: auto;
    bottom: auto;
    height: calc(100dvh - var(--navbar-height));
    max-width: none;
    width: var(--sidebar-width);
    flex-shrink: 0;
    align-self: flex-start;
    box-shadow: none;
    transform: none;
    transition: width var(--duration-slow) var(--easing-out);
  }
  .ui-shell-sidebar.is-compact {
    width: var(--sidebar-compact-width);
    padding: var(--space-6) var(--space-3);
  }
  .ui-shell-sidebar.is-expanded { width: var(--sidebar-width); }
  .ui-shell-sidebar.is-compact .ui-shell-sidebar__link {
    justify-content: center;
    padding: var(--space-3);
  }
  .ui-shell-sidebar.is-compact .sidebar-link__label { display: none; }
  .ui-shell-sidebar.is-compact .sidebar-section__heading { display: none; }
  .ui-shell-sidebar.is-compact .sidebar-section.is-collapsed .sidebar-section__items { display: flex; }
}
@media (prefers-reduced-motion: reduce) {
  .ui-shell-sidebar,
  .ui-shell-sidebar .sidebar-section__toggle .svg { transition: none; }
}
.sidebar-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-4);
}
.sidebar-section:first-of-type { padding-top: var(--space-2); }
.sidebar-section__heading { margin: 0; }
.sidebar-section__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--space-3);
  border: none;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-subtle);
  background: transparent;
  cursor: pointer;
  transition: color var(--duration-fast) var(--easing-default);
  .svg {
    width: var(--icon-size-sm);
    height: var(--icon-size-sm);
    flex-shrink: 0;
    transition: transform var(--duration-fast) var(--easing-default);
  }
  &:hover { color: var(--color-text); }
  &:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
}
.sidebar-section.is-collapsed .sidebar-section__toggle .svg { transform: rotate(-90deg); }
.sidebar-section__items { display: flex; flex-direction: column; gap: var(--space-2); }
.sidebar-section.is-collapsed .sidebar-section__items { display: none; }
.ui-shell-sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  text-decoration: none;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-subtle);
  transition:
    background var(--duration-fast) var(--easing-default),
    color var(--duration-fast) var(--easing-default);
  .svg { width: var(--icon-size-sm); height: var(--icon-size-sm); flex-shrink: 0; }
  &:hover:not(.ui-shell-sidebar__link--active) {
    background: var(--color-surface-raised);
    color: var(--color-text);
  }
  &:focus-visible { outline: none; box-shadow: var(--focus-ring); }
}
.ui-shell-sidebar__link--active {
  background: var(--color-brand-surface);
  color: var(--color-brand);
  font-weight: var(--font-weight-semibold);
}
.sidebar-link__label { display: inline; }


/* fulgore: ui/skeleton_block.css */
@keyframes skeleton-shimmer {
  0%   { background-position: 200% center; }
  100% { background-position: -200% center; }
}

.ui-skeleton-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ui-skeleton-block--card {
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  gap: var(--space-3);
}

.ui-skeleton-block__title {
  height: 0.875rem;
  width: 45%;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--color-surface-raised) 25%,
    var(--color-surface-subtle) 50%,
    var(--color-surface-raised) 75%
  );
  background-size: 200%;
  animation: skeleton-shimmer 1.5s infinite;
  margin-bottom: var(--space-1);
}

.ui-skeleton-block__row {
  display: flex;
  flex-direction: row;
  gap: var(--space-3);
  height: 2rem;
  border-radius: var(--radius-sm);
  background: linear-gradient(
    90deg,
    var(--color-surface-raised) 25%,
    var(--color-surface-subtle) 50%,
    var(--color-surface-raised) 75%
  );
  background-size: 200%;
  animation: skeleton-shimmer 1.5s infinite;
}

.ui-skeleton-block--card .ui-skeleton-block__row {
  height: 1.25rem;
}

.ui-skeleton-block__cell--wide {
  flex: 3;
}

.ui-skeleton-block__cell--narrow {
  flex: 1;
}


/* fulgore: ui/spinner.css */
.ui-spinner {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.ui-spinner::before {
  content: "";
  display: block;
  width: 1em;
  height: 1em;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-brand);
  border-radius: var(--radius-full);
  animation: spinner-spin var(--duration-slow) linear infinite;
}

@keyframes spinner-spin {
  to { transform: rotate(360deg); }
}

.spinner-label {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}


/* fulgore: ui/stat_grid.css */
/* ── Stat grid: reusable dt/dd key-value layout ──
 *
 * PAS un sidecar orphelin : aucun stat_grid.rb n'existe et c'est voulu.
 * `.ui-stat-grid` est une layout utilitaire cross-composant, émise par
 * UI::Bases::StatCard (qui a par ailleurs son propre sidecar
 * bases/stat_card.css) et écrite en dur chez des consumers (Fatima
 * admin/orders/*_section, FBordes manage/dashboard). La purger ou la
 * renommer casse leur rendu sans erreur Ruby. */

.ui-stat-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: 0;
}

.ui-stat-grid__label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  font-weight: var(--font-weight-medium);
  position: relative;
}

.ui-stat-grid__icon {
  font-size: var(--font-size-base);
}

.ui-stat-grid__value {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
  text-align: right;
}

/* ── Tooltip repositioning ── */

.ui-stat-grid .ui-help-tooltip__panel {
  top: calc(100% + var(--space-1));
  right: auto;
  left: 0;
  transform: none;
}


/* fulgore: ui/status_icon.css */
.ui-status-icon {
  font-size: var(--font-size-base);
  flex-shrink: 0;
  line-height: 1;
}


/* fulgore: ui/summary_row.css */
.ui-summary-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  color: inherit;
}

.ui-summary-row--clickable {
  border-radius: var(--radius-md);
  transition: background var(--duration-fast) var(--easing-default);
}

.ui-summary-row--clickable:hover {
  background: var(--color-surface-subtle);
}

.ui-summary-row__icon {
  font-size: var(--font-size-base);
  flex-shrink: 0;
  line-height: 1;
}

.ui-summary-row__label {
  flex: 1;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ui-summary-row__dots {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
}

.ui-summary-row__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
}

.ui-summary-row__dot--validated {
  background: var(--color-success);
}

.ui-summary-row__dot--pending {
  background: var(--color-warning);
}

.ui-summary-row__dot--rejected {
  background: var(--color-danger);
}

.ui-summary-row__dot--missing {
  background: var(--color-neutral-300);
}

.ui-summary-row__count {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}


/* fulgore: ui/tabs.css */
.ui-tabs__list {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-4);
  scrollbar-width: none;
}

.ui-tabs__list::-webkit-scrollbar {
  display: none;
}

.ui-tabs__tab {
  flex-shrink: 0;
  padding: var(--space-2) var(--space-4);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--duration-fast) var(--easing-default),
              border-color var(--duration-fast) var(--easing-default);
}

.ui-tabs__tab:hover {
  color: var(--color-text);
}

.ui-tabs__tab--active {
  color: var(--color-brand);
  border-bottom-color: var(--color-brand);
}

.ui-tabs__panel[hidden] {
  display: none;
}


/* fulgore: ui/timeline.css */
.ui-timeline {
  padding: var(--space-4) 0;
}

/* ── Track ── */

.ui-timeline__track {
  position: relative;
  height: 2rem;
  margin: 0 var(--space-6);
}

.ui-timeline__line {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--color-border-strong);
  transform: translateY(-50%);
}

.ui-timeline__dot {
  position: absolute;
  top: 50%;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: var(--radius-full);
  background: var(--color-neutral-400);
  border: 2px solid var(--color-surface);
  transform: translate(-50%, -50%);
  z-index: 1;
}

.ui-timeline__dot--past {
  background: var(--color-brand);
}

.ui-timeline__today {
  position: absolute;
  top: 50%;
  width: 0.5rem;
  height: 1.25rem;
  background: var(--color-danger);
  border-radius: var(--radius-sm);
  transform: translate(-50%, -50%);
  z-index: 2;
}

/* ── Labels ── */

.ui-timeline__labels {
  position: relative;
  margin: var(--space-2) var(--space-6) 0;
  height: 3.5rem;
}

.ui-timeline__label {
  position: absolute;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  white-space: nowrap;
}

.ui-timeline__label-text {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.ui-timeline__label-date {
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.ui-timeline__delay {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-subtle);
  margin-top: var(--space-1);
}

.ui-timeline__delay--urgent {
  color: var(--color-danger);
  font-weight: var(--font-weight-semibold);
}


/* fulgore: ui/user_badge.css */
.ui-user-badge {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.ui-user-badge__circle {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  overflow: hidden;
  color: var(--color-neutral-0);
  font-weight: var(--font-weight-semibold);
  flex-shrink: 0;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.ui-user-badge__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.ui-user-badge__name {
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ui-user-badge__email {
  color: var(--color-text-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ui-user-badge__avatar {
  width: 100%;
  height: 100%;
  border-radius: var(--radius-full);
  object-fit: cover;
}

.ui-user-badge__you {
  color: var(--color-text-subtle);
  font-style: italic;
  font-size: var(--font-size-xs);
}

/* --- Tailles --- */

.ui-user-badge--sm .ui-user-badge__circle {
  width: 1.5rem;
  height: 1.5rem;
  font-size: var(--font-size-xs);
}

.ui-user-badge--sm .ui-user-badge__name {
  font-size: 0.8125rem;
}

.ui-user-badge--sm .ui-user-badge__email {
  font-size: var(--font-size-xs);
}

.ui-user-badge--md .ui-user-badge__circle {
  width: 2rem;
  height: 2rem;
  font-size: 0.8125rem;
}

.ui-user-badge--md .ui-user-badge__name {
  font-size: var(--font-size-sm);
}

.ui-user-badge--md .ui-user-badge__email {
  font-size: var(--font-size-xs);
}

.ui-user-badge--lg .ui-user-badge__circle {
  width: 2.5rem;
  height: 2.5rem;
  font-size: var(--font-size-base);
}

.ui-user-badge--lg .ui-user-badge__name {
  font-size: var(--font-size-base);
}

.ui-user-badge--lg .ui-user-badge__email {
  font-size: var(--font-size-sm);
}


/* fulgore: ui/widgets/grid.css */
.ui-widgets-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--space-6);
  align-items: start;
}


/* activities/feed.css */
.activities-feed {
  display: flex;
  flex-direction: column;

  width: 100%;
  box-sizing: border-box;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.activities-feed__heading {
  margin: 0;
  padding: var(--space-3) var(--space-4);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-soft);
  border-bottom: 1px solid var(--color-border);
}

.activities-feed__list {
  display: flex;
  flex-direction: column;
}

.activities-feed__footer {
  display: flex;
  justify-content: flex-end;

  padding: var(--space-2) var(--space-4) var(--space-3);
}


/* activities/item.css */
.activities-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.activities-item:last-child {
  border-bottom: none;
}

/* `--unseen` marque, PARMI les lignes montrées (le panneau affiche les 5
   dernières de TOUT LE MONDE), celle qui compte réellement dans la pastille
   de la cloche (`Activity.unseen_by`, `Activities::Item#initialize`).

   `background: var(--color-brand-surface)` N'EST PAS un choix local : c'est
   la signature exacte que fulgore emploie déjà pour « non lu » sur ses deux
   composants — `ui-notification-item--unread` et
   `ui-chat-conversation-item--unread` (vérifié dans la gem,
   `bundle show fulgore`, `notification/item.css` et
   `chat/conversation_item.css`). Reprendre ce fond plutôt qu'inventer une
   bordure, un point ou du gras évite un second langage visuel pour la même
   notion ailleurs dans la flotte fulgore.

   Aucun survol n'est posé ici : `.activities-item` n'en a aucun (contrairement
   aux deux composants fulgore ci-dessus, qui sont des liens cliquables et
   assombrissent leur fond au survol avec `--color-brand-a10`). Une ligne
   d'activité n'est pas un lien — rien à distinguer au survol tant que ça
   reste vrai. */
.activities-item--unseen {
  background: var(--color-brand-surface);
}

.activities-item__header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

.activities-item__header .ui-user-badge {
  min-width: 0;
}

.activities-item__timestamp {
  flex-shrink: 0;

  font-size: var(--font-size-xs);
  line-height: var(--line-height-snug);
  color: var(--color-text-subtle);
}

.activities-item__body {
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text);
  overflow-wrap: break-word;
  word-break: break-word;
}

.activities-item__actor {
  color: var(--color-text);
}

.activities-item__card-link {
  font-weight: var(--font-weight-semibold);
}


/* activities/list.css */
.activities-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.activities-list__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.activities-list__title {
  margin: 0;
}

.activities-list__list {
  display: flex;
  flex-direction: column;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
}


/* activities/panel.css */
/* État vide du fil d'activités — même veine que `.shared-activity-bell__loading`
   (shared/activity_bell.css) : une ligne de texte centrée, discrète, aérée. */
.activities-panel__empty {
  margin: 0;
  padding: var(--space-6);

  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}


/* auth/card.css */
/* Écrans d'authentification (connexion, demande de reset, nouveau mot de
   passe) — carte centrée à largeur bornée. `.auth-card` est la classe
   d'encapsulation d'Auth::Card, qui enveloppe les trois formulaires de la
   gem : c'est ce qui rend l'habillage commun aux trois écrans et non au seul
   écran de connexion. */

.auth-card {
  /* La gouttière s'ajoute AUTOUR des 28rem de contenu : le desktop garde sa
     largeur, et sous 28rem le formulaire cesse de coller les deux bords. */
  max-width: calc(28rem + 2 * var(--space-4));
  padding-inline: var(--space-4);
  margin: var(--space-16) auto;
  display: grid;
  gap: var(--space-6);

  h2 {
    margin: 0;
  }

  form {
    display: grid;
    gap: var(--space-5);
  }

  /* Le formulaire de la gem arrive dans SON propre wrapper : le gap de la
     carte ne l'atteint donc pas, et h2 (margin: 0) se colle au 1er champ.
     On rend au wrapper la grille que la carte appliquait autrefois
     directement à h2 + form. */
  > div {
    display: grid;
    gap: var(--space-6);
  }
}


/* cards/category_page.css */
.cards-category-page {
  container-type: inline-size;

  display: grid;
  /* minmax(0, 1fr) : sans grid-template-columns, la colonne implicite `auto`
     se dimensionne au max-content des enfants (barre de sujets/onglets plus
     larges) → WebKit déborde de qq px et casse la symétrie de gouttière. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.cards-category-page__heading {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Les filtres passent à la ligne plutôt que de défiler — verdict repris du deck,
   mesuré en WebKit à 390px : le défilement laissait 4 entrées hors champ, avec
   une scrollbar masquée, donc invisibles et sans affordance. Une seule barre
   ici au lieu de deux (la catégorie est l'adresse), d'où l'interligne normal. */
.cards-category-page__filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  row-gap: var(--space-1);

  padding-bottom: var(--space-1);
}

.cards-category-page__filter-link {
  flex-shrink: 0;
}

.cards-category-page__panel-content {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

/* ⚠️ PAS DE PALIER AU-DELÀ : il serait MORT, et c'est mesuré, pas déduit. Le
   gabarit à deux colonnes plafonne le contenu à 720px (n5), et
   `container-type: inline-size` mesure ce conteneur-là, pas le viewport. Relevé
   en WebKit sur cette page — 390px de viewport → conteneur 358, 1 colonne ;
   1280 → conteneur 720, 2 colonnes ; 1920 → conteneur 720, 2 colonnes ENCORE.
   Un `@container (min-width: 1024px)` promettant 3 colonnes a vécu ici jusqu'au
   2026-08-02 : personne ne les a jamais vues. Rouvrir trois colonnes suppose de
   bouger ce plafond D'ABORD. `search_results.css` s'arrête ici pour la même
   raison — les deux grilles de tuiles ont désormais le même modèle. */
@container (min-width: 640px) {
  .cards-category-page__panel-content {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Le badge vit à l'intérieur du <button> d'onglet rendu par fulgore : `UI::Tabs`
   injecte le label tel quel (tabs.rb:39), donc un label html_safe y passe sans
   qu'on touche la gem. Le sélecteur du badge actif s'appuie sur la classe que la
   gem pose elle-même (`.ui-tabs__tab--active`). */
.cards-category-page__tab-badge {
  display: inline-block;
  min-width: 1.25em;
  padding: 0 var(--space-1);

  border-radius: 999px;
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);

  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1.5;
  text-align: center;
}

.ui-tabs__tab--active .cards-category-page__tab-badge {
  background: var(--color-brand);
  color: var(--color-text-inverted);
}

/* Même verdict que la barre de sujets, et pour la même raison : ON PASSE À LA
   LIGNE, ON NE FAIT JAMAIS DÉFILER. `UI::Tabs` met sa liste en `overflow-x:
   auto` avec `scrollbar-width: none` — le pire des deux mondes ici, puisque
   WebKit masque alors la barre : le débordement devient invisible ET sans
   affordance. Mesuré sur le deck en 390px : les six onglets badgés font 696px
   de large pour 358 disponibles. Surcharge locale plutôt que correction dans la
   gem : le défilement des onglets est un choix qui tient ailleurs, ce sont ces
   six onglets-ci, sur un téléphone, qui n'en veulent pas. */
.cards-category-page .ui-tabs__list {
  flex-wrap: wrap;
  overflow-x: visible;
  row-gap: var(--space-1);
}


/* cards/item.css */
/* Le fond dit le statut d'avancement. La teinte arrive par `--cards-item-hue`,
   posée sur l'élément par `Cards::Item#hue_style` depuis le réglage d'équipe
   (`/reglages/couleurs`) — jamais une classe par couleur.

   ⚠️ Cette variable DOIT garder sa valeur par défaut ici. Elle sert deux fois :
   une tuile rendue sans teinte (un spec, un appelant futur) reste lisible, et
   le hook `component_write_check` refuse une variable employée sans être
   définie dans une feuille.

   7 %, et pas les 18 % qu'emploient les pastilles douces en thème sombre : une
   pastille est un petit aplat qu'on veut voir, un fond de tuile s'étale sur
   toute la page et doit se deviner. Dilué dans `--color-surface`, qui bascule
   seul avec le thème — d'où l'absence totale de bloc sombre pour cette règle,
   alors que le même effet en classes par couleur en demanderait 36. */
.cards-item {
  --cards-item-hue: var(--color-neutral-500);

  container-type: inline-size;

  display: grid;
  gap: var(--space-3);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: color-mix(in srgb, var(--cards-item-hue) 7%, var(--color-surface));
  box-shadow: var(--shadow-sm);
}

.cards-item__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* `flex: 1 1 55%` fait tout le travail : le titre réclame au moins 55 % de la
   rangée, donc le groupe de pastilles passe DESSOUS dès qu'il ne tient plus
   dans les 45 % restants, au lieu de comprimer le titre jusqu'à quatre lignes.
   Sans `flex-wrap: wrap` sur le parent, cette base ne servirait à rien — les
   deux vont ensemble. */
.cards-item__title {
  flex: 1 1 55%;
  min-width: 0;

  margin: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.cards-item__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.cards-item__thumbnail {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface-alt);
}

.cards-item__thumbnail-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cards-item__description {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

/* La pastille de nouveautés ne se comprime jamais : rétrécie, un « 12 » devient
   un ovale illisible. L'alignement vertical est celui de `__meta`, qui centre
   la pastille avec les pilules de sa rangée. */
.cards-item__unseen {
  flex-shrink: 0;
}

/* Le filet sépare les gestes du reste de la tuile. `--cards-item-hue` le teinte
   aussi, à peine plus soutenu que le fond : sans ça, un filet neutre coupe la
   tuile en deux au lieu de l'articuler. */
.cards-item__updates {
  display: grid;
  gap: var(--space-1);

  margin: 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid color-mix(in srgb, var(--cards-item-hue) 22%, transparent);

  list-style: none;
}

.cards-item__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

@container (min-width: 420px) {
  .cards-item {
    padding: var(--space-5);
  }
}


/* cards/markdown_area.css */
/* La barre passe à la ligne plutôt que de défiler : à 4 boutons elle tient
   en 390px, mais la 5e (Tableau) déborderait sans flex-wrap — même verdict
   que cards-category-page__filter-bar, mesuré en WebKit où la scrollbar est
   masquée. */
.cards-markdown-area__toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.cards-markdown-area__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-size: var(--font-size-xs);
  color: var(--color-text-soft);
  background: var(--color-surface-subtle);

  cursor: pointer;
  transition:
    background var(--duration-fast) var(--easing-default),
    border-color var(--duration-fast) var(--easing-default);
}

.cards-markdown-area__button:hover {
  background: var(--color-surface-raised);
  border-color: var(--color-border-strong);
}

/* L'auto-grow JS (controller markdown-area, action #grow) pilote la hauteur
   du textarea : une barre de défilement interne masquerait l'étirement.
   resize: vertical reste hérité de .ui-input textarea (fulgore) — l'ajustement
   manuel cohabite avec l'auto-grow sans conflit. */
.cards-markdown-area textarea {
  overflow-y: hidden;
}


/* cards/picker.css */
/* Cards::Picker — sélecteur de carte, monté aujourd'hui dans la barre markdown
   (`Cards::MarkdownArea`) et sur la fiche d'une fiche de jeu, en trois modes
   (émettre · envoyer · champ nu, cf. picker.rb).

   La charpente (ancre, déclencheur, panneau, liste, message vide) vit dans
   `shared/picker.css` depuis n100 : elle était recopiée à l'identique dans
   `sheets/picker.css`, 8 règles sur 9. Ce fichier ne garde que ce qui est
   PROPRE à ce sélecteur — la recherche, le choix, la navigation au clavier, le
   mode champ nu — plus l'ancrage du panneau, la seule chose qui le sépare de
   son jumeau.

   ⚠️⚠️ Ne rien redéclarer de ce que porte `shared/picker.css` SANS RENFORCER
   LA SPÉCIFICITÉ : le bundle concatène par ordre alphabétique, donc `shared/`
   passe APRÈS ce fichier et gagne à spécificité égale. C'est arrivé une fois
   ici, au découpage même : `.cards-picker--inline { display: block }` battait
   `.cards-picker { display: inline-block }` tant que les deux vivaient dans ce
   fichier, et se serait fait retourner en silence une fois la seconde partie
   dans `shared/`. D'où le sélecteur doublé du mode champ nu, plus bas. */

/* Ancrage `right: 0` plutôt que `left: 0` : dans la barre markdown, le picker
   est le DERNIER contrôle (`markdown_area.rb` — `buttons.map + [link_picker]`),
   donc le cas à défendre en premier est celui d'un bouton EN FIN DE LIGNE,
   proche du bord droit d'un champ plafonné à 720px (`Cards::Form`). Un panneau
   ancré à gauche grandirait vers la droite et déborderait précisément dans ce
   cas. Ancré à droite, il grandit vers la gauche, dans l'espace déjà occupé
   par les boutons précédents — le même choix que fait `ui-row-actions-menu__
   menu` (fulgore) pour un déclencheur de fin de ligne comparable.
   `Sheets::Picker`, monté seul, prend l'autre bord.
   Le garde-fou `width: min(...)` vit dans `shared/picker.css` : il protège le
   bord opposé quel que soit l'ancrage. */
.cards-picker__panel {
  right: 0;
}

.cards-picker__filter {
  box-sizing: border-box;
  width: 100%;

  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  background: var(--color-surface);
}

.cards-picker__filter:focus {
  outline: none;
  border-color: var(--color-brand);
  box-shadow: var(--focus-ring);
}

/* Bouton pleine largeur, sans habillage de bouton — le fond ne vient que de
   `:hover` et `.is-active` ci-dessous. Titre tronqué plutôt que multi-lignes :
   les lignes de la liste gardent une hauteur fixe, indispensable pour un
   défilement et une navigation clavier lisibles (une ligne qui grandirait
   déplacerait les options suivantes pendant la navigation). Les dropdowns
   voisins de la gem (`ui-directions-dropdown__item`, `ui-row-actions-menu__
   item`) traitent déjà leurs libellés en une seule ligne ; `text-overflow:
   ellipsis` ajoute le signal visuel de troncature qui leur manque, utile ici
   parce qu'un titre de carte est un texte libre, pas un libellé de menu
   fixe et court. */
.cards-picker__choice {
  display: block;
  width: 100%;
  overflow: hidden;

  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-size: var(--font-size-sm);
  text-align: left;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--color-text);
  background: none;

  cursor: pointer;
  transition: background var(--duration-fast) var(--easing-default);
}

.cards-picker__choice:hover {
  background: var(--color-surface-subtle);
}

.cards-picker__choice:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Surlignage clavier posé par `card-picker_controller#navigate` (`.is-active`
   sur le `<li>`) — délibérément distinct du `:hover` ci-dessus (teinte de
   marque plutôt qu'un simple survol neutre), sinon les deux états
   deviendraient indiscernables quand souris et clavier se combinent. Fond
   connu (`--color-surface` du panneau, pas un fond inconnu) → token
   sémantique solide `--color-brand-surface`, pas un `rgba()`/`-a10` réservé
   aux fonds inconnus (cf. règle tokens, §5). Ciblé sur `.cards-picker__choice`
   (pas sur le `<li>`) : le bouton est déjà pleine largeur, inutile de
   dupliquer le fond sur son parent. */
.cards-picker__option.is-active .cards-picker__choice {
  color: var(--color-brand-dark);
  background: var(--color-brand-surface);
}

/* ============================================================
   MODE « CHAMP NU » (`inline: true`, picker.rb, mission 2026-08-06)
   ============================================================
   Pas de déclencheur : `.cards-picker__filter` est un enfant direct de la
   racine (picker.rb#call), et le panneau porte `hidden` au rendu — piloté
   directement par `card_picker_controller#filter`/`#clear`, jamais par
   `.is-open` (`dropdown_controller` n'est même pas monté en inline,
   `inline_root_data`, picker.rb). */

/* La racine occupe la largeur qu'on lui donne, plafonnée. `display: block`
   écrase l'`inline-block` de `.picker` (`shared/picker.css`) : un
   `inline-block` sans largeur propre calcule sa taille sur son CONTENU, et un
   `<input width:100%>` dedans retombe alors sur sa taille intrinsèque
   (l'attribut HTML `size` par défaut, ~20 caractères) au lieu de remplir le
   conteneur — indépendamment de tout parent flex qui pourrait par ailleurs le
   « bloquifier ».

   ⚠️⚠️ SÉLECTEUR DOUBLÉ, ET CE N'EST PAS DÉCORATIF. `.picker` et
   `.cards-picker--inline` pèsent pareil (0,1,0), et `shared/picker.css` passe
   APRÈS ce fichier dans le bundle : à égalité, c'est `inline-block` qui
   gagnerait, le champ retomberait à ~20 caractères, et rien ne le signalerait.
   Doubler la classe porte la règle à (0,2,0) et referme le piège. Les deux
   classes vivent sur le MÊME élément (`root_css_class`, picker.rb).

   `max-width: 26rem` reprend le plafond déjà posé par `.shared-navbar
   .shared-search-field` (navbar.css) pour EXACTEMENT le même besoin : un champ
   de recherche autonome, sans bouton pour l'encadrer. C'est le seul autre
   précédent du projet, préféré à une valeur inventée. */
.cards-picker.cards-picker--inline {
  display: block;
  width: 100%;
  max-width: 26rem;
}

/* Le panneau se cale sur la largeur du CHAMP, pas sur les 18rem fixes du
   mode déroulant : ici il n'y a pas de bouton étroit à côté duquel se poser,
   il y a un champ, et un panneau plus étroit ou plus large que lui se
   lirait comme un accident. `width: 100%` calcule contre la racine
   (`.picker { position: relative }`, `shared/picker.css`, déjà plafonnée à
   26rem ci-dessus) — le panneau suit donc automatiquement ce plafond, sans le
   redupliquer ici. `left: 0` : ancré au bord gauche du champ, qui démarre lui
   aussi au bord gauche de la racine (flux normal) — pas de bord droit à
   défendre comme en dropdown, il n'y a pas de bouton en fin de ligne.
   (0,2,0) contre le `width` de `shared/picker.css` en (0,1,0) : gagne, ordre
   de concaténation ou non.) */
.cards-picker--inline .cards-picker__panel {
  left: 0;
  width: 100%;
}

/* ⚠️ `hidden` DOIT GAGNER. `.picker__panel` est `display: none` par défaut
   (`shared/picker.css`) ; le mode déroulant l'ouvre via `.is-open`, mais
   l'inline n'a JAMAIS cette classe — c'est `card_picker_controller#filter` qui
   retire/repose directement l'attribut `hidden`. Sans le `:not([hidden])`
   ci-dessous, une règle `display: flex` posée sur `.cards-picker--inline
   .cards-picker__panel` afficherait le panneau MÊME AU REPOS (hidden posé dès
   le rendu, avant toute frappe) : `[hidden]` n'est qu'un attribut HTML, son
   seul effet vient de la règle UA `[hidden] { display: none }` (origine UA, la
   plus faible priorité de la cascade) — n'importe quelle règle AUTEUR de
   spécificité égale ou supérieure la bat, y compris le `display: none` de la
   feuille partagée. `:not([hidden])` inverse la condition : la règle ne gagne
   QUE quand l'attribut est absent, donc seulement une fois que le JS l'a
   retiré.
   Vérifié en WebKit à 390 et 1280 : panneau invisible au rendu (hidden
   posé), visible après frappe d'une lettre (hidden retiré par le
   contrôleur), masqué de nouveau après effacement du champ (hidden
   reposé). */
.cards-picker--inline .cards-picker__panel:not([hidden]) {
  display: flex;
}


/* cards/search_results.css */
.cards-search-results {
  container-type: inline-size;

  display: grid;
  /* minmax(0, 1fr) : sans grid-template-columns, la colonne implicite `auto`
     se dimensionne au max-content des enfants → WebKit déborde de quelques px
     et casse la symétrie de gouttière. Même verdict que sur les autres listes
     de cartes. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.cards-search-results__heading {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Le conteneur ne fait que borner la pastille à son contenu :
   `UI::Display::Counter` est un `inline-flex`, et la grille étirerait sinon son
   fond sur toute la largeur. */
.cards-search-results__counter {
  display: flex;
}

.cards-search-results__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .cards-search-results__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* cards/see_also.css */
/* `min-width: 0` pour la même raison que `.cards-show__detail` : ce bloc est un
   item de la grille `.cards-show__body`, et `1fr`/`2fr` valent `minmax(auto,
   Nfr)` — sans ça l'item refuse de descendre sous son min-content, qu'un titre
   de carte long (« Direction artistique — l'ombre construit la forme ») rend
   très large. */
.cards-see-also {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  min-width: 0;

  /* Une SURFACE, et pas seulement un titre suivi d'une liste (JC, 2026-08-05) :
     sans fond ni cadre, la section flottait dans le même blanc que le corps de
     la carte, et rien ne disait où elle commençait. C'est l'encadré latéral de
     Wikipédia. Le fond suffit — pas de bordure en plus, qui doublerait le
     signal. `--color-surface-raised` bascule seul avec le thème sombre. */
  padding: var(--space-4);
  border-radius: var(--radius-md);

  background-color: var(--color-surface-raised);
}

/* Aligné sur `.cards-show__remarks-heading` : les deux titres de section de la
   fiche doivent peser pareil. */
.cards-see-also__heading {
  margin: 0;

  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

/* Les puces sont retirées : la liste de fulgore les remettrait, et le rendu
   voulu est celui d'un « Voir aussi » de Wikipédia — des titres alignés, pas un
   inventaire à points. */
.cards-see-also__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  margin: 0;
  padding: 0;

  list-style: none;
}

/* `overflow-wrap` et non `word-break` : un titre de carte plus long que la
   colonne doit se couper AU MOT, jamais au caractère. La colonne fait un tiers
   de la fiche en desktop — « Caractéristique de capacité de combat » y tient
   sur deux lignes. */
.cards-see-also__item {
  min-width: 0;

  overflow-wrap: anywhere;
}

/* Les liens de la section doivent avoir EXACTEMENT l'apparence de ceux du corps
   de la carte (`.markdown a`, shared/markdown.css:56-59) — c'est ce qui fait
   qu'on les lit comme des liens, et c'est le point de la demande : se rapprocher
   de Wikipédia. `UI::Links::Plain` de fulgore pose `color: inherit` et
   `text-decoration: none` (il est taillé pour les titres de tuiles, pas pour du
   texte courant), donc sans cette règle les titres sortaient en noir, non
   soulignés — vus à la capture, pas devinés.
   La couleur seule est reprise : le composant de fulgore garde son anneau de
   focus et son atténuation au survol. Spécificité (0,2,0) contre (0,1,0) pour
   `.ui-link` — elle passe devant sans dépendre de l'ordre du bundle. */
.cards-see-also__item .ui-link {
  color: var(--color-brand);
  text-decoration: underline;
}


/* cards/show.css */
/* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
   posée par `layout_tag` — cette classe-ci ne porte plus que ce que la gem ne
   sait pas.

   `min-width: 0` obligatoire : ce bloc est un item de la grille fulgore
   `.ui-bases-record-view__body` (1fr 1fr), et `1fr` vaut `minmax(auto, 1fr)` —
   sans ça l'item refuse de descendre sous son min-content, que les blocs de
   code du markdown (non coupables) rendent très large. Déborde sinon en 390px,
   WebKit ET Chromium. */
.cards-show__detail {
  min-width: 0;
}

/* La fiche passe en deux colonnes DÈS QU'UNE carte en cite une autre : le texte
   à gauche, le « Voir également » à droite. Sans citante, `context_component`
   rend `nil`, le corps redevient `:only-child` et fulgore lui rend sa mise en
   page centrée — l'immense majorité des fiches ne bouge donc pas d'un pixel.

   Trois précautions, toutes payées ailleurs dans ce fichier :

   ⚠️ 1. La classe est DOUBLÉE (`.cards-show__body.ui-bases-record-view__body`).
   Les deux vivent sur le MÊME élément (record_view.rb:46), donc à spécificité
   égale (0,1,0 chacune) le gagnant dépendrait de l'ordre du bundle — le piège
   déjà écrit en tête de fichier pour `.cards-show__description`. Doublée, la
   règle pèse (0,2,0) et passe devant à coup sûr.

   ⚠️ 2. La media query n'est PAS décorative. Une media query n'ajoute AUCUNE
   spécificité : sans elle, cette règle l'emporterait aussi sur le `1fr` que
   fulgore repose sous 768px (record_view.css:60-63), et deux colonnes sur un
   écran de 390px débordent. Le seuil est `769px` parce que celui de fulgore est
   `max-width: 768px` — les deux doivent rester collés.

   ⚠️ 3. `minmax(0, …)` des deux côtés, jamais `2fr 1fr` nu : `Nfr` vaut
   `minmax(auto, Nfr)`, et un bloc de code du markdown (non coupable) pousserait
   alors la colonne de gauche au-delà de la page. */
@media (min-width: 769px) {
  .cards-show__body.ui-bases-record-view__body {
    /* Une largeur FIXE pour l'encadré, et non une fraction (JC, 2026-08-05).
       En `2fr 1fr`, la colonne de droite grandissait avec l'écran alors que son
       contenu — deux titres — ne grandit pas : un tiers de la page pour une
       ligne de texte, et un grand blanc dessous. Mesuré sur la capture de JC en
       1920 : le texte tombait à 760px pendant que 380px de colonne restaient
       vides. Ici la boîte fait 18rem quoi qu'il arrive, et tout le reste va au
       texte. */
    /* 48rem est EXACTEMENT la mesure que fulgore donne au corps quand il est
       seul (`:only-child`, record_view.css:31-36) : la reprendre ici fait qu'une
       carte citée et une carte non citée gardent la même mesure de lecture. La
       page, elle, s'élargit à 1040 quand elle porte une fiche
       (`application.css`) — c'est ce qui empêche l'encadré de manger le texte.
       Aucun `max-width` sur ce bloc : `.app-main` borne et centre déjà. */
    grid-template-columns: minmax(0, 48rem) 18rem;
  }
}

/* Les actions passent à la ligne plutôt que de déborder : trois boutons dont
   deux à libellé long ne tiennent pas sur une rangée en 390px. */
.cards-show__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.cards-show__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* La typographie du corps est portée par `.markdown` (shared/markdown.css),
   posée sur le même élément : ne rien redéclarer ici, les deux classes ont la
   même spécificité et le gagnant dépendrait de l'ordre du bundle. */
.cards-show__description {
  min-width: 0;
}

/* LES DEUX SECTIONS DE `list_component` — remarques et fiches — partagent
   leur boîte ET leur intitulé, réunis ici en n116 après avoir été écrits deux
   fois dans ce fichier.

   Pas de `min-width: 0` : contrairement à `.cards-show__detail`, ces blocs ne
   sont PAS des items de `.ui-bases-record-view__body` (1fr 1fr). Ils viennent
   de `list_component`, rendu par `Show#list_slot` dans `.ui-bases-show__list`
   — de simples blocs `width: 100%` en dehors de cette grille, siège de
   `body_slot`. Le piège documenté en tête de fichier ne s'y rejoue pas.

   ⚠️ CE QUI LES DISTINGUE N'EST PAS ICI, ET NE PEUT PAS L'ÊTRE : les fiches
   restent TOUJOURS rendues, même sans rien de rattaché, parce que leur
   section porte le seul déclencheur `Sheets::Picker` qui permet d'en
   rattacher une première ; les remarques, elles, disparaissent entièrement
   quand leur liste est vide. C'est un escamotage décidé dans le Ruby
   (`Cards::Show#remarks_block` rend `nil`, `#sheets_block` non), et aucune
   règle ci-dessous ne suppose la présence d'une liste — titre, message vide
   et déclencheur doivent rester présentables seuls. */
.cards-show__remarks,
.cards-show__sheets {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

/* Même forme des deux côtés : un intitulé au-dessus d'une liste, sous la
   description de la carte — donc le même habillage. */
.cards-show__remarks-heading,
.cards-show__sheets-heading {
  margin: 0;

  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

/* Une seule colonne : les tuiles `.remarks-item` portent des blocs de texte
   (cible(s), corps, auteur) plus riches que les vignettes de sprites, donc
   pas de palier multi-colonnes comme `sprites-gallery__list`. `minmax(0,
   1fr)` reste nécessaire : sans lui, une tuile au contenu large (ex. liste de
   cartes visées) pousserait la colonne implicite `auto` au-delà de 390px. */
.cards-show__remarks-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

/* ============================================================
   SECTION « FICHES » — les fiches de jeu rattachées à cette carte.
   ============================================================
   Sa boîte et son intitulé sont réunis avec ceux des remarques tout en haut
   de ce fichier : même forme, mêmes déclarations.

   ⚠️ LA LISTE DES FICHES RATTACHÉES N'EST PLUS STYLÉE ICI. Ses cinq blocs
   (`__sheets-empty`, `__sheets-list`, `__sheet`, son `.ui-link` et son
   `form`) vivaient AUSSI dans `sheets/show.css` sous d'autres noms, à
   l'identique au caractère près : ils sont partis en n116 dans
   `shared/attached_list.css`, avec le composant qui les rend
   (`Shared::AttachedList`).

   Ce qui reste ici, c'est la SECTION et son titre — eux ne sont pas
   partagés, parce que les deux sections divergent vraiment : celle-ci porte
   le déclencheur de `Sheets::Picker` et reste rendue même vide. */


/* cards/update_line.css */
.cards-update-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);

  font-size: var(--font-size-xs);
  line-height: var(--line-height-snug);
  color: var(--color-text-subtle);
}

/* min-width: 0 sur l'élément flexible, sinon un mot long (un display_name
   collé, un statut traduit à rallonge) refuse de se couper et pousse la durée
   hors de la tuile — le flex par défaut ne descend pas sous la taille du
   contenu. */
.cards-update-line__sentence {
  min-width: 0;
  overflow-wrap: anywhere;
}

.cards-update-line__actor {
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-soft);
}

/* La durée ne se comprime jamais : « il y a 2 jours » tronqué à « il y a 2 j… »
   ne dit plus rien, alors que la phrase, elle, supporte de passer à la ligne. */
.cards-update-line__age {
  flex-shrink: 0;
  color: var(--color-text-muted);
}


/* characters/index_page.css */
/* La page `/personnages` : titre + bouton de création, puis la liste des
   tuiles. Calquée sur `.places-index-page` (`places/index_page.css`) — même
   structure à deux blocs, même absence d'onglets (un `Character` ne porte
   aucun statut, `character.rb`). */

.characters-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.characters-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.characters-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une seule colonne jusqu'à 640px, deux au-delà — même palier que
   `.places-index-page__list` et `.sheets-index-page__list`, pour que les
   listes du hub se lisent pareil au même point de rupture. `minmax(0, 1fr)`
   des deux côtés : sans lui la colonne implicite se dimensionne au
   max-content d'une tuile et déborde sous le seuil. */
.characters-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .characters-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* characters/item.css */
/* La tuile d'un personnage : titre + méta (bord, nombre de portraits).
   Racine `<article>` posée par le composant lui-même
   (`skip_encapsulation? true`) : tout l'habillage vit ici, sur la classe
   racine — même motif que `.places-item`.

   ⚠️ AUCUNE VIGNETTE, à la différence de `.places-item` : le hub ne détient
   pas les images des portraits, seulement leur chemin dans les assets du
   jeu (`character_portrait.rb`). Rien à afficher, donc rien à styler — pas
   un oubli. */

.characters-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.characters-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.characters-item__meta {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}


/* characters/show.css */
/* La fiche d'un personnage, calquée sur `Sheets::Show` (`sheets/show.css`) —
   même base `UI::Bases::Show`, même absence de `list_component`/
   `footer_slot` (`Characters::Show` ne les surcharge pas), donc pas de
   `.characters-show__list` ni `__footer` à styler : ces classes n'existent
   pas dans le DOM de ce composant.

   `.characters-show__detail` est le SEUL enfant de
   `.ui-bases-record-view__body` (pas de `context_component` surchargé) : la
   règle fulgore `:only-child { max-width: 48rem; margin-inline: auto }`
   (record_view.css) s'applique donc automatiquement — rien à redéclarer. */

.characters-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas, et que sa classe seule ne saurait pas accueillir. */
  container-type: inline-size;

  /* Même parade que `.sheets-show__detail` : ce bloc est un item de la
     grille fulgore `.ui-bases-record-view__body` (`1fr` = `minmax(auto,
     1fr)`) — sans `min-width: 0` il refuse de descendre sous son
     min-content. Un chemin de portrait non coupable
     (`portraits/coal_lassitude.png`) est exactement le genre de contenu qui
     rejouerait le débordement. */
  min-width: 0;
}

/* Les quatre sections partagent la même colonne titre + contenu. */
.characters-show__side,
.characters-show__portraits,
.characters-show__add-portrait,
.characters-show__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

/* Les titres de section partagent UNE règle, comme
   `.sheets-show__stats-heading` et ses trois voisines — les recopier serait
   le doublon structurel que `css-auditor` classe `duplicate`. */
.characters-show__side-heading,
.characters-show__portraits-heading,
.characters-show__add-portrait-heading {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.characters-show__side-value,
.characters-show__portraits-empty {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* ============================================================
   LA LISTE DE PORTRAITS
   ============================================================
   Chaque `<li>` porte DEUX formulaires frères (un `<form>` ne peut pas en
   contenir un autre) — l'édition (`.characters-show__portrait-form`) et la
   suppression, posée nue par `UI::Buttons::ConfirmDestroy`
   (`ui/bases/button.rb#call` rend un `<form>` SANS classe dès qu'une `url:`
   est passée). D'où le sélecteur `form` non préfixé plus bas : seul crochet
   disponible sur ce second formulaire, et il reste scopé sous
   `.characters-show__portrait`.

   ⚠️⚠️ DIVERGENCE ASSUMÉE AVEC `.sheets-show__stat`, qui tient sur UNE
   rangée dès 390px. Là-bas ça marche parce que le second champ est un
   NOMBRE À DEUX CHIFFRES, figé à 64px — il reste ~178px au premier. Ici les
   DEUX champs sont du texte, et le second est un chemin de fichier
   (« portraits/coal_lassitude.png », 28 caractères) : le compacter de la
   même façon donnerait deux champs illisibles côte à côte. La rangée est
   donc EMPILÉE en dessous de 480px de conteneur, et seulement alors mise
   en ligne. Conséquence assumée : les étiquettes restent VISIBLES ici, et
   la recette `.ui-visually-hidden` recopiée dans `sheets/show.css` n'a
   aucune raison d'être rejouée. */

.characters-show__portraits-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
  padding: 0;

  list-style: none;
}

.characters-show__portrait {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);

  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* Le formulaire de suppression (le `form` nu, sans classe) ne cède jamais
   sa place au formulaire d'édition voisin. `:not(.characters-show__portrait-form)`
   est nécessaire : sans lui le sélecteur `form` matcherait AUSSI le
   formulaire d'édition (qui EST un `<form>`) et lui imposerait
   `flex-shrink: 0`, en conflit avec le `flex: 1` voulu juste en dessous.
   Même piège documenté dans `sheets/show.css`. */
.characters-show__portrait > form:not(.characters-show__portrait-form) {
  flex-shrink: 0;
}

/* Empilé par défaut (cf. la divergence expliquée plus haut), en rangée
   au-delà de 480px de conteneur. `flex: 1` : ce formulaire partage la
   rangée de `.characters-show__portrait` avec le formulaire de suppression
   et doit lui laisser sa largeur de bouton. `min-width: 0` : sans lui un
   flex-item refuse de descendre sous son min-content. */
.characters-show__portrait-form,
.characters-show__add-portrait-form {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--space-3);

  min-width: 0;
}

/* Le bouton « Enregistrer » (icône seule) ne rétrécit jamais sous sa taille
   de contenu : un flex-item accepte `flex-shrink: 1` par défaut, ce qui
   l'écraserait avant les champs voisins (eux `min-width: 0`, donc candidats
   naturels à la compression). */
.characters-show__portrait-form .ui-button {
  flex-shrink: 0;
  align-self: flex-start;
}

@container (min-width: 480px) {
  .characters-show__portrait-form,
  .characters-show__add-portrait-form {
    flex-direction: row;
    align-items: flex-end;
  }

  /* Les deux champs se partagent la place à parts égales. `min-width: 0` :
     un `.ui-input` ne rétrécit pas sous la largeur de son `<input>` interne
     (`width: 100%`, `ui/bases/input.css`) sans lui — c'est ce qui empêche
     les deux champs de partager la rangée. */
  .characters-show__portrait-form .ui-input,
  .characters-show__add-portrait-form .ui-input {
    flex: 1 1 0;
    min-width: 0;
  }

  .characters-show__portrait-form .ui-button {
    align-self: flex-end;
  }
}

/* ============================================================
   ÉDITER / SUPPRIMER LE PERSONNAGE
   ============================================================ */
.characters-show__actions {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-3);
}


/* comments/item.css */
.comments-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);

  background: var(--color-surface);
}

.comments-item__header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

.comments-item__header .ui-user-badge {
  min-width: 0;
}

.comments-item__timestamp {
  flex-shrink: 0;

  font-size: var(--font-size-xs);
  line-height: var(--line-height-snug);
  color: var(--color-text-subtle);
}

.comments-item__body {
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text);
  overflow-wrap: break-word;
  word-break: break-word;
}


/* comments/thread.css */
.comments-thread {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.comments-thread__heading {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

.comments-thread__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}


/* dashboard/category_tile.css */
/* La tuile EST un <a> : tout le rectangle est cliquable, y compris le pied de
   décomptes. D'où `display: flex` sur le lien lui-même plutôt que sur un
   enfant, et le retrait explicite du soulignement — `UI::Links::Plain` de la
   gem porte ces réglages, mais il n'accepte pas un contenu structuré. */
.dashboard-category-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);

  padding: var(--space-4) var(--space-3);
  box-sizing: border-box;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);

  color: var(--color-text);
  text-decoration: none;
  text-align: center;

  transition: border-color 0.15s ease, background 0.15s ease;
}

.dashboard-category-tile:hover {
  border-color: var(--color-brand);
  background: var(--color-surface-raised);
}

/* L'anneau de focus est repris de la gem plutôt que réinventé : c'est le même
   geste au clavier que sur n'importe quel lien du hub. */
.dashboard-category-tile:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.dashboard-category-tile__icon {
  width: 2rem;
  height: 2rem;

  /* Les SVG de fulgore n'ont pas de `fill` déclaré : sans cette ligne ils
     sortent en noir, illisibles dans le thème sombre. `currentColor` les fait
     suivre la couleur du texte de la tuile, donc aussi sa teinte au survol. */
  fill: currentColor;
  color: var(--color-text-subtle);
}

.dashboard-category-tile:hover .dashboard-category-tile__icon {
  color: var(--color-brand);
}

.dashboard-category-tile__count {
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  line-height: 1.1;
  color: var(--color-text);
}

.dashboard-category-tile__label {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

/* Le pied passe à la ligne, il ne défile jamais : verdict déjà rendu trois fois
   dans ce projet (barres de filtres, onglets). Une catégorie peut porter les
   sept sujets, et sur un téléphone ça fait deux ou trois rangs. */
.dashboard-category-tile__subjects {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1) var(--space-2);

  margin-top: var(--space-2);
}

.dashboard-category-tile__subject {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);

  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.dashboard-category-tile__subject-count {
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}


/* dashboard/remarks_tile.css */
/* La grande sœur des quatre tuiles de catégorie : même vocabulaire visuel
   (surface, bordure, rayon, survol), mais large plutôt que carrée — elle vit
   en frère direct de `.dashboard-show__grid` dans `.dashboard-show`, une
   grille une-colonne qui lui donne déjà toute la largeur. Pas de
   `grid-column` à bricoler ici.
   Comme `CategoryTile`, la tuile EST le lien (`skip_encapsulation?`) : le
   `display: flex` porte sur la racine cliquable elle-même. */
.dashboard-remarks-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);

  padding: var(--space-4) var(--space-3);
  box-sizing: border-box;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-surface);

  color: var(--color-text);
  text-decoration: none;
  text-align: center;

  transition: border-color 0.15s ease, background 0.15s ease;
}

.dashboard-remarks-tile:hover {
  border-color: var(--color-brand);
  background: var(--color-surface-raised);
}

.dashboard-remarks-tile:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.dashboard-remarks-tile__icon {
  width: 2rem;
  height: 2rem;

  /* Les SVG de fulgore n'ont pas de `fill` déclaré : sans cette ligne ils
     sortent en noir, illisibles dans le thème sombre. */
  fill: currentColor;
  color: var(--color-text-subtle);
}

.dashboard-remarks-tile:hover .dashboard-remarks-tile__icon {
  color: var(--color-brand);
}

/* Le chiffre est le héros de la tuile : plus grand que celui des tuiles de
   catégorie (2xl côté CategoryTile), pour porter le poids visuel d'une
   tuile deux fois plus large. */
.dashboard-remarks-tile__count {
  font-size: var(--font-size-4xl);
  font-weight: var(--font-weight-bold);
  line-height: 1.1;
  color: var(--color-text);
}

.dashboard-remarks-tile__label {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

/* Le pied de répartition par catégorie : discret, et il PASSE À LA LIGNE
   plutôt que de déborder. Jusqu'à quatre entrées « icône + libellé +
   nombre » ne tiennent pas sur une rangée à 390px — même verdict que
   `CategoryTile__subjects`. */
.dashboard-remarks-tile__categories {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-1) var(--space-3);

  width: 100%;
  margin-top: var(--space-2);
  padding-top: var(--space-2);

  border-top: 1px solid var(--color-border);
}

.dashboard-remarks-tile__category {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);

  min-width: 0;

  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.dashboard-remarks-tile__category-icon {
  flex-shrink: 0;
  width: 1rem;
  height: 1rem;

  fill: currentColor;
}

.dashboard-remarks-tile__category-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dashboard-remarks-tile__category-count {
  flex-shrink: 0;

  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}


/* dashboard/show.css */
.dashboard-show {
  container-type: inline-size;

  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.dashboard-show__heading {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une colonne sur téléphone, deux dès que la place existe — c'est la demande
   de JC. Le palier est à 480px et non 640 comme sur les listes de cartes : une
   tuile de compteur tient dans bien moins de large qu'une tuile de carte, qui
   porte un titre et trois lignes de description. */
.dashboard-show__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 480px) {
  .dashboard-show__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));

    /* Les quatre tuiles font la MÊME hauteur, y compris d'une rangée à l'autre.
       Sans cette ligne, chaque rangée se dimensionne sur son contenu : mesuré
       en WebKit à 1280px, 164px pour la rangée du haut (dont une tuile porte
       deux rangs de sujets) contre 145 pour celle du bas. Dix-neuf pixels
       d'écart sur quatre boutons censés être des jumeaux, relevés par l'axe
       `siblingControlCoherence` avant que l'œil ne les voie.
       ⚠️ Ici et pas au-dessus : en une seule colonne, aucune tuile n'en côtoie
       une autre, et l'égalisation ne ferait que creuser du vide sous les
       catégories à peu de sujets. */
    grid-auto-rows: 1fr;
  }
}


/* dialogue_lines/form.css */
/* L'écran de saisie d'une ligne. Une seule chose à corriger ici : la mise en
   page de la case à cocher.

   ⚠️⚠️ POURQUOI CE FICHIER EXISTE. `.ui-input--checkbox` (fulgore,
   `ui/inputs/checkbox.css`) est en `flex-direction: row-reverse` — pensé pour
   un couple case + libellé, où l'inversion met la case AVANT son libellé
   alors que le DOM les écrit dans l'autre ordre. Mais `UI::Bases::Input#call`
   rend TROIS enfants (libellé, champ, indication), et l'inversion emporte
   l'indication avec : elle se retrouve TOUT À GAUCHE, avant la case, donc
   lue avant qu'on sache de quoi elle parle. Vu à l'écran en WebKit 390 sur
   « Affichée à l'écran », pas déduit du CSS.

   La vraie correction est côté gem — `.ui-input--checkbox` devrait poser son
   ordre explicitement au lieu de compter sur `row-reverse`, qui ne tient que
   tant qu'il y a exactement deux enfants. Hors périmètre de cette session
   (tout changement fulgore passe par son gate de compatibilité et un bump du
   `Gemfile.lock` que suivent quatre autres projets). En attendant, l'ordre
   est rétabli ICI, scopé à ce seul formulaire — même arbitrage, et même
   honnêteté, que la recette `.ui-visually-hidden` recopiée dans
   `sheets/show.css`. */

.dialogue-lines-form .ui-input--checkbox {
  /* On repart du sens de lecture normal plutôt que de bricoler des `order`
     dans un axe inversé, où « premier » veut dire « à droite ». */
  flex-direction: row;
  flex-wrap: wrap;
}

/* L'ordre voulu : la case, son libellé, puis l'indication sur sa propre
   ligne. Les trois `order` sont posés explicitement — c'est précisément
   l'absence de cette déclaration qui rend la règle de la gem fragile. */
.dialogue-lines-form .ui-input--checkbox input[type="checkbox"] {
  order: 0;
}

.dialogue-lines-form .ui-input--checkbox label {
  order: 1;
}

/* `flex-basis: 100%` la pousse à la ligne suivante et lui donne toute la
   largeur — une indication d'une phrase entière n'a pas sa place à côté d'une
   case de 18px. */
.dialogue-lines-form .ui-input--checkbox .ui-input__hint {
  order: 2;
  flex-basis: 100%;
}


/* dialogue_options/form.css */
/* L'écran de saisie d'une option. Une seule chose à corriger ici : la mise en
   page de la case à cocher — même défaut, même correction que
   `dialogue_lines/form.css` (lu en entier avant d'écrire ceci).

   ⚠️⚠️ POURQUOI CE FICHIER EXISTE. `.ui-input--checkbox` (fulgore,
   `ui/inputs/checkbox.css`) est en `flex-direction: row-reverse` — pensé pour
   un couple case + libellé, où l'inversion met la case AVANT son libellé
   alors que le DOM les écrit dans l'autre ordre. Mais `UI::Bases::Input#call`
   rend TROIS enfants (libellé, champ, indication) dès qu'un `hint:` est posé
   — ce qui est le cas de `spoken_field` — et l'inversion emporte l'indication
   avec : elle se retrouve TOUT À GAUCHE, avant la case.

   La vraie correction est côté gem (hors périmètre de cette session, même
   gate de compatibilité qu'à chaque changement fulgore). En attendant,
   l'ordre est rétabli ICI, scopé à ce seul formulaire. */

.dialogue-options-form .ui-input--checkbox {
  flex-direction: row;
  flex-wrap: wrap;
}

.dialogue-options-form .ui-input--checkbox input[type="checkbox"] {
  order: 0;
}

.dialogue-options-form .ui-input--checkbox label {
  order: 1;
}

.dialogue-options-form .ui-input--checkbox .ui-input__hint {
  order: 2;
  flex-basis: 100%;
}


/* dialogues/box_preview.css */
/* L'aperçu d'un texte dans la boîte de dialogue du jeu — le repli exact, pas
   une illustration. Racine `<div>` posée par le composant
   (`skip_encapsulation? true`).

   ⚠️⚠️ AUCUNE MESURE N'EST ÉCRITE ICI, ET C'EST DÉLIBÉRÉ. Largeur, corps,
   interligne, position du trait : tout arrive en style EN LIGNE depuis
   `Dialogues::BoxPreview`, parce que ces chiffres viennent de
   `game/src/conversation.rs` et doivent se relire à côté du commentaire qui
   les justifie. Une largeur en dur dans cette feuille serait un chiffre
   orphelin — et une variable maison (`--rf-…`) serait indiscernable d'un token
   fulgore mal orthographié, qui retombe en silence sur `initial`.

   ⚠️⚠️ CE QUI FAIT LA JUSTESSE DU REPLI TIENT EN TROIS DÉCLARATIONS, et deux
   d'entre elles sont ici : `overflow-wrap: normal`, qui coupe au MOT comme
   `lignes_repliees`, et `white-space: pre-wrap`. La troisième — la famille et
   la largeur — est posée en ligne. Changer l'une des trois fait mentir
   l'aperçu sans rien casser à l'écran. */

.dialogues-box-preview {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);

  margin-top: var(--space-1);

  /* ⚠️⚠️ SANS LUI, LE `overflow-x: auto` DU CADRE NE SERT À RIEN, ET SA
     PROMESSE ÉTAIT FAUSSE. Cet aperçu est un item de `.ui-form`, qui est en
     `display: grid` (fulgore, `fulgore/form.css:3`) : un item de grille est à
     `min-width: auto`, donc il REFUSE de descendre sous la largeur de son
     contenu. Le logement de menu à 370 px imposait 388 px à toute la colonne,
     et c'est la PAGE qui débordait — mesuré en WebKit à 390 : page à 404 px,
     aperçu à 388, la grille des portraits entraînée avec. Avec `min-width: 0` :
     page à 390, aperçu à 358, et la boîte défile enfin dans son cadre comme le
     commentaire d'à côté le promet depuis n93. Piège nommé dans la skill
     `visual-render` — item de grille à `min-width: auto`, invisible à Chrome
     pendant longtemps. */
  min-width: 0;
}

.dialogues-box-preview__caption {
  margin: 0;

  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
}

/* ⚠️ `overflow-x: auto` SUR LE CADRE, ET C'EST LA SEULE PARADE HONNÊTE. La
   boîte a une largeur IMPOSÉE par le jeu — 289 px pour une réplique, 370 pour
   un logement de menu — qu'on ne peut ni réduire ni laisser se replier sans
   détruire ce que l'aperçu mesure. Sur un téléphone étroit, un logement de
   menu déborde donc de quelques pixels : il défile dans son propre cadre,
   jamais la page. */
.dialogues-box-preview__frame {
  overflow-x: auto;

  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);

  background: var(--color-surface-subtle);
}

/* L'aperçu est rendu À 1 POUR 1, à la taille exacte du jeu — 12 px de corps,
   c'est petit, et c'est la vérité.

   ⚠️⚠️ UN `zoom: 2` A ÉTÉ ESSAYÉ POUR LA LISIBILITÉ, ET RETIRÉ APRÈS MESURE en
   WebKit et en Chromium. Contrairement à ce qu'on attend de `zoom`, le cadre
   parent ne réservait PAS la place du rendu agrandi dans Chromium : la boîte de
   370 px se faisait couper à droite par son propre cadre.
   ⚠️ Le compteur, lui, n'y était pour rien — sa divergence entre moteurs
   existe à toutes les échelles, et c'est le contrôleur qui la traite (il compte
   des ordonnées, pas des rectangles). Ne pas croire, en relisant ce bloc, que
   retirer le `zoom` a réglé le comptage. */
.dialogues-box-preview__box {
  position: relative;
  display: inline-block;
}

/* Le trait qui marque la dernière ligne tenable. Sa position vient du
   composant. Ce qui passe dessous DISPARAÎT dans le jeu — Bevy replie le texte
   dans son `TextBounds` —, donc ce trait est la seule chose qui distingue une
   réplique courte d'une réplique tronquée. */
.dialogues-box-preview__rule {
  position: absolute;
  right: 0;
  left: 0;

  border-top: 1px dashed var(--color-danger);

  pointer-events: none;
}

/* ⚠️ `white-space: pre-wrap` — un saut de ligne SAISI est conservé jusqu'au
   jeu, et Bevy le rend comme une rupture. L'aperçu le rend donc aussi.
   ⚠️⚠️ SUR CE POINT PRÉCIS L'APERÇU EST PLUS JUSTE QUE LE TEST RUST :
   `lignes_repliees` découpe avec `split_whitespace`, qui traite un saut de
   ligne comme une simple espace et sous-compte donc les lignes. Aucune
   réplique n'en porte aujourd'hui — le défaut est endormi, pas absent.

   ⚠️ `overflow-wrap`/`word-break: normal` explicites : un mot plus long que la
   boîte doit DÉBORDER, pas se couper. C'est ce que fait `lignes_repliees`, qui
   pose le mot entier sur une ligne neuve quoi qu'il arrive. */
.dialogues-box-preview__text {
  margin: 0;

  color: var(--color-text);

  white-space: pre-wrap;
  overflow-wrap: normal;
  word-break: normal;
}

.dialogues-box-preview__warning {
  margin: 0;

  color: var(--color-danger);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
}


/* dialogues/index_page.css */
/* La page `/dialogues` : titre + bouton de création, puis la liste des
   tuiles. Calquée sur `.characters-index-page` et `.places-index-page` —
   même structure à deux blocs, même absence d'onglets (un `Dialogue` ne
   porte aucun statut). */

.dialogues-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.dialogues-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.dialogues-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Même palier que les trois autres listes du hub (640px), pour qu'elles se
   lisent pareil au même point de rupture. `minmax(0, 1fr)` des deux côtés :
   sans lui la colonne implicite se dimensionne au max-content d'une tuile et
   déborde sous le seuil. */
.dialogues-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .dialogues-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* dialogues/item.css */
/* La tuile d'un dialogue : titre + nombre de lignes. Racine `<article>`
   posée par le composant (`skip_encapsulation? true`), tout l'habillage vit
   sur la classe racine — même motif que `.characters-item`.

   Aucune vignette : un dialogue n'a rien à montrer en image. */

.dialogues-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.dialogues-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.dialogues-item__meta {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}


/* dialogues/portrait_picker.css */
/* La grille de têtes qui remplace la liste déroulante du portrait, dans les
   deux formulaires qui citent un `DialogueLine`/`DialogueOption` (racine
   `<fieldset>`, `skip_encapsulation? true`).

   ⚠️⚠️ AUCUNE VARIABLE MAISON `--rf-…` — même refus que dans
   `dialogues/box_preview.css` : une variable non-fulgore serait indiscernable
   d'un token fulgore mal orthographié, qui retombe en silence sur `initial`.

   ⚠️ JC JUGE CET ÉCRAN AU POUCE, SUR SON TÉLÉPHONE. La grille cible environ
   quatre têtes par largeur de téléphone (390 px) — voir le calcul sous
   `__grid`. */

/* ⚠️⚠️ LE RYTHME DU FORMULAIRE SE POSE ICI, ET IL FAUT LES DEUX CÔTÉS.
   fulgore espace ses champs par `.ui-form .ui-input + .ui-input { margin-top }`
   (`fulgore/form.css:5`) : un champ qui n'est pas un `.ui-input` casse la
   chaîne DANS LES DEUX SENS — il ne reçoit rien du précédent, et le suivant ne
   reçoit rien de lui. Mesuré en rendu : la légende « Expression » collait au
   sous-titre de « Qui parle », et « Côté » collait à la grille. `margin-block`
   et non `margin-top` pour cette raison ; le formulaire étant en `grid`, ces
   marges ne se replient sur rien. */
.dialogues-portrait-picker {
  margin: var(--space-6) 0;
  padding: 0;
  border: 0;
}

/* Un `<fieldset>` porte ses bordures et ses marges par défaut dans tous les
   navigateurs (même parade que `places/show.css:220`, sur un groupe de
   champs voisin) : on les retire, la légende suffit à grouper. */
.dialogues-portrait-picker__label {
  padding: 0;

  font-weight: var(--font-weight-semibold);
}

.dialogues-portrait-picker__hint {
  margin: var(--space-1) 0 var(--space-3);

  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

/* Rendu par le composant à la place de la grille quand la fiche du
   personnage n'a encore aucun portrait (`PortraitPicker#empty_message`). */
.dialogues-portrait-picker__empty {
  margin: 0;

  /* `subtle` et non `muted` : c'est la teinte des deux autres états vides du
     namespace (`dialogues/show.css`, listes de lignes et de menu). */
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
}

/* ⚠️⚠️ MINMAX DÉRIVÉ DES TOKENS, PAS UN CHIFFRE POSÉ : `--space-16` (64 px)
   est la boîte de la vignette elle-même ; `--space-4` (16 px) budgète le
   padding de la tuile (2 × `--space-1`) ET sa bordure (2 × 1,5 px), avec un
   peu de marge — 8 + 3 = 11 px de besoin réel contre 16 px offerts.

   Calcul des colonnes à 390 px (téléphone) : `.app-main` prend
   `padding-inline: var(--space-4)` de chaque côté (`application.css`) et
   `.ui-form` fulgore n'ajoute aucun padding horizontal propre
   (`ui/bases/form.css`) — 358 px restent. Avec un minmax de 80 px et un
   gap de `--space-3` (12 px) : 4 tuiles tiennent (4×80 + 3×12 = 356 ≤ 358),
   une 5ᵉ ne tient pas (5×80 + 4×12 = 448 > 358). ✅ Confirmé par un rendu
   réel à 390 × 844, WebKit ET Chromium, sur les deux formulaires : quatre
   têtes par rangée, aucun débordement horizontal. */
.dialogues-portrait-picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-16) + var(--space-4)), 1fr));
  gap: var(--space-3);
}

/* ⚠️⚠️ LE CONTRÔLEUR STIMULUS MASQUE UNE TUILE PAR `hidden`, PAS PAR UNE
   CLASSE (`portrait_picker_controller.js#reduce` : `tile.hidden = …`). Le
   seul `display` posé sur cette racine est donc gardé par `:not([hidden])` —
   même parade que `cards/picker.css:176`.
   AUCUNE règle `[hidden] { display: none }` n'est ajoutée : la règle UA du
   navigateur suffit, et c'est VÉRIFIÉ ICI plutôt que déduit d'un précédent —
   aucune règle AUTEUR ne pose `display` sur ce `<label>` (`.ui-input label`
   est bornée à `.ui-input`, et ce composant rend sa propre racine), et le
   masquage a été mesuré en navigateur.
   ⚠️ Ne pas chercher d'appui du côté de fulgore : `ui/tabs.css:38`,
   `pill_select.css:42` et `file_zone.css:61` écrivent au CONTRAIRE leur
   `[hidden] { display: none }` en toutes lettres — leur `display` d'auteur
   porte sur le même élément, le nôtre non. */
.dialogues-portrait-picker__tile {
  box-sizing: border-box;
  /* `min-width: 0` : sans lui, un item de grille refuse de descendre sous la
     largeur de son contenu non cassable (même piège que
     `places/show.css:207` sur `.ui-input`) — une légende sans espace
     ferait déborder toute la grille, page comprise. */
  min-width: 0;

  padding: var(--space-1);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-lg);

  text-align: center;

  cursor: pointer;
  transition:
    border-color var(--duration-fast) var(--easing-default),
    background var(--duration-fast) var(--easing-default);

  &:not([hidden]) {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
  }

  &:hover {
    border-color: var(--color-brand);
  }

  /* La vignette (64 px) dépasse déjà seule le seuil tapable de 44 pt iOS ;
     padding et bordure ajoutent à la marge, jamais en-dessous. */
  &:has(:checked) {
    border-color: var(--color-brand);
    background: var(--color-brand-a10);

    .dialogues-portrait-picker__caption {
      color: var(--color-text);
      font-weight: var(--font-weight-medium);
    }
  }

  /* Le radio est le porteur du focus (voir `__radio` plus bas, qui éteint son
     propre anneau natif) : c'est la tuile ENTIÈRE qui doit le montrer, pas un
     petit cercle de 20 px. */
  &:has(:focus-visible) {
    outline: none;
    box-shadow: var(--focus-ring);
  }
}

/* ⚠️ Reste VISIBLE et petit (JC) : si `:has()` venait à ne pas s'appliquer
   (WebKit le porte depuis 15.4 — la cible ici est Safari iOS récent), l'état
   coché resterait lisible par ce radio seul. */
.dialogues-portrait-picker__radio {
  width: var(--space-5);
  height: var(--space-5);

  accent-color: var(--color-brand);

  cursor: pointer;

  /* L'anneau de focus natif se déplace sur la tuile — voir
     `__tile:has(:focus-visible)` ci-dessus. */
  &:focus-visible {
    outline: none;
  }
}

/* Le cadre (`--blank`, `--missing`) et la vignette partagent EXACTEMENT la
   même boîte, `box-sizing: border-box` compris — sinon la bordure du cadre
   l'agrandirait au-delà des 64 px de l'image, et les tuiles se décaleraient
   selon ce que le build de jeu porte ou non. */
.dialogues-portrait-picker__frame,
.dialogues-portrait-picker__image {
  box-sizing: border-box;
  width: var(--space-16);
  height: var(--space-16);
}

.dialogues-portrait-picker__frame {
  display: flex;
  align-items: center;
  justify-content: center;

  border-radius: var(--radius-sm);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
}

/* La tuile vide (`value=""`), toujours en tête de grille. */
.dialogues-portrait-picker__frame--blank {
  border: 1.5px dashed var(--color-border-strong);

  color: var(--color-text-disabled);
  background: var(--color-surface-subtle);
}

/* La fiche cite un portrait dont l'image n'est pas encore dans le build du
   jeu (`GameBuild.asset_url` a rendu `nil` — voir `portrait_frame` dans le
   composant). Distinct du blanc : ce n'est pas un choix, c'est un manque. */
.dialogues-portrait-picker__frame--missing {
  border: 1.5px dashed var(--color-warning);

  color: var(--color-warning-text);
  background: var(--color-warning-surface);
}

/* Dessins pixel par pixel réduits de 128 à 64 : le lissage par défaut du
   navigateur en fait une bouillie, et JC juge la TÊTE. */
.dialogues-portrait-picker__image {
  border-radius: var(--radius-sm);

  object-fit: cover;
  image-rendering: pixelated;
}

.dialogues-portrait-picker__caption {
  font-size: var(--font-size-xs);

  color: var(--color-text-subtle);

  overflow-wrap: break-word;
}

/* Le nom du personnage, au-dessus de l'expression. Le contrôleur Stimulus le
   masque (`hidden`) dès que la grille est réduite à un seul locuteur : il ne
   distingue alors plus rien et coûte deux lignes par tuile. D'où un `display`
   gardé par `:not([hidden])`, comme sur `__tile`. */
.dialogues-portrait-picker__who {
  color: var(--color-text-muted);

  &:not([hidden]) {
    display: block;
  }
}

.dialogues-portrait-picker__expression {
  display: block;
}

/* Le dessin du portrait n'est pas dans le build servi. La raison était dans un
   `title=`, c'est-à-dire nulle part pour un doigt (JC, 2026-09-15) : elle
   s'écrit sous la tête, dans la même teinte que le cadre `--missing` qu'elle
   explique. */
.dialogues-portrait-picker__missing-note {
  display: block;

  color: var(--color-warning-text);
  font-weight: var(--font-weight-medium);
}


/* dialogues/show.css */
/* La fiche d'un dialogue : ses ÉCHANGES dans l'ordre, chacun avec ses lignes
   puis son menu (ou son enchaînement). Calquée sur `.characters-show` pour la
   charpente (sections, titres, états vides, formulaire d'édition sur place),
   mais avec un niveau de nid supplémentaire — l'échange — qui doit se
   détacher visuellement pour que la scène se lise comme un script.

   `.dialogues-show__detail` est le SEUL enfant de
   `.ui-bases-record-view__body` : la règle fulgore
   `:only-child { max-width: 48rem; margin-inline: auto }` (record_view.css)
   s'applique donc automatiquement. */

.dialogues-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas, et que sa classe seule ne saurait pas accueillir. */
  container-type: inline-size;

  /* Même parade que `.characters-show__detail` : item de la grille fulgore
     `1fr` = `minmax(auto, 1fr)`, il refuse de descendre sous son min-content
     sans ça. Un texte de réplique sans espace (un cri, « TAAA — TA-TAAAA ! »)
     est exactement le contenu qui rejouerait le débordement. */
  min-width: 0;
}

.dialogues-show__exchanges,
.dialogues-show__add-exchange,
.dialogues-show__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  min-width: 0;
}

.dialogues-show__exchanges {
  gap: var(--space-5);
}

/* ============================================================
   UN ÉCHANGE
   ============================================================
   Une boîte à part entière — bordure plus marquée que celle d'une simple
   ligne (`--color-border-strong`, pas `--color-border`) : c'est elle qui doit
   se voir en premier au défilement, avant même de lire son contenu. */
.dialogues-show__exchange {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  padding: var(--space-4);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-lg);
  background: var(--color-surface-subtle);

  min-width: 0;
}

.dialogues-show__exchange-heading {
  margin: 0;
  padding-bottom: var(--space-2);
  border-bottom: 2px solid var(--color-border-strong);

  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.dialogues-show__exchange-errors {
  padding: var(--space-3);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-md);
  background: var(--color-danger-surface);
}

.dialogues-show__exchange-errors ul {
  margin: 0;
  padding-left: var(--space-4);

  font-size: var(--font-size-sm);
  color: var(--color-danger-dark);
}

/* Le formulaire d'édition (un champ + un bouton) et la suppression, posée
   nue par `UI::Buttons::ConfirmDestroy` — deux formulaires FRÈRES, un
   `<form>` ne pouvant pas en contenir un autre. Même charpente que
   `.characters-show__portrait`. */
.dialogues-show__exchange-tools {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.dialogues-show__exchange-tools > form:not(.dialogues-show__exchange-edit-form) {
  flex-shrink: 0;
}

.dialogues-show__exchange-edit-form {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--space-2);

  min-width: 0;
}

@container (min-width: 480px) {
  .dialogues-show__exchange-edit-form {
    flex-direction: row;
    align-items: flex-end;
  }

  .dialogues-show__exchange-edit-form .ui-input {
    flex: 1 1 0;
    min-width: 0;
  }
}

.dialogues-show__exchange-edit-form .ui-button {
  flex-shrink: 0;
}

/* ============================================================
   LES LIGNES D'UN ÉCHANGE
   ============================================================
   ⚠️⚠️ DIVERGENCE DE FOND, conservée de l'ancien composant : chaque ligne
   est du TEXTE, pas un formulaire ouvert — la modification se fait sur un
   autre écran (arbitrage n93, cf. `show.rb`). `<h3>`, pas `<h2>` : cette
   rubrique est maintenant sous le titre de l'ÉCHANGE. */
.dialogues-show__lines,
.dialogues-show__add-line,
.dialogues-show__menu,
.dialogues-show__add-option {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

.dialogues-show__lines-heading {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.dialogues-show__lines-empty,
.dialogues-show__menu-empty {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* `<ol>` et non `<ul>` : l'ordre EST l'information (c'est lui qu'on change
   avec les flèches). Le compteur natif est retiré — le rang se lit dans la
   succession, un numéro en plus serait du bruit à côté du nom de celui qui
   parle. */
.dialogues-show__lines-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
  padding: 0;

  list-style: none;
}

.dialogues-show__line {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);

  min-width: 0;
}

/* Qui parle, son bord, son expression — la ligne de service, en retrait.
   `overflow-wrap: anywhere` : un nom de personnage est du texte libre. */
.dialogues-show__line-meta {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

/* LE TEXTE, qui est le sujet de l'écran : taille de lecture, pas de méta.
   `white-space: pre-wrap` : le texte vient d'un `<textarea>`, ses retours à
   la ligne sont un choix d'écriture et doivent survivre à l'affichage. */
.dialogues-show__line-text {
  margin: 0;

  font-size: var(--font-size-base);
  line-height: var(--line-height-loose);
  color: var(--color-text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Les quatre gestes. `flex-wrap` OBLIGATOIRE et non décoratif : deux flèches
   en icône plus « Modifier » et « Supprimer » en toutes lettres ne tiennent
   pas sur une rangée de 390px moins les marges — sans le retour à la ligne,
   c'est un débordement, pas un tassement. Partagé par les lignes ET les
   options du menu. */
.dialogues-show__line-actions,
.dialogues-show__option-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Les formulaires nus posés par `UI::Bases::Button` (les flèches) et
   `UI::Buttons::ConfirmDestroy` (supprimer) ne rétrécissent jamais : ils
   portent un bouton de taille fixe, et céder les écraserait avant le lien
   « Modifier », seul élément dont la largeur peut varier. Même piège que
   `.characters-show__portrait > form`. */
.dialogues-show__line-actions form,
.dialogues-show__option-actions form {
  flex-shrink: 0;
}

/* ============================================================
   LE MENU
   ============================================================ */
.dialogues-show__options-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
  padding: 0;

  list-style: none;
}

.dialogues-show__option {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  padding: var(--space-3);
  border: 1px solid var(--color-brand-light);
  border-radius: var(--radius-md);
  background: var(--color-surface);

  min-width: 0;
}

.dialogues-show__option-text {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.dialogues-show__option-speaker,
.dialogues-show__option-destination,
.dialogues-show__option-condition {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

/* ============================================================
   L'ENCHAÎNEMENT
   ============================================================
   Discret, à dessein : ce n'est pas un choix du joueur, c'est un raccord de
   montage. Pas de bordure ni de fond propre (à la différence d'une option de
   menu) — seule la couleur atténuée le distingue du texte courant. */
.dialogues-show__enchainement {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.dialogues-show__enchainement-text {
  margin: 0;

  font-size: var(--font-size-sm);
  font-style: italic;
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}

/* ============================================================
   ÉDITER / SUPPRIMER LE DIALOGUE
   ============================================================ */
.dialogues-show__actions {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-3);
}


/* game/screen.css */
/* ⚠️ CE COMPOSANT N'EST PLUS SERVI DANS LA PAGE DU HUB depuis le 2026-08-07 :
   il a son propre gabarit (`layouts/game.html.erb`), qui lui donne l'écran
   entier et le centre (`.game-body__main`, `application.css`). D'où un
   composant qui ne pose plus ni cadre ni gouttière — il ne reste que le
   canevas et son message de repli.
   Le but est de voir le moteur TEL QU'IL SERA sur un téléphone, pas un aperçu
   rétréci entre une barre et une colonne. */
.game-screen {
  display: flex;
  align-items: center;
  justify-content: center;

  width: 100%;
  height: 100%;
  box-sizing: border-box;
}

/* Le titre reste dans le HTML mais quitte l'écran : une page sans titre est un
   trou pour un lecteur d'écran, et sur un téléphone tenu couché la hauteur est
   précisément ce qui manque. `clip-path` plutôt que `display: none`, qui le
   retirerait aussi de l'arbre d'accessibilité. */
.game-screen__heading {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.game-screen__canvas-area {
  display: flex;
  align-items: center;
  justify-content: center;

  width: 100%;
  height: 100%;
}

/* ⚠️ `max-height` AUTANT QUE `max-width`, et c'est ce qui fait tenir le jeu sur
   un téléphone couché. Le canevas est un élément remplacé : avec ses deux
   dimensions en `auto` et ses deux plafonds posés, le navigateur conserve ses
   proportions et le borde du côté qui déborde. Sans le plafond de HAUTEUR, un
   écran plus large que haut laissait le bas du jeu sortir de la fenêtre — et
   il n'y a plus de page qui défile pour aller le chercher. */
.game-screen__canvas {
  display: block;
  max-width: 100%;
  max-height: 100dvh;
  width: auto;
  height: auto;

  /* Le canevas est mis à l'échelle par la page (`max-width` ci-dessus). Sans
     consigne, le navigateur INTERPOLE : il fabrique des pixels intermédiaires
     entre deux voisins, et chaque frontière entre deux images jointives devient
     un trait qui n'existe nulle part dans le jeu.

     ⚠ Le réglage interne de Bevy (`ImagePlugin::default_nearest`, `main.rs`) ne
     couvre PAS ce cas : il décide comment une texture est échantillonnée DANS le
     jeu, pas comment la page agrandit le résultat. Il faut les deux, et le
     défaut n'apparaît que le jour où deux images se touchent — il avait mis
     cinquante sessions à se voir sous le run-and-gun. */
  image-rendering: pixelated;

  /* Sans ceci, un glissement du pouce sur le canevas FAIT DÉFILER LA PAGE au
     lieu de piloter le jeu : le navigateur s'attribue le geste avant que le jeu
     ne le voie. Toute commande tactile serait mort-née. */
  touch-action: none;
}

/* Le canevas est rendu par la page, donc TOUJOURS présent — même quand aucun
   build n'est déployé. Sans dimensions de repli, il s'affiche en 0 px et
   l'écran devient entièrement vide, ce qui se lit comme une panne plutôt que
   comme un jeu absent.
   ⚠️ Une LARGEUR est nécessaire en plus de la hauteur depuis que le composant
   n'a plus de cadre : sans elle, le message de repli se retrouvait seul au
   milieu d'un écran noir. */
.game-screen__canvas:not([width]) {
  min-width: 16rem;
  min-height: 12rem;
}

/* ⚠ CE QUE CES DEUX RÈGLES FONT VRAIMENT : elles changent l'APPARENCE de
   l'anneau de focus, elles ne le font pas disparaître. Mesuré, pas supposé.

   Bevy pose `tabindex="0"` sur le canevas et LE FOCALISE tout seul au
   démarrage (`document.activeElement === canvas`, relevé le 2026-08-04). Le
   navigateur applique alors son `outline: 3px auto` : le jeu s'encadre d'un
   trait bleu vif, épais, permanent — puisque rien ne reprend le focus.

   Le canevas DOIT rester focalisable, sans quoi aucune touche ne l'atteint. On
   remplace donc le trait par le halo du design system, celui que portent tous
   les autres interactifs du hub.

   ⚠ ET WEBKIT FAIT MATCHER `:focus-visible` SUR CE FOCUS PROGRAMMATIQUE — le
   halo est donc présent DÈS LE CHARGEMENT, mesuré à l'écran. Écrire ici que
   `:focus-visible` réserve l'anneau à la navigation au clavier serait faux, et
   c'est ce que la première version de ce commentaire disait. Le résultat reste
   celui qu'on veut : le canevas a bel et bien le clavier, et il le dit dans la
   langue du reste de l'écran plutôt qu'avec le bleu du navigateur. */
.game-screen__canvas:focus {
  outline: none;
}

.game-screen__canvas:focus-visible {
  /* Le halo du design system plutôt qu'un `outline` à nous : c'est ce que
     portent tous les autres interactifs du hub, et le canevas n'a aucune
     raison de s'annoncer autrement. */
  box-shadow: var(--focus-ring);
}

.game-screen__load-error {
  margin: 0;
  padding: var(--space-4);
  color: var(--color-text-muted);
  text-align: center;
}

/* ── « TOURNE TON TÉLÉPHONE » ──────────────────────────────────────────────
   Le jeu se joue à l'horizontale, et sur iPhone rien ne peut l'imposer : iOS
   ignore la clef `orientation` du manifeste, et Safari n'implémente pas le
   verrouillage d'orientation. On le dit, faute de pouvoir le faire.

   Absent de l'écran par défaut — c'est la règle sous condition qui l'allume. */
.game-screen__rotate {
  display: none;
}

/* ⚠️ `pointer: coarse` EN PLUS DE L'ORIENTATION, et ce n'est pas du zèle : une
   fenêtre de bureau plus haute que large est « en portrait » elle aussi, et le
   message s'y afficherait alors qu'aucune rotation n'y a de sens. La condition
   du doigt réserve le message aux appareils qui se tournent.

   ⚠️ IL RECOUVRE, IL NE MASQUE PAS. Un `display: none` sur le canevas
   ramènerait sa taille à zéro, et Bevy redimensionnerait sa cible de rendu —
   le jeu reviendrait cassé au retour en paysage. `position: fixed` le laisse
   intact derrière, et le moteur continue de tourner sans rien savoir. */
@media (orientation: portrait) and (pointer: coarse) {
  .game-screen__rotate {
    display: flex;
    align-items: center;
    justify-content: center;

    position: fixed;
    inset: 0;
    z-index: 1;

    margin: 0;
    padding: var(--space-6);

    background: var(--color-surface);
    color: var(--color-text);
    font-size: var(--font-size-lg);
    text-align: center;
    text-wrap: balance;
  }

  /* Recouvert ne veut pas dire inerte : le canevas garde le `tabindex="0"`
     que Bevy lui pose, et un utilisateur au clavier ou au lecteur d'écran
     atterrissait dans un jeu qu'on venait de lui masquer. `visibility:
     hidden` le retire du focus ET de l'arbre d'accessibilité SANS toucher à
     sa boîte — contrairement à `display: none` (cf. ci-dessus), la taille ne
     bouge pas et Bevy ne redimensionne rien. */
  .game-screen__canvas-area {
    visibility: hidden;
  }
}


/* images/form.css */
.images-form__current {
  margin-block-end: var(--space-4);
}

.images-form__current-img {
  display: block;
  max-width: 100%;
  max-height: 16rem;
  width: auto;
  border-radius: var(--radius-md);
  object-fit: contain;
}


/* images/index.css */
/* `ui-list-table` met toutes ses cellules en `white-space: nowrap`
   (fulgore, `bases/list.css`) — un réglage taillé pour les colonnes courtes
   auxquelles ce socle sert d'habitude : ville, prix, statut. Une légende monte
   ici à 140 caractères : en nowrap elle pousse la table 236px au-delà de son
   cadre au viewport 1280 et se fait couper net, sans même l'ellipse qui
   signalerait le texte manquant. Seules les cellules de données sont
   libérées : la poignée et le rang tiennent sur un caractère, un retour à la
   ligne ne ferait que les déformer. */
.images-index .ui-list-table td[data-label] {
  white-space: normal;
  min-width: 0;
}

.images-row-thumb {
  display: block;
  width: 96px;
  height: 72px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background-color: var(--color-surface-subtle);
  flex-shrink: 0;
  cursor: pointer;
}

.images-row-thumb__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-width: 768px) {
  td:has(> .images-row-thumb) {
    display: block;
    padding: 0;
    margin-bottom: var(--space-3);
    border-bottom: none;
  }

  td:has(> .images-row-thumb)::before {
    display: none;
  }

  .images-row-thumb {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
  }
}


/* items/index_page.css */
/* La page `/objets` : titre + bouton de création, puis la liste des tuiles.
   Calquée sur `.characters-index-page` (`characters/index_page.css`) — même
   structure à deux blocs, même absence d'onglets (un `Item` ne porte aucun
   statut, `item.rb`). */

.items-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.items-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.items-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une seule colonne jusqu'à 640px, deux au-delà — même palier que
   `.characters-index-page__list` et `.places-index-page__list`, pour que les
   listes du hub se lisent pareil au même point de rupture. `minmax(0, 1fr)`
   des deux côtés : sans lui la colonne implicite se dimensionne au
   max-content d'une tuile et déborde sous le seuil. */
.items-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .items-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* items/item.css */
/* La tuile d'un objet : titre + méta (nature, rectangle, pile). Racine
   `<article>` posée par le composant lui-même (`skip_encapsulation? true`) :
   tout l'habillage vit ici, sur la classe racine — même motif que
   `.characters-item`.

   ⚠️ AUCUNE VIGNETTE, à la différence de `.places-item` : le hub ne détient
   pas le dessin d'un objet, seulement son chemin dans les assets du jeu
   (`item.rb`). Rien à afficher, donc rien à styler — pas un oubli. */

.items-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.items-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.items-item__meta {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}


/* items/show.css */
/* La fiche d'un objet, calquée sur `Characters::Show`
   (`characters/show.css`) — même base `UI::Bases::Show`, même absence de
   `list_component` et de `footer_slot` (`Items::Show` ne les surcharge pas),
   donc pas de `.items-show__list` ni `__footer` à styler : ces classes
   n'existent pas dans le DOM de ce composant.

   `.items-show__detail` est le SEUL enfant de
   `.ui-bases-record-view__body` (pas de `context_component` surchargé) : la
   règle fulgore `:only-child { max-width: 48rem; margin-inline: auto }`
   (record_view.css) s'applique donc automatiquement — rien à redéclarer.

   ⚠️ BEAUCOUP PLUS COURT QUE `characters/show.css`, et ce n'est pas un
   oubli : un objet ne porte aucune collection éditable en place. Pas de
   liste de lignes à deux formulaires frères, donc aucune des parades que
   ce fichier-là documente (le sélecteur `form` non préfixé, le
   `flex-shrink` du bouton, le palier à 480px). */

.items-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas, et que sa classe seule ne saurait pas accueillir. */
  container-type: inline-size;

  /* Même parade que `.characters-show__detail` : ce bloc est un item de la
     grille fulgore `.ui-bases-record-view__body` (`1fr` = `minmax(auto,
     1fr)`) — sans `min-width: 0` il refuse de descendre sous son
     min-content. Un chemin de dessin non coupable
     (`objets/boite-de-cartouches.png`) est exactement le genre de contenu
     qui rejouerait le débordement. */
  min-width: 0;
}

.items-show__properties,
.items-show__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

/* ============================================================
   CE QUE L'OBJET EST
   ============================================================
   Une liste de définitions. Empilée par défaut — étiquette au-dessus de sa
   valeur — puis en deux colonnes au-delà de 480px de conteneur, comme la
   rangée de `.characters-show__portrait`. La colonne d'étiquettes est figée
   à 12rem : « Rectangle occupé » et « Grille donnée » sont les deux plus
   longues, et une colonne `auto` les ferait sauter d'une ligne à l'autre
   selon la présence de la grille, qui n'existe que sur un sac. */

.items-show__properties-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);

  margin: 0;
}

.items-show__property-label {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.items-show__property-value {
  margin: 0 0 var(--space-2);

  font-size: var(--font-size-base);
  color: var(--color-text);
  /* Un chemin de dessin ne porte aucune espace : sans cette règle il
     déborderait de sa colonne au lieu de se couper. */
  overflow-wrap: anywhere;
}

@container (min-width: 480px) {
  .items-show__properties-list {
    grid-template-columns: 12rem minmax(0, 1fr);
    align-items: baseline;
  }

  /* La marge basse ne servait qu'à séparer les paires empilées : en deux
     colonnes, c'est le `gap` de la grille qui s'en charge. */
  .items-show__property-value {
    margin-bottom: 0;
  }
}

/* ============================================================
   ÉDITER / SUPPRIMER L'OBJET
   ============================================================ */
.items-show__actions {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-3);
}


/* places/index_page.css */
/* La page `/lieux` : titre + bouton de création, puis la liste des tuiles.
   Calquée sur `.sheets-index-page` (`sheets/index_page.css`) — même
   structure à deux blocs, même absence d'onglets (un `Place` ne porte
   aucun statut, `place.rb`). */

.places-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.places-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.places-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une seule colonne jusqu'à 640px, deux au-delà — même palier que
   `.sheets-index-page__list`, pour que les listes du hub se lisent pareil
   au même point de rupture. `minmax(0, 1fr)` des deux côtés : sans lui la
   colonne implicite se dimensionne au max-content d'une tuile (un titre de
   lieu non coupable) et déborde sous le seuil. */
.places-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .places-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* places/item.css */
/* La tuile d'un lieu : vignette + titre + méta (vue, famille, sorties).
   Racine `<article>` posée par le composant lui-même
   (`skip_encapsulation? true`) : tout l'habillage vit ici, sur la classe
   racine — même motif que `.sheets-item`.

   `flex column`, comme `.sheets-item` (pas `grid` comme `.cards-item`) :
   trois enfants directs au plus (`__thumbnail`, `__title`, `__meta`),
   aucune ligne à réserver séparément. */

.places-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

/* Même traitement que `.cards-item__thumbnail` : `16 / 9`, recadrée,
   fond de repli visible tant que l'image charge — un lieu sans image reste
   listé, la tuile n'a alors ni `__thumbnail` ni `__thumbnail-img`
   (`Places::Item#thumbnail` rend `nil`). */
.places-item__thumbnail {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface-alt);
}

.places-item__thumbnail-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.places-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.places-item__meta {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}


/* places/show.css */
/* La fiche d'un lieu, calquée sur `.sheets-show__detail`
   (`sheets/show.css`) — même base `UI::Bases::Show`, même absence de
   `list_component`/`footer_slot`.

   `.places-show__detail` est le SEUL enfant de `.ui-bases-record-view__body`
   (`Places::Show` ne surcharge pas `context_component`) : la règle fulgore
   `:only-child { max-width: 48rem; margin-inline: auto; width: 100% }`
   (record_view.css) s'applique donc automatiquement — pas besoin de
   redéclarer une largeur ici. */

.places-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas, et que sa classe seule ne saurait pas accueillir. */
  container-type: inline-size;

  /* Même parade que `.sheets-show__detail` : cet item de la grille fulgore
     `.ui-bases-record-view__body` (1fr 1fr) refuse de descendre sous son
     min-content sans `min-width: 0` — un titre de lieu long pourrait
     rejouer le même débordement. */
  min-width: 0;
}

/* ============================================================
   L'IMAGE PEINTE
   ============================================================
   Fond de repli `--color-surface-alt` (même token que
   `.cards-item__thumbnail`) : visible tant que l'image ne remplit pas tout
   le cadre (`object-fit: contain`, contrairement à la vignette de la tuile
   qui recadre en `cover` — ici c'est l'image ENTIÈRE qu'on veut voir, pas
   un aperçu recadré). */
.places-show__painting {
  display: flex;
  align-items: center;
  justify-content: center;

  width: 100%;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;

  background: var(--color-surface-alt);
}

.places-show__painting-img {
  display: block;
  width: 100%;
  max-height: 24rem;
  object-fit: contain;
}

.places-show__painting-missing {
  margin: 0;
  padding: var(--space-6);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-lg);

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  text-align: center;
}

/* ============================================================
   VUE ET FAMILLE
   ============================================================ */
.places-show__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-6);

  margin: 0;
}

.places-show__meta-term {
  margin: 0;

  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.places-show__meta-description {
  margin: 0 0 0 0;

  font-size: var(--font-size-base);
  color: var(--color-text);
  overflow-wrap: anywhere;
}

/* Les deux sections de sorties partagent la même colonne titre + contenu —
   même motif que `.sheets-show__stats, .sheets-show__add-stat, ...`. */
.places-show__exits,
.places-show__add-exit {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

.places-show__exits-heading,
.places-show__add-exit-heading {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.places-show__exits-empty,
.places-show__add-exit-unavailable {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* ============================================================
   LA LISTE DE SORTIES
   ============================================================ */
.places-show__exits-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  margin: 0;
  padding: 0;

  list-style: none;
}

.places-show__exit {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);

  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.places-show__exit-label {
  flex-shrink: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
}

/* La mention « par un geste » ne paraît que sur les sorties qui en portent
   une, donc jamais sur la plupart : elle se lit comme une note à côté du
   lien, pas comme une colonne vide ailleurs. `margin-left: auto` la pousse
   contre le bouton de retrait plutôt que de la coller au lieu visé, dont le
   titre peut être long. */
.places-show__exit-gesture {
  flex-shrink: 0;
  margin-left: auto;

  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

/* Le lien vers le lieu visé est un flex-item à côté de l'étiquette et du
   bouton de retrait — même motif que `.sheets-show__card .ui-link`.
   `flex-shrink: 0` sur le formulaire nu du bouton (posé sans classe par
   `ConfirmDestroy`, `ui/bases/button.rb#call`) : il ne doit jamais être
   celui qui cède la place. */
.places-show__exit .ui-link {
  min-width: 0;
  overflow-wrap: anywhere;
}

.places-show__exit form {
  flex-shrink: 0;
}

/* Empilé en mobile, en rangée au-delà de 480px de CONTENEUR — même palier
   que `.sheets-show__add-stat-form`. */
.places-show__add-exit-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Le champ caché `place_id` ne doit réserver ni gap ni piste : c'est un
   `<input type="hidden">`, invisible par nature, mais son wrapper
   `.ui-input` (display: grid, `fulgore/input.css`) resterait sinon un
   enfant flex à part entière — même piège déjà documenté dans
   `sprite_imports/unfold.css`. */
.places-show__add-exit-form .ui-input--hidden {
  display: none;
}

@container (min-width: 480px) {
  .places-show__add-exit-form {
    flex-direction: row;
    align-items: flex-end;
  }

  /* Les champs `.ui-input` visibles (menu de destination, étiquette) se
     partagent la rangée à parts égales, le bouton `UI::Buttons::Submit`
     garde sa largeur de contenu (inline-flex, non concerné). `min-width: 0`
     : même piège que `.sheets-show__add-stat-form .ui-input` — sans lui un
     `.ui-input` refuse de descendre sous la largeur de son `<select>`/
     `<input>` interne (`width: 100%`). */
  .places-show__add-exit-form .ui-input:not(.ui-input--hidden) {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* Le seuil et l'arrivée — deux groupes de champs numériques en cases de
   16 px. Un `<fieldset>` porte ses bordures et ses marges par défaut dans
   tous les navigateurs : on les retire, la légende suffit à grouper. */
.places-show__exit-position {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.places-show__exit-position legend {
  padding: 0;
  font-weight: var(--font-weight-semibold);
}

/* La consigne s'adresse à qui saisit — elle dit l'unité (des cases, pas des
   pixels) et le lieu concerné. Elle occupe toute la largeur du groupe. */
.places-show__exit-position-hint {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

/* La consigne du geste porte la même mise en page que celle des positions,
   et PAS la même classe — le spec de l'écran vérifie que toute consigne de
   position parle en « cases de 16 px », ce que celle-ci ne fait pas : elle
   dit ce que le seuil VEUT DIRE, pas dans quelle unité il se saisit. */
.places-show__exit-gesture-hint {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

@container (min-width: 480px) {
  /* ⚠️ `flex-wrap` EST INDISPENSABLE ICI. Le formulaire passe en rangée à
     cette largeur ; sans retour à la ligne, les deux groupes ci-dessous —
     six champs à eux deux — se serreraient sur la même rangée que le menu
     de destination et l'étiquette, et chaque champ tomberait à quelques
     dizaines de pixels. */
  .places-show__add-exit-form {
    flex-wrap: wrap;
  }

  /* Chaque groupe prend sa propre rangée, sous les deux champs du haut. */
  .places-show__exit-position {
    flex: 1 1 100%;
  }
}

/* ============================================================
   ÉDITER / SUPPRIMER LE LIEU
   ============================================================ */
.places-show__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}


/* places/zone_editor.css */
/* Le pinceau de la zone de marche : peindre au doigt, en portrait comme en
   paysage. Mobile-first — c'est l'écran de référence, pas une variante.

   `.places-zone-editor__form` est le SEUL enfant de `.ui-bases-record-view__body`
   (`Places::ZoneEditor` hérite `UI::Bases::Show` sans `context_component`) :
   la règle fulgore `:only-child { max-width: 48rem; margin-inline: auto }`
   (record_view.css) s'applique déjà — inutile de la redire ici.

   Le `<div data-controller="zone-paint">` qui porte `viewport`, `hint`,
   `palette` et le bouton n'a pas de classe (ce sont des TARGETS Stimulus,
   `places/zone_editor.rb#zone_paint_scope`) : on le cible par son attribut,
   comme `ui/inputs/color.css` cible déjà `[data-controller="color-picker"]`
   dans la gem — pas une entorse locale. */

/* Enfant de la GRILLE `.ui-bases-record-view__body` (fulgore,
   record_view.css) : à `min-width: auto`, l'item grid refuse de rétrécir
   sous la scène zoomée (802px pour la tente) et gonfle jusqu'au
   `max-width: 48rem` de la gem — c'est alors la PAGE qui scrolle, pas la
   fenêtre. Mesuré en WebKit 390px : formulaire à 768px, body à 358px.
   Le piège grid/flex nommé par la skill visual-render. */
.places-zone-editor__form {
  min-width: 0;
}

.places-zone-editor__form [data-controller="zone-paint"] {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Le champ caché (`walk_zone`) ne doit réserver ni gap ni piste : son
   wrapper `.ui-input` (display: grid, fulgore/input.css) resterait sinon un
   enfant à part entière de la colonne — même piège déjà tenu dans
   `places/show.css` pour `place_id`. */
.places-zone-editor__form .ui-input--hidden {
  display: none;
}

/* ============================================================
   LE VIEWPORT — fenêtre scrollable sur l'image zoomée
   ============================================================
   `overflow: auto`, PAS `touch-action: none` : c'est ici, et seulement ici,
   que le doigt-scroll natif reste possible en dehors du tracé (bord de
   l'image plus petite que la fenêtre). Le pinceau à deux doigts déplace
   aussi cette fenêtre par `scrollLeft`/`scrollTop` JS
   (`zone_paint_controller.js#move`).

   `max-height` en `vh` et non en `%` : le parent ne pose aucune hauteur
   propre (colonne flex, hauteur de contenu), un `%` y vaudrait `auto`. Plus
   bas en mobile paysage — la hauteur d'écran y est la plus rare — pour que
   la palette et Enregistrer restent visibles SOUS la grille sans un second
   scroll de page. */
.places-zone-editor__viewport {
  overflow: auto;
  max-height: 55vh;

  /* Même piège un cran plus bas : item de la colonne flex ci-dessus,
     `min-width: auto` l'empêcherait de rétrécir sous la scène. */
  min-width: 0;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface-alt);
}

@media (min-width: 768px) {
  .places-zone-editor__viewport {
    max-height: 70vh;
  }
}

/* ============================================================
   LE STAGE — image + grille superposées
   ============================================================
   `position: relative` : ancre `__grid` en `position: absolute; inset: 0`.
   Largeur/hauteur réelles posées en style inline par le JS
   (`zone_paint_controller.js#setup`, commenté « STYLE INLINE ASSUMÉ ») —
   dépendent de l'image chargée, une donnée que ce CSS ne connaît pas à
   l'écriture.

   `touch-action: none` ICI ET SUR `__grid`, JAMAIS SUR `__viewport` : sans
   lui, le tracé au doigt fait défiler la PAGE (le navigateur interprète le
   glissement comme un scroll et cesse de délivrer `pointerover`,
   `zone_paint_controller.js#start`). Sur `__viewport` ce serait la
   fenêtre elle-même qui perdrait son scroll natif un doigt. */
.places-zone-editor__stage {
  position: relative;
  touch-action: none;
}

/* ============================================================
   L'IMAGE — décor peint, zoomé ×2
   ============================================================ */
.places-zone-editor__image {
  display: block;
  image-rendering: pixelated;
}

/* ============================================================
   LA GRILLE — cases posées par le JS
   ============================================================
   `grid-template-columns` arrive en inline (JS, largeur de case 32px) ;
   `grid-auto-rows` la complète ICI pour que les rangées, que le JS ne pose
   pas, fassent la même hauteur — sans lui une `.zone-paint__cell` à
   `height: 100%` s'étirerait sur une rangée implicite dimensionnée par son
   contenu (un bouton vide, quasi nulle). */
.places-zone-editor__grid {
  position: absolute;
  inset: 0;
  display: grid;
  grid-auto-rows: 32px;

  touch-action: none;
}

/* ============================================================
   LES CASES — créées par le JS, pas par ce composant
   ============================================================
   `.zone-paint__cell`, nommée sur le contrôleur Stimulus (`zone-paint`) et
   non sur `css_class_name` : le serveur ne rend AUCUNE case
   (`places/zone_editor.rb#grid`, commentaire « Vide »), c'est le JS qui les
   construit une à une (`zone_paint_controller.js#buildGrid`) — il n'y a
   pas de classe Ruby à dériver ici.

   Bouton nu : `width/height: 100%` (pas de px arbitraire) pour remplir sa
   case de grille exactement — la taille réelle vient de `__grid`
   (`grid-template-columns`/`grid-auto-rows`), jamais fixée deux fois. */
.zone-paint__cell {
  display: block;
  width: 100%;
  height: 100%;

  padding: 0;
  border: 1px solid var(--color-border);
  appearance: none;

  background: transparent;

  cursor: pointer;
}

.zone-paint__cell:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Voile translucide : on peint PAR-DESSUS le décor, pas à sa place — le
   voile doit laisser deviner l'image sous la case. Pas de token alpha vert
   dans fulgore (seuls `--color-brand-a10/a20` et `--color-danger-a10/a20`
   existent, `variables.css`) : même construction `color-mix()` que ces
   tokens, appliquée à `--color-success` plutôt qu'une couleur brute — à
   remonter dans fulgore comme `--color-success-a20` si un second
   consommateur en a besoin.

   `[data-letter="."]` : rien à écrire, le fond `transparent` de la case
   nue suffit déjà — une case bloquée ne porte aucun voile. */
.zone-paint__cell[data-letter="x"] {
  background: color-mix(in srgb, var(--color-success) 40%, transparent);
}

/* ============================================================
   L'AIDE — un lieu large déborde sur un téléphone
   ============================================================
   Montrée/masquée par le JS selon une mesure réelle du débordement
   (`zone_paint_controller.js#montrerLAide`), jamais déduite ici d'un
   nombre de colonnes. */
.places-zone-editor__hint {
  margin: 0;

  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

/* ============================================================
   LA PALETTE — les deux pinceaux
   ============================================================ */
.places-zone-editor__palette {
  display: flex;
  gap: var(--space-2);
}

.places-zone-editor__brush {
  padding: var(--space-2) var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);

  color: var(--color-text);
  background: var(--color-surface);

  cursor: pointer;
  transition:
    background var(--duration-fast) var(--easing-default),
    border-color var(--duration-fast) var(--easing-default),
    color var(--duration-fast) var(--easing-default);
}

.places-zone-editor__brush:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* État enfoncé — le JS pose aussi `aria-pressed` (`letterValueChanged`),
   ce modificateur ne porte que le visuel. */
.places-zone-editor__brush.is-active {
  border-color: var(--color-brand);

  color: var(--color-text-inverted);
  background: var(--color-brand);
}


/* readings/screen.css */
/* Les lectures — un DOCUMENT, pas un écran de saisie.

   ⚠️ AUCUNE REDÉCLARATION DE `:root` ICI, à la différence du sidecar de la
   roadmap. `components.css` est UN fichier concaténé de tous les sidecars :
   un `:root` posé dans l'un d'eux s'applique à TOUT le hub, y compris aux
   écrans qui n'ont rien demandé. Cette page se contente donc des jetons de
   fulgore (`--color-*`, `--space-*`, `--radius-*`), comme les tuiles de
   l'accueil.

   ⚠️ LES DOMAINES SONT DES ATTRIBUTS `data-domaine`, PAS DES CLASSES. Une
   classe nue `.technique` ou `.phare` se ferait repeindre par le prochain
   sidecar qui en déclare une, sans qu'aucun test ne rougisse. Le spec du
   composant garde cette règle. */

.readings-screen {
  max-width: 72ch;
  margin: 0 auto;
  padding: var(--space-6) var(--space-4) var(--space-16);

  display: flex;
  flex-direction: column;
  gap: var(--space-12);

  color: var(--color-text);

  /* Déclarée ICI et pas laissée à l'héritage, comme le fait le sidecar de la
     roadmap : ce document se relit parfois hors du gabarit du hub (aperçu,
     capture de preuve), et il y sortait en serif — la fonte par défaut du
     navigateur. Le jeton est celui de fulgore, pas une pile de noms recopiée. */
  font-family: var(--font-family-base);
  line-height: 1.65;
}

/* ── Tête ───────────────────────────────────────────── */

.readings-screen__tete {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.readings-screen__eyebrow {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.readings-screen h1 {
  margin: 0;
  font-size: var(--font-size-4xl);
  font-weight: var(--font-weight-bold);
  line-height: 1.15;
}

.readings-screen__chapo {
  margin: 0;
  font-size: var(--font-size-lg);
  color: var(--color-text-muted);
}

.readings-screen__compteurs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.readings-screen__compteurs li {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

.readings-screen__compteurs b {
  color: var(--color-text);
  font-weight: var(--font-weight-semibold);
}

/* ── L'encart de ce qui a été écarté ─────────────────── */

.readings-screen__alerte {
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-text-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-subtle);
}

.readings-screen__alerte h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
}

.readings-screen__alerte p {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.readings-screen__alerte p:last-child {
  margin-bottom: 0;
}

/* ── Un domaine ──────────────────────────────────────── */

.readings-screen__domaine {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  /* La teinte du domaine passe par une variable locale, posée plus bas par
     l'attribut. Un seul endroit à changer pour repeindre un domaine. */
  --teinte: var(--color-text-subtle);
}

.readings-screen__domaine[data-domaine="technique"]    { --teinte: var(--color-blue-500); }
.readings-screen__domaine[data-domaine="artistique"]   { --teinte: var(--color-orange-500); }
.readings-screen__domaine[data-domaine="narratif"]     { --teinte: var(--color-indigo-500); }
.readings-screen__domaine[data-domaine="gameplay"]     { --teinte: var(--color-teal-500); }
.readings-screen__domaine[data-domaine="organisation"] { --teinte: var(--color-brown-500); }

.readings-screen__domaine-tete {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);

  padding-bottom: var(--space-2);
  border-bottom: 2px solid var(--teinte);
}

.readings-screen__domaine h2 {
  margin: 0;
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);
  line-height: 1.25;
}

/* La catégorie de carte à laquelle le domaine correspond dans le hub. Elle est
   en minuscules et en mono parce que c'est une VALEUR de la base
   (`cards.category`), pas un mot de prose — sauf pour l'organisation, qui n'a
   pas de catégorie et le dit en français. */
.readings-screen__categorie {
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);

  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  color: var(--color-neutral-0);
  background: var(--teinte);
  white-space: nowrap;
}

.readings-screen__constat,
.readings-screen__chez-nous {
  margin: 0;
}

/* « Ce que ça change chez nous » se distingue du constat : c'est la seule
   partie de la page qui parle du projet et non des sources. */
.readings-screen__chez-nous {
  padding-left: var(--space-4);
  border-left: 2px solid var(--color-border);
  color: var(--color-text-muted);
}

/* ── Les ressources d'un domaine ─────────────────────── */

.readings-screen__ressources {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

.readings-screen__ressources li {
  display: flex;
  flex-direction: column;
  gap: 2px;

  padding-left: var(--space-4);
  border-left: 2px solid var(--color-border);
}

/* Une entrée à ouvrir en premier. Le repère est la BARRE, pas une couleur de
   texte : le lien doit garder la couleur des liens du hub, sinon on perd
   l'indice le plus utile de la page — ce qui est cliquable. */
.readings-screen__ressources li[data-rang="phare"] {
  border-left-color: var(--teinte);
}

.readings-screen__ressources li[data-rang="phare"] .readings-screen__lien {
  font-weight: var(--font-weight-semibold);
}

.readings-screen__lien {
  color: var(--color-brand);
  text-decoration: underline;
  text-underline-offset: 2px;

  /* ⚠️ Un titre d'article anglais peut dépasser la largeur d'un téléphone sans
     jamais offrir de point de coupure. Sans ça, toute la page glisse
     latéralement — le défaut que la roadmap a payé sur ses tables. */
  overflow-wrap: anywhere;
}

.readings-screen__lien:hover {
  color: var(--color-brand-dark);
}

.readings-screen__lien:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.readings-screen__auteur {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

.readings-screen__auteur::before {
  content: "— ";
}

/* Le serveur de ces quatre-là refuse les robots : le lien s'ouvre dans un
   navigateur, mais personne ne l'a lu depuis la session qui a fait la veille.
   Dit à côté du lien plutôt qu'en note de bas de page : c'est une réserve sur
   la source, elle doit voyager avec elle. */
.readings-screen__robot {
  align-self: flex-start;
  margin-top: 2px;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);

  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
}

.readings-screen__quoi {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ── Les manques ─────────────────────────────────────── */

.readings-screen__manques {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  padding: var(--space-5);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
}

.readings-screen__manques h2 {
  margin: 0;
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
}

.readings-screen__liste {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
  padding-left: var(--space-5);
}

.readings-screen__liste li {
  color: var(--color-text-muted);
}

/* ── Pied ────────────────────────────────────────────── */

.readings-screen__pied {
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

.readings-screen__pied p {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* Au-delà du téléphone, la page reste à sa mesure de lecture : c'est un
   document, l'élargir ne ferait que rallonger les lignes. Seuls les marges et
   le titre respirent. */
@media (min-width: 640px) {
  .readings-screen {
    padding: var(--space-10) var(--space-6) var(--space-20);
  }

  .readings-screen h1 {
    font-size: var(--font-size-5xl);
  }
}


/* remarks/index_page.css */
.remarks-index-page {
  container-type: inline-size;

  display: grid;
  /* minmax(0, 1fr) : sans grid-template-columns, la colonne implicite `auto` se
     dimensionne au max-content des enfants (onglets plus larges) → WebKit
     déborde de quelques px. Même parade que sur les pages de catégorie. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.remarks-index-page__heading {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.remarks-index-page__panel-content {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

/* Une seule colonne jusqu'à 640px, deux au-delà. ⚠️ Le palier à trois colonnes
   des pages de catégorie n'est PAS repris : il y est mort (le contenu plafonne
   à 720px et `container-type: inline-size` mesure ce conteneur-là), et le
   recopier ici n'aurait fait qu'ajouter une règle qui ne se déclenche jamais. */
@container (min-width: 640px) {
  .remarks-index-page__panel-content {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Le badge vit à l'intérieur du <button> d'onglet rendu par fulgore : `UI::Tabs`
   injecte le label tel quel, donc un label html_safe y passe sans toucher la
   gem. Le sélecteur actif s'appuie sur la classe que la gem pose elle-même. */
.remarks-index-page__tab-badge {
  display: inline-block;
  min-width: 1.25em;
  padding: 0 var(--space-1);

  border-radius: 999px;
  background: var(--color-surface-raised);
  color: var(--color-text-subtle);

  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1.5;
  text-align: center;
}

.ui-tabs__tab--active .remarks-index-page__tab-badge {
  background: var(--color-brand);
  color: var(--color-text-inverted);
}

/* ON PASSE À LA LIGNE, ON NE FAIT JAMAIS DÉFILER — même verdict que sur les
   pages de catégorie : `UI::Tabs` met sa liste en `overflow-x: auto` avec
   `scrollbar-width: none`, et WebKit masque alors la barre, ce qui rend le
   débordement invisible ET sans affordance. Cinq onglets badgés ne tiennent pas
   sur 358px. */
.remarks-index-page .ui-tabs__list {
  flex-wrap: wrap;
  overflow-x: visible;
  row-gap: var(--space-1);
}


/* remarks/item.css */
/* Le fond dit le statut, exactement comme sur la tuile de carte. La teinte
   arrive par `--remarks-item-hue`, posée sur l'élément par
   `Remarks::Item#hue_style` depuis le réglage d'équipe (`/reglages/couleurs`).

   ⚠️ La valeur par défaut DOIT rester déclarée ici : une tuile rendue sans
   teinte (un spec, un appelant futur) reste lisible, et le hook
   `component_write_check` refuse une variable employée sans être définie dans
   une feuille. */
.remarks-item {
  --remarks-item-hue: var(--color-neutral-500);

  container-type: inline-size;

  display: grid;
  gap: var(--space-3);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: color-mix(in srgb, var(--remarks-item-hue) 7%, var(--color-surface));
  box-shadow: var(--shadow-sm);
}

.remarks-item__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* `flex: 1 1 55%` : le titre réclame au moins 55 % de la rangée, donc la
   pastille passe DESSOUS quand elle ne tient plus dans les 45 % restants, au
   lieu de comprimer le titre. Va de pair avec le `flex-wrap` du parent. */
.remarks-item__title {
  flex: 1 1 55%;
  min-width: 0;

  margin: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.remarks-item__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.remarks-item__body {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

/* Le filet sépare les cartes visées du corps de la remarque, teinté par la même
   variable que le fond — un filet neutre couperait la tuile en deux au lieu de
   l'articuler. Même parti pris que `.cards-item__updates`. */
.remarks-item__targets {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);

  padding: var(--space-3) 0 0;
  border-top: 1px solid color-mix(in srgb, var(--remarks-item-hue) 22%, transparent);

  min-width: 0;
}

/* L'intitulé, discret et en capitales : il nomme la liste sans concurrencer le
   titre de la remarque. Mêmes réglages que `.remarks-show__targets-heading`,
   pour que la tuile et la fiche se lisent pareil. */
.remarks-item__targets-label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Les cartes visées passent à la ligne plutôt que de déborder : une remarque
   peut en viser quatre, et leurs titres sont longs. */
.remarks-item__targets-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);

  margin: 0;
  padding: 0;

  list-style: none;
}

.remarks-item__target {
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}

.remarks-item__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.remarks-item__author {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

@container (min-width: 420px) {
  .remarks-item {
    padding: var(--space-5);
  }
}


/* remarks/show.css */
/* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
   posée par `layout_tag` — cette classe-ci ne porte plus que ce que la gem ne
   sait pas.

   `min-width: 0` obligatoire : ce bloc est un item de la grille fulgore
   `.ui-bases-record-view__body` (1fr 1fr), et `1fr` vaut `minmax(auto, 1fr)` —
   sans ça l'item refuse de descendre sous son min-content, que les blocs de
   code du markdown (non coupables) rendent très large. Même parade que sur la
   fiche d'une carte. */
.remarks-show__detail {
  min-width: 0;
}

.remarks-show__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.remarks-show__author {
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}

/* Même mise en colonne pour les deux sections titrées de la fiche, et groupée
   pour la même raison que leurs titres juste dessous. */
.remarks-show__targets,
.remarks-show__actions-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

/* Les deux titres de section de la fiche partagent UNE règle : les recopier en
   ferait un doublon structurel, exactement ce que `css-auditor` classe
   `duplicate`. Ils ne divergent sur rien aujourd'hui — le jour où l'un doit
   changer, on le sort de ce groupe à ce moment-là, pas en prévision. */
.remarks-show__targets-heading,
.remarks-show__actions-heading {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.remarks-show__targets-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);

  margin: 0;
  padding: 0;

  list-style: none;
}

.remarks-show__target {
  font-size: var(--font-size-sm);
  overflow-wrap: anywhere;
}

/* La typographie du corps est portée par `.markdown` (shared/markdown.css),
   posée sur le même élément : ne rien redéclarer ici, les deux classes ont la
   même spécificité et le gagnant dépendrait de l'ordre du bundle. */
.remarks-show__body {
  min-width: 0;
}

/* Le formulaire de classement : le champ de mot au-dessus, la rangée de
   boutons de statut en dessous. */
.remarks-show__actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
}

/* Les trois boutons de statut passent à la ligne plutôt que de déborder :
   « Non pertinente » et « À implémenter » ne tiennent pas sur une rangée de
   390px. */
.remarks-show__status-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}


/* roadmap/screen.css */
/* ⚠️ TOUS LES JETONS DE CE FICHIER SONT PRÉFIXÉS `--rs-`, ET C'EST LA RÈGLE.
   `components.css` est un seul fichier concaténé de 172 sidecars, et fulgore
   pose déjà 160 jetons dans `variables.css` : un `--fond` ou un `--surface`
   déclaré nu sur `:root` global repeindrait la page voisine — ou serait repeint
   par elle — selon l'ordre d'import, sans qu'aucun test ne rougisse. Aucun des
   160 ne porte ces noms AUJOURD'HUI ; le préfixe rend la collision impossible
   au lieu d'improbable. Même raisonnement que les états en `data-etat` plutôt
   qu'en classes nues. */
/* Les six jetons de départ : les quatre gris et les trois couleurs d'état. Les
   trois blocs de thème plus bas les redéclarent en dur, jamais par renvoi —
   c'est pourquoi `--rs-nuit`, `--rs-encre` et `--rs-brume-pale` ont été
   retirés le 2026-09-01 : déclarés, jamais lus. */
:root {
  --rs-brume: #6d7688;
  --rs-braise: #d1571f;
  --rs-mousse: #4f7f57;
  --rs-ocre: #b0821f;

  --rs-fond: #e8eaee;
  --rs-surface: #f6f7f9;
  --rs-surface-creuse: #dfe2e8;
  --rs-trait: #c8ccd5;
  --rs-trait-fin: #d8dbe2;
  --rs-texte: #1b202a;
  --rs-texte-doux: #545d6e;
  --rs-texte-tenu: #7a8394;

  --rs-mesure: 68ch;
  --rs-rayon: 3px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --rs-fond: #0f1218;
    --rs-surface: #161b24;
    --rs-surface-creuse: #12161e;
    --rs-trait: #2b323f;
    --rs-trait-fin: #222834;
    --rs-texte: #dfe3ea;
    --rs-texte-doux: #a3acbb;
    --rs-texte-tenu: #798394;
    --rs-braise: #ef7434;
    --rs-mousse: #6ba372;
    --rs-ocre: #d0a03c;
    --rs-brume: #7b8496;
  }
}

:root[data-theme="dark"] {
  --rs-fond: #0f1218;
  --rs-surface: #161b24;
  --rs-surface-creuse: #12161e;
  --rs-trait: #2b323f;
  --rs-trait-fin: #222834;
  --rs-texte: #dfe3ea;
  --rs-texte-doux: #a3acbb;
  --rs-texte-tenu: #798394;
  --rs-braise: #ef7434;
  --rs-mousse: #6ba372;
  --rs-ocre: #d0a03c;
  --rs-brume: #7b8496;
}

:root[data-theme="light"] {
  --rs-fond: #e8eaee;
  --rs-surface: #f6f7f9;
  --rs-surface-creuse: #dfe2e8;
  --rs-trait: #c8ccd5;
  --rs-trait-fin: #d8dbe2;
  --rs-texte: #1b202a;
  --rs-texte-doux: #545d6e;
  --rs-texte-tenu: #7a8394;
  --rs-braise: #d1571f;
  --rs-mousse: #4f7f57;
  --rs-ocre: #b0821f;
  --rs-brume: #6d7688;
}

.roadmap-screen {
  background: var(--rs-fond);
  color: var(--rs-texte);
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  -webkit-text-size-adjust: 100%;
}

.roadmap-screen {
  max-width: 980px;
  margin: 0 auto;
  padding: clamp(24px, 5vw, 64px) clamp(16px, 4vw, 40px) 96px;
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 6vw, 72px);
}

/* ── Tête ─────────────────────────────────────────── */

.roadmap-screen__tete { display: flex; flex-direction: column; gap: 14px; }

.roadmap-screen__eyebrow {
  font-family: "RF Display", ui-sans-serif, system-ui, sans-serif;
  font-size: 16px;
  letter-spacing: 0.04em;
  color: var(--rs-braise);
  margin: 0;
}

.roadmap-screen h1 {
  font-family: "RF Display", ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(32px, 7vw, 48px);
  line-height: 1.15;
  letter-spacing: 0.01em;
  text-wrap: balance;
  margin: 0;
  font-weight: 700;
}

.roadmap-screen__chapo {
  max-width: var(--rs-mesure);
  color: var(--rs-texte-doux);
  font-size: 17px;
  margin: 0;
}

.roadmap-screen__compteurs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 10px;
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 12px;
  color: var(--rs-texte-tenu);
  font-variant-numeric: tabular-nums;
}
.roadmap-screen__compteurs li {
  border: 1px solid var(--rs-trait-fin);
  border-radius: var(--rs-rayon);
  padding: 3px 8px;
  background: var(--rs-surface);
}
.roadmap-screen__compteurs li b { color: var(--rs-texte); font-weight: 500; }
.roadmap-screen__compteurs li[data-etat="zero"] { border-color: var(--rs-braise); color: var(--rs-braise); }
.roadmap-screen__compteurs li[data-etat="zero"] b { color: var(--rs-braise); }

/* ── Titres de section ────────────────────────────── */

.roadmap-screen section > h2 {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--rs-texte-tenu);
  margin: 0 0 4px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--rs-trait);
}

.roadmap-screen section > .roadmap-screen__intro {
  max-width: var(--rs-mesure);
  color: var(--rs-texte-doux);
  margin: 14px 0 0;
}

/* ── Le rail ──────────────────────────────────────── */

.roadmap-screen__rail-bloc { display: flex; flex-direction: column; gap: 18px; }

.roadmap-screen__rail {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 3px;
  position: relative;
}

.roadmap-screen__borne {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.roadmap-screen__borne .roadmap-screen__barre {
  height: 40px;
  border-radius: 2px;
  background: var(--rs-surface-creuse);
  border: 1px solid var(--rs-trait-fin);
  position: relative;
  overflow: hidden;
}
.roadmap-screen__borne .roadmap-screen__barre span {
  position: absolute;
  inset: auto 0 0 0;
  display: block;
}
.roadmap-screen__borne[data-etat="fait"] .roadmap-screen__barre span { background: var(--rs-mousse); height: 100%; }
.roadmap-screen__borne[data-etat="entame"] .roadmap-screen__barre span { background: var(--rs-ocre); height: 45%; }
.roadmap-screen__borne[data-etat="absent"] .roadmap-screen__barre span { background: var(--rs-brume); height: 8%; opacity: 0.5; }

.roadmap-screen__borne .roadmap-screen__num {
  font-family: "RF Display", ui-sans-serif, sans-serif;
  font-size: 16px;
  color: var(--rs-texte-tenu);
  font-variant-numeric: tabular-nums;
}
.roadmap-screen__borne .roadmap-screen__nom {
  font-size: 11px;
  line-height: 1.25;
  color: var(--rs-texte-doux);
  hyphens: auto;
}
.roadmap-screen__borne.roadmap-screen__borne--frontiere { position: relative; }
.roadmap-screen__borne.roadmap-screen__borne--frontiere::before {
  content: "";
  position: absolute;
  left: -3px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--rs-braise);
}

.roadmap-screen__frontiere {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 12px;
  color: var(--rs-braise);
  border-left: 2px solid var(--rs-braise);
  padding-left: 12px;
}
.roadmap-screen__frontiere > span:first-child { white-space: nowrap; }
.roadmap-screen__frontiere em { font-style: normal; color: var(--rs-texte-doux); flex: 1 1 20ch; }

/* ⚠️ LES DIX ÉTIQUETTES SE CHEVAUCHENT SOUS ~640 px, et rien ne le disait :
   la page ne défile pas latéralement pour autant, chaque nom débordant
   simplement dans la colonne voisine. Dix colonnes de 35 px ne tiennent pas un
   mot français. La barre garde alors son travail — jusqu'où on peut jouer, lu
   d'un coup d'œil — et les noms se lisent dans le tableau juste en dessous, où
   ils sont de toute façon répétés. Vu sur capture à 390 px, pas déduit. */
@media (max-width: 640px) {
  .roadmap-screen__borne .roadmap-screen__nom { display: none; }
  .roadmap-screen__borne .roadmap-screen__barre { height: 52px; }
  .roadmap-screen__rail { gap: 2px; }
}

/* ── La matrice ───────────────────────────────────── */

.roadmap-screen__cadre-table {
  overflow-x: auto;
  border: 1px solid var(--rs-trait);
  border-radius: var(--rs-rayon);
  background: var(--rs-surface);
  margin-top: 16px;
}

.roadmap-screen table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.roadmap-screen th, .roadmap-screen td {
  padding: 9px 12px;
  text-align: left;
  border-bottom: 1px solid var(--rs-trait-fin);
  vertical-align: top;
}
.roadmap-screen tbody tr:last-child td { border-bottom: 0; }

.roadmap-screen thead th {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rs-texte-tenu);
  white-space: nowrap;
  background: var(--rs-surface-creuse);
}

.roadmap-screen__matrice td.roadmap-screen__beat-nom { min-width: 15rem; }
.roadmap-screen__matrice td.roadmap-screen__beat-nom .roadmap-screen__rang {
  font-family: "RF Display", ui-sans-serif, sans-serif;
  color: var(--rs-texte-tenu);
  margin-right: 8px;
  font-variant-numeric: tabular-nums;
}
.roadmap-screen__matrice th.roadmap-screen__metier, .roadmap-screen__matrice td.roadmap-screen__etat { text-align: center; }
.roadmap-screen__matrice td.roadmap-screen__etat { width: 4.5rem; }

.roadmap-screen__pastille {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  vertical-align: -1px;
}
.roadmap-screen__pastille[data-etat="fait"] { background: var(--rs-mousse); }
.roadmap-screen__pastille[data-etat="entame"] { background: var(--rs-ocre); }
.roadmap-screen__pastille[data-etat="absent"] { background: transparent; border: 1.5px solid var(--rs-brume); }
.roadmap-screen__pastille[data-etat="neant"] { background: transparent; border: 0; width: auto; height: auto; color: var(--rs-texte-tenu); }

.roadmap-screen__legende {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 11px;
  color: var(--rs-texte-tenu);
}
.roadmap-screen__legende li { display: flex; align-items: center; gap: 7px; }

/* ── Les beats ────────────────────────────────────── */

.roadmap-screen__beats { display: flex; flex-direction: column; gap: 20px; }

.roadmap-screen__beat {
  background: var(--rs-surface);
  border: 1px solid var(--rs-trait-fin);
  border-left: 3px solid var(--rs-brume);
  border-radius: var(--rs-rayon);
  padding: 20px clamp(16px, 3vw, 26px) 22px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.roadmap-screen__beat[data-etat="fait"] { border-left-color: var(--rs-mousse); }
.roadmap-screen__beat[data-etat="entame"] { border-left-color: var(--rs-ocre); }
.roadmap-screen__beat[data-etat="absent"] { border-left-color: var(--rs-brume); }

.roadmap-screen__beat-tete {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
}
.roadmap-screen__beat-tete .roadmap-screen__rang {
  font-family: "RF Display", ui-sans-serif, sans-serif;
  font-size: 26px;
  color: var(--rs-texte-tenu);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.roadmap-screen__beat-tete h3 {
  margin: 0;
  font-size: 20px;
  line-height: 1.25;
  font-weight: 600;
  text-wrap: balance;
  flex: 1 1 12rem;
}
.roadmap-screen__jauge {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 2px;
  white-space: nowrap;
}
.roadmap-screen__jauge[data-etat="fait"] { color: var(--rs-mousse); border: 1px solid var(--rs-mousse); }
.roadmap-screen__jauge[data-etat="entame"] { color: var(--rs-ocre); border: 1px solid var(--rs-ocre); }
.roadmap-screen__jauge[data-etat="absent"] { color: var(--rs-brume); border: 1px solid var(--rs-brume); }

.roadmap-screen__beat p { margin: 0; max-width: var(--rs-mesure); }
.roadmap-screen__beat .roadmap-screen__joue { color: var(--rs-texte-doux); }
.roadmap-screen__beat .roadmap-screen__joue b { color: var(--rs-texte); font-weight: 600; }

.roadmap-screen__decision {
  border-left: 2px solid var(--rs-braise);
  padding: 2px 0 2px 14px;
  color: var(--rs-texte-doux);
  font-size: 15px;
}
.roadmap-screen__decision .roadmap-screen__quand {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rs-braise);
  display: block;
  margin-bottom: 3px;
}

.roadmap-screen__manques { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.roadmap-screen__manques li {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: 4px 14px;
  align-items: baseline;
  padding-top: 9px;
  border-top: 1px solid var(--rs-trait-fin);
}
.roadmap-screen__manques li:first-child { border-top: 0; padding-top: 0; }
.roadmap-screen__manques .roadmap-screen__tag {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--rs-texte-tenu);
  white-space: nowrap;
}
.roadmap-screen__manques .roadmap-screen__tag::before {
  content: "";
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  margin-right: 7px;
  vertical-align: 1px;
  background: var(--rs-brume);
}
.roadmap-screen__manques li[data-poids="entame"] .roadmap-screen__tag::before { background: var(--rs-ocre); }
.roadmap-screen__manques li[data-poids="absent"] .roadmap-screen__tag::before { background: transparent; border: 1.5px solid var(--rs-brume); }
.roadmap-screen__manques li[data-poids="dur"] .roadmap-screen__tag::before { background: var(--rs-braise); }
.roadmap-screen__manques li[data-poids="dur"] .roadmap-screen__tag { color: var(--rs-braise); }
.roadmap-screen__manques .roadmap-screen__quoi { color: var(--rs-texte-doux); font-size: 15px; }
.roadmap-screen__manques .roadmap-screen__quoi strong { color: var(--rs-texte); font-weight: 600; }

@media (max-width: 560px) {
  .roadmap-screen__manques li { grid-template-columns: 1fr; }
}

/* ── Divers ───────────────────────────────────────── */

.roadmap-screen code, .roadmap-screen__mono {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 0.86em;
  background: var(--rs-surface-creuse);
  padding: 1px 5px;
  border-radius: 2px;
}
.roadmap-screen__beat code, .roadmap-screen__transverse code, .roadmap-screen__bloc-questions code { background: var(--rs-surface-creuse); }

.roadmap-screen__alerte {
  border: 1px solid var(--rs-braise);
  border-radius: var(--rs-rayon);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: color-mix(in srgb, var(--rs-braise) 7%, transparent);
}
.roadmap-screen__alerte h3 {
  margin: 0;
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--rs-braise);
  font-weight: 500;
}
.roadmap-screen__alerte p { margin: 0; max-width: var(--rs-mesure); color: var(--rs-texte-doux); }
.roadmap-screen__alerte p strong { color: var(--rs-texte); }

.roadmap-screen__transverse td.roadmap-screen__chantier { font-weight: 600; min-width: 14rem; }
.roadmap-screen__transverse td.roadmap-screen__bloque { color: var(--rs-texte-doux); }
.roadmap-screen__transverse td.roadmap-screen__jauge-cell { white-space: nowrap; }
.roadmap-screen__transverse tr[data-poids="dur"] td.roadmap-screen__chantier { color: var(--rs-braise); }

.roadmap-screen ol.roadmap-screen__questions {
  margin: 16px 0 0;
  padding: 0 0 0 1.4em;
  display: flex;
  flex-direction: column;
  gap: 11px;
  max-width: var(--rs-mesure);
  color: var(--rs-texte-doux);
}
.roadmap-screen ol.roadmap-screen__questions li::marker {
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 12px;
  color: var(--rs-texte-tenu);
}
.roadmap-screen ol.roadmap-screen__questions strong { color: var(--rs-texte); font-weight: 600; }

.roadmap-screen ul.roadmap-screen__gestes {
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 11px;
  max-width: var(--rs-mesure);
  color: var(--rs-texte-doux);
}
.roadmap-screen ul.roadmap-screen__gestes li {
  padding-left: 18px;
  position: relative;
}
.roadmap-screen ul.roadmap-screen__gestes li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 7px; height: 7px;
  border: 1.5px solid var(--rs-brume);
  border-radius: 50%;
}
.roadmap-screen ul.roadmap-screen__gestes li[data-etat="urgent"]::before { background: var(--rs-braise); border-color: var(--rs-braise); }
.roadmap-screen ul.roadmap-screen__gestes strong { color: var(--rs-texte); font-weight: 600; }

.roadmap-screen footer.roadmap-screen__pied {
  border-top: 1px solid var(--rs-trait);
  padding-top: 18px;
  font-family: "RF Mono", ui-monospace, monospace;
  font-size: 11px;
  color: var(--rs-texte-tenu);
  line-height: 1.7;
}

.roadmap-screen :focus-visible { outline: 2px solid var(--rs-braise); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}


/* shared/activity_bell.css */
/* Ancre du panneau absolu : sans `position: relative` ici, `.shared-activity-bell__panel`
   se positionnerait par rapport au premier ancêtre positionné trouvé plus haut dans
   l'arbre (potentiellement la barre ou un ancêtre encore plus haut), pas par rapport à
   la cloche elle-même. */
.shared-activity-bell {
  position: relative;
}

/* Le bouton porte `.shared-icon-button` (`application.css`, section BRIQUES
   PARTAGÉES) : la boîte carrée de 32 px et la taille d'icône y vivent, partagées
   avec le menu ☰ et l'interrupteur de thème — c'est cette duplication-là que
   l'auditeur CSS re-signalait à chaque passage. Ne reste ici que ce qui est
   propre à la cloche : `position: relative`, l'ancre de la pastille positionnée
   en absolu par-dessus. Sans lui, la pastille se calerait sur le premier ancêtre
   positionné trouvé plus haut. */
.shared-activity-bell__button {
  position: relative;
}

/* Pur positionneur — n'ajoute AUCUNE des propriétés visuelles de `.ui-notification-badge`
   (couleur, forme, texte, chiffres tabulaires), qui vient de fulgore et les porte déjà ;
   la toucher ferait diverger tous les consumers de la gem. `pointer-events: none` : la
   pastille déborde SUR le bouton, un clic dessus doit atteindre le bouton en dessous, pas
   s'arrêter sur ce wrapper. Léger débordement (un quart de `--space-1` négatif de chaque
   côté) pour lire comme une pastille de notification posée sur le coin, pas un badge à
   plat dedans. */
.shared-activity-bell__badge {
  position: absolute;
  top: calc(var(--space-1) * -1);
  right: calc(var(--space-1) * -1);

  pointer-events: none;
}

/* Mobile-first : la base pose le comportement SOUS 480px (`fixed`, gouttières gauche ET
   droite), `min-width: 480px` bascule vers le comportement desktop (`absolute` ancré au
   bouton). Les deux ne peuvent pas se réduire à un seul jeu de règles enrichi — la cloche
   n'est PAS au bord de l'écran (l'interrupteur de thème est à sa droite, navbar.css:75-101)
   donc un panneau de 22rem en `absolute; right: 0` déborderait par la gauche sur un
   téléphone étroit ; en `fixed` avec deux gouttières, il ne peut plus déborder d'aucun
   côté quelle que soit la position de la cloche qui l'ouvre. C'est le point que la capture
   `visual-render` va vérifier.

   `.is-open` est posé par le contrôleur Stimulus `dropdown` de fulgore SUR LA RACINE
   (`dropdown_controller.js`, `open()` → `this.element.classList.add("is-open")`), jamais
   sur le panneau lui-même — d'où le sélecteur composé plus bas plutôt qu'un simple
   `&.is-open`.

   Pas de transition d'ouverture : `display: none` → `display: flex` ne s'anime pas (les
   transitions ne portent pas sur `display`), poser une déclaration `transition` ici serait
   du CSS mort. Une ouverture animée demanderait de repasser en `opacity`/`visibility`
   façon `.app-backdrop` (application.css:124-135) — hors scope de ce brief. */

/* Colonne flex + `overflow: hidden` (et non plus `overflow-y: auto`) : le panneau ne
   défile plus lui-même. C'est `.activities-feed__list`, plus bas dans la chaîne, qui
   porte le scroll — pour que le titre et le pied "Tout voir" restent TOUJOURS visibles,
   jamais emportés par un défilement du panneau entier. `overflow: hidden` ici n'est pas
   que la conséquence de ce déplacement : sans lui, les coins carrés du `<turbo-frame>`
   et de la `<section>` qu'il contient déborderaient visuellement des coins arrondis du
   panneau (`border-radius` ci-dessous) une fois leur hauteur plafonnée par `max-height`. */
.shared-activity-bell__panel {
  display: none;
  flex-direction: column;
  position: fixed;
  top: var(--navbar-height);
  /* ⚠ `--space-3` (12 px) ET PAS `--space-2` (8 px), et le gain n'est pas
     esthétique. Le titre du panneau s'étend sur toute sa largeur : à 8 px de
     retrait, sa BOÎTE arrivait à 10 px du bord de l'écran, sous le seuil de
     l'axe `viewportGutter` de `visual-render`, qui rougissait à chaque audit du
     panneau ouvert. Son ENCRE, elle, était à 26 px — le texte n'a jamais été
     collé, seul son cadre l'était. Mesuré le 2026-08-29, boîte contre encre.

     Corrigé quand même : un axe qu'on sait rouge pour une bonne raison finit
     par être lu comme rouge pour rien. À 12 px, la boîte passe à 14 et l'axe
     redevient vert, sans que personne ait à se souvenir de l'exception. */
  left: var(--space-3);
  right: var(--space-3);

  width: auto;
  max-height: 28rem;
  overflow: hidden;

  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);

  box-shadow: var(--shadow-xl);
  /* Doit passer au-dessus du contenu ET de la sidebar. La sidebar collante porte
     `--z-raised` (10, cf. application.css:120-123, commentaire du fond de tiroir) ; le
     contenu n'a pas de z-index posé (0 implicite). `--z-dropdown` (100) les dépasse
     largement tout en restant sous `--z-sticky`/`--z-modal`/`--z-toast` — même choix que
     fait `.ui-notification-bell__dropdown` dans la gem pour le même besoin (fulgore
     `ui/notification/bell.css`), repris ici plutôt que réinventé. */
  z-index: var(--z-dropdown);
}

.shared-activity-bell.is-open .shared-activity-bell__panel {
  display: flex;
}

@media (min-width: 480px) {
  .shared-activity-bell__panel {
    position: absolute;
    top: 100%;
    left: auto;
    right: 0;
    margin-top: var(--space-2);

    width: 22rem;
  }
}

/* `<turbo-frame>` est un élément inconnu du navigateur : sans réinitialisation, son
   `display` par défaut est `inline` — une boîte inline ne reçoit aucune hauteur et ne
   transmet aucun contexte flex à ce qu'elle contient. `display: flex` + `flex-direction:
   column` le fait entrer dans la même colonne que le panneau ; `flex: 1` le fait remplir
   la hauteur que le panneau lui laisse (plafonnée par `max-height` quand le contenu
   déborde, naturelle sinon). `min-height: 0` annule le minimum implicite (`min-height:
   auto`) que porte tout enfant flex : sans cette ligne, le `<turbo-frame>` refuserait de
   rétrécir sous la hauteur de son contenu, `max-height` n'aurait plus aucun effet
   pratique sur la chaîne, et on retrouverait le bug mesuré (panneau aussi haut que le
   contenu, pied hors champ). */
.shared-activity-bell__panel turbo-frame {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* Même chaînage un cran plus bas. `.activities-feed` (feed.css) est déjà une colonne
   flex dans TOUS ses contextes de rendu. ⚠️ Depuis le 2026-08-01, ce panneau est son
   SEUL point de rendu : le deck global qui l'affichait aussi en tête de l'accueil a
   disparu avec le tableau de bord. La feuille reste écrite pour valoir ailleurs. Les
   deux déclarations ci-dessous ne touchent donc PAS feed.css : elles s'ajoutent
   seulement ICI, sous `.shared-activity-bell__panel`, pour que la section remplisse le
   `<turbo-frame>` et puisse rétrécir sous son propre contenu — même raison que ci-dessus. */
.shared-activity-bell__panel .activities-feed {
  flex: 1;
  min-height: 0;
}

/* Le seul maillon qui absorbe vraiment le trop-plein. Titre et pied gardent leur
   `min-height: auto` implicite (donc leur hauteur de contenu réelle, jamais rognés) ;
   la liste seule reçoit `min-height: 0` et peut donc rétrécir en dessous de sa hauteur
   naturelle jusqu'à tenir dans ce qu'il lui reste — `overflow-y: auto` scrolle alors CE
   reste, jamais le panneau entier.

   Cas d'une seule activité : rien de ce qui précède ne s'active. Panel, `<turbo-frame>`
   et `.activities-feed` restent tous en hauteur `auto` — aucun n'a de hauteur fixe,
   seulement un `max-height` sur le panneau qui PLAFONNE sans jamais ÉTIRER. Le panneau
   se limite donc à la hauteur réelle du contenu, et `.activities-feed__list` ne
   déclenche ni scroll ni vide sous le pied : il n'y a tout simplement pas d'espace
   excédentaire à distribuer via `flex: 1` tant que rien au-dessus n'impose une hauteur
   définie au panneau. */
.shared-activity-bell__panel .activities-feed__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.shared-activity-bell__loading {
  margin: 0;
  padding: var(--space-6);

  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}


/* shared/attached_list.css */
/* Les rangées d'enregistrements rattachés, des DEUX côtés de la relation
   `CardSheet` : les fiches d'une carte (`Cards::Show`) et les cartes d'une
   fiche (`Sheets::Show`). Ces règles vivaient en double, à l'identique, dans
   `cards/show.css` et `sheets/show.css` — réunies ici en n116 avec le
   composant qui les porte.

   ⚠️ CE FICHIER NE STYLE NI LA SECTION NI SON TITRE. Ils restent chez chaque
   écran (`.cards-show__sheets`, `.sheets-show__cards` et leurs `-heading`),
   parce que les deux sections divergent réellement : l'une porte le
   déclencheur de rattachement, l'autre non. Ne pas les remonter ici « par
   symétrie ». */

/* Une seule colonne : chaque ligne ne porte qu'un titre et un bouton
   détacher, rien d'assez court pour justifier un palier multi-colonnes. */
.shared-attached-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  margin: 0;
  padding: 0;

  list-style: none;
}

/* Titre à gauche, bouton détacher à droite. `flex-wrap` fait passer le bouton
   à la ligne plutôt que déborder quand le titre est long à 390 px. */
.shared-attached-list__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);

  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* `min-width: 0` + `overflow-wrap: anywhere` : le lien (`UI::Links::Plain`,
   classe `.ui-link`) est un flex-item à côté du bouton de détachement — sans
   ça un titre long pousserait le bouton hors de la rangée au lieu de passer à
   la ligne avec lui. */
.shared-attached-list__row .ui-link {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* `ConfirmDestroy` pose un `<form>` NU (sans classe) dès qu'une `url:` est
   fournie (`ui/bases/button.rb#call`) — seul crochet disponible pour ce
   bouton, scopé sous la rangée, jamais global. `flex-shrink: 0` : le bouton
   ne doit jamais être celui qui cède la place face à un titre long. */
.shared-attached-list__row form {
  flex-shrink: 0;
}

/* La phrase sourde qui REMPLACE la liste quand rien n'est rattaché — jamais
   rendue en même temps qu'elle. */
.shared-attached-list__empty {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}


/* shared/brand.css */
/* Le lien est rendu par `UI::Links::Plain` — donc une ancre, donc un élément
   EN LIGNE. Sans ce `display: flex`, elle ajoute sous la planche la descente
   de sa ligne de base et décale ce qui l'entoure. `flex-shrink: 0` : la
   marque ne cède jamais la sienne, c'est à la piste de grille qui l'entoure
   de le faire (`navbar.css`). */
.shared-brand {
  display: flex;
  flex-shrink: 0;
}

/* 40 px, et cette hauteur est celle de TOUS les rendus depuis que la marque a
   quitté la colonne de gauche (2026-08-03) : la barre est son seul lieu, sa
   hauteur est POSÉE à 4 rem et non déduite du logo, et un logo à 64 px la
   ferait déborder verticalement. `navbar.css` ne pose donc plus d'exception en
   face — si une seconde place devait un jour accueillir la marque, c'est là
   qu'une taille propre se réécrirait, pas ici.

   Le rapport 3/4 du fichier tient l'image à 30 px de large à cette hauteur.
   L'image porte la signature complète du studio, lettrage compris, et non une
   pastille : c'est ce qui interdit de la réduire davantage. */
.shared-brand__logo {
  display: block;
  flex-shrink: 0;

  width: auto;
  height: 40px;
}


/* shared/markdown.css */
/* Styling partagé du markdown rendu par MarkdownRenderer (classe .markdown).
   Pas de composant Ruby associé : c'est une feuille de contenu généré,
   consommée par Cards::Show. Sélecteurs descendants assumés — le HTML est
   produit par Commonmarker, on ne peut pas y poser de classes BEM.
   Le glob de css:bundle balaie app/components/**, ce fichier y est donc inclus. */

.markdown {
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--font-size-base);
  line-height: var(--line-height-normal);
  color: var(--color-text-subtle);
}

.markdown > :first-child { margin-top: 0; }
.markdown > :last-child { margin-bottom: 0; }

.markdown p {
  margin: var(--space-3) 0;
}

.markdown strong {
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4 {
  margin: var(--space-4) 0 var(--space-2);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

.markdown h1 { font-size: var(--font-size-lg); }
.markdown h2 { font-size: var(--font-size-base); }
.markdown h3,
.markdown h4 { font-size: var(--font-size-sm); }

.markdown ul,
.markdown ol {
  margin: var(--space-3) 0;
  padding-left: var(--space-5);
}

.markdown li {
  margin: var(--space-1) 0;
}

.markdown li > p {
  margin: 0;
}

.markdown a {
  color: var(--color-brand);
  text-decoration: underline;
}

/* Le lien mort — un renvoi vers une carte du canon qui n'existe plus. Posé par
   `Cards::Show#mark_dead_link` : c'est la SEULE classe de ce fichier qui a une
   prise, tous les autres sélecteurs sont descendants faute de pouvoir accrocher
   quoi que ce soit sur la sortie de Commonmarker.

   ⚠️ (0,2,0) contre (0,1,1) pour `.markdown a` juste au-dessus : elle gagne, et
   c'est pour ça qu'elle n'a pas à répéter la couleur de lien.

   ⚠️⚠️ `underline line-through` et JAMAIS `line-through` seul. La forme courte
   REMPLACE `text-decoration-line` : le soulignement de `.markdown a` sautait, et
   le renvoi mort devenait le seul du corps de texte à ne pas ressembler à un
   lien — alors que la doctrine est qu'il en RESTE un (il mène à un 404 qui
   confirme). Le lien rouge de Wikipédia garde le sien.

   ⚠️ Et le hub est une application web MOBILE : `title` ne s'affiche jamais au
   doigt, `cursor: help` n'existe pas. Sur l'écran principal il ne reste que la
   couleur et le trait — d'où l'importance de ne pas laisser le trait manger le
   soulignement au passage. */
.markdown .markdown__dead-link {
  color: var(--color-danger);
  text-decoration: underline line-through;
  cursor: help;
}

.markdown code {
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background-color: var(--color-surface-raised);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

/* Le bloc de code porte les prompts IA : il doit se sélectionner d'un geste,
   et défiler dans sa propre boîte plutôt que d'élargir la page. */
.markdown pre {
  max-width: 100%;
  margin: var(--space-3) 0;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface-subtle);
  overflow-x: auto;
}

.markdown pre code {
  padding: 0;
  background-color: transparent;
  white-space: pre;
}

/* Idem : une table large défile dans sa boîte, jamais au détriment de la page. */
.markdown table {
  display: block;
  max-width: 100%;
  margin: var(--space-3) 0;
  overflow-x: auto;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}

/* `overflow-wrap: normal` ANNULE ici le `anywhere` de `.markdown` : dans une
   colonne étroite, celui-ci coupe les mots caractère par caractère et rend la
   table illisible sur mobile. En le neutralisant, chaque cellule impose au moins
   la largeur de son plus long mot — la table dépasse alors sa boîte et défile,
   ce qui est le comportement voulu. */
.markdown th,
.markdown td {
  min-width: 6rem;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border);
  overflow-wrap: normal;
  text-align: left;
  vertical-align: top;
}

.markdown th {
  background-color: var(--color-surface-subtle);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
  color: var(--color-text);
}

.markdown blockquote {
  margin: var(--space-3) 0;
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border-strong);
  color: var(--color-text-muted);
}

.markdown hr {
  margin: var(--space-4) 0;
  border: 0;
  border-top: 1px solid var(--color-border);
}


/* shared/navbar.css */
/* Même politique que pour les barres de filtres du deck : jamais de défilement
   horizontal, une largeur qui manque se rattrape en cédant de la place plutôt
   qu'en débordant. Ce qui a changé, c'est COMMENT elle est tenue. La barre
   n'était qu'une rangée de commandes de largeur fixe, où un ajout pouvait un
   jour dépasser la place disponible — d'où le repli en `flex-wrap` et son
   filet. Elle est désormais une grille à trois colonnes (marque | recherche |
   actions) dont seule la colonne du milieu peut rétrécir ; les deux colonnes
   latérales sont bornées par leur propre contenu (une image, un groupe de
   boîtes carrées), pas par ce qu'il y a en face. Rien ne peut plus déborder
   par accumulation, il n'y a plus de rangée à faire déborder — `flex-wrap`
   disparaît donc, il n'a plus de recto sur lequel s'exercer : la recherche
   cède sa largeur avant que quoi que ce soit d'autre n'ait besoin de se
   replier, et le libellé du bouton « Nouvelle carte » s'efface (cf.
   `.visually-hidden-sm`, `application.css`) sous 640 px — AVANT que la place
   ne manque, jamais après. Le filet de n21 n'est pas perdu, il est remplacé
   par un budget de largeur qui ne peut structurellement pas déborder plutôt
   que par une clause de repli. */

/* GRILLE et non rangée : une rangée flex centre le GROUPE d'enfants restant
   une fois les tailles fixes retirées, pas un enfant précis — le milieu ne
   tomberait au centre de la barre que si marque et actions pesaient
   exactement le même poids, ce qui n'est vrai qu'à partir de 1024 px (la
   marque est masquée au-delà, cf. plus bas). Une grille à trois colonnes dont
   les deux colonnes latérales sont TOUTES DEUX `minmax(0, 1fr)` les rend
   égales par construction : la colonne du milieu tombe au centre de la barre
   quoi qu'il y ait à gauche et à droite, sans dépendre de ce qu'elles
   contiennent.

   ⚠️ LES TROIS pistes sont des pistes `fr`, et les deux latérales ont pour
   plancher leur propre contenu (`minmax(max-content, 1fr)`). La première
   écriture donnait `minmax(0, 26rem)` au milieu : un plafond FIXE n'est PAS
   une piste `fr`, la grille le sert AVANT le partage — la piste du milieu
   prenait ses 416 px pleins et laissait aux actions ce qui restait. Mesuré à
   640 px le 2026-08-03 : piste d'actions de 88 px pour ~350 px de boutons. Le
   plafond de 26 rem a donc déménagé sur le FORMULAIRE lui-même (`max-width`
   plus bas), où il borne une largeur sans confisquer une piste ; les actions
   ne peuvent plus être comprimées, leur plancher étant leur `max-content`.

   Le prix, assumé : quand la barre est trop étroite pour que les deux pistes
   latérales soient égales (les actions dépassent leur part), le milieu cesse
   d'être exactement au centre et glisse vers la gauche. C'est de la physique,
   pas un défaut — à cette largeur, centrer exigerait de comprimer les actions.
   Centrage exact à partir d'environ 1100 px, à moins de 20 px près à 1024. */
/* ⚠️ Les trois `grid-column` ci-dessous ne sont PAS décoratifs, et surtout pas
   redondants avec l'ordre du DOM. Ils ont été posés le 2026-08-03, quand la
   marque passait encore en `display: none` au-delà de 1024 px — or un élément
   de grille en `display: none` est RETIRÉ DE LA GRILLE, pas seulement de
   l'écran : le placement automatique refaisait alors ses comptes et faisait
   glisser la recherche dans la 1ʳᵉ colonne et les actions dans celle du milieu.
   Résultat mesuré ce jour-là : champ de recherche collé à gauche, bloc
   d'actions au centre — exactement l'inverse de la demande, et INVISIBLE à
   l'audit de débordement (rien ne dépasse, tout est simplement à la mauvaise
   place).

   Ce `display: none` est parti le soir même (la marque vit désormais dans la
   barre à TOUTES les largeurs, cf. plus bas), donc le piège ne se déclenche
   plus aujourd'hui. Les placements restent : ils rendent la disposition
   indépendante de qui est affiché, et le jour où un enfant de cette barre se
   masquera à son tour, rien ne bougera. */
.shared-navbar {
  display: grid;
  grid-template-columns: minmax(max-content, 1fr) minmax(0, 1fr) minmax(max-content, 1fr);
  align-items: center;
  gap: var(--space-2);

  min-height: 4rem;              /* hauteur POSÉE, cf. § hauteur plus bas */
  padding-inline: var(--space-4);
  box-sizing: border-box;

  position: relative;            /* ancre le ::after du biseau */
  background: var(--color-surface-subtle);
}

/* Sous 640 px le centrage est une prétention : le bloc d'actions pèse 152 px
   (4 boîtes de 32 px + 3 gaps de --space-2) et la marque 40 — des colonnes
   latérales forcées égales feraient déborder les actions à droite avant même
   que la marque n'ait fini d'occuper sa place à gauche. On retombe donc en
   rangée simple, où le champ de recherche prend ce qui reste
   (`.shared-search-field { flex: 1 }`, `search_field.css`). */
@media (max-width: 639px) {
  .shared-navbar {
    display: flex;
  }
}

/* La marque vit dans la barre à TOUTES les largeurs, et nulle part ailleurs
   (choix JC 2026-08-03). Elle coiffait jusque-là la colonne de gauche
   au-dessus de 1024 px, et se masquait ici — laissant en desktop un rectangle
   de 288 × 64 px vide en haut à gauche, avec sous lui un logo collé au coin de
   la colonne, plus à gauche que les icônes des liens. Le coin haut-gauche de
   la barre est la place que ce logo cherchait.

   `min-width: 0` la laisse céder sa place à la colonne de recherche plutôt que
   forcer la grille à s'élargir sur son contenu. */
.shared-navbar__brand {
  grid-column: 1;

  display: flex;
  align-items: center;
  min-width: 0;
}

/* Le champ tient la piste du milieu. Le plafond de 26 rem est posé ICI et non
   sur la piste (cf. le ⚠️ en tête) : une piste plafonnée est servie avant le
   partage et affame ses voisines, un formulaire plafonné ne prend que ce dont
   il a besoin. `margin-inline: auto` le recentre dans sa piste quand celle-ci
   est plus large que 26 rem — sans quoi il se collerait à gauche et la barre
   paraîtrait décentrée alors que la grille serait juste. */
.shared-navbar .shared-search-field {
  grid-column: 2;

  width: 100%;
  max-width: 26rem;
  margin-inline: auto;
}

/* La hauteur du logo n'est plus posée ici : la barre étant son seul lieu de
   rendu, les 40 px sont devenus le défaut du composant lui-même
   (`brand.css`) — il n'y a plus d'exception à écrire en face d'une règle. */

/* Colle le bouton « Nouvelle carte » et le `<nav>` des trois icônes contre le
   bord droit de leur colonne de grille. `.shared-navbar__nav` n'a plus qu'à
   aligner ses propres enfants entre eux : le calage à droite est déjà fait
   par son parent. */
.shared-navbar__actions {
  grid-column: 3;

  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  min-width: 0;
}

.shared-navbar__nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Le texte n'est PAS masqué ici : c'est `.visually-hidden-sm`
   (`application.css`, section BRIQUES PARTAGÉES) qui le retire à l'œil sous
   640 px tout en le gardant dans l'arbre d'accessibilité. Le masquer pour de
   vrai avec `display: none` donnerait au bouton le nom accessible « + » — un
   glyphe, pas un mot — au lieu de « Nouvelle carte ». */
.shared-navbar__new-card .ui-button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  white-space: nowrap;
}

.shared-navbar__new-card-plus {
  font-size: var(--font-size-lg);
  line-height: 1;
}

/* Sous 640 px le bouton n'a plus qu'un « + » à afficher : le padding
   horizontal de `.ui-button--sm` est calibré pour un libellé, il laisserait
   un glyphe seul flotter dans une boîte trop large. */
@media (max-width: 639px) {
  .shared-navbar__new-card .ui-button {
    padding-inline: var(--space-3);
  }
}

/* ============================================================
   LE RELIEF — options 02 + 04 retenues par JC sur maquette.

   04 « même matière » : le fond de la barre est celui de la colonne
   (`--color-surface-subtle`, posé plus haut) — barre et colonne forment
   désormais un seul L de chrome, et c'est la zone de contenu qui devient le
   creux. Il n'y a donc plus de raccord à dessiner entre les deux : sur toute
   la largeur de la colonne, RIEN ne doit être posé — y tracer un trait
   rouvrirait exactement la coupure que 04 vient de supprimer.

   02 « biseau gravé » : un trait qui creuse (`--navbar-groove`) surmonté
   d'une lèvre éclairée (`--navbar-lip`), mais SEULEMENT le long du contenu —
   d'où `inset-inline-start: var(--sidebar-width)` à partir de 1024 px, le
   seuil où la colonne existe et où le biseau doit s'arrêter à son bord. */
.shared-navbar::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 2px;
  background: linear-gradient(180deg,
    var(--navbar-groove) 0 1px,
    var(--navbar-lip)    1px 2px);
  pointer-events: none;
}

@media (min-width: 1024px) {
  .shared-navbar::after {
    inset-inline-start: var(--sidebar-width);
  }
}

/* Les deux couleurs du biseau s'INVERSENT selon le thème — c'est le cœur du
   choix, pas un détail : sur un fond quasi noir une ombre noire n'existe pas
   (`application.css`, section MODE SOMBRE — « une ombre noire à 10 % n'existe
   plus sur du noir »). C'est la LUMIÈRE qui creuse en sombre, l'ombre qui
   creuse en clair. Même motif à quatre blocs que la bascule d'icône de thème
   plus bas dans ce fichier, avec le même garde-fou `:not([data-theme="light"])`
   — s'y reporter pour le raisonnement complet sur pourquoi CE garde-fou, et
   non l'ordre des règles, est ce qui tranche le cas système-sombre +
   choix-clair. */
.shared-navbar {
  --navbar-groove: var(--color-border);
  --navbar-lip: rgb(255 255 255 / 0.9);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shared-navbar {
    --navbar-groove: rgb(0 0 0 / 0.6);
    --navbar-lip: rgb(255 255 255 / 0.06);
  }
}

:root[data-theme="dark"] .shared-navbar {
  --navbar-groove: rgb(0 0 0 / 0.6);
  --navbar-lip: rgb(255 255 255 / 0.06);
}

:root[data-theme="light"] .shared-navbar {
  --navbar-groove: var(--color-border);
  --navbar-lip: rgb(255 255 255 / 0.9);
}

/* Les boutons du menu et du thème portent `.shared-icon-button`
   (`application.css`, section BRIQUES PARTAGÉES) : la boîte carrée de 32 px,
   le fond transparent, l'atténuation au survol, l'anneau de focus et la taille
   d'icône y vivent désormais, partagés avec le bouton de la cloche
   (`shared/activity_bell.css`) qui les recopiait. Ne reste ici que ce qui est
   propre au thème — la bascule d'icône ci-dessous.

   `fill: currentColor` vient de la règle globale `svg` d'`application.css` :
   pas de redite, ni ici ni dans la brique. */

/* Bascule d'icône en CSS pur : le contrôleur Stimulus ne fait que
   poser/retirer `data-theme` sur <html>, jamais toucher au SVG. On
   montre l'action DISPONIBLE plutôt que l'état courant — lune =
   « passer au sombre », soleil = « passer au clair ».

   Chaque règle qui touche au cas système-sombre porte
   :not([data-theme="light"]) — comme dans le bloc mode sombre de
   application.css. C'est ce garde-fou, et non l'ordre des règles,
   qui règle le piège : un système sombre avec un choix explicite
   « clair » sort du match de la règle système AVANT même de se poser
   la question de spécificité, donc seule la règle [data-theme="light"]
   ci-dessous s'applique et redonne la lune. */

/* Défaut (clair, aucun data-theme) : lune visible, soleil masqué. */
.shared-navbar__theme-icon--sun {
  display: none;
}

/* Système sombre, aucun choix explicite : soleil visible, lune masquée. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .shared-navbar__theme-icon--moon {
    display: none;
  }

  :root:not([data-theme="light"]) .shared-navbar__theme-icon--sun {
    display: block;
  }
}

/* Choix explicite « sombre » : soleil visible, lune masquée. */
:root[data-theme="dark"] .shared-navbar__theme-icon--moon {
  display: none;
}

:root[data-theme="dark"] .shared-navbar__theme-icon--sun {
  display: block;
}

/* Choix explicite « clair » — y compris sur système sombre : lune
   visible, soleil masqué. */
:root[data-theme="light"] .shared-navbar__theme-icon--moon {
  display: block;
}

:root[data-theme="light"] .shared-navbar__theme-icon--sun {
  display: none;
}


/* shared/picker.css */
/* La charpente commune aux deux sélecteurs — `Cards::Picker` et
   `Sheets::Picker`. Feuille SANS composant Ruby, comme `shared/markdown.css` :
   le glob de `css:bundle` balaie `app/components` en récursif et prend toute
   feuille qu'il y trouve, il n'y a pas de classe à écrire pour qu'elle soit
   servie.

   ⚠️⚠️ NE JAMAIS ÉCRIRE UN MOTIF DE GLOB EN TOUTES LETTRES DANS UN COMMENTAIRE
   CSS. La ligne ci-dessus disait d'abord le motif littéral, deux étoiles, une
   barre, une étoile, un point, css — et sa fin, étoile-barre, FERME le
   commentaire au milieu de la phrase. Tout le reste du texte devient alors un
   sélecteur, le parseur avale jusqu'à la première accolade, et la règle
   `.picker` juste dessous disparaît AVEC lui. Le fichier reste parfaitement
   lisible, aucun outil ne bronche, rubocop ne voit pas le CSS : le panneau du
   sélecteur s'ancrait simplement sur la fenêtre au lieu de son champ, et
   c'est une mesure au navigateur qui l'a dit. Payé le 2026-08-23 (n100).

   Elle est née le 2026-08-23 (n100) d'une mesure : sur les 9 règles de
   `sheets/picker.css`, 8 étaient la copie EXACTE de leur jumelle dans
   `cards/picker.css` — seul l'ancrage du panneau différait, d'un mot. Deux
   copies dérivent en silence, et la seconde n'a personne pour la mesurer :
   c'est déjà arrivé ici, une troncature écrite sans `display: block` est
   restée inerte des semaines dans `sheets/` parce que le défaut n'avait été
   mesuré que dans `cards/`.

   Montage : chaque élément porte DEUX classes, la commune puis la sienne
   (`class="picker__panel cards-picker__panel"`), exactement comme
   `UI::Bases::RecordView#body_slot` écrit `ui-bases-record-view__body
   <ns>__body`. La commune porte ce qui est partagé, la locale ce qui sépare.

   ⚠️⚠️ ORDRE DE CONCATÉNATION — le glob est TRIÉ, donc `shared/picker.css`
   passe APRÈS `cards/picker.css` et `sheets/picker.css` dans le bundle. À
   spécificité égale, c'est donc CETTE feuille qui gagnerait. Aucune propriété
   déclarée ici ne doit l'être dans les deux sidecars : ils ne gardent que
   `right`/`left` sur le panneau, absents d'ici. Ajouter une propriété ici qui
   existe déjà là-bas la retournerait EN SILENCE. */

.picker {
  position: relative;
  display: inline-block;
}

/* ⚠️ Cette classe n'est posée QUE si l'appelant ne fournit pas son propre
   habillage : `Cards::Picker#trigger_button` rend `@trigger_class ||
   "picker__trigger …"`, et la barre markdown passe le sien. Poser
   `picker__trigger` en dur à côté rendrait ce style INCONTOURNABLE, et la
   barre markdown gagnerait une bordure qu'elle n'a jamais eue.
   `Sheets::Picker` n'offre aucun remplacement : ce style y est définitif. */
.picker__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  background: var(--color-surface);

  cursor: pointer;
  transition:
    background var(--duration-fast) var(--easing-default),
    border-color var(--duration-fast) var(--easing-default);
}

.picker__trigger:hover {
  background: var(--color-surface-subtle);
  border-color: var(--color-border-strong);
}

.picker__trigger:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Panneau flottant : masqué par défaut, affiché par `.is-open` que
   `dropdown_controller` (fulgore) pose SUR LA RACINE, jamais sur le panneau —
   d'où le sélecteur composé plus bas plutôt qu'un `&.is-open`.

   ⚠️ AUCUN `left` ni `right` ici : c'est la seule chose qui sépare les deux
   sélecteurs, et elle dépend du contexte de chacun (voir leur sidecar).

   `width` bornée par `min()` : protège le bord OPPOSÉ à l'ancrage, quel qu'il
   soit — le panneau tient dans la fenêtre même si son déclencheur se retrouve
   près d'un bord sur un petit écran. Une détection de collision en JS ferait
   mieux, hors du scope d'un sidecar. `box-sizing: border-box` : sans lui,
   bordure et remplissage s'ajouteraient par-dessus cette largeur bornée et la
   dépasseraient de quelques pixels — exactement ce que le `min()` empêche. */
.picker__panel {
  display: none;
  flex-direction: column;
  position: absolute;
  top: calc(100% + var(--space-1));
  z-index: var(--z-dropdown);
  box-sizing: border-box;

  width: min(18rem, calc(100vw - var(--space-8)));
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);

  background: var(--color-surface);
  box-shadow: var(--shadow-md);
}

.picker.is-open .picker__panel {
  display: flex;
}

/* La liste ne pousse jamais la page, elle défile sur elle-même. `14rem` ≈ 6
   lignes visibles — une hauteur de défilement, pas un espacement, d'où le rem
   plutôt qu'un `--space-*` (même traitement que `shared-activity-bell__panel`,
   `max-height: 28rem`).

   ⚠️⚠️ Piège volontairement évité, et il vaut pour les DEUX sélecteurs : un
   `<li>` est `display: list-item`, et l'attribut `hidden` que
   `card-picker_controller#filter` pose sur les options hors recherche s'appuie
   sur la règle UA `[hidden] { display: none }`. Toute règle `display:` posée
   sur une option — même un `display: block` anodin pour retirer la puce —
   appartient à l'origine « auteur » et écraserait cette règle UA, quelle que
   soit la spécificité : le `<li>` resterait visible malgré `hidden`, et le
   filtre semblerait ne rien faire. On ne pose donc AUCUN `display` sur une
   option, nulle part. La puce part une seule fois, ici, par `list-style`, qui
   ne touche pas `display`. Si un besoin futur impose un `display` sur une
   option, ajouter dans la même règle `…__option[hidden] { display: none }`
   pour rouvrir la trappe proprement. */
.picker__list {
  max-height: 14rem;
  overflow-y: auto;

  margin: 0;
  padding: 0;
  list-style: none;
}

.picker__empty {
  margin: 0;
  padding: var(--space-4) var(--space-2);

  font-size: var(--font-size-sm);
  text-align: center;
  color: var(--color-text-subtle);
}

/* Une option qui POSTE son choix est un `<form>` nu rendu par
   `UI::Bases::Button` dès qu'une `url:` lui est fournie (`ui/bases/button.rb`),
   contenant un `<button class="ui-button …">` — pas un bouton écrit à la main.
   Les deux sélecteurs le font, et il faut déshabiller ce bouton de son look
   « pilule centrée » pour le lire comme une ligne de menu : pleine largeur,
   aligné à gauche, une seule ligne, tronqué aux points de suspension.

   Rien à faire sur le `<form>` : mesuré au rendu, il est déjà `display: block`
   (défaut UA) et s'étire à la pleine largeur du `<li>`. Seul le bouton posait
   un problème de largeur.

   ⚠️ `display: block` n'est PAS cosmétique : `.ui-button` est `inline-flex`
   (button.css), donc sa largeur se cale sur son CONTENU, et surtout
   `text-overflow: ellipsis` n'a AUCUN effet sur un conteneur flex. Sans lui, la
   troncature écrite juste dessous est INERTE et un titre long se fait couper
   net par le bord du panneau — mesuré à 292px de contenu pour 270px
   disponibles. Le défaut a vécu des semaines dans `sheets/picker.css`
   précisément parce que la règle y était recopiée, et que seule sa jumelle
   avait été mesurée. C'est cette histoire-là qui a fait naître ce fichier.

   ⚠️ Le retrait était le SEUL écart entre les deux copies : 12px côté carte,
   choisi pour aligner les options sur le champ de recherche juste au-dessus,
   contre les 20px par défaut de `.ui-button` côté fiche — qui n'alignaient
   rien, ce sélecteur n'ayant pas de champ. Aligné sur 12px, tranché par JC le
   2026-08-23 après mesure.

   ⚠️ Pas de `box-sizing: border-box` à ajouter : un `<button>` natif le porte
   déjà dans la feuille du navigateur (vérifié au rendu).

   ⚠️ Si une option gagne un jour une `icon:`, cette règle casse le `gap` que le
   flex de la base assure entre icône et libellé — ils retombent en flux inline
   normal, sans casse visuelle mais sans l'espacement voulu. À revoir à ce
   moment-là : aucun appelant ne passe d'icône aujourd'hui.

   ⚠️⚠️ Et RIEN ici ne pose de `display` sur `.picker__option` lui-même — voir
   le piège de l'attribut `hidden` documenté plus haut, sur la liste. */
.picker__option .ui-button {
  display: block;
  width: 100%;
  overflow: hidden;

  padding: var(--space-2) var(--space-3);

  text-align: left;
  text-overflow: ellipsis;
  /* `white-space: nowrap` est déjà porté par `.ui-button` (button.css) — la
     troncature n'a besoin que de la propriété manquante ci-dessus. */
}


/* shared/search_field.css */
/* Le champ hérite son apparence de `ui/bases/filter_bar.css` dans la gem
   (`.filter-field--search`, `.filter-search`, `.filter-search-clear`).
   ⚠️ PAS de `ui/inputs/search.css` : le composant Ruby est bien
   `UI::Inputs::Search`, mais il n'a pas de sidecar — son habillage vit chez la
   barre de filtres, d'où il a été détaché. Ce chemin a été écrit faux ici
   pendant des mois ; il envoie chercher dans un fichier qui n'existe pas. Ce
   sidecar ne règle que ce que la barre impose : le champ occupe une piste de
   grille bornée (`.shared-navbar`, `navbar.css`) au lieu d'un espace fixe, et
   doit pouvoir rétrécir sous sa largeur intrinsèque plutôt que la déborder. */
.shared-search-field {
  display: block;
  width: 100%;

  min-width: 0;
}

/* Sous 640 px la barre repasse en rangée simple (`navbar.css`) : le champ
   n'a plus de piste de grille pour le borner, il doit prendre ce qui reste
   une fois la marque et les actions posées. */
@media (max-width: 639px) {
  .shared-search-field {
    flex: 1;
  }
}

.shared-search-field .filter-field--search {
  width: 100%;
}

/* ⚠️ LE CONTENEUR ET LA LARGEUR DE L'INPUT SONT PARTIS DANS LA GEM le
   2026-08-03 (fulgore `dfde2b0`). Ce fichier a porté du 3 août au 3 août un
   `display: flex; width: 100%` sur `.filter-search-wrapper` et un
   `width: 100%` sur `.filter-search` : `filter_bar.css` posait `inline-flex`
   sur le conteneur, dont la largeur était donc « indéfinie » au sens de CSS,
   et l'input ne pouvait alors RIEN remplir. Le même défaut se rejouait dans
   les quatre autres consumers, d'où la correction en amont.
   ⚠️ NE PAS LES REPOSER ICI en croyant compléter la gem : elle fait
   maintenant `display: flex` sur le conteneur et `flex: 1` sur l'input.
   Un `width: 100%` rajouté par-dessus obligerait à redéclarer
   `box-sizing: border-box`, qui ferait alors compter les 46 px de padding
   dans le `min-width: 16rem` de la gem — mesuré : 46 px de champ perdus. */

/* ⚠️ CELLE-CI RESTE, et elle n'a rien à voir avec ce qui précède. La gem pose
   `min-width: 16rem` sur `.filter-search` ; sans cet écrasement WebKit refuse
   de rétrécir l'input sous son `min-content`, ce qui ferait déborder la barre
   à 390 px. Le piège grid/flex documenté dans la skill visual-render. */
.shared-search-field .filter-search {
  min-width: 0;
}


/* shared/sidebar.css */
/* Le sidecar ne fait que TROIS choses : coller le pied en bas, recaler la
   colonne sur une barre qui défile, et neutraliser le mode compact de la gem.
   Toute la disposition, le tiroir mobile et la colonne collante viennent de
   `ui/shell/sidebar.css` via la classe `ui-shell-sidebar` — rien de tout ça
   n'est redit ici. */

.shared-sidebar {
  display: flex;
  flex-direction: column;
}

/* ⚠️ La gem cale sa sidebar sur `top: var(--navbar-height)`, ce qui suppose
   une barre FIXE — celle d'un back-office, toujours là. La nôtre défile avec
   la page (choix JC, 2026-08-01 : une barre fixe mangerait 80 px d'écran en
   permanence sur téléphone, et notre logo en fait 64 à lui seul).

   Conséquence MESURÉE, pas supposée : à 2000 px de défilement, la barre est
   sortie de l'écran depuis longtemps mais la colonne reste collée 80 px plus
   bas — un bandeau vide en haut, dont le fond et la bordure démarrent au
   milieu de l'écran. On la recolle donc à 0, et sa hauteur suit.

   Le prix : en HAUT de page, la colonne part sous la barre et dépasse de
   80 px sous la fenêtre. Invisible tant qu'elle tient cinq entrées — elle
   porte déjà `overflow-y: auto` si un jour elle en porte trente. */
@media (min-width: 1024px) {
  .ui-shell-sidebar.shared-sidebar {
    top: 0;
    height: 100dvh;
  }
}

/* Même correction pour le tiroir sous 1024 px : calé sur `--navbar-height`, il
   laissait la même bande vide dès que la barre avait défilé. À `top: 0` il
   couvre l'écran entier — ce qu'on attend d'un tiroir — et le `max-width:
   80vw` de la gem garde une lisière de fond à taper pour le refermer. */
@media (max-width: 1023px) {
  .ui-shell-sidebar.shared-sidebar {
    top: 0;
  }
}

/* Plus rien ne coiffe la colonne : elle démarre sur son premier lien. La
   marque y avait pris la place du bouton « Nouvelle carte » monté dans la
   barre, et héritait de l'écart qu'il posait ; elle a suivi le même chemin le
   2026-08-03 (`navbar.css`). L'écart au-dessus du premier lien est désormais
   la seule marge interne de la colonne (`padding: var(--space-6)`, posé par la
   gem) — il n'y a plus de règle à écrire ici pour lui. */

/* `margin-top: auto` pousse la déconnexion en bas de la colonne, quel que soit
   le nombre de liens au-dessus. */
.shared-sidebar__footer {
  margin-top: auto;
  padding-top: var(--space-4);
}

/* Le hub n'a PAS de mode compact : il ne pose aucun bouton
   `sidebar-expand-toggle`, donc `nav-sidebar#toggleWidth` n'est jamais appelé.
   Mais `is-compact` peut quand même atterrir sur un écran large par un chemin
   détourné : en dessous de 1024 px, `nav_sidebar_controller#toggleVisibility`
   (nav_sidebar_controller.js:30) pose `compact` et non `expanded` pour ouvrir
   le tiroir ; si la fenêtre est ensuite élargie, `#onResize` ne ramène l'état
   sauvé que depuis `hidden`, jamais depuis `compact` — la classe reste. La
   sidebar tomberait alors à 4.5rem de large, libellés masqués, avec un bouton
   « Nouvelle carte » et une déconnexion qui portent du texte : débordement
   assuré. On rend donc `is-compact` visuellement identique à `is-expanded`.

   Spécificité : `.ui-shell-sidebar.shared-sidebar.is-compact` pèse (0,3,0)
   contre (0,2,0) pour les règles de la gem — c'est ce qui les écrase. Ne pas
   retirer `.shared-sidebar` du sélecteur en croyant simplifier. */
@media (min-width: 1024px) {
  .ui-shell-sidebar.shared-sidebar.is-compact {
    width: var(--sidebar-width);
    padding: var(--space-6) var(--space-4);
  }

  .ui-shell-sidebar.shared-sidebar.is-compact .sidebar-link__label {
    display: inline;
  }

  .ui-shell-sidebar.shared-sidebar.is-compact .ui-shell-sidebar__link {
    justify-content: flex-start;
    padding: var(--space-3) var(--space-4);
  }
}


/* sheets/index_page.css */
/* La page `/fiches` : titre + bouton de création, puis la liste des tuiles.
   Calquée sur `Remarks::IndexPage` (index_page.rb), moins les onglets de
   statut — une `Sheet` n'en porte aucun (`sheet.rb`), il n'y a donc qu'UN
   panneau à mettre en page, jamais plusieurs à faire cohabiter dans une
   grille `UI::Tabs`. D'où la racine en simple pile verticale (`flex column`)
   plutôt qu'en `grid` comme `.remarks-index-page` : ce `grid-template-columns:
   minmax(0, 1fr)` de la page jumelle parait une colonne implicite trop large
   à cause des onglets, un problème qui n'existe pas ici faute d'onglets. */

.sheets-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.sheets-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.sheets-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une seule colonne jusqu'à 640px, deux au-delà — même palier que
   `.remarks-index-page__panel-content`, pour que les deux listes de
   /remarques et /fiches se lisent pareil au même point de rupture.
   `minmax(0, 1fr)` des deux côtés : sans lui la colonne implicite se
   dimensionne au max-content d'une tuile (un résumé de comptage n'est pas
   coupable en soi, mais un libellé de chiffre imprévu pourrait l'être) et
   déborde sous le seuil au lieu de rester dans son 1fr. */
.sheets-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .sheets-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* sheets/item.css */
/* La tuile d'une fiche : titre + résumé de comptage, calquée sur
   `Remarks::Item` (item.rb) mais SANS pastille de statut ni teinte de fond —
   une `Sheet` ne porte ni statut ni hue (`sheet.rb`, `Sheets::Item` ne définit
   ni `hue_style` ni `header_meta`). D'où une carte bien plus nue : pas de
   `--remarks-item-hue`, pas de `color-mix`, pas de `__header`/`__meta`.

   Racine `<article>` posée par le composant lui-même
   (`skip_encapsulation? true`) : tout l'habillage (bordure, fond, padding)
   vit ici, sur la classe racine — jamais sur un enfant, sinon la zone
   visuelle de la tuile ne correspondrait plus à sa zone cliquable/lisible.

   `flex column`, PAS `grid` comme `.cards-item`/`.remarks-item` — divergence
   volontaire, pas un oubli d'alignement (relevé par `css-auditor`, D3) : ces
   deux tuiles-là empilent 3+ blocs (en-tête + pastilles, corps, cibles,
   pied), `grid` leur sert à réserver des lignes propres à chacun. Celle-ci
   n'a que DEUX enfants directs (`__title`, `__summary`, item.rb) — un `flex`
   fait le même travail avec un rouage de moins. `gap: var(--space-2)` plutôt
   que `--space-3` pour la même raison : moins d'air nécessaire entre 2 blocs
   qu'entre 3+. */

.sheets-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.sheets-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}

.sheets-item__summary {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
  overflow-wrap: anywhere;
}


/* sheets/picker.css */
/* Sheets::Picker — le symétrique de `Cards::Picker` côté fiches (picker.rb).
   La charpente (ancre, déclencheur, panneau, liste, message vide, et la
   surcharge du bouton fulgore posée sur les options) vit dans
   `shared/picker.css` depuis n100 : elle était recopiée ici à l'identique.
   Il ne reste QU'UNE règle propre à ce sélecteur — l'ancrage de son panneau.

   ⚠️ Ne rien redéclarer de ce que porte `shared/picker.css` : le bundle
   concatène par ordre alphabétique, donc `shared/` passe APRÈS ce fichier et
   gagnerait à spécificité égale — la redéclaration serait retournée en
   silence. */

/* `left: 0`, pas `right: 0` comme `Cards::Picker`. Le choix de celui-ci est
   motivé par SON contexte : dernier bouton d'une barre d'outils qui hugue le
   bord droit d'un champ de 720px (markdown_area.rb). Le seul appelant RÉEL de
   `Sheets::Picker` (`cards/show.rb:184`) le monte SEUL, en fin de section
   `.cards-show__sheets` — aucune barre d'outils, aucun bord droit à défendre.
   Un panneau ancré à gauche part du bord gauche du déclencheur, dans le sens
   naturel de lecture. Le garde-fou `width: min(...)` de `shared/picker.css`
   reste identique : il protège le bord opposé (droit, ici). */
.sheets-picker__panel {
  left: 0;
}


/* sheets/show.css */
/* La fiche d'une fiche de jeu, calquée sur `Remarks::Show` (show.rb) — même
   base `UI::Bases::Show`, même absence de `list_component`/`footer_slot`
   (`Sheets::Show` ne les surcharge pas : pas de fil de discussion, pas de
   pagination précédent/suivant), donc pas de `.sheets-show__list` ni
   `.sheets-show__footer` à styler ici, contrairement à `Remarks::Show` — ces
   classes n'existent tout simplement pas dans le DOM de ce composant.

   `.sheets-show__detail` est le SEUL enfant de `.ui-bases-record-view__body`
   (`Sheets::Show` ne surcharge pas `context_component`) : la règle fulgore
   `:only-child { max-width: 48rem; margin-inline: auto; width: 100% }`
   (record_view.css) s'applique donc automatiquement — pas besoin de
   redéclarer une largeur ici. */

.sheets-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas, et que sa classe seule ne saurait pas accueillir. */
  container-type: inline-size;

  /* Même parade que `.remarks-show__detail` et `.cards-show__detail` : ce
     bloc est un item de la grille fulgore `.ui-bases-record-view__body`
     (1fr 1fr, `1fr` vaut `minmax(auto, 1fr)`) — sans `min-width: 0` l'item
     refuse de descendre sous son min-content. Ici le contenu n'a pas de
     markdown (une `Sheet` n'a pas de corps), mais un libellé de chiffre non
     coupable (`sheet_stat.label`, texte libre) pourrait rejouer le même
     débordement — coût nul à s'en prémunir. */
  min-width: 0;
}

/* Les quatre sections de la fiche (chiffres, ajout, cartes, rattachement)
   partagent la même colonne titre + contenu. */
.sheets-show__stats,
.sheets-show__add-stat,
.sheets-show__cards,
.sheets-show__attach-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  min-width: 0;
}

/* REVIREMENT 2026-08-06 : les deux règles qui vivaient ici (`align-self:
   flex-start` sur `.cards-picker`, `right: auto; left: 0` sur
   `.cards-picker__panel`) sont PARTIES — dead code, pas un oubli.

   Elles corrigeaient le mode À BOUTON : une racine `inline-block` sans
   largeur propre (étirée par `align-items: stretch`, le défaut de cette
   colonne flex) et un panneau ancré `right: 0` (pensé pour la barre
   markdown, où le picker est un bouton en fin de ligne). `attach_card_
   picker` (show.rb) est passé en mode `inline: true` ce même jour —
   `Sheets::Show` ne rend plus JAMAIS le mode à bouton — et le composant
   porte désormais SA PROPRE largeur en mode inline
   (`.cards-picker--inline { width: 100%; max-width: 26rem }`,
   cards/picker.css) plus son propre panneau calé à gauche sur la largeur du
   champ (`.cards-picker--inline .cards-picker__panel { left: 0; width:
   100% }`, même fichier). Conséquence pour CHAQUE règle :
   - une racine à largeur EXPLICITE (`width: 100%`, pas `auto`) n'est plus
     concernée par `align-self`/`align-items` — cette propriété ne pilote
     que la taille par défaut d'un axe `auto`, jamais une taille déjà posée ;
   - le panneau n'a plus besoin d'aucun ancrage venu d'ici, le composant le
     calcule lui-même contre SA propre racine.

   Vérifié en injectant les CSS réels dans la page rendue (WebKit 390 et
   1280, panneau ouvert ET fermé) : `align-self: stretch` forcé par-dessus
   le `flex-start` d'origine, PUIS les deux règles retirées entièrement —
   largeur et position de la racine et du panneau strictement identiques
   dans les trois cas (390 : 358px de large ; 1280 : 416px = 26rem
   plafonné). Rien à garder ici. */

/* Les quatre titres de section partagent UNE règle, comme
   `.remarks-show__targets-heading, .remarks-show__actions-heading` — les
   recopier quatre fois serait le doublon structurel que `css-auditor`
   classe `duplicate`. */
.sheets-show__stats-heading,
.sheets-show__add-stat-heading,
.sheets-show__cards-heading,
.sheets-show__attach-card-heading {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Les paragraphes d'état partagent aussi UNE règle.

   ⚠️ `__add-stat-complete` n'est PAS un état vide, malgré la compagnie : il dit
   l'inverse — les sept libellés du type sont tous posés, il n'y a plus rien à
   ajouter. Il rejoint ce groupe parce qu'il joue le même rôle à l'écran (une
   phrase sourde là où un bloc devrait être), pas parce qu'il dit la même
   chose.

   ⚠️ `__cards-empty` A QUITTÉ CE GROUPE en n116 : le message vide des cartes
   rattachées est rendu par `Shared::AttachedList` et se style dans son
   sidecar. Les déclarations y sont les mêmes — c'est une reprise, pas une
   divergence — mais elles ne bougeront plus ensemble. */
.sheets-show__stats-empty,
.sheets-show__add-stat-complete {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* ============================================================
   LA LISTE DE CHIFFRES — une ligne par chiffre, du 390px au 1280px.
   ============================================================
   Chaque `<li>` porte DEUX formulaires frères (impossible de les fusionner :
   un `<form>` ne peut pas en contenir un autre) — le formulaire d'édition
   (`.sheets-show__stat-form` : libellé + valeur + bouton « Enregistrer ») et
   le formulaire de suppression posé nu par `UI::Buttons::ConfirmDestroy`
   (`ui/bases/button.rb#call` — un `<form>` SANS classe dès qu'une `url:` est
   passée). D'où le sélecteur `form` non préfixé plus bas : c'est le seul
   crochet disponible sur ce second formulaire, et il reste scopé sous
   `.sheets-show__stat`, jamais global.

   ⚠️ REVIREMENT 2026-08-06 (tranché par JC ce matin). Le paragraphe qui
   vivait ici disait « quatre éléments interactifs sur une seule rangée n'y
   tiennent pas, quelle que soit la largeur ». C'était vrai TANT QUE le Ruby
   rendait deux boutons texte (« Enregistrer »/« Supprimer ») et deux champs
   avec leur étiquette visible — cinq fiches pleines empilaient alors les
   formulaires sur 5 838px pour CINQ chiffres, mesuré en WebKit à 390px. Le
   Ruby vient de compacter les deux boutons en icônes (`stat_save_button`/
   `stat_delete_button`, `icon_only: true`) ; ce fichier compacte le reste :
   étiquettes masquées à l'œil (section dédiée plus bas) et champ valeur
   ramené à une largeur de chiffre plutôt qu'une largeur de champ générique.
   Calcul à la main sur les dimensions réelles à 390px de viewport (moins le
   padding de page, moins `--space-3` de part et d'autre du `<li>`, moins
   les deux boutons icône ~30px chacun, moins le champ valeur figé à 64px,
   moins les `gap`) : ~178px restent au champ libellé — largement assez pour
   un libellé d'une dizaine de caractères. Plus besoin de `@container` pour
   CETTE rangée : elle tient à la largeur la plus contrainte du projet, pas
   seulement au-delà d'un palier. */

.sheets-show__stats-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
  padding: 0;

  list-style: none;
}

.sheets-show__stat {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);

  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* Le formulaire de suppression (le `form` nu, sans classe) ne doit jamais
   céder sa place au formulaire d'édition voisin — même parade que
   `.shared-attached-list__row form` (`shared/attached_list.css`, où elle a
   déménagé en n116 depuis ce fichier). `:not(.sheets-show__
   stat-form)` est nécessaire : sans lui, le sélecteur `form` matcherait
   AUSSI `.sheets-show__stat-form` (qui EST elle-même un `<form>`) et lui
   imposerait `flex-shrink: 0`, entrant en conflit avec le `flex: 1` voulu
   juste en dessous — c'est cette rangée-ci qui doit pouvoir céder de la
   place à la marge (elle porte le champ libellé, seul dont la largeur peut
   varier), jamais le bouton Supprimer. */
.sheets-show__stat > form:not(.sheets-show__stat-form) {
  flex-shrink: 0;
}

/* Le formulaire d'ajout garde son layout d'origine : empilé en mobile, en
   rangée au-delà de 480px de CONTENEUR (cf. `@container` plus bas).
   `.sheets-show__stat-form` en sort : il prend sa propre rangée
   inconditionnelle juste après, ce besoin-ci ne lui étant plus commun.

   ⚠️ `.sheets-show__attach-card-form` partageait ces deux règles et a été
   RETIRÉ le 2026-08-06 : le rattachement n'est plus un formulaire à champs
   mais `Cards::Picker`, qui porte son propre CSS (`cards/picker.css`). Le
   sélecteur n'a plus aucun nœud à styler. */
.sheets-show__add-stat-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ⚠️ RANGÉE INCONDITIONNELLE (pas de `@container`) : libellé + valeur +
   Enregistrer doivent tenir à 390px comme à 1280px, c'est la demande de ce
   matin — un palier de conteneur serait revenu à empiler en dessous de lui,
   exactement ce qu'on vient de corriger. `flex: 1` : ce formulaire partage
   la rangée de `.sheets-show__stat` avec le formulaire de suppression
   voisin et doit lui laisser sa largeur de bouton au lieu de l'écraser.
   `min-width: 0` : même piège documenté plus haut sur `.sheets-show__
   detail` — sans lui un flex-item refuse de descendre sous son min-content. */
.sheets-show__stat-form {
  display: flex;
  align-items: flex-end;
  flex: 1;
  gap: var(--space-3);

  min-width: 0;
}

/* Par défaut les deux champs `.ui-input` de `.sheets-show__stat-form`
   (libellé, valeur) se partagent la place à parts égales — la règle
   suivante retire ensuite le champ valeur de ce partage pour lui donner une
   largeur fixe.
   ⚠️ PIÈGE n23 relu avant d'écrire ceci : son défaut n°2 donnait un plafond
   fixe (`minmax(0, 26rem)`) à la piste qui DEVAIT rester flexible, servi
   AVANT le partage — la piste censée grandir restait figée à son plafond et
   affamait ses voisines. Ici c'est l'inverse : la piste flexible (le
   libellé) porte bien `flex: 1 1 0`, SANS plafond ; seule la piste qui DOIT
   rester étroite (la valeur, un chiffre de jeu) reçoit une largeur fixe, et
   seulement elle — cf. règle suivante, qui la retire du partage plutôt que
   de la contraindre en amont. `min-width: 0` : un `.ui-input` ne rétrécit
   pas sous la largeur de son `<input>` interne (`width: 100%`, `ui/bases/
   input.css`) sans lui. */
.sheets-show__stat-form .ui-input {
  flex: 1 1 0;
  min-width: 0;
}

/* Le champ valeur reçoit sa largeur fixe : `:has(input[type="number"])` le
   cible sans dépendre de l'ordre DOM (`stat_label_field` avant
   `stat_value_field` dans `show.rb`) — un futur réordonnancement du Ruby ne
   casserait pas ce sélecteur. Cette règle gagne sur la précédente par
   spécificité seule (le `:has()` ajoute un cran), pas par ordre de cascade
   — aucun risque de « servi avant le partage » façon Grid ici, on est en
   flexbox et les deux règles s'appliquent au même élément sans ambiguïté
   d'ordre. `var(--space-16)` (4rem/64px) : aucun token de largeur n'existe
   dans la gem (vérifié dans `fulgore/app/assets/stylesheets/variables.css`
   — seule l'échelle `--space-*` existe), 64px couvre le padding horizontal
   de `.ui-input input` (`--space-4` de chaque côté, 32px) plus un nombre à
   deux chiffres, avec une marge confortable. */
.sheets-show__stat-form .ui-input:has(input[type="number"]) {
  flex: 0 0 var(--space-16);
}

/* Le bouton « Enregistrer » (icône seule, `stat_save_button`) ne doit
   jamais rétrécir sous sa taille de contenu : un flex-item accepte
   `flex-shrink: 1` par défaut, ce qui l'écraserait avant le champ libellé
   voisin (lui `min-width: 0`, donc candidat naturel à la compression) sans
   cette exemption explicite. */
.sheets-show__stat-form .ui-button {
  flex-shrink: 0;
}

@container (min-width: 480px) {
  .sheets-show__add-stat-form {
    flex-direction: row;
    align-items: flex-end;
  }

  /* Les champs `.ui-input` grandissent à parts égales, le bouton
     `UI::Buttons::Submit` garde sa largeur de contenu (inline-flex, non
     concerné par ce sélecteur). `min-width: 0` : sans lui, un flex-item
     `.ui-input` refuse de descendre sous la largeur de son `<input>` interne
     (`width: 100%` dans `ui/bases/input.css`), empêchant les deux champs de
     partager la rangée.

     ⚠️ Le `:not(.ui-input--hidden)` a été retiré en même temps que le
     formulaire de rattachement (2026-08-06) : il n'excluait que le champ
     caché `sheet_id` de CE formulaire-là, et le formulaire d'ajout n'a aucun
     champ caché. */
  .sheets-show__add-stat-form .ui-input {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* ============================================================
   ÉTIQUETTES « Libellé »/« Valeur » — masquées à l'ŒIL, pas à l'oreille.
   ============================================================
   ⚠️ DUPLICATION ASSUMÉE, pas un oubli. `UI::Bases::Input#label_element`
   (fulgore) n'a AUCUNE option de masquage accessible : `label: nil`/
   `label: false` retirent le `<label>` de L'ARBRE D'ACCESSIBILITÉ entier
   (`return unless label`, rien d'autre — lu dans le source avant d'écrire
   ceci), pas seulement de l'écran. La classe canonique qui porte ce
   masquage existe déjà : `.ui-visually-hidden` (`fulgore/a11y.css`, déjà
   importée par `application.css` de ce hub) — mais on ne peut PAS la POSER
   depuis ici : c'est le Ruby qui ajoute des classes à l'élément rendu par
   la gem, et `UI::Bases::Input` n'a aucun point d'entrée pour ça (ni
   `label_class:`, ni rien d'approchant). La VRAIE correction est une
   option `visually_hidden_label:` côté gem — à proposer côté fulgore, hors
   périmètre de cette mission (CSS sidecar seul). En attendant, ce sidecar
   recopie la recette EXACTE de `.ui-visually-hidden` (mêmes déclarations,
   y compris la marge négative qui rattrape le pixel encore occupé dans le
   flux — dérive déjà relevée une fois dans `application.css` pour
   `.visually-hidden-sm`, pas la peine de la rejouer ici) sous un sélecteur
   scopé à CETTE rangée seulement.

   ⚠️ NE JAMAIS `display: none` NI `visibility: hidden` : les deux
   retirent l'élément de l'arbre d'accessibilité EN MÊME TEMPS qu'ils le
   retirent de l'écran — l'exact inverse du but recherché, un lecteur
   d'écran n'annoncerait plus « Libellé »/« Valeur » du tout.

   Scope strict à `.sheets-show__stat-form label` : `.sheets-show__add-
   stat-form` (la saisie vierge en bas, hors mission) GARDE ses étiquettes
   visibles — une ligne d'ajout vide bénéficie de voir « Libellé »/
   « Valeur » écrits, contrairement à une ligne déjà remplie où le contenu
   du champ (« agilité », « 6 ») porte lui-même l'information. */
.sheets-show__stat-form label {
  position: absolute;

  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;

  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ============================================================
   LES CARTES RATTACHÉES
   ============================================================
   ⚠️ PLUS AUCUNE RÈGLE ICI. Les quatre blocs qui vivaient là
   (`__cards-list`, `__card`, son `.ui-link` et son `form`) avaient un jumeau
   identique au caractère près dans `cards/show.css` — l'autre bout de la même
   relation `CardSheet`. Ils sont partis en n116 dans
   `shared/attached_list.css`, avec le composant qui les rend
   (`Shared::AttachedList`).

   La divergence assumée avec `.remarks-show__targets-list` reste vraie et a
   déménagé avec eux : là-bas chaque cible n'est qu'un lien (pas d'action),
   donc des pills qui passent à la ligne suffisent ; ici chaque carte porte un
   bouton de détachement, et une liste de RANGÉES bordées se lit mieux qu'un
   nuage de pills dont l'un porterait un bouton et pas les autres.

   Ce qui reste dans ce fichier, c'est la SECTION et son titre
   (`.sheets-show__cards`, `.sheets-show__cards-heading`, plus haut). */

/* ============================================================
   ÉDITER / SUPPRIMER LA FICHE
   ============================================================ */
.sheets-show__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}


/* sprite_imports/deposit.css */
/* Ligne d'état du découpage d'un .aseprite. L'élément n'est pas rendu par le
   composant : c'est `aseprite_convert_controller` qui le pose en tête du
   formulaire quand une conversion démarre, et le retire en quittant la page.
   Son style vit quand même ici, avec le composant qui porte le contrôleur —
   c'est le seul endroit où on pense à le relire.

   Le vocabulaire d'état de fulgore n'expose pas de `--color-info-text` ni de
   `--color-danger-text` : le texte se prend sur la nuance foncée de la famille
   (`--color-blue-700`, `--color-danger-dark`), posée sur la surface claire. */
.aseprite-convert__statut {
  margin: 0 0 var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-info-surface);
  color: var(--color-blue-700);
  font-size: var(--font-size-sm);
}

/* Un échec de découpage dit quoi faire ensuite (réexporter depuis Aseprite) :
   il doit se distinguer d'un simple avancement, sinon il se lit comme une
   étape en cours et l'envoi paraît figé sans raison. */
.aseprite-convert__statut[data-erreur="true"] {
  background: var(--color-danger-surface);
  color: var(--color-danger-dark);
}


/* sprite_imports/unfold.css */
.sprite-imports-unfold {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-5);

  width: 100%;
  box-sizing: border-box;
}

.sprite-imports-unfold__heading {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

.sprite-imports-unfold__skipped {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-warning-surface);
  color: var(--color-warning-text);
  font-size: var(--font-size-sm);
}

.sprite-imports-unfold__section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.sprite-imports-unfold__section-heading {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

.sprite-imports-unfold__rows {
  display: flex;
  flex-direction: column;
}

.sprite-imports-unfold__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.sprite-imports-unfold__row:last-child {
  border-bottom: none;
}

.sprite-imports-unfold__row--error {
  background: var(--color-danger-a10);
  border-radius: var(--radius-md);
  border-bottom-color: transparent;
}

.sprite-imports-unfold__row-errors {
  padding: 0 var(--space-3) var(--space-3);
  background: var(--color-danger-a10);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}

.sprite-imports-unfold__row-errors ul {
  margin: 0;
  padding-left: var(--space-4);
}

.sprite-imports-unfold__row-errors li {
  font-size: var(--font-size-xs);
  color: var(--color-danger);
}

/* Le champ caché `key` ne doit réserver ni gap ni piste de grille : c'est un
   `<input type="hidden">`, invisible par nature, mais son wrapper `.ui-input`
   (display: grid) resterait sinon un enfant flex à part entière. */
.sprite-imports-unfold__row .ui-input--hidden {
  display: none;
}

.sprite-imports-unfold__row-label {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1);
  /* Base 100% : le libellé occupe systématiquement sa propre ligne tant que
     le container reste étroit. Sans ça, la place qu'il laisse derrière lui
     dépend de la longueur réelle du texte — un des deux `__meta` s'y glisse
     tantôt oui tantôt non, et se retrouve désaligné une ligne sur deux. */
  flex: 1 1 100%;
  min-width: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  cursor: pointer;
}

.sprite-imports-unfold__state {
  font-weight: var(--font-weight-semibold);
}

.sprite-imports-unfold__animation-name {
  overflow-wrap: anywhere;
  color: var(--color-text-soft);
}

.sprite-imports-unfold__direction {
  color: var(--color-text-subtle);
}

.sprite-imports-unfold__meta {
  flex: 0 0 auto;
  font-size: var(--font-size-xs);
  color: var(--color-text-subtle);
  white-space: nowrap;
}

.sprite-imports-unfold__field {
  /* Base généreuse pour le champ Action (le geste principal de l'écran) :
     grandit avec l'espace disponible, `min-width: 0` pour rester libre de
     rétrécir sous WebKit (un item flex refuse sinon de descendre sous le
     min-content de son `<input>`). */
  flex: 1 1 10rem;
  min-width: 0;
}

.sprite-imports-unfold__field .ui-input label {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

.sprite-imports-unfold__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

/* Le bouton (dernier enfant) devient la cible pleine largeur sur écran
   étroit ; le lien retour garde sa taille naturelle au-dessus. */
.sprite-imports-unfold__actions > :last-child {
  width: 100%;
}

@container (min-width: 480px) {
  .sprite-imports-unfold__actions {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }

  .sprite-imports-unfold__actions > :last-child {
    width: auto;
  }
}

@container (min-width: 640px) {
  .sprite-imports-unfold__row-label {
    flex-basis: 18rem;
  }
}


/* sprites/backup_crop_fields.css */
/* Le repli des six marques de secours du formulaire de planche.

   ⚠ LA MARGE DU HAUT EST À NOUS, et ce n'est pas cosmétique. `.ui-form` espace
   ses champs par `.ui-input + .ui-input` (`fulgore`, `ui/bases/form.css`) : un
   sélecteur de VOISIN. Le `<details>` n'est pas un `.ui-input`, il ne reçoit
   donc rien de la cadence qui le précède — sans cette marge, le repli collerait
   au champ d'au-dessus.

   ⚠ ET LA MARGE DU BAS DU RÉSUMÉ AUSSI, pour la même raison en miroir : le
   premier champ à l'intérieur suit un `<summary>`, pas un `.ui-input`, donc la
   règle de voisinage ne le voit pas non plus. Les cinq suivants, eux, sont bien
   voisins entre eux et gardent l'espacement de fulgore — la règle imbriquée
   compile en sélecteur DESCENDANT, elle traverse le `<details>`. */
.sprites-backup-crop-fields {
  margin-top: var(--space-6);
}

/* `cursor: pointer` parce que le `<summary>` est un bouton et doit s'annoncer
   comme tel — le navigateur ne le fait pas de lui-même. Le triangle de
   déploiement, lui, est natif : on ne le remplace pas, il porte déjà l'état
   ouvert/fermé sans qu'on ait à le dire. */
.sprites-backup-crop-fields__summary {
  margin-bottom: var(--space-6);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);

  cursor: pointer;
}


/* sprites/gallery.css */
.sprites-gallery {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  width: 100%;
  box-sizing: border-box;
}

.sprites-gallery__heading {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
}

.sprites-gallery__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .sprites-gallery__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* sprites/item.css */
.sprites-item {
  container-type: inline-size;

  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

.sprites-item__sheet {
  overflow-x: auto;
  box-sizing: border-box;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  background: var(--color-surface-subtle);
}

.sprites-item__sheet-img {
  display: block;
  max-width: none;
  height: auto;
  image-rendering: pixelated;
}

.sprites-item__metadata {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
}

.sprites-item__action {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: var(--color-text);
  text-transform: capitalize;
}

.sprites-item__meta {
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-subtle);
}

.sprites-item__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

/* La scène que le jeu joue avec cette planche, et son lieu entre parenthèses.

   ⚠ Elle vivait dans l'infobulle de la pastille, et il n'y a pas de survol sur
   un téléphone — or le hub est mobile-first et se juge sur iPhone. Le bloc
   n'apparaît que sur les planches jouées, une poignée sur plus de cent : les
   autres vignettes ne grandissent pas.

   ⚠ Même taille et même teinte que `__meta` : c'est une métadonnée de plus, pas
   un titre. Lui donner du poids ferait de la marque du jeu la chose la plus
   voyante de la vignette, devant le dessin. */
.sprites-item__scene {
  margin: 0;

  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-subtle);
}

/* La marque du jeu, juste après le statut.

   ⚠ Son enveloppe ne porte plus SEULEMENT l'infobulle qui nomme la scène
   (`UI::Pill` ne prend pas de `title`) : la scène est maintenant écrite en
   clair au-dessus. L'enveloppe reste pour l'alignement ci-dessous, et
   l'infobulle avec elle — elle ne coûte rien et sert le bureau.

   ⚠ `display: inline-flex` n'est pas décoratif : un `span` en flux poserait la
   pastille sur la ligne de base du texte, 1 px plus bas que celle du statut à
   côté. Deux pastilles côte à côte qui ne s'alignent pas se voient. */
.sprites-item__game {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

/* Le logo du fichier de travail. `margin-inline-start: auto` le colle au menu
   plutôt qu'au statut : le pied répartit ses deux extrêmes, une troisième
   pièce au milieu flotterait sans point d'ancrage.

   ⚠ `image-rendering: pixelated` n'est pas cosmétique — le logo est du pixel
   art, et le lissage par défaut du navigateur le rendrait flou à cette
   taille. */
.sprites-item__source {
  width: 1rem;
  height: 1rem;
  margin-inline-start: auto;
  image-rendering: pixelated;
  flex: 0 0 auto;
}

@container (min-width: 420px) {
  .sprites-item {
    padding: var(--space-5);
  }
}


/* title_cards/index_page.css */
/* La page `/cartons` : titre + bouton de création, puis la liste des tuiles.
   Calquée sur `.items-index-page` (`items/index_page.css`) — même structure à
   deux blocs, même absence d'onglets (un `TitleCard` ne porte aucun statut,
   `title_card.rb`). */

.title-cards-index-page {
  container-type: inline-size;

  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.title-cards-index-page__heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.title-cards-index-page__title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* Une seule colonne jusqu'à 640px, deux au-delà — même palier que
   `.items-index-page__list`, `.characters-index-page__list` et
   `.places-index-page__list`, pour que les listes du hub se lisent pareil au
   même point de rupture. `minmax(0, 1fr)` des deux côtés : sans lui la colonne
   implicite se dimensionne au max-content d'une tuile et déborde sous le
   seuil. */
.title-cards-index-page__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3);
}

@container (min-width: 640px) {
  .title-cards-index-page__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}


/* title_cards/item.css */
/* La tuile d'un carton : l'acte au-dessus, le titre en dessous. Racine
   `<article>` posée par le composant lui-même (`skip_encapsulation? true`) :
   tout l'habillage vit ici, sur la classe racine — même motif que
   `.items-item` et `.characters-item`.

   ⚠️ L'ACTE EST AU-DESSUS, à la différence de la méta des autres tuiles du
   hub, qui vient toujours en pied. Ce n'est pas de la méta : c'est la moitié
   haute du carton tel que le joueur le lira. La tuile respecte donc l'ordre de
   sortie — le raisonnement est écrit dans `title_cards/item.rb`. */

.title-cards-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);

  width: 100%;
  box-sizing: border-box;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  background: var(--color-surface);
  box-shadow: var(--shadow-sm);
}

/* Petites capitales espacées : c'est ce qui distingue l'acte du titre sans un
   second niveau de titraille. Même traitement que
   `.items-show__property-label`, dont c'est exactement le rôle — dire « ceci
   qualifie ce qui suit ». */
.title-cards-item__act {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  overflow-wrap: anywhere;
}

/* `gap: var(--space-1)` sur la racine et non `--space-2` comme `.items-item` :
   les deux lignes appartiennent au même carton et doivent se lire d'un bloc,
   là où le titre et la méta d'un objet sont deux informations distinctes. */
.title-cards-item__title {
  margin: 0;

  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  overflow-wrap: anywhere;
}


/* title_cards/show.css */
/* La fiche d'un carton, calquée sur `Items::Show` (`items/show.css`) — même
   base `UI::Bases::Show`, même absence de `list_component` et de
   `footer_slot` (`TitleCards::Show` ne les surcharge pas), donc pas de
   `.title-cards-show__list` ni `__footer` à styler : ces classes n'existent
   pas dans le DOM de ce composant.

   `.title-cards-show__detail` est le SEUL enfant de
   `.ui-bases-record-view__body` (pas de `context_component` surchargé) : la
   règle fulgore `:only-child { max-width: 48rem; margin-inline: auto }`
   (record_view.css) s'applique donc automatiquement — rien à redéclarer. */

.title-cards-show__detail {
  /* La colonne verticale vient de `.ui-bases-record-view__layout` (fulgore),
     posée par `layout_tag` — restent ici les deux règles que la gem ne porte
     pas. */
  container-type: inline-size;

  /* Même parade que `.items-show__detail` : ce bloc est un item de la grille
     fulgore `.ui-bases-record-view__body` (`1fr` = `minmax(auto, 1fr)`) — sans
     `min-width: 0` il refuse de descendre sous son min-content. Un titre
     d'acte sans espace est exactement le genre de contenu qui rejouerait le
     débordement. */
  min-width: 0;
}

.title-cards-show__preview,
.title-cards-show__slug,
.title-cards-show__actions {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* ============================================================
   CE QUE LE JOUEUR VERRA
   ============================================================
   L'aperçu — l'acte, un trait, le titre — sur un fond sombre et centré, pour
   qu'on le lise comme un écran et non comme deux champs d'un formulaire.

   ⚠️ CE N'EST PAS UNE MAQUETTE FIDÈLE, et il ne faut pas la lire comme telle :
   l'écriture du jeu n'est pas servie par le hub, et la vue du jeu fait
   480 × 270. Ce bloc montre l'ORDRE et le TRAIT — le raisonnement est écrit
   dans `title_cards/show.rb`. D'où le choix de NE PAS figer une largeur en
   pixels : elle suggérerait une fidélité que ce bloc n'a pas. */

.title-cards-show__preview {
  align-items: center;
  gap: var(--space-3);

  box-sizing: border-box;
  padding: var(--space-6) var(--space-4);
  border-radius: var(--radius-lg);

  /* Le noir du jeu au moment du carton — le voile est opaque quand le texte
     s'écrit (`game/src/voile.rs`). Une couleur en dur et non un jeton de
     thème : ce bloc représente un écran de jeu, pas une surface du hub, et il
     doit rester noir même quand le hub passe en thème clair. */
  background: #0b0b0d;

  text-align: center;
}

.title-cards-show__preview-act {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: #cfc6ae;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  overflow-wrap: anywhere;
}

/* Le trait que le jeu TIRE entre les deux temps. Court et centré : sur toute
   la largeur il ferait une séparation de section, pas un ornement de carton. */
.title-cards-show__preview-rule {
  width: 6rem;
  margin: 0;
  border: 0;
  border-top: 1px solid #6b6350;
}

.title-cards-show__preview-title {
  margin: 0;

  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-snug);
  color: #f7e8c7;
  overflow-wrap: anywhere;
}

/* ============================================================
   LE NOM QUE LE JEU CITE
   ============================================================ */

.title-cards-show__slug {
  gap: var(--space-1);
}

.title-cards-show__slug-label {
  margin: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* En chasse fixe : c'est une chaîne à recopier telle quelle dans du Rust, et
   la chasse fixe est ce qui distingue un `l` d'un `1` à la lecture.
   `user-select: all` pour qu'un seul geste la sélectionne en entier — elle se
   recopie plus souvent qu'elle ne se lit. */
.title-cards-show__slug-value {
  margin: 0;

  font-family: var(--font-family-mono);
  font-size: var(--font-size-base);
  color: var(--color-text);
  overflow-wrap: anywhere;
  user-select: all;
}

.title-cards-show__slug-hint {
  margin: 0;

  font-size: var(--font-size-sm);
  color: var(--color-text-subtle);
}

/* ============================================================
   ÉDITER / SUPPRIMER LE CARTON
   ============================================================ */
.title-cards-show__actions {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--space-3);
}
