/* =========================================================================
   View-specific CSS
   =========================================================================

   Load order (annuaire/templates/annuaire/base.html):
       bootstrap.css → tokens.css → components.css → {vendor_css} → main.css

   This file is deliberately last, because most of what is left in it exists
   to win against a *vendor* stylesheet (leaflet, family-chart, pdf.js), which
   loads in the vendor_css block above. Generic layout, cards, forms, tables,
   alerts and typography now live in components.css; what remains here is the
   per-feature CSS and the vendor overrides.

   Colours come from --fb-* tokens (see tokens.css). Do not reintroduce
   literal hex values: they break Nightfall silently.

   Sections
     1  Portraits and relation photos
     2  Presence calendar
     3  Map (leaflet)
     4  Genealogy (family-chart)
     5  Person / address pickers
     6  Markdown body and editor
     7  Document viewer (pdf.js and images)
     8  Place placeholder
   ========================================================================= */

/* =========================================================================
   1. Portraits and relation photos
   ========================================================================= */

/* Was .expandable-image: a 100px→200px grow over a *1 second* transition on
   hover, which reflowed the page under the cursor and shifted whatever you
   were about to click. It is now a fixed medallion; enlargement is the
   image viewer's job (document_viewer.js), which already does zoom properly. */
.profile-portrait {
    width: 150px;
    height: 150px;
    object-fit: cover;
    border-radius: 50%;
    border: 3px solid var(--fb-border);
    background: var(--fb-panel);
    transition: border-color var(--fb-dur-fast) var(--fb-ease);
}

a:hover > .profile-portrait { border-color: var(--fb-gold); }

.profile-picture {
    width: 1.5em;
    height: 1.5em;
    border-radius: 50%;
    object-fit: cover;
}

/* Shared across annuaire, profil and généalogie -- applied at every photo render
   site conditionally on Person.deceased. */
.person-photo--deceased {
    filter: grayscale(1);
    border-color: var(--fb-ink);
}

.relations-inline {
    display: flex;
    align-items: center;
    gap: var(--fb-sp-2);
    flex-wrap: wrap;
}

/* --- Profile record (#124) ---------------------------------------------- */
/* Page-scoped on purpose: the .fb-record component is shared with the place
   detail page, so these rules hang off .profile-record instead. */

/* The actions moved out of the rail to below the relations, where they read as
   "edit what you just looked at" rather than page-level chrome. Laid out as a
   row here; the rail's own grid stacking was only right in a 280px column. */
.profile-record__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--fb-sp-2);
    margin-top: var(--fb-sp-5);
}

/* Below this width .fb-record collapses to one column, which is also where the
   sticky rail stops working: in a single-row grid area a sticky box has no
   travel. Turning the record into a block container gives it some.
   --profile-sticky-top is measured and published by profile_sticky_identity.js;
   the literal is only the no-JS fallback. */
@media (max-width: 767.98px) {
    .profile-record { display: block; }

    /* display: contents drops the rail's own box -- and with it the shared
       component's sticky rule the rail would otherwise still carry -- so its
       children flow straight into .profile-record instead of the whole rail
       (identity card + info card together) pinning as one unit (#124,
       reopened: the info card kept scrolling but never left the viewport). */
    .profile-record .fb-record__rail { display: contents; }
    .profile-record .fb-record__rail > * { margin-bottom: var(--fb-sp-4); }

    .profile-record .profile-identity {
        position: sticky;
        top: var(--profile-sticky-top, 68px);
        /* Well under the top bar's own layer, which must stay above this. */
        z-index: 3;
        transition: padding var(--fb-dur-fast) var(--fb-ease);
    }

    /* Condensed: photo and name side by side, everything else stood down. */
    .profile-record .profile-identity--pinned {
        flex-direction: row;
        align-items: center;
        text-align: left;
        gap: var(--fb-sp-3);
        padding: var(--fb-sp-3);
    }

    .profile-record .profile-identity--pinned .profile-portrait {
        width: 44px;
        height: 44px;
        border-width: 2px;
    }

    .profile-record .profile-identity--pinned .fb-record__name { font-size: 1.125rem; }

    .profile-record .profile-identity--pinned .fb-meta { display: none; }
}

/* =========================================================================
   2. Presence calendar
   ========================================================================= */

.presence-calendar { margin: var(--fb-sp-4) 0; }

.presence-calendar-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--fb-sp-2);
    margin-bottom: var(--fb-sp-2);
}

.presence-calendar-scroll {
    overflow-x: auto;
    border: 1px solid var(--fb-border);
    border-radius: var(--fb-radius);
}

.presence-calendar-grid {
    display: grid;
    grid-template-columns: 160px repeat(var(--cal-cols, 42), minmax(22px, 1fr));
    grid-auto-rows: 32px;
    column-gap: 0;
    row-gap: 2px;
    background: var(--fb-parchment);
    min-width: 760px;
}

