:root {
    --bs-body-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --bs-body-bg: #f7f7fb;
    --bs-body-color: #262a35;
    --bs-primary: #4f56d3;
    --bs-primary-rgb: 79, 86, 211;
    --bs-link-color: #4f56d3;
    --bs-link-color-rgb: 79, 86, 211;
    --bs-link-hover-color: #3c42a8;
    --bs-link-hover-color-rgb: 60, 66, 168;
    --bs-border-color: #e7e7f1;
    --bs-border-radius: 0.6rem;
    --bs-border-radius-sm: 0.45rem;
    --bs-border-radius-lg: 0.9rem;
    --accent-gradient: linear-gradient(135deg, #4f56d3, #7c6ff0);
    --container-app-width: 920px;
    --container-admin-form-width: 480px;
}

.container-app {
    max-width: var(--container-app-width);
}

.container-admin-form {
    max-width: var(--container-admin-form-width);
}

html {
    background: radial-gradient(circle at top left, rgba(79, 86, 211, 0.08), transparent 50%);
    background-attachment: fixed;
}

body {
    letter-spacing: -0.01em;
    background: transparent;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

h1, h2, h3, h4, h5, h6 {
    font-weight: 600;
    letter-spacing: -0.02em;
}

a {
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.btn {
    font-weight: 500;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.btn:hover {
    transform: translateY(-1px);
}

/* Buttons welded into a group (e.g. radio btn-group toggles) must stay flat:
   lifting a single segment on hover shifts it out of line and breaks the row. */
.btn-group .btn:hover,
.btn-group-vertical .btn:hover {
    transform: none;
}

.btn-primary {
    --bs-btn-bg: var(--bs-primary);
    --bs-btn-border-color: var(--bs-primary);
    --bs-btn-hover-bg: #3c42a8;
    --bs-btn-hover-border-color: #3c42a8;
    --bs-btn-active-bg: #3c42a8;
    --bs-btn-active-border-color: #3c42a8;
    box-shadow: 0 6px 16px rgba(79, 86, 211, 0.25);
}

.btn-primary:hover {
    box-shadow: 0 8px 20px rgba(79, 86, 211, 0.32);
}

.spin-slow {
    display: inline-block;       /* needed so transform applies cleanly */
    animation: spin 20s linear infinite;
}

/* Item de menu correspondant à la page courante : trait doré (2px) sous le
   libellé. Pattern générique pour tous les items de la navbar. */
.navbar-nav .nav-link.active {
    text-decoration: underline;
    text-decoration-color: #c89c5a;
    text-decoration-thickness: 2px;
    text-underline-offset: 6px;
}

/* Accent doré (couleur du logo), utilisé pour marquer l'état actif de la
   navbar (ex. icône engrenage sur la page d'admin). */
.text-gold {
    color: #c89c5a !important;
}

/* Badge « Admin » : marque une action réservée aux admins mais rendue côté
   front (page publique). Accent violet, volontairement distinct du primaire
   (indigo) et du danger (rouge) pour ne pas être confondu avec une action
   destructrice. Voir le partial @Shared/_admin_badge.html.twig.
   NB : le badge reste un inline-block (jamais dans un conteneur flex), pour
   qu'il échappe naturellement au soulignement au survol du lien parent —
   text-decoration:none ne sort pas un flex item d'un underline propagé. */
.badge-admin {
    background-color: #ede9fb;
    color: #4c3aa8;
    font-weight: 500;
}
.btn .badge-admin {
    top: 0
}

/* Valeur littérale mise en avant au fil d'une phrase (email visé par une
   suppression, identifiant, nom d'événement...). Une puce discrète teintée du
   primaire, plutôt que le <code> de Bootstrap dont le rose signale du code et
   entre en concurrence avec le rouge des confirmations destructrices.
   La graisse suffit à accrocher l'œil : on garde la police du texte, un <code>
   monospace coupant la lecture d'une phrase courante. */
.value-inline {
    font-family: inherit;
    font-size: inherit;
    font-weight: 600;
    color: #3c42a8;
    background-color: rgba(79, 86, 211, 0.07);
    border-radius: var(--bs-border-radius-sm);
    padding: 0.05em 0.35em;
    /* Un email long ne doit pas déborder d'une modale étroite. */
    word-break: break-word;
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

.card {
    border-color: var(--bs-border-color);
    border-radius: 1rem;
    box-shadow: 0 10px 30px rgba(20, 20, 43, 0.06);
    background-color: #fff;
    background-image: linear-gradient(180deg, #ffffff 0%, #f8f8fc 100%);
}

/* Tableaux logés dans une carte : la table colle aux bords. Pas d'overflow:hidden
   sur la carte (sinon les menus déroulants des cellules seraient rognés) — on
   arrondit donc à la main les cellules du bas et on retire leur bordure pour
   épouser le radius de la carte. Le haut (thead transparent) épouse déjà le radius. */
.card > .table,
.card > .table-responsive > .table {
    margin-bottom: 0;
}

.card > .table > tbody > tr:last-child > td,
.card > .table-responsive > .table > tbody > tr:last-child > td {
    border-bottom: 0;
}

.card > .table > tbody > tr:last-child > td:first-child,
.card > .table-responsive > .table > tbody > tr:last-child > td:first-child {
    border-bottom-left-radius: 1rem;
}

.card > .table > tbody > tr:last-child > td:last-child,
.card > .table-responsive > .table > tbody > tr:last-child > td:last-child {
    border-bottom-right-radius: 1rem;
}

/* Densité des tableaux, réglée globalement plutôt que classe par classe.
   Bootstrap serre beaucoup (0.5rem, et 0.25rem en .table-sm) : à la taille de
   texte du thème les lignes se touchent et la liste devient illisible. On
   redéfinit donc les variables Bootstrap : les templates gardent leurs classes
   (.table-sm reste « la variante compacte »), seule l'échelle change. */
.table {
    --bs-table-cell-padding-y: 0.85rem;
    --bs-table-cell-padding-x: 1rem;
    /* Zébrures et survol volontairement très légers : ils guident l'œil sur une
       ligne, ils ne doivent pas dessiner des bandes. Teintés du primaire pour
       rester dans la palette plutôt qu'un gris sale. */
    --bs-table-striped-bg: rgba(79, 86, 211, 0.028);
    --bs-table-hover-bg: rgba(79, 86, 211, 0.055);
    --bs-table-border-color: var(--bs-border-color);
    vertical-align: middle;
}

.table-sm {
    --bs-table-cell-padding-y: 0.6rem;
    --bs-table-cell-padding-x: 0.85rem;
}

/* Les cellules d'un tableau dense contiennent souvent des badges/boutons `btn-sm`
   qui, sans ça, gonflent la hauteur de ligne de façon irrégulière. */
.table td > .btn-sm,
.table td > .badge {
    vertical-align: middle;
}

/* Alignement par défaut : tout est centré, en-tête compris, sauf la dernière
   colonne (celle des actions) poussée en bout de ligne. L'essentiel est que
   l'en-tête et ses cellules partagent la règle : un en-tête centré au-dessus de
   cellules à gauche donne l'impression d'un tableau désaxé. Une colonne qui veut
   autre chose (un long texte à lire, p. ex.) pose text-start sur son <th> et ses
   <td> — les utilitaires Bootstrap sont en !important, ils l'emportent. */
.table > :not(caption) > * > * {
    text-align: center;
}

.table > :not(caption) > * > *:last-child {
    text-align: end;
}

.table > thead th {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    color: var(--bs-secondary-color);
    white-space: nowrap;
    padding-top: 0.7rem;
    padding-bottom: 0.7rem;
    border-bottom-color: var(--bs-border-color);
}

/* En-tête posé sur un aplat très pâle : sépare la légende des données sans
   ajouter un trait de plus. Couleur opaque (et non un rgba) : certains en-têtes
   sont `position: sticky` (cf. .participants-table) et les lignes défileraient
   au travers d'un fond translucide. */
.card > .table > thead th,
.card > .table-responsive > .table > thead th {
    background-color: #f6f6fb;
}

.card > .table > thead tr:first-child > th:first-child,
.card > .table-responsive > .table > thead tr:first-child > th:first-child {
    border-top-left-radius: 1rem;
}

.card > .table > thead tr:first-child > th:last-child,
.card > .table-responsive > .table > thead tr:first-child > th:last-child {
    border-top-right-radius: 1rem;
}

/* Aligne la 1re/dernière colonne sur la gouttière de la carte (card-body en
   pose autant), pour que le texte ne colle pas au bord arrondi. */
.card .table > :not(caption) > * > *:first-child {
    padding-left: 1.25rem;
}

.card .table > :not(caption) > * > *:last-child {
    padding-right: 1.25rem;
}

.form-control, .form-select {
    border-color: var(--bs-border-color);
    padding-top: 0.55rem;
    padding-bottom: 0.55rem;
}

.form-control:focus, .form-select:focus {
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 0.2rem rgba(79, 86, 211, 0.15);
}

.js-autosize-textarea {
    resize: none;
    overflow-y: hidden;
}

/* Rendered Markdown (terms of use, privacy policy): scaled down so a
   heading in the source content doesn't dwarf the compact card it sits in. */
.js-markdown-content {
    font-size: 0.875rem;
}

.js-markdown-content h1 {
    font-size: 1.1rem;
}

.js-markdown-content h2 {
    font-size: 1rem;
}

.js-markdown-content h3,
.js-markdown-content h4,
.js-markdown-content h5,
.js-markdown-content h6 {
    font-size: 0.9rem;
}

.navbar {
    box-shadow: 0 1px 0 var(--bs-border-color);
}

.page-header {
    position: relative;
}

/* Le lien de retour est ancré juste au-dessus du titre de page (et non à un
   offset fixe dans la page), pour que le titre garde la même position
   verticale qu'il y ait un retour au-dessus de lui ou non. */
.breadcrumb {
    position: absolute;
    bottom: 100%;
    left: 0;
    margin-bottom: 0.35rem;
}

.dropdown-menu {
    border-color: var(--bs-border-color);
    border-radius: 0.8rem;
    box-shadow: 0 12px 30px rgba(20, 20, 43, 0.12);
    padding: 0.5rem;
}

.dropdown-item {
    border-radius: 0.5rem;
    padding: 0.5rem 0.75rem;
}

/* Dropdown items that open a dialog are anchors without an href (e.g. the
   participant roster's "resend questionnaire" action). Keep them behaving like
   the other link items: pointer cursor, and the shared a:hover underline. */
a.dropdown-item[role="button"] {
    cursor: pointer;
}

.dropdown-header {
    padding: 0.5rem 0.75rem;
}

.avatar-gradient {
    background: var(--accent-gradient);
}

/* Profile photo: a fixed-size circular avatar. Clicking (or tapping) the avatar
   itself opens the file picker to change it -- signalled by the camera badge --
   and the remove link sits below it. No hover-only affordance, so touch and
   desktop behave the same. */
.avatar-photo {
    width: 140px;
    height: 140px;
    font-size: 2.75rem;
}

.avatar-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.avatar-photo-label {
    cursor: pointer;
}

/* Small camera badge over the avatar's corner: the always-visible affordance
   that the avatar itself is clickable to change the photo. */
.avatar-photo-badge {
    position: absolute;
    right: 2px;
    bottom: 2px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background-color: var(--bs-primary, #4c34c6);
    color: #fff;
    border: 2px solid var(--bs-body-bg, #fff);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

/* Keeps the crop area a sensible height whatever the source image ratio, so
   the modal never grows taller than the viewport (Cropper.js sizes to it). */
.avatar-cropper-frame {
    max-height: 60vh;
    overflow: hidden;
}

.avatar-cropper-frame img {
    max-width: 100%;
}

.alert {
    border: none;
}

.js-toast-slide-in {
    animation: toast-slide-in 0.3s ease-out;
}

@keyframes toast-slide-in {
    from {
        transform: translateX(100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@property --construction-angle {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
}

.construction-frame {
    position: relative;
    border-radius: 1rem;
    padding: 2px;
}

.construction-frame::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    padding: 6px;
    background: conic-gradient(
        from var(--construction-angle),
        #f5c518 0deg 30deg, #1a1a1a 30deg 60deg,
        #f5c518 60deg 90deg, #1a1a1a 90deg 120deg,
        #f5c518 120deg 150deg, #1a1a1a 150deg 180deg,
        #f5c518 180deg 210deg, #1a1a1a 210deg 240deg,
        #f5c518 240deg 270deg, #1a1a1a 270deg 300deg,
        #f5c518 300deg 330deg, #1a1a1a 330deg 360deg
    );
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    animation: construction-tape-rotate 50s linear infinite;
    pointer-events: none;
}

@keyframes construction-tape-rotate {
    to {
        --construction-angle: 360deg;
    }
}

.event-banner {
    background-size: cover;
    background-position: center;
    /* The dark plate the tint gradient is built on: it is what the white title
       sits on while the banner picture is still downloading (see the lazy-media
       rules further down), and if the picture never arrives. */
    background-color: #14142b;
    color: #fff;
    padding: 2.5rem 0;
}

.event-banner-eyebrow {
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.8rem;
    font-weight: 600;
    opacity: 0.85;
}

.event-banner-title {
    font-size: clamp(1.5rem, 4vw, 2.5rem);
}

/* Event subscription card (templates/events/_subscription_card.html.twig):
   a banner-topped card used in the my-subscriptions / consulted lists. The
   whole card is clickable via the title's stretched-link. */
.event-subscription-card {
    overflow: hidden;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.event-subscription-card:hover,
.event-subscription-card:focus-within {
    border-color: var(--bs-primary);
    box-shadow: 0 10px 30px rgba(var(--bs-primary-rgb), 0.12);
    transform: translateY(-2px);
}

.event-subscription-card-media {
    position: relative;
    min-height: 120px;
    background-color: #14142b;
    background-image: linear-gradient(120deg, rgba(20, 20, 43, 0.85), rgba(79, 86, 211, 0.85));
    background-size: cover;
    background-position: center;
}

.event-subscription-card-media .badge {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
}

.admin-banner-title {
    font-size: clamp(2rem, 6vw, 4rem);
    font-weight: 700;
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}

.table-clickable-row {
    cursor: pointer;
}

/* Reusable navigation card (templates/shared/_nav_card.html.twig): a whole-card
   link with a hover affordance (arrow slides in + slight lift) so it reads as
   clickable. */
.nav-card {
    cursor: pointer;
    transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.nav-card:hover,
.nav-card:focus-within {
    border-color: var(--bs-primary);
    box-shadow: 0 10px 30px rgba(var(--bs-primary-rgb), 0.12);
    transform: translateY(-2px);
}

/* Carte d'une section réservée au super admin : bordure rouge, conservée au
   survol pour que le repère visuel ne disparaisse pas. */
.nav-card-restricted {
    border-color: var(--bs-danger);
}

.nav-card-restricted:hover,
.nav-card-restricted:focus-within {
    border-color: var(--bs-danger);
    box-shadow: 0 10px 30px rgba(var(--bs-danger-rgb), 0.12);
}

.nav-card-icon {
    font-size: 1.375rem;
    color: var(--bs-primary);
}

.nav-card-arrow {
    position: absolute;
    top: 1rem;
    right: 1rem;
    color: var(--bs-primary);
    opacity: 0;
    transform: translateX(-4px);
    transition: opacity 0.15s ease, transform 0.15s ease;
}

.nav-card:hover .nav-card-arrow,
.nav-card:focus-within .nav-card-arrow {
    opacity: 1;
    transform: translateX(0);
}

.event-countdown-segment {
    min-width: 3.5rem;
}

.event-countdown-value {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.event-countdown-label {
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.7rem;
    opacity: 0.8;
}

.event-cta-float {
    position: fixed;
    right: 1.25rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 1030;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.65rem 1.1rem;
    border-radius: 2rem;
    background: var(--accent-gradient);
    color: #fff;
    font-weight: 600;
    text-decoration: none;
    box-shadow: 0 0.5rem 1.25rem rgba(var(--bs-primary-rgb), 0.35);
    transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.15s ease;
}

.event-cta-float:hover,
.event-cta-float:focus {
    color: #fff;
    transform: translateY(-50%) scale(1.03);
}

.event-cta-float.is-hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
}

.event-gallery-cta {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.9rem 1.15rem;
    border-radius: var(--bs-border-radius-lg);
    background: var(--accent-gradient);
    color: #fff;
    text-decoration: none;
    box-shadow: 0 8px 20px rgba(var(--bs-primary-rgb), 0.3);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.event-gallery-cta:hover,
.event-gallery-cta:focus {
    color: #fff;
    transform: translateY(-1px);
    box-shadow: 0 10px 24px rgba(var(--bs-primary-rgb), 0.38);
}

.event-gallery-cta-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.2);
    font-size: 1.1rem;
}

.event-gallery-cta-badge {
    flex-shrink: 0;
    padding: 0.2rem 0.65rem;
    border-radius: 2rem;
    background: rgba(255, 255, 255, 0.22);
    font-size: 0.75rem;
    font-weight: 600;
}

/* Clickable "My questionnaire" card (icon left, title, arrow right). A neutral
   card so it reads distinctly from the accent-gradient gallery CTA above it. */
.event-questionnaire-cta {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.9rem 1.15rem;
    border-radius: var(--bs-border-radius-lg);
    background: #fff;
    border: 1px solid var(--bs-border-color);
    color: var(--bs-body-color);
    text-decoration: none;
    box-shadow: var(--bs-box-shadow-sm);
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.event-questionnaire-cta:hover,
.event-questionnaire-cta:focus {
    color: var(--bs-body-color);
    transform: translateY(-1px);
    border-color: var(--bs-primary);
    box-shadow: 0 8px 20px rgba(var(--bs-primary-rgb), 0.15);
}

.event-questionnaire-cta-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border-radius: 50%;
    background: rgba(var(--bs-primary-rgb), 0.12);
    color: var(--bs-primary);
    font-size: 1.1rem;
}

/* Placeholder swapped in by image-fallback.js when an <img> fails to load
   (its src becomes a transparent pixel, so all visible content here is CSS).
   The icon is a fixed-size background-image, not the <img> content itself,
   so it always renders at 48x48 centered -- independent of the box's own
   size, whether that's a small thumbnail or a full-width banner. */
img.img-broken {
    background-color: #ced4da;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23fff' d='M12 10a2 2 0 0 0-2 2v24a2 2 0 0 0 2 2h24a2 2 0 0 0 2-2V12a2 2 0 0 0-2-2H12zm0 2h24v16.17l-6.29-6.29a1 1 0 0 0-1.42 0L22 28.17l-3.29-3.29a1 1 0 0 0-1.42 0L12 30.17V12zm0 21.17 5.29-5.29a1 1 0 0 1 1.09-.22l.13.22 3.29 3.29 6.29-6.29a1 1 0 0 1 1.09-.22l.13.22L36 31.17V34H12v-.83zM18 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 48px 48px;
    min-width: 48px;
    min-height: 48px;
}

/* Waiting state of a lazily-loaded picture (see public/js/lazy-media.js). Until
   its file has downloaded the element wears .lazy-media and shows a shimmering
   plate; the picture then fades in (.lazy-media-in) and both classes go away, so
   the surface is back to its own styling. Both classes are added by the script:
   with JS off, nothing here applies and pictures render as the server sent them.

   Two shapes share the same plate: an <img>, whose own (still empty) box shows
   it, and an element flagged data-lazy-bg, whose url() layer the script lifted
   out of its inline background-image -- hence the compound selector, which has
   to outweigh the surface's own background rules (.event-banner and friends). */
img.lazy-media,
[data-lazy-bg].lazy-media {
    background-image: linear-gradient(
        100deg,
        rgba(255, 255, 255, 0) 20%,
        rgba(255, 255, 255, 0.55) 50%,
        rgba(255, 255, 255, 0) 80%
    );
    background-repeat: no-repeat;
    background-size: 200% 100%;
    background-position: -50% 0;
    animation: lazy-media-shimmer 1.4s ease-in-out infinite;
}

img.lazy-media {
    /* An <img> with no content yet is fully transparent: give the sweep a plate
       to run on, and keep the box from collapsing where the layout does not
       size it on its own. The display is left alone so an image sitting in a
       line of text does not jump between block and inline as it loads. */
    background-color: var(--bs-tertiary-bg, #f1f3f5);
    min-width: 2rem;
    min-height: 2rem;
}

.lazy-media-in {
    animation: lazy-media-fade-in 0.35s ease;
}

@keyframes lazy-media-shimmer {
    to {
        background-position: 150% 0;
    }
}

@keyframes lazy-media-fade-in {
    from {
        opacity: 0;
    }
}

@media (prefers-reduced-motion: reduce) {
    img.lazy-media,
    [data-lazy-bg].lazy-media,
    .lazy-media-in {
        animation: none;
    }
}

/* Media widget cards (see templates/shared/_media_widget.html.twig and
   public/js/media-upload.js). Each card is a square thumbnail with delete/copy
   actions revealed on hover and an optional usage overlay at the bottom-left. */
.media-card {
    position: relative;
    aspect-ratio: 1 / 1;
    border-radius: 0.5rem;
    overflow: hidden;
    background-color: #e9ecef;
}

.media-card img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.media-card-actions {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    display: flex;
    gap: 0.3rem;
    opacity: 0;
    transition: opacity 0.15s ease;
}

.media-card:hover .media-card-actions,
.media-card:focus-within .media-card-actions {
    opacity: 1;
}

.media-card-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.92);
    color: #212529;
    cursor: pointer;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

.media-card-action:hover {
    background-color: #fff;
}

.media-card-action--danger:hover {
    background-color: var(--bs-danger, #dc3545);
    color: #fff;
}

.media-card-action:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.media-card-usage {
    position: absolute;
    left: 0.5rem;
    bottom: 0.4rem;
    color: #fff;
    font-size: 0.8rem;
    font-weight: 600;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
    pointer-events: none;
}

.media-card-usage.is-unused {
    font-weight: 400;
    opacity: 0.9;
}

/* In-flight upload: the locally-read photo is dimmed behind a progress bar and
   a status line, and only becomes a regular card once the server answers. */
.media-card--uploading img {
    opacity: 0.45;
}

.media-card-progress {
    position: absolute;
    left: 0.5rem;
    right: 0.5rem;
    bottom: 1.6rem;
    height: 0.35rem;
    border-radius: 0.35rem;
    overflow: hidden;
    background-color: rgba(0, 0, 0, 0.35);
}

.media-card-progress-bar {
    height: 100%;
    width: 0;
    border-radius: 0.35rem;
    background-color: var(--bs-primary, #0d6efd);
    transition: width 0.15s linear;
}

/* Bytes are sent but the server is still decoding/compressing: the remaining
   wait has no measurable length, so the bar sweeps instead of standing still. */
.media-card-progress.is-indeterminate .media-card-progress-bar {
    animation: media-card-progress-sweep 1.1s ease-in-out infinite;
}

@keyframes media-card-progress-sweep {
    0% { opacity: 0.35; }
    50% { opacity: 1; }
    100% { opacity: 0.35; }
}

.media-card-status {
    position: absolute;
    left: 0.5rem;
    right: 0.5rem;
    bottom: 0.4rem;
    color: #fff;
    font-size: 0.75rem;
    line-height: 1.1;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
    pointer-events: none;
}

.media-card--failed {
    background-color: var(--bs-danger, #dc3545);
}

.media-card--failed img {
    opacity: 0.25;
}

.media-card--failed .media-card-status {
    bottom: 2.2rem;
}

.media-card-retry {
    position: absolute;
    left: 50%;
    bottom: 0.5rem;
    transform: translateX(-50%);
}

@media (prefers-reduced-motion: reduce) {
    .media-card-progress-bar {
        transition: none;
    }
    .media-card-progress.is-indeterminate .media-card-progress-bar {
        animation: none;
    }
}

/* Gallery tiles (templates/events/gallery.html.twig). The tile is the lightbox
   trigger; the credit and the delete button sit on top of it. */
.gallery-tile {
    position: relative;
}

.gallery-tile .js-gallery-item {
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: 0.375rem;
}

/* Block, so the photo fills its square instead of sitting on a text baseline
   with a gap underneath. */
.gallery-tile .js-gallery-item img {
    display: block;
}

/* The "add my photos" cell: same square as a photo tile, dashed so it reads as
   an empty slot rather than a picture. */
.gallery-tile-upload-button {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    width: 100%;
    aspect-ratio: 1 / 1;
    padding: 0.5rem;
    border: 2px dashed var(--bs-border-color, #dee2e6);
    border-radius: 0.375rem;
    background-color: transparent;
    color: var(--bs-secondary-color, #6c757d);
    cursor: pointer;
    text-align: center;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.gallery-tile-upload-button:hover,
.gallery-tile-upload-button:focus-visible,
/* media-upload.js flags a drag-over by adding .border-primary to the dropzone. */
.gallery-tile-upload.border-primary .gallery-tile-upload-button {
    border-color: var(--bs-primary, #0d6efd);
    color: var(--bs-primary, #0d6efd);
}

.gallery-tile-upload-button .bi {
    font-size: 1.5rem;
    line-height: 1;
}

.gallery-tile-upload-label {
    font-size: 0.7rem;
    line-height: 1.15;
    /* The cell is tiny on a phone; the icon alone carries the meaning there. */
    display: none;
}

@media (min-width: 576px) {
    .gallery-tile-upload-label {
        display: block;
    }
}

.gallery-tile-author {
    position: absolute;
    left: 0.5rem;
    right: 2rem;
    bottom: 0.35rem;
    color: #fff;
    font-size: 0.7rem;
    line-height: 1.2;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
    pointer-events: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.gallery-tile-delete {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.92);
    color: #212529;
    cursor: pointer;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    /* Always visible on touch, where there is no hover to reveal it. */
    opacity: 1;
}

.gallery-tile-delete:hover {
    background-color: var(--bs-danger, #dc3545);
    color: #fff;
}

@media (hover: hover) {
    .gallery-tile-delete {
        opacity: 0;
        transition: opacity 0.15s ease;
    }

    .gallery-tile:hover .gallery-tile-delete,
    .gallery-tile:focus-within .gallery-tile-delete {
        opacity: 1;
    }
}

/* Download shortcut on a tile: same chip as the delete button, opposite corner
   (top-left) so the two never overlap when a photo is both yours and downloadable. */
.gallery-tile-download {
    position: absolute;
    top: 0.35rem;
    left: 0.35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background-color: rgba(255, 255, 255, 0.92);
    color: #212529;
    cursor: pointer;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    /* Always visible on touch, where there is no hover to reveal it. */
    opacity: 1;
}

.gallery-tile-download:hover {
    background-color: var(--bs-primary, #0d6efd);
    color: #fff;
}

@media (hover: hover) {
    .gallery-tile-download {
        opacity: 0;
        transition: opacity 0.15s ease;
    }

    .gallery-tile:hover .gallery-tile-download,
    .gallery-tile:focus-within .gallery-tile-download {
        opacity: 1;
    }
}

/* One-digit verification-code boxes (challenge_code_widget). The caret is
   hidden and the focused box is highlighted instead, so the active slot reads
   as a selected zone rather than a blinking cursor. */
.otp-input {
    /* Keep every box on a single line: the boxes shrink to fit narrow
       screens rather than wrapping onto a second row. The gap shrinks with
       the viewport so the row always fits, even on the smallest phones. */
    flex-wrap: nowrap;
    gap: clamp(0.25rem, 2vw, 0.5rem) !important;
}

.otp-box {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
    max-width: 3rem;
    /* Square-ish even when shrunk on small screens. */
    aspect-ratio: 6 / 7;
    height: auto;
    padding: 0;
    /* Scale the digit down with the box so it never forces an overflow. */
    font-size: clamp(1rem, 5vw, 1.5rem);
    font-weight: 600;
    caret-color: transparent;
}

.otp-box::selection {
    background: transparent;
}

.otp-box:focus {
    background-color: rgba(var(--bs-primary-rgb), 0.08);
    border-color: var(--bs-primary);
    box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
}

/* Site footer: a dark grey band with muted text, sitting below the content
   (the body is a min-height flex column so it stays at the bottom). */
.app-footer {
    background-color: #2b2f33;
    color: #adb5bd;
}

.app-footer a {
    color: #adb5bd;
    text-decoration: none;
}

.app-footer a:hover {
    color: #f8f9fa;
}

/* Language switcher entries: muted like the rest of the footer, with the
   active locale brought forward. Buttons are styled to match the anchors. */
.app-footer .locale-option {
    color: #adb5bd;
    text-decoration: none;
    line-height: 1;
}

.app-footer .locale-option:hover {
    color: #f8f9fa;
}

.app-footer .locale-option.active {
    color: #fff;
    font-weight: 600;
}

/* Discreet language switcher floating below a centered card on footer-less
   pages (sign-in, landing, blocked). Flags only, dimmed on a light background,
   the active locale brought to full opacity. */
.locale-floating {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 1rem;
}

.locale-floating .locale-option {
    padding: 0;
    line-height: 1;
    opacity: 0.5;
    transition: opacity 0.15s ease;
}

/* Soft shadow behind each flag so it stays visible on light/gradient
   backgrounds (the flags carry no border of their own). */
.locale-floating .locale-option img {
    border-radius: 2px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.locale-floating .locale-option:hover,
.locale-floating .locale-option.active {
    opacity: 1;
}

/* Required-field indicator: the form theme (bootstrap_5_layout) adds a
   "required" class to labels and legends of mandatory fields; render the
   conventional red asterisk after them. Forms where requiredness is obvious
   (auth flows) opt out with the no-required-marker class. */
label.required::after,
legend.required::after {
    content: " *";
    color: var(--bs-danger, #dc3545);
}

.no-required-marker label.required::after,
.no-required-marker legend.required::after {
    content: none;
}

/* intl-tel-input (phone-input.js) wraps the field in an inline-block .iti; make
   it span the full width like every other Bootstrap form control, and keep its
   country dropdown above the modal it may be opened from. */
.iti {
    width: 100%;
}

.iti__dropdown-content {
    z-index: 1100;
}

/* Participant detail modal: the group "party" split, shown as adult / teenager
   / child icons (label on each icon's tooltip) with the count beside them. */
.detail-party {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
    margin-top: .25rem;
}

.detail-party-item {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}

.detail-party-comment {
    margin-top: .25rem;
    white-space: pre-wrap;
}

/* Participant detail modal: the questionnaire answers shown "at a glance" as a
   two-column grid, each behind the same icon as its questionnaire section. */
.subscription-glance {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem 1.5rem;
}

.subscription-glance-item {
    display: flex;
    align-items: flex-start;
    gap: .625rem;
}

.subscription-glance-item--full {
    grid-column: 1 / -1;
}

.subscription-glance-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    background: var(--bs-secondary-bg);
    color: var(--bs-secondary-color);
}

.subscription-glance-body {
    min-width: 0;
}

.subscription-glance-label {
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .02em;
    color: var(--bs-secondary-color);
    margin-bottom: .1rem;
}

.subscription-glance-value {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

@media (max-width: 575.98px) {
    .subscription-glance {
        grid-template-columns: 1fr;
    }
}

/* Participants roster: the whole row opens the details modal, so flag it as
   clickable (the actions menu and the comment form keep their own cursor). */
.participant-row {
    cursor: pointer;
}

/* Participant detail modal: the dedicated box for walk-in (manual) participants,
   grouping the actions that only apply to them (invite / edit / delete). */
.manual-box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding: .75rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background-color: var(--bs-tertiary-bg);
}

.manual-box-info {
    display: flex;
    align-items: center;
    gap: .75rem;
}

.manual-box-info > i {
    font-size: 1.5rem;
    color: var(--bs-secondary-color);
}

.manual-box-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}

/* Participant detail modal: the compact user card (avatar next to the
   participant's name, email, phone and location). */
.user-card {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius-lg);
    background: var(--bs-tertiary-bg);
}

.user-card-avatar {
    flex: 0 0 auto;
    width: 56px;
    height: 56px;
    font-size: 1.5rem;
    object-fit: cover;
}

.user-card-body {
    min-width: 0;
}

/* When the participant has an account, the whole card opens their member
   profile: signal it with a pointer, a hover lift and a chevron. */
.user-card--clickable {
    cursor: pointer;
    transition: border-color .15s ease-in-out, background-color .15s ease-in-out;
}

.user-card--clickable:hover,
.user-card--clickable:focus-visible {
    border-color: var(--bs-primary-border-subtle);
    background: var(--bs-primary-bg-subtle);
}

.user-card-go {
    align-self: center;
    color: var(--bs-secondary-color);
}

.user-card-name {
    font-weight: 600;
    font-size: 1.05rem;
    overflow-wrap: anywhere;
}

.user-card-email {
    font-size: .875rem;
    overflow-wrap: anywhere;
}

.user-card-line {
    font-size: .9rem;
    margin-top: .15rem;
}

.user-card-line .bi {
    color: var(--bs-secondary-color);
    margin-right: .35rem;
}

/* Participant detail modal: the Organisation section, three cards recapping the
   participant's team / accommodation / transport board placement. */
.org-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1rem;
}

.org-card {
    padding: .75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
}

.org-card--empty {
    background: var(--bs-tertiary-bg);
}

.org-card-head {
    display: flex;
    align-items: center;
    gap: .375rem;
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .02em;
    color: var(--bs-secondary-color);
    margin-bottom: .35rem;
}

.org-card-name {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.org-card-role {
    margin-top: .25rem;
    font-size: .875rem;
}

@media (max-width: 575.98px) {
    .org-cards {
        grid-template-columns: 1fr;
    }
}

/* Event journal: a Facebook-style two-sided timeline. A central vertical line
   runs down the thread; each entry hangs to one side of it (the side encodes
   the entry's scope, not an alternation), with a round icon marker sitting on
   the line. Below the md breakpoint the thread collapses to a single left rail. */
.journal-timeline {
    position: relative;
}

.journal-timeline::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    background: var(--bs-border-color);
    transform: translateX(-50%);
    pointer-events: none;
}

.journal-item {
    position: relative;
    width: 50%;
    padding: 0 2.5rem 1.75rem;
    box-sizing: border-box;
    /* The half-width item box reaches the centre line, the same column as the
       loader button; only the card (offset by the padding) should capture
       clicks, so the button below stays reachable. */
    pointer-events: none;
}

.journal-card {
    pointer-events: auto;
}

/* Placed with margins (not a relative offset) so the box the item occupies in
   flow is the box it shows in: an offset item would leave an empty half-width
   box behind that overlays whatever sits below the thread (e.g. the loader). */
.journal-item--start {
    margin-right: 50%;
}

.journal-item--end {
    margin-left: 50%;
}

.journal-item-marker {
    position: absolute;
    top: 0;
    /* Purely decorative (aria-hidden) and sitting on the centre line, the same
       column as the loader button: let clicks pass through to what is below. */
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: var(--bs-body-bg);
    border: 2px solid var(--bs-border-color);
    color: var(--bs-primary);
    font-size: 1.1rem;
    z-index: 1;
}

.journal-item--start .journal-item-marker {
    right: -1.25rem;
}

.journal-item--end .journal-item-marker {
    left: -1.25rem;
}

/* The loader sits on the thread's centre line, the same column as the icon
   markers (z-index 1); lift it so its button, not a marker, takes the click. */
[data-journal-scroll] {
    position: relative;
    z-index: 2;
}

.journal-card {
    border-color: var(--bs-border-color);
}

/* The shared-photo thumbnail sizes itself on the picture (height capped, width
   free), so while it waits it has no width of its own: hold a square the size of
   the cap, which the real picture replaces when it lands. */
.journal-card-thumbnail.lazy-media {
    width: 8rem;
    height: 8rem;
}

.journal-card-details dt {
    overflow-wrap: anywhere;
}

.journal-card-details dd {
    overflow-wrap: anywhere;
}

@media (max-width: 767.98px) {
    .journal-timeline::before {
        left: 1.25rem;
    }

    .journal-item,
    .journal-item--start,
    .journal-item--end {
        width: 100%;
        margin-left: 0;
        margin-right: 0;
        padding-left: 3.5rem;
        padding-right: 0;
    }

    .journal-item--start .journal-item-marker,
    .journal-item--end .journal-item-marker {
        left: 0;
        right: auto;
    }
}

/* Participants roster status board: five icon-only facet columns, an inline
   editable organizer comment, and the legend key. */
.participant-facet {
    width: 1%;
    white-space: nowrap;
}

/* A solid background and a bottom rule keep the header legible over the striped
   rows sliding underneath. */
.participants-table thead th {
    background-color: #fff;
    box-shadow: inset 0 -1px 0 var(--bs-border-color);
}

/* Sticky header on wide screens only. Two things have to hold together:
   - the horizontal-scroll container (Bootstrap's .table-responsive) is dropped,
     so the header anchors to the page scroll rather than an inner scrollport;
   - only then is the header made sticky.
   Below this breakpoint .table-responsive keeps its horizontal scrolling, and
   since an `overflow-x: auto` also makes the Y axis a scrollport, a sticky header
   there would anchor to that container and sit offset from the rows it heads
   (which is exactly what a narrow screen used to show). A sticky header matters
   less on narrow screens, so it simply stays in flow.

   admin-participants.js measures the page's sticky navbar and sets
   --participants-thead-top (the rem value is only a fallback). */
@media (min-width: 992px) {
    .participants-table-wrap {
        overflow: visible;
    }

    .participants-table thead th {
        position: sticky;
        top: var(--participants-thead-top, 3.5rem);
        z-index: 3;
    }
}

/* The legend key laid out as vertical lists, one column per lifecycle facet. */
.participants-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
}

.participants-legend-group {
    min-width: 14rem;
}

.organizer-comment {
    cursor: text;
    min-width: 6rem;
    min-height: 1.5rem;
}

.organizer-comment-empty {
    color: var(--bs-secondary-color);
    font-style: italic;
}

.organizer-comment-input {
    min-width: 8rem;
}

.user-card-badges {
    margin-top: 0.25rem;
}

/* Settlement map (back-office). The height is fixed in pixels rather than left
   to the content: the screenshot step grows the window right before capturing,
   and a map that resized with it would be caught re-rendering, giving a
   different PNG on every run. */
.settlement-map-canvas {
    height: 32rem;
}

@media (max-width: 575.98px) {
    .settlement-map-canvas {
        height: 20rem;
    }
}

/* The fixed height above is what the fullscreen button exists to escape, so it
   must not survive into fullscreen. */
.settlement-map-canvas:fullscreen {
    height: 100%;
}

.settlement-map-loader {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Over the basemap while it paints, but under the map's own controls. */
    z-index: 2;
    background-color: var(--bs-body-bg);
}
