/* =========================================================================
   Laboratoire Jean Perrin — main stylesheet
   Editorial-inspired responsive layout (sidebar + content).
   Light theme is the default; dark theme via [data-theme="dark"].
   Brand colours: primary #1C69A8, light #9dc7ec.
   ========================================================================= */

:root {
    --brand: #1C69A8;
    --brand-light: #9dc7ec;
    --brand-dark: #144d7c;
    /* Text drawn on top of a --brand fill. */
    --on-brand: #ffffff;

    --bg: #ffffff;
    --bg-alt: #f4f6f8;
    --sidebar-bg: #f4f6f8;
    --text: #2b2f33;
    --text-muted: #6b7177;
    --border: #e1e5ea;
    --card-bg: #ffffff;
    --shadow: 0 1px 3px rgba(20, 40, 70, 0.08), 0 6px 18px rgba(20, 40, 70, 0.06);

    /* Text colours an editor may pick, one token per theme so that a colour
       chosen on the light site stays readable on the dark one. Names, never
       hexadecimals, in the content itself. */
    --ink-red: #a83232;
    --ink-green: #1f6b3c;
    --ink-amber: #8a5a0b;

    /* An archived page, wherever the site has to say so: the badge in the tree
       and the banner an administrator gets on the page itself. Amber, filled,
       and dark enough on its own ground to be read as text. */
    --archived-bg: #fdf1dc;
    --archived-ink: #7a4e08;

    /* The colour of the editing tools. Deliberately not the site's blue: a tool
       acts on the page, it is not part of it, and the two must not be read as
       the same thing. Teal at #2A9689 clears the 3:1 that a non-text control
       needs against white. */
    /* The motto of the home page. Not the accent blue: on a line of text it
       reads as a link that does not answer. A deep petrol green instead —
       darker and pulled towards olive, a cousin of the tool colour rather than
       of the links. 9:1 against white, so it is a text colour and not a
       decoration. */
    --motto: #2F4F49;
    --motto-mark: #b9cdc7;

    --tool: #2A9689;
    --tool-soft: #e6f4f2;
    --tool-border: #b3ded7;
    --tool-on: #ffffff;
    --tool-danger: #a83232;

    --sidebar-width: 14.7rem;
    --radius: 0.5rem;
    --content-max: 64rem;
    --font: "Helvetica Neue", Arial, "Segoe UI", system-ui, sans-serif;
}

[data-theme="dark"] {
    --bg: #14181c;
    --bg-alt: #1b2127;
    --sidebar-bg: #11161b;
    --text: #e6e9ec;
    --text-muted: #9aa3ab;
    --border: #2a323a;
    --card-bg: #1b2127;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 6px 18px rgba(0, 0, 0, 0.35);
    /* PROJECT.md: the accent is #1C69A8 in light and #9dc7ec in dark. A light
       accent needs dark text on top of it, hence --on-brand. */
    --brand: #9dc7ec;
    --brand-light: #2c4760;
    --brand-dark: #c3ddf4;
    --on-brand: #14181c;
    /* Lightened for a dark ground; the light values would be near-black. */
    --ink-red: #ef9a95;
    --ink-green: #a7d8bb;
    --ink-amber: #e6b566;
    --archived-bg: #3a2e18;
    --archived-ink: #f0c987;
    /* Lightened for a dark ground, where #2A9689 reads as a hole. */
    /* On a dark ground the same green would be a hole; lifted, and the mark
       kept low enough not to shout. */
    --motto: #9ec9bf;
    --motto-mark: #2f4a45;

    --tool: #56c6b5;
    --tool-soft: #1c2f2d;
    --tool-border: #2f4d49;
    --tool-on: #10201e;
    --tool-danger: #ef9a95;
}

/* One page carries both readings of anything the theme changes — the mark in
   the sidebar, the glyph of the toggle — and the stylesheet shows the one that
   applies. The theme is switched by flipping the attribute on <html>
   (static/js/theme.js), so anything the server had chosen would be one click
   late. `data-theme` is always written, light or dark, so exactly one shows.

   ⚠️ `!important`, deliberately. This is a utility whose whole job is to hide,
   and the elements it hides are inside components that set `display` on them:
   `.slider .action-icons .icon-btn svg` and `.docs-brand img` are both more
   specific than a theme attribute plus a class, so without it the page showed
   the sun *and* the moon, and both logos. Any component rule added later would
   reopen the same hole, silently. */
[data-theme="light"] .theme-only-dark,
[data-theme="dark"] .theme-only-light { display: none !important; }

/* No web font is loaded any more. Rouge Script was here for the motto of the
   home page; the motto is gone and the quotation that replaced it is set in the
   text face of the site, which reads better at the length of a sentence. The
   file itself stays in ../fonts (with its licence) in case the script is wanted
   again — declaring a face nothing uses would only invite the question. */

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 16px;
    line-height: 1.65;
}

.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

img { max-width: 100%; height: auto; }

.skip-link {
    position: absolute;
    left: -999px;
    top: 0;
    background: var(--brand);
    color: var(--on-brand);
    padding: 0.5rem 1rem;
    z-index: 1000;
}
.skip-link:focus { left: 0; }

/* ---------- Layout -------------------------------------------------------- */
#layout { display: flex; min-height: 100vh; }

#sidebar {
    width: var(--sidebar-width);
    flex: 0 0 var(--sidebar-width);
    background: var(--sidebar-bg);
    border-right: 1px solid var(--border);
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
}

.sidebar-inner {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    padding: 1.75rem 1.5rem;
    min-height: 100%;
}

#content { flex: 1 1 auto; min-width: 0; }

.wrapper {
    max-width: var(--content-max);
    margin: 0 auto;
    padding: 2.5rem 2rem 4rem;
}
/* The 2.5 rem above the content sit under the breadcrumb. The home page has
   none — `.page-header:empty` is hidden — so that space fell between the
   banner and the motto and read as a hole. The adjacent sibling is exactly
   « the content of a page without a breadcrumb ». */
.page-header:empty + .wrapper { padding-top: 1.1rem; }

/* ---------- Sidebar header & actions -------------------------------------- */
.sidebar-header {
    display: flex;
    justify-content: center;
    padding: 0.5rem 0 0.25rem;
}

.brand-logo { width: 14rem; max-width: 100%; height: auto; }

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    height: 2rem;
    padding: 0 0.4rem;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--card-bg);
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1;
}
.icon-btn:hover { color: var(--brand); border-color: var(--brand-light); text-decoration: none; }

/* ---------- Sidebar search (minimised) ------------------------------------ */
.sidebar-search {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    border-bottom: 1px solid var(--border);
    padding-bottom: 0.35rem;
}
.sidebar-search input {
    flex: 1 1 auto;
    width: 100%;
    padding: 0.35rem 0.1rem;
    border: none;
    background: transparent;
    color: var(--text);
    font-size: 0.95rem;
}
.sidebar-search input:focus { outline: none; }
.sidebar-search button {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}
.sidebar-search:focus-within { border-color: var(--brand); }
.sidebar-search:focus-within button { color: var(--brand); }

/* ---------- Sidebar nav --------------------------------------------------- */
.sidebar-nav ul { list-style: none; margin: 0; padding: 0; }
.sidebar-nav li { border-bottom: 1px solid var(--border); }
.sidebar-nav a {
    display: block;
    padding: 0.6rem 0.25rem;
    color: var(--text);
    font-weight: 500;
}
.sidebar-nav a:hover { color: var(--brand); text-decoration: none; }
.menu-counter {
    display: inline-block;
    margin-left: 0.4rem;
    padding: 0 0.4rem;
    border-radius: 999px;
    background: var(--bg-alt);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.5;
}
.sidebar-nav a:hover .menu-counter { color: var(--brand); }

/* Accordion for the rubrics that have sub-pages. The summary carries both the
   link to the rubric and the caret; the browser's own marker is dropped. */
.nav-group > summary { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; cursor: pointer; list-style: none; }
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary > a { flex: 1; min-width: 0; }
.nav-caret { display: flex; padding: 0.2rem; color: var(--text-muted); transition: transform 0.15s ease; }
.nav-group[open] > summary .nav-caret { transform: rotate(90deg); }
.nav-group > summary:hover .nav-caret { color: var(--brand); }
.sidebar-nav .nav-subnav { margin: 0 0 0.5rem 0.25rem; padding: 0 0 0 0.75rem; border-left: 2px solid var(--border); }
.sidebar-nav .nav-subnav li { border-bottom: 0; }
.sidebar-nav .nav-subnav a { padding: 0.32rem 0.25rem; color: var(--text-muted); font-size: 0.88rem; font-weight: 400; }
.sidebar-nav .nav-subnav a:hover, .sidebar-nav .nav-subnav a.is-current { color: var(--brand); }
.sidebar-nav .nav-subnav a.is-current { font-weight: 600; }

/* ---------- Sidebar visit block ------------------------------------------- */
.sidebar-visit { margin-top: auto; display: flex; flex-direction: column; gap: 0.75rem; }
.sidebar-visit h2 { font-size: 1rem; margin: 0; color: var(--text); }
.sidebar-visit address {
    font-style: normal;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text-muted);
}
.sidebar-visit address a { color: var(--text-muted); }
.sidebar-visit address a:hover { color: var(--brand); }

.visit-btn {
    display: block;
    text-align: center;
    padding: 0.55rem 1rem;
    background: var(--brand);
    color: var(--on-brand);
    border-radius: var(--radius);
    font-weight: 600;
    font-size: 0.9rem;
}
.visit-btn:hover { background: var(--brand-dark); text-decoration: none; }

/* ---------- Banner arrows ------------------------------------------------- */
.slider-banner { position: relative; }
.slider-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    width: 2.2rem;
    height: 2.2rem;
    border: 0;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.35);
    color: #fff;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.2s ease;
}
.slider-banner:hover .slider-arrow,
.slider-arrow:focus-visible { opacity: 1; }
.slider-arrow:hover { background: rgba(0, 0, 0, 0.55); }
.slider-arrow-prev { left: 0.6rem; }
.slider-arrow-next { right: 0.6rem; }
@media (prefers-reduced-motion: reduce) { .slider-arrow { transition: none; } }

.field-help { margin: 0.3rem 0 0; font-size: 0.82rem; color: var(--text-muted); }

/* ---------- Profile extras ------------------------------------------------ */
.profile-themes { list-style: none; margin: 0.4rem 0 0; padding: 0; font-size: 0.9rem; }
.profile-themes li { padding: 0.2rem 0; }
.profile-publications { padding-left: 1.1rem; font-size: 0.92rem; }
.profile-publications li { margin-bottom: 0.45rem; }
.profile-publications span { display: block; font-size: 0.85rem; }

/* The campus plan is a printed document with its own colours: it keeps a white
   ground in both themes rather than being inverted into an unreadable map. */
.access-map {
    position: relative;
    display: block;
    width: 100%;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: #fff;
}
.access-map img { display: block; width: 100%; height: auto; }
.access-map:hover { border-color: var(--brand); }
.access-map-hint {
    position: absolute;
    right: 0.35rem;
    bottom: 0.35rem;
    display: flex;
    padding: 0.25rem;
    border-radius: var(--radius);
    background: var(--brand);
    color: var(--on-brand);
}
/* Sized by the plan itself so it always fits the viewport whole: a fixed width
   made a tall screen cut the rue Jussieu off the bottom. */
.map-dialog {
    width: auto;
    max-width: 94vw;
    max-height: 94vh;
    padding: 0;
    overflow: hidden;
    border: 0;
    border-radius: var(--radius);
    background: #fff;
}
.map-dialog::backdrop { background: rgba(0, 0, 0, 0.65); }
.map-dialog img { display: block; width: auto; height: auto; max-width: 94vw; max-height: 94vh; }
.map-dialog form { margin: 0; }
.map-dialog-close {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--brand);
    color: var(--on-brand);
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}