.presence-calendar-grid .cal-corner,
.presence-calendar-grid .cal-day,
.presence-calendar-grid .cal-name {
    background: var(--fb-card);
    padding: 4px 6px;
    font-size: var(--fb-text-xs);
    border-bottom: 1px solid var(--fb-border);
    overflow: hidden;
    white-space: pre-line;
    line-height: 1.1;
}

.presence-calendar-grid .cal-day {
    text-align: center;
    color: var(--fb-bark);
}

.presence-calendar-grid .cal-day-weekend { background: var(--fb-parchment); }

.presence-calendar-grid .cal-day-today {
    background: var(--fb-warning-soft);
    font-weight: 700;
    color: var(--fb-ink);
}

.presence-calendar-grid .cal-day-month-start { border-left: 2px solid var(--fb-border-strong); }

.presence-calendar-grid .cal-name {
    font-weight: 600;
    text-align: left;
    border-right: 1px solid var(--fb-border);
}

.presence-calendar-grid .cal-lane {
    background: var(--fb-card);
    border-bottom: 1px solid var(--fb-border);
}

.presence-calendar-grid .cal-bar {
    align-self: center;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: var(--fb-text-xs);
    color: var(--fb-bar-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    height: 22px;
    line-height: 18px;
    margin: 0 1px;
}

/* Retuned from Bootstrap's grey/green/blue to the landscape palette: a past
   stay is bark, a current one spruce, a future one ember. The fills come from
   dedicated --fb-bar-* tokens rather than those palette colours directly,
   because the label sitting on them has to stay legible in Nightfall too --
   dark spruce carried --fb-card text at 1.18:1. */
.presence-calendar-grid .cal-bar.bar-past { background: var(--fb-bar-past); }
.presence-calendar-grid .cal-bar.bar-current { background: var(--fb-bar-current); }
.presence-calendar-grid .cal-bar.bar-future { background: var(--fb-bar-future); }

.presence-calendar-grid .cal-empty-message {
    padding: var(--fb-sp-3);
    color: var(--fb-bark);
    background: var(--fb-card);
    text-align: center;
}

.presence-calendar-legend {
    display: flex;
    gap: var(--fb-sp-2);
    font-size: var(--fb-text-sm);
    color: var(--fb-bark);
}

.presence-calendar-legend .presence-calendar-bar {
    display: inline-block;
    padding: 2px var(--fb-sp-2);
    border-radius: 4px;
    color: var(--fb-bar-ink);
    font-size: var(--fb-text-xs);
}

.presence-calendar-legend .bar-past { background: var(--fb-bar-past); }
.presence-calendar-legend .bar-current { background: var(--fb-bar-current); }
.presence-calendar-legend .bar-future { background: var(--fb-bar-future); }

.cal-window-toggle.active,
.presence-view-toggle .btn.active {
    background: var(--fb-ember);
    border-color: var(--fb-ember);
    color: var(--fb-card);
}

.presence-view-toggle { margin-bottom: var(--fb-sp-2); }

/* Presence rows keep their semantic tint inside .fb-table. */
.presence-current { background: var(--fb-success-soft); }
.presence-past td { color: var(--fb-bark); }

.presence-past-details { margin-top: var(--fb-sp-4); }

.presence-past-details summary {
    cursor: pointer;
    color: var(--fb-bark);
    font-size: var(--fb-text-sm);
    margin-bottom: var(--fb-sp-2);
}

.presence-actions { display: flex; gap: var(--fb-sp-2); }

.presence-form { max-width: 400px; }

/* =========================================================================
   3. Map (leaflet)
   ========================================================================= */

.map-container {
    /* Leaflet forces position:relative on its container inline but leaves
       z-index auto, so its panes (z-index 200-700, see vendor/leaflet/leaflet.css)
       leak into the root stacking context and paint over .person-picker-results
       (z-index 10) above it. z-index:0 makes the map its own stacking context,
       trapping its panes below any positioned sibling. */
    position: relative;
    z-index: 0;
    height: 70vh;
    min-height: 320px;
    border: 1px solid var(--fb-border);
    border-radius: 0 0 var(--fb-radius) var(--fb-radius);
}

.map-marker-avatar {
    /* --map-marker-size is published from map_init.js (MARKER_SIZE) -- single
       source of truth shared by JS icon sizing and this CSS. */
    width: var(--map-marker-size, 60px);
    height: var(--map-marker-size, 60px);
    border-radius: 50%;
    overflow: hidden;
    border: 2px solid var(--fb-card);
    box-shadow: var(--fb-shadow-2);
    background-color: var(--fb-card);
}

.map-marker-avatar img {
    /* Leaflet's own CSS (vendor/leaflet/leaflet.css) sets
       ".leaflet-marker-pane img { width: auto; ... }" at higher specificity
       than this rule, which left non-square photos at their intrinsic
       aspect ratio -- flush-left in the circle instead of filling it.
       !important is what beats it; object-fit alone was a no-op once the
       box already matched that intrinsic ratio. */
    width: 100% !important;
    height: 100%;
    object-fit: cover;
    object-position: center;
    display: block;
}

/* A place with no photo: the ridge mark, drawn by map_init.js's RIDGE_SVG.
   Was a 🏔️ emoji until the Alpenglow design pass -- it rendered as a tofu box
   wherever the system has no emoji font, and could not follow the theme. */
.map-marker-avatar-placeholder {
    display: flex;
    align-items: flex-end;
    background-color: var(--fb-sky);
}

.map-marker-avatar-placeholder .fb-ridge {
    /* The medallion is a circle, so the ridge fills it rather than sitting in
       it: the round overflow:hidden on .map-marker-avatar does the cropping. */
    width: 100%;
    height: 70%;
}

/* An event marker: a centered calendar glyph, not the bottom-aligned ridge
   (a different modifier so this doesn't affect the place placeholder). */
.map-marker-avatar-event {
    align-items: center;
    justify-content: center;
}

.map-marker-avatar-event .fb-icon {
    width: 55%;
    height: 55%;
}

.map-marker-cluster {
    position: relative;
    width: var(--map-marker-size, 60px);
    height: var(--map-marker-size, 60px);
}

.map-marker-cluster-avatar {
    position: absolute;
    top: 0;
    left: 0;
    width: calc(var(--map-marker-size, 60px) * 0.7);
    height: calc(var(--map-marker-size, 60px) * 0.7);
}

.map-marker-cluster-avatar:nth-child(1) { z-index: 3; }

.map-marker-cluster-avatar:nth-child(2) {
    top: calc(var(--map-marker-size, 60px) * 0.2);
    left: calc(var(--map-marker-size, 60px) * 0.2);
    z-index: 2;
}

.map-marker-cluster-avatar:nth-child(3) {
    top: calc(var(--map-marker-size, 60px) * 0.4);
    left: calc(var(--map-marker-size, 60px) * 0.4);
    z-index: 1;
}

.map-marker-cluster-badge {
    position: absolute;
    bottom: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 9px;
    background-color: var(--fb-ember);
    color: var(--fb-card);
    font-size: 0.7em;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--fb-card);
    z-index: 4;
}

