/*!
 * ScoutMagic — Copyright (C) 2026 Xavier Dubois and contributors
 * Licensed under AGPL-3.0-or-later. See LICENSE and NOTICE.
 */

:root {
    /* Single micro type size for dense chrome (calendar weekday header,
       event bars, overflow badge, "new" markers). Used to be an ad-hoc
       spread of 0.65/0.68/0.7/0.72rem — one variable so the smallest text
       on the site never drifts below the ~11.5px legibility floor again.
       Anything that needs to read as body copy uses .small/0.875rem
       instead; this is strictly for compact, glanceable labels. */
    --fs-micro: 0.72rem;
}

/* Core member_photo() component (see Core\Photo, TwigFactory::create()).
   3:4 portrait format, same size wherever it's used (e.g. every photo on the
   trombinoscope, including the highlighted "responsable" card). */
.member-photo {
    width: 96px;
    aspect-ratio: 3 / 4;
    object-fit: cover;
    border-radius: 0.375rem;
}
/* No-photo fallback: same box as a real photo, with a centered initials
   circle (same look as the account menu avatar). */
.member-photo-placeholder {
    background-color: var(--bs-secondary-bg, #e9ecef);
    display: flex;
    align-items: center;
    justify-content: center;
}
.member-photo-initials {
    width: 60%;
    aspect-ratio: 1 / 1;
    font-size: 0.9rem;
}
/* Member page (Espace des animés) header — a circular variant of
   member_photo(), sized for a page header rather than a trombinoscope
   grid cell. Same self-contained sizing technique as .member-photo above
   (fixed width + aspect-ratio directly on the photo/placeholder class
   itself, NOT a percentage of a wrapper) — that's deliberate, not
   incidental: member_photo()'s editable-mode markup wraps the photo in
   `.editable-image`, which has no explicit size of its own (see
   editable.css — only position:relative, for the overlay button).
   Percentage sizing (width/height:100%) inside that wrapper has nothing
   definite to resolve against and silently falls back to the image's
   raw intrinsic pixel size — which is how a placeholder ended up a
   normal small box while a real uploaded photo rendered at full
   original size. aspect-ratio sidesteps that failure mode entirely
   (it computes a definite height from the width alone, independent of
   any ancestor), which is exactly why the trombinoscope/Staffs photos
   never had this bug despite going through the same .editable-image
   wrapper in configuration mode. Sized to roughly match the two-line
   name/section text next to it, while staying comfortably bigger than
   the 44px edit-overlay button that sits in its corner (in
   configuration mode, or for the member themselves) — a placeholder box
   smaller than its own "change photo" button reads as broken. */
.member-photo-header {
    width: 64px;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    flex-shrink: 0;
}
.member-photo-header .member-photo-initials {
    width: 100%;
    height: 100%;
    font-size: 1.5rem;
}

/* .section-picker's horizontal-scroll rules are gone with the chips they
   scrolled: partials/section_picker.html.twig now maps onto the select
   bar, which is one full-width row opening a panel and never scrolls
   sideways. The wrapper class stays as a page-level hook. */

/* partials/select_bar.html.twig — the disclosure panel anchored under
   the bar. Bootstrap has no disclosure component, so nothing here
   duplicates one (AGENTS.md): this is the native <details> marker being
   removed and the panel being given a scroll of its own, and that is
   all. Touch sizing is NOT here — the <summary> carries .tap-target and
   takes its height from app.css's `pointer: coarse` block (design.md
   §7.2), which is the only place touch sizing lives. */
.select-bar-details > summary {
    cursor: pointer;
    list-style: none;
}
.select-bar-details > summary::-webkit-details-marker {
    display: none;
}
.select-bar-chevron {
    transition: transform 0.15s ease-in-out;
}
.select-bar-details[open] .select-bar-chevron {
    transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
    .select-bar-chevron { transition: none; }
}
/* The panel scrolls itself rather than the page: a unit with thirty
   sections must not push the page content it sits above off-screen. */
.select-bar-panel {
    max-height: 60vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

/* partials/nav_rail.html.twig — one horizontally-scrollable row of
   Bootstrap `nav-underline` tabs. The component itself is Bootstrap's;
   these rules only hide the scrollbar and add the scroll affordance it
   leaves behind. */
.nav-rail .nav {
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* Scrolling shadows: the two body-coloured gradients are painted with
       `background-attachment: local` so they travel WITH the content and
       cover the shadow beneath them at each end, while the two dark
       gradients are `scroll` and stay pinned to the box. The result is a
       fade that appears only when the rail actually overflows and
       disappears once that end is reached — a plain always-on mask would
       dim the last tab even on a two-tab rail that never scrolls.
       --bs-body-bg is Bootstrap's own token, so this follows dark mode
       (design.md §7.8) instead of hardcoding white. */
    background-image:
        linear-gradient(to right, var(--bs-body-bg), rgba(0, 0, 0, 0)),
        linear-gradient(to left, var(--bs-body-bg), rgba(0, 0, 0, 0)),
        linear-gradient(to right, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0)),
        linear-gradient(to left, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0));
    background-position: left center, right center, left center, right center;
    background-repeat: no-repeat;
    background-size: 1.25rem 100%, 1.25rem 100%, 0.6rem 100%, 0.6rem 100%;
    background-attachment: local, local, scroll, scroll;
}
.nav-rail .nav::-webkit-scrollbar {
    display: none;
}

/* Core\View\MonthGrid\MonthGridBuilder month view, rendered by
   partials/month_grid.html.twig — generic, reusable by any module (not
   calendar-specific). No Bootstrap component duplicated here — Bootstrap
   has no calendar/month-grid component. Each week is ONE CSS grid (7
   columns x (1 + row_count) rows): row 1 holds the day-number labels, the
   rows below hold event bars, computed server-side (MonthGridBuilder's
   greedy row-packing, capped at a fixed row count so a day's cell height
   never grows with its event count — see .calendar-day-overflow for what
   happens beyond the cap). The day-cell background/border box
   (.calendar-day-cell) spans row 1 through the last bar row so it reads as
   a single iCal-style cell containing the number and "its" events —
   .calendar-day-number sits on top of it (row 1 only, pointer-events:none
   so clicks/hover fall through to the cell behind it) rather than nesting,
   since CSS grid placement is sibling-based, not parent/child. Mobile-first:
   small cells/text by default, roomier from the md breakpoint. */
