:root {
  --ink: #172A3A;            /* Memory Blue - dominant: text, buttons, headers */
  --ink-soft: #4A5763;       /* secondary text - navy-tinted grey, not raw taupe
                                 (raw taupe fails AA as text, 2.44:1 on ivory) */
  --paper: #F5F1E8;          /* Warm Ivory - default page background */
  --paper-raised: #ffffff;   /* Pure White - cards & photos only */
  --line: #DED7C7;           /* card borders */
  --line-soft: #EFE9DC;      /* inner dividers */
  --taupe: #A89B7A;          /* Eternal Taupe - non-text uses only (surfaces,
                                 dividers, icon tints); fails AA as text */
  --accent: #C7A25A;         /* Heritage Gold - decorative accent only. Never
                                 text under 18px, never twice in one block */
  --accent-label: #8C7A4E;   /* darkened gold for small label/eyebrow text
                                 (AA-large only, 3.72:1 - not for body copy) */
  --accent-text: #7A6230;    /* darker still - the only gold shade meant to
                                 carry actual informational text at any size
                                 (surname, timeline dates): 5.15:1, passes AA
                                 for normal text too. An external UX audit
                                 measured --accent (2.13:1) and --accent-label
                                 (3.72:1) both failing AA at those two exact
                                 uses - see .surname/.timeline-date below. */
  --accent-soft: color-mix(in srgb, var(--taupe) 16%, var(--paper));
  --radius: 14px;
  --shadow: 0 10px 30px -18px rgba(23, 42, 58, 0.35);
}

* { box-sizing: border-box; }

/* Several panels give a <details>'s content its own explicit `display`
   (e.g. .stack-form's `display: flex`), which in some browser engines
   outranks the UA rule that hides non-summary content while a <details>
   is closed - the content then keeps painting outside the collapsed
   <details> box (which has no overflow clipping), overlapping whatever
   comes after it in the page. Restating the rule explicitly, with
   enough weight to win regardless of what a descendant declares, keeps
   closed panels (media/archive edit forms, the manual-fact form, the
   suggestion box) genuinely collapsed. */
details:not([open]) > :not(summary) { display: none !important; }
/* <picture> (WebP + fallback, see webp_variant_url) disappears from the
   box model entirely so every existing "img { width/height: 100% }" rule
   keeps sizing the real <img> exactly as it did before this wrapper
   existed - no per-component CSS needed to accommodate it. */
picture { display: contents; }

body {
  margin: 0;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  color: var(--ink);
  background: var(--paper);
  line-height: 1.6;
  /* Global safety net, not just the biography reader: any free-text
     field a family fills in (biography, testimonial, timeline event,
     caption...) can contain one long run with no space/hyphen to break
     on - a pasted URL, or someone mashing the keyboard (Ced's 2026-10-07
     report: Bernard Breton's biography overflowed the reader's leather-
     book frame sideways instead of wrapping). Neither property alone is
     fully bulletproof across every browser's own quirks (Ced's
     2026-10-14 follow-up report, same text, still on responsive) -
     word-break is the older, more broadly-supported mechanism and
     overflow-wrap the modern one; declaring both is the standard
     belt-and-suspenders pairing for this exact case. Both only ever
     kick in for text with nowhere else to break; normal text still
     wraps on spaces exactly as before.*/
  overflow-wrap: break-word;
  word-break: break-word;
}

h1, h2, h3 {
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-weight: 300;
  color: var(--ink);
  margin: 0 0 0.5em;
}

a { color: var(--ink); }

.muted { color: var(--ink-soft); font-size: 0.9em; }
.empty-state { color: var(--ink-soft); font-style: italic; }

.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.25rem 2rem;
  border-bottom: 1px solid var(--line);
  background: var(--paper-raised);
  position: sticky;
  top: 0;
  z-index: 10;
}
.header-nav { display: flex; align-items: center; gap: 1rem; }
/* Buttons in tight flex rows (header CTA, owner toolbar) must never be
   squeezed below their own text width - a shrunk pill wraps its label
   across 2-3 lines with no text-align set to re-center them, which is
   what "mis-centered button text" on mobile actually turned out to be
   (found 2026-08-18). Let the flex row wrap onto a second line instead
   of shrinking the button. */
.header-nav .btn, .owner-bar .btn { white-space: nowrap; flex-shrink: 0; }
.lang-switcher { position: relative; }
.lang-current { font-size: 0.85rem; color: var(--ink-soft); cursor: pointer; padding: 0.4rem 0.2rem; }
.lang-menu {
  display: none;
  position: absolute; top: 100%; inset-inline-end: 0;
  background: var(--paper-raised); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: var(--shadow); padding: 0.4rem; min-width: 140px; z-index: 20;
}
.lang-switcher:hover .lang-menu, .lang-switcher:focus-within .lang-menu, .lang-switcher.is-open .lang-menu { display: flex; flex-direction: column; }
.lang-current { background: none; border: none; font-family: inherit; }
.lang-menu a { padding: 0.4rem 0.6rem; border-radius: 6px; text-decoration: none; color: var(--ink); font-size: 0.85rem; }
.lang-menu a:hover { background: var(--accent-soft); }
.lang-menu a.is-active { font-weight: 600; color: var(--ink); }

html[dir="rtl"] .timeline { border-left: none; border-right: 2px solid var(--accent-soft); }
html[dir="rtl"] .timeline li::before { left: auto; right: -6.5px; }
html[dir="rtl"] .drag-handle { transform: scaleX(-1); }
/* The biography reader's "leather book" cover (see .reader below) bakes
   in a left-edge spine and a left ruled margin line - a purely visual
   metaphor that never had an RTL counterpart, so an Arabic/Hebrew/Urdu
   reader (found during a routine live check on Mauricette's own page,
   not something Ced reported) saw perfectly right-aligned RTL prose
   sitting inside a cover whose spine was still on its "wrong" edge -
   the side where the book's pages would end, not where they begin.
   Mirrors every left/right value used by .reader/.reader-body and their
   pseudo-elements below (and their mobile overrides further down) -
   same values, opposite side, nothing about the actual text layout
   changes. */
html[dir="rtl"] .reader { padding-left: 1.5rem; padding-right: 2.5rem; }
html[dir="rtl"] .reader::before {
  left: auto; right: 0;
  background: linear-gradient(270deg, #1c0f08, #4a2f1f 65%, transparent);
  border-radius: 0 var(--radius) var(--radius) 0;
  box-shadow: inset 1px 0 0 rgba(199, 162, 90, 0.3);
}
html[dir="rtl"] .reader::after { inset: 0.5rem 1.35rem 0.5rem 0.5rem; }
html[dir="rtl"] .reader-body { padding-left: 1.6rem; padding-right: 2.1rem; }
html[dir="rtl"] .reader-body::before { left: auto; right: 1.15rem; }
.brand { display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; }
.brand-word {
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 16px;
  font-weight: 300;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  color: var(--ink);
}

main { max-width: 920px; margin: 0 auto; padding: 2.5rem 1.5rem 5rem; }

.site-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
  padding: 2.5rem 1rem;
  color: var(--ink-soft);
  font-size: 0.85rem;
  border-top: 1px solid var(--line);
}