.map-marker-cluster-heading { font-weight: 600; margin-bottom: 4px; }

.map-marker-cluster-list { list-style: none; margin: 0; padding: 0; }

/* Leaflet's popups and controls are white cards with grey text by default --
   jarring against the parchment ground, and unreadable in Nightfall. */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip,
.leaflet-control-layers,
.leaflet-bar a {
    background: var(--fb-card);
    color: var(--fb-ink);
}

.leaflet-bar a,
.leaflet-control-layers { border-color: var(--fb-border); }

.leaflet-bar a:hover { background: var(--fb-panel); color: var(--fb-ember); }

/* =========================================================================
   4. Genealogy (family-chart)
   ========================================================================= */

.genealogie-container {
    height: 70vh;
    min-height: 320px;
    border: 1px solid var(--fb-border);
    border-radius: 0 0 var(--fb-radius) var(--fb-radius);
    background: var(--fb-card);
    /* family-chart still appends an (empty) .f3-nav-cont here at createChart
       time even though the search widget itself is now mounted in the
       toolbar's #genealogie-search (see family_tree.js's setPersonDropdown
       cont option) -- kept relative in case a future vendor update anchors
       something else to it. */
    position: relative;
}

.genealogie-chart-mount {
    /* f3.createChart() overwrites whatever className it's given, so the .f3
       class family-chart's own CSS requires (e.g. ".f3 div.card-image-circle
       img") has to live on an outer element it never touches -- this div is
       the one actually passed to createChart, sized to fill that wrapper. */
    width: 100%;
    height: 100%;
}

.genealogie-container svg {
    /* family-chart sizes its internal drawing to its container but never sets
       the <svg> element's own box -- without this it falls back to the
       browser's 300x150 default, showing only a tiny cropped corner of the
       (correctly laid out) tree. */
    width: 100%;
    height: 100%;
    display: block;
}

.genealogie-layout { gap: var(--fb-sp-4); }

.genealogie-branch-picker { max-width: 400px; }

.genealogie-search { position: relative; }

.genealogie-detail {
    flex: 0 0 320px;
    padding: var(--fb-sp-4);
    background: var(--fb-card);
    border: 1px solid var(--fb-border);
    border-radius: var(--fb-radius);
    box-shadow: var(--fb-shadow-1);
    align-self: flex-start;
}