.calendar-weekday-row {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
    font-size: var(--fs-micro);
}
.calendar-week {
    /* Single source of truth for bar-row height — partials/month_grid.html.twig
       reads this same variable for grid-template-rows, so the CSS bar height
       and the grid track height can never drift apart again (they did: a
       real bug, found and fixed — see the pointer:coarse note on
       .calendar-event-bar--clickable below for what that looked like). */
    --calendar-bar-height: 1.35rem;
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    border-left: 1px solid var(--bs-border-color, #dee2e6);
    border-top: 1px solid var(--bs-border-color, #dee2e6);
}
/* The two rules below carry a user-agent reset because partials/month_grid.html.twig
   renders the cell and the bar as a real <button> when the caller makes them
   actionable (and a plain <div> when it does not) — see that file's header for
   why. The reset is what makes the two render identically: without it a button
   brings its own border, padding, background, centred text and system font. */
.calendar-day-cell {
    appearance: none;
    -webkit-appearance: none;
    padding: 0;
    background-color: transparent;
    color: inherit;
    font: inherit;
    text-align: inherit;
    border: 0;
    border-radius: 0;
    border-right: 1px solid var(--bs-border-color, #dee2e6);
    border-bottom: 1px solid var(--bs-border-color, #dee2e6);
}
.calendar-day-cell.is-padding {
    background-color: var(--bs-tertiary-bg, #f8f9fa);
}
.calendar-day-cell.is-today {
    background-color: var(--bs-primary-bg-subtle, #cfe2ff);
}
.calendar-day-cell--clickable {
    cursor: pointer;
}
.calendar-day-cell--clickable:hover {
    background-color: var(--bs-secondary-bg, #e9ecef);
}
.calendar-day-number {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.25rem;
    min-height: 1.4rem;
    padding: 0.2rem 0.35rem;
    font-size: 0.75rem;
    color: var(--bs-body-color);
    pointer-events: none;
}
.calendar-day-cell.is-padding .calendar-day-number {
    color: var(--bs-secondary-color, #6c757d);
}
.calendar-day-cell.is-today .calendar-day-number {
    font-weight: 600;
}
/* Count of events hidden past MonthGridBuilder's row cap (see
   GridEvent/day.overflow_count) — a small badge next to the day number,
   hover/title-tooltip lists the hidden events' labels. pointer-events:auto
   re-enables hit-testing for just this child inside the otherwise
   pass-through .calendar-day-number (a browser only fires the native
   title-tooltip on an element that actually receives the hover). */
.calendar-day-overflow {
    pointer-events: auto;
    cursor: default;
    flex-shrink: 0;
    font-size: var(--fs-micro);
    font-weight: 600;
    color: var(--bs-secondary-color, #6c757d);
}
.calendar-event-bar {
    appearance: none;
    -webkit-appearance: none;
    border: 0;
    text-align: left;
    font-family: inherit;
    font-weight: inherit;
    margin: 1px;
    padding: 0 0.3rem;
    border-radius: 0.25rem;
    font-size: var(--fs-micro);
    /* Derived from the same --calendar-bar-height the grid row height comes
       from (minus the 1px top/bottom margin above) — never a value of its
       own, or the two drift apart again. */
    line-height: calc(var(--calendar-bar-height) - 2px);
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    align-self: start;
}
.calendar-event-bar.continues-before {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}
.calendar-event-bar.continues-after {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}
.calendar-event-bar--clickable {
    cursor: pointer;
}

/* Receipt upload drop zone (modules/finance/views/receipts/form.html.twig).
   The real control is the visually-hidden file input inside it, so the zone
   has to be the thing that shows focus: position:relative keeps the hidden
   input inside the zone's own box rather than letting it be positioned
   against some far-away ancestor, and :focus-within gives a keyboard user
   the focus ring they would otherwise never see. */
.receipt-drop-zone {
    position: relative;
}
.receipt-drop-zone:focus-within {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 2px;
}

/* "Prochains évènements" list item (modules/calendar/views/public.html.twig)
   — opens the same #eventDetailsModal as a grid bar, via the same
   showEventDetails() data-* contract. Not part of the grid itself, so it
   gets its own hook class rather than reusing .calendar-event-bar--clickable. */
.calendar-upcoming-event-item {
    cursor: pointer;
}
/* The list item cannot itself be a <button> — it holds flow content (several
   <div>s and a badge), which a button may not contain. So the event label is
   the button, stretched by Bootstrap's .stretched-link over the whole
   (position:relative) list item, which keeps the entire row clickable exactly
   as before while making the real control focusable and Enter/Space-operable
   with no script. Same user-agent reset as the grid bar above. */
.calendar-upcoming-event-trigger {
    appearance: none;
    -webkit-appearance: none;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
}

/* Touch-target exception to AGENTS.md's 44px minimum, deliberate and
   documented (not an oversight): a month grid packs several event bars
   into one ~22px grid row inside a 7-column day cell, back-to-back with a
   1px margin and no slack above/below to widen an invisible hit area into
   without covering a stacked sibling event, nor sideways without spilling
   into the next day's column. The previous approach grew the bar itself to
   44px on touch (line-height:2rem + min-height:44px) — but the grid row
   height (see --calendar-bar-height above) never grew to match, so the
   44px-tall bar simply overflowed its ~22px track and covered the
   neighbouring row/day below it. That was the actual reported bug. The bar
   now keeps its real, compact height as its tap target on every pointer
   type — the legibility font-size bump this block used to add on coarse
   pointers is now the base size (--fs-micro), and the box itself no
   longer grows.
   Cell height / MonthGridBuilder's maxVisibleRows: unaffected by this —
   that cap was always a fixed PHP-side constant (3), never derived from
   any CSS value; this fix only makes the grid's own rendered row height
   finally match what that cap was always meant to look like. */
@media (pointer: coarse) {
    .calendar-event-bar {
        /* Same --fs-micro as the base rule now (the bump this used to
           apply became the baseline); only the roomier padding remains
           coarse-specific. */
        padding: 0.15rem 0.4rem;
    }
}

@media (min-width: 768px) {
    .calendar-week { --calendar-bar-height: 1.5rem; }
    .calendar-day-number { min-height: 1.75rem; padding: 0.3rem 0.5rem; font-size: 0.85rem; }
    .calendar-event-bar { font-size: 0.75rem; }
}

/* Modules\Groups\views\show.html.twig — the group page's two columns,
   from 992px up.

   The problem this solves is a width one. The feed spans the whole
   `.container` base.html.twig renders (up to 1200px here), and
   `.groups-media-grid-1` is 100% wide with a 4/3 box, so ONE photo in a
   message came out close to 900px tall on a laptop: a single message
   filled the screen and the conversation around it disappeared. On a
   phone the very same rules give exactly the right result, which is why
   nothing below 992px changes — this whole file's mobile-first default
   (AGENTS.md § CSS / frontend) stays the shipped behaviour and the
   breakpoint only adds to it.

   So above 992px the feed is capped at 680px — a comfortable measure for
   a column of messages — and the width that frees up becomes a real side
   column instead of margin: the group's search box, its members, its
   latest photos.

   Grid rather than a Bootstrap row, for one reason: the search box has to
   be ABOVE the composer in the document (a member arriving to find
   something said last month should not scroll past every message to
   discover that looking for it is possible — show.html.twig's own
   comment), and be the FIRST thing in the side column on a desktop. One
   element placed into two different layouts is what grid placement is
   for; a Bootstrap row would have meant rendering the form twice, with
   two elements sharing one id.

   Track 2 is `1fr` rather than a fixed width: what the side column is
   worth is exactly what the feed's cap gives back — about 250px at
   992px, about 470px on a 1200px screen. */
.groups-layout-side-extra {
    /* Below the breakpoint this block is `d-none` and never drawn: the
       feed paginates as you scroll (groups.js), so anything placed after
       it on a phone is content nobody reaches. It is a desktop
       affordance, not a mobile section moved to the bottom. */
    margin-bottom: 1rem;
}

/* The side column's photo teaser — a plain square grid, deliberately NOT
   `.groups-media-grid`: these thumbnails carry no `data-media-id`, so
   groups.js's media polling (which resolves a pending cell with
   `document.querySelector('[data-media-id="…"]')`, first match wins)
   cannot pick one of them instead of the feed cell it meant to fill.
   Only media already processed are listed here for the same reason —
   there is nothing to poll. */
.groups-side-photo-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2px;
    border-radius: 0.5rem;
    overflow: hidden;
}
.groups-side-photo-grid img {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

@media (min-width: 992px) {
    .groups-layout {
        display: grid;
        grid-template-columns: minmax(0, 680px) minmax(0, 1fr);
        column-gap: 1.5rem;
        /* Both columns start at the top; without this the shorter one
           stretches and its cards grow to the feed's height. */
        align-items: start;
    }
    /* The feed spans both rows of the side column, so it keeps running
       past the members/photos blocks instead of stopping level with
       them. `min-width: 0` is what lets a long word or a wide code span
       inside a message be cut rather than widen the track. */
    .groups-layout-feed {
        grid-column: 1;
        grid-row: 1 / span 2;
        min-width: 0;
    }
    .groups-layout-search {
        grid-column: 2;
        grid-row: 1;
        min-width: 0;
    }
    .groups-layout-side-extra {
        grid-column: 2;
        grid-row: 2;
        min-width: 0;
    }
}

/* Modules\Groups\views\show.html.twig — the composer folded down to one
   line, revealed by public/assets/js/groups.js.

   The tint itself is Bootstrap's (the bg-primary-subtle /
   border-primary-subtle utilities on the element), so only the two
   things no utility supplies live here.

   `.btn` with no variant defines none of the hover custom properties, and
   the two utilities that tint the bar are `!important` anyway — so the
   usual variant hover could neither have been used nor have won. A
   shadow rather than a colour change: the bar keeps its fill, which is
   what makes it read as "the same line, ready" instead of as a button
   lighting up. */
.groups-composer-collapsed:hover,
.groups-composer-collapsed:focus-visible {
    box-shadow: var(--bs-box-shadow-sm);
}
/* A flex item will not shrink below its own content's width without
   this, so `text-truncate` alone cannot ellipsize here: the invitation
   would push the two icons off a 360px screen instead of being cut.
   There is no Bootstrap utility for it — `min-width-0` is written in a
   few templates in this repository and has never existed. */
.groups-composer-collapsed-label {
    flex: 1 1 auto;
    min-width: 0;
}

/* Modules\Groups\views\partials\post_media_grid.html.twig — a post's
   attached media (1 to 4), and the flat "Galerie du groupe" grid
   (gallery.html.twig) reuses the single-cell rule below on its own
   plain wrapping grid. Module spec: 1 media wide, 2 side by side, 3
   with one tall on the left, 4 in a 2x2 — expressed with CSS grid
   rather than Bootstrap's column system, since the 3-media layout's
   spanning first cell has no equivalent there. */
.groups-media-grid {
    display: grid;
    gap: 2px;
    border-radius: 0.5rem;
    overflow: hidden;
}
.groups-media-grid-1 {
    grid-template-columns: 1fr;
    aspect-ratio: 4 / 3;
}
.groups-media-grid-2 {
    grid-template-columns: 1fr 1fr;
    aspect-ratio: 16 / 9;
}
.groups-media-grid-3 {
    grid-template-columns: 2fr 1fr;
    grid-template-rows: 1fr 1fr;
    aspect-ratio: 4 / 3;
}
.groups-media-grid-3 .groups-media-cell:first-child {
    grid-row: 1 / span 2;
}
.groups-media-grid-4 {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    aspect-ratio: 4 / 3;
}
.groups-media-cell {
    position: relative;
    display: block;
    background-color: var(--bs-tertiary-bg);
}

/* The same 992px rule as the two-column layout above, and the reason it
   exists: a 4/3 box at the full width of a desktop feed is a photo taller
   than the screen it is read on. 420px is a picture in a conversation
   rather than a poster — and the same number app.css caps a rich-text
   image at, so a photo posted in a group and a photo pasted into an
   article do not come out at two different sizes. Written on the wrapper
   so every count keeps its own internal proportions (1 wide, 2 side by
   side, 3 with a tall cell on the left, 4 in a 2x2) untouched.

   Deliberately not scoped to the group page: the same card, with the same
   grid, is what the search results, a message's own permalink and the
   moderator's reports page draw, and they had the same photo. */
@media (min-width: 992px) {
    .groups-media-grid {
        max-width: 420px;
    }
}

/* Modules\Groups\views\partials\post_link.html.twig — a post's optional
   link preview card. The image is capped rather than aspect-ratio-boxed
   like the media grid above: an og:image is whatever shape the linked
   page happened to provide, and cropping a preview thumbnail can hide
   the very thing (a poster, a logo) the preview exists to show. */
.groups-link-preview {
    border: 1px solid var(--bs-border-color);
    overflow: hidden;
}
.groups-link-preview-image {
    display: block;
    width: 100%;
    max-height: 200px;
    object-fit: cover;
}
.groups-link-preview-description {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Modules\Groups\views\partials\reactions.html.twig — the six-emoji picker
   and the running tally, on posts and (in the -compact variant) on
   replies. Every button is a real form submit, so the 44px touch target
   is on the button itself rather than on a wrapper. */
.groups-reaction-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    line-height: 1;
    font-size: 1.1rem;
    border: 1px solid transparent;
    background: transparent;
    border-radius: 999px;
}
.groups-reaction-btn:hover,
.groups-reaction-btn:focus-visible {
    background-color: var(--bs-tertiary-bg);
}
.groups-reaction-btn-own {
    border-color: var(--bs-primary-border-subtle);
    background-color: var(--bs-primary-bg-subtle);
}
.groups-reaction-count {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    background-color: var(--bs-tertiary-bg);
    font-size: 0.8rem;
    line-height: 1.4;
}
.groups-reaction-own {
    background-color: var(--bs-primary-bg-subtle);
    font-weight: 600;
}
/* On a reply the picker would dominate the bubble at full size, so it
   shrinks — the tally keeps its own size, since that is the part people
   actually read. */
.groups-reactions-compact .groups-reaction-btn {
    min-width: 34px;
    min-height: 34px;
    font-size: 0.95rem;
}

/* Modules\Groups\views\partials\reactions.html.twig — the six-emoji picker,
   folded behind a single "Réagir" button on a phone.

   Six 44px targets plus the tally do not fit on one line at 360px, so the
   row wrapped and every message paid two lines for a control most readers
   never use.

   Gated on `.groups-js`, which public/assets/js/groups.js puts on <html>:
   without JavaScript nothing here applies, the toggle stays hidden and the
   picker stays exactly as it has always been — visible and usable. The
   toggle is never the ONLY way to reach a reaction.

   Desktop is untouched: the media query is the only place either rule
   lives. */
.groups-reaction-toggle {
    display: none;
}
/* A message's own text. `overflow-wrap: anywhere` because a member can
   paste a URL with no space in it for two hundred characters: the anchor
   elides what it SHOWS (Core\View\TextLinker), but a single unbroken
   60-character token still has to be allowed to break rather than push
   the card wider than the phone holding it. */
.groups-post-body {
    overflow-wrap: anywhere;
}

/* Owned here rather than by Bootstrap's `d-inline-flex` utility, which is
   `display: inline-flex !important` and would make both of these
   impossible to hide. */
.groups-reaction-picker {
    display: inline-flex;
}
/* Owned here rather than by `flex-wrap`, for the same reason: that
   utility is `!important` and the one place this row must NOT wrap
   (the open picker on a phone, below) could never turn it off. */
.groups-reactions {
    flex-wrap: wrap;
}
@media (max-width: 767.98px) {
    /* The two halves swap by UNFOLDING rather than by appearing: the
       emoji run grows out to the right from where the button was, and the
       button collapses into it at the same time, so the row is never
       wider open than closed and nothing jumps.

       max-width, not display: `display` cannot be transitioned, and the
       clip an animated max-width produces is exactly the effect wanted —
       the six buttons are uncovered left to right. `visibility` rides
       along with a delay equal to the fold, so a folded picker is not
       merely invisible but out of the tab order too; without it a
       keyboard would land on six buttons nobody can see.

       The width ceilings are deliberately generous: they bound the
       animation, never the layout, and both blocks are inline-flex, so
       neither ever actually reaches the number. */
    .groups-js .groups-reaction-toggle,
    .groups-js .groups-reaction-picker {
        overflow: hidden;
        white-space: nowrap;
        transition:
            max-width 0.25s ease-out,
            opacity 0.18s ease-out,
            visibility 0s linear 0.25s;
    }
    /* Read as one column with the « Commenter » line underneath it
       (.groups-thread-summary), because on a card they are the two
       things you can do to a message and a reader takes them in
       together. Issue #361 was the two ways they did not:

       - `font-size: 0.875rem`, not 0.8rem. Both are "small" and neither
         was chosen against the other; the summary line's is the one the
         rest of this file calls body copy, so it wins.
       - `padding-left: 0`, against Bootstrap's `.btn-sm` 0.5rem. That
         half-rem is why the button looked indented on a message with NO
         reactions yet and correct on one with some: the tally that sits
         first in the row when it exists is a `btn-link p-0`, so the row
         starts flush, and without it the toggle's own padding became the
         row's left edge. The summary line below has no padding at all,
         so 8px was the whole of the misalignment. The right padding
         stays — nothing lines up against that side — and the 44px touch
         target is kept by min-height, never by the horizontal padding. */
    .groups-js .groups-reaction-toggle {
        display: inline-flex;
        min-height: 44px;
        max-width: 8rem;
        padding-left: 0;
        color: var(--bs-secondary-color);
        font-size: 0.875rem;
    }
    .groups-js .groups-reaction-picker {
        max-width: 0;
        opacity: 0;
        visibility: hidden;
    }
    .groups-js .groups-reactions-open .groups-reaction-toggle {
        max-width: 0;
        min-width: 0;
        padding-left: 0;
        padding-right: 0;
        opacity: 0;
        visibility: hidden;
    }
    /* The row must hold on ONE line at 360px even when a tally is
       already sitting in it — six 44px buttons plus a tally of three or
       four counts is wider than any phone, and the row wrapped, putting
       the emoji under the message they belong to.

       So the open picker is allowed to shrink instead of wrap: it takes
       the space left beside the tally (flex: 1 1 auto over min-width: 0)
       and its buttons shrink with it, down to a floor of 32px. The 44px
       target is kept whenever there is room for it — a message with no
       reactions yet, and every screen wider than this media query — and
       given up only in the one case where the alternative is a control
       that does not fit at all. */
    .groups-js .groups-reactions-open {
        flex-wrap: nowrap;
    }
    /* The picker keeps its whole width and never gives any of it up:
       six 34px targets and their gaps come to ~224px, which fits on the
       narrowest phone this project supports with room to spare. */
    .groups-js .groups-reactions-open .groups-reaction-btn {
        min-width: 34px;
        width: 34px;
        font-size: 0.95rem;
    }
    /* The tally is what yields, because it is the part that can: it
       takes whatever is left and scrolls sideways inside itself if a
       message collected more kinds of reaction than fit. Nothing is
       lost, and the row stays one line. */
    .groups-js .groups-reactions-open .groups-reaction-tally {
        flex: 0 1 auto;
        min-width: 0;
        overflow-x: auto;
    }
    .groups-js .groups-reactions-open .groups-reaction-picker {
        flex: 0 0 auto;
        max-width: 22rem;
        opacity: 1;
        visibility: visible;
        /* No delay on the way IN — the picker has to be usable the
           instant it is on screen. */
        transition:
            max-width 0.25s ease-out,
            opacity 0.18s ease-out,
            visibility 0s;
    }
    /* Somebody who has asked their system for less movement gets the
       swap and none of the travel. */
    @media (prefers-reduced-motion: reduce) {
        .groups-js .groups-reaction-toggle,
        .groups-js .groups-reaction-picker,
        .groups-js .groups-reactions-open .groups-reaction-toggle,
        .groups-js .groups-reactions-open .groups-reaction-picker {
            transition: none;
        }
    }
}

/* Modules\Groups\views\partials\reply_card.html.twig — one level of
   replies, never nested (schema.sql has no parent_reply_id), so a single
   fixed indent is all that is ever needed. */
.groups-reply {
    padding-left: 0.75rem;
    border-left: 2px solid var(--bs-border-color);
}
.groups-reply-bubble {
    background-color: var(--bs-tertiary-bg);
    border-radius: 0.75rem;
    padding: 0.5rem 0.75rem;
}
/* Modules\Groups\views\partials\post_card.html.twig — the photo attached
   to a comment, shown before it is sent. Same 72px cell as the message
   composer's own media previews, so the two composers look like one
   feature rather than two. */
.groups-reply-image-thumb {
    display: inline-block;
    width: 72px;
    height: 72px;
    border-radius: 0.375rem;
    overflow: hidden;
    background: var(--bs-tertiary-bg);
}
.groups-reply-image-thumb-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.groups-reply-image-link {
    max-width: 220px;
}
.groups-reply-image-link img {
    width: 100%;
    height: auto;
    border-radius: 0.5rem;
}

/* Modules\Groups\views\show.html.twig — the composer's poll panel sits in
   the same row as the Photos button: the two things you attach to a
   message, side by side. Closed, the <details> is exactly as wide as the
   button its <summary> draws; open, it takes the whole row so the
   question and its choices are full-width fields rather than a
   button-wide column. Pure CSS, because the disclosure is the browser's
   own and has to keep working with no JavaScript at all. */
.groups-poll-details > summary {
    list-style: none;
}
.groups-poll-details > summary::-webkit-details-marker {
    display: none;
}
.groups-poll-details[open] {
    flex: 1 0 100%;
}

/* Modules\Groups\views\partials\post_card.html.twig — a message's
   conversation, folded away behind the browser's own <details>/<summary>
   disclosure so a feed reads as a list of messages rather than as every
   answer anybody ever gave. Styled as a quiet text control, not a button:
   it is the line you read past when you are not interested.

   The default triangle marker is replaced by a chevron that turns with
   the disclosure — `list-style: none` for every engine, plus the WebKit
   pseudo-element Safari still needs. min-height keeps the whole line a
   44px touch target (AGENTS.md § CSS / frontend). */
.groups-thread-summary {
    list-style: none;
    cursor: pointer;
    min-height: 44px;
    font-size: 0.875rem;
    color: var(--bs-secondary-color);
    user-select: none;
}
.groups-thread-summary::-webkit-details-marker {
    display: none;
}
.groups-thread-summary:hover,
.groups-thread-summary:focus-visible {
    color: var(--bs-body-color);
}
.groups-thread-summary .bi-chat-left-text {
    transition: transform 0.15s ease-in-out;
}
.groups-thread[open] > .groups-thread-summary .bi-chat-left-text {
    transform: rotate(-8deg);
}
.groups-thread-count {
    text-decoration: underline;
    text-underline-offset: 0.2em;
}
/* The "2 nouveaux" badge. The thread opens itself when it is there, so
   this is a marker of what changed rather than a control — it is never
   the only way to reach the new comments. */
.groups-thread-new {
    font-size: var(--fs-micro);
}

/* Modules\Groups — an auto-hidden post or reply, as its group's moderators
   see it. Nobody else ever receives this markup: the row is excluded in
   the SQL (Repository\PostRepository, Repository\ReplyRepository), so this
   class dims a message rather than hiding one. */
.groups-post-hidden {
    opacity: 0.72;
    border-style: dashed;
}

/* Modules\Groups — the message the AI moderation refused, handed back to
   its own author with the suggested rewording. It lives in the author's
   session for exactly one render (Support\RejectedDraft) and is never
   stored anywhere else. */
.groups-rejected-suggestion {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* Modules\Groups — the composer while its dynamic submit (groups.js) is in
   flight: greyed out rather than merely disabled, so publishing a post
   reads as "in progress" instead of "broken". The textarea and submit
   button are ALSO set .disabled by the same code (real, not just visual,
   since a greyed-out-but-clickable button would double-submit) — this
   class only adds the look. */
.groups-composer-busy {
    opacity: 0.6;
}

/* Core\Http\Controller\SupportController — the "Aperçu de ce qui est envoyé"
   block on Configuration > Diagnostic. The statistics payload is a couple of
   dozen lines of JSON: capping the height keeps the rest of the page (the
   support-package block and its warning) reachable without a long scroll,
   and the block scrolls on its own instead. */
.support-payload-preview {
    max-height: 26rem;
    overflow: auto;
}

/* Modules\SupportDashboard — the two current-state charts. Chart.js needs a
   sized container to render into (its canvas is responsive but has no
   intrinsic height), and a fixed height keeps the two cards level whatever
   each chart's legend does. */
.support-chart {
    position: relative;
    height: 260px;
}

/* Core\Mail\Feedback — the two week-by-week charts of « Courrier sortant »
   (issue #420). Same need as `.support-chart` above and deliberately not the
   same class: that one is a module's, named after it, and core reaching into
   a module's stylesheet would tie the two together for the sake of two
   declarations. Lower than that one, because these cards carry a <details>
   table under the canvas and the chart should not push it off the screen —
   and one height for both charts rather than two classes for twenty pixels,
   the seed chart's legend being the taller of the two. */
.outbound-trend-chart {
    position: relative;
    height: 240px;
}

/* Modules\SupportDashboard — the raw report JSON in the detail dialog. The
   dialog already scrolls; this caps the JSON block so the metrics above it
   stay reachable without scrolling past a hundred lines. */
.support-raw-json {
    max-height: 20rem;
    overflow: auto;
}

/* The ticket queue's first cell carries the reference AND an excerpt of
   the description, so it needs a floor: left to itself in a narrow
   viewport the column collapses to two words a line and every row grows
   to ten lines tall, which is worse than the horizontal scroll
   `.table-responsive` already provides. Same reasoning and the same
   shape as the Passage tables' own min-width (roadmap IT-02). */
.support-ticket-cell {
    min-width: 17.5rem;
}

/* Prose, not code — a ticket description, a resolution note, the AI's
   grouped summary. All three are ordinary text blocks whose line breaks
   live in the MARKUP (Twig's `nl2br`), never in a `white-space` property.

   That distinction is the whole point of this rule and it was learned the
   hard way. These used to be `pre` elements relying on `white-space:
   pre-wrap` from this very file — which base.html.twig deliberately does
   not load, so each page has to ask for it. A page that renders without
   the stylesheet then shows somebody's sentence on ONE line and pushes
   the whole layout sideways, which is exactly what was reported. A text
   block wraps by itself; what is left here is a cap on height and a
   promise that a pasted URL cannot widen the card.

   `pre-wrap` must NOT come back: with the breaks already in the markup it
   would double every one of them, and on the analysis (rendered HTML) it
   would turn the newlines BETWEEN elements into visible blank space. */
.support-ticket-description,
.support-analysis {
    max-height: 30rem;
    overflow-y: auto;
    overflow-wrap: anywhere;
}

.support-analysis > :first-child {
    margin-top: 0;
}

.support-analysis > :last-child {
    margin-bottom: 0;
}

.support-analysis h4,
.support-analysis h5,
.support-analysis h6 {
    font-size: 1rem;
    font-weight: 600;
    margin-top: 1rem;
}

/* ── Availability month grid (Core\View\MonthGrid\DayStateGridBuilder,
   partials/month_day_grid.html.twig) ──────────────────────────────────────
   The sibling of .calendar-week above, and deliberately a separate set of
   rules rather than a variant of it: that grid stacks event bars under a
   day number (its cell height is driven by --calendar-bar-height and a row
   cap), this one has exactly one uniform square per day and nothing to
   stack. Sharing a class between them would couple two layouts that only
   look alike.

   Every state gets its colour from --daygrid-day-color, which the partial
   sets inline per cell only when the caller supplied one; the defaults
   below are what a caller gets for free. Colour is never the sole carrier
   of meaning — the partial always renders the state's French label as
   visually-hidden text and as a native tooltip. Mobile-first: cells are
   sized off the viewport by default, capped from the md breakpoint. */
.daygrid {
    border-left: 1px solid var(--bs-border-color, #dee2e6);
    border-top: 1px solid var(--bs-border-color, #dee2e6);
}
.daygrid-week {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
}
.daygrid-day {
    /* 44px minimum touch target (AGENTS.md § CSS / frontend). The aspect
       ratio keeps the cells square as the grid widens, so the month reads
       as a calendar rather than a stretched table. */
    min-height: 44px;
    aspect-ratio: 1 / 1;
    display: flex;
    align-items: center;
    justify-content: center;
    /* User-agent reset — the partial renders this element as a real
       <button> when the day is selectable, so the button and the div it
       replaces must render identically. Same reason as
       .calendar-day-cell above. */
    appearance: none;
    -webkit-appearance: none;
    padding: 0;
    font: inherit;
    color: inherit;
    border: 0;
    border-radius: 0;
    border-right: 1px solid var(--bs-border-color, #dee2e6);
    border-bottom: 1px solid var(--bs-border-color, #dee2e6);
    background-color: var(--daygrid-day-color, var(--bs-body-bg, #fff));
}
.daygrid-day-number {
    font-size: 0.85rem;
    line-height: 1;
}
.daygrid-day.is-padding {
    opacity: 0.45;
}
.daygrid-day.is-today .daygrid-day-number {
    font-weight: 700;
    text-decoration: underline;
}

/* Default palette per DayState::STATE_* — overridden per cell whenever the
   caller passes an explicit colour. */
.daygrid-day--free { --daygrid-day-color: var(--bs-success-bg-subtle, #d1e7dd); }
.daygrid-day--occupied { --daygrid-day-color: var(--bs-danger-bg-subtle, #f8d7da); }
.daygrid-day--partial { --daygrid-day-color: var(--bs-warning-bg-subtle, #fff3cd); }
.daygrid-day--selected { --daygrid-day-color: var(--bs-primary, #0d6efd); color: #fff; }
/* "Occupied in the morning, free from midday" — a diagonal split rather
   than a third flat colour, because the day genuinely is half of each and
   a flat blend would read as its own unrelated status. */
.daygrid-day--departing {
    background-image: linear-gradient(
        135deg,
        var(--bs-danger-bg-subtle, #f8d7da) 0 50%,
        var(--bs-success-bg-subtle, #d1e7dd) 50% 100%
    );
}
/* Past, inside a notice window, or beyond the horizon — greyed out, and
   pointedly NOT the occupied colour: telling a visitor "taken" for a day
   that is merely too late to request makes them give up on a free day. */
.daygrid-day--unselectable {
    --daygrid-day-color: var(--bs-tertiary-bg, #f8f9fa);
    color: var(--bs-secondary-color, #6c757d);
}
.daygrid-day--selectable {
    cursor: pointer;
}
.daygrid-day--selectable:hover {
    filter: brightness(0.94);
}
.daygrid-day--selectable:focus-visible {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: -2px;
}
@media (min-width: 768px) {
    .daygrid-day {
        aspect-ratio: auto;
        min-height: 3rem;
    }
    .daygrid-day-number { font-size: 0.95rem; }
}

/* partials/rich_text_form_field.html.twig — a `{{ mot_cle }}` placeholder
   rendered as one indivisible chip. The point is the `contenteditable=
   "false"` in the markup, not the styling; what the styling adds is that an
   author can SEE which parts of their text are substituted and which are
   their own words. Kept alongside the editor rather than in app.css because
   the field is opt-in per page, like everything else in this file. */
.doc-keyword {
    display: inline-block;
    padding: 0 0.35em;
    border-radius: 0.25rem;
    background-color: var(--bs-primary-bg-subtle, #cfe2ff);
    color: var(--bs-primary-text-emphasis, #052c65);
    font-family: var(--bs-font-monospace, monospace);
    font-size: 0.85em;
    white-space: nowrap;
    user-select: all;
}
.rich-text-form-surface:focus {
    outline: 2px solid var(--bs-primary, #0d6efd);
    outline-offset: 1px;
}

/* partials/audit_timeline.html.twig — the per-entity change history
   (ARCHITECTURE.md §8.65). Everything here is geometry: the line down the
   left is Bootstrap's own .border-start, and each entry's marker is a dot
   straddling it. The colours come from utilities on the markup
   (.bg-primary / .bg-warning), so the timeline follows the theme in both
   modes without this file knowing which one is active.

   The dot is sized in em rather than px: it sits next to text and should
   grow with it when a reader enlarges the page. */
.audit-timeline-list > .audit-entry > .audit-entry-marker {
    width: 0.6em;
    height: 0.6em;
    left: 0;
    top: 0.45em;
    margin-left: -1rem;
}

/* ── Star rating input (Modules\Camps\views\_rating_input.html.twig) ──
   Five radio buttons drawn as stars. No JavaScript anywhere in here:
   what fills a star is the state of a radio, which the browser already
   maintains, and a rating widget that stops working when a script fails
   to load would be a worse dropdown than the dropdown it replaced.

   The radios stay in the page — moved out of sight, never `display: none`
   — because a hidden-by-display radio leaves the tab order and the radio
   group with it: no arrow keys, no focus ring, nothing for a screen
   reader to land on. Each one keeps its accessible name from the
   visually-hidden text inside its own label. */
.rating-input {
    display: inline-flex;
    align-items: center;
}
/* The star is what the radio is positioned against, so focusing one by
   keyboard scrolls the page to the star and not to wherever an
   unpositioned absolute box happened to land — inside a dialog, that is
   somewhere else entirely. */
.rating-input-star {
    position: relative;
    display: inline-flex;
}
.rating-input input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}
/* Icon-only tappable targets in a row, sized here rather than left to
   app.css's pointer:coarse block for the same reason .groups-reaction-btn
   above is: the drawn star IS the control, and a 20px star that only
   grows on a touch device is a control a mouse user aims at too. */
.rating-input label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin: 0;
    cursor: pointer;
    font-size: 1.35rem;
    line-height: 1;
    color: var(--bs-secondary-color);
    border-radius: var(--bs-border-radius);
}
.rating-input input:focus-visible + label {
    outline: 2px solid var(--bs-primary);
    outline-offset: -2px;
}
/* Both icons are always in the markup and exactly one of them is shown —
   the outline and the fill are different glyphs, and swapping a class
   would need the script this component does without. */
.rating-input .bi-star-fill {
    display: none;
    /* The same yellow as the stars this note is READ as afterwards
       (_stars.html.twig's .text-warning), so the row a chief clicks and
       the row the next staff sees are one thing. */
    color: var(--bs-warning);
}
/* A star is filled when it is the one chosen, or when a star further
   along the row is: a note of 3 fills the first three, which is what
   makes the row read as a number rather than as five switches. */
.rating-input-star:has(input:checked) .bi-star,
.rating-input-star:has(~ .rating-input-star input:checked) .bi-star {
    display: none;
}
.rating-input-star:has(input:checked) .bi-star-fill,
.rating-input-star:has(~ .rating-input-star input:checked) .bi-star-fill {
    display: inline-block;
}

/* Hover preview, on pointers that have a hover: the row shows the note
   that a click would give, and the stored one steps aside while it does —
   two ratings lit at once would be a lie about which one is recorded. */
@media (hover: hover) {
    .rating-input:hover .rating-input-star .bi-star {
        display: inline-block;
    }
    .rating-input:hover .rating-input-star .bi-star-fill {
        display: none;
    }
    .rating-input:hover .rating-input-star:hover .bi-star,
    .rating-input:hover .rating-input-star:has(~ .rating-input-star:hover) .bi-star {
        display: none;
    }
    .rating-input:hover .rating-input-star:hover .bi-star-fill,
    .rating-input:hover .rating-input-star:has(~ .rating-input-star:hover) .bi-star-fill {
        display: inline-block;
    }
}

/* ---------------------------------------------------------------------
   Le sélecteur de créance (modules/finance/views/partials/
   receivable_picker.html.twig).

   Les suggestions sont une liste déroulante POSITIONNÉE, pas un bloc dans
   le flux, et c'est une règle de mise en page plutôt qu'un goût : ces
   sélecteurs vivent dans un `row g-2 align-items-end`, qui aligne le BAS
   des colonnes et non les contrôles. Tout ce qui pend sous un champ — un
   texte d'aide, une liste de résultats — remonte l'input de ce champ et
   le désaligne de celui d'à côté. C'est exactement le défaut signalé sur
   « Non imputés », où le champ Créance et le champ Montant ne se
   trouvaient plus sur la même ligne.

   Le sélecteur de séjour du courrier de camps
   (modules/camps/views/partials/stay_picker.html.twig) obéit à la même
   règle et la partage ici plutôt que d'en écrire une deuxième copie : ce
   sont deux noms pour une seule mise en page, et deux copies dériveraient
   à la première retouche.

   The generic search picker (partials/search_picker.html.twig) also
   reuses it: its list hangs below the search field, inside a relative
   container that wraps only that field.
   --------------------------------------------------------------------- */
.receivable-picker,
.stay-picker {
    position: relative;
}

.receivable-picker__results,
.stay-picker__results,
.search-picker__results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    /* Au-dessus du champ suivant et des cartes en dessous, sous les
       modales de Bootstrap (1050) et sa barre de navigation (1030). */
    z-index: 1000;
    max-height: 16rem;
    overflow-y: auto;
    box-shadow: var(--bs-box-shadow, 0 .5rem 1rem rgba(0, 0, 0, .15));
}

/* Each result keeps the height of its content (issue #645). The list is a
   Bootstrap list-group, so a flex column; once its rows add up to more than
   max-height, WebKit shrinks them to fit instead of letting the list scroll
   — every row the same, too short height, the date line cut off by the next
   row. Chrome and Firefox honour min-height: auto and never showed it. */
.receivable-picker__results > *,
.stay-picker__results > *,
.search-picker__results > * {
    flex-shrink: 0;
}

/* ---------------------------------------------------------------------
   La visionneuse de fichiers (assets/js/file-viewer.js).

   Elle existe parce que l'application installée n'a ni barre d'adresse ni
   bouton retour : une navigation vers un fichier remplace la fenêtre et il
   ne reste rien à presser. Regarder une photo se fait donc ICI, avec un
   bouton Fermer.

   Le recouvrement est construit en JavaScript au premier usage, d'où ces
   règles plutôt que des styles en ligne : un balisage rendu sur chaque
   page pour n'être presque jamais ouvert est du poids mort, et la page
   publique de suivi d'une location a prouvé qu'il n'était pas seulement
   mort — elle affirme n'offrir aucun téléchargement, et un recouvrement
   portant « download » lui faisait manquer sa propre règle.
   --------------------------------------------------------------------- */
.file-viewer {
    position: fixed;
    inset: 0;
    /* Au-dessus des modales de Bootstrap (1050), comme la visionneuse de
       la galerie (1080) dont ceci reprend le comportement. */
    z-index: 1080;
    background: rgba(0, 0, 0, .92);
}

/* Le fond ferme au clic ; ce qu'il contient, non. Les pointeurs sont donc
   désactivés sur l'étage et rendus au contenu. */
.file-viewer__stage {
    pointer-events: none;
}

.file-viewer__content {
    pointer-events: auto;
    max-width: 100%;
    max-height: 85vh;
    object-fit: contain;
}

/* ---------------------------------------------------------------------
   The carpool organiser's point picker (public/assets/js/
   covoiturage-organize.js). A height is the one thing Leaflet cannot do
   without: a map in a zero-height box renders nothing at all.
   --------------------------------------------------------------------- */
.carpool-point-map {
    height: 14rem;
}

/* The address's fixed marker on that map (issue #642): a ring, so it can
   never be taken for the draggable pin Leaflet draws for the meeting
   point. Built by L.divIcon, whose own white square is cleared here. */
.leaflet-marker-icon.carpool-address-marker {
    background: transparent;
    border: 0;
}

.carpool-address-marker::before {
    content: "";
    display: block;
    width: 18px;
    height: 18px;
    box-sizing: border-box;
    border: 4px solid var(--bs-danger);
    border-radius: 50%;
    background: var(--bs-body-bg);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, .35);
}