/* ---------- Sidebar credits ----------------------------------------------- */
.sidebar-credits {
    margin-top: 1.25rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--border);
    font-size: 0.78rem;
    line-height: 1.6;
    color: var(--text-muted);
}
.sidebar-credits p { margin: 0; }
.sidebar-credits a { color: var(--text-muted); }
.sidebar-credits a:hover { color: var(--brand); }

/* ---------- Header / breadcrumb ------------------------------------------- */
.page-header:empty { display: none; }
.page-header { padding: 1.25rem 2rem 0; max-width: var(--content-max); margin: 0 auto; }
.action-icons { display: flex; gap: 0.35rem; }
.breadcrumb { font-size: 0.9rem; color: var(--text-muted); display: flex; gap: 0.5rem; flex-wrap: wrap; }
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: var(--brand); }

/* ---------- Typography ---------------------------------------------------- */
h1 { font-size: 2rem; margin: 0 0 1rem; color: var(--text); }
h2 { font-size: 1.4rem; margin: 2rem 0 0.75rem; color: var(--text); }
.muted, .text-muted { color: var(--text-muted); }

/* ---------- Hero ---------------------------------------------------------- */
/* The head of the home page: the name of the laboratory on one side, the unit
   and its supervising bodies on the other. It carried a motto until the motto
   claimed a field the laboratory does not hold alone; what says something about
   the place now is the Jean Perrin quotation at the foot of the page. */
.hero {
    display: grid;
    /* The name takes what it needs and the unit line takes the rest, against
       the right edge. A fixed ratio broke one or the other depending on the
       width. */
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.4rem 2rem;
    /* Baselines rather than boxes: the name and the unit line are read as one
       line, and centring two texts of different sizes leaves neither aligned. */
    align-items: baseline;
    padding: 1.1rem 0 1.2rem;
    border-bottom: 2px solid var(--brand-light);
    margin-bottom: 1.6rem;
}
/* The name and its stamp sit side by side, and the stamp drops under the name
   when the two no longer fit rather than shrinking it. */
.hero-identity { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.1rem 0.7rem; }
/* A little under the 2 rem of the other pages: the name is already in the logo
   at the top of every page, and here it shares the line with the unit. */
.hero h1 { margin: 0; font-size: 1.75rem; }
.hero-subtitle { margin: 0; color: var(--text-muted); font-size: 1.05rem; text-align: right; }

/* The date stamp. Small, spaced, slightly off-square — a rubber stamp struck
   next to the name, not a label. It is set in the text face: a script at this
   size would be a smudge. The petrol green of the quotation rather than the
   accent blue, so it is never mistaken for a link. */
.hero-since {
    align-self: center;
    padding: 0.12rem 0.55rem;
    border: 1px solid var(--motto-mark);
    border-radius: 999px;
    color: var(--motto);
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    transform: rotate(-2.5deg);
}
@media (max-width: 52rem) {
    /* Side by side, the two columns would each be too narrow to hold a line. */
    .hero { grid-template-columns: minmax(0, 1fr); gap: 0.5rem; }
    .hero-subtitle { text-align: left; }
}

/* ---------- Jean Perrin quotation (home only) ----------------------------- */
/* Under the spotlights, one quotation drawn at random on every visit. The
   laboratory is named after a man who wrote remarkably well and the page ends
   on his voice rather than on a slogan.

   Set in the text face of the site, at reading size: the line the motto used to
   occupy was four words in a script, this is a sentence — it has to be read,
   not admired. The colour and the mark are what set it apart. */
.home-quote {
    padding-top: 1.6rem;
    border-top: 1px solid var(--border);
}
.perrin-quote {
    position: relative;
    /* A measure, not a column: 44 rem holds some 75 signs a line, which is what
       a paragraph of prose wants. Centred under a content column that is
       wider. */
    max-width: 44rem;
    margin: 0 auto;
    /* The words start over the mark rather than after it: the overlap is the
       point, otherwise the mark is just a bullet. */
    padding-left: 2rem;
}
/* Drawn behind the words, which ride over it: both are positioned, so the one
   painted last wins — the pseudo element first, the text after it.

   Times is asked for by name: its opening quotation mark is a pair of heavy
   commas, where Georgia draws two thin tapered strokes that read as an accent
   at this size. */
.perrin-quote::before {
    position: absolute;
    top: -1.9rem;
    left: 0.35rem;
    color: var(--motto-mark);
    content: "\201C";
    font-family: "Times New Roman", Times, serif;
    font-size: 7rem;
    line-height: 1;
}
.perrin-quote-text {
    position: relative;
    margin: 0;
    color: var(--motto);
    font-size: 1.12rem;
    line-height: 1.6;
}
/* The attribution, against the right edge of the measure: the eye leaves the
   last word of the quotation and finds the name below it. The dash is drawn
   here rather than typed in the markup — it is punctuation for the eye, and a
   reader who listens hears the name without it. */
.perrin-quote-credit {
    margin-top: 0.5rem;
    color: var(--text-muted);
    font-size: 0.92rem;
    text-align: right;
}
.perrin-quote-credit::before { content: "\2014\00a0"; }
.perrin-quote-credit a { color: inherit; text-decoration: none; }
/* The link is not advertised: it is the name of the man, and only a reader who
   goes for it learns that it opens his page. */
.perrin-quote-credit a:hover,
.perrin-quote-credit a:focus-visible { color: var(--motto); text-decoration: underline; }
.perrin-quote-source { font-style: italic; }
@media (max-width: 40rem) {
    .perrin-quote { padding-left: 1.3rem; }
    .perrin-quote::before { top: -1.3rem; left: 0.2rem; font-size: 5rem; }
    .perrin-quote-text { font-size: 1.02rem; }
}

/* ---------- Shared carousel dots ------------------------------------------ */
.carousel-dots { display: flex; gap: 0.5rem; }
.carousel-dot {
    width: 0.6rem;
    height: 0.6rem;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--border);
    cursor: pointer;
    transition: background 0.2s ease, transform 0.2s ease;
}
.carousel-dot:hover { background: var(--brand-light); }
.carousel-dot.is-active { background: var(--brand); transform: scale(1.25); }

/* ---------- Banner slider (every page) ------------------------------------ */
.slider { margin: 0; max-width: none; padding: 0.5rem 0 0; }

.slider-topbar {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    margin-bottom: 0.4rem;
    padding: 0 0.75rem;
    min-height: 1.5rem;
}
.slider-dots { flex-wrap: wrap; }
.slider-dots .carousel-dot { width: 0.4rem; height: 0.4rem; }
.slider .action-icons { position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%); }
.slider .action-icons .icon-btn {
    border: none;
    background: transparent;
    min-width: 1.9rem;
    height: 1.9rem;
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 700;
}
.slider .action-icons .icon-btn svg { display: block; }
.slider .action-icons .icon-btn:hover { color: var(--brand); }

.slider-banner {
    position: relative;
    aspect-ratio: 20 / 3;
    overflow: hidden;
    box-shadow: var(--shadow);
    background: var(--bg-alt);
}
.slider-track { position: absolute; inset: 0; }
.slider-slide {
    position: absolute;
    inset: 0;
    display: block;
    opacity: 0;
    transition: opacity 0.6s ease;
    pointer-events: none;
}
.slider-slide.is-active { opacity: 1; pointer-events: auto; }
.slider-slide img { width: 100%; height: 100%; object-fit: cover; }
.slider-caption {
    position: absolute;
    left: 0;
    /* Stay clear of the partner-logo panel anchored to the right. */
    right: 13rem;
    bottom: 0;
    padding: 1.2rem 1.5rem 0.5rem;
    color: #fff;
    font-weight: 600;
    font-size: 1.05rem;
    line-height: 1.25;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}

/* Partner logos overlay: quarter-ellipse in the top-right corner. */
.slider-partners {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    gap: 0.9rem;
    align-items: center;
    padding: 0.7rem 1.3rem 2.2rem 1.9rem;
    background: rgba(255, 255, 255, 0.82);
    border-left: 1px solid rgba(150, 160, 170, 0.45);
    border-bottom: 1px solid rgba(150, 160, 170, 0.45);
    border-bottom-left-radius: 90% 220%;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    z-index: 3;
}
.slider-partners img { height: 2.6rem; width: auto; display: block; }
.slider-partners a { display: inline-flex; }

/* ---------- "Zoom sur" spotlight slideshow (home only) -------------------- */
/* The home page is three boxes of equal height — the introduction, the news,
   the seminars — then the spotlights across the full width. Each box is a
   window on a section rather than a copy of it, and each scrolls inside its
   own frame: the height of the page no longer depends on how much news the
   month happened to bring. */
.home-block { margin-top: 2.5rem; }
.home-columns { display: grid; grid-template-columns: 1fr; gap: 1.25rem; margin-top: 1.75rem; }
.home-col {
    display: flex;
    flex-direction: column;
    /* No fixed height: grid items of one row already stretch to the tallest, so
       the three align on their own and none shows a scrollbar it does not need.
       The ceiling is only there to stop an exceptional month of news from
       stretching the page — that is when scrolling inside becomes the right
       answer. */
    min-height: 16rem;
    max-height: 34rem;
    padding: 1rem 1.15rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--card-bg);
}
.home-col-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.home-col-heading h2 { margin: 0; font-size: 1.1rem; }
.home-col-more { font-size: 0.85rem; white-space: nowrap; }
.home-col-body { flex: 1; min-height: 0; overflow-y: auto; margin-top: 0.6rem; }
.home-col-body.editorial-content p,
.home-col-body.editorial-content p,
.home-col-body .editorial-content p { margin: 0 0 0.7rem; font-size: 0.92rem; line-height: 1.55; }
/* Pinned to the foot of the box, never scrolled away with the text. */
.home-col-footer { margin: 0.6rem 0 0; padding-top: 0.6rem; border-top: 1px solid var(--border); font-size: 0.86rem; }

/* One item, condensed: thumbnail on the left, two lines of text on the right —
   a muted line of date and speaker, then the title. Never more, so a row keeps
   the same height whatever it carries. */
.home-row {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: 0.7rem;
    align-items: center;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border);
    color: inherit;
}
.home-row:last-child { border-bottom: 0; }
.home-row:hover { text-decoration: none; background: var(--bg-alt); }
.home-row .item-thumb,
.home-row-thumb-placeholder { width: 2.9rem; height: 2.9rem; border-radius: 0.25rem; }
.home-row .item-thumb { object-fit: cover; }
/* An item without a picture keeps the column all the same: without it the
   titles of a list would start at two different places. */
.home-row-thumb-placeholder { background: var(--bg-alt); }
.home-row-body { display: block; min-width: 0; }
.home-row-kicker,
.home-row-title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-row-kicker { font-size: 0.78rem; color: var(--text-muted); }
.home-row-title { font-size: 0.92rem; line-height: 1.35; }
.home-row:hover .home-row-title { color: var(--brand); }
.home-row.is-upcoming .home-row-kicker { color: var(--brand); font-weight: 600; }