/* Fullscreen toggle (6.4): a CSS fixed overlay rather than the native
   Fullscreen API or a Bootstrap modal -- no permission/gesture dance, and it
   keeps the toolbar (search/export/branch-picker) visible and usable. */
.genealogie-panel--fullscreen {
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: var(--fb-parchment);
    padding: var(--fb-sp-3);
    display: flex;
    flex-direction: column;
}

.genealogie-panel--fullscreen .genealogie-layout {
    flex: 1 1 auto;
    min-height: 0;
}

.genealogie-panel--fullscreen .genealogie-container {
    height: auto;
    flex: 1 1 auto;
    min-height: 0;
    margin-top: 0;
}

/* The detail panel floats over the tree instead of taking its own column, so
   the tree can span the full fullscreen width beneath it while the panel
   stays reachable on card click. Overrides Bootstrap's d-none/d-lg-block
   display utilities via higher specificity, not by touching that class
   string (annuaire/tests/test_views_family_tree.py asserts on it verbatim). */
.genealogie-panel--fullscreen .genealogie-detail {
    position: absolute;
    top: 76px;
    right: 24px;
    width: 320px;
    max-height: calc(100% - 100px);
    overflow-y: auto;
    margin-top: 0;
    background: var(--fb-card);
    box-shadow: var(--fb-shadow-3);
    z-index: 1;
}

.genealogie-panel--fullscreen .genealogie-detail.d-none { display: block !important; }

.genealogie-detail-header {
    display: flex;
    align-items: center;
    gap: var(--fb-sp-3);
}

.genealogie-detail-avatar {
    width: 4em;
    height: 4em;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid var(--fb-border);
}

.genealogie-chip { margin: 0 4px 4px 0; }

/* Généalogie — surcharges du thème sombre de family-chart (vendored
   family-chart.css: it ships a dark UI theme with white text/edges, but the
   app renders it on our own card ground. Never edit the vendor CSS
   directly -- override the specific selectors here instead. Selectors below
   match family-chart.css as vendored at v1 (see annuaire/static/vendor/family-chart/). */

/* Edges between nodes are drawn stroke:#fff by the vendor JS -- invisible on
   a light background. */
.f3 .link {
    stroke: var(--fb-bark);
    stroke-width: 1.5px;
}

/* Search autocomplete: vendor sets a dark background but never sets a text
   color, so text inherits Bootstrap's near-black body color -- black on
   near-black. Re-theme the whole widget to our palette instead of
   touching --background-color/--text-color, which are load-bearing for the
   person cards' own white-on-translucent-plate labels. Scoped to
   .genealogie-search (the toolbar mount point, see family_tree.js's
   setPersonDropdown cont option) rather than .f3 -- the widget now lives
   outside the chart container, where the vendor's own --background-color/
   --text-color custom properties (declared on .f3) wouldn't resolve either. */
.genealogie-search .f3-autocomplete input,
.genealogie-search .f3-autocomplete-items,
.genealogie-search .f3-autocomplete-item > div {
    background-color: var(--fb-card);
    color: var(--fb-ink);
    border-color: var(--fb-border-strong);
}

.genealogie-search .f3-autocomplete-item > div:hover,
.genealogie-search .f3-autocomplete-item.f3-selected > div {
    background-color: var(--fb-ember);
    color: var(--fb-card);
}

.genealogie-search .f3-autocomplete-toggle { color: var(--fb-ink); }

.genealogie-search .f3-autocomplete input:focus { outline: 2px solid var(--fb-alpenglow); }

/* The vendor result list is in normal flow with a max-height transition --
   harmless inside the old absolutely-positioned .f3-nav-cont overlay, but in
   the toolbar's normal-flow row it would grow the row and shove the chart
   down on every keystroke without this. */
.genealogie-search .f3-autocomplete-items {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    z-index: 5;
}

/* Long French names get hard-clipped by the vendor's single-line label.
   max-width is a CSS custom property set from family_tree.js (LABEL_MAX_WIDTH,
   itself derived from CARD_X_SPACING there) -- invariant: label max-width
   <= card x-spacing - 10px, or adjacent labels overlap (labels are centered
   and overflow symmetrically past the card).
   width: max-content is required alongside max-width: the vendor positions
   this label with only `left: 50%` (no `right`), so as an absolutely
   positioned box with width left `auto`, the browser's shrink-to-fit sizing
   derives its width from that offset geometry (~50% of the card) instead of
   from max-width, collapsing every label to a near-single-character column.
   Giving it an explicit (non-auto) width opts out of that offset-based
   sizing -- max-width then clamps it and normal wrapping proceeds as
   intended. */
.f3 div.card-image-circle div.card-label {
    white-space: normal;
    width: max-content;
    max-width: var(--genealogie-label-max-width, 150px);
    overflow-wrap: anywhere;
}

