    /* Native form-control accent (date/time pickers, checkboxes, radios,
       range, progress). Inherits to every <input> unless overridden. */
    html { accent-color: #006a85; }

    /* Date/time picker buttons (calendar icon, dropdown arrow) — tint the
       built-in indicator so it matches the Castflow teal instead of staying
       black/blue in Safari and Chrome. */
    input[type="date"]::-webkit-calendar-picker-indicator,
    input[type="time"]::-webkit-calendar-picker-indicator,
    input[type="datetime-local"]::-webkit-calendar-picker-indicator,
    input[type="month"]::-webkit-calendar-picker-indicator,
    input[type="week"]::-webkit-calendar-picker-indicator {
      filter: invert(31%) sepia(94%) saturate(386%) hue-rotate(154deg) brightness(94%) contrast(98%);
      cursor: pointer;
      opacity: 0.85;
    }
    input[type="date"]::-webkit-calendar-picker-indicator:hover,
    input[type="time"]::-webkit-calendar-picker-indicator:hover,
    input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover {
      opacity: 1;
    }
    .no-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }
    .no-scrollbar::-webkit-scrollbar { display: none; }
    /* Step-1 composer: the project-title box is a contenteditable, so its
       placeholder has to be CSS. Rendering the placeholder as real content is
       what made "Nouveau projet" the value — click the pencil, type, and you got
       "Nouveau projetPub Loto". Empty span + attr(data-ph). */
    #cfComposerTitle:empty::before { content: attr(data-ph); color: #b6c0c5; }
    /* Same trick, as a class: an in-place title box renders EMPTY when the
       project has no name, and its placeholder is CSS — never text content,
       or clicking the pencil and typing gives "Nouveau projetPub Loto". */
    .cf-title-edit:empty::before { content: attr(data-ph); color: #b6c0c5; }
    .cf-title-edit:focus { background: #f5f8f9; }

    /* Hoverable help marker (export wizard, and anywhere else that needs to
       explain a control without spending a paragraph of screen on it).
       A `title=` tooltip was the cheap option and it is not good enough here:
       it takes ~1s to appear, cannot hold markup, and never shows on a tablet.
       So this is a real popover — CSS only, because the SPA renders through
       innerHTML and any JS-bound handler would be lost on the next render.
       :focus-within is what makes it work on touch: the marker is tabbable, and
       a tap focuses it. */
    .cf-help { position: relative; display: inline-flex; align-items: center; vertical-align: middle;
               color: #9CA3AF; cursor: help; outline: none; }
    .cf-help:hover, .cf-help:focus-within { color: #006a85; }
    .cf-help > .cf-help-pop {
      position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
      width: 260px; max-width: 78vw; padding: 10px 12px; border-radius: 9px;
      background: #111827; color: #F9FAFB; font-size: 11.5px; line-height: 1.5; font-weight: 400;
      text-align: left; text-transform: none; letter-spacing: 0; white-space: normal;
      box-shadow: 0 6px 20px rgba(0,0,0,.22); z-index: 90;
      opacity: 0; visibility: hidden; transition: opacity .12s ease;
    }
    .cf-help > .cf-help-pop::after {
      content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
      border: 5px solid transparent; border-top-color: #111827;
    }
    .cf-help:hover > .cf-help-pop, .cf-help:focus-within > .cf-help-pop { opacity: 1; visibility: visible; }
    .cf-help > .cf-help-pop code { background: rgba(255,255,255,.14); padding: 1px 4px; border-radius: 3px; }
    .cf-help > .cf-help-pop strong { color: #fff; }
    /* Near the right edge the centred popover would overflow the viewport. */
    .cf-help.cf-help-right > .cf-help-pop { left: auto; right: 0; transform: none; }
    .cf-help.cf-help-right > .cf-help-pop::after { left: auto; right: 10px; transform: none; }

    /* Sidebar rail stays scrollable but never shows a scrollbar. Without this,
       OSes that render persistent (non-overlay) scrollbars show an ugly mini-bar
       in the 72px collapsed rail whenever the nav is a hair taller than the
       viewport — visible to some users, invisible to those on overlay scrollbars. */
    .sidebar-main { scrollbar-width: none; -ms-overflow-style: none; }
    .sidebar-main::-webkit-scrollbar { display: none; }

    /* Découverte (v7g): feed + right rail stack on narrow viewports. */
    @media (max-width: 1080px) {
      .opp-feed-layout { flex-direction: column !important; }
      .opp-feed-layout > .opp-sidebar { width: 100% !important; }
      .opp-sticky { position: static !important; }
      .opp-sidebar-sticky { position: static !important; max-height: none !important; overflow: visible !important; }
    }

    /* Découverte: lock the filter/search toolbar + the "Comptes à suivre" rail on
       scroll. top offsets clear the 56px app topbar (sticky, z-index 30). */
    /* background matches `main.main-content` (#F5F6F8), not the Découverte
       design token #f8fafc: the bar is opaque and slides over the page, so a
       3-point difference reads as a lighter rectangle following the scroll. */
    /* CFT-1598: `padding-bottom: 16px` is the SPACE UNDER THE CATEGORY ROW, and it
       has to be inside this box. It used to be the row's own `margin-bottom`, which
       collapses OUT of a box with no bottom padding, so the opaque bar ended exactly
       at the pills and the feed scrolled flush against them (measured 0 px, and the
       32 px added to the feed layout in PR #3034 never showed while scrolled because
       it is a margin in the flow, not part of what is pinned). Inside the bar it is
       painted with the bar's own surface, so feed content is clipped 16 px BELOW the
       pills at any depth, and it equals the gap the panel and the band keep from the
       row at rest. */
    .opp-sticky { position: sticky; top: 56px; z-index: 20; background: #F5F6F8; padding-bottom: 16px; }
    /* The sidebar sticks BELOW the header, whose height is dynamic (the
       active-filter line comes and goes, and the controls row wraps):
       `--opp-header-h` is written by oppSyncStickyHeight() in
       11_opportunities.js, and the fallback is the old fixed value so a render
       that has not measured yet still sticks. Since CFT-1522 it measures the
       PINNED box only: the facet panel and the sources band are no longer in it,
       so opening the filters no longer moves the rail 240px.
       `.opp-sidebar` must STRETCH: the flex row is `align-items:flex-start`, so
       the column was only as tall as its own content and a sticky child had no
       distance to travel — it simply scrolled away. */
    .opp-sidebar { align-self: stretch; }
    /* +56px = the app topbar the header itself is offset by. Without it the rail
       pins at `H` while the header's pinned box ends at `56 + H`, so its
       first 40px sat UNDER an opaque header at z-index 20 — « the thing on the
       right should always be visible ». The box's own 16px bottom padding is the
       gap now (CFT-1598), so the rail starts exactly where the feed's visible
       area starts instead of a second 16px lower. */
    .opp-sidebar-sticky { position: sticky; top: calc(56px + var(--opp-header-h, 212px)); align-self: flex-start;
                          max-height: calc(100vh - 56px - var(--opp-header-h, 212px) - 20px); overflow-y: auto; }

    /* CFT-1522: what is TALL hangs out of the pinned box.
       `.opp-sticky` is the bar (tabs, search, sort, view, the category row and
       the active-filter line): one screen's worth of chrome that has to stay
       reachable at any scroll depth. `.opp-away` is the facet panel and the
       sources band, and it is NOT pinned: it sits in the flow, so scrolling down
       carries it up with the publications and under the bar, instead of painting
       ~370px of header over the start of the feed. Once it is out of sight the
       JS hides it for real (with a scroll compensation, so nothing moves), and
       scrolling back up past a pixel threshold brings it back pinned, which is
       what pushes the publications down rather than covering them. The state
       machine and the threshold are in 11_opportunities.js. */
    .opp-away { display: flex; flex-direction: column; }
    /* CFT-1598: the box that MOVES. `.opp-away` stays where the block belongs (in
       the flow at rest, pinned while revealed) and `.opp-away-in` slides inside
       it (11_opportunities.js, _oppPlayReveal / _oppPlayExit), so the block always
       comes out from under the bar and goes back under it. `opp-sliding` is added
       for the length of one slide, ONLY: it clips the travelling box to this one's
       own edges, which is what stops it drawing over the banners above the bar when
       the page is at the top. It is not a permanent `overflow: hidden` because that
       would clip the panel's own outline for no reason the rest of the time. */
    .opp-away-in { display: flex; flex-direction: column; }
    .opp-away.opp-sliding { overflow: hidden; }
    /* CFT-1598: a blurred block near the viewport gets a compositor layer of its own, so
       a layout shift above it moves it instead of re-blurring it on the CPU inside
       every re-rastered tile (that was the freeze). Given and taken away by
       oppArmBlurLayers() as the block enters and leaves a band around the viewport;
       see the note above `_oppBlurCss` for why it is not simply always on. */
    .opp-blur-live { will-change: transform; }
    /* `!important` is REQUIRED, not shouted: both collapsible blocks carry an
       inline `display:flex`, which beats any stylesheet declaration without it.
       And the whole behaviour is scoped to the width where the header is
       actually sticky: below that index.html makes it static and it scrolls
       away by itself, which is the same condition oppArmHeaderCollapse() checks.
       A stale collapsed state can therefore never hide the filters on a narrow
       window where no control exists to bring them back. */
    @media (min-width: 1081px) {
      .opp-away.opp-collapsed .opp-collapsible { display: none !important; }
      /* The caret lives in the bar, so its rotation reads the class off the
         sticky box. Both boxes carry it, on purpose. */
      .opp-sticky.opp-collapsed .opp-header-caret { transform: rotate(180deg); }
      /* Revealed by an upward scroll, or by a control, while the block's own
         flow box is hundreds of pixels above the viewport: it has to arrive
         PINNED under the bar or nothing appears at all. Opaque and the same
         surface as the bar, because it paints over the feed while pinned.
         z-index 18 keeps it under `.opp-sticky` (20) AND under the sort menu's
         click-catcher (19), which has to be able to swallow a click aimed at
         closing that menu. */
      .opp-away.opp-revealed {
        position: sticky; top: calc(56px + var(--opp-header-h, 160px)); z-index: 18;
      }
      /* The opaque surface is on the box that MOVES, not on the pinned one: were
         it on `.opp-away.opp-revealed` a 310 px opaque rectangle would appear
         over the feed in one frame and the content would slide in over it, which
         is the pop again. On `.opp-away-in` the surface arrives WITH the content. */
      .opp-away.opp-revealed > .opp-away-in { background: #F5F6F8; }
    }

    /* ── The motion ──────────────────────────────────────────────
       There used to be a FLIP here, and a `.opp-anim-leave` rule that had to
       outrank the `display:none` above so a block could keep painting while it
       faded. CFT-1522 retired both: the pinned box is now the bar alone and its
       height never changes, so no row inside the header ever moves, and the
       close happens where the reader cannot see it. What is left is the
       arrival, animated in JS on the two blocks themselves (_oppPlayReveal).

       `--opp-motion` is the one curve and `--opp-motion-ms` the one duration,
       so the caret and the blocks arrive together. */
    :root { --opp-motion: cubic-bezier(.32,.72,0,1); --opp-motion-ms: 340ms; }

    /* One duration for the whole header, so switching it off is one line. The
       JS half refuses on its own (_oppPlayReveal returns early); this is what
       silences the caret, which is a plain CSS transition and would otherwise
       keep easing on its own. */
    @media (prefers-reduced-motion: reduce) { :root { --opp-motion-ms: 0ms; } }
    .opp-header-caret { transition: transform var(--opp-motion-ms) var(--opp-motion); }
    @media (max-width: 1080px) { .opp-header-toggle { display: none !important; } }

    /* Saved-confirmation pop on the Sauvegarder button (no toast — Charbel). */
    @keyframes oppSavedPop { 0% { transform: scale(0.9); } 55% { transform: scale(1.06); } 100% { transform: scale(1); } }
    .opp-saved-flash { animation: oppSavedPop 0.32s ease-out; }

    /* Circular avatars: crop from the TOP of the photo — actor photos are
       portraits with the eyes in the top third, so a centered crop cuts faces. */
    img.rounded-full.object-cover { object-position: 50% 0%; }

    html { scroll-behavior: smooth; }
    * { -webkit-overflow-scrolling: touch; }

    /* Number inputs styled as compact "display values" (e.g. Mensurations
       cm / kg cards) — hide native spinners so the unit label sits right
       next to the number. */
    .cf-num-tight::-webkit-inner-spin-button,
    .cf-num-tight::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

    .dual-range-input::-webkit-slider-thumb {
      -webkit-appearance: none;
      pointer-events: all;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: #006a85;
      cursor: pointer;
      box-shadow: 0 0 4px rgba(0,106,133,0.5);
    }
    .dual-range-input::-moz-range-thumb {
      pointer-events: all;
      appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: #006a85;
      cursor: pointer;
      border: 0;
      box-shadow: 0 0 4px rgba(0,106,133,0.5);
    }