.btn {
  display: inline-block;
  padding: 0.7rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  text-decoration: none;
  font-weight: 500;
  cursor: pointer;
  font-size: 0.95rem;
  text-align: center;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.btn:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.btn-primary { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.btn-ghost { background: transparent; color: var(--ink); }
.btn-small { padding: 0.5rem 1rem; font-size: 0.85rem; }
.btn-tiny { padding: 0.35rem 0.8rem; font-size: 0.78rem; border-radius: 999px; }

/* One visible save action for two backend-distinct forms that read as
   a single "save my identity" step to the family (civil names + main
   identity fields - see data-ajax-save-group in panels.js). Sits as a
   sibling right after both forms, not inside either one. */
.identity-save-btn { display: block; margin: 0.4rem 0 1.8rem; width: fit-content; }
.identity-save-btn:disabled { opacity: 0.6; cursor: default; transform: none; box-shadow: none; }
.btn-danger { background: transparent; border-color: #c0392b; color: #c0392b; }
.btn-danger:hover { background: #c0392b; color: #fff; }

/* Visible feedback for the "armed" (first-click) state of a two-click
   .confirm-form button (see panels.js) - a targeted UX audit found this
   state had no CSS at all, so the double-click mechanism worked but
   looked like nothing happened on the first click. */
.btn.confirm-armed { background: var(--accent-text); border-color: var(--accent-text); color: #fff; }

.hero { text-align: center; padding: 3rem 0 2.5rem; }
.hero h1 { font-size: 2.4rem; max-width: 700px; margin: 0 auto 0.6em; }
.lede { color: var(--ink-soft); max-width: 620px; margin: 0 auto 1rem; font-size: 1.05rem; }
.hero-detail { color: var(--ink-soft); max-width: 620px; margin: 0 auto 1rem; font-size: 0.98rem; }
.hero-trust { max-width: 620px; margin: 0 auto 1rem; font-size: 0.88rem; color: var(--ink-soft); }
.hero-tagline { max-width: 620px; margin: 0 auto 1.6rem; font-size: 1rem; font-style: italic; }
.home-search { display: flex; justify-content: center; gap: 0.5rem; max-width: 420px; margin: 1.6rem auto 0; }
.home-search input { flex: 1; padding: 0.6rem 0.9rem; border: 1px solid var(--line); border-radius: 999px; font-size: 0.95rem; }

.related-memorials-list { display: flex; flex-wrap: wrap; gap: 0.8rem; }
.related-memorial-card {
  display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0.9rem 0.5rem 0.5rem;
  border: 1px solid var(--line); border-radius: 999px; text-decoration: none; color: var(--ink);
  background: var(--paper-raised); transition: border-color 0.18s ease;
}
.related-memorial-card:hover { border-color: var(--ink-soft); }
.related-memorial-card img, .related-memorial-card .card-placeholder {
  width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex-shrink: 0;
}
.related-memorial-card .card-placeholder { display: flex; align-items: center; justify-content: center; background: var(--paper); }

/* Deliberately plain, not card/pill-styled like the related-memorials
   list above - a discreet list of links, not a promotional block (Ced's
   2026-09-28 requirement: "jamais publicitaire"). Same muted/hover
   treatment as .suggestion-box summary below (Ced's 2026-09-30 request:
   "Demander l'adresse d'envoi" and these partner links should read as
   the same kind of secondary, low-key action). */
.partner-services-list { list-style: none; padding: 0; margin: 0.6rem 0 0; display: flex; flex-direction: column; gap: 0.4rem; }
.partner-services-list a { color: var(--ink-soft); font-weight: 400; font-size: 0.9rem; }
.partner-services-list a:hover { color: var(--ink); }

/* Rich partner-service display (Ced's 2026-10-06 request): one column
   always, even on desktop - these are a handful of short, plain-text
   offers, not a card grid, and a single column keeps the icon/action/
   description/name reading order unambiguous at any width. */
.partner-audience-group { margin-top: 0.8rem; }
.partner-audience-group:first-of-type { margin-top: 0.6rem; }
.partner-audience-heading {
  margin: 0 0 0.3rem; font-size: 0.78rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-soft);
}
.partner-services-list-rich { gap: 0.9rem; }
/* The icon sits INLINE with the action link (same line, same left edge
   as "POUR LES PROCHES"/"Liens utiles") rather than in its own flex
   column - an icon column pushed the link/description/partner-name
   ~28px to the right of every heading above and below it, a visible
   left-edge zigzag Ced flagged (2026-10-06, worse on mobile where that
   gap reads as a bigger share of the line). Flush left at every line now. */
/* gap widened from 0.1rem (Ced's 2026-10-14 follow-up, "améliore la
   disposition des paragraphes"): the action link/description/partner-
   name were nearly touching, reading as one dense block rather than
   three distinct lines - this is purely vertical breathing room, on top
   of (not instead of) the description/name's own left-indent above. */
.partner-service-item { display: flex; flex-direction: column; gap: 0.35rem; }
.partner-service-icon { flex: none; color: var(--ink-soft); margin-top: 0.15rem; }
/* align-items: flex-start, not center (Ced's 2026-10-14 report, desktop
   AND responsive): the action label commonly wraps across 2-3 lines
   (it's the whole offer description, not a short nav label), which made
   the "whole row" tall enough that center-aligning put the icon
   floating mid-way between the wrapped lines instead of level with the
   first one. The icon's own margin-top nudges it down just enough to
   sit on the first line's cap-height instead of its very top edge. */
.partner-service-action {
  display: inline-flex; align-items: flex-start; gap: 0.5rem; position: relative;
  color: var(--ink); font-weight: 500; font-size: 0.92rem; text-decoration: underline;
  text-underline-offset: 2px; text-decoration-color: var(--line);
}
/* A real ≥44px tap target on touch, WITHOUT visually looking like a
   button and without the real text box itself growing to fill it
   (Ced's 2026-10-16 report: the old min-height: 44px approach left ~23px
   of invisible space below the actual text, inside the link's own box,
   stacking with the gap below for a much bigger visual gap down to the
   description than the gap's own 0.35rem value suggested). An invisible
   overlay extends the hit area instead - the visible layout now reflects
   only the real text height. */
.partner-service-action::before {
  content: ""; position: absolute; inset: -12px 0;
}
.partner-service-action:hover { text-decoration-color: var(--ink); }
/* Normal inline content (not its own flex item) inside the action link,
   alongside the icon (Ced's 2026-10-16 report: "(nouvel onglet)" sat
   visibly off the main text's baseline) - flex's align-items only
   aligns each flex ITEM's box, never two differently-sized inline runs
   WITHIN one item by their shared text baseline the way normal inline
   layout already does everywhere else on this page. Wrapping the label
   and the hint together into one flex item lets ordinary inline
   baseline alignment handle the two different font sizes correctly. */
.partner-service-action-text { display: inline; }
/* Visible (not just screen-reader) new-tab notice: seniors clicking a
   memorial link shouldn't be surprised a new tab opened - PROMPT
   ACCESSIBILITE_SENIORS 2026-09-28 principle applied here too. */
.partner-service-new-tab-hint {
  margin-left: 0.35rem; color: var(--ink-soft); font-size: 0.78rem; font-weight: 400;
  text-decoration: none;
  /* The phrase itself must never break across two lines (Ced's 2026-10-14
     report: "(nouvel onglet)" wrapping mid-phrase in responsive) - it can
     still wrap AS A WHOLE onto its own line if the action text above it
     already fills the line, just never split internally. */
  white-space: nowrap;
}
/* Indented to align with the action link's own TEXT (icon width 18px +
   its 0.5rem gap), not the icon's left edge (Ced's 2026-10-14 follow-up
   report) - a deliberate partial reversal of the flush-left call above:
   the description/name read as belonging to the action text they sit
   under, not to the decorative icon beside it. The action link's own
   first line stays flush left (unchanged), so there's no zigzag against
   "POUR LES PROCHES"/"Liens utiles" above it - only these two lines
   shift. */
.partner-service-description-line { margin-left: 1.625rem; font-size: 0.85rem; color: var(--ink-soft); }
.partner-service-description { color: var(--ink-soft); font-size: 0.85rem; }
/* Same size/family as the description it's now folded onto the end of
   (Ced's 2026-10-17 report: the partner name read distinct and bigger
   than its surroundings) - nesting it inside .partner-service-description-line
   instead of being .partner-service-item's own direct child (Ced's
   2026-10-16 merge) meant it no longer matched the OTHER rule below,
   which only applies to a direct child, so it fell back to the page's
   normal ambient font-size instead of blending into the sentence it's
   now part of. */
.partner-service-description-line .partner-service-name { color: var(--ink-soft); font-size: inherit; font-family: inherit; font-weight: inherit; }
.partner-service-item > .partner-service-name { color: var(--ink-soft); font-size: 0.78rem; margin-left: 1.625rem; }
.partner-transparency-note { margin-top: 0.8rem; color: var(--ink-soft); font-size: 0.78rem; }

/* "Services pratiques" hub (Ced's 2026-09-30 restructuring): each
   sub-item (adresse d'envoi, partenaires, cérémonie, communauté) reads
   as its own block within the shared section, separated by a hairline
   rather than nested cards - a lighter touch matching this section's
   already-established "jamais publicitaire" plainness. */
.service-item { margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--line); }
.service-item:first-child { margin-top: 0.6rem; padding-top: 0; border-top: none; }
.service-item h3 { margin: 0 0 0.4rem; font-size: 1.05rem; }

.qrcode-formats { margin: 0.6rem 0; font-size: 0.85rem; }
.qrcode-formats a { color: var(--ink); }
.qrcode-download-page { max-width: 480px; margin: 0 auto; text-align: center; }
.qrcode-download-preview { margin: 1rem auto; display: block; }
.qrcode-download-buttons { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; margin: 1.2rem 0; }
/* padding-left overridden from the browser default (~40px) - on a
   narrow viewport especially, that indent reads as misaligned with
   everything else on this page (image, buttons, intro text), which are
   all flush with the article's own edge (Ced's 2026-09-15 report). */
.qrcode-download-page ul { text-align: left; margin-top: 1.2rem; padding-left: 1.3em; }
/* Same close-cross toolbar as .edit-modal-toolbar, but with nothing to
   discard (this popup previews a QR file, not a form) - flex-end instead
   of space-between keeps the X alone in the top-right corner. */
.qrcode-modal-toolbar { display: flex; justify-content: flex-end; margin: -0.4rem -0.4rem 0.4rem 0; }
.qrcode-modal-preview { display: block; margin: 0.8rem 0; max-width: 100%; }
.qr-formats-details, .family-qrcode-block { margin-top: 0.8rem; font-size: 0.85rem; }
.qr-formats-details summary, .family-qrcode-block summary { cursor: pointer; color: var(--ink-soft); }
.qr-formats-details ul { margin: 0.6rem 0 0; padding-left: 1.2rem; color: var(--ink-soft); }
.qr-formats-details li { margin-bottom: 0.35rem; }
.family-qrcode-block { padding-top: 0.8rem; border-top: 1px solid var(--line); }
.family-qrcode-block p.muted { margin-top: 0.6rem; }
/* The memorial's own QR code - its heading too, not just the image
   itself (Ced's 2026-09-10 follow-up) - and the two "Vous êtes..."
   links right below it (family access / lost link) - centered on the
   page rather than left-aligned by the module/paragraph default. */
.module-qrcode > h2 { text-align: center; }
.module-qrcode > img { display: block; margin: 0 auto; }
/* Same .field-hint negative margin-top bug as the other instances above
   (see the .export-actions/.confirm-form/.testimony-search-form + hint
   fixes) - here it's not a button but the QR code <img> right before it
   getting overlapped (Ced's 2026-09-15 report). */
.module-qrcode > img + .qrcode-formats-link { margin-top: 0.5rem; }
.qrcode-formats-link { text-align: center; }
.family-access-visitor-notice, .lost-link-notice { text-align: center; }

.why-section { max-width: 680px; margin: 3rem auto 0; }
.why-section h2 { margin-bottom: 0.4rem; }
.why-list { list-style: none; padding: 0; margin: 1.2rem 0 0; }
.why-list li { padding: 0.7rem 0; border-bottom: 1px solid var(--line); color: var(--ink-soft); }
.why-list li:last-child { border-bottom: none; }
.why-list a { margin-left: 0.4rem; font-weight: 500; }

/* The four-step "Creer/Raconter/Reunir/Transmettre" section, right under
   the hero (2026-09-10 UX brief, Lot 1 Phase 2) - the guiding principle
   made visible as a sequence rather than left as a single paragraph of
   prose in the hero lede. */
.how-it-works { max-width: 900px; margin: 3rem auto 0; text-align: center; }
.how-it-works h2 { margin-bottom: 1.6rem; }
.steps-list {
  list-style: none; padding: 0; margin: 0; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.8rem;
}
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-number {
  /* --accent (Heritage Gold) is documented as decorative-only, never a
     text/on-text color at this size (~2.4:1, fails WCAG AA) - the same
     issue as the pre-existing .badge-verified below. --accent-text is
     the darkened variant the palette itself sets aside for exactly
     this, at 5.8:1 with white text (Ralph-loop accessibility pass,
     2026-09-11). */
  width: 2.2rem; height: 2.2rem; border-radius: 50%; background: var(--accent-text);
  color: var(--paper-raised); display: flex; align-items: center; justify-content: center;
  font-weight: 600; margin-bottom: 0.6rem;
}
.step-item strong { margin-bottom: 0.3rem; }
.step-item p { color: var(--ink-soft); font-size: 0.9rem; margin: 0; }

.faq-section { max-width: 680px; margin: 3rem auto 0; }
.faq-section h2 { margin-bottom: 1rem; }

.suggestion-section { max-width: 680px; margin: 3rem auto 0; }
.suggestion-section h2 { margin-bottom: 0.4rem; }
.suggestion-section .stack-form { margin-top: 1rem; }

.family-page-cover { width: 100%; height: 220px; border-radius: var(--radius); overflow: hidden; margin-bottom: 1.2rem; }
.family-page-cover img { width: 100%; height: 100%; object-fit: cover; }
.family-page-edit { margin: 1rem 0 2rem; font-size: 0.9rem; }
.family-page-edit summary { cursor: pointer; color: var(--ink-soft); }
.family-page-edit .stack-form { margin-top: 0.8rem; }
.family-page-members { margin-top: 2rem; }
.faq-item { border-bottom: 1px solid var(--line); padding: 0.9rem 0; }
.faq-item summary { cursor: pointer; font-weight: 500; color: var(--ink); }
.faq-item p { margin: 0.7rem 0 0; color: var(--ink-soft); }

.memorial-grid-section h2 { margin-bottom: 1rem; }
.memorial-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; }
.memorial-card {
  display: block;
  text-decoration: none;
  color: inherit;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.memorial-card-portrait {
  position: relative;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-weight: 300;
  font-size: 2rem;
  overflow: hidden;
}
/* The small zoom gives detect_face_position()'s clamped crop (see
   app/face_detection.py's _CROP_EDGE_MARGIN) real margin to crop into
   instead of landing right at the photo's own edge - the fix for the
   recurring black/white edge-band bug (Bernard Breton, Leiner Hoyos,
   Gloria Alicia covers). */
.memorial-card-portrait img:not(.memorial-card-flag) { width: 100%; height: 100%; object-fit: cover; transform: scale(1.1); }
.memorial-card-flags {
  /* Up to 2 flags side by side (Memorial.get_card_flags(): nationality +
     country of death, when both are known and different) in the same
     bottom-right corner a single flag used to occupy alone. */
  position: absolute;
  bottom: 0.4rem;
  right: 0.4rem;
  display: flex;
  gap: 3px;
}
.memorial-card-flag {
  /* A real flag image, not a Unicode flag emoji - Windows (Segoe UI
     Emoji) never renders those as actual flags, only as the bare
     two-letter fallback glyphs (Ced's 2026-10-26 report, confirmed
     live: Peru's card showed a literal "PE"). An <img> from a flag CDN
     renders identically on every platform instead. */
  width: 20px;
  height: 15px;
  object-fit: cover;
  border-radius: 2px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}
.memorial-card-flag-globe {
  /* The globe emoji (unlike a flag sequence) renders as a real glyph on
     every platform including Windows, so no CDN image is needed here -
     shown instead of any flag when birth/nationality/death name 3
     genuinely different countries, rather than picking 2 and implying
     the third doesn't matter. */
  position: absolute;
  bottom: 0.3rem;
  right: 0.4rem;
  font-size: 1.15rem;
  line-height: 1;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
.card-placeholder { position: relative; width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; }
.card-placeholder-icon { position: absolute; inset: 0; opacity: 0.28; color: var(--ink-soft); }
.card-placeholder-icon svg { width: 100%; height: 100%; }
.card-initials { position: relative; z-index: 1; letter-spacing: 0.03em; color: var(--ink); }
.memorial-card-body { padding: 0.8rem 1rem; display: flex; flex-direction: column; gap: 0.2rem; }
.memorial-card-location { font-size: 0.82rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Secondary hero CTA, LinkedIn-style: a small, clearly subordinate link
   next to the primary button - "create a memorial" stays the one thing
   most visitors came for; this is an option for the minority who want
   it, not a second button competing for the same attention. */
/* Two sober, outlined pill buttons (2026-09-11 request) instead of an
   inline "link · link →" row - reuses the existing .btn-ghost.btn-small
   pattern already used elsewhere (e.g. the owner bar) rather than a new
   button style, so it reads as part of the same design system, not a
   marketing banner. */
.hero-secondary-cta { margin: 0.9rem 0 0; display: flex; justify-content: center; gap: 0.7rem; flex-wrap: wrap; }

.family-grid-section h2 { margin-bottom: 1rem; }
.family-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; }
.family-card {
  display: block;
  text-decoration: none;
  color: inherit;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.family-card-cover {
  /* Anchors .memorial-card-flags/.memorial-card-flag-globe (Ced's
     2026-11-07 request: family cards get the same flag badge individual
     memorial cards already have, see FamilyPage.get_card_flags()) -
     those are position:absolute, same as on .memorial-card-portrait. */
  position: relative;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-weight: 300;
  font-size: 2rem;
  overflow: hidden;
}
.family-card-cover img { width: 100%; height: 100%; object-fit: cover; }
.family-card-body { padding: 0.8rem 1rem; display: flex; flex-direction: column; gap: 0.2rem; }

.form-page { max-width: 640px; margin: 0 auto; }
.stack-form { display: flex; flex-direction: column; gap: 1rem; margin-top: 1.5rem; }
.stack-form label { display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.9rem; color: var(--ink-soft); font-weight: 500; min-width: 0; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; min-width: 0; }
.form-row > label { min-width: 0; }
/* Sits under a birth/death date input, letting a family record a date in
   its own calendar (Persian, Hijri, Hebrew...) instead of only Gregorian
   (Ced's 2026-11-06 request) - deliberately smaller/lighter than the date
   input itself since "gregorian" (the default) is the right choice for
   almost everyone and this should read as a secondary, optional control. */
.calendar-select { font-size: 0.8rem; padding: 0.35rem 0.5rem; align-self: flex-start; color: var(--ink-soft); }
input[type=text], input[type=url], input[type=color], input[type=email], input[type=search], textarea, select {
  font-family: inherit;
  font-size: 1rem;
  padding: 0.65rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper-raised);
  color: var(--ink);
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
textarea { resize: vertical; }
.consent-box { background: var(--accent-soft); border-radius: var(--radius); padding: 1rem 1.2rem; }

/* Visually sets "Gestion partagée" apart from the plain "Lien permanent"
   block right above it in Réglages - both used the exact same neutral
   presentation before, which a targeted UX audit found let the two
   very different links (public address vs. full editing power) blend
   together. A left accent border, distinct from the tinted boxes used
   elsewhere for calmer content, reads as "handle this one with more
   care" without resorting to alarming red. */
.settings-danger-zone {
  border-left: 3px solid #c0392b; padding: 0.2rem 0 0.2rem 1rem; margin: 1.5rem 0;
}
.settings-danger-zone h2 { margin-top: 0; }
.checkbox-label { display: flex; gap: 0.6rem; align-items: flex-start; font-weight: 400; font-size: 0.92rem; color: var(--ink); }
.checkbox-label.small { font-size: 0.85rem; }
.checkbox-label input { margin-top: 0.25rem; flex-shrink: 0; }

.legal-box { display: flex; flex-direction: column; gap: 0.9rem; border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.2rem; background: var(--paper-raised); }
.legal-language-notice { margin: 0; font-size: 0.82rem; }
.legal-box .checkbox-label span { font-size: 0.85rem; line-height: 1.45; color: var(--ink-soft); }

.legal-page { max-width: 720px; margin: 0 auto; }
.legal-page h2 { margin-top: 2rem; font-size: 1.15rem; }
.legal-page ul, .legal-page ol { padding-left: 1.2rem; }
.legal-page li { margin-bottom: 0.4rem; }
.content-guide .faq-section { margin: 2rem 0 0; max-width: none; }
.content-guide-cta { margin-top: 2.5rem; text-align: center; }

.banner { background: var(--accent-soft); border-radius: var(--radius); padding: 0.9rem 1.2rem; margin-bottom: 1.5rem; font-size: 0.95rem; }
.banner a { margin-left: 0.5rem; font-weight: 600; }
.banner-success { border: 1px solid var(--accent); }
.banner-warning { border: 1px solid #c0392b; background: rgba(192, 57, 43, 0.08); }
/* Shown right after confirming/accepting/rejecting a discovery item
   (app/templates/discovery.html) - a brief highlight so the action's
   outcome ("added to Identity", "ignored"...) is actually noticed
   rather than blending into a page that just silently redirected. */
.banner-flash { animation: banner-flash-in 0.5s ease-out; }
@keyframes banner-flash-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }

/* Sticky so it stays visible however far the visitor scrolls on
   demo-test-memorial (the one showcase memorial linked from the
   homepage, see home.html's "voir un memorial d'exemple" CTA) - a
   fictional profile must never read as a real one, even mid-scroll or
   on a link shared in isolation (see also is_example's SEO/Person
   JSON-LD guards in models.py/memorial.html). Discreet on purpose (a
   thin accent border, not an alarming color) to match the site's sober
   tone - clear, not jarring. Stuck at `top: 81px` (.site-header's own
   sticky height, same constant .section-nav below already relies on)
   rather than `top: 0`, so it stacks directly under the header instead
   of both racing for the same spot - and .section-nav's own sticky
   offset is pushed down by this banner's height (~40px) via :has()
   below, only on the one page that ever renders it. */
.example-banner {
  position: sticky; top: 81px; z-index: 15; text-align: center;
  background: var(--paper-raised); border-bottom: 1px solid var(--accent);
  color: var(--accent-text); font-size: 0.85rem; padding: 0.55rem 1rem;
  margin: 0 0 1.2rem;
}
body:has(.example-banner) .section-nav { top: 121px; }
.memory-cta-banner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.memory-cta-banner p { margin: 0; }
.memory-cta-banner .btn { flex-shrink: 0; }
.memory-cta-actions { display: flex; gap: 0.7rem; flex-wrap: wrap; min-width: 0; }
.memory-dashboard-heading { margin: 0 0 0.6rem; font-weight: 600; }
.memory-dashboard-stats { list-style: none; padding: 0; margin: 0 0 0.6rem; display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.9rem; color: var(--ink-soft); }
.memory-dashboard-discovery-link { margin: 0; font-size: 0.9rem; }
.dashboard-stat-details summary { cursor: pointer; color: var(--ink-soft); }
.dashboard-stat-details summary:hover { color: var(--ink); }
.dashboard-stat-detail-list { list-style: disc; margin: 0.3rem 0 0.3rem 1.4rem; padding: 0; font-size: 0.85rem; color: var(--ink-soft); }
.dashboard-stat-detail-list li { margin: 0.15rem 0; }
.dashboard-stat-detail-list li.dashboard-stat-empty { list-style: none; margin-left: -1.4rem; font-style: italic; }
/* This renders at the page's normal ~16px/weight-500 body size, not the
   "large text" WCAG carve-out --accent-label is documented safe for
   (3.72:1, AA-large only) - needs the 4.5:1 normal-text minimum, same
   family of fix as .step-number/.badge-verified (Ralph-loop
   accessibility pass, 2026-09-11). */
.media-edit-summary-prompt { color: var(--accent-text); font-weight: 500; }
.media-edit-summary-prompt svg { vertical-align: -1px; margin-right: 0.2rem; }
.media-context-hint { margin: 0.3rem 0 0.8rem; }

.owner-bar {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.6rem 1rem;
  margin-bottom: 1.5rem;
  font-size: 0.88rem;
}

.owner-bar-preview {
  background: color-mix(in srgb, var(--theme-color, #172A3A) 12%, var(--paper-raised));
  border-color: var(--theme-color, var(--line));
}

.memorial-hero { text-align: center; padding-bottom: 2rem; border-bottom: 1px solid var(--line); margin-bottom: 2rem; }
.memorial-cover { position: relative; height: 140px; border-radius: var(--radius); margin-bottom: -48px; background-size: cover; background-position: center; }
.memorial-portrait {
  position: relative;
  width: 96px; height: 96px; border-radius: 50%;
  background: var(--paper-raised);
  border: 4px solid var(--paper-raised);
  margin: 0 auto 1rem;
  display: flex; align-items: center; justify-content: center;
  font-family: "Jost", Futura, "Century Gothic", sans-serif; font-weight: 300; font-size: 2.2rem;
  box-shadow: var(--shadow);
  overflow: hidden;
}
.memorial-portrait img { width: 100%; height: 100%; object-fit: cover; transform: scale(1.1); }

.edit-pencil {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; color: var(--ink-soft); font-size: 0.85em;
  opacity: 0.85; border-radius: 50%;
}
.edit-pencil:hover { opacity: 1; }
.edit-pencil-cover, .edit-pencil-portrait {
  position: absolute; width: 28px; height: 28px;
  background: var(--ink, #172A3A); color: #fff; opacity: 0.85;
}
.edit-pencil-cover { top: 8px; right: 8px; }
.edit-pencil-portrait { bottom: -2px; right: -2px; border: 2px solid var(--paper-raised); }

/* LinkedIn-style photo editing affordance: hovering the cover banner or the
   profile circle shows its own darken overlay + pointer cursor, so each
   photo reads as independently clickable — the pencil badge stays put. */
.memorial-cover.owner-editable-photo, .memorial-portrait.owner-editable-photo { cursor: pointer; }
.memorial-cover.owner-editable-photo::before {
  content: ""; position: absolute; inset: 0; border-radius: var(--radius);
  background: rgba(0, 0, 0, 0); transition: background 0.15s ease;
}
.memorial-cover.owner-editable-photo:hover::before,
.memorial-cover.owner-editable-photo:focus-visible::before { background: rgba(0, 0, 0, 0.18); }
.memorial-portrait.owner-editable-photo img { transition: filter 0.15s ease; }
.memorial-portrait.owner-editable-photo:hover img,
.memorial-portrait.owner-editable-photo:focus-visible img { filter: brightness(0.8); }
.memorial-portrait.owner-editable-photo:focus-visible, .memorial-cover.owner-editable-photo:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.btn-icon-only { display: inline-flex; align-items: center; justify-content: center; padding: 0.5rem; }

.suggestion-box { margin-top: 1.2rem; font-size: 0.9rem; }
.suggestion-box summary { cursor: pointer; color: var(--ink-soft); }
.suggestion-box summary:hover { color: var(--ink); }

/* Collapsed-by-default optional fields on /creer (2026-09-10 UX brief,
   Lot 1 Phase 4) - a first-time visitor sees name + dates only; the rest
   of the form (still the same fields, same route) is one click away. */
.enrich-now-details { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem 1.2rem; }
.enrich-now-details summary { cursor: pointer; font-weight: 500; }
.enrich-now-details summary:hover { color: var(--accent-text); }
.enrich-now-details[open] summary { margin-bottom: 0.9rem; }

/* Short legal summary + expandable full text (Phase 5) - the checkbox
   itself is unchanged (same name, still required); only the wall of text
   moves behind a fold instead of always being fully shown. */
.legal-details { margin-top: 0.4rem; font-size: 0.85rem; }
.legal-details summary { cursor: pointer; color: var(--ink-soft); text-decoration: underline; }
.legal-details summary:hover { color: var(--ink); }
.legal-details p { margin-top: 0.5rem; color: var(--ink-soft); }
.suggestion-form { margin-top: 0.8rem; max-width: 480px; }
.honeypot-field { position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; left: -9999px; }

.invite-emails-label { display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.88rem; color: var(--ink-soft); margin: 1rem 0 0.6rem; }
.invite-emails-label textarea { width: 100%; box-sizing: border-box; }

dialog.edit-modal {
  width: min(560px, 90vw);
  max-height: 85vh;
  overflow-y: auto;
  border: none;
  border-radius: var(--radius);
  padding: 1.6rem;
  background: var(--paper-raised);
  box-shadow: var(--shadow);
}
dialog.edit-modal::backdrop { background: rgba(20, 18, 14, 0.55); }
.edit-modal-toolbar { display: flex; align-items: center; justify-content: space-between; margin: -0.4rem -0.4rem 0.4rem 0; }
.edit-modal-close-form { margin: 0; }
/* Both buttons sized to a real touch target (Ced's 2026-09-28 elderly-
   friendliness audit: the previous ~27-34px boxes were well under the
   ~44px guideline for someone with reduced tactile precision) and the
   close button carries its own plain-language label (not a bare "✕")
   so its role next to "Fermer sans enregistrer" reads unambiguously -
   min-height/width plus flex centering keeps it comfortable now that
   its content is a word, not a single glyph. */
.edit-modal-discard {
  border: none; background: transparent; font-size: 0.9rem; cursor: pointer;
  color: var(--ink-soft); padding: 0.6rem 0.8rem; border-radius: 8px; text-decoration: underline;
  min-height: 44px; display: inline-flex; align-items: center;
  transition: background 0.18s ease, color 0.18s ease;
}
.edit-modal-discard:hover { background: var(--paper); color: var(--ink); }
.edit-modal-close {
  border: none; background: transparent; font-size: 0.9rem; font-weight: 600; line-height: 1; cursor: pointer;
  color: var(--ink); padding: 0.6rem 0.9rem; border-radius: 8px;
  min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center;
  transition: background 0.18s ease, color 0.18s ease;
}
.edit-modal-close:hover { background: var(--paper); }
/* Sits right under the toolbar, visible the whole time the modal is
   open (not just as a toast after closing) - Eric N's 2026-09-29 support
   email mistook the old "Terminé" label for a final, irreversible action
   on his Transmission letter, so the reassurance needs to be seen at the
   moment of deciding to close, not only after. */
.edit-modal-reassurance { font-size: 0.85rem; margin: 0 0 0.8rem; }
.edit-modal-body h2:first-child, .edit-modal-body h3:first-child { margin-top: 0; }
/* Same touch-target reasoning as above, scoped to the panels themselves
   rather than raised globally (.btn-tiny in particular is also used in
   plenty of denser, non-elderly-facing contexts elsewhere on the site
   where the smaller size is fine) - the primary "Enregistrer"/"Ajouter"
   action in every one of these panels gets noticeably easier to hit. */
.edit-modal-body .btn-small { padding: 0.7rem 1.3rem; font-size: 1rem; min-height: 44px; }
.edit-modal-body .btn-tiny { padding: 0.5rem 0.9rem; font-size: 0.88rem; min-height: 38px; }
.memorial-hero h1 { font-size: 2rem; }
.nickname { color: var(--ink-soft); font-style: italic; font-size: 0.7em; }
.dates { color: var(--ink-soft); margin: 0.2rem 0 1rem; }
.presentation-box {
  max-width: 560px; margin: 1rem auto 0; padding: 1rem 1.2rem;
  background: var(--accent-soft); border-radius: var(--radius); font-style: italic;
}
.share-row { display: flex; justify-content: center; gap: 0.6rem; margin-top: 1.2rem; flex-wrap: wrap; }
.link-permanent-note { flex-basis: 100%; text-align: center; margin-top: 0.2rem; }
.contribution-nudge { margin-top: 1rem !important; text-align: center; }
.share-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px;
  padding: 0; border-radius: 999px; border: 1px solid var(--line);
  background: var(--paper-raised); color: var(--ink-soft); text-decoration: none; cursor: pointer;
}
.share-btn:hover { background: var(--accent-soft); color: var(--ink); }
.translate-btn { background: none; border: none; color: var(--ink); font-size: 0.82rem; cursor: pointer; padding: 0.2rem 0; text-decoration: underline; }
.translate-note { font-size: 0.82rem; font-style: italic; margin-top: 0.2rem; }
.auto-translation { margin-top: 0.4rem; padding: 0.6rem 0.8rem; background: var(--accent-soft); border-radius: 8px; }
/* Toggled back to the original text (translate.js's toggleOriginal():
   only the <p> hides, this box itself never does, so the label stays
   reachable to switch back) - Ced's 2026-10-25 report: the now-visible
   original text (.reader-preview, a separate, PRECEDING sibling of this
   box) read as one complete page, with this box's own background/
   padding still showing below it as a second, floating card holding
   nothing but the link. Collapsing it to a plain label exactly here
   matches how the never-yet-translated .translate-btn looks in that
   same spot - one page, one link at its bottom, in both states. */
.auto-translation:has(p[hidden]) { background: none; padding: 0; border-radius: 0; }
/* Now a real button, not a static disclaimer (Ced's 2026-09-28 report):
   a bilingual reader should see, at a glance, an obvious way back to the
   original text instead of a passive italic label - min-height keeps it
   a real touch target, same reasoning as the edit-panel buttons above. */
.auto-translation-label {
  display: block; font-size: 0.82rem; color: var(--ink); background: none; border: none;
  text-decoration: underline; cursor: pointer; padding: 0.2rem 0; margin: 0 0 0.3rem; min-height: 32px;
}
.auto-translation p:last-child { margin: 0; }

.module { padding: 2rem 0; border-bottom: 1px solid var(--line); }
.module:last-child { border-bottom: none; }

/* Facebook-style section nav (Ced's 2026-09-09 request): "Tout" plus one
   tab per section that actually rendered - hidden radios (see the
   .reader-control pattern used throughout this file) + :has() on the
   common .memorial-page ancestor drive both which tab shows (a tab only
   reveals itself once a matching .module-X exists ANYWHERE on the page,
   so this never has to repeat each module's own, sometimes elaborate,
   rendering condition) and which section is visible. Sticks just below
   the site's own sticky header while scrolling through any section -
   the exact behavior Ced described ("peu importe la section que l'on
   est en train de parcourir"). */
.section-nav {
  position: sticky; top: 81px; z-index: 9;
  background: var(--paper-raised); border-bottom: 1px solid var(--line);
}
/* The scroll-shadow above already signals "more tabs this way" (see the
   .section-nav-tabs comment), but a passive gradient alone turned out not
   to be enough for a mouse-only desktop visitor with no touch/trackpad
   swipe and no visible scrollbar (scrollbar-width: none) - a real
   screenshot (Ced's 2026-09-16 report) showed "Cérémonie & Sépulture"
   cut off at the edge with no obvious way to reach it. These sit right
   over that same fade zone and are the actual, clickable way to get
   there; section-nav.js shows/hides each one only when there's really
   more content in that direction, exactly the same overflow check the
   shadow's own "cover" layers already rely on. (No extra position:
   relative needed on .section-nav itself - position: sticky above
   already establishes a containing block for these, same as relative/
   absolute/fixed would.) */
.section-nav-scroll {
  position: absolute; top: 0; bottom: 0; width: 32px; z-index: 2;
  /* padding-bottom alone (was 0.55rem) couldn't drop the icon low enough
     to clear a tab label's own descenders ("g" in "Témoignages" - Ced's
     2026-10-16 report, both arrows, responsive) without going negative,
     which padding can't do. A first attempt (margin-bottom: -5px) pushed
     it too far the other way, past the nav's own bottom border (Ced's
     2026-10-14 follow-up) - measured directly (Range.getBoundingClientRect
     on the tab label's own text node vs the nav's border-bottom) rather
     than guessed again: plain flex-end with no extra margin already
     centers the icon almost exactly in the gap between the "g"'s real
     descender and the border below it, so the fix is to simply not push
     it any further. */
  display: flex; align-items: flex-end; justify-content: center; padding-bottom: 0; margin-bottom: 0;
  background: none; border: none; margin-left: 0; margin-right: 0; margin-top: 0; cursor: pointer;
  color: var(--ink-soft); opacity: 0; pointer-events: none;
  transition: opacity 0.15s, color 0.15s;
}
.section-nav-scroll.is-visible { opacity: 1; pointer-events: auto; }
.section-nav-scroll:hover, .section-nav-scroll:focus-visible { color: var(--ink); }
.section-nav-scroll svg { width: 16px; height: 16px; }
.section-nav-scroll-left { left: 0; }
.section-nav-scroll-right { right: 0; }
.section-nav-control { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* gap (not a per-tab trailing margin) spaces every tab identically,
   including the last one from the container's own edge - so the gap
   before "Tout" and the one after the last tab end up equal too, both
   made only of each tab's own symmetric horizontal padding plus the
   shared page gutter and this container's own symmetric padding (bumped
   up on Ced's 2026-09-10 follow-up: the shared gutter alone read as too
   tight before "Tout"). Once enough tabs are active (the padding/gap
   above genuinely can't fit every one of them on an ordinary screen -
   Ced's 2026-09-10 follow-up, a real screenshot of "Cérémonie &
   Sépulture" sitting right at the edge, unscrolled) the row scrolls
   instead of wrapping - by design, a single sticky line. What was
   actually missing wasn't more margin (there's no amount of padding
   that makes 9 tabs fit in less width) but a visible sign this genuinely
   scrolls: a soft edge fade in whichever direction still has more tabs,
   which disappears once that direction is fully scrolled - the classic
   background-attachment:local "scroll shadow" trick, no JS. The two
   "cover" layers (local - they travel with the scrolled content) hide
   each shadow exactly when its own edge is the true start/end of the
   row; the two shadow layers themselves (scroll - fixed to the
   container's own visible edge) only ever show through once their
   cover has scrolled out of the way. */
.section-nav-tabs {
  /* Right padding bumped again (0.8rem -> 1.4rem, 2026-09-11 follow-up):
     even scrolled all the way to the true end, the last tab's own label
     (whichever one that is in the active locale - "Cerémonie &
     Sépulture" in French, but this must hold for any language) needs a
     visibly clear gap before the bandeau's own edge, not just the
     scroll-shadow's fade doing the work. Left padding bumped to match,
     per the container's own symmetric-gutter design (see comment above). */
  display: flex; gap: 1.2rem; padding: 0 1.4rem; overflow-x: auto; scrollbar-width: none;
  /* Snap to each tab's own left edge (Ced's 2026-09-15 report: on some
     reloads the row lands scrolled to a spot that cuts a tab label
     mid-word, e.g. "Galerie & médias" showing as "alerie & médias" -
     whatever nudges scrollLeft there, whether a click, a browser reload
     restoring the previously-checked radio, or a manual swipe, this
     guarantees the row always comes to rest on a clean tab boundary
     instead). scroll-padding-inline matches the container's own padding
     so a snapped tab sits exactly where it would if scrolled there by
     hand, flush with the shared gutter. */
  scroll-snap-type: x mandatory; scroll-padding-inline: 1.4rem;
  background-image:
    linear-gradient(to right, var(--paper-raised) 30%, transparent),
    linear-gradient(to left, var(--paper-raised) 30%, transparent),
    radial-gradient(farthest-side at 0% 50%, rgba(23, 20, 15, 0.18), transparent),
    radial-gradient(farthest-side at 100% 50%, rgba(23, 20, 15, 0.18), transparent);
  background-repeat: no-repeat;
  background-size: 32px 100%, 32px 100%, 14px 100%, 14px 100%;
  background-position: left top, right top, left top, right top;
  background-attachment: local, local, scroll, scroll;
}
.section-nav-tabs::-webkit-scrollbar { display: none; }
.section-nav-tab {
  display: none; flex: none; padding: 0.85rem 0.2rem; margin: 0; scroll-snap-align: start;
  border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.88rem; color: var(--ink-soft); -webkit-tap-highlight-color: transparent;
}
.section-nav-tab:hover { color: var(--ink); }
.section-nav-tab-all { display: flex; } /* always available - "Tout" always has something to show */
.memorial-page:has(.module-biography) .section-nav-tab-biography,
.memorial-page:has(.module-timeline) .section-nav-tab-timeline,
.memorial-page:has(.module-media) .section-nav-tab-media,
.memorial-page:has(.module-transmission) .section-nav-tab-transmission,
.memorial-page:has(.module-archives) .section-nav-tab-archives,
.memorial-page:has(.module-ceremony) .section-nav-tab-ceremony,
.memorial-page:has(.module-testimonials) .section-nav-tab-testimonials,
.memorial-page:has(.module-community) .section-nav-tab-community {
  display: flex;
}
.memorial-page:has(#page-view-all:checked) .section-nav-tab-all,
.memorial-page:has(#page-view-biography:checked) .section-nav-tab-biography,
.memorial-page:has(#page-view-timeline:checked) .section-nav-tab-timeline,
.memorial-page:has(#page-view-media:checked) .section-nav-tab-media,
.memorial-page:has(#page-view-transmission:checked) .section-nav-tab-transmission,
.memorial-page:has(#page-view-archives:checked) .section-nav-tab-archives,
.memorial-page:has(#page-view-ceremony:checked) .section-nav-tab-ceremony,
.memorial-page:has(#page-view-testimonials:checked) .section-nav-tab-testimonials,
.memorial-page:has(#page-view-community:checked) .section-nav-tab-community {
  border-bottom-color: var(--accent); color: var(--accent-text); font-weight: 600;
}

/* A specific tab (not "Tout"): only that one section's module shows, at
   its full natural height - .modules-grid stays plain block flow, no
   grid/clamp rule below applies since none of them are scoped to it. */
.memorial-page:has(#page-view-biography:checked) .modules-grid .module:not(.module-biography),
.memorial-page:has(#page-view-timeline:checked) .modules-grid .module:not(.module-timeline),
.memorial-page:has(#page-view-media:checked) .modules-grid .module:not(.module-media),
.memorial-page:has(#page-view-transmission:checked) .modules-grid .module:not(.module-transmission),
.memorial-page:has(#page-view-archives:checked) .modules-grid .module:not(.module-archives),
.memorial-page:has(#page-view-ceremony:checked) .modules-grid .module:not(.module-ceremony),
.memorial-page:has(#page-view-testimonials:checked) .modules-grid .module:not(.module-testimonials),
.memorial-page:has(#page-view-community:checked) .modules-grid .module:not(.module-community) {
  display: none;
}
/* A short single section (Ced's 2026-09-11 report: "Ailleurs sur le web"
   and "Cérémonie & Sépulture" specifically, since those two happen to be
   the shortest for a real memorial) could sit so close to the document's
   own end that scrolling to align its top with the sticky nav's bottom
   would need MORE scroll room than the page actually has - the browser
   then clamps to its true max scroll, which is a position past where the
   nav's own sticky containing block still has room for it to stay
   stuck, so the nav visibly falls out of its stuck position and slides
   down (revealing --paper showing through above it, read as "a colored
   margin"). A generous min-height on the single-section container
   guarantees that room always exists, whatever the content or language -
   not scoped to any one section, since the same short-content situation
   could occur for any of them (a nearly-empty module, a short
   translation, etc.), not just these two on this one real page. */
.memorial-page:not(:has(#page-view-all:checked)) .modules-grid {
  min-height: calc(100vh - 12rem);
}

/* The reading column, only in the full single-section "Biographie" tab
   (Ced's 2026-09-12 request, "meilleurs standards de liseuses en
   ligne") - `main` alone already lets a line run up to ~90-100
   characters at 920px wide, well past the ~65-75 most reading-app
   guidance treats as comfortable. Left unscoped, this would also narrow
   the small dashboard-preview card the same book renders inside while
   browsing "Tout" (see the grid rules below) - not asked for and not
   wanted, since that card's own width already comes from the grid, not
   from a comfortable-reading concern. */
.memorial-page:not(:has(#page-view-all:checked)) .module-biography .reader {
  max-width: 44rem;
  margin-inline: auto;
}

/* "Tout" (default): every rendered section as a clamped card in a
   modular grid - a dashboard-widget overview rather than the full
   linear read, per Ced's request ("modules/blocs empilés... grille
   modulaire ou layout en mosaïque ou widget dashboard"). grid-auto-rows
   is a fine 1px increment, not a real row height - dashboard-grid.js
   gives every card an explicit grid-row-end: span N sized to its own
   rendered height, which is what actually turns this into a Pinterest-
   style packed mosaic instead of a plain CSS grid (where dense packing
   alone still leaves dead space under any short card next to a taller
   one, since a non-masonry grid's row height is shared by the whole
   row, not per column - Ced's 2026-11-01 "rends plus belles les
   dispositions" report on the candle-wall/Transmission cards). Without
   JS, every card just keeps its default span of 1 row (1px) - too short
   to matter, so the grid quietly falls back to ordinary same-row-height
   dense packing, never visibly broken. */
.memorial-page:has(#page-view-all:checked) .modules-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  grid-auto-flow: dense; grid-auto-rows: 1px; gap: 1.5rem; align-items: start;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module {
  /* min-width: 0 overrides the grid item's own implicit min-width: auto
     default (Ced's 2026-10-14 iPhone report, pinpointed to the exact
     character where it should have wrapped: Safari/WebKit computes a
     grid item's min-content width from the longest unbreakable text run
     even with overflow-wrap/word-break set, unlike Chrome's engine -
     the one thing every desktop/mobile *emulation* in this session
     couldn't catch, since it's still Chromium doing the actual layout
     regardless of viewport size). Harmless on every other browser,
     where this was already the effective behavior. */
  min-width: 0;
  max-height: 300px; overflow: hidden; position: relative;
  margin: 0; padding: 1.4rem 1.5rem; border: 1px solid var(--line); border-bottom: 1px solid var(--line);
  border-radius: var(--radius); background: var(--paper-raised); box-shadow: var(--shadow);
}
/* Real weight should follow real content weight (Ced's 2026-09-10
   report: every card reading identically, from a full life story down
   to an empty "suivre les mises à jour" form, made the whole overview
   feel like an admin dashboard rather than the start of a tribute).
   Biography, Ligne de vie and the gallery are where a memorial's actual
   substance lives, so they earn a wider card - `dense` packing above
   lets the smaller, more utilitarian cards (Communauté, Ailleurs sur le
   web, Cérémonie...) fill in around them regardless of which modules the
   family happens to have active, in whatever order. Two columns is the
   minimum width auto-fit needs before a span-2 card means anything - on
   a single narrow column (see the 640px breakpoint below) every card is
   already full-width on its own. */
@media (min-width: 700px) {
  .memorial-page:has(#page-view-all:checked) .modules-grid .module-biography,
  .memorial-page:has(#page-view-all:checked) .modules-grid .module-timeline,
  .memorial-page:has(#page-view-all:checked) .modules-grid .module-media {
    grid-column: span 2;
  }
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-timeline,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-media {
  max-height: 420px;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 4rem;
  background: linear-gradient(180deg, transparent, var(--paper-raised)); pointer-events: none;
}
/* The fade above assumes every card overflows its clamp - true for a
   full biography, but not for a short Transmission (one letter) or
   Testimonials (a couple of candles, no messages yet), where it read as
   a card cut off mid-content with nothing actually hidden beneath (same
   2026-11-01 report as the grid-row comment above). dashboard-grid.js
   measures each card's real scrollHeight vs. its clamped clientHeight
   and adds .no-clip only when nothing is actually being hidden. Without
   JS the fade still shows on every card, same as before this fix - a
   cosmetic miss, never a functional one. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module.no-clip::after {
  display: none;
}
/* An explicit "Lire la suite..." inside the fade, not just the implicit
   cutoff + invisible whole-card click target above (Ced's 2026-10-13
   report: the fade alone wasn't a discoverable enough invitation to
   keep reading). Reuses the same .module-card-link navigation (a label
   for the module's own radio tab), just made visible this once. */
.module-read-more { display: none; }
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .module-read-more {
  /* Bottom-right, flush with the CREAM PAGE's own right edge, not the
     brown leather cover around it (Ced's 2026-10-24 report: right:1.5rem
     - exactly .reader's own right padding, lining the pill up with the
     book's outer cover edge - repeatedly read as "misaligned", because a
     visitor's eye anchors the pill to the readable text it sits beside,
     not to a decorative margin outside the page. .reader-body (the cream
     page itself) sits a further 1.5rem inside .reader's own padding, so
     3rem total clears both insets and lands the pill right where the
     page's own edge is). An opaque backdrop rather than trusting the
     card's own fade-mask (.module::after, a 4rem gradient to
     --paper-raised) to have already hidden whatever text sits here -
     that gradient only fades ENOUGH for an average case; a longer-
     wrapped biography (a French translation runs longer than English, a
     narrow mobile viewport wraps more per line) still lands fully
     legible text directly behind this fixed position, genuinely
     overlapping it (Ced's 2026-10-19 report, still reproducing after the
     first position fix). A small opaque label physically covers whatever
     is behind it regardless of content length or viewport. */
  display: inline-block; position: absolute; right: 3rem; bottom: 0.7rem; z-index: 2;
  background: var(--paper-raised); padding: 0.25rem 0.65rem; border-radius: 999px;
  box-shadow: 0 1px 3px rgba(23, 42, 58, 0.18);
  color: var(--accent-text); font-weight: 600; font-size: 0.85rem; cursor: pointer;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .module-read-more:hover {
  text-decoration: underline;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography.no-clip .module-read-more {
  display: none;
}
/* The gallery preview needs more room than 300px for its own heading
   plus one full row of square covers and their captions - otherwise
   every caption gets hard-clipped mid-line, unreadable (Ced's
   2026-09-09 request: "lire complètement un titre de media"). The
   "+ Créer un album" tile is an owner affordance, not content to
   preview, and competing with a real album for that first, fully-
   visible slot - left out of "Tout" specifically for that reason. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-media { max-height: 400px; }
.memorial-page:has(#page-view-all:checked) .modules-grid .module-media .album-card-add { display: none; }
/* Smaller, fixed-size covers than the gallery's own responsive grid -
   in a single, narrow mosaic column (any viewport under ~750px, where
   .modules-grid itself is down to one column) the gallery's normal
   150px-minimum columns force one huge square per row, pushing its own
   caption below the clamp before it can even render. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-media .album-grid {
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
}
/* Click-anywhere-on-the-tile (Ced's 2026-09-09 request - "n'importe quel
   endroit"), only while "Tout" is selected: covers the whole card ON TOP
   of its normal-flow content (z-index puts it above any non-positioned
   descendant - see the CSS2.1 stacking order), so a click lands on this
   first almost everywhere in the card, including over plain text. The
   one deliberate exception is the owner's own edit-pencil - a distinct,
   important action that must never be silently swallowed - promoted
   above the overlay with its own higher z-index. */
.module-card-link { display: none; }
.memorial-page:has(#page-view-all:checked) .modules-grid .module-card-link {
  display: block; position: absolute; inset: 0; z-index: 1; cursor: pointer;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module .edit-pencil {
  position: relative; z-index: 2;
}
/* Same reasoning as the edit-pencil above, for the translate/show-
   original controls (Ced's 2026-09-28 report: clicking "Afficher
   l'original" from "Tout" just opened the Biographie tab instead of
   toggling - the click was landing on the invisible card-link overlay,
   one z-index above these buttons' own unpositioned, normal-flow
   stacking level, not on the button itself). */
.memorial-page:has(#page-view-all:checked) .modules-grid .module .translate-btn,
.memorial-page:has(#page-view-all:checked) .modules-grid .module .auto-translation-label {
  position: relative; z-index: 2;
}
/* "Tourner la page" itself never shows in "Tout" at all (Ced's
   2026-10-20 request) - .module-read-more below is the single "see
   more" entry point there regardless of whether this card's biography
   happens to need a fold, worded the same way ("Lire la suite...") and
   positioned the same way (bottom-right pill) either way. Two earlier
   attempts tried to reposition/reword THIS element instead and both
   broke on the same root cause: position:absolute here resolves against
   the nearest positioned ancestor, which is .reader-body (itself
   position:relative, for its own unrelated decorative needs), not
   .module-biography - .reader-body's own height grows with whatever
   content it holds (un-clamped, unlike .module-biography's max-height:
   420px + overflow:hidden), so a translated biography replacing the
   preview with a much taller block pushed this control thousands of
   pixels below the visible card (Ced's 2026-10-19 report: the link
   disappeared once translated) regardless of how this element's own
   bottom/right offsets were set. .module-read-more sidesteps this
   entirely by being a direct child of .module-biography itself (see its
   own comment below) - simplest fix is to use that one control
   everywhere in "Tout" instead of also trying to fix this one's
   positioning. reader.js opens this fold (and keeps it open) once
   .module-read-more sends the visitor to Biographie, so nothing is
   actually lost by hiding it here. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-page {
  display: none;
}
/* The biography preview should read as "the first lines actually
   written" (Ced's 2026-09-09 request: "uniquement les textes
   renseignés"), not the reader's own chrome (reading time, comfort
   buttons, chapter title/rank/triangle, pagination, next-chapter link) -
   none of that is content, and there's no room for it in a 300px card
   anyway. Hiding .reader-accordion-title doesn't break the CSS that
   shows its .reader-accordion-body (:checked + title + body still
   matches a display:none sibling, same DOM position). */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-meta,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-audio-player,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-accordion-title,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-pagination,
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-next-chapter {
  display: none;
}
/* The preview must always show the foreword (or, failing that, chapter
   1 - whichever is genuinely first) - never whatever chapter the family
   happened to leave open while actually reading (Ced's 2026-09-10
   report: going back to "Tout" after reading could land on a closed
   accordion, an empty-looking card). Overrides the live :checked state
   entirely for this preview only - the family's real reading position
   inside the "Biographie" tab itself is untouched. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-accordion-body {
  display: none !important;
}
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-body > .reader-accordion-input:first-of-type + .reader-accordion-title + .reader-accordion-body {
  display: block !important;
}
/* Reserves real empty space below the visible chapter's own text, inside
   the same fixed-height clipped card (Ced's 2026-10-14 report: "Lire la
   suite" overlapping the biography's own last visible line) - a long
   chapter can otherwise fill the card right up to its bottom edge, so the
   link ends up sitting on top of real sentence text instead of in clear
   space below it. This padding eats into the SAME fixed card height the
   text itself is clipped against, so it reliably pushes the text's own
   cutoff point higher rather than just adding more total height. */
.memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .reader-body {
  padding-bottom: 4.5rem;
}
@media (max-width: 640px) {
  .memorial-page:has(#page-view-all:checked) .modules-grid { grid-template-columns: 1fr; }
}

.timeline { list-style: none; padding: 0; margin: 0; }
.timeline-date { display: block; font-size: 0.8rem; color: var(--accent-text); font-weight: 600; }
/* A photo, not just a text link, earns this its place in the "Tout"
   preview - the gallery already gets to show real covers there (see
   .modules-grid .module-media above), a linked album deserves the same
   (Ced's 2026-09-10 request). */
.timeline-linked-album { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.4rem; font-size: 0.85rem; }
.timeline-linked-album img { width: 36px; height: 36px; border-radius: 6px; object-fit: cover; flex-shrink: 0; }

/* "Ligne de vie" styles, chosen by the family (Ced's 2026-09-09 request -
   see TIMELINE_STYLES in app/routers/_shared.py for the allowlist and
   panels/_timeline.html for the picker). Classic is the original design,
   unchanged in look. */
.timeline-classic { border-left: 2px solid var(--accent-soft); }
.timeline-classic li { padding: 0 0 1.4rem 1.2rem; position: relative; }
.timeline-classic li::before {
  content: ""; position: absolute; left: -6.5px; top: 4px;
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent);
}

/* Cards: each event as its own bordered, shadowed block - no connecting
   line, reads as a stack of distinct memories rather than one thread. */
.timeline-cards { display: flex; flex-direction: column; gap: 0.9rem; }
.timeline-cards li {
  padding: 1rem 1.1rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper-raised); box-shadow: var(--shadow);
}

/* Alternating: a centered line, events zigzagging left/right of it - a
   classic "family history" timeline read. Two narrow columns either
   side of a centered line stop having room to breathe on a phone, so
   it collapses back to the classic single-column line under 680px. */
.timeline-alternating { position: relative; display: flex; flex-direction: column; gap: 1.6rem; }
.timeline-alternating::before {
  content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px;
  background: var(--accent-soft); transform: translateX(-50%);
}
.timeline-alternating li {
  position: relative; width: calc(50% - 1.6rem);
  padding: 0.9rem 1rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper-raised); box-shadow: var(--shadow);
}
.timeline-alternating li::before {
  content: ""; position: absolute; top: 1.1rem;
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent);
}
.timeline-alternating li:nth-child(odd) { align-self: flex-start; }
.timeline-alternating li:nth-child(odd)::before { right: -1.85rem; }
.timeline-alternating li:nth-child(even) { align-self: flex-end; }
.timeline-alternating li:nth-child(even)::before { left: -1.85rem; }
@media (max-width: 680px) {
  .timeline-alternating { display: block; }
  .timeline-alternating::before { left: -6.5px; transform: none; }
  .timeline-alternating li { width: auto; margin-left: 1.2rem; }
  .timeline-alternating li:nth-child(odd), .timeline-alternating li:nth-child(even) { align-self: auto; }
  .timeline-alternating li::before { left: -1.85rem !important; right: auto !important; }
}

/* Branches: a thicker, gradient-shaded trunk instead of a plain thin
   line, each step marked with a leaf (the classic two-corners-rounded
   "CSS leaf" shape - a square with two opposite corners at border-radius
   0 and the other two at 100%, rotated 45°) instead of a dot - Ced's
   2026-09-10 request for a genuinely tree-like read, not just another
   geometric layout. */
.timeline-branches { position: relative; list-style: none; }
.timeline-branches::before {
  content: ""; position: absolute; left: 5px; top: 0; bottom: 0; width: 3px; border-radius: 3px;
  background: linear-gradient(180deg, var(--accent-text), var(--accent) 55%, var(--accent-soft));
}
.timeline-branches li { padding: 0 0 1.5rem 1.7rem; position: relative; }
.timeline-branches li::before {
  content: ""; position: absolute; left: -1.5px; top: 2px; width: 15px; height: 15px;
  background: var(--accent); border-radius: 0 100% 0 100%; transform: rotate(-45deg);
  box-shadow: 0 1px 2px rgba(23, 42, 58, 0.2);
}

/* Blossom: a delicate dotted stem instead of a solid line, each step
   marked with a small four-petal flower (layered radial-gradients, no
   extra markup or image needed) instead of a dot. */
.timeline-blossom { position: relative; list-style: none; }
.timeline-blossom::before {
  content: ""; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px;
  background-image: repeating-linear-gradient(180deg, var(--accent-soft) 0 4px, transparent 4px 9px);
}
.timeline-blossom li { padding: 0 0 1.6rem 1.9rem; position: relative; }
.timeline-blossom li::before {
  content: ""; position: absolute; left: -3px; top: 0; width: 17px; height: 17px;
  background-image:
    radial-gradient(circle 4.5px at 50% 16%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 4.5px at 50% 84%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 4.5px at 16% 50%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 4.5px at 84% 50%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 3px at 50% 50%, var(--accent-text) 95%, transparent 100%);
}

.media-year-heading { margin: 1.5rem 0 0.6rem; font-size: 1.05rem; color: var(--ink-soft); }
.media-year-heading:first-of-type { margin-top: 0.5rem; }
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 0.8rem; }
.media-grid img { width: 100%; height: 140px; object-fit: cover; border-radius: 10px; }
.media-grid .media-inline-video { width: 100%; height: 140px; object-fit: cover; border-radius: 10px; background: #000; }
.media-figure-audio { grid-column: 1 / -1; }
.media-inline-audio { width: 100%; }
.media-figure[data-lightbox] { cursor: zoom-in; }
.media-figure[data-lightbox] img { transition: opacity 0.15s ease; }
.media-figure[data-lightbox]:hover img { opacity: 0.85; }
.media-figure { position: relative; }
.album-owner-controls { position: absolute; top: 0.4rem; left: 0.4rem; right: 0.4rem; display: flex; justify-content: space-between; z-index: 3; }
.album-owner-controls form { margin: 0; }
.media-grid figcaption { font-size: 0.8rem; color: var(--ink-soft); margin-top: 0.3rem; }
.mood-chip {
  display: inline-flex; align-items: center; gap: 0.3rem; margin-left: 0.35rem; padding: 0.05rem 0.55rem 0.05rem 0.4rem;
  border-radius: 999px; background: var(--accent-soft); color: var(--ink);
  font-size: 0.75rem; font-style: normal;
}
.mood-chip::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }

.lightbox {
  position: fixed; inset: 0; background: rgba(10, 9, 7, 0.92); z-index: 100;
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; padding: 3rem 4rem;
}
.lightbox[hidden] { display: none; }
.lightbox-image { max-width: 100%; max-height: 75vh; object-fit: contain; border-radius: 6px; }
.lightbox-caption { color: var(--paper); font-size: 0.9rem; text-align: center; max-width: 60ch; }
.lightbox-close, .lightbox-prev, .lightbox-next {
  position: absolute; background: rgba(255, 255, 255, 0.12); color: #fff; border: none; border-radius: 50%;
  cursor: pointer; font-size: 1.4rem; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
}
.lightbox-close:hover, .lightbox-prev:hover, .lightbox-next:hover { background: rgba(255, 255, 255, 0.24); }
.lightbox-close { top: 1.2rem; right: 1.2rem; }
.lightbox-prev { left: 1.2rem; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 1.2rem; top: 50%; transform: translateY(-50%); }

.places-list, .letters-list, .archives-list, .testimonials-list, .fact-history { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.9rem; }
.testimonials-list li { background: var(--paper-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }

/* Places, now shown inside the merged "Ligne de vie" section (see
   memorial.html) rather than their own "Carte de vie" section - styled
   as small cards (same treatment as .testimonials-list li) so they read
   as a consistent, self-contained group rather than a plain bold-text
   list sitting awkwardly under the timeline's gold-dot rail. */
.places-frame { margin-top: 1.8rem; padding-top: 1.4rem; border-top: 1px solid var(--line); }
.places-frame h3 { font-size: 1rem; color: var(--ink-soft); margin: 0 0 0.8rem; }
/* Leaflet needs an explicit height on its container - shown only once
   3+ distinct cities are geocoded (see memorial.html/models.py). */
.places-map { height: 320px; border-radius: var(--radius); overflow: hidden; margin-top: 0.8rem; z-index: 0; }
.place-card { background: var(--paper-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }

.testimonial-form { margin-top: 1.5rem; max-width: 480px; }

/* Ambient, dignified framing above the candle/message forms (Ced's
   2026-10-27 request) - never a gate, just a one-line reminder of what
   this space is for, dismissed once per visitor via localStorage. */
.respect-reminder {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.65rem 1rem;
  margin-bottom: 1rem;
  font-size: 0.85rem;
  font-style: italic;
  color: var(--ink-soft);
}
.respect-reminder-dismiss {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.2rem;
  line-height: 1;
  color: var(--ink-soft);
  padding: 0;
  flex-shrink: 0;
}

.candle-light { display: flex; align-items: flex-start; gap: 0.7rem; flex-wrap: wrap; margin-bottom: 1rem; }
.btn-candle { display: inline-flex; align-items: center; gap: 0.4rem; }
.candle-count { font-size: 0.85rem; align-self: center; }

.candle-form-details summary { list-style: none; cursor: pointer; }
.candle-form-details summary::-webkit-details-marker { display: none; }
.candle-form-details summary::marker { content: ""; }
.candle-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.7rem;
  padding: 0.9rem;
  background: var(--paper-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-width: 420px;
}
.candle-style-picker { display: flex; gap: 0.5rem; }
.candle-style-option { cursor: pointer; display: inline-flex; }
.candle-style-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.candle-style-option input:checked + .candle-visual { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 4px; }
.candle-name-input, .candle-message-input {
  flex: 1 1 100%;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.9rem;
  font-family: inherit;
}

/* The candle itself is pure CSS - no image asset at all - flickering via
   a cheap transform+opacity animation (GPU-friendly, no layout
   thrashing), so a wall of dozens of these on a well-visited memorial
   costs nothing extra to render. A two-tone flame (a warm outer body plus
   a brighter ::after core, same two-layer shape every real candle flame
   actually has) reads as far more convincing than a single flat blob. */
.candle-visual { position: relative; display: inline-flex; flex-direction: column; align-items: center; width: 22px; height: 36px; }
.candle-flame {
  position: relative;
  width: 11px; height: 16px;
  border-radius: 50% 50% 50% 50% / 65% 65% 35% 35%;
  background: radial-gradient(circle at 50% 70%, #ff9d2e, #ff7a1a 70%, #e85d00 100%);
  box-shadow: 0 0 10px 3px rgba(255, 150, 40, 0.5), 0 0 3px 1px rgba(255, 210, 120, 0.85);
  animation: candle-flicker 2.6s ease-in-out infinite;
  transform-origin: 50% 100%;
}
.candle-flame::after {
  content: "";
  position: absolute;
  left: 50%; bottom: 10%;
  width: 5px; height: 9px;
  border-radius: 50% 50% 50% 50% / 65% 65% 35% 35%;
  background: radial-gradient(circle at 50% 65%, #fffdf0, #ffe79a 60%, #ffcf4d 100%);
  transform: translateX(-50%);
}
.candle-body { width: 14px; height: 20px; border-radius: 2px; margin-top: -2px; position: relative; overflow: hidden; }
.candle-body::after {
  /* A soft diagonal highlight for a touch of wax-like dimensionality,
     instead of a flat tinted rectangle. */
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0.4), transparent 35%, transparent 65%, rgba(0, 0, 0, 0.12));
}
.candle-style-classic .candle-body { background: linear-gradient(#f5f1e6, #e3dcc8); }
.candle-style-amber .candle-body { background: linear-gradient(#f2b35a, #c97d1f); }
.candle-style-rose .candle-body { background: linear-gradient(#f3b8c9, #d9718f); }
.candle-style-azure .candle-body { background: linear-gradient(#aed7ef, #4f93b8); }

@keyframes candle-flicker {
  0%, 100% { transform: scale(1) rotate(-1.5deg); opacity: 1; }
  50% { transform: scale(0.92) rotate(1.5deg); opacity: 0.88; }
}
@media (prefers-reduced-motion: reduce) {
  .candle-flame, .candle-flame::after { animation: none; }
}

.candle-wall { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.9rem 0.6rem; }
.candle-wall-item { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; width: 64px; text-align: center; }
/* A handful of distinct timing offsets, cycled via nth-child, keeps a
   wall of many candles from flickering in perfect unison (which reads as
   obviously animated rather than organic) - negative delays start each
   one partway through its own cycle immediately, no JS/randomness needed. */
.candle-wall-item:nth-child(2n) .candle-flame { animation-duration: 2.1s; animation-delay: -0.4s; }
.candle-wall-item:nth-child(3n) .candle-flame { animation-duration: 2.9s; animation-delay: -1.1s; }
.candle-wall-item:nth-child(5n) .candle-flame { animation-duration: 2.3s; animation-delay: -0.7s; }
.candle-wall-item:nth-child(7n) .candle-flame { animation-duration: 3.1s; animation-delay: -1.6s; }
.candle-wall-name { font-size: 0.72rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

.customize-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; gap: 1rem; flex-wrap: wrap; }
.customize-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.2rem; align-items: start; }
.panel { background: var(--paper-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.4rem; }
.panel .stack-form { margin-top: 1rem; }
.panel-intro { margin-top: -0.4rem; margin-bottom: 0.8rem; font-size: 0.88rem; line-height: 1.4; }
.field-hint { margin-top: -0.5rem; font-size: 0.8rem; }

/* "Suggestions IA pour cette section" button + its explanatory hint
   (see _biography.html, _geography.html, _timeline.html) sit in plain
   block flow after a </form>, not inside one - .field-hint's -0.5rem
   margin-top (meant to hug a hint directly under its own form label,
   inside a flex .stack-form) doesn't apply here, and was instead
   pulling the hint text up into the button above it - most visible
   once the button's own text wrapped onto two lines. */
.ai-assist-btn { display: block; width: fit-content; margin-top: 0.8rem; }
.ai-assist-hint { margin-top: 0.5rem; font-size: 0.8rem; }
.field-error { margin: -0.3rem 0 0.4rem; font-size: 0.82rem; color: #c0392b; }
[aria-invalid="true"] { border-color: #c0392b; }
.export-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.export-actions + .field-hint { margin-top: 0.5rem; }
/* Same fix as .export-actions + .field-hint above, for the "Régénérer le
   lien" .confirm-form (_settings.html) followed by two .field-hint - the
   negative margin-top was pulling the hint text up into the button. */
.confirm-form + .field-hint { margin-top: 0.5rem; }
/* Same fix again, for the "Repérer des personnes à inviter" button
   (_invite.html) followed by its empty-state .field-hint - the negative
   margin-top made the button and the hint text below it visually run
   together (Ced's 2026-09-15 report). */
.testimony-search-form + .field-hint { margin-top: 0.5rem; }
/* Same fix again, for the "Envoyer par e-mail" .invite-send-form
   (_invite.html) followed by the "ou copiez ce message..." alternative
   hint - without this the negative margin-top pulled that hint text up
   into the submit button above it (Ced's 2026-09-15 reorder request). */
.invite-send-form + .invite-copy-alt-hint { margin-top: 0.5rem; }
.editor-toolbar { display: block; margin-bottom: 0.3rem; }
.editor-toolbar .btn-tiny { padding: 0.2rem 0.6rem; }
.editor-bold-btn { font-weight: 700; }
.prose { text-align: justify; line-height: 1.6; }

/* "Liseuse de vie" (2026-09-07, extended 2026-09-09 into a per-section
   accordion, refined again 2026-09-09 into a bound-book presentation):
   the public biography reads as a small, personal notebook rather than a
   web text block. Deliberately zero JavaScript - the fold-more-text (for
   a biography with no **bold** sections at all) is a native
   <details>/<summary>, and both the section accordion and
   reading-comfort settings are hidden radio inputs. The accordion reuses
   one purely structural CSS pattern -
   `.reader-accordion-input:checked + .reader-accordion-title + .reader-accordion-body`
   (adjacent-sibling chaining, not an ID lookup) - so it works for any
   number of sections without a per-ID rule, and clicking a *different*
   section's title natively closes the current one and opens the new one
   (same-name radio group), which is exactly what the "next chapter" link
   reuses to advance the reading - its click plays the same page-flip
   animation as the title-triangle, since both just check the same
   radio. */
.reader {
  --reader-bg: var(--paper-raised);
  position: relative;
  background:
    repeating-linear-gradient(128deg, rgba(0, 0, 0, 0.07) 0 2px, transparent 2px 7px),
    radial-gradient(140% 120% at 18% 0%, #6b4530 0%, #4a2f1f 45%, #2f1c10 100%);
  border: 1px solid #23140b;
  border-radius: var(--radius);
  box-shadow: var(--shadow), inset 0 0 0 1px rgba(199, 162, 90, 0.28), inset 0 2px 3px rgba(255, 255, 255, 0.1);
  padding: 1.4rem 1.5rem 1.4rem 2.5rem;
}
.reader::before {
  /* the bound spine along the cover's left edge */
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 1rem;
  background: linear-gradient(90deg, #1c0f08, #4a2f1f 65%, transparent);
  border-radius: var(--radius) 0 0 var(--radius);
  box-shadow: inset -1px 0 0 rgba(199, 162, 90, 0.3);
}
.reader::after {
  /* a thin gilt frame set into the leather, like an embossed cover */
  content: ""; position: absolute; inset: 0.5rem 0.5rem 0.5rem 1.35rem;
  border: 1px solid rgba(199, 162, 90, 0.38); border-radius: calc(var(--radius) - 5px);
  pointer-events: none;
}
.reader-control { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* The page itself, set into the leather cover - keeps the reading
   comfort/sepia toggle scoped to the paper, not the cover around it.
   Notebook cue #1: a soft inner margin rule, like a school cahier's
   ruled margin - everything in the reader sits to its right. */
.reader-body {
  position: relative;
  background: var(--reader-bg);
  border-radius: 8px;
  padding: 1.5rem 1.6rem 1.5rem 2.1rem;
  box-shadow: inset 0 1px 2px rgba(23, 42, 58, 0.08), 0 1px 0 rgba(255, 255, 255, 0.4);
  font-family: "Literata", Georgia, "Times New Roman", serif; color: var(--ink);
  transition: background-color 0.3s ease;
}
/* A direct override rather than reassigning --reader-bg on a :has() match:
   keeps the sepia swap a plain conditional rule (like the font-size ones
   below) instead of relying on a custom property to redefine a value
   that .reader-body also transitions - simpler to reason about and one
   less moving part between the :has() match and the property it sets. */
.reader:has(#reader-theme-sepia:checked) .reader-body { background-color: #F1E4C8; }
.reader:has(#reader-theme-sepia:checked) .reader-preview-folded::after { background: linear-gradient(180deg, transparent, #F1E4C8); }
.reader-body::before {
  content: ""; position: absolute; top: 0.2rem; bottom: 0.2rem; left: 1.15rem;
  width: 1px; background: color-mix(in srgb, var(--accent) 35%, transparent);
}
.reader:has(#reader-font-small:checked) .reader-body { font-size: 0.92rem; }
.reader:has(#reader-font-large:checked) .reader-body { font-size: 1.18rem; }

.reader-meta {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.6rem;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.82rem; color: var(--ink-soft); margin: 0 0 1.1rem;
}
.reader-comfort { display: inline-flex; flex-wrap: wrap; gap: 0.5rem; }
/* Ced's 2026-09-10 report: on narrow viewports the 5 buttons used to wrap
   one by one wherever they stopped fitting, which could split "Clair"/
   "Sépia" onto two different lines. Ced's 2026-09-12 follow-up asked to
   also tighten the space these controls take - both are solved the same
   way: font-size (A-/A/A+) and theme (Clair/Sépia) are each one shared-
   border segment (a single .reader-comfort-group, same class for both,
   `overflow:hidden` clipping the buttons' own transparent borders to its
   rounded corners) instead of 5 individually-bordered, individually-
   gapped buttons - the wrap point (if any) now only ever falls between
   the two segments, never inside one, and removing 4 redundant borders/
   gaps visibly shrinks the whole control row. */
.reader-comfort-group {
  display: inline-flex; flex-wrap: nowrap;
  border: 1px solid var(--line); border-radius: 6px; overflow: hidden;
}
.reader-comfort-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.5rem; min-height: 2.75rem; padding: 0 0.35rem;
  border: 1px solid transparent; cursor: pointer;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.78rem; color: var(--ink-soft); background: transparent;
  -webkit-tap-highlight-color: transparent;
}
.reader-comfort-btn + .reader-comfort-btn { border-left-color: var(--line); }
.reader-comfort-btn:hover { border-color: var(--accent); color: var(--ink); }
.reader-comfort-btn:active { background: var(--accent-soft); }
/* "Écouter cet article" (Ced's 2026-10-05 request) - app/static/js/
   reader_audio.js drives this purely via data-* attributes and a
   handful of classes; the progress bar's fill width is set inline by
   that script (an estimate - window.speechSynthesis exposes no real
   playback position), never computed here. */
.reader-audio-player { margin: 0 0 1.1rem; }
/* Who wrote the biography and their link to the deceased (Ced's
   2026-11-08 request: visitors had no way to know this at all) -
   deliberately small and italic, a discreet byline rather than a banner,
   sitting above the text it's attributed to. */
.reader-author-note { font-style: italic; font-size: 0.85rem; color: var(--ink-soft); margin: 0 0 1.1rem; }
.reader-audio-toggle {
  display: inline-flex; align-items: center; gap: 0.5rem;
  border: 1px solid var(--line); border-radius: 999px; padding: 0.4rem 0.9rem;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.82rem; color: var(--ink-soft); background: transparent; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.reader-audio-toggle:hover { border-color: var(--accent); color: var(--ink); }
.reader-audio-icon { font-size: 0.7rem; }
.reader-audio-progress-wrap { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.5rem; }
.reader-audio-progress-wrap[hidden] { display: none; }
.reader-audio-progress {
  flex: 1; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden;
}
.reader-audio-progress-fill { height: 100%; width: 0; background: var(--accent); border-radius: 2px; }
.reader-audio-time { font-size: 0.75rem; color: var(--ink-soft); white-space: nowrap; }
.reader-audio-info {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.1rem; height: 1.1rem; border-radius: 50%; border: 1px solid var(--line);
  font-size: 0.68rem; color: var(--ink-soft); cursor: help;
}
/* Ced's 2026-09-12 report: switching to "Sépia" visibly shifted the
   whole Clair/Sépia segment (and everything sharing its row) - the
   `font-weight: 600` this rule used to add to the checked label made
   its text render wider than the unchecked state (measured: "Sépia"
   grew from 42.8px to 44.1px the moment it became checked), so every
   toggle nudged the segment's own width and, with it, its position in
   the row. Border/background/text-color alone are still three distinct
   visual cues for "this one is selected" - dropping the fourth (weight)
   is what removes the width change, not a smaller stylistic tweak. */
.reader:has(#reader-font-small:checked) label[for="reader-font-small"],
.reader:has(#reader-font-default:checked) label[for="reader-font-default"],
.reader:has(#reader-font-large:checked) label[for="reader-font-large"],
.reader:has(#reader-theme-light:checked) label[for="reader-theme-light"],
.reader:has(#reader-theme-sepia:checked) label[for="reader-theme-sepia"] {
  border-color: var(--accent); color: var(--accent-text); background: var(--accent-soft);
}
#reader-font-small:focus-visible ~ .reader-body label[for="reader-font-small"],
#reader-font-default:focus-visible ~ .reader-body label[for="reader-font-default"],
#reader-font-large:focus-visible ~ .reader-body label[for="reader-font-large"],
#reader-theme-light:focus-visible ~ .reader-body label[for="reader-theme-light"],
#reader-theme-sepia:focus-visible ~ .reader-body label[for="reader-theme-sepia"] {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* Accordion: each section's own title bar - clicking it (a checkbox+
   label, see the doc comment above) opens or closes just that section,
   independently of every other one (Ced's 2026-09-10 report: opening a
   later chapter used to close whichever one was already open, back when
   these were a same-name radio group instead). */
.reader-accordion-title {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 2.75rem; padding: 0.5rem 0.2rem; margin: 0 -0.2rem;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  border-bottom: 1px solid var(--line-soft); box-sizing: border-box;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.92rem; color: var(--ink-soft);
}
.reader-accordion-title:hover { color: var(--ink); }
.reader-accordion-title:active { background: var(--accent-soft); }
.reader-accordion-input:focus-visible + .reader-accordion-title { outline: 2px solid var(--accent); outline-offset: -2px; }
.reader-accordion-input:checked + .reader-accordion-title { color: var(--accent-text); font-weight: 600; }
.reader-accordion-title-text { flex: 1; }
/* Same disclosure convention as "Proposer un complément ou une
   correction" (a plain native <details> marker there) - a literal
   triangle here since this is a radio+label, not <details>, kept at the
   same leading position and the same behavior: it points right when
   closed, and rotates to point down once the section is open. */
.reader-accordion-triangle { color: var(--accent); font-size: 0.8em; line-height: 1; transition: transform 0.25s ease; }
.reader-accordion-input:checked + .reader-accordion-title .reader-accordion-triangle { transform: rotate(90deg); }

/* Notebook cue #2: a small "§1, §2, §3..." entry badge - only real,
   titled sections (now called chapters) count (an untitled foreword
   stays unmarked, see build_biography_reader's own reasoning). Pill
   shape, not a fixed circle, so a two-digit chapter number still fits. */
.reader-rank {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6rem; height: 1.6rem; flex: none; border-radius: 999px;
  padding: 0 0.4rem;
  background: var(--accent-soft); color: var(--accent-text);
  font-size: 0.72rem; font-weight: 700;
}

.reader-accordion-body { display: none; padding: 1rem 0 1.2rem; }
/* Opening a chapter (whichever radio+label triggered it - the chapter's
   own triangle, or the previous chapter's "next chapter" link) plays a
   brief page-turn: `display: none -> block` restarts a CSS animation on
   an element the same way it would on first insertion, so this replays
   every time without any JS. */
.reader-accordion-input:checked + .reader-accordion-title + .reader-accordion-body {
  display: block;
  animation: reader-page-flip-in 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes reader-page-flip-in {
  from { transform: perspective(1200px) rotateY(-22deg); opacity: 0.5; filter: brightness(0.88); }
  to { transform: perspective(1200px) rotateY(0deg); opacity: 1; filter: brightness(1); }
}

/* "N sur <total>" pagination - only real, ranked chapters get one (never
   the untitled foreword, which has no rank to paginate). */
.reader-pagination {
  margin: 0.8rem 0 0; font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.78rem; letter-spacing: 0.03em; color: var(--ink-soft);
}

.reader-next-chapter {
  display: inline-flex; align-items: center; gap: 0.55rem; min-height: 2.75rem;
  margin-top: 0.6rem; padding: 0.3rem 0.2rem; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.85rem; font-weight: 600; color: var(--accent-text);
}
.reader-next-chapter:hover { color: var(--ink); }
.reader-next-chapter:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
/* A dog-eared corner that lifts and turns over on hover/click - the same
   gesture as .reader-corner below, more pronounced since this button is
   the one that actually turns to a new chapter. */
.reader-page-flip {
  width: 1.1rem; height: 1.1rem; flex: none;
  background: linear-gradient(135deg, transparent 50%, var(--accent-soft) 52%);
  border: 1px solid var(--line-soft); border-top: none; border-left: none;
  border-radius: 0 0 4px 0; transform-origin: bottom right;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.reader-next-chapter:hover .reader-page-flip,
.reader-next-chapter:focus-visible .reader-page-flip {
  transform: perspective(400px) rotateY(150deg) scale(1.15);
  box-shadow: -2px -2px 6px rgba(23, 42, 58, 0.2);
}
.reader-next-chapter:active .reader-page-flip { transform: perspective(400px) rotateY(170deg) scale(1.25); }

/* Notebook cue #3: faint horizontal rules behind the prose, like lined
   paper - spaced in em so they keep pace with the comfort font-size
   setting rather than only matching the default size. */
.reader-preview, .reader-rest {
  line-height: 1.75em;
  background-image: repeating-linear-gradient(
    to bottom, transparent, transparent calc(1.75em - 1px),
    var(--line-soft) calc(1.75em - 1px), var(--line-soft) 1.75em
  );
}
.reader-preview { position: relative; }
.reader-preview-folded { padding-bottom: 0.6rem; }
.reader-preview-folded::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2.4em;
  background: linear-gradient(180deg, transparent, var(--reader-bg));
  pointer-events: none;
}
div:has(> .reader-page[open]) > .reader-preview-folded::after { opacity: 0; transition: opacity 0.3s ease; }

.reader-page { margin-top: 0.3rem; }
.reader-turn-page {
  display: inline-flex; align-items: center; gap: 0.5rem; min-height: 2.75rem;
  padding: 0.4rem 0.2rem; margin: -0.4rem -0.2rem; box-sizing: border-box;
  cursor: pointer; list-style: none; border-radius: 6px;
  font-family: "Jost", Futura, "Century Gothic", sans-serif;
  font-size: 0.85rem; font-weight: 600; color: var(--accent-text);
  -webkit-tap-highlight-color: transparent;
}
.reader-turn-page::-webkit-details-marker { display: none; }
.reader-turn-page::marker { content: ""; }
.reader-turn-page:hover { color: var(--ink); }
.reader-turn-page:active { background: var(--accent-soft); }
.reader-turn-page:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.reader-corner {
  width: 0.9rem; height: 0.9rem; flex: none;
  background: linear-gradient(135deg, transparent 50%, var(--accent-soft) 52%);
  border: 1px solid var(--line-soft); border-top: none; border-left: none;
  border-radius: 0 0 3px 0; transform-origin: bottom right;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.reader-page:hover .reader-corner, .reader-turn-page:focus-visible .reader-corner {
  transform: rotate(-10deg) scale(1.2);
  box-shadow: -2px -2px 6px rgba(23, 42, 58, 0.15);
}
.reader-label-open { display: none; }
.reader-page[open] .reader-label-closed { display: none; }
.reader-page[open] .reader-label-open { display: inline; }
/* Overrides the UA stylesheet that hides everything but <summary> in a
   closed <details> - needed so .reader-rest can animate on its own CSS
   terms (max-height/opacity) instead of snapping open/closed instantly. */
.reader-page .reader-rest {
  display: block !important;
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height 0.6s ease, opacity 0.45s ease;
}
.reader-page[open] .reader-rest { max-height: 6000px; opacity: 1; margin-top: 0.9rem; line-height: 1.75; }
.panel-warning { border-color: #c9a13b; background: color-mix(in srgb, #c9a13b 8%, var(--paper-raised)); }

.completeness-leads { list-style: none; padding: 0; margin: 0.6rem 0 1.6rem; display: flex; flex-direction: column; gap: 0.4rem; }
.completeness-leads li {
  padding: 0.5rem 0.8rem; border-radius: 8px; background: var(--accent-soft);
  font-size: 0.88rem; color: var(--ink);
}
.module-list { list-style: none; padding: 0; margin: 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
.module-item {
  display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
  padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper); cursor: grab;
}
/* No-op whenever the row fits on one line (which is every width above
   ~390px, and every desktop width) - only kicks in for a long module
   name (e.g. "Galerie & médias") combined with the move/activate
   buttons on the narrowest dialog width (337px, at a 375px viewport).
   Deliberately not a media-query breakpoint tied to one pixel value -
   this reflows correctly for any future longer translated module name
   too, not just the ones measured today (found via a Ralph-loop
   desktop/responsive parity audit, see PROGRESS.md). */
.module-item.is-inactive { opacity: 0.5; }
.module-item .module-name { flex: 1; font-weight: 500; }
.drag-handle { color: var(--ink-soft); }

.media-manage-list { list-style: none; padding: 0; margin: 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
.media-manage-item {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper); cursor: grab;
}
.media-manage-thumb { width: 56px; height: 56px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
.media-manage-thumb-generic {
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-size: 0.7rem; background: var(--accent-soft); color: var(--ink-soft);
}
.media-manage-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.media-edit-details summary { cursor: pointer; font-size: 0.9rem; }
.media-edit-details .stack-form { margin-top: 0.6rem; }

/* Add-by-URL, tucked away as a secondary path behind the main file
   upload (see the upload form in _media.html) - a family adding photos
   normally never sees these fields at all. */
.media-url-details { margin: 0.2rem 0 0; }
.media-url-details summary { cursor: pointer; font-size: 0.85rem; color: var(--ink-soft); }
.media-url-details[open] { display: flex; flex-direction: column; gap: 0.9rem; margin-bottom: 0.4rem; }
.media-url-details[open] summary { margin-bottom: 0.2rem; }
/* Same collapsed-by-default pattern as .media-url-details, for the
   "nature" and source-document fields in _memory.html - moved out of the
   main flow so writing a memory only asks for the memory itself by
   default (Ced's 2026-09-28 elderly-friendliness pass). */
.memory-details-optional { margin: 0.2rem 0 0; }
.memory-details-optional summary { cursor: pointer; font-size: 0.85rem; color: var(--ink-soft); }
.memory-details-optional[open] { display: flex; flex-direction: column; gap: 0.9rem; margin-bottom: 0.4rem; }
.memory-details-optional[open] summary { margin-bottom: 0.2rem; }
/* The Galerie panel's three secondary tools (voice recording, albums,
   AI tribute video) - a heavier version of the same collapsed-by-
   default pattern above, since each one can itself contain a real
   sub-form or a whole album grid, not just a couple of fields (Ced's
   2026-09-28 report: adding one photo used to mean scrolling past all
   three, permanently expanded, before or after the actual upload form). */
.media-optional-tool {
  border-top: 1px solid var(--line); padding-top: 1rem; margin-top: 1.4rem;
}
.media-optional-tool summary {
  cursor: pointer; font-weight: 600; font-size: 0.95rem; color: var(--ink);
  padding: 0.3rem 0; min-height: 44px; display: flex; align-items: center;
}
.media-optional-tool[open] summary { margin-bottom: 0.6rem; }

.timeline-manage-list { list-style: none; padding: 0; margin: 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
.timeline-manage-item {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: 10px; background: var(--paper);
}
.timeline-manage-item .timeline-edit-details { flex: 1; min-width: 0; }
.timeline-edit-details summary { cursor: pointer; font-size: 0.9rem; }
.timeline-edit-details .stack-form { margin-top: 0.6rem; }

.archive-manage-list { list-style: none; padding: 0; margin: 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
.archive-manage-item {
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--paper);
}
.archive-manage-info { flex: 1; min-width: 0; }
.archive-edit-details summary { cursor: pointer; font-size: 0.9rem; }
.archive-edit-details .stack-form { margin-top: 0.6rem; }

.inline-form { margin: 0; }
.move-buttons { display: flex; gap: 0.2rem; }

/* Trash-icon delete buttons: delete immediately on click, no
   confirmation step (the family asked for this explicitly — a photo or
   album should be gone the moment they click, not after a second click
   or a dialog). */
.btn-icon-trash {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--line);
  background: var(--paper); color: var(--ink-soft); cursor: pointer; flex-shrink: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
.btn-icon-trash:hover { background: #c0392b; color: #fff; border-color: #c0392b; }

/* Album covers: a couple of the album's OWN other photos peek out behind
   the cover — real pictures, never a blank filler card — offset toward
   one corner only (not fanned both ways) for a calmer, more regular
   "stack" read. margin-bottom on the container reserves room for the
   overflow so it never bleeds into the album name below. */
.album-cover-stack { position: relative; display: block; aspect-ratio: 1 / 1; margin-bottom: 20px; }
.album-cover-back {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  border-radius: 10px; border: 2px solid var(--paper-raised);
  box-shadow: 0 2px 5px rgba(20, 16, 8, 0.14);
}
.album-cover-back-1 { transform: translate(8%, 8%); z-index: 0; }
.album-cover-back-2 { transform: translate(15%, 15%); z-index: -1; filter: brightness(0.93); }
.album-cover-thumb {
  position: relative; z-index: 2; display: block; width: 100%; height: 100%;
  object-fit: cover; border-radius: 10px; box-shadow: 0 3px 8px rgba(20, 16, 8, 0.18);
}
.album-cover-thumb-empty { background: var(--accent-soft); }

.album-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1.4rem 1.6rem; margin: 1.2rem 0 1.8rem; }
.album-card { display: flex; flex-direction: column; gap: 0.35rem; text-decoration: none; color: inherit; }
.album-card:hover .album-cover-thumb { transform: scale(1.03); transition: transform 0.15s ease; }
.album-card-name { font-weight: 600; }
.album-card-count { font-size: 0.8rem; }

/* "+ Créer un album" tile: first position in the grid, same size as a
   real album card (it reuses .album-cover-stack's own square aspect
   ratio) so creating the very first album is as visually obvious as
   opening an existing one - Ced's 2026-09-09 request. Dashed border
   reads as an empty slot to fill in, never a real photo. */
.album-card-add .album-cover-stack {
  display: flex; align-items: center; justify-content: center;
  border: 2px dashed var(--line); border-radius: 10px;
  background: transparent; transition: border-color 0.15s ease, background-color 0.15s ease;
}
.album-card-add:hover .album-cover-stack { border-color: var(--accent); background: var(--accent-soft); }
.album-card-add-icon { font-size: 2.4rem; line-height: 1; font-weight: 300; color: var(--ink-soft); }
.album-card-add:hover .album-card-add-icon { color: var(--accent-text); }
.album-card-add .album-card-name { text-align: center; color: var(--ink-soft); }
.album-card-add:hover .album-card-name { color: var(--ink); }

.album-manage-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem; margin: 1.2rem 0 1.6rem; }
.album-manage-card {
  border: 1px solid var(--line); border-radius: 10px; padding: 0.7rem;
  background: var(--paper);
}
.album-manage-summary {
  display: flex; flex-direction: column; gap: 0.35rem; cursor: pointer; list-style: none;
}
.album-manage-summary::-webkit-details-marker { display: none; }
.album-manage-info { display: flex; flex-direction: column; gap: 0.1rem; }
.album-manage-info strong { display: block; font-size: 0.85rem; overflow-wrap: anywhere; }
.album-manage-photos {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: 0.5rem;
  margin: 0.8rem 0; padding-top: 0.8rem; border-top: 1px solid var(--line);
}
.album-manage-photo { position: relative; }
.album-manage-photo-thumb { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 8px; display: block; }
.album-manage-photo.is-cover .album-manage-photo-thumb { outline: 2px solid #C7A25A; outline-offset: 1px; }
.album-manage-photo form.inline-form { position: absolute; top: 0.25rem; right: 0.25rem; }
.album-manage-photo .btn-icon-trash { width: 26px; height: 26px; background: rgba(250, 247, 242, 0.92); }
.album-manage-photo form.album-cover-set-form { position: absolute; top: 0.25rem; left: 0.25rem; margin: 0; }
.btn-icon-star {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line);
  background: rgba(250, 247, 242, 0.92); color: var(--ink-soft); cursor: pointer;
}
.btn-icon-star:hover { background: #C7A25A; color: #fff; border-color: #C7A25A; }
.btn-icon-rotate {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line);
  background: rgba(250, 247, 242, 0.92); color: var(--ink-soft); cursor: pointer;
}
.btn-icon-rotate:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.btn-icon-add {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--line);
  background: var(--paper); color: var(--ink-soft); cursor: pointer; flex-shrink: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
.btn-icon-add:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.btn-icon-add input[type="file"] { display: none; }

.album-rename-form { display: flex; gap: 0.4rem; align-items: center; margin-bottom: 0.4rem; }
.album-rename-input {
  flex: 1; min-width: 0; border: 1px solid var(--line); border-radius: 6px;
  padding: 0.35rem 0.6rem; font: inherit; background: var(--paper);
}
.album-rename-input-title { font-family: "Jost", Futura, "Century Gothic", sans-serif; font-size: 1.4rem; font-weight: 300; }
.album-cover-badge {
  position: absolute; top: 0.25rem; left: 0.25rem; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; background: #C7A25A; color: #fff;
}
.album-manage-actions { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }

.album-page { max-width: 900px; margin: 0 auto; padding: 2rem 1.2rem; }
.album-back { margin-bottom: 0.4rem; }
.album-back a { color: var(--ink-soft); text-decoration: none; }
.album-back a:hover { text-decoration: underline; }
.move-buttons button:disabled { opacity: 0.3; cursor: not-allowed; }

.contributors-credit { font-size: 0.85rem; font-style: italic; }
.follow-form { display: flex; gap: 0.5rem; margin-top: 0.6rem; max-width: 380px; }
.follow-form input { flex: 1; padding: 0.55rem 0.8rem; border: 1px solid var(--line); border-radius: 999px; font-size: 0.9rem; }
.checklist-panel { background: var(--accent-soft); }
.checklist { list-style: none; padding: 0; margin: 0.8rem 0 0; display: flex; flex-direction: column; gap: 0.5rem; }
.checklist a { color: var(--ink); text-decoration: none; font-size: 0.95rem; }
.checklist li.is-done a { color: var(--ink-soft); text-decoration: line-through; }

.portrait-form { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 1.2rem; }
.portrait-preview {
  position: relative;
  width: 72px; height: 72px; border-radius: 50%; overflow: hidden;
  background: var(--accent-soft); display: flex; align-items: center; justify-content: center;
  font-family: "Jost", Futura, "Century Gothic", sans-serif; font-weight: 300; font-size: 1.6rem; flex-shrink: 0;
  cursor: grab; touch-action: none;
}
.portrait-preview.dragging-position { cursor: grabbing; }
.portrait-preview img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
.portrait-preview input[type="file"] { display: none; }
.portrait-edit-badge {
  position: absolute; bottom: 0; right: 0; width: 26px; height: 26px; border-radius: 50%;
  background: var(--ink, #172A3A); color: #fff; display: flex; align-items: center; justify-content: center;
  font-size: 0.85rem; border: 2px solid var(--paper-raised); cursor: pointer;
}
.portrait-hint { font-size: 0.82rem; }

.cover-form { margin-bottom: 0.8rem; }
.cover-preview {
  position: relative;
  display: block;
  height: 140px; border-radius: var(--radius); cursor: grab; touch-action: none;
  background: var(--accent-soft) center/cover no-repeat;
  border: 1px dashed var(--line);
  overflow: hidden;
}
.cover-preview.dragging-position { cursor: grabbing; }
.cover-upload-form { margin: 0; }
.cover-preview input[type="file"] { display: none; }
/* A distinct circular camera badge in the corner (LinkedIn-style cover
   editor) instead of a low-contrast text pill sitting on top of
   whatever the photo happens to show — it needs to stay visible and
   findable regardless of how busy the photo underneath is. */
.cover-edit-badge {
  position: absolute; bottom: 0.7rem; right: 0.7rem; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.95); color: var(--ink);
  border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px rgba(20, 16, 8, 0.3);
  cursor: pointer; transition: transform 0.15s ease, background 0.15s ease;
}
.cover-edit-badge:hover { background: #fff; transform: scale(1.07); }
.cover-preview + .field-hint { margin-top: 0.5rem; }

.suggestion-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin: -0.4rem 0 0.4rem; }
.suggestion-chips .muted { width: 100%; font-size: 0.8rem; }
.suggestion-chips-label { display: inline-flex; align-items: center; gap: 0.3rem; }
.chip {
  border: 1px solid var(--line); background: var(--paper); border-radius: 999px;
  padding: 0.35rem 0.8rem; font-size: 0.8rem; cursor: pointer; color: var(--ink-soft);
  max-width: 100%; text-align: left; white-space: normal;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.chip:hover { background: var(--accent-soft); color: var(--ink); border-color: var(--accent); }
.names-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; margin: 0.8rem 0; }
.names-list li { background: var(--paper); border: 1px solid var(--line); border-radius: 10px; padding: 0.6rem 0.8rem; display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.names-actions { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.featured-parts-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; margin: 0.8rem 0; }
.featured-parts-list li { background: var(--paper); border: 1px solid var(--line); border-radius: 10px; padding: 0.5rem 0.8rem; }
.checkbox-row { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.checkbox-row input[type="checkbox"] { width: auto; }

/* A field group revealed only once its own "does this even apply?"
   checkbox is ticked (see data-toggle-target in panels.js - the
   recognized/spouse name toggles in _identity.html are the first use:
   most memorials never need either, so showing both unconditionally
   read as required fields rather than the rare exception they are). */
.conditional-fields { display: flex; flex-direction: column; gap: 1rem; margin-top: 0.6rem; }
.conditional-fields[hidden] { display: none; }

.death-marker-picker { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.5rem; margin: 0.8rem 0; }
.death-marker-option {
  display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
  background: var(--paper); border: 1px solid var(--line); border-radius: 10px; padding: 0.5rem 0.7rem; font-size: 0.85rem;
}
.death-marker-option input[type="radio"] { width: auto; }

.timeline-style-picker { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 0.6rem; margin: 0.8rem 0; }
.timeline-style-option {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; cursor: pointer;
  background: var(--paper); border: 1px solid var(--line); border-radius: 10px; padding: 0.7rem; font-size: 0.85rem;
}
.timeline-style-option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.timeline-style-option input[type="radio"] { width: auto; }

/* A small CSS-only diagram of each timeline style (Ced's 2026-09-09
   request) - one shared line + 3 item bars, rearranged per style rather
   than 3 separate hand-drawn previews. Purely decorative (aria-hidden). */
.timeline-style-preview { position: relative; display: block; width: 100%; height: 44px; }
.timeline-style-preview .tsp-item { position: absolute; height: 5px; border-radius: 2px; background: var(--accent); }
.timeline-style-preview .tsp-line { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--ink-soft); }
.timeline-style-preview-classic .tsp-line { left: 6px; }
.timeline-style-preview-classic .tsp-item { left: 13px; right: 4px; }
.timeline-style-preview-classic .tsp-item:nth-child(2) { top: 3px; }
.timeline-style-preview-classic .tsp-item:nth-child(3) { top: 19px; }
.timeline-style-preview-classic .tsp-item:nth-child(4) { top: 35px; }
.timeline-style-preview-cards .tsp-line { display: none; }
.timeline-style-preview-cards .tsp-item {
  left: 0; right: 0; height: 9px; border-radius: 3px;
  background: var(--accent-soft); border: 1px solid var(--accent);
}
.timeline-style-preview-cards .tsp-item:nth-child(2) { top: 0; }
.timeline-style-preview-cards .tsp-item:nth-child(3) { top: 13px; }
.timeline-style-preview-cards .tsp-item:nth-child(4) { top: 27px; }
.timeline-style-preview-alternating .tsp-line { left: 50%; }
.timeline-style-preview-alternating .tsp-item { width: 42%; }
.timeline-style-preview-alternating .tsp-item:nth-child(2) { left: 0; top: 2px; }
.timeline-style-preview-alternating .tsp-item:nth-child(3) { right: 0; top: 19px; }
.timeline-style-preview-alternating .tsp-item:nth-child(4) { left: 0; top: 36px; }
.timeline-style-preview-branches .tsp-line {
  left: 6px; width: 3px; border-radius: 3px;
  background: linear-gradient(180deg, var(--accent-text), var(--accent) 55%, var(--accent-soft));
}
.timeline-style-preview-branches .tsp-item {
  left: 12px; width: 10px; height: 10px; border-radius: 0 100% 0 100%;
  transform: rotate(-45deg); background: var(--accent);
}
.timeline-style-preview-branches .tsp-item:nth-child(2) { top: 2px; }
.timeline-style-preview-branches .tsp-item:nth-child(3) { top: 17px; }
.timeline-style-preview-branches .tsp-item:nth-child(4) { top: 32px; }
.timeline-style-preview-blossom .tsp-line {
  left: 6px; width: 2px; background: none;
  background-image: repeating-linear-gradient(180deg, var(--accent-soft) 0 3px, transparent 3px 7px);
}
.timeline-style-preview-blossom .tsp-item {
  left: 12px; width: 12px; height: 12px; border-radius: 0; background: none;
  background-image:
    radial-gradient(circle 3.5px at 50% 16%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 3.5px at 50% 84%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 3.5px at 16% 50%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 3.5px at 84% 50%, var(--accent) 95%, transparent 100%),
    radial-gradient(circle 2.5px at 50% 50%, var(--accent-text) 95%, transparent 100%);
}
.timeline-style-preview-blossom .tsp-item:nth-child(2) { top: 1px; }
.timeline-style-preview-blossom .tsp-item:nth-child(3) { top: 16px; }
.timeline-style-preview-blossom .tsp-item:nth-child(4) { top: 31px; }
.death-marker-preview { display: inline-flex; align-items: center; justify-content: center; width: 1.4em; color: var(--ink-soft); }
.death-marker-glyph { display: inline-flex; vertical-align: -2px; }
.death-marker-om { font-size: 1.15em; line-height: 1; font-family: "Nirmala UI", "Noto Sans Devanagari", "ITF Devanagari", sans-serif; }
.surname { font-variant: small-caps; letter-spacing: 0.04em; }
.memorial-hero h1 .surname { color: var(--accent-text); }
.alt-names { color: var(--ink-soft); font-size: 0.95rem; margin: 0.2rem 0 0.6rem; }
.name-note { font-size: 0.85em; font-style: italic; }
.generate-draft-form { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.generate-draft-form p { margin: 0.4rem 0 0; }
.permalink-row { display: flex; gap: 0.5rem; margin-top: 0.8rem; align-items: flex-start; }
.permalink-row input, .permalink-row textarea { flex: 1; padding: 0.6rem 0.8rem; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); color: var(--ink-soft); font-size: 0.85rem; font-family: inherit; resize: vertical; }

/* Post-creation confirmation banner (2026-09-10 UX brief, Lot 1 Phase 4) -
   QR thumbnail + public URL alongside the existing owner-link box, plus
   the two natural next actions (enrich, invite family), reusing the
   already-built memory/invite panels rather than inventing new ones. */
.welcome-public-row { display: flex; gap: 1rem; align-items: center; margin-top: 1rem; }
.welcome-public-row img { border-radius: 8px; background: var(--paper-raised); flex-shrink: 0; }
.welcome-public-col { flex: 1; min-width: 0; }
.welcome-next-actions { display: flex; gap: 0.7rem; flex-wrap: wrap; margin-top: 1rem; }

.slug-input-row {
  display: flex; align-items: stretch; border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--paper); font-size: 0.85rem;
}
.slug-prefix { padding: 0.6rem 0.5rem 0.6rem 0.8rem; color: var(--ink-soft); background: var(--accent-soft); white-space: nowrap; display: flex; align-items: center; }
.slug-input-row input { border: none; background: transparent; padding: 0.6rem 0.8rem 0.6rem 0.2rem; flex: 1; min-width: 0; font-family: inherit; font-size: inherit; }
.slug-input-row input:focus { outline: none; }
/* The input's own outline is suppressed above (it would clip oddly
   against the prefix pill), but a keyboard user still needs a visible
   focus signal - highlight the whole row instead (Ralph-loop
   accessibility pass, 2026-09-11: this had no focus indicator at all). */
.slug-input-row:focus-within { border-color: var(--ink); box-shadow: 0 0 0 1px var(--ink); }

.ceremony-actions { display: flex; align-items: center; gap: 1.2rem; margin-top: 1rem; flex-wrap: wrap; }
.ceremony-qrcode { border-radius: 8px; border: 1px solid var(--line); background: #fff; padding: 4px; }
.tribute-video-section { margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.tribute-video-public { margin-top: 1.2rem; }
.tribute-video-preview, .tribute-video-player {
  display: block; max-width: 100%; width: 100%; max-height: 480px;
  border-radius: var(--radius); background: #000; margin-top: 0.6rem;
}
.tribute-video-actions { display: flex; gap: 0.6rem; margin-top: 0.6rem; }
.tribute-video-processing { color: var(--ink-soft); font-style: italic; }
.tribute-video-failed { color: #c0392b; }

.moderation-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 1rem; }
.moderation-actions { display: flex; gap: 0.5rem; margin-top: 0.4rem; }

.contributions-list { list-style: none; padding: 0; margin: 0.6rem 0; display: flex; flex-direction: column; gap: 0.4rem; }
.contributions-list li { background: var(--accent-soft); border-radius: 8px; padding: 0.5rem 0.7rem; font-size: 0.92rem; }
.contribution-credit { font-style: italic; }

.memory-profile-status { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; font-size: 0.9rem; margin-bottom: 1rem; }
.memory-profile-badge { display: inline-block; padding: 0.25rem 0.7rem; border-radius: 999px; font-weight: 600; font-size: 0.8rem; }
.memory-profile-structured { background: var(--line); }
.memory-profile-enriched { background: var(--accent-soft); }
.memory-profile-coherent { background: var(--theme-color, #172A3A); color: var(--paper); }

.discovery-actions { display: flex; gap: 0.8rem; margin: 1.2rem 0; flex-wrap: wrap; }
.fact-list { display: flex; flex-direction: column; gap: 1rem; }
.fact-card { background: var(--paper-raised); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.2rem; }
.fact-content { font-size: 1.02rem; }
.fact-meta { display: flex; flex-wrap: wrap; gap: 1rem; font-size: 0.82rem; color: var(--ink-soft); margin: 0.6rem 0; }
.fact-meta dt { font-weight: 600; display: inline; }
.fact-meta dd { display: inline; margin: 0 0 0 0.3rem; }
.confidence { padding: 0.1rem 0.5rem; border-radius: 999px; background: var(--accent-soft); }
.manual-fact-details { margin: 1rem 0 1.6rem; border: 1px solid var(--line); border-radius: var(--radius); padding: 0.8rem 1rem; background: var(--paper); }
.manual-fact-details summary { cursor: pointer; font-weight: 600; }
.manual-fact-details .stack-form { margin-top: 0.8rem; }
.fact-conflict-group {
  border: 1px solid #C7A25A; border-radius: var(--radius); padding: 0.8rem;
  display: flex; flex-direction: column; gap: 1rem; background: rgba(201, 146, 47, 0.06);
}
.fact-conflict-note { margin: 0; font-size: 0.85rem; font-weight: 600; color: #8a6423; }
.fact-actions { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 0.8rem; }
.fact-buttons { display: flex; gap: 0.5rem; }
.badge-ai {
  display: inline-block; font-size: 0.72rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-soft); background: var(--accent-soft);
  border-radius: 999px; padding: 0.2rem 0.7rem; margin: 0 0 0.5rem;
}
.status { font-weight: 600; text-transform: capitalize; margin-right: 0.4rem; }
.journal-entry-details { display: inline; }
.journal-entry-details summary { display: inline; cursor: pointer; list-style: none; }
.journal-entry-details summary::-webkit-details-marker { display: none; }
.journal-entry-details[open] summary { display: none; }
.journal-entry-details p { display: inline; margin: 0; }
/* Explicit, unmistakably-clickable affordance rather than relying on
   plain truncated text alone reading as "clickable" - found live
   (2026-09-04): a bare "…" at the end of cut-off text didn't read as
   interactive at all, just as a dead end. */
.journal-read-more { color: var(--accent-text); text-decoration: underline; font-weight: 600; white-space: nowrap; }
.journal-entry-details summary:hover .journal-read-more { color: var(--ink); }
.registry-links { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 0.6rem; }
.registry-links a { display: inline-block; padding: 0.5rem 1rem; border: 1px solid var(--line); border-radius: 999px; background: var(--paper-raised); text-decoration: none; font-size: 0.88rem; transition: background 0.18s ease; }
.registry-links a:hover { background: var(--accent-soft); }

/* Assistant mémoire (/decouverte) page structure - 2026-09-04 streamlining:
   the two decision queues (sources to confirm, facts to validate) get a
   distinct "this needs your attention" treatment, while everything else
   (registries, manual links, journal) stays plain reference material. */
.discovery-person-name { font-size: 1.2rem; font-weight: 600; margin: 0.2rem 0 0; }
.discovery-subtitle { margin: 0.1rem 0 1rem; }
.discovery-actionable {
  border: 1px solid var(--accent); border-radius: var(--radius);
  background: var(--accent-soft); padding: 1.2rem; margin: 1.2rem 0 1.6rem;
}
.discovery-actionable > .fact-list, .discovery-actionable > .empty-state { margin-top: 0.8rem; }
.discovery-manual-block { margin: 1.6rem 0; }
.discovery-registry-external { opacity: 0.85; }
.discovery-manual-links {
  border: 1px solid var(--accent); border-radius: var(--radius);
  padding: 1.2rem; margin: 1rem 0; background: var(--paper-raised);
}
.badge-verified, .badge-unverified {
  display: inline-block; font-size: 0.68rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; border-radius: 999px; padding: 0.15rem 0.6rem; margin-left: 0.5rem;
  vertical-align: middle;
}
.badge-verified { background: var(--accent-text); color: var(--paper); }
.badge-unverified { background: var(--line); color: var(--ink-soft); }

@media (max-width: 640px) {
  .site-header { padding: 1rem; flex-wrap: wrap; row-gap: 0.6rem; }
  .header-nav { width: 100%; justify-content: flex-end; }
  /* .site-header wraps to two rows here, taller than the desktop value
     .section-nav's own `top` assumes - keeps it flush under the header
     instead of sliding a strip of it underneath. */
  .section-nav { top: 112px; }
  body:has(.example-banner) .section-nav { top: 152px; }
  /* The pill radius (999px) reads fine on the single-row desktop layout,
     but once the buttons wrap to 2-3 rows on a narrow screen it makes the
     curved ends visually crowd the text on the wrapped rows, reading as
     off-center (external UX audit, mobile pass). A moderate radius suits
     a multi-row box instead. */
  .owner-bar { flex-wrap: wrap; row-gap: 0.4rem; border-radius: var(--radius); justify-content: center; }
  .form-row { grid-template-columns: 1fr; }
  .hero h1 { font-size: 1.8rem; }
  /* The QR thumbnail + public-address column (welcome banner, Lot 1) sit
     side by side on desktop; on a narrow screen the column has too
     little width left once the 84px image takes its share, so it stacks
     instead (mobile-first pass, Lot 3 Phase 11). */
  .welcome-public-row { flex-direction: column; align-items: center; text-align: center; }
  /* Ced's 2026-09-12 request: the reading-comfort controls (font size +
     theme, each its own segment - see .reader-comfort-group above) sit on
     the same row as the reading-time text on desktop (.reader-meta,
     space-between) - nothing guarantees both halves still fit one line
     together on a narrow phone, so this stacks them instead of leaving it
     to chance. Also bumps the reading-time/chapter-pagination text up
     slightly - both were sized for a wide desktop line, a bit small for
     text read this often on a phone. */
  .reader-meta { flex-direction: column; align-items: flex-start; }
  .reader-time, .reader-pagination { font-size: 0.88rem; }
  /* Ced's 2026-09-13 request: justify the biography text on mobile and
     widen its actual column - at a narrow phone width the book cover's
     decorative padding (fine at its full size on a wide desktop reader,
     reserved for the spine graphic and the ruled margin line) left so
     little real text width that a short phrase ("sensible et forte à la
     fois") routinely broke into a lone 2-word orphan line. Shrinking
     both paddings reclaims real width instead of just moving the same
     narrow column around - the spine and ruled-margin line shrink by
     the same amount, so neither graphic is left floating past its own
     edge. hyphens:auto (the <html lang> this needs is already set, see
     base.html) lets a long word break instead of jumping whole to the
     next line, which is what most often produced the orphan in the
     first place, and pairs with justify to avoid the ragged whitespace
     rivers narrow justified columns are otherwise prone to. */
  .reader { padding: 1.4rem 1rem 1.4rem 1.5rem; }
  .reader::before { width: 0.6rem; }
  .reader::after { inset: 0.5rem 0.5rem 0.5rem 0.95rem; }
  .reader-body { padding: 1.2rem 1rem 1.2rem 1.3rem; }
  .reader-body::before { left: 0.7rem; }
  /* .module-read-more's own right offset (desktop rule above, 3rem) is
     .module-biography's own right padding (1.5rem, unchanged at this
     width - it isn't touched above) plus .reader's own right padding,
     which the rule just above DID shrink to 1rem on a screen this
     narrow - so the offset shrinks by that same 0.5rem (3rem -> 2.5rem)
     to keep landing exactly on .reader-body's own edge, the cream page,
     rather than overshooting past it now that .reader's padding alone
     gave up half a rem of the distance this offset has to cover. */
  .memorial-page:has(#page-view-all:checked) .modules-grid .module-biography .module-read-more { right: 2.5rem; }
  .reader-preview, .reader-rest {
    text-align: justify;
    -webkit-hyphens: auto;
    hyphens: auto;
  }
  /* Same left/right mirror as the desktop RTL block near .timeline
     above, just re-applied on top of this breakpoint's own narrower
     values - without this, an RTL reader on a phone (not just desktop)
     would still get the spine/ruled-margin line on the wrong edge. */
  html[dir="rtl"] .reader { padding-left: 1rem; padding-right: 1.5rem; }
  html[dir="rtl"] .reader::after { inset: 0.5rem 0.95rem 0.5rem 0.5rem; }
  html[dir="rtl"] .reader-body { padding-left: 1rem; padding-right: 1.3rem; }
  html[dir="rtl"] .reader-body::before { left: auto; right: 0.7rem; }
}

/* Sober micro-interactions, manager mode (see PROGRESS.md/EVOLUTION.md
   for the full list of where these are used). Silent register only:
   a brief fade/color transition, never an icon or added word - the
   .toast above stays reserved for moments the family just validated a
   deliberate action (see panels.js). */
.sortable-list [draggable="true"] { transition: opacity 0.18s ease; }
.sortable-list [draggable="true"].is-dragging { opacity: 0.4; }

/* Instant, silent removal for .btn-icon-trash (photo/album/archive-link/
   video delete) - a brief fade instead of the item just vanishing on
   the next full-page reload. No confirmation dialog and no added word:
   this is deliberately the same frictionless single-click deletion
   already in place, not a new safety gate. */
.item-removing { transition: opacity 0.2s ease, transform 0.2s ease; opacity: 0; transform: scale(0.97); pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Save confirmation toast: green, checkmark, auto-dismissing — for
   actions saved via fetch() in place (see panels.js), which would
   otherwise be silent since nothing on screen visibly reloads. */
.toast-container {
  position: fixed; bottom: 1.6rem; left: 50%; transform: translateX(-50%);
  /* width: max-content (not the default shrink-to-fit auto) - with only
     `left` set and no `right`, a fixed-position box's shrink-to-fit
     available width is (containing block width - left offset), which at
     left:50% halves with the viewport. On desktop that's still hundreds
     of px, so it never visibly wrapped; at a 375px mobile width it drops
     to ~187px, forcing toast text into an awkward mid-phrase wrap
     (external UX audit, mobile pass). max-content sizes to the real
     content instead, and max-width keeps it from overflowing on a
     genuinely long message. */
  width: max-content; max-width: calc(100vw - 2rem);
  z-index: 500; display: flex; flex-direction: column; gap: 0.5rem; align-items: center;
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: 0.5rem;
  background: #2e7d46; color: #fff; padding: 0.55rem 1.1rem; border-radius: 999px;
  font-size: 0.88rem; font-weight: 500; box-shadow: 0 6px 18px rgba(20, 16, 8, 0.22);
  opacity: 0; transform: translateY(12px); transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.visible { opacity: 1; transform: translateY(0); }

/* Undo toast for .btn-icon-trash deletions (see panels.js) - deliberately
   NOT the green save-confirmation styling above (this is neither a save
   nor something to celebrate) and NOT a confirmation dialog before the
   click (families explicitly asked for instant single-click deletion).
   The undo window is the safety net instead. */
.toast-undo { background: var(--ink); pointer-events: auto; }
.toast-undo-btn {
  background: none; border: none; color: #fff; font: inherit; font-weight: 700;
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer; padding: 0;
}

/* Pull-to-refresh (see pull-to-refresh.js) - only ever inserted into the
   DOM when the site is running as an installed standalone app, where
   iOS/Android remove the browser's own native pull-to-refresh gesture
   with no way to opt back in. Sits above the page, pulled into view by
   the script as the finger moves; never visible in a normal browser tab. */
.ptr-indicator {
  position: fixed; top: 0; left: 0; right: 0; height: 44px; z-index: 1000;
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  background: var(--paper-raised); border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow); color: var(--ink-soft); font-size: 0.82rem;
  transform: translateY(-44px); pointer-events: none;
}
.ptr-spinner {
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--ink-soft);
  flex-shrink: 0;
}
.ptr-indicator.is-refreshing .ptr-spinner { animation: ptr-spin 0.7s linear infinite; }
.ptr-indicator.is-armed .ptr-spinner { border-top-color: var(--accent-text); }
@keyframes ptr-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ptr-indicator.is-refreshing .ptr-spinner { animation: none; }
}

/* Registre National Historique pilot (app/routers/registry.py) - a plain
   list, deliberately unstyled beyond the rest of .form-page: these pages
   have no photo/theme-color data to show, unlike a real memorial card. */
.registry-list { list-style: none; padding: 0; margin: 1.5rem 0; }
.registry-list-item {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; padding: 0.6rem 0; border-bottom: 1px solid var(--line);
}
/* Deliberately plain, no memorial-style visual language (candle/tribute
   tone) on these pages - see the 2026-11-12 "extraction INSEE" framing
   decision: this is a bare public-data record, not Memoreva-authored
   content, and should read that way at a glance. */
.registry-source-badge {
  font-size: 0.85rem; color: var(--muted); border: 1px solid var(--line);
  border-radius: 6px; padding: 0.4rem 0.75rem; display: inline-block;
  margin-bottom: 1rem;
}
.registry-cross-links { margin-top: 2rem; }
.registry-cross-links h2 { font-size: 1.05rem; }
.registry-takedown-hint { margin-top: 2rem; }