@media (min-width: 60rem) {
    .home-columns { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.spotlight { margin-top: 2.5rem; }
/* The spotlight heading carries its own "add" control, so the title and the
   icon sit on one line rather than the icon dropping below it. */
.spotlight-heading { display: flex; align-items: center; gap: 0.5rem; }
.spotlight-heading h2 { margin: 0; }

.slideshow {
    /* Width of the caption panel beside the photograph. Read by the grid and by
       the "next" arrow, so moving one moves the other. */
    --zoom-caption: 40%;
    position: relative;
    margin-top: 1rem;
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow);
    border: 1px solid var(--border);
    background: var(--card-bg);
}
/* Photograph and caption side by side, the caption on a plain background.
   It used to be white text laid over the picture: legibility then changed with
   every pixel the line crossed, and no photograph is uniform enough for that.
   Below the picture instead when the screen is too narrow to hold both. */
.slideshow-track { position: relative; height: 26rem; }
.slideshow-slide {
    position: absolute;
    inset: 0;
    margin: 0;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr) auto;
    opacity: 0;
    transition: opacity 0.6s ease;
    pointer-events: none;
}
.slideshow-slide.is-active { opacity: 1; pointer-events: auto; }
.slideshow-slide img { width: 100%; height: 100%; object-fit: cover; }
.slideshow-caption {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 0;
    overflow-y: auto;
    padding: 1.1rem 1.4rem;
    color: var(--text);
    background: var(--card-bg);
    border-top: 1px solid var(--border);
}
.slideshow-caption h3 { margin: 0 0 0.35rem; font-size: 1.2rem; }
.slideshow-caption p { margin: 0; font-size: 0.94rem; line-height: 1.55; }
.slideshow-text p { margin: 0; }
.slideshow-link {
    display: inline-block;
    margin-top: 0.6rem;
    align-self: flex-start;
    color: var(--brand);
    font-weight: 600;
    border-bottom: 1px solid var(--brand-light);
}
.slideshow-link:hover { text-decoration: none; border-color: var(--brand); }

@media (min-width: 60rem) {
    .slideshow-track { height: auto; aspect-ratio: 16 / 6; }
    .slideshow-slide {
        grid-template-columns: minmax(0, calc(100% - var(--zoom-caption))) minmax(0, var(--zoom-caption));
        grid-template-rows: 1fr;
    }
    .slideshow-caption { border-top: 0; border-left: 1px solid var(--border); padding: 1.5rem 1.6rem; }
    /* Both arrows stay over the photograph. Left where it was, the "next" one
       sat on top of the caption and hid the end of every line. The width of the
       panel is a single custom property so the two cannot drift apart. */
    .slideshow .carousel-next { right: calc(var(--zoom-caption) + 0.75rem); }
}

/* Below the pictures, in the ordinary flow. They used to be white dots laid on
   the photograph; the caption is now a plain panel and half of them would have
   fallen on it, invisible. */
.slideshow-dots {
    display: flex;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.7rem 0;
    background: var(--card-bg);
    border-top: 1px solid var(--border);
}
.slideshow-dots .carousel-dot { background: var(--border); }
.slideshow-dots .carousel-dot.is-active { background: var(--brand); }

/* The arrows cross both halves of a slide — the photograph and the plain panel
   — so they carry their own opaque disc rather than relying on either. */
.carousel-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--card-bg);
    color: var(--text);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    box-shadow: var(--shadow);
    transition: background 0.2s ease, color 0.2s ease;
}
.carousel-arrow:hover { background: var(--brand); color: var(--on-brand); border-color: var(--brand); }
.carousel-prev { left: 0.75rem; }
.carousel-next { right: 0.75rem; }

/* ---------- Cards / grids ------------------------------------------------- */
.rubric-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 1.5rem; }
.card {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    box-shadow: var(--shadow);
}

/* ---------- People -------------------------------------------------------- */
/* Directory grouped by research theme, as on the legacy site: a titled block
   per theme, permanent members on the left and everybody else on the right. */
.theme-block { margin-bottom: 2rem; }
.theme-heading {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0 0 0.9rem;
    padding: 0.35rem 0.75rem 0.35rem 0;
    background: var(--bg-alt);
    border-bottom: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 1.1rem;
}
.theme-heading a, .theme-heading span { min-width: 0; color: var(--text); }
.theme-heading a:hover { color: var(--brand); }
.theme-icon { width: 2.5rem; height: 2.5rem; object-fit: cover; border-radius: var(--radius) 0 0 var(--radius); flex: none; }
.theme-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.5rem 1.5rem; align-items: start; }
/* A group without a permanent/non-permanent split flows over the full width:
   support staff, and the alphabetical view. */