/* #81: family_tree.js stamps every node with gender='M' as a layout-only hint
   for the vendor's spouse-side/children-sort logic (never a real, displayed,
   or persisted value -- see the comment at that stamp). Without this, every
   avatar ring would render the vendor's --male-color instead of staying
   neutral. Must not use the .card-genderless selector -- that override was
   deliberately removed as dead code (#59/#80) and stays removed; this
   neutralizes card-male instead. */
.f3 div.card-male .card-inner,
.f3 div.card-male .person-icon svg {
    background-color: var(--genderless-color);
}

/* Deceased-member styling (Phase 2, #104): family_tree.js stamps .card--deceased on
   the outer .card via chart.setAfterUpdate() (re-stamped on every re-render, since
   family-chart rebuilds .card_cont on each updateMainId/branch change). The .card
   itself has no image, so the filter/border apply one level down at
   .card-image-circle -- already the vendor's circular avatar wrapper. */
.f3 div.card.card--deceased div.card-image-circle {
    border: 2px solid var(--fb-ink);
}

.f3 div.card.card--deceased div.card-image-circle img { filter: grayscale(1); }

/* #121: family-chart names its node wrapper class="card", which collides with
   Bootstrap's own component of that name (loaded globally in base.html) --
   its background, border and radius paint a filled rounded square behind
   every circular node. Only those three are neutralized: the vendor's layout
   has always run with Bootstrap's flex in effect, so touching display here
   would change behaviour that currently works. */
.f3 div.card {
    background: none;
    border: 0;
    border-radius: 0;
}

/* --- Mobile: give the tree the screen (#125) ----------------------------- */
/* Two things were taking the space. The toolbar's three long captions wrap it
   onto several rows, so below this width the buttons are icon-only (the text
   stays in the DOM and in aria-label -- see genealogie.html). And the height
   was in vh, which resolves against the *largest* viewport, so the bottom of
   the chart sat underneath the mobile browser's own bar; dvh follows the bar
   as it collapses. The subtraction is the page chrome above the chart
   (top bar, page head, toolbar) -- an estimate, and min-height still floors it.
   The vh line is declared first as the fallback for browsers without dvh. */
@media (max-width: 991.98px) {
    .genealogie-toolbar__label { display: none; }

    .genealogie-container {
        height: 70vh;
        height: calc(100dvh - 13rem);
    }
}

/* =========================================================================
   5. Person / address pickers
   ========================================================================= */

.person-picker,
.address-picker { position: relative; }

.person-picker-results,
.address-picker-results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 10;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--fb-card);
    border: 1px solid var(--fb-border-strong);
    border-radius: 0 0 var(--fb-radius-sm) var(--fb-radius-sm);
    max-height: 240px;
    overflow-y: auto;
    box-shadow: var(--fb-shadow-2);
}

.person-picker-inline {
    width: 260px;
    max-width: 100%;
}

.person-picker-result,
.person-picker-empty,
.address-picker-result,
.address-picker-empty {
    padding: var(--fb-sp-2) var(--fb-sp-3);
    cursor: pointer;
}

.person-picker-empty,
.address-picker-empty {
    color: var(--fb-bark);
    cursor: default;
    font-style: italic;
}

.address-picker-context {
    display: block;
    color: var(--fb-bark);
}

.person-picker-result:hover,
.person-picker-result.highlighted,
.address-picker-result:hover,
.address-picker-result.highlighted {
    background: var(--fb-ember-soft);
    color: var(--fb-ember);
}

.person-picker-badge,
.attachment-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--fb-sp-1);
    font-size: var(--fb-text-sm);
    padding: var(--fb-sp-1) var(--fb-sp-2);
}

.person-picker-badge .btn-close,
.attachment-badge .btn-close { font-size: 0.65rem; }

.attachment-badge-caption {
    max-width: 200px;
    height: auto;
}

.attachment-badge-edit {
    text-decoration: none;
    line-height: 1;
}

/* =========================================================================
   6. Markdown body and editor
   ========================================================================= */

.markdown-body { overflow-wrap: break-word; }

.markdown-body > *:first-child { margin-top: 0; }
.markdown-body > *:last-child { margin-bottom: 0; }

.markdown-body img {
    max-width: 100%;
    height: auto;
    border-radius: var(--fb-radius-sm);
}

.markdown-body pre {
    padding: var(--fb-sp-2) var(--fb-sp-3);
    background: var(--fb-parchment);
    border: 1px solid var(--fb-border);
    border-radius: var(--fb-radius-sm);
    overflow-x: auto;
}

.markdown-body code {
    background: var(--fb-parchment);
    border-radius: 3px;
    padding: 1px 4px;
    color: var(--fb-ember);
}