.people-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: 0.4rem 1.5rem; align-items: start; }
.theme-column { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
/* Associate researchers close a theme on a line of their own: the grid has two
   tracks, so this third child lands in the left column with nothing facing it.
   The rule above the names says the line is not the continuation of a column. */
.theme-associates { margin-top: 0.6rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.theme-associates-label { margin: 0 0 0.2rem; font-size: 0.82rem; font-weight: 600; color: var(--text-muted); }

.person-row {
    display: grid;
    grid-template-columns: 2.75rem minmax(0, 1fr);
    align-items: center;
    gap: 0.7rem;
    padding: 0;
    background: var(--bg-alt);
    border-radius: var(--radius);
    color: var(--text);
}
.person-row:hover { background: var(--brand-light); text-decoration: none; }
/* Portraits come from three decades of cameras: the grayscale filter is what
   makes the column look like one list rather than a patchwork. The profile
   page shows the same file in colour. */
.person-photo {
    width: 2.75rem;
    height: 2.75rem;
    object-fit: cover;
    border-radius: var(--radius) 0 0 var(--radius);
    filter: grayscale(100%);
}
/* The stand-in portrait is already grey; desaturating it again would do
   nothing, and it must not be dimmed further than a real photograph. */
.person-photo-default { filter: none; }
.person-row-text { display: flex; flex-direction: column; min-width: 0; padding: 0.25rem 0.6rem 0.25rem 0; }
.person-name { font-weight: 500; }
.person-status { color: var(--text-muted); font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Preview of the image already attached, above its replacement field. */
.thumb-preview { margin: 0 0 0.5rem; }
.thumb-preview img { width: 6.25rem; height: 6.25rem; object-fit: cover; border-radius: var(--radius); background: var(--bg-alt); }

/* An address assembled by static/js/protected-email.js. */
.protected-email { overflow-wrap: anywhere; }

/* A member who has left. Amber rather than red: it is a fact about the page,
   not a fault, and the page is worth reading. */
.profile-former {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 0 0 1rem;
    padding: 0.6rem 0.9rem;
    border-left: 3px solid var(--ink-amber);
    border-radius: var(--radius);
    background: var(--bg-alt);
    color: var(--text-muted);
}

/* ---------- Profile ------------------------------------------------------- */
/* Condensed header of a profile, as on the legacy site: portrait on the left,
   then name and address, status and ORCID, and where to find the person. The
   portrait keeps its colours here -- only the directory desaturates. */
.profile-banner {
    display: grid;
    grid-template-columns: 11rem minmax(0, 1fr);
    gap: 1.25rem;
    align-items: start;
    margin-bottom: 1rem;
    padding: 1rem;
    background: var(--bg-alt);
    border-radius: var(--radius);
}
.profile-photo { width: 11rem; height: 11rem; object-fit: cover; border-radius: var(--radius); }
.profile-photo-default { object-fit: contain; background: #e4e8ec; }
.profile-identity { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.profile-line { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.5rem 1rem; }
.profile-line h1 { margin: 0; font-size: 1.6rem; }
.profile-email { font-style: italic; }
.profile-orcid { color: var(--text-muted); font-size: 0.9rem; }
.profile-facts { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.15rem 0.75rem; margin: 0.4rem 0 0; font-size: 0.85rem; }
.profile-facts dt { color: var(--text-muted); font-weight: 600; }
.profile-facts dd { margin: 0; }
.profile-phone { margin-left: 0.6rem; }
.profile-phone::before { content: "\260E\FE0E\00A0"; color: var(--text-muted); }

.profile-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 2rem; }
/* Nothing to put beside the text: the prose takes the width rather than
   leaving a third of the page to an empty column. */
.profile-grid-single { grid-template-columns: minmax(0, 1fr); }
.profile-status { color: var(--text-muted); font-size: 1.1rem; }
.profile-contact { background: var(--bg-alt); border-radius: var(--radius); padding: 1.25rem; align-self: start; }
/* Le titre de l'encart ne rajoute pas sa marge au rembourrage de la boite :
   le « margin-top: 2rem » des h2 de la page s'ajoutait aux 1,25rem de padding,
   ce qui creusait 3,25rem au-dessus de « Thematiques » quand les trois autres
   cotes en avaient 1,25. Regle sur le premier enfant, quel qu'il soit, pour
   qu'un titre remplace un jour par autre chose ne rouvre pas le trou. */
.profile-contact > :first-child { margin-top: 0; }
.profile-contact dt { font-weight: 600; margin-top: 0.6rem; font-size: 0.85rem; color: var(--text-muted); }
.profile-contact dd { margin: 0; }
/* Corridor of a room, secondary to its number. */
.room-corridor { color: var(--text-muted); font-size: 0.85em; }
.room-corridor:not(:empty)::before { content: "\00A0\2014\00A0"; }

/* A sentence of introduction above a listing, in the measure of running prose
   rather than the full width of the screen. */
.page-intro { max-width: 46rem; margin: 0 0 1.2rem; color: var(--text); }

/* ---------- Editorial pages ---------------------------------------------- */
/* Summary of a rubric: one line per page, a small thumbnail on its left, the
   shape the directory gives a member (`.person-row`). It was a grid of cards
   6.25 rem tall, and a rubric holding eight pages filled a screen with them
   before the text of the page had said anything. A summary is a list. */
/* Wider than a column of the directory, which holds names: measured on « La
   recherche », whose pages are titled « Imagerie calcique et comportement du
   poisson zèbre et Danionella Cerebrum ». At 19 rem the content column held
   three of them and every title was cut after four words.

   Columns rather than a grid: a page listing six sub-pages sits next to one
   listing none, and in a grid the empty neighbour is stretched to the height of
   the tall one — the summary was more hole than list. Columns let each entry
   take its own height and the next one follow. `break-inside` keeps an entry
   and its sub-pages together. */
.child-pages { columns: 26rem 2; column-gap: 1.5rem; margin: 0 0 1.5rem; }
.child-page-item { position: relative; min-width: 0; break-inside: avoid; margin-bottom: 0.4rem; }
.child-page-item > a,
.child-page-item > summary > a { display: grid; grid-template-columns: 2.75rem minmax(0, 1fr); align-items: center; gap: 0.7rem; color: var(--text); font-weight: 500; }
/* The ground is on whatever *is* the row — the link alone, or the summary that
   holds the link and its caret — so that a row with sub-pages and a row without
   are the same width and the caret sits inside the tile rather than beside it. */
.child-page-item > a,
.child-page-item > summary { background: var(--bg-alt); border-radius: var(--radius); }
.child-page-item > a:hover,
.child-page-item > summary:hover { background: var(--brand-light); text-decoration: none; }
/* A page holding sub-pages of its own is an accordion, the same one as the left
   menu: the row stays a link and the caret beside it opens the branch. */
.child-page-item > summary { display: flex; align-items: center; gap: 0.15rem; padding-right: 0.4rem; cursor: pointer; list-style: none; }
.child-page-item > summary::-webkit-details-marker { display: none; }
.child-page-item > summary > a { flex: 1; min-width: 0; }
.child-page-item[open] > summary .nav-caret { transform: rotate(90deg); }
.child-page-item > summary:hover .nav-caret { color: var(--brand); }
.child-pages img { width: 2.75rem; height: 2.75rem; object-fit: cover; border-radius: var(--radius) 0 0 var(--radius); background: var(--bg-alt); }
/* One line, cut by an ellipsis: every row is then the same height, which is
   what makes a list of twenty readable. The whole title is in the tooltip and
   in the page the link opens. `a > span` and not `span`: the caret of the
   accordion is a span too, and would inherit this padding. */
.child-pages a > span { padding: 0.3rem 0.6rem 0.3rem 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Fallback tile when an editor provided no illustration. */
.child-page-fallback {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--radius) 0 0 var(--radius);
    background: var(--bg-alt);
    color: var(--brand);
    font-size: 1.15rem;
    font-weight: 700;
    padding: 0;
}

/* Second level of the summary, on a theme root only. Indented past the
   thumbnail: the offset is what says these pages hang from the row above. */
.child-page-subpages { list-style: none; margin: 0.2rem 0 0.5rem 1.35rem; padding: 0.1rem 0 0.1rem 1rem; border-left: 2px solid var(--border); font-size: 0.85rem; }
.child-page-subpages li { padding: 0.08rem 0; }
.child-page-subpages a { color: var(--text-muted); }
.child-page-subpages a:hover { color: var(--brand); }

/* Sub-pages deeper in the tree only need a compact list. */
.child-pages-compact { display: block; margin-bottom: 1.5rem; border-left: 3px solid var(--border); padding-left: 0.9rem; }
.child-pages-compact .child-page-item { border-bottom: none; display: flex; align-items: center; gap: 0.5rem; }
.child-pages-compact .child-page-item > a,
.child-pages-compact .child-page-item > summary { display: inline; background: none; }
.child-pages-compact .child-page-item > summary > a { display: inline; font-weight: 500; }
/* The deep variant has no thumbnail and its link is inline: an ellipsis
   cannot apply to it, so a long title wraps rather than running off the
   side of a telephone. */
.child-pages-compact a > span { padding: 0.15rem 0; overflow: visible; white-space: normal; }
/* Running prose is justified, everywhere it runs: pages, news, seminars, job
   offers, profiles, the home introduction, and the editor itself so that what
   is written is what will be read. `hyphens` is not optional company for it —
   French words are long, and justification without hyphenation tears white
   holes in a narrow column (measured on the home page, P7-11). It needs the
   `lang` attribute, which `base.html` sets on `<html>`.
   Table cells keep their own alignment, set with the table rules below. */
.editorial-content, .profile-body, .ql-editor { text-align: justify; hyphens: auto; }
/* What is not running prose: code must not be hyphenated or stretched, and a
   caption of two lines reads better flush left than justified. */
.editorial-content :is(pre, code, figcaption),
.ql-editor :is(pre, code, figcaption) { text-align: left; hyphens: none; }
/* Character size and colour, written as classes by the editor and read here.
   Sizes are relative, so they follow the base size instead of fixing pixels;
   colours are tokens, so each theme resolves its own.

   The editor selectors carry `.ql-snow` on purpose. Quill ships its own
   `.ql-editor .ql-size-small` and even its own `.ql-color-red`, and its
   stylesheet is loaded after this one (`head_extra` comes after `main.css`),
   so an equal selector would lose and the editor would show a different size
   and a different red from the page it is writing. */
.editorial-content .ql-size-small, .ql-snow .ql-editor .ql-size-small { font-size: .82em; }
.editorial-content .ql-size-large, .ql-snow .ql-editor .ql-size-large { font-size: 1.35em; }
.editorial-content .ql-size-huge, .ql-snow .ql-editor .ql-size-huge { font-size: 1.9em; }
.editorial-content .ql-color-brand, .ql-snow .ql-editor .ql-color-brand { color: var(--brand); }
.editorial-content .ql-color-red, .ql-snow .ql-editor .ql-color-red { color: var(--ink-red); }
.editorial-content .ql-color-green, .ql-snow .ql-editor .ql-color-green { color: var(--ink-green); }
.editorial-content .ql-color-amber, .ql-snow .ql-editor .ql-color-amber { color: var(--ink-amber); }
.editorial-content .ql-color-muted, .ql-snow .ql-editor .ql-color-muted { color: var(--text-muted); }

/* The colour picker's swatches. Quill paints each one by writing
   `style.backgroundColor = <value>` on the element — and « red » and « green »
   happen to be CSS colour keywords, so the swatch showed pure red where the
   text is #a83232. An inline style is only beaten by `!important`; these five
   declarations are the reason that keyword exists. The swatch then shows the
   token the text will actually use. */
/* The label of the picker carried the same lie: Quill paints its little bar by
   writing the value into the element, so it showed **pure red** where the text
   comes out #a83232 — and nothing at all for « brand » or « amber », which are
   not CSS colours. A neutral bar that follows the toolbar tells no story; the
   swatches below show the real inks. */
.ql-snow .ql-color .ql-picker-label .ql-color-label { fill: currentColor !important; stroke: currentColor !important; }
.ql-snow .ql-color .ql-picker-item[data-value="brand"] { background-color: var(--brand) !important; }
.ql-snow .ql-color .ql-picker-item[data-value="red"] { background-color: var(--ink-red) !important; }
.ql-snow .ql-color .ql-picker-item[data-value="green"] { background-color: var(--ink-green) !important; }
.ql-snow .ql-color .ql-picker-item[data-value="amber"] { background-color: var(--ink-amber) !important; }
.ql-snow .ql-color .ql-picker-item[data-value="muted"] { background-color: var(--text-muted) !important; }

.editorial-content { max-width: 48rem; }
.editorial-content img { height: auto; }
.editorial-content::after { display: block; clear: both; content: ""; }

/* Editorial tables and embeds must never widen the page on a phone. */
.editorial-content table {
    display: block;
    width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    font-size: 0.9rem;
}
.editorial-content th,
.editorial-content td {
    padding: 0.4rem 0.7rem;
    border-bottom: 1px solid var(--border);
    text-align: left;
    vertical-align: top;
}
.editorial-content thead th { border-bottom: 2px solid var(--border); white-space: nowrap; }

/* A table built in the editor carries its own layout, like an image does: a
   width in per cent of the column, the same left/centre/right anchoring with
   the text wrapping around it, and one of two rulings — hairlines or none.
   It leaves the scrolling-block behaviour above, which exists for the wide
   tables inherited from the old site: a table whose width is a fraction of
   the column cannot widen a phone, and a block cannot be floated. */
.editorial-content table[data-ljp-size], .editorial-content table[data-ljp-align],
.ql-editor table[data-ljp-size], .ql-editor table[data-ljp-align] {
    display: table;
    max-width: 100%;
    overflow: visible;
}
.editorial-content table[data-ljp-size="25"], .ql-editor table[data-ljp-size="25"] { width: 25%; }
.editorial-content table[data-ljp-size="33"], .ql-editor table[data-ljp-size="33"] { width: 33%; }
.editorial-content table[data-ljp-size="50"], .ql-editor table[data-ljp-size="50"] { width: 50%; }
.editorial-content table[data-ljp-size="75"], .ql-editor table[data-ljp-size="75"] { width: 75%; }
.editorial-content table[data-ljp-size="100"], .ql-editor table[data-ljp-size="100"] { width: 100%; }
.editorial-content table[data-ljp-align="left"], .ql-editor table[data-ljp-align="left"] { float: left; margin: .25rem 1rem .75rem 0; }
.editorial-content table[data-ljp-align="center"], .ql-editor table[data-ljp-align="center"] { float: none; margin: .75rem auto; }
.editorial-content table[data-ljp-align="right"], .ql-editor table[data-ljp-align="right"] { float: right; margin: .25rem 0 .75rem 1rem; }
.editorial-content table[data-ljp-border="thin"] :is(th, td),
.ql-editor table[data-ljp-border="thin"] :is(th, td) { border: 1px solid var(--border); }
.editorial-content table[data-ljp-border="none"] :is(th, td),
.ql-editor table[data-ljp-border="none"] :is(th, td) { border: 0; }
@media (max-width: 30rem) {
    /* Text wrapping around a fraction of a phone screen leaves nothing to read. */
    .editorial-content table[data-ljp-align] {
        float: none;
        width: 100%;
        margin: .75rem 0;
    }
}
.editorial-content iframe {
    width: 100%;
    max-width: 100%;
    aspect-ratio: 4 / 3;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.editorial-content code {
    padding: 0.1rem 0.35rem;
    background: var(--bg-alt);
    border-radius: 0.25rem;
    font-size: 0.9em;
}
/* Width and anchoring of an editorial image, or of the figure that carries it
   with its caption. Percentages, never pixels: the text column is not the same
   width on a phone and on a desktop. */
img[data-ljp-size="25"], figure[data-ljp-size="25"], video[data-ljp-size="25"] { width: 25%; }
img[data-ljp-size="33"], figure[data-ljp-size="33"], video[data-ljp-size="33"] { width: 33%; }
img[data-ljp-size="50"], figure[data-ljp-size="50"], video[data-ljp-size="50"] { width: 50%; }
img[data-ljp-size="75"], figure[data-ljp-size="75"], video[data-ljp-size="75"] { width: 75%; }
img[data-ljp-size="100"], figure[data-ljp-size="100"], video[data-ljp-size="100"] { width: 100%; }
img[data-ljp-align="left"], figure[data-ljp-align="left"], video[data-ljp-align="left"] { float: left; margin: .25rem 1rem .75rem 0; }
img[data-ljp-align="center"], figure[data-ljp-align="center"], video[data-ljp-align="center"] { display: block; float: none; margin: .75rem auto; }
img[data-ljp-align="right"], figure[data-ljp-align="right"], video[data-ljp-align="right"] { float: right; margin: .25rem 0 .75rem 1rem; }

/* A film the laboratory hosts itself — a simulation, a recorded experiment.
   It obeys the same two layout attributes as an image, and it keeps its own
   ground: these films are shot on black, and a light page behind a letterboxed
   frame would fringe it. Nothing is fetched before the reader presses play
   (`preload="metadata"` in the markup), so a page carrying one costs the same
   as a page carrying its poster. */
.editorial-content video, .ql-editor video {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius);
    background: #000;
}
.editorial-content figure video, .ql-editor figure video { display: block; width: 100%; }

/* A photograph and its credit line, the pair the legacy site built out of a
   one-column layout table. `<figure>` is the element for it: no width or float
   in the markup, so the same content reflows on a narrow screen. */
.editorial-content figure, .ql-editor figure { margin: 0; }
.editorial-content figure img, .ql-editor figure img { display: block; width: 100%; }
.editorial-content figcaption,
.ql-editor figcaption {
    margin-top: 0.35rem;
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1.35;
}
@media (max-width: 30rem) {
    /* Wrapping text around a third of a phone screen leaves two words a line. */
    .editorial-content figure[data-ljp-align],
    .editorial-content video[data-ljp-align] {
        float: none;
        width: 100%;
        margin: 0.75rem 0;
    }
}
.article-people { margin-top: 2rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.article-people ul { columns: 2; padding-left: 1.2rem; }
.content-filters { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border); }
.content-filters a { padding: 0.4rem 0.25rem; border-bottom: 2px solid transparent; color: var(--text-muted); }
.content-filters a:hover, .content-filters a.is-active { border-color: var(--brand); color: var(--brand); text-decoration: none; }
.content-list { border-top: 1px solid var(--border); }
.content-row, .event-row { padding: 1.15rem 0; border-bottom: 1px solid var(--border); }
.content-row h2 { margin: 0 0 0.25rem; font-size: 1.15rem; }
.content-row p, .event-row p { margin: 0.2rem 0; }
.event-row { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 1.1rem; align-items: start; }
/* Date badge and thumbnail share a single cell so the badge can overhang the
   image by a few pixels, as on a seminar poster. Both are positioned, so the
   cell carries the size. */
.event-media { position: relative; width: 4.75rem; height: 4.75rem; }
.event-row.has-thumb .event-media { width: 10.4rem; height: 6.25rem; }
.event-media > .item-thumb { position: absolute; top: 0; right: 0; }
.event-date { position: absolute; left: 0; top: 50%; z-index: 1; display: flex; width: 4.75rem; height: 4.75rem; flex-direction: column; align-items: center; justify-content: center; transform: translateY(-50%); border: 1px solid var(--border); border-radius: 50%; background: var(--bg-alt); color: var(--text-muted); line-height: 1.15; text-align: center; }
/* A seminar still to come keeps the accent colour; past ones are muted. */
.event-row.is-upcoming .event-date { border-color: var(--brand); background: var(--brand); color: var(--on-brand); }
.event-date strong { font-size: 0.92rem; }
.event-date small { font-size: 0.68rem; }
/* The speaker matters nearly as much as the subject, so it leads the row and
   the title, one line and clipped, sits under it. */
.event-speaker { display: flex; align-items: baseline; gap: 0.5rem; margin: 0 0 0.15rem; font-size: 1.05rem; font-weight: 600; }
.event-speaker-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.event-place { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.event-flag { padding: 0.05rem 0.5rem; border-radius: 999px; background: var(--brand); color: var(--on-brand); font-size: 0.68rem; font-weight: 700; text-transform: uppercase; white-space: nowrap; }
.event-row h2.event-title { margin: 0 0 0.2rem; font-size: 0.98rem; font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.event-excerpt { color: var(--text-muted); }
.content-meta, .content-kicker { color: var(--text-muted); font-size: 0.85rem; }
.content-kicker { margin: 0 0 0.35rem; color: var(--brand); font-weight: 600; text-transform: uppercase; }
.content-detail > header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }

/* Offers read the other way round: research themes in rows, recruitment levels
   in columns. The list underneath stays the reference; a cell holds one icon
   per subject, its title in the tooltip, and a dash when nothing is open. */
.job-matrix { margin-bottom: 2rem; }
.job-matrix > h2 { margin: 0 0 0.75rem; font-size: 1.05rem; }
.matrix-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.matrix-table th, .matrix-table td { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); text-align: left; }
.matrix-table thead th { color: var(--text-muted); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
/* Even level columns, whatever a cell holds; the theme column takes the rest. */
.matrix-table th:not(:first-child), .matrix-table td { width: 6.5rem; text-align: center; }
.matrix-table tbody th { max-width: 20rem; font-weight: 500; }
/* A count, not a row of icons: the table answers « combien », and the list
   underneath answers « lesquels ». */
.job-count { font-variant-numeric: tabular-nums; font-weight: 600; }
/* The theme name stays on one line, cut with an ellipsis; the full title is in
   the tooltip and one click away. */
.matrix-theme { display: block; max-width: 20rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-none { color: var(--text-muted); }

/* Thumbnails of pages, seminars, news items and offers. The legacy site
   displayed them at 100 x 100 next to a title, and every image inherited
   from it is a small square: the box keeps that size so nothing is upscaled,
   and object-fit crops the rare non-square one instead of distorting it. */
.item-thumb { width: 6.25rem; height: 6.25rem; object-fit: cover; border-radius: var(--radius); background: var(--bg-alt); }
.content-row.has-thumb { display: grid; grid-template-columns: 6.25rem minmax(0, 1fr); gap: 1.1rem; align-items: start; }
.content-detail.has-thumb { display: grid; grid-template-columns: 6.25rem minmax(0, 1fr); gap: 1.25rem; align-items: start; }
.content-detail.has-thumb > .editorial-content, .content-detail.has-thumb > section { grid-column: 1 / -1; }
@media (max-width: 30rem) {
    /* Below that width the text column would be too narrow to read. */
    .content-row.has-thumb, .content-detail.has-thumb { grid-template-columns: minmax(0, 1fr); }
    .event-row.has-thumb .event-media { width: 4.75rem; height: 4.75rem; }
    .event-media > .item-thumb { display: none; }
}
.inline-filters { display: flex; align-items: end; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.inline-filters label { display: grid; gap: 0.25rem; color: var(--text-muted); font-size: 0.85rem; }
.inline-filters select { min-width: 9rem; }
/* One shape for every list of publications: the index, a profile, a research
   page. A card on a light ground so the eye finds where one ends and the next
   begins, and three lines that never wrap — so every card is the same height,
   which is what makes a long list readable at all. */
.pub-list { display: grid; gap: 0.35rem; }
/* Past a certain number the list scrolls in its own frame instead of pushing
   everything below it off the screen. */
.pub-list.is-scrollable {
    max-height: 32rem;
    overflow-y: auto;
    padding-right: 0.4rem;
}
.pub-card {
    display: grid;
    gap: 0.08rem;
    padding: 0.55rem 0.75rem;
    border-radius: var(--radius);
    background: var(--bg-alt);
}
/* The crop is what keeps the height constant; the full title is in the
   tooltip and in the page the link opens. */
.pub-card > * {
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pub-title { font-weight: 600; }
.pub-authors { font-size: 0.88rem; }
/* An author who has a page on the site is a link, and a link that only shows
   itself under the pointer is invisible to whoever is not moving one. Colour
   and underline, both: the line mixes links and plain names, and the reader
   must see which is which without trying. */
.pub-authors a {
    color: var(--brand);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 0.15em;
}
.pub-authors a:hover, .pub-authors a:focus-visible { text-decoration-thickness: 2px; }
.pub-meta { color: var(--text-muted); font-size: 0.82rem; }

/* Kept for the import preview, which shows candidates rather than rows of the
   site's own list. */
.publication-list { border-top: 1px solid var(--border); }
.publication-row { padding: 1.1rem 0; border-bottom: 1px solid var(--border); }
.publication-row h2 { margin: 0.15rem 0 0.35rem; font-size: 1.1rem; }
.publication-authors { margin: 0.25rem 0; }
.publication-metadata { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 0.35rem 1rem; }
.publication-metadata dt { color: var(--text-muted); font-weight: 600; }
.publication-metadata dd { margin: 0; overflow-wrap: anywhere; }
.search-group { margin: 0 0 2rem; }
.search-group > h2 { display: flex; align-items: center; gap: 0.5rem; padding-bottom: 0.45rem; border-bottom: 1px solid var(--border); }
.search-group > h2 span { color: var(--text-muted); font-size: 0.8rem; font-weight: 400; }
.search-results article { padding: 0.8rem 0; border-bottom: 1px solid var(--border); }
.search-results h3 { margin: 0 0 0.2rem; font-size: 1rem; }
.search-results p { margin: 0; color: var(--text-muted); }
.import-results { margin: 1.5rem 0; border-top: 1px solid var(--border); }
.import-results .is-duplicate { opacity: 0.72; }
.import-select { display: grid; grid-template-columns: 1.5rem minmax(0, 1fr); gap: 0.65rem; align-items: start; }
.import-select > span { display: grid; gap: 0.2rem; }
.import-select small { color: var(--text-muted); }

/* ---------- Auth ---------------------------------------------------------- */
.auth-card { max-width: 26rem; margin: 1rem auto; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 2rem; box-shadow: var(--shadow); }
.auth-notice { margin: 1rem 0 1.5rem; padding: 1rem; border-left: 3px solid var(--brand); background: var(--bg-alt); }
.auth-notice h2 { margin: 0 0 0.5rem; font-size: 1rem; }
.auth-notice p { margin: 0.4rem 0; font-size: 0.9rem; line-height: 1.5; }
.field { margin-bottom: 1rem; display: flex; flex-direction: column; gap: 0.3rem; }
.field-inline { flex-direction: row; align-items: center; gap: 0.5rem; }
.field label { font-weight: 500; font-size: 0.9rem; }
.field input[type="text"], .field input[type="search"], .field input[type="password"], .field input[type="email"], .field input {
    padding: 0.55rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
}
.field.has-error input { border-color: #a83232; }
.btn {
    display: inline-block;
    padding: 0.6rem 1.4rem;
    border: none;
    border-radius: var(--radius);
    font-weight: 600;
    cursor: pointer;
}
.btn-primary { background: var(--brand); color: var(--on-brand); }
.btn-primary:hover { background: var(--brand-dark); }
.btn-secondary { background: var(--bg-alt); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { border-color: var(--brand-light); text-decoration: none; }
.btn-danger { background: #a83232; color: #fff; }
.btn-danger:hover { background: #842626; }
.link-muted { display: inline-block; margin-left: 1rem; color: var(--text-muted); font-size: 0.9rem; }

/* ---------- Administration ------------------------------------------------ */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; }
.section-heading h1, .section-heading p { margin-bottom: 0; }
/* Title of an editorial page, with its thumbnail beside it. The pair has to
   stay together on the left: `space-between` on the heading would otherwise
   push the image, the title and the tools to three corners. */
.page-title { display: flex; align-items: center; gap: 1rem; min-width: 0; }
.page-illustration { flex: none; width: 6.25rem; height: 6.25rem; object-fit: cover; border-radius: var(--radius); background: var(--bg-alt); }
/* ⚠️ The tile count is **not** written into the grid. It was `repeat(4, …)` and
   adding a fifth figure put it alone on a second row; the separators were cut
   by `:nth-child` rules that assumed four. `auto-fit` + `minmax` wraps on its
   own, and the separators are drawn on every tile and clipped by the
   container, which is right whatever the count and wherever it wraps. */
.admin-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); overflow: hidden; }
.admin-stat { padding: 1.25rem; box-shadow: 1px 0 0 var(--border), 0 1px 0 var(--border); }
.admin-stat strong, .admin-stat span { display: block; }
.admin-stat strong { font-size: 1.7rem; line-height: 1.2; color: var(--brand); }
.admin-stat span { color: var(--text-muted); font-size: 0.85rem; }
.admin-section { display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 1.5rem 0; border-bottom: 1px solid var(--border); }
.admin-section h2 { margin: 0; }
.admin-section p { margin: 0.25rem 0 0; color: var(--text-muted); }
.admin-filters { display: grid; grid-template-columns: minmax(12rem, 2fr) repeat(3, minmax(8rem, 1fr)) auto auto; align-items: end; gap: 0.75rem; margin-bottom: 1.5rem; }
.admin-filters label { display: flex; flex-direction: column; gap: 0.25rem; color: var(--text-muted); font-size: 0.85rem; }
.admin-filters input, .admin-filters select { width: 100%; min-height: 2.65rem; padding: 0.5rem 0.65rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); }
.table-scroll { overflow-x: auto; border-top: 1px solid var(--border); }
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.admin-table th, .admin-table td { padding: 0.7rem 0.65rem; border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }
.admin-table th { color: var(--text-muted); font-size: 0.8rem; font-weight: 600; text-transform: uppercase; }
.status-badge { display: inline-block; padding: 0.15rem 0.45rem; border-radius: 0.25rem; font-size: 0.78rem; font-weight: 600; }
.status-active { background: #e9f6ee; color: #1f6b3c; }
.status-inactive { background: var(--bg-alt); color: var(--text-muted); }
[data-theme="dark"] .status-active { background: #1f3a2a; color: #a7d8bb; }
/* The tree of the site's pages.

   The nesting is the data — a page that moves carries what is inside its own
   item — so the indentation is not decoration: it is what the editor reads.
   Each level is drawn with a rule down its left side and a short tick into
   each row, which is what makes a depth of four legible at a glance. */
.page-tree, .page-tree-children { margin: 0; padding: 0; list-style: none; }
.page-tree-children {
    margin-left: 0.85rem;
    padding-left: 0.9rem;
    border-left: 1px solid var(--border);
}
.page-tree-item { position: relative; }
/* The tick joining a row to the rule of its level. */
.page-tree-children > .page-tree-item::before {
    position: absolute;
    top: 1.05rem;
    left: -0.9rem;
    width: 0.6rem;
    border-top: 1px solid var(--border);
    content: "";
}
.page-tree-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 2.1rem;
    padding: 0.1rem 0.3rem;
    border-radius: var(--radius);
}
.page-tree-row:hover { background: var(--bg-alt); }
/* One line, always: a long title is cut rather than pushing the actions down. */
.page-tree-title {
    overflow: hidden;
    min-width: 0;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A rubric reads as a heading; the pages under it are plainer. */
.page-tree > .page-tree-item > .page-tree-row > .page-tree-title { font-size: 1.02rem; font-weight: 600; }
.page-tree-badge { padding: 0.05rem 0.4rem; border: 1px solid var(--border); border-radius: 1rem; color: var(--text-muted); font-size: 0.72rem; }
.page-tree-count { color: var(--text-muted); font-size: 0.75rem; }
/* A section wears the tool colour: it is part of the machinery of the site,
   not something anyone wrote. */
.page-tree-badge-section { border-color: var(--tool-border); color: var(--tool); }
/* Archived: the amber of a warning, and a fill rather than an outline. It is
   the one state of a page that changes what a visitor gets — the address
   answers the « page disparue » screen — so it must be seen without reading. */
.page-tree-badge-archived {
    border-color: transparent;
    background: var(--archived-bg);
    color: var(--archived-ink);
}
.page-tree-count::before { content: "·"; margin-right: 0.35rem; }

/* The three actions are one control, not three: no gap between them, a single
   frame around the set, and they stay quiet until the row is pointed at — but
   never for the keyboard, which cannot point. */
/* The tools of a row are the site's tools; only their place is particular. */
.page-tree-actions { margin-left: auto; }

.page-tree-note { max-width: 48rem; }

/* The publications an import proposes: one line each, the checkbox facing the
   title, the metadata below it. They are read one by one before being ticked,
   so they must not run together as a paragraph. */
.import-list { display: grid; gap: 0.15rem; margin: 0.8rem 0 1.4rem; max-width: 56rem; }
.import-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.7rem;
    align-items: start;
    padding: 0.6rem 0.5rem;
    border-radius: var(--radius);
    cursor: pointer;
}
.import-row:hover { background: var(--bg-alt); }
.import-row input { margin-top: 0.3rem; }
.import-row-body { display: grid; gap: 0.1rem; min-width: 0; }
.import-row-body strong { font-weight: 600; line-height: 1.35; }
.import-row-body span { font-size: 0.86rem; }
.import-authors { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Said once, at the top, and it must not read as an error. */
.import-warning {
    max-width: 56rem;
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--tool-border);
    border-radius: var(--radius);
    background: var(--tool-soft);
    color: var(--text);
}

/* =========================================================================
   Editing tools

   One object for every tool of the site — the pencil on a seminar, the plus in
   a listing, the cross in a tree, the arrows of the carousel. There used to be
   text buttons and bare glyphs side by side in the same header, different on
   every page; a tool is now always this.

   A box of its own colour rather than grey text: at 45 % opacity the old
   glyphs were nearly invisible, and a control nobody sees does not exist. The
   words appear on hover **and on keyboard focus** — a tooltip that only answers
   the mouse leaves out everyone who does not use one — and they are on the
   element as `aria-label` besides, for whoever hears the page.
   ========================================================================= */
.tool {
    position: relative;
    display: inline-flex;
    width: 2rem;
    height: 2rem;
    flex: none;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--tool-border);
    border-radius: var(--radius);
    /* No fill at rest. The frame and the colour are enough to say « tool », and
       a tree of twenty-six rows carries seventy-four of these: filled, they
       become a wall of tiles and the titles disappear behind them. The fill
       arrives with the pointer, where the attention already is. */
    background: transparent;
    color: var(--tool);
    font-size: 1.1rem;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.tool:hover, .tool:focus-visible {
    border-color: var(--tool);
    background: var(--tool);
    color: var(--tool-on);
    text-decoration: none;
}
/* The row a tool belongs to lights its tools softly, so a dense list still
   shows where the pointer is without every tile shouting. */
.page-tree-row:hover .tool, .media-item:hover .tool { background: var(--tool-soft); }
.page-tree-row:hover .tool:hover, .media-item:hover .tool:hover { background: var(--tool); }
.tool-danger:hover, .tool-danger:focus-visible {
    border-color: var(--tool-danger);
    background: var(--tool-danger);
    color: #ffffff;
}
/* A form is plumbing around a tool, never a box of its own. */
.tool-form { display: inline-flex; margin: 0; }
/* Several tools read as one control: a single row, evenly spaced, and they do
   not stretch when the row they sit in is a grid. */
.tool-group { display: inline-flex; align-items: center; gap: 0.3rem; }

/* The words of a tool. Anchored to its right edge on purpose: a tooltip that
   grows to the right would push past the edge of the page for the tools that
   sit at the end of a row — and a page that scrolls sideways is a bug. */
.tool::after {
    position: absolute;
    z-index: 6;
    top: calc(100% + 0.35rem);
    right: 0;
    padding: 0.25rem 0.5rem;
    border-radius: var(--radius);
    background: var(--text);
    color: var(--bg);
    content: attr(data-tip);
    font-size: 0.78rem;
    line-height: 1.3;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
}
.tool:hover::after, .tool:focus-visible::after { opacity: 1; }
/* A tool in a heading sits on the baseline of the words it belongs to, and is
   a little smaller so it does not shout over the title. */
h1 > .tool, h2 > .tool, h3 > .tool {
    width: 1.75rem;
    height: 1.75rem;
    font-size: 0.95rem;
    vertical-align: middle;
}


/* The handle only means something once « Réorganiser » has been pressed. */
.page-tree-handle { display: none; padding: 0 0.15rem; border: 0; background: transparent; color: var(--text-muted); cursor: grab; }
.page-tree-reordering .page-tree-handle { display: inline-flex; }
.page-tree-reordering .page-tree-row { background: var(--bg-alt); }
.page-tree-reordering .page-tree-actions { display: none; }
.page-tree-ghost { opacity: 0.4; }
.page-tree-chosen { outline: 2px solid var(--brand); }
.page-tree-controls { display: flex; gap: 0.5rem; }
/* `.btn` sets `display: inline-flex`, which outranks the browser's own rule for
   `[hidden]`: without this, « Enregistrer » and « Annuler » were on screen
   before anything could be rearranged. */
.page-tree-controls [hidden] { display: none; }

.pagination { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
.media-upload-form { margin-bottom: 2rem; }
.media-filters { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 1rem; }
.media-filters a { padding: 0.3rem 0.55rem; border-bottom: 2px solid transparent; color: var(--text-muted); font-size: 0.85rem; }
.media-filters a:hover, .media-filters a.is-active { border-color: var(--brand); color: var(--brand); text-decoration: none; }
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 1rem; }
.media-item { min-width: 0; border-bottom: 1px solid var(--border); }
.media-item img, .media-placeholder { width: 100%; height: 8rem; object-fit: cover; background: var(--bg-alt); }
.media-placeholder { display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-weight: 700; }
.media-item > div:last-child { display: flex; flex-direction: column; padding: 0.55rem 0.2rem; overflow-wrap: anywhere; }
.media-item span { color: var(--text-muted); font-size: 0.78rem; }
.media-actions { display: flex; gap: 0.2rem; margin-top: 0.3rem; }
.media-actions form { margin: 0; }
.admin-form { max-width: 52rem; }
.admin-form fieldset { margin: 0 0 1.5rem; padding: 0; border: 0; border-top: 1px solid var(--border); }
.admin-form legend { padding: 1rem 0 0.5rem; font-weight: 600; font-size: 1.05rem; }
.form-row { display: grid; grid-template-columns: minmax(11rem, 1fr) minmax(15rem, 2fr); gap: 1rem; align-items: center; padding: 0.55rem 0; }
.form-row > label { font-size: 0.9rem; font-weight: 500; }
.form-row > div { min-width: 0; }
/* ⚠️ Radios are excluded like checkboxes. They arrived with the page picker of
   « Page parente », and this rule gave each one width: 100%, a padding and a
   border: the button swelled to fill its label and squeezed the title beside it
   down to an ellipsis of two letters. A radio has an intrinsic size, exactly
   like a checkbox. */
.form-row input:not([type="checkbox"]):not([type="radio"]), .form-row select, .form-row textarea { width: 100%; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); }
.form-row textarea { min-height: 12rem; resize: vertical; font-family: var(--font); line-height: 1.5; }
.field-control.has-translation { display: grid; grid-template-columns: minmax(0, 1fr) 2.25rem; gap: 0.35rem; align-items: center; }
.translation-icon { width: 2.25rem; height: 2.25rem; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-alt); color: var(--text); cursor: pointer; }
.translation-icon:hover, .translation-icon:focus-visible { border-color: var(--brand); color: var(--brand); }
.translation-icon-rich:not(.is-ready) { display: none; }
/* La barre d'attente de la traduction progressive. Elle compte des blocs, pas
   des secondes : c'est la seule mesure honnete quand chaque bloc part dans sa
   propre requete. */
.translation-progress { margin-top: .5rem; }
.translation-progress[hidden] { display: none; }
.translation-progress-bar { height: .35rem; overflow: hidden; border-radius: var(--radius); background: var(--bg-alt); }
.translation-progress-bar > span { display: block; width: 0; height: 100%; background: var(--brand); transition: width .2s ease; }
.translation-progress-label { margin: .35rem 0 0; color: var(--text-muted); font-size: .82rem; }
.rich-text-source[hidden] { display: none !important; }
/* A rich editor in half a form row is a keyhole: its toolbar cannot fit on one
   line, and one writes in a column narrower than the page being produced. The
   label goes above it and the editor takes the whole width of the form. */
/* The two languages of a form, as two panels of one container. The strip is
   hidden until the script takes over, so a page without JavaScript shows both
   panels and stays exactly the form it was. */
.lang-tabs { margin: 0 0 1rem; border: 1px solid var(--border); border-radius: var(--radius); }
.lang-tabs-strip { display: flex; gap: 0.25rem; padding: 0.4rem 0.4rem 0; border-bottom: 1px solid var(--border); background: var(--bg-alt); }
.lang-tab {
    padding: 0.45rem 0.9rem;
    border: 1px solid transparent;
    border-bottom: 0;
    border-radius: var(--radius) var(--radius) 0 0;
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.92rem;
    cursor: pointer;
}
.lang-tab:hover { color: var(--text); }
.lang-tab.is-active {
    border-color: var(--border);
    /* One pixel down, over the strip's own border: the panel and its tab read
       as the same sheet of paper. */
    margin-bottom: -1px;
    padding-bottom: calc(0.45rem + 1px);
    background: var(--bg);
    color: var(--text);
    font-weight: 600;
}
.lang-panel { padding: 0.2rem 0.9rem 0.6rem; }
.lang-panel[hidden] { display: none; }

.form-row-rich { grid-template-columns: minmax(0, 1fr); gap: 0.35rem; align-items: start; }
/* Quill spaces its groups by 15 px and pads the bar by 8. Nine groups of tools
   is 135 px of air; at five it is 45, and every icon stays on one line down to
   a narrow window — which is the whole point of having dropped the
   strikethrough and the numbered list. */
/* `.rich-editor-shell` is not decoration here: Quill's own stylesheet is loaded
   after this file, so an equally specific selector would lose. */
.rich-editor-shell .ql-toolbar.ql-snow { padding: 6px; }
.rich-editor-shell .ql-toolbar.ql-snow .ql-formats { margin-right: 5px; }
.rich-editor-shell { position: relative; min-width: 0; }
.rich-editor { height: 15rem; background: var(--bg); color: var(--text); }
.rich-editor + .field-error { margin-top: 0.4rem; }
.ql-toolbar.ql-snow, .ql-container.ql-snow { border-color: var(--border); }
.ql-toolbar.ql-snow { background: var(--bg-alt); }
.ql-toolbar.ql-snow { position: relative; padding-right: 3.25rem; }
.ql-toolbar .translation-icon-rich { position: absolute; top: 0.35rem; right: 0.45rem; }
.ql-editor { font-family: var(--font); font-size: 1rem; line-height: 1.6; }
.ql-editor::after { display: block; clear: both; content: ""; }
.ql-editor img { max-width: 100%; height: auto; cursor: grab; }
.ql-editor img.is-selected { outline: 3px solid var(--brand); outline-offset: 2px; }
.ql-editor img:active { cursor: grabbing; }
.image-format-toolbar { position: absolute; z-index: 4; display: flex; gap: .4rem; padding: .35rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .18); }
.image-format-toolbar[hidden] { display: none; }
.image-format-group { display: flex; }
.image-format-group + .image-format-group { padding-left: .4rem; border-left: 1px solid var(--border); }
.image-format-toolbar button { min-width: 2.25rem; min-height: 2rem; padding: .25rem .4rem; border: 1px solid transparent; background: transparent; color: var(--text); cursor: pointer; }
.image-format-toolbar button:hover, .image-format-toolbar button:focus-visible, .image-format-toolbar button.is-active { border-color: var(--brand); background: var(--bg-alt); color: var(--brand); }
/* The table toolbar carries words, not glyphs, so its buttons are wider and it
   may wrap rather than run off the side of a narrow editor. */
.table-format-toolbar { max-width: 100%; flex-wrap: wrap; }
.table-format-toolbar button { white-space: nowrap; font-size: .85rem; }
/* Quill draws no cell borders in the editor; without them a table being built
   is invisible. This is the editor's own guide, not the published look, which
   comes from the data-ljp-border attribute. */
/* Quill's own words, replaced by the page's. It writes them with CSS
   `content`, which cannot read JavaScript — but it can read a custom property,
   and `applyQuillLabels` sets one per string from the translated payload. The
   fallback after the comma keeps the editor readable if the payload is absent.
   The extra `.ql-toolbar` / `.ql-container` outranks Quill's own rule, which
   is loaded after this file. */
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-item::before { content: var(--ql-size-normal, "Normal"); }
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before { content: var(--ql-size-small, "Petit"); }
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before { content: var(--ql-size-large, "Grand"); }
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-toolbar.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before { content: var(--ql-size-huge, "Très grand"); }
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-item::before { content: var(--ql-header-normal, "Normal"); }
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { content: var(--ql-header-2, "Titre 2"); }
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-toolbar.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { content: var(--ql-header-3, "Titre 3"); }
.ql-container.ql-snow .ql-tooltip::before { content: var(--ql-link-visit, "Adresse :"); }
.ql-container.ql-snow .ql-tooltip[data-mode="link"]::before { content: var(--ql-link-enter, "Adresse du lien :"); }
.ql-container.ql-snow .ql-tooltip a.ql-action::after { content: var(--ql-link-edit, "Modifier"); }
.ql-container.ql-snow .ql-tooltip.ql-editing a.ql-action::after { content: var(--ql-link-save, "Enregistrer"); }
.ql-container.ql-snow .ql-tooltip a.ql-remove::before { content: var(--ql-link-remove, "Retirer"); }

.ql-editor table { border-collapse: collapse; }
.ql-editor table td, .ql-editor table th { min-width: 3rem; padding: .35rem .5rem; outline: 1px dashed var(--border); }
[data-theme="dark"] .ql-snow .ql-stroke { stroke: var(--text); }
[data-theme="dark"] .ql-snow .ql-fill { fill: var(--text); }
[data-theme="dark"] .ql-snow .ql-picker { color: var(--text); }
.media-picker-dialog { width: min(64rem, calc(100% - 2rem)); max-height: 88vh; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); }
.media-picker-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
.media-picker-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border); background: var(--bg-alt); }
.media-picker-header h2, .media-picker-header p { margin: 0; }
.media-picker-header p { color: var(--muted); font-size: .9rem; }
.media-picker-close { border: 0; background: transparent; color: var(--text); font-size: 1.7rem; cursor: pointer; }
.media-picker-upload { display: grid; grid-template-columns: minmax(12rem, 1fr) minmax(12rem, 1fr) auto; gap: .75rem; align-items: end; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); }
.media-picker-upload label { display: grid; gap: .25rem; font-size: .85rem; font-weight: 600; }
.media-picker-upload input { min-width: 0; padding: .45rem; border: 1px solid var(--border); background: var(--bg); color: var(--text); }
.media-picker-upload button { min-height: 2.5rem; padding: .45rem .8rem; border: 1px solid var(--brand); background: var(--brand); color: var(--on-brand); cursor: pointer; }
.media-picker-message { grid-column: 1 / -1; margin: 0; color: var(--muted); }
.media-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 1rem; min-height: 8rem; padding: 1.5rem; }
.media-picker-item { min-width: 0; border: 1px solid var(--border); background: var(--bg-alt); }
.media-picker-insert { width: 100%; min-height: 10rem; display: flex; align-items: center; justify-content: center; padding: .5rem; border: 0; background: var(--bg); cursor: pointer; }
.media-picker-item img { width: auto; max-width: 200px; max-height: 12rem; object-fit: contain; }
.media-picker-item-details { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .55rem .65rem; }
.media-picker-item-details span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.media-picker-delete { border: 0; background: transparent; color: #b33a3a; cursor: pointer; }
.form-row.has-error input, .form-row.has-error select { border-color: #a83232; }
.field-error { margin: 0.25rem 0 0; color: #a83232; font-size: 0.82rem; }
.form-actions, .action-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.admin-actions { max-width: 52rem; margin-top: 2.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.admin-actions h2 { font-size: 1.1rem; }
/* A dead end is the one page nobody chose to open, so it does not read like a
   fault report: a photograph of Jean Perrin on one side, what happened and the
   two ways out on the other. */
.error-page {
    display: grid;
    /* The portrait carries the page, so it takes a real width — a thumbnail
       beside a headline would look like an illustration of an error message,
       which is the opposite of the intention. */
    grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
    gap: 2.2rem;
    align-items: center;
    max-width: 56rem;
    padding: 2.5rem 0;
}
/* The code is a label, not the message: it says which failure this is, for
   whoever needs to know, and leaves the line under it to do the talking. */
.error-code {
    margin: 0 0 0.3rem;
    color: var(--brand);
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    line-height: 1;
}
.error-page h1 { margin: 0 0 0.9rem; font-size: 2.5rem; line-height: 1.12; }
.error-page p { max-width: 34rem; }
/* Two ways out, side by side, as on the expired-session page. */
.error-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.5rem; }

/* The portrait keeps its own frame and its own caption: it is a photograph
   from an archive, not an illustration the site drew. The rounded corner and
   the hairline are what stop a black-and-white plate from reading as a hole in
   the page, in either theme. */
.error-portrait { margin: 0; }
.error-portrait img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.error-portrait figcaption {
    margin-top: 0.5rem;
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1.4;
}
/* His own words, in the colour the home page gives them, with the rule that
   says « this is quoted » and no quotation mark to fight the portrait. */
.error-quote {
    margin: 1.4rem 0 0;
    padding-left: 0.9rem;
    border-left: 2px solid var(--motto-mark);
    color: var(--motto);
}
.error-quote p { margin: 0; line-height: 1.55; }
.error-quote footer { margin-top: 0.35rem; color: var(--text-muted); font-size: 0.85rem; }
.error-quote-source { font-style: italic; }
@media (max-width: 48rem) {
    /* Side by side, the portrait would leave the text a column too narrow to
       read; above the text it keeps a reasonable size and stays first. */
    .error-page { grid-template-columns: minmax(0, 1fr); gap: 1.4rem; }
    .error-portrait { max-width: 15rem; }
    .error-page h1 { font-size: 2rem; }
}

/* An archived page opens for an administrator alone, with the banner that says
   why and the control that puts it back. Amber, like the badge in the tree. */
.page-archived-notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1rem;
    margin: 0 0 1.5rem;
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    background: var(--archived-bg);
    color: var(--archived-ink);
}
.page-archived-notice span { max-width: 44rem; }
.page-archived-notice form { margin: 0; }

/* ---------- Flash messages ------------------------------------------------ */
.flash-stack { margin-bottom: 1.5rem; display: flex; flex-direction: column; gap: 0.5rem; }
.flash { padding: 0.75rem 1rem; border-radius: var(--radius); border: 1px solid var(--border); }
.flash-error { background: #fdecec; border-color: #f5b5b5; color: #8a1f1f; }
.flash-success { background: #e9f6ee; border-color: #a7d8bb; color: #1f6b3c; }
.flash-info { background: #eaf2fb; border-color: var(--brand-light); color: #144d7c; }
[data-theme="dark"] .flash-error { background: #3a1f1f; color: #f3b5b5; }
[data-theme="dark"] .flash-success { background: #1f3a2a; color: #a7d8bb; }
[data-theme="dark"] .flash-info { background: #1f2d3a; color: var(--brand); }

/* ---------- Mobile sidebar toggle ----------------------------------------- */
#sidebar-toggle {
    display: none;
    position: fixed;
    top: 1rem;
    left: 1rem;
    z-index: 200;
    width: 2.75rem;
    height: 2.75rem;
    flex-direction: column;
    gap: 0.3rem;
    align-items: center;
    justify-content: center;
    background: var(--brand);
    border: none;
    border-radius: var(--radius);
    cursor: pointer;
}
#sidebar-toggle span { display: block; width: 1.3rem; height: 2px; background: var(--on-brand); }

/* ---------- Responsive ---------------------------------------------------- */
@media (max-width: 60rem) {
    #sidebar {
        position: fixed;
        left: 0;
        top: 0;
        transform: translateX(-100%);
        transition: transform 0.25s ease;
        z-index: 150;
        box-shadow: var(--shadow);
    }
    body.sidebar-open #sidebar { transform: translateX(0); }
    #sidebar-toggle { display: flex; }
    .wrapper { padding-top: 4.5rem; }
    .profile-grid { grid-template-columns: 1fr; }
    /* The portrait moves above the identity block on a narrow screen. */
    .profile-banner { grid-template-columns: minmax(0, 1fr); justify-items: center; text-align: center; }
    .profile-line { justify-content: center; }
    .profile-facts { justify-content: center; text-align: left; }
    /* Two columns of names do not fit a phone. */
    .theme-columns { grid-template-columns: minmax(0, 1fr); }
    .admin-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 48rem) {
    /* The banner is only a few dozen pixels tall here: the partner logos shrink
       and the caption would sit on top of them, so it steps aside. */
    .slider-partners { padding: 0.45rem 0.8rem 1.4rem 1.2rem; gap: 0.6rem; }
    .slider-partners img { height: 1.8rem; }
    .slider-caption { display: none; }
    .slider-arrow { display: none; }
}

@media (max-width: 36rem) {
    .admin-filters { grid-template-columns: 1fr; }
    .admin-section { align-items: flex-start; flex-direction: column; }
    .section-heading { align-items: flex-start; flex-direction: column; }
    .form-row { grid-template-columns: 1fr; gap: 0.25rem; }
    .media-picker-upload { grid-template-columns: 1fr; }
}

/* ---------- Print --------------------------------------------------------- */
/* The legacy site offered a dedicated print view; here the page itself prints:
   navigation, banner and controls disappear, the content takes the full width. */
@media print {
    #sidebar,
    #sidebar-toggle,
    .slider,
    .slideshow,
    .breadcrumb,
    .form-actions,
    .inline-filters,
    .pagination,
    .flash-stack,
    .skip-link,
    .btn,
    .sidebar-credits { display: none !important; }

    html, body { background: #fff; color: #000; font-size: 11pt; }
    #layout { display: block; }
    #content, .wrapper, .editorial-content { max-width: none; margin: 0; padding: 0; }
    .page-header { padding: 0; }

    a { color: #000; text-decoration: underline; }
    /* Spell out link targets, which a printed page cannot follow. */
    .editorial-content a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; }

    h1, h2, h3 { page-break-after: avoid; }
    img, table, figure, .child-page-item, .publication-row { page-break-inside: avoid; }
    .child-pages { grid-template-columns: minmax(0, 1fr); }
    .editorial-content table { display: table; width: 100%; }
    .editorial-content iframe { display: none; }
}

/* Publication authors: what the site recognised while the names are typed.
   Recognition that only shows up after saving is recognition nobody trusts. */
/* Aligned under the field it comments on, not under its label. */
.form-row-preview { padding-top: 0; align-items: start; }
.author-preview { font-size: 0.85rem; }
.author-preview-empty { margin: 0; color: var(--text-muted); }
.author-preview-list { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0; padding: 0; list-style: none; }
.author-preview-list li {
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-alt);
}
.author-preview-list li.is-known { border-color: var(--tool); }
.author-preview-list li.is-known a { color: var(--tool); text-decoration: none; font-weight: 600; }
.author-preview-list li.is-known a:hover { text-decoration: underline; }
.author-preview-list li.is-unknown { color: var(--text-muted); }
.author-preview-typed { color: var(--text-muted); font-weight: 400; }

/* A publication page: where to read the article, and how to cite it. */
.publication-links { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 1rem 0 0; }
.publication-abstract { text-align: justify; hyphens: auto; }

.citation-block { margin-top: 2.5rem; }
.citation-tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.6rem; }
.citation-tab {
    padding: 0.35rem 0.8rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}
.citation-tab:hover { color: var(--text); }
.citation-tab.is-active { border-color: var(--tool); background: var(--bg-alt); color: var(--tool); font-weight: 600; }
.citation-panel { position: relative; }
.citation-panel pre {
    margin: 0;
    padding: 0.9rem 3rem 0.9rem 0.9rem;
    /* An entry carries the abstract, which is a page of text on its own: the
       block scrolls rather than pushing the rest of the page out of sight. */
    max-height: 22rem;
    overflow: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-alt);
}
.citation-panel code { font-family: ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace; font-size: 0.82rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.citation-copy {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    display: flex;
    padding: 0.35rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--tool);
    cursor: pointer;
}
.citation-copy:hover { border-color: var(--tool); }
.citation-copy.is-copied { border-color: var(--tool); background: var(--tool); color: #fff; }
.citation-download { margin: 0.6rem 0 0; font-size: 0.85rem; }

/* The note printed next to a name — the other laboratory of an associate
   researcher. Quieter than the name it follows, never louder. */
.person-note { color: var(--text-muted); font-size: 0.9em; font-weight: 400; }
.person-note a { color: inherit; text-decoration: underline; }
.person-note a:hover { color: var(--brand); }

/* Members of a content page: who works on the subject, right under the title. */
/* Who works on a page, in a box that folds. A page carrying thirty names put
   its own text off the screen; the box is lightly greyed so that it reads as an
   apparatus of the page rather than as part of what the page says. */
.page-people {
    margin: 0 0 1.6rem;
    padding: 0.5rem 0.9rem;
    background: var(--bg-alt);
    border-radius: var(--radius);
}
.page-people > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.15rem 0;
    cursor: pointer;
    font-weight: 600;
    list-style: none;
}
.page-people > summary::-webkit-details-marker { display: none; }
.page-people > summary:hover { color: var(--brand); }
.page-people[open] > summary { margin-bottom: 0.5rem; }
.page-people[open] > summary .nav-caret { transform: rotate(90deg); }
.page-people-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.2rem 2rem; }
.page-people-list { margin: 0; padding: 0; list-style: none; }
.page-people-list li { padding: 0.15rem 0; line-height: 1.4; }
.page-people-status { color: var(--text-muted); font-size: 0.82rem; }
.page-people-heading { margin: 1rem 0 0.3rem; font-size: 1rem; }
.page-people-associates { columns: 2; column-gap: 2rem; }
@media (max-width: 700px) {
    .page-people-columns { grid-template-columns: 1fr; }
    .page-people-associates { columns: 1; }
}

/* The « Membres » picker of a page form. */
.people-picker { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
.people-picker-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-bottom: 1px solid var(--border); }
.people-picker-head .people-picker-search { flex: 1 1 auto; min-width: 0; }
.people-picker-count { color: var(--text-muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.people-picker-toggle { padding: 0.3rem 0.7rem; font-size: 0.82rem; }
.people-picker-toggle.is-active { border-color: var(--tool); color: var(--tool); }
/* ⚠️ A grid, **not** `columns`. A multi-column box with a constrained height
   spills into further columns *sideways*: with the sixty members of the
   directory the list scrolled horizontally, 1279 px wide inside a 422 px box,
   and dragged the whole form with it. A grid fragments downwards, which is
   what a scrolling list has to do. */
.people-picker-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0 1rem; align-content: start; max-height: 18rem; margin: 0; padding: 0.3rem; overflow-y: auto; list-style: none; }
.people-picker-list label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.1rem 0.4rem; padding: 0.15rem 0.3rem; border-radius: var(--radius); cursor: pointer; }
.people-picker-list label:hover { background: var(--bg-alt); }
/* ⚠️ No `nowrap`: « Natalia Belén Beiza Canelo » is wider than a telephone
   column and pushed the whole form sideways. The status used to share the row
   and keep names short; now the name is the row and it wraps. */
.people-picker-name { font-size: 0.9rem; overflow-wrap: anywhere; }
.people-picker-former { color: var(--text-muted); font-size: 0.75rem; }
.people-picker-empty { margin: 0; padding: 0.6rem; color: var(--text-muted); font-size: 0.85rem; }
/* The grid folds to one column on its own (`auto-fill` + `minmax`), so the
   old narrow-screen override has nothing left to do. */

/* The « Pages associées » field: the site's tree with a checkbox on each row.
   A flat select of forty titles hid the one thing the association means — a
   publication attached to a rubric is inherited by every page below it. */
.page-picker { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
.page-picker-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-bottom: 1px solid var(--border); }
.page-picker-head .page-picker-search { flex: 1 1 auto; min-width: 0; }
.page-picker-count { color: var(--text-muted); font-size: 0.82rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.page-picker-only { padding: 0.3rem 0.7rem; font-size: 0.82rem; }
.page-picker-only.is-active { border-color: var(--tool); color: var(--tool); }
/* ⚠️ The box does **not** scroll as a whole any more, and must not: with one
   scroll for everything, a long « Pages du site » tree pushed the whole
   « Pages des membres » group past the bottom edge — measured at 31 px below
   it — and nothing said anything was down there. The member pages were in the
   field and unreachable in practice. Each group scrolls inside itself instead,
   so a group can never hide another one. */
.page-picker-body { padding: 0.3rem; }
.page-picker-group + .page-picker-group { margin-top: 0.5rem; border-top: 1px solid var(--border); padding-top: 0.5rem; }
.page-picker-group-label { display: flex; align-items: center; gap: 0.4rem; margin: 0.1rem 0 0.3rem; padding: 0 0.3rem; color: var(--text-muted); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }
.page-picker-group-count { padding: 0 0.35rem; border-radius: 1rem; background: var(--bg-alt); font-variant-numeric: tabular-nums; letter-spacing: 0; }
/* Each group scrolls on its own, and says so: the gradients below are painted
   on the scroll container, so they only show while there is more to reach
   (`background-attachment: local` pins the white covers to the content). */
.page-picker-group [data-page-group-tree] {
    max-height: 11rem;
    overflow-y: auto;
    background:
        linear-gradient(var(--bg) 30%, rgba(0, 0, 0, 0)) top / 100% 1.2rem no-repeat local,
        linear-gradient(rgba(0, 0, 0, 0), var(--bg) 70%) bottom / 100% 1.2rem no-repeat local,
        radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0)) top / 100% 0.5rem no-repeat scroll,
        radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0)) bottom / 100% 0.5rem no-repeat scroll;
}
.page-picker-tree, .page-picker-children { margin: 0; padding: 0; list-style: none; }
/* The indent is a rule, not a margin: a branch of six pages is easier to read
   against a line than against empty space. */
.page-picker-children { margin-left: 0.75rem; padding-left: 0.55rem; border-left: 1px solid var(--border); }
.page-picker-row { display: flex; align-items: center; gap: 0.25rem; min-width: 0; }
.page-picker-caret { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 1.15rem; height: 1.15rem; padding: 0; border: 0; border-radius: var(--radius); background: none; color: var(--text-muted); cursor: pointer; transition: transform 0.15s ease; transform: rotate(90deg); }
.page-picker-caret:hover { color: var(--text); background: var(--bg-alt); }
.page-picker-caret[aria-expanded="false"] { transform: rotate(0deg); }
.page-picker-caret-empty { cursor: default; }
.page-picker-label { display: flex; align-items: center; gap: 0.4rem; min-width: 0; padding: 0.15rem 0.3rem; border-radius: var(--radius); cursor: pointer; }
.page-picker-label:hover { background: var(--bg-alt); }
.page-picker-label-inert { cursor: default; color: var(--text-muted); }
.page-picker-label-inert:hover { background: none; }
/* The label is sized by its content, so the badge sits right after the name
   instead of being pushed to the far edge of the field; the title is what
   gives way when a deep branch runs out of width, and the badge never does. */
.page-picker-title { flex: 0 1 auto; min-width: 0; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.page-picker-badge { flex: none; padding: 0.05rem 0.4rem; border: 1px solid var(--border); border-radius: 1rem; color: var(--text-muted); font-size: 0.72rem; white-space: nowrap; }
.page-picker-empty { margin: 0; padding: 0.6rem; color: var(--text-muted); font-size: 0.85rem; }

/* The other end of an association, searched rather than listed: 475
   publications cannot be a list of checkboxes. */
.item-picker { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); padding: 0.5rem; }
.item-picker-search { width: 100%; }
.item-picker-results, .item-picker-chosen { margin: 0.4rem 0 0; padding: 0; list-style: none; }
.item-picker-results { max-height: 14rem; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-alt); }
.item-picker-add { display: block; width: 100%; padding: 0.3rem 0.5rem; border: 0; background: none; color: var(--text); font: inherit; font-size: 0.88rem; text-align: left; cursor: pointer; }
.item-picker-add:hover { background: var(--bg); color: var(--brand); }
.item-picker-empty { padding: 0.4rem 0.5rem; color: var(--text-muted); font-size: 0.85rem; }
.item-picker-row label { display: flex; align-items: baseline; gap: 0.45rem; padding: 0.2rem 0.3rem; border-radius: var(--radius); cursor: pointer; }
.item-picker-row label:hover { background: var(--bg-alt); }
.item-picker-title { font-size: 0.88rem; }
.item-picker-none { margin: 0.4rem 0 0; padding: 0 0.3rem; color: var(--text-muted); font-size: 0.82rem; }

/* The slides of the carousel, in one list: they used to be administered one at
   a time from the form of the page each one linked to. */
.banner-list { margin: 0; padding: 0; list-style: none; }
.banner-row { display: grid; grid-template-columns: 12rem minmax(0, 1fr) auto auto; align-items: center; gap: 0.9rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border); }
/* Thumbnail, caption and target, the size of the file, then the tools. */
/* The 20/3 of the banner itself, so the row shows the real framing. */
.banner-thumb { width: 12rem; aspect-ratio: 20 / 3; object-fit: cover; border-radius: var(--radius); background: var(--bg-alt); }
.banner-row-text { min-width: 0; }
.banner-caption { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.banner-target { margin: 0.1rem 0 0; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.banner-size { color: var(--text-muted); font-size: 0.85rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.banner-size-off { color: #a83232; }
/* A sentence of guidance under a field, not an error and not a label. */
.form-note { margin: 0 0 0.6rem; color: var(--text-muted); font-size: 0.85rem; }
.banner-preview img { width: 100%; max-width: 34rem; aspect-ratio: 20 / 3; object-fit: cover; border-radius: var(--radius); }
@media (max-width: 48rem) {
    /* The thumbnail takes the full width and the rest stacks under it. */
    .banner-row { grid-template-columns: minmax(0, 1fr) auto auto; }
    .banner-thumb { grid-column: 1 / -1; width: 100%; }
}