.markdown-body pre code {
    background: none;
    padding: 0;
    color: inherit;
}

.markdown-body table {
    border-collapse: collapse;
    margin-bottom: var(--fb-sp-4);
}

.markdown-body th,
.markdown-body td {
    border: 1px solid var(--fb-border);
    padding: var(--fb-sp-1) var(--fb-sp-2);
}

.markdown-body th { background: var(--fb-panel); }

.markdown-body blockquote {
    margin: var(--fb-sp-4) 0;
    padding-left: var(--fb-sp-4);
    border-left: 3px solid var(--fb-alpenglow);
    color: var(--fb-bark);
}

/* Éditeur Markdown */

.markdown-editor .nav-tabs { margin-bottom: var(--fb-sp-2); }

.markdown-editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 6px;
}

.markdown-editor-toolbar-btn {
    min-width: 32px;
    padding: 2px var(--fb-sp-2);
    border: 1px solid var(--fb-border-strong);
    border-radius: var(--fb-radius-sm);
    background: var(--fb-card);
    color: var(--fb-ink);
    line-height: 1.4;
    cursor: pointer;
}

.markdown-editor-toolbar-btn:hover {
    background: var(--fb-panel);
    border-color: var(--fb-alpenglow);
    color: var(--fb-ember);
}

.markdown-editor-preview {
    min-height: 100px;
    max-height: 400px;
    overflow-y: auto;
    padding: var(--fb-sp-2) var(--fb-sp-3);
    background: var(--fb-card);
    border: 1px solid var(--fb-border-strong);
    border-radius: var(--fb-radius-sm);
}

.markdown-editor-empty { margin: 0; }

/* =========================================================================
   7. Document viewer (pdf.js and images)
   ========================================================================= */

.document-viewer { margin-bottom: var(--fb-sp-5); }

.document-viewer-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--fb-sp-2);
    flex-wrap: wrap;
    padding: var(--fb-sp-3);
    background: var(--fb-card);
    border: 1px solid var(--fb-border);
    border-bottom: 0;
    border-radius: var(--fb-radius) var(--fb-radius) 0 0;
}

.document-viewer-caption { font-weight: 600; }

.document-viewer-actions {
    display: flex;
    align-items: center;
    gap: var(--fb-sp-2);
    flex-wrap: wrap;
}

.document-viewer-stage {
    height: 70vh;
    max-width: 100%;
    min-height: 320px;
    overflow: auto;
    overflow-anchor: none;
    background: var(--fb-parchment);
    border: 1px solid var(--fb-border);
    border-radius: 0 0 var(--fb-radius) var(--fb-radius);
    display: flex;
    flex-direction: column;
    /* `safe` keeps content centered when it fits, but falls back to flex-start once
       it overflows -- plain `center` in a *scrollable* flex container makes the
       overflow above the visible area unreachable (scrollTop can't go negative),
       which is what made the PDF viewer open on a far page with its first pages
       permanently clipped (#109). Also relied on by 4.2's zoomed-image panning. */
    align-items: safe center;
    justify-content: safe center;
    padding: var(--fb-sp-3);
}

/* Multi-image carousel (#101): one image at a time on mobile (each strip item takes
   the stage's full width), several on desktop -- a plain CSS scroll-snap strip rather
   than a JS carousel library, so touch swipe is native and there's no extra dependency. */
.document-viewer-stage.document-viewer-strip {
    flex-direction: row;
    align-items: stretch;
    justify-content: flex-start;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    gap: var(--fb-sp-3);
}

.document-viewer-strip-item {
    flex: 0 0 100%;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

@media (min-width: 992px) {
    .document-viewer-strip-item { flex: 0 0 calc(33.333% - 8px); }
}

.document-viewer-strip-caption {
    margin-top: var(--fb-sp-2);
    font-size: var(--fb-text-sm);
    color: var(--fb-bark);
    text-align: center;
}

/* Zoom (#101): click-to-toggle between "fit" (default, shrunk to its container) and
   "1:1" (natural size, scrollable via .document-viewer-stage's own overflow: auto).
   touch-action leaves pinch-zoom to the browser rather than a custom gesture handler. */
.document-viewer-zoomable {
    cursor: zoom-in;
    touch-action: pinch-zoom;
}

.document-viewer-zoomable--zoomed { cursor: zoom-out; }

/* Fullscreen: a CSS fixed overlay rather than the native Fullscreen API, matching
   .genealogie-panel--fullscreen's documented rationale (no permission/gesture dance).
   z-index 1050 sits above the mobile offcanvas menu toggle (z-index 1045). */
.document-viewer--fullscreen {
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: var(--fb-parchment);
    padding: var(--fb-sp-3);
    margin: 0;
    display: flex;
    flex-direction: column;
}

.document-viewer--fullscreen .document-viewer-stage {
    flex: 1;
    height: auto;
    min-height: 0;
}

/* Close button only makes sense once fullscreen has replaced the whole publication
   view (#131) -- inline, the viewer already sits in the page, so there's nothing to
   return to. The enter-fullscreen button hides in turn so the toolbar doesn't carry
   two exit-shaped controls at once. */
.document-viewer-close-btn { display: none; }
.document-viewer--fullscreen .document-viewer-close-btn { display: inline-block; }
.document-viewer--fullscreen .document-viewer-fullscreen-btn { display: none; }

.document-file-preview-image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

/* Two-class selector to out-specificity .document-file-preview-image above (#111) --
   the zoom toggle class alone used to lose to it since both were single-class rules
   and this one came first in source order. `flex: none` stops the column flex
   container in .document-viewer-stage from shrinking the now-unclamped image back
   down to fit. */
.document-file-preview-image.document-viewer-zoomable--zoomed {
    max-width: none;
    max-height: none;
    width: auto;
    height: auto;
    flex: none;
}

/* Fullscreen multi-image carousel (#111): one image at a time, regardless of the
   desktop 3-up strip above -- overrides .document-viewer-strip-item's flex basis and
   switches the strip's scroll-snap from "proximity" (used for casual browsing) to
   "mandatory" (locks to exactly one image per swipe/scroll) while in fullscreen. */
.document-viewer--fullscreen .document-viewer-strip-item { flex: 0 0 100%; }

.document-viewer--fullscreen .document-viewer-strip { scroll-snap-type: x mandatory; }

.document-viewer-carousel-counter {
    font-size: var(--fb-text-sm);
    color: var(--fb-bark);
    align-self: center;
}

.document-viewer-pdf-pages { width: 100%; }

.document-viewer-pdf-page-wrap {
    display: block;
    /* Sized in CSS pixels by document_viewer.js, mirroring the canvas's own box
       exactly -- both the visual chrome below and the link overlay's percentage-based
       positions (#146) depend on this wrapper matching the canvas pixel-for-pixel. */
    position: relative;
    max-width: none;
    flex: none;
    margin: 0 auto var(--fb-sp-3);
    box-shadow: var(--fb-shadow-2);
    background: var(--fb-card);
}

.document-viewer-pdf-page {
    display: block;
    /* Sized in CSS pixels by document_viewer.js, independently of the canvas backing
       store: the backing store carries devicePixelRatio * zoom real pixels (#109), and
       an off-screen page's store is collapsed to 1x1 to reclaim memory without the
       page losing its place. A max-width fit rule here would override both. */
    max-width: none;
    flex: none;
}

/* Hyperlinks embedded in the PDF (#146) -- a transparent overlay anchor per link
   annotation, positioned by buildLinkOverlay() in document_viewer.js. */
.document-viewer-pdf-link {
    position: absolute;
    display: block;
    cursor: pointer;
}

.document-viewer-pdf-link:hover,
.document-viewer-pdf-link:focus-visible {
    outline: 2px solid var(--fb-alpenglow);
    outline-offset: 1px;
}

.document-viewer-zoom {
    display: flex;
    align-items: center;
    gap: 4px;
}

.document-viewer-zoom-level {
    font-size: var(--fb-text-sm);
    color: var(--fb-bark);
    min-width: 48px;
    text-align: center;
}

.document-file { margin-bottom: var(--fb-sp-3); }

/* =========================================================================
   8. Place placeholder
   -------------------------------------------------------------------------
   Was a 🏔️ emoji, which renders as a tofu box wherever the system has no
   emoji font -- already visible in the project's own .shots/01-landing.png.
   It is now the ridge, on the sky band. See annuaire/templates/annuaire/_ridge.html.
   ========================================================================= */

.place-photo-placeholder {
    background: var(--fb-sky);
    border-radius: var(--fb-radius-sm);
    display: flex;
    align-items: flex-end;
    overflow: hidden;
    width: 100%;
    aspect-ratio: 3 / 2;
}

.place-photo-placeholder.large {
    width: 100%;
    aspect-ratio: 3 / 2;
}

.place-photo-placeholder.mini {
    width: 54px;
    height: 36px;
    aspect-ratio: auto;
}

/* =========================================================================
   9. Photothèque
   ========================================================================= */

.photo-upload-list {
    list-style: none;
    margin: var(--fb-sp-3) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--fb-sp-2);
}

.photo-upload-row {
    display: flex;
    align-items: center;
    gap: var(--fb-sp-3);
}

.photo-upload-row__thumbnail {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: var(--fb-radius-sm);
    flex-shrink: 0;
}

.photo-upload-row__name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.photo-upload-row__progress {
    flex: 0 0 120px;
    width: 120px;
}

.photo-upload-row--done .photo-upload-row__status { color: var(--fb-success); }
.photo-upload-row--error .photo-upload-row__status { color: var(--fb-danger); }

/* Lightbox (#album grid doubling as the document-viewer carousel strip, extended
   rather than duplicated -- see documents/static/js/document_viewer.js). Only the
   *normal* (non-fullscreen) layout differs from the document/publication viewers: a
   grid, since an album is browsed as a grid, not swiped one at a time, until the
   lightbox (fullscreen) actually opens. Guarded with :not(.document-viewer--fullscreen)
   rather than relying on selector specificity to beat the existing fullscreen rules
   below -- once --fullscreen is toggled on, these stop matching entirely and every
   pre-existing fullscreen rule applies completely unmodified. */
.document-viewer--gallery:not(.document-viewer--fullscreen) .document-viewer-stage.document-viewer-strip {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: var(--fb-sp-3);
    overflow-x: visible;
    height: auto;
}

.document-viewer--gallery:not(.document-viewer--fullscreen) .document-viewer-strip-item {
    flex: none;
}

.document-viewer--gallery:not(.document-viewer--fullscreen) .document-viewer-strip-item img {
    aspect-ratio: 1 / 1;
    width: 100%;
    max-width: 100%;
    max-height: none;
    object-fit: cover;
}

/* =========================================================================
   10. Unified calendar
   -------------------------------------------------------------------------
   Type colours are deliberately distinct from section 2's presence-bar
   colours (--fb-bar-past/current/future = bark/spruce/ember) -- those encode
   a *time-relative* state (past/current/future) within one type (presence),
   while these three encode *type* (event/presence/birthday) within one
   month. Every chip/dot carries a word label too (docs/design_system.md's
   colour-plus-label rule): never colour alone.
   ========================================================================= */

.unified-calendar-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--fb-sp-2);
    margin-bottom: var(--fb-sp-2);
}

.cal-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--fb-sp-2);
    margin-bottom: var(--fb-sp-3);
}

.cal-filter-chip {
    border: 1px solid var(--fb-border-strong);
    border-radius: var(--fb-radius-pill);
    background: var(--fb-card);
    color: var(--fb-bark);
    padding: 2px var(--fb-sp-3);
    font-size: var(--fb-text-sm);
    cursor: pointer;
}

.cal-filter-chip.active { color: var(--fb-card); }
.cal-filter-chip.cal-type-event.active { background: var(--fb-info); border-color: var(--fb-info); }
.cal-filter-chip.cal-type-presence.active { background: var(--fb-slate-mist); border-color: var(--fb-slate-mist); }
.cal-filter-chip.cal-type-birthday.active { background: var(--fb-gold); border-color: var(--fb-gold); }

.cal-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 2px;
}

.cal-dot.cal-type-event { background: var(--fb-info); }
.cal-dot.cal-type-presence { background: var(--fb-slate-mist); }
.cal-dot.cal-type-birthday { background: var(--fb-gold); }

.cal-chip {
    display: inline-block;
    border-radius: var(--fb-radius-pill);
    padding: 1px var(--fb-sp-2);
    font-size: var(--fb-text-xs);
    color: var(--fb-card);
    margin-right: var(--fb-sp-2);
}

.cal-chip.cal-type-event { background: var(--fb-info); }
.cal-chip.cal-type-presence { background: var(--fb-slate-mist); }
.cal-chip.cal-type-birthday { background: var(--fb-gold); }

.unified-calendar-month {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 1px;
    background: var(--fb-border);
    border: 1px solid var(--fb-border);
    border-radius: var(--fb-radius);
    overflow: hidden;
    margin-bottom: var(--fb-sp-4);
}

.cal-month-cell {
    background: var(--fb-card);
    min-height: 64px;
    padding: 4px 6px;
}

.cal-month-cell--empty { background: var(--fb-parchment); }

.cal-month-cell--today {
    background: var(--fb-warning-soft);
}

.cal-month-cell__day {
    display: block;
    font-size: var(--fb-text-xs);
    color: var(--fb-bark);
}

.cal-month-cell__dots { margin-top: 4px; }

.unified-calendar-agenda {
    display: grid;
    gap: var(--fb-sp-2);
}

.cal-agenda-item {
    display: flex;
    align-items: center;
    gap: var(--fb-sp-2);
    padding: var(--fb-sp-2);
    border: 1px solid var(--fb-border);
    border-radius: var(--fb-radius);
    background: var(--fb-card);
    color: var(--fb-ink);
}

.cal-agenda-item__title { flex: 1; }

.cal-subscribe { margin-bottom: var(--fb-sp-4); }

.cal-subscribe-row {
    display: flex;
    gap: var(--fb-sp-2);
    align-items: center;
}

.cal-subscribe-row input { flex: 1; }
