/* =====================================================================
   Prosyjob — application layer
   =====================================================================
   Loaded LAST, after both prosyjob-components.css and whichever vendor theme
   AssetStacks has activated. This is the only stylesheet the application
   itself owns; everything under assets/user-template and assets/admin-template
   is generated and must not be edited (see assets/VENDORING.md).

   It holds three kinds of rule and nothing else:
     1. contracts the JS depends on and no vendor sheet may break;
     2. the few shell variants the template did not ship;
     3. the dark theme.

   RULE FOR EVERY WAVE: no raw hex below this line — always a var(--pj-*).
   That discipline is what keeps the dark block at the bottom to ~50 lines
   instead of a second pass over 97 templates.
   ===================================================================== */


/* ── 0. The token scale, on every stack ────────────────────────────────
   The `--pj-*` scale is declared in `user-template/css/prosyjob.css` — and
   AssetStacks switches that sheet OFF for the admin console, because the two
   vendor themes both define `.card` / `.btn` / `.header` and only one may
   apply at a time.

   So every `var(--pj-…)` written below resolved to *nothing* on the admin
   console in light mode. That is not a fallback to a default: a custom
   property with no value makes the whole declaration invalid at computed-value
   time, so `border: 1px solid var(--pj-line)` becomes `border-color: inherit`.
   Measured on /admin/villes before this block existed: the header's theme
   toggle came out a 20x40 oval with a black border, and the language pill
   inherited plain black — the two controls E7-13 had just "fixed" in dark,
   where the block at the bottom of this file happens to define the same names.

   The scale is therefore restated here, in the one sheet every stack loads.
   It is a MIRROR of prosyjob.css, not a second source of truth:
   `tools/check-assets.mjs` compares the two and fails the build if they drift.
   The dark block at the end of this file overrides these at the same `:root`
   level and wins on order, so dark keeps working on all three shells. */
:root {
    --pj-orange: #EF5A28;
    --pj-orange-dark: #D8481A;
    --pj-orange-soft: #FEEDE5;
    /* E8-03: the surface pair. White on the brand orange is 3,41:1 — fine for
       an accent, short of AA under a 15px button label. Splitting the fill off
       leaves `--pj-orange` the brand colour everywhere it is *text* or a
       border, exactly as `--primary-fill` did for the blue in E7-15. The hover
       has to be darker than the resting fill; the old hover
       (`--pj-orange-dark`) is lighter than it, so the button would have
       brightened on hover. Mirrored from prosyjob.css — check-assets.mjs
       fails if the two drift. */
    --pj-orange-fill: #CE4416;
    --pj-orange-fill-hover: #B03A12;
    /* E10-02: the brand as *text*. `--pj-orange-dark` measured 3,80:1 on the
       pale orange chip behind an active menu entry (29 of them across 13
       screens) and `--pj-orange` 3,41:1 on white for outline-button labels.
       Third cut of the same pattern after --pj-orange-fill: the brand keeps
       `--pj-orange` for borders, icons and decoration, and darkens only where
       it is read. The dark block below overrides this to the lifted orange —
       dark already measured 6,23:1 and must not move. */
    --pj-orange-text: #C04017;
    --pj-navy: #34506E;
    --pj-navy-dark: #263D55;
    --pj-blue: #3B5BDB;

    --pj-ink: #1C2A3A;
    /* E8-02: was #7A8699 — 3,69:1 on white and 3,41:1 on the page background.
       Measuring where muted text actually lands put every occurrence on the
       page background (#F4F6FA), never on a card, so that is the surface the
       value has to clear: #657085 gives 4,61:1 there, 4,99:1 on white. */
    --pj-muted: #657085;
    --pj-line: #E9EDF3;
    --pj-bg: #F4F6FA;
    --pj-card: #FFFFFF;

    --pj-radius: 16px;
    --pj-radius-sm: 12px;
    --pj-radius-pill: 999px;
    --pj-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 8px 24px rgba(16, 24, 40, .06);
    --pj-shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);

    --pj-sidebar-w: 264px;
    --pj-rail-w: 300px;
    --pj-topbar-h: 72px;
    --pj-gap: 24px;
    --pj-max: 1560px;
}

/* Not in prosyjob.css — the raised surface the dark block needs for inputs,
   hovered rows and the admin console's inner panels. Declared here in both
   themes so a rule may use it without asking which one is active. */
:root {
    --pj-card-2: #EEF2F7;
}


/* ── 1. Contracts ──────────────────────────────────────────────────────
   The visibility guard. 154 places across 57 files set `element.hidden =
   true` and expect it to hold, and 47 templates ship the bare attribute.
   Bootstrap's reboot provides this too, but a vendor theme that sets
   `display` on `.card` / `.btn` / `.row` / `.form-group` would beat it — so
   it is restated here, at the end of the cascade, where nothing can.
   AppShell.assertHiddenGuard() probes this on every render in dev.
   Moved here from the component sheet, which is no longer the last one loaded. */
[hidden] {
    display: none !important;
}

.hidden {
    display: none !important;
}

/* The shell toggles `is-active`, the template's own CSS keys off `active`.
   Aliasing costs one rule; renaming would mean touching six JS files and
   would still break `.pj-shortcut.is-active`, which prosyjob.css spells with
   the `is-` prefix. So the vocabulary stays as the JS already writes it. */
.pj-nav a.is-active,
.pj-quicknav a.is-active {
    background: var(--pj-orange-soft);
    color: var(--pj-orange-text);
    font-weight: 700;
}

.pj-nav a.is-active i {
    color: var(--pj-orange);
}

.pj-quicknav a.is-active {
    background: transparent;
    border-bottom: 2px solid var(--pj-orange);
    border-radius: 0;
}


/* ── 2. Shell geometry ─────────────────────────────────────────────────
   The shell must present the same three columns on every page at a given
   viewport. It did not: `/tableau-de-bord` came out 1016px wide with a 356px
   centre column where `/profil` filled 1560px with 900px, at the same 1920px
   viewport.

   `#app` is `display: flex; flex-direction: column` (prosyjob-components.css),
   so `.pj-shell` and `.pj-footer` are flex items, and both carry
   `margin: 0 auto` from prosyjob.css to centre themselves under `--pj-max`.
   An auto margin on a flex item's *cross* axis cancels the default `stretch`,
   and the item is then sized to its own max-content instead of to the line —
   so the `minmax(0, 1fr)` centre track was measuring the page's content, not
   the space available to it. Every page got its own layout, and the footer
   (415px, centred on itself) never lined up with the shell above it.

   `width: 100%` restores the stretch. `max-width: var(--pj-max)` still caps
   it and the auto margins still centre it, so the intended geometry is
   unchanged — it is now simply derived from the viewport at every breakpoint
   rather than from whatever the page happened to render. */
.pj-shell,
.pj-footer {
    width: 100%;
}

/* One flat background for the whole shell, in both themes.
   `.layout-auth` and `.layout-guest` in prosyjob-components.css paint a blue
   radial tint over `linear-gradient(180deg, var(--surface-sunken), #eff4fb)`.
   That second stop is a raw light hex, so under `[data-theme="dark"]` the
   page started at #1e2733 and ended at #eff4fb — scrolling a connected page
   faded the background from dark to near-white behind dark cards. Repointing
   the hex at a token would only make the gradient theme-aware, and the pj
   shell does not want a gradient at all: prosyjob.css already states the
   intent on `body.pj-body`, a single `--pj-bg`. The shells take that and the
   gradient goes, which is also what keeps the two shells identical.
   Scoped to `.pj-app` — the guest and connected shells — so the admin
   console (`.layout-admin`, mutually exclusive with it) is untouched. */
.pj-app {
    background: var(--pj-bg);
}

/* The admin console's content column, spaced like the other two shells.

   `auth-layout.html` and `guest-layout.html` both mount `<main id="content"
   class="pj-main">`, and `.pj-main` (prosyjob.css) is what stacks a page's
   blocks with `--pj-gap`. `admin-layout.html` mounts a bare `<main
   id="content">`: the 27 administration screens therefore had NO spacing at
   all between their top-level blocks — measured at 0px, on screens stacking
   up to eight of them.

   The rule cannot live in prosyjob.css, which AssetStacks switches off on
   this stack; hence here.

   20px, the same figure as `.connected-page`, so a page reads with the same
   rhythm whichever console it belongs to. `--pj-gap` (24px) is deliberately
   not reused: that token is the gap BETWEEN the shell's own regions, not
   inside a page. */
.layout-admin #content {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* The gap alone did not give 20px: Bootstrap puts `margin-bottom:
   var(--bs-card-group-margin)` (0.75rem) on every `.card`, and the admin
   theme puts its own on `.page-header`. Those margins ADD to the gap —
   measured 36px and 40px between blocks where 20 was intended. Spacing is
   now carried by the gap and nothing else, which is the point of having one.

   Scoped to the direct children of the content column: margins inside a card
   are none of this rule's business. */
.layout-admin #content > * {
    margin-bottom: 0;
}

/* Le même oubli, une strate plus bas : la cible de rendu qui avale le `gap`.

   `.pj-card` ne porte aucune marge — tout l'espacement d'une page vient du
   `gap` de son conteneur. Neuf pages écrivent pourtant leurs cartes dans un
   `<div id="…-body">` intermédiaire, sans classe, posé entre la section et
   elles :

       <section class="jo-public">       <- display: grid; gap: 16px
           <div id="jo-public-body">     <- l'unique enfant, donc le seul
               <article class="pj-card">    élément que ce gap sépare de…
               <article class="pj-card">    personne
           </div>
       </section>

   Le gap de la section s'applique à ce div-là et jamais aux cartes : elles se
   touchaient bord à bord, réduites au filet d'1px de leur bordure là où 16
   étaient prévus (/emplois/offre, la fiche formation, les fiches entreprise
   et organisme, la fiche candidat, les deux pages d'invitation, l'aperçu de
   CV, les statistiques organisme).

   Le div est une cible de rendu, pas une boîte de mise en page. On lui donne
   donc le rythme de la page plutôt que de le rendre transparent avec
   `display: contents` : `querySelector('#…-body').innerHTML = …` reste alors
   ce qu'il paraît être, une écriture dans un conteneur qui sait empiler.
   16px, la valeur des sections qui le contiennent, et `align-content: start`
   pour qu'une page courte n'étale pas ses cartes sur la hauteur disponible.

   Ici plutôt que dans la feuille des composants : huit modules s'en servent,
   et la règle doit valoir sur les trois coquilles. */
.pj-stack {
    display: grid;
    gap: 16px;
    align-content: start;
    /* Une pile est à une colonne, et cette colonne ne doit jamais s'élargir
       pour un contenu trop large : sans `minmax(0, 1fr)`, sa piste hérite du
       minimum `auto` et une carte plus large que la colonne centrale la fait
       grandir au lieu de déborder d'elle-même. C'est par ce chemin que la
       carte de « Mon site web » est passée sous le rail (2026-08-30) : la
       piste de `#wbs-body` mesurait 871 px dans un élément de 765. Le contenu
       trop large doit désormais déborder de sa propre pile — visible, donc
       corrigeable — plutôt que déformer la page autour de lui. */
    grid-template-columns: minmax(0, 1fr);
}

/* ── 2b. Shell variants the template did not ship ──────────────────────
   prosyjob.css defines the 3-column shell and a 2-column `--norail`, but
   nothing for a logged-out visitor: the template's own pages are all
   connected. 16 of our page classes serve `auth: "public"` routes and so
   render in the guest layout too, where a user sidebar would be nonsense. */
.pj-shell--guest {
    grid-template-columns: minmax(0, 1fr);
}

/* Guest topbar: no avatar or bell, a pair of CTAs instead. */
.pj-topbar__cta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

/* Every bare <button> in the connected shell must opt out of the component
   sheet's full-width contract. Scoped to the shell so it cannot leak into a
   page. 
.pj-topbar button,
.pj-side button,
.pj-rail button {
    width: auto;
}*/

/* Tap targets. The project rule is >= 44px and it is measured, not eyeballed
   (`.btn--sm` sat at 38px for five milestones because nobody checked). The
   template's sidebar links come out at 43px from padding alone, so the floor
   is set explicitly rather than tuned via padding. */
.pj-nav a,
.pj-shortcut,
.pj-lang {
    min-height: 44px;
}

.pj-nav a {
    box-sizing: border-box;
}

/* ── L'accordeon de la barre laterale ──────────────────────────────────
   Demande en recette : cinq rubriques, repliees par defaut, « pour eviter
   que le menu ne paraisse surcharge ».

   Le bouton PORTE deja `.pj-side__label` (prosyjob.css) et lui emprunte donc
   sa typographie ; ces regles ne font que le rendre cliquable sur toute la
   largeur et lui donner sa hauteur de cible. `all: unset` n'est pas utilise :
   il effacerait aussi le libelle herite. On defait a la main les trois
   proprietes que l'agent utilisateur pose sur un <button> et rien d'autre.

   La fleche tourne au lieu de changer d'icone : deux glyphes different
   demanderaient un aller-retour dans le DOM a chaque bascule, et une
   transition sur `transform` est la seule chose ici que le compositeur peut
   animer sans reflow. `prefers-reduced-motion` la coupe — une fleche qui
   pivote n'apprend rien de plus qu'une fleche deja pivotee. */
.pj-side__group {
    display: block;
}

.pj-side__toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    min-height: 44px;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
    /* `.pj-side__label` (prosyjob.css) fournit deja taille, graisse, casse,
       interlettrage et couleur : ce sont des regles d'auteur, elles battent
       l'agent utilisateur sans qu'on ait a les repeter. Seule la FAMILLE doit
       etre reprise — c'est la seule que `.pj-side__label` ne declare pas, donc
       la seule ou le `font: 400 13.33px Arial` du <button> passerait.

       Ne pas ecrire `font: inherit` ici : la forme courte reinitialise aussi
       taille et graisse, et le libelle est ressorti en 16px maigre sans
       capitales — mesure, pas suppose. */
    font-family: inherit;
}

.pj-side__toggle:hover,
.pj-side__toggle:focus-visible {
    color: var(--pj-ink);
}

.pj-side__chev {
    font-size: 10px;
    flex: none;
    transition: transform .18s ease;
}

/* Ferme = fleche vers la droite ; ouvert = vers le bas (l'orientation du
   glyphe au repos). L'etat est lu sur `aria-expanded`, qui est la meme source
   que celle annoncee aux lecteurs d'ecran : impossible que le dessin et
   l'annonce divergent. */
.pj-side__toggle[aria-expanded="false"] .pj-side__chev {
    transform: rotate(-90deg);
}

@media (prefers-reduced-motion: reduce) {
    .pj-side__chev {
        transition: none;
    }
}

/* Bootstrap buttons come out at 41px here, `.btn-lg` included — under the
   floor, and it is the primary action on most screens. The component sheet
   used to set this on `.btn` behind the E7 gate; the gate went in E7-14, so
   this is now the only place the floor is stated for the pj dialect.
   Scoped to `.pj-app`, which is the guest and connected shells only: the
   admin console's table-row icon buttons are `.btn-sm` by design and its
   layout does not carry this class.
   Inline text links are deliberately not included — WCAG 2.5.5 exempts a
   link inside a block of text, and padding them out would wreck the copy. */
.pj-app .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
}

/* prosyjob.css drops the round buttons to 40px below 420px to fit the bar.
   That is exactly the width where tap accuracy matters most, and 44px still
   fits: four of them plus the burger and the language pill come to ~266px of
   a 360px viewport. Measured at a real 357px viewport, not eyeballed. */
@media (max-width: 420px) {
    .pj-iconbtn,
    .pj-burger {
        width: 44px;
        height: 44px;
    }
}

/* The legal links (E14-04). They used to sit at the bottom of the connected
   sidebar, which the guest shell does not have — so a logged-out visitor, the
   one most likely to be looking for a privacy policy, never saw them. They are
   in both footers now, and the sidebar's "À propos" block is gone.

   Five links have to wrap at 360px, so this is a wrapping flex row with a gap
   rather than the `·`-separated span it replaces: a hard separator between
   items that wrap leaves an orphaned dot at the end of a line. */
.pj-footer__links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    column-gap: 16px;
    row-gap: 0;
}

/* The five authentication screens paint their own header; BasePage sets this
   from `get chrome()` so the shell's bar and footer step aside rather than
   duplicating theirs. (The guest home does NOT — since E7-02 the shell emits
   the same bar it used to draw itself.) */
.is-chromeless .pj-topbar,
.is-chromeless .pj-footer {
    display: none;
}

/* Those pages are `body.account-page`, whose own `.content` supplies the
   vertical rhythm of the centred card. The shell grid's padding would stack
   on top of it and push the card off-centre. */
.is-chromeless .pj-shell {
    padding: 0;
    max-width: none;
}

/* The template ships one card width. Registration collects a whole identity
   and verification carries two independent forms side by side, so both need
   more room than a two-field sign-in. */
.login-right--wide {
    max-width: 720px;
}

/* `.pj-stats` is a fixed three columns, which orphans the fourth card when a
   grid holds four — the dashboard has exactly four profiles. Auto-fit keeps
   the tiles even at any count and needs no breakpoint of its own. */
.pj-stats--auto {
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

/* A `.pj-stat` shows one number. The dashboard's profile tiles carry two or
   three counters plus an action, so they lay them out in a row and the card
   becomes a column with the button pinned to the bottom — otherwise tiles with
   different counter counts end up with buttons at different heights. */
.pj-stat {
    display: flex;
    flex-direction: column;
}

.pj-stat__row {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    margin: 4px 0 18px;
}

.pj-stat__row .pj-stat__num {
    font-size: 26px;
}

.pj-stat .btn {
    margin-top: auto;
}

/* Not-yet-activated profiles read as an invitation, not as data. */
.pj-stat--muted .pj-stat__ico {
    background: color-mix(in srgb, var(--pj-muted) 14%, transparent);
    color: var(--pj-muted);
}

.pj-stat--muted .pj-stat__desc {
    margin-bottom: 18px;
}

/* ── Controls that must work on every stack ────────────────────────────
   The theme toggle and the language pill appear in all three shells, but the
   classes the template gives them (`.pj-iconbtn`, `.pj-lang`) live in
   prosyjob.css — the *front* theme, which AssetStacks switches off for the
   admin console. Their baseline therefore has to live here, in the one sheet
   that is always applied. The front theme still layers its own look on top
   where it is active. */
.pj-theme-toggle {
    width: 40px;
    height: 40px;
    min-height: 40px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--pj-line);
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font-size: 16px;
    cursor: pointer;
}

.pj-theme-toggle:hover {
    background: var(--pj-orange-soft);
    color: var(--pj-orange);
}

.pj-lang-trigger {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
    cursor: pointer;
}

/* In the admin header the trigger is a <button> inside a `.dropdown`, so the
   theme's `.pj-flag-nav > .nav-link` rule (a direct-child selector) never
   matches it. Give it the same shape here rather than reshuffling the markup
   to satisfy a selector. */
.layout-admin .pj-lang-trigger {
    background: transparent;
    border: 0;
    color: var(--pj-ink);
    padding: 0 6px;
}

/* Both controls are bare <button>s, and prosyjob-components.css neutralises those for the
   admin console with `.layout-admin button { width: auto; min-height: auto }`
   — one class plus one type, which outranks the plain `.pj-theme-toggle`
   above and flattened the toggle to 20x40 and the language pill to 85x24.
   Restated at two classes so it wins, and at 44px because that is the tap
   target this project holds itself to and these sit in the header of a
   console that is used on a phone like everything else.
   Those neutralisations are still there; the 44px floor has to outrank them. */
.layout-admin .pj-theme-toggle,
.layout-admin .pj-lang-trigger {
    min-height: 44px;
}

.layout-admin .pj-theme-toggle {
    width: 44px;
    height: 44px;
}

/* Tap targets in the console.
   E7-04 exempted the admin buttons from the 44px floor — "the admin console's
   table-row icon buttons are `.btn-sm` by design" — but the result was never
   measured. At a real 356px viewport they come out 31px tall, and they are the
   edit and delete controls of every list in the console; the sidebar links
   land at 43 and the header's "+" at 42. The floor is a project rule, not a
   preference, and an administrator holds the same phone as everyone else.
   Raised here rather than by un-sizing `.btn-sm`, so the buttons keep their
   compact type and padding and only the hit area grows. Re-measured at 356px:
   no row wraps, nothing overflows. */
.layout-admin .btn {
    min-height: 44px;
}

.layout-admin .add-button {
    width: 44px;
    height: 44px;
}

.layout-admin .sidebar-menu li a {
    min-height: 44px;
}

/* The console menu has to be scrollable, because it is taller than any screen.
   The vendor sets `.sidebar { position: fixed; top: 60px; bottom: 0;
   overflow-y: hidden }` and hands the scrolling to the jQuery slimScroll
   plugin — which was never vendored (`$.fn.slimScroll` is undefined, and the
   `.slimScrollDiv` it would build is nowhere in the page). So the overflow
   stayed hidden with nothing taking over, and the menu was simply cut off with
   no way to reach the rest.

   Nothing failed loudly, which is why it lasted: the clipped rubrics are still
   reachable by typing their URL, so whoever knows the way never notices. The
   25 entries measure 1495px, needing a 1555px viewport — at 1366x768, fifteen
   of them were unreachable, Référentiels, Pays & tarifs and Traductions among
   them.

   The browser does this better than the plugin would. `overscroll-behavior`
   keeps the wheel from scrolling the page behind once the menu bottoms out. */
.layout-admin .sidebar {
    overflow-y: auto;
    overscroll-behavior: contain;
    /* The platform scrollbar is 15px of grey against a soft `#FFF8F7` panel,
       which reads as a seam down the menu. Thin, in the token line colour, and
       it stays a real scrollbar — visible, draggable, and there before the user
       hovers, which an overlay one would not be. No label is truncated by the
       width it takes: measured at 240px box / 225px usable. */
    scrollbar-width: thin;
    scrollbar-color: var(--pj-line) transparent;
}

.layout-admin .user-menu .nav-item {
    display: flex;
    align-items: center;
}

/* The vendor theme's custom checkbox paints its checked state with the theme's
   *own* orange `#E65425`, not the Prosyjob `#EF5A28`. prosyjob.css patches
   `.btn-primary` and friends but never got to `.checkmark`, and these appear
   on every filter panel — the one place two nearly-identical oranges sit next
   to each other and read as a mistake. */
.pj-app .custom_check input:checked ~ .checkmark {
    /* A filled box carrying a white tick — a surface, so it takes the E8-03
       fill like every other one. */
    background-color: var(--pj-orange-fill);
    border-color: var(--pj-orange-fill);
}

/* The template's bell indicator is a bare 8px dot. The app has always shown
   an unread *count*, which is strictly more information, so the dot grows a
   pill form when NavComponent gives it text.

   `.pj-iconbtn` is load-bearing, not decoration. The vendored rule is
   `.pj-iconbtn .pj-dot` — (0,2,0) — and this class alone is (0,1,0), so every
   one of the five properties below that the vendor also sets (top, right,
   width, height, border-radius) was LOSING, whatever the sheet order. The
   count pill rendered 18×8 with a 50% radius: a squashed orange ellipse with
   the number clipped out of it. Measured, then compared side by side against
   the vendored rule suppressed.

   Fixed here rather than by editing the generated theme: `vendor-template.mjs`
   rewrites that tree from upstream, so a hand-edit there is reverted by the
   next run. Raising our own selector keeps the bare 8px dot intact for any
   future use and leaves the vendored file untouched. */
.pj-iconbtn .pj-dot--count {
    width: auto;
    min-width: 18px;
    height: 18px;
    top: 2px;
    right: 2px;
    padding: 0 5px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    color: #fff;
    border-radius: var(--pj-radius-pill);
}


/* ── 2b. Shared referential controls ───────────────────────────────────
   ReferenceSelect, CityAutocomplete and SkillPicker are imported by 15, 8 and
   4 modules respectively, so they are re-skinned once here and every module
   inherits it. Moved out of the component sheet because they are shared
   infrastructure that every stack needs, including the admin console.

   The markup stays bespoke on purpose. Remapping the autocomplete onto
   Bootstrap's `.dropdown-menu` would buy nothing and would put the
   `is-active` class the keyboard handler toggles at risk. Only the colours
   move to tokens, so dark mode comes for free. */
.rf-autocomplete-wrap {
    position: relative;
}

.rf-autocomplete {
    position: absolute;
    z-index: 20;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    margin: 0;
    padding: 4px;
    list-style: none;
    max-height: 260px;
    overflow-y: auto;
    background: var(--pj-card);
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius-sm);
    box-shadow: var(--pj-shadow);
}

.rf-autocomplete__item {
    display: flex;
    align-items: baseline;
    gap: 8px;
    /* 44px: the minimum tap target this project holds itself to. */
    min-height: 44px;
    padding: 10px 12px;
    border-radius: 8px;
    cursor: pointer;
}

.rf-autocomplete__item:hover,
.rf-autocomplete__item.is-active {
    background: var(--pj-orange-soft);
}

.rf-autocomplete__item small {
    color: var(--pj-muted);
    font-size: 0.8rem;
}

.rf-autocomplete__item em {
    margin-left: auto;
    font-style: normal;
    font-size: 0.72rem;
    color: var(--pj-orange-text);
}

.rf-autocomplete__item--new {
    color: var(--pj-orange);
}

.rf-autocomplete__empty {
    padding: 12px;
    color: var(--pj-muted);
    font-size: 0.85rem;
}

/* Chips (skills, aliases, interests) */
.rf-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0 6px 6px 0;
    padding: 6px 8px 6px 12px;
    background: color-mix(in srgb, var(--pj-navy) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--pj-navy) 18%, transparent);
    border-radius: var(--pj-radius-pill);
    font-size: 0.85rem;
    color: var(--pj-ink);
}

/* A skill the user typed that does not exist yet — pending curation. */
.rf-chip--new {
    background: var(--pj-orange-soft);
    border-color: color-mix(in srgb, var(--pj-orange) 35%, transparent);
}

/* Read-only chip: same shape, no remove button. */
.rf-chip--static {
    background: color-mix(in srgb, var(--pj-navy) 8%, transparent);
    border-color: color-mix(in srgb, var(--pj-navy) 18%, transparent);
}

.rf-chip__remove {
    width: 24px;
    height: 24px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: color-mix(in srgb, var(--pj-ink) 8%, transparent);
    color: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.rf-chip__remove:hover {
    background: color-mix(in srgb, var(--pj-ink) 18%, transparent);
}

.rf-skills__chips {
    margin-bottom: 8px;
}

.rf-skills__hint,
.rf-skills__empty {
    display: block;
    margin-top: 6px;
    font-size: 0.8rem;
    color: var(--pj-muted);
}

/* BaseForm emits Bootstrap's `.invalid-feedback` since E7, but templates that
   have not been converted yet still carry static `<span class="field-error">`
   placeholders they fill themselves. Aliasing the two keeps a half-converted
   form from showing two different error styles. Deleted in Wave 9 with the
   last `.field-error` in the templates. */
.field-error {
    color: var(--bs-form-invalid-color, #dc3545);
    font-size: 0.875em;
}

/* BaseForm marks the offending control `is-invalid`, but Bootstrap only paints
   a border for `.form-control.is-invalid`. Legacy templates use bare inputs
   inside `.field`, so on those the flag would set the message and leave the
   field looking fine — the one place a user actually looks. Matching the
   element directly covers both dialects. */
input.is-invalid,
select.is-invalid,
textarea.is-invalid {
    border-color: var(--bs-form-invalid-border-color, #dc3545);
}


/* ── 3. Loading state ──────────────────────────────────────────────────
   The template shipped prosyjob-loader.js for this, but it intercepts every
   link click in the capture phase and only hides on `window.load` — which
   never fires again in an SPA, so it would lock the app on first navigation.
   Its look is kept; the trigger stays our own ref-counted [activity_loader],
   driven by the axios interceptors in framework/core/ApiClient.js. */
.global-loader {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--pj-bg) 82%, transparent);
}

.loader-spinner {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 4px solid var(--pj-line);
    border-top-color: var(--pj-orange);
    border-right-color: var(--pj-orange);
    animation: pj-spin 0.7s linear infinite;
}

@keyframes pj-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Opt-in per call site via BasePage.busy(); never a global listener. */
.pj-btnloading {
    position: relative !important;
    pointer-events: none !important;
    color: transparent !important;
}

.pj-btnloading > * {
    visibility: hidden !important;
}

.pj-btnloading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    border: 2px solid rgb(255 255 255 / 55%);
    border-top-color: #fff;
    animation: pj-spin 0.6s linear infinite;
    visibility: visible !important;
}

@media (prefers-reduced-motion: reduce) {
    .loader-spinner,
    .pj-btnloading::after {
        animation-duration: 2s;
    }
}


/* ── 4. Dark theme ─────────────────────────────────────────────────────
   Neither vendor theme ships one: zero prefers-color-scheme rules in either.
   Three layers make it up, and only the middle one is ours to write:

     Bootstrap primitives  →  free, via [data-bs-theme="dark"] (5.3.8)
     the .pj-* shell       →  this block, because prosyjob.css is tokenised
     Kofejob overrides     →  Wave 10, over whatever survives the purge

   `data-theme` is set on <html> by the inline script in index.html before
   first paint, so there is no flash. */
:root[data-theme="dark"] {
    --pj-bg: #0f141b;
    --pj-card: #171e28;
    --pj-card-2: #1e2733;
    --pj-line: #2a3441;

    --pj-ink: #e8edf4;
    --pj-muted: #94a3b5;

    /* Lifted from #EF5A28: the light-theme orange clears 4.5:1 on a dark
       surface only just, and this keeps a margin for small text. */
    --pj-orange: #ff6b3d;
    --pj-orange-dark: #ff8355;
    --pj-orange-soft: #3a1f14;
    /* E10-02 is a LIGHT-theme correction. Dark already measured 6,23:1 for the
       same elements, and darkening the token here would have taken it to 2,88 —
       the same trap the pale chips sprang in E8-02. So the text token simply
       keeps the value those elements already resolved to. */
    --pj-orange-text: #ff8355;

    /* Navy is the light theme's structural colour; on dark it has to come up
       to stay visible as anything other than background. */
    --pj-navy: #4e7099;
    --pj-navy-dark: #3d5a7d;
    --pj-blue: #6d87e8;

    /* Shadows do not read on dark surfaces — borders carry the elevation. */
    --pj-shadow: 0 0 0 1px rgb(255 255 255 / 4%);
    --pj-shadow-sm: 0 0 0 1px rgb(255 255 255 / 3%);

    color-scheme: dark;
}

/* Legacy tokens, pointed at their dark equivalents.
   =================================================
   prosyjob-components.css defines its own palette in five `:root` blocks and 6 700
   lines hang off it. Repainting those rules one by one would be a second full
   pass over the app; repointing the eleven structural tokens does the whole
   thing at once — and it works on pages that have not been converted yet,
   which is what keeps the app usable in dark mode all the way through E7
   rather than only at the end.

   This remap is what makes that whole sheet follow the theme; it stays as long
   as the component sheet writes `--color-*` rather than `--pj-*`.

   The status palettes (`--ap-*` applications, `--mt-*` matching, `--pay-*`
   payments) are left alone: they are saturated signal colours that read on
   either background, and their meaning must not shift with the theme. */
:root[data-theme="dark"] {
    --color-bg: var(--pj-bg);
    --color-surface: var(--pj-card);
    --color-text: var(--pj-ink);
    --color-muted: var(--pj-muted);
    --color-border: var(--pj-line);

    --connected-bg: var(--pj-bg);
    --connected-surface: var(--pj-card);
    --connected-text: var(--pj-ink);
    --connected-muted: var(--pj-muted);
    --connected-border: var(--pj-line);
    --connected-shadow: var(--pj-shadow-sm);

    /* E7-15: the accent pair was missed the first time round, and it is the
       colour of every "read more", every recommendation score and every active
       filter chip. `#2563eb` on a `#171e28` card measures 1.9:1 — the link that
       tells you where to go next was the least readable text on the page. */
    --color-primary: var(--pj-blue);
    --color-primary-dark: var(--pj-navy);
    --connected-primary: var(--pj-blue);
    --connected-primary-deep: var(--pj-navy);
    /* `--primary-fill` is deliberately NOT lifted — it is the same blue used
       as a solid background under a white label (the active tab of every
       pipeline, the active CMS filter). Lifting it there costs contrast
       instead of buying it: white on #264A78 is 9:1, white on the lifted
       #6d87e8 is 3.3:1. One colour, two jobs, opposite directions — the
       component sheet now names them separately. */
    --connected-accent: var(--pj-orange);
    --connected-accent-soft: var(--pj-orange-soft);

    /* Pale panels the component sheet writes as raw hex. These are surfaces
       carrying theme-following text (`--connected-text` / `--connected-muted`),
       so unlike the status chips they cannot stay light: the background would
       hold still while the text above it turned pale. Named here rather than
       repainted rule by rule, and given their current light values in the
       component sheet's own `:root` so light is unchanged. */
    --surface-sunken: var(--pj-card-2);
    --surface-chip: var(--pj-card-2);
    --surface-track: var(--pj-line);
    --chip-ink: var(--pj-ink);
}

/* Bootstrap's dark mode covers `.form-control`, but a legacy template's bare
   `<input>` inside `.field` never matches it, and the component sheet paints those with
   an explicit light background. Same reasoning as the token remap: fix the
   dialect, not the individual pages. */
:root[data-theme="dark"] .pj-app input:not([type="checkbox"]):not([type="radio"]),
:root[data-theme="dark"] .pj-app select,
:root[data-theme="dark"] .pj-app textarea {
    background-color: var(--pj-card-2);
    border-color: var(--pj-line);
    color: var(--pj-ink);
}

/* …and the same shape again for the ones that cannot be typed in (E12-04).
   The rule above matches `:disabled` too, so a locked field came out with the
   same background, text and border as an editable one — measured on /profil in
   edit mode, where email and phone are shown but not editable: identical
   computed styles, the cursor the only difference, and a cursor is invisible
   until you hover.

   The selector repeats that shape rather than reading `.form-control:disabled`
   because the rule above is (0,5,1) and would otherwise win on specificity —
   an `!important` here would be papering over the cascade, not using it.
   Repainting a control that is already non-interactive cannot break a working
   screen; it only stops one from lying. */
:root[data-theme="dark"] .pj-app input:not([type="checkbox"]):not([type="radio"]):disabled,
:root[data-theme="dark"] .pj-app input:not([type="checkbox"]):not([type="radio"])[readonly],
:root[data-theme="dark"] .pj-app select:disabled,
:root[data-theme="dark"] .pj-app textarea:disabled,
:root[data-theme="dark"] .pj-app textarea[readonly] {
    background-color: var(--pj-bg);
    color: var(--pj-muted);
    cursor: not-allowed;
}

/* ── Le remplissage automatique de Chrome ──────────────────────────────
   Signale en recette : en thème sombre, le champ « Email » de /connexion
   reste BLANC pendant que le champ mot de passe juste dessous est correct.

   Ce n'est pas une règle qui manque chez nous, c'en est une qui vient du
   thème. `user-template/css/style.css` (et son jumeau côté console,
   `admin-template/css/style.css`) posent :

       input:-webkit-autofill { -webkit-box-shadow: 0 0 0 1000px white inset !important }

   — un blanc en dur, en `!important`, sans thème. Chrome peint ce fond par
   une ombre interne, donc `background-color` ne le recouvre pas : la règle
   sombre plus haut repeint bien l'input, et l'ombre repasse par-dessus.
   Le texte, lui, garde `--pj-ink` (clair) : clair sur blanc, illisible.
   D'où l'asymétrie observée — seul l'identifiant est rempli au chargement
   (`autocomplete="username"`), le mot de passe attend un geste.

   Trois conséquences pour la forme de ce correctif :
   - `!important` est nécessaire, pas décoratif : il faut battre celui du
     thème, et la spécificité seule n'y suffit pas.
   - `-webkit-text-fill-color` double `color`, que Chrome ignore ici.
   - la règle n'est PAS limitée à `.pj-app` : les deux thèmes portent la
     même ligne, et les jetons `--pj-*` sont miroités sur `:root` (§ en tête
     de ce fichier) précisément pour qu'une règle partagée par les trois
     coquilles puisse s'écrire une seule fois.

   Elle vit ici, dans le bloc sombre, parce que c'est là qu'on la cherchera —
   mais la première moitié est bien commune aux deux thèmes. Se vérifie avec
   un identifiant *enregistré* dans le navigateur : taper la valeur à la main
   ne déclenche pas `:-webkit-autofill`. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
select:-webkit-autofill {
    -webkit-box-shadow: 0 0 0 1000px var(--pj-card) inset !important;
    -webkit-text-fill-color: var(--pj-ink) !important;
    caret-color: var(--pj-ink);
}

/* En sombre, l'input plein est `--pj-card-2` (règle plus haut) et non
   `--pj-card` : sans ce second passage, un champ rempli automatiquement
   serait d'un ton plus foncé que son voisin vide. */
:root[data-theme="dark"] input:-webkit-autofill,
:root[data-theme="dark"] input:-webkit-autofill:hover,
:root[data-theme="dark"] input:-webkit-autofill:focus,
:root[data-theme="dark"] input:-webkit-autofill:active,
:root[data-theme="dark"] textarea:-webkit-autofill,
:root[data-theme="dark"] select:-webkit-autofill {
    -webkit-box-shadow: 0 0 0 1000px var(--pj-card-2) inset !important;
}

:root[data-theme="dark"] .pj-app label,
:root[data-theme="dark"] .pj-app h1,
:root[data-theme="dark"] .pj-app h2,
:root[data-theme="dark"] .pj-app h3,
:root[data-theme="dark"] .pj-app h4 {
    color: var(--pj-ink);
}

/* The vendor themes hardcode white backgrounds on the shell chrome, so the
   handful of places prosyjob.css did not tokenise are corrected here. */
:root[data-theme="dark"] .pj-burger,
:root[data-theme="dark"] .pj-iconbtn,
:root[data-theme="dark"] .pj-lang {
    background: var(--pj-card-2);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .pj-search input {
    background: var(--pj-card-2);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .pj-nav a:hover {
    background: var(--pj-card-2);
}

/* The brand mark is navy + orange on transparent and disappears against a
   dark surface. Until the designer supplies a light variant, lift it just
   enough to read. See the "Known gap" note in assets/VENDORING.md.
   The admin header carries the same mark (measured on a 1300px viewport: the
   navy "Prosy" and "ob" were all but gone, leaving a floating orange J), so
   it takes the same treatment — the two shells must not show two different
   logos to the same person switching between them. */
:root[data-theme="dark"] .pj-topbar__brand img,
:root[data-theme="dark"] .layout-admin .header .logo img {
    filter: brightness(0) invert(1) sepia(1) saturate(6) hue-rotate(346deg);
}

/* Authentication screens.
   The vendor theme hardcodes this whole surface light — `.account-page` is
   `#fff`, the card is `#fff`, and `.focus-label` is `#131135`. Converted in
   Wave 1, so they get their dark treatment here rather than waiting for
   Wave 10: a page that ships in a broken state in one theme is not converted,
   it is half-converted. */
:root[data-theme="dark"] body.account-page {
    background-color: var(--pj-bg);
}

:root[data-theme="dark"] .account-page .content {
    background: transparent;
}

:root[data-theme="dark"] .account-content {
    background-color: var(--pj-card);
    border: 1px solid var(--pj-line);
}

:root[data-theme="dark"] .account-content .focus-label,
:root[data-theme="dark"] .account-page .login-header h3 {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .account-page .form-control {
    background-color: var(--pj-card-2);
    border-color: var(--pj-line);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .account-page .form-control::placeholder {
    color: var(--pj-muted);
}

:root[data-theme="dark"] .account-page .dont-have,
:root[data-theme="dark"] .account-page .text-muted,
:root[data-theme="dark"] .account-page .toggle-password {
    color: var(--pj-muted) !important;
}

/* The auth screens' fixed top-right slot holds the theme toggle beside the
   language pill; prosyjob.css sizes the container for one control only. */
.pj-auth-lang {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* ── The avatar placeholder ────────────────────────────────────────────
   `is-default-avatar` was set on the top bar's and the sidebar's <img> and
   styled NOWHERE: the only rule naming it targets
   `.connected-profile-menu__trigger`, a different element that paints its
   picture as a background. So the placeholder fell through to the vendored
   `.pj-avatar-btn img` / `.pj-side__user img`, which are `object-fit: cover` —
   and the asset was a 76x50 LANDSCAPE thumbnail, so roughly a third of its
   width was cropped away inside a round slot.

   `img.` for the same reason as the terms link and the bell badge: those two
   vendored rules are (0,1,1), a bare class is (0,1,0) and loses however late
   it is declared. Qualifying by tag ties and wins on order.

   `contain` because a placeholder must never be cropped, and the surface is a
   token so it follows the theme — the asset is transparent precisely so this
   rule owns the colour.

   No padding here on purpose. A percentage padding resolves against the
   PARENT's width, not the image's, which measured 6px in the top bar and 32px
   in the sidebar — and 32px of padding on a 46px `box-sizing: border-box` box
   pushed the element out to 64px. The breathing room belongs inside the SVG's
   own viewBox, where it is the same at every size. */
img.is-default-avatar {
    object-fit: contain;
    object-position: center;
    background-color: var(--pj-card-2);
}


/* ── A select that looks like one ──────────────────────────────────────
   `select.form-control` is `appearance: none` with nothing put back, so every
   select in the app paints exactly like a text input. That is an app-wide gap
   (logged in the plan; fixing it on `select.form-control` would restyle ~20
   screens no single task verifies), so the caret is opt-in — but shared, since
   E12-04 needed it a second time and a per-page copy is how a third one gets
   it slightly wrong.

   Lives here rather than in the component sheet because `/profil` also renders
   inside the admin console, where the front theme is switched off; this file is
   on all three stacks.

   A `mask` would let the colour be a token, but it needs a wrapper element to
   hang off — the data URI carries the colour instead, which is why the dark
   theme gets its own copy. That is the exception the no-raw-hex rule is worth
   making, not a licence to spread it: the two strokes are `--pj-muted` in each
   theme (#657085 / #94a3b5) and must be changed with it, since no checker can
   see inside a URL. */
.select-caret {
    padding-right: 34px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23657085' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 14px 14px;
}

:root[data-theme="dark"] .select-caret {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2394a3b5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}


/* ── Registration: dialling code + local number (E12-02) ───────────────
   One field to the eye, two controls to the DOM.

   The select is sized by its content, and the pair is allowed to WRAP. Both
   were measured, not assumed: `.form-control` spends 30px on padding and a
   native select ~20px on its arrow, so a select given 45% of a 360px card has
   69px left for a label that needs 107 — every country came out truncated,
   and "+225 Côte d'Ivoire" was still cut at 768. Splitting the row differently
   only moves the problem onto the number input.

   So on a phone they stack (each control full width, nothing clipped) and
   from ~330px of available row they sit side by side. `flex-basis: 14ch` on
   the number is what decides that: it is the width below which a 9-digit
   number stops being readable, and the point at which the row breaks. */
.pj-phone {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: stretch;
}

.pj-phone__code {
    width: auto;
    flex: 0 0 auto;
}

.pj-phone__number {
    flex: 1 1 14ch;
    min-width: 0;
}

/* The terms row: box, label and link on one baseline, wrapping as a unit on a
   narrow screen. Bootstrap's `.form-check` indents its label with padding and
   pulls the box into it with a negative margin — which puts the sibling link
   at the wrong place once the label no longer wraps it, so the row is laid
   out here instead. Row height is held at 44px for E10-01. */
.pj-terms {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding-left: 0;
    min-height: 44px;
}

.pj-terms .form-check-input {
    margin: 0;
    flex: 0 0 auto;
}

.pj-terms .form-check-label {
    margin: 0;
}

/* `a.` — not a stylistic flourish. prosyjob.css carries `.pj-app a { color:
   inherit }`, which is specificity (0,1,1); a bare `.pj-terms__link` is
   (0,1,0) and loses however late it is declared, so the link painted as body
   text and stopped looking like a link at all. Qualifying by tag ties the
   specificity and wins on order, and unlike `.pj-app .pj-terms__link` it still
   holds if this control is ever used outside the pj shell. */
a.pj-terms__link {
    color: var(--pj-orange-text);
    text-decoration: underline;
}


/* ── 4b. Dark theme — the guest and connected shells ───────────────────
   prosyjob.css is tokenised and follows the theme; `user-template/style.css`,
   the Kofejob sheet underneath it, is not — and it owns `.card`, which is the
   frame of nearly every screen in the user space. Measured across 25 user
   screens before this block: `.card` stayed `#fff` while Bootstrap's dark mode
   lightened the text inside it, so headings came out at 1.2:1 and body copy at
   1.3:1 — the same failure the admin console had, one shell over.

   Same method as the console (E7-13): a luminance probe for surfaces that stay
   light, a contrast pass for text under 4.5:1, both re-run in light so that
   what fails in *both* themes is not counted as a dark regression. */

:root[data-theme="dark"] .pj-app .card,
:root[data-theme="dark"] .pj-app .card-header,
:root[data-theme="dark"] .pj-app .card-footer {
    background-color: var(--pj-card);
    border-color: var(--pj-line);
}

/* Bootstrap's muted grey is `#6c757d` whatever the theme: 3.5:1 on the dark
   card, and it is the colour of every hint, count and secondary line. */
:root[data-theme="dark"] .pj-app .text-muted {
    color: var(--pj-muted) !important;
}


:root[data-theme="dark"] .pj-app .btn-outline-secondary {
    color: var(--pj-muted);
    border-color: var(--pj-line);
}

/* The primary button is white on brand orange: 3.4:1 in light, which is a
   brand decision this wave does not touch. In dark it had drifted to 2.8:1,
   and that part *is* ours — `--pj-orange` is lifted to #ff6b3d so orange text
   stays legible on a dark page, which makes it a worse background for white
   text. The fill goes back to the brand orange, so the button reads the same
   in both themes. Getting it past 4.5:1 needs a darker brand orange or a
   non-white label, and that is a call for the designer.

   The fill is restored by redefining the two tokens *on the button*, not by
   another `background-color`: prosyjob.css paints it
   `background-color: var(--pj-orange) !important`, which no ordinary
   declaration can outrank, but a custom property redefined on the element
   changes what that `var()` resolves to. */
:root[data-theme="dark"] .pj-app .btn-primary {
    --pj-orange: #EF5A28;
    --pj-orange-dark: #D8481A;
}

/* The template's own button pair: `--outline` hardcodes a white fill, and
   `.pj-eyebrow` / `.pj-shortcut__ico` a pale grey one. */
:root[data-theme="dark"] .pj-app .pj-btn--outline {
    background: var(--pj-card-2);
    color: var(--pj-ink);
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .pj-app .pj-eyebrow,
:root[data-theme="dark"] .pj-app .pj-shortcut__ico {
    background: var(--pj-card-2);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .pj-app .pj-shortcut:hover {
    background: var(--pj-card-2);
}

/* Navy is the light theme's structural colour and is lifted only a little in
   dark, which is enough behind a border but not enough for a line of text. */
:root[data-theme="dark"] .pj-app .pj-auth-tagline {
    color: var(--pj-muted);
}

/* A *category* chip painted with the "hired" green tint, which no longer works
   once its label lifts. On dark it becomes the neutral raised chip the rest of
   the app uses. */
:root[data-theme="dark"] .pj-app .cms-card__cat {
    background: var(--pj-card-2);
}

/* The commercial hero's floating "glass" card is a 94%-opaque white pane, so
   its text — which inherits the page colour — came out at 1:1 in dark. Same
   pane, same translucency, over the dark surface. */
:root[data-theme="dark"] .pj-app .commercial-hero-card__mini-card {
    background: color-mix(in srgb, var(--pj-card) 94%, transparent);
}

/* The shared Pagination component renders the template's `.paginations` list,
   whose links are `#161c2d` — near-black, and Bootstrap's `#6c757d` where the
   theme does not reach. 3.9:1 on the dark page, on a control that appears on
   22 screens. */
:root[data-theme="dark"] .pj-app .paginations li a {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .pj-app .paginations li a:hover {
    color: var(--pj-orange);
}


/* ── 5. Dark theme — the admin console ─────────────────────────────────
   The user surfaces get dark almost for free: prosyjob.css is tokenised, so
   the block above repaints it. The Kofejob admin theme is not — it writes
   #fff, #FFF8F7, #131523 and #333 literally — and it is the one place where
   "no dark theme" is worse than it sounds: the console ships the theme toggle
   in its header, and Bootstrap 5.3's `[data-bs-theme="dark"]` obeys it and
   lightens the *text* while the vendor sheet keeps the card backgrounds
   white. Measured before this block: `.card` text came out #dee2e6 on #fff
   (1.3:1) and the active breadcrumb 1.3:1. A working-looking button that
   makes the console unreadable.

   Wave 10 (E7-15) was scheduled to do this. Doing it in the admin wave
   instead, for the same reason Wave 1 took the auth screens: a surface that
   ships broken in one theme is not converted, it is half-converted.

   THE LIST IS MEASURED, NOT TRANSCRIBED. The vendor sheet has 61 rules that
   paint something white; the console only ever shows the handful below. It
   was collected by walking all 27 screens with a probe that reports every
   element whose computed background is lighter than 0.55 luminance, plus a
   contrast pass that reports every text node under 4.5:1 — then re-run in
   light so that what fails in *both* themes is not counted as a dark-mode
   regression. Two known survivors, both pre-existing and both unchanged by
   the theme: white on the brand orange `.btn-primary` is 3.4:1 (a brand
   decision, and it is the same in light), and the pale status chips
   (`.mt-badge`, `.pc-state`, `.sa-state`, `.wd-badge`) stay light on purpose —
   they are signal tints chosen in E7-04 to hold their meaning in both
   themes, and their own text colour keeps them legible. */

:root[data-theme="dark"] body {
    background-color: var(--pj-bg);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .header,
:root[data-theme="dark"] .layout-admin .header .header-left {
    background: var(--pj-card);
    border-bottom-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .sidebar {
    background-color: var(--pj-card);
    border-right: 1px solid var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .sidebar-menu li a {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .sidebar-menu > ul > li > a:hover {
    background-color: var(--pj-card-2);
}

/* The vendor paints the active entry white — the light theme's "raised" cue,
   which on a dark sidebar reads as a hole. Raised means lighter *relative to
   the surface*, so it becomes card-2. */
:root[data-theme="dark"] .layout-admin .sidebar-menu li.active > a {
    background: var(--pj-card-2);
    color: var(--pj-orange);
}

/* The seven section headers, and the active entry, are the vendor's #EF5A28.
   On the dark sidebar that clears 4.4:1 — just under the floor for 14px
   semibold — so they take the lifted dark-theme orange like everything else. */
:root[data-theme="dark"] .layout-admin .menu-title,
:root[data-theme="dark"] .layout-admin .sidebar-menu li.menu-title a {
    color: var(--pj-orange);
}

:root[data-theme="dark"] .layout-admin .card,
:root[data-theme="dark"] .layout-admin .card .card-header,
:root[data-theme="dark"] .layout-admin .card-footer {
    background: var(--pj-card);
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin h1,
:root[data-theme="dark"] .layout-admin h2,
:root[data-theme="dark"] .layout-admin h3,
:root[data-theme="dark"] .layout-admin h4,
:root[data-theme="dark"] .layout-admin h5,
:root[data-theme="dark"] .layout-admin h6,
:root[data-theme="dark"] .layout-admin .page-title,
:root[data-theme="dark"] .layout-admin .card .card-header .card-title,
:root[data-theme="dark"] .layout-admin .breadcrumb a {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .text-muted,
:root[data-theme="dark"] .layout-admin .page-header .breadcrumb,
:root[data-theme="dark"] .layout-admin .breadcrumb-item.active,
:root[data-theme="dark"] .layout-admin .dropdown-header {
    color: var(--pj-muted) !important;
}

:root[data-theme="dark"] .layout-admin .table {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .table thead th,
:root[data-theme="dark"] .layout-admin .table tbody tr,
:root[data-theme="dark"] .layout-admin .table td,
:root[data-theme="dark"] .layout-admin .table th {
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .table-hover tbody tr:hover {
    background-color: var(--pj-card-2);
}

:root[data-theme="dark"] .layout-admin .table-hover tbody tr:hover td {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .table-striped > tbody > tr:nth-of-type(2n+1),
:root[data-theme="dark"] .layout-admin .table-striped tbody tr:nth-of-type(odd) {
    background-color: var(--pj-card-2);
}

:root[data-theme="dark"] .layout-admin .table-striped tbody tr:nth-of-type(even) {
    background-color: transparent;
}

:root[data-theme="dark"] .layout-admin .form-control,
:root[data-theme="dark"] .layout-admin .form-select {
    background-color: var(--pj-card-2);
    border-color: var(--pj-line);
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .form-control::placeholder {
    color: var(--pj-muted);
}

:root[data-theme="dark"] .layout-admin .modal-content {
    background: var(--pj-card);
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .modal-header,
:root[data-theme="dark"] .layout-admin .modal-footer {
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .dropdown-menu {
    background-color: var(--pj-card);
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .dropdown-item {
    color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .dropdown-item:hover,
:root[data-theme="dark"] .layout-admin .dropdown-item:focus {
    background-color: var(--pj-card-2);
    color: var(--pj-ink);
}

/* Bootstrap's outline-secondary keeps its light-theme #6c757d whatever the
   theme is; that is 3.5:1 on the dark card. */
:root[data-theme="dark"] .layout-admin .btn-outline-secondary {
    color: var(--pj-muted);
    border-color: var(--pj-line);
}

/* The console's referential tabs are the one admin control painted with
   `--color-primary` as a solid fill under a white label, so lifting that token
   for text (E7-15) cost them: 3.6:1 became 3.3:1. Same split as the user side,
   applied here rather than in the component sheet so the light theme keeps the
   brighter blue it has always had on this screen. */
:root[data-theme="dark"] .layout-admin .rf-tab.is-active {
    background: var(--primary-fill);
    border-color: var(--primary-fill);
}

/* The "+" in a page header is a white square with a near-black border — the
   inversion of what it should be on a dark page. Its hover swaps the two, so
   the swap is mirrored too. */
:root[data-theme="dark"] .layout-admin .add-button {
    background: var(--pj-card-2);
    color: var(--pj-ink);
    border-color: var(--pj-line);
}

:root[data-theme="dark"] .layout-admin .add-button:hover {
    background: var(--pj-ink);
    color: var(--pj-bg);
    border-color: var(--pj-ink);
}

:root[data-theme="dark"] .layout-admin .user-menu > li > a,
:root[data-theme="dark"] .layout-admin .user-menu .nav-link {
    color: var(--pj-ink);
}

/* The one status token used as a *surface* rather than as a chip: the active
   row of the matching weight history is a pale green card carrying inherited
   body text, so dark mode lit the text and left the card pale — 1.2:1, the
   single dark-only regression the sweep found across the 27 screens. The
   signal moves to the border, which is what the row already used to say
   "this is the version in force". The other `--*-soft` tokens stay as they
   are: they are chip backgrounds paired with their own saturated text. */
:root[data-theme="dark"] .mt-history__item--active {
    background: var(--pj-card-2);
    border-color: var(--mt-very);
}

/* E8-02 — the pale chips, in dark.
   `.bg-*-light` is a 12% tint of its own hue carrying that hue as text, and the
   tint composites against whatever is behind it. In light that lands on a pale
   surface, so the text has to darken (done in the vendored sheet, via
   vendor-template.mjs). In dark the very same tint composites to a *dark*
   surface, where the original saturated hue already measures 5,1:1 to 7,0:1 —
   darkening it there took it the wrong way, down to 2,7:1.

   So the darkening is a light-theme correction only, and dark restores the
   original hue. Measured against the real composited surface, not against
   white: `.bg-danger-light` is the one whose original was itself short in dark
   (4,17:1) and is lightened instead. */
:root[data-theme="dark"] .bg-info-light    { color: #1db9aa !important; }
:root[data-theme="dark"] .bg-primary-light { color: #2196f3 !important; }
:root[data-theme="dark"] .bg-warning-light { color: #f39c12 !important; }
:root[data-theme="dark"] .bg-success-light { color: #26af48 !important; }
:root[data-theme="dark"] .bg-purple-light  { color: #c580ff !important; }
:root[data-theme="dark"] .bg-danger-light  { color: #e84e4e !important; }

/* ── Contextual menu in the sidebar (E9-06) ────────────────────────────
   prosyjob.css hides the whole right rail below 1200px, so the rubric's
   contextual menu was invisible in the off-canvas drawer AND in the
   two-column layout between 860 and 1200 — that is roughly 90% of this
   platform's audience, which arrives on a phone.

   The block is therefore rendered in both places and exactly one is shown:
   above 1200px the rail carries it and this copy stands down. The breakpoint
   is the rail's own, so the two can never both appear or both vanish. */
.pj-side__ctx .pj-shortcuts--menu {
    /* The sidebar column is 264px against the rail's 300px; the tiles need to
       give the label the difference rather than overflow it. */
    gap: 6px;
}

.pj-side__ctx .pj-shortcut {
    padding: 8px 10px;
}

@media (min-width: 1201px) {
    .pj-side__ctx {
        display: none;
    }
}

/* ── Tap targets, the rest of them (E10-01) ────────────────────────────
   E7-13 raised the admin console to 44px and `.pj-app .btn` covers buttons,
   but an audit of 10 screens at 390px still found 53 interactive elements
   under the floor once WCAG 2.5.5's exemption (a link inside a block of text)
   is honoured. They fall into six families, so this is six rules rather than
   fifty-three patches.

   Scoped to `.pj-app` — the guest and connected shells, which is where the
   phone audience is — exactly like the `.btn` rule above. `.layout-admin` is a
   separate stack and had its own pass in E7-13. */

/* Form fields sat at 40px: the single largest family, and the one a user has
   to hit repeatedly rather than once. */
.pj-app .form-control,
.pj-app select.form-control,
.pj-app textarea.form-control {
    min-height: 44px;
}

/* The rich-text editor's own `.rte__editor` (prosyjob-components.css) also
   carries `.form-control` for its border/padding/focus styling, so the
   44px rule above — same specificity, later file — was quietly winning
   over its 70px and collapsing a six-button toolbar (bold/italic/underline/
   lists/link/video) onto a field that read as one text line (2026-09-05,
   measured at 44px on a live page before this fix). `.rte .rte__editor`
   matches the same two classes the element already carries, so it ties on
   specificity and wins on being the later rule — it exists to win, not to
   set its own number, which is why it echoes 70px rather than choosing it. */
.rte .rte__editor {
    min-height: 70px;
}

/* Round icon buttons in the top bar — the theme toggle is the visible one. */
.pj-app .pj-iconbtn {
    min-width: 44px;
    min-height: 44px;
}

/* The brand mark is the "go home" link on every screen, at 30px. */
.pj-app .pj-topbar__brand {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
}

/* Footer links measured 16px. They are navigation — a `<nav>` since E14-04 —
   not prose in a paragraph, so 2.5.5 does not exempt them. */
.pj-app .pj-footer a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
}

/* The account menu, 28px per item since E8-01. */
.pj-app .dropdown-item {
    display: flex;
    align-items: center;
    min-height: 44px;
}

/* A checkbox is the one control that must NOT simply grow to 44px — a 44px
   box looks broken. The box grows a little and its row carries the rest of
   the target; the label is what the finger actually lands on. Pseudo-elements
   are not an option here: `input` is a replaced element and does not render
   ::before/::after reliably. */
.pj-app .form-check-input {
    width: 22px;
    height: 22px;
}

.pj-app .form-check,
.pj-app .custom_check {
    display: flex;
    align-items: center;
    min-height: 44px;
}

/* The search page's filter accordion toggle measured 19px — it is the control
   that opens every filter on the job and training searches, on the screen size
   where filters matter most. */
.pj-app .filter-title > a {
    display: flex;
    align-items: center;
    min-height: 44px;
}

/* The minted Miles QR (E16-04). Capped because a code the width of a desktop
   card is harder to photograph, not easier, and it is scanned from a phone.

   The cap needs BOTH declarations below, and the first one alone never worked:
   it was written for a theme rule assumed to be `<container> img` at 0,1,1 and
   the real one is `.pj-app .pj-main img, .pj-app .pj-maincol img` at **0,2,1**,
   which beats a bare `.cm-hold__qr` (0,1,0) whatever the sheet order — order
   only settles a tie. So the QR rendered at the full card width, 637px on this
   viewport, for as long as the feature has existed, with a comment above it
   explaining why it could not. Measure the element; a rule that is present and
   matching can still be losing.

   The second selector mirrors the offender's shape to clear it at 0,3,0. The
   bare class stays for anywhere the QR is drawn outside the main column. */
.cm-hold__qr {
  display: block;
  width: 100%;
  max-width: 220px;
  height: auto;
  margin: 4px 0;
}

.pj-app .pj-main .cm-hold__qr,
.pj-app .pj-maincol .cm-hold__qr {
  max-width: 220px;
}

/* =====================================================================
   Confirmation par mot de passe (2026-08-08)
   =====================================================================
   The dialog in front of the ten actions that hand power to somebody else.
   It lives here, unscoped, and not in the component sheet, because it is
   raised from three shells: the admin console (`.layout-admin`, where
   `prosyjob.css` and every `--pj-*` token it declares are switched off), the
   connected user shell and — through a deep link — the guest one. A rule that
   must hold regardless of the vendor stack belongs in the last sheet loaded.
   Only tokens mirrored at `:root` in this same file are used below. */
.cpd-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1080; /* above the Bootstrap modals the member screens already open */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(9, 14, 20, 0.55);
}

.cpd-panel {
  width: 100%;
  max-width: 420px;
  max-height: 90vh;
  overflow-y: auto;
  padding: 22px;
  border-radius: 10px;
  background: var(--pj-card);
  color: var(--pj-ink);
  box-shadow: 0 18px 48px rgba(9, 14, 20, 0.32);
}

.cpd-title {
  margin: 0 0 8px;
  font-size: 1.15rem;
}

/* The subject of the action — a name, an email. Wrapped rather than truncated:
   confirming a transfer to the wrong person is the mistake this dialog exists
   to prevent, so the name must be readable in full. */
.cpd-detail {
  margin: 0 0 10px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.cpd-why {
  margin: 0 0 16px;
  font-size: 0.88rem;
  color: var(--pj-muted);
}

.cpd-panel .form-control {
  width: 100%;
  min-height: 44px; /* tap target floor, measured — see CLAUDE.md §9 */
}

/* No `--pj-danger` exists in the scale, and inventing one here would have to be
   mirrored in prosyjob.css or check-assets.mjs would fail — see CLAUDE.md §9 on
   why the scale is declared twice. The brand's accessible red-orange is already
   a token and reads as an error on both grounds. */
.cpd-error {
  margin: 8px 0 0;
  font-size: 0.85rem;
  color: var(--pj-orange-text);
}

.cpd-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 18px;
}

/* Every bare `<button>` in the connected shell is width:100% (see CLAUDE.md
   §9), so a row of two needs both `width: auto` and something to divide the
   space — otherwise they stack at any width. */
.cpd-actions .btn {
  flex: 1 1 140px;
  width: auto;
  min-height: 44px;
}

/* =====================================================================
   Console d'administration — accueil (`.adb-`)
   =====================================================================
   Ces regles vivent ici et non dans la feuille de composants parce que
   `.layout-admin` eteint `prosyjob.css` : sur cette pile, seule cette feuille
   est chargee, et c'est elle qui recopie l'echelle `--pj-*` (voir la note du
   §9 du contrat). Une regle de la console ecrite ailleurs n'existerait pas.

   Mobile d'abord : ce qui suit est le dessin a 360 px, les media queries
   enrichissent. */
.adb-stack {
  display: grid;
  gap: 20px;
  margin-top: 4px;
}

.adb-card {
  margin: 0;
}

.adb-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 14px;
}

.adb-head__title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
}

.adb-head__total {
  color: var(--pj-muted);
  font-weight: 600;
}

.adb-period {
  width: auto;
  min-height: 44px;
}

.adb-chip {
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--pj-card-2);
  color: var(--pj-muted);
  font-size: 0.8rem;
  font-weight: 600;
}

/* ── La file d'attente ─────────────────────────────────────────────────── */

.adb-item {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  align-items: center;
  gap: 4px 10px;
  padding: 12px 0;
  border-bottom: 1px solid var(--pj-line);
}

.adb-item:last-of-type {
  border-bottom: 0;
}

.adb-item__icon {
  color: var(--pj-muted);
  text-align: center;
}

.adb-item__title {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.adb-item__meta {
  color: var(--pj-muted);
  font-size: 0.85rem;
}

.adb-item__text,
.adb-badge,
.adb-open {
  grid-column: 2;
}

.adb-badge {
  justify-self: start;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--pj-card-2);
  color: var(--pj-muted);
  font-size: 0.78rem;
  font-weight: 600;
}

/* L'urgence porte un mot ET une couleur : la couleur seule ne se lit pas en
   daltonisme ni en impression. */
.adb-badge--urgent {
  background: var(--pj-orange-soft);
  color: var(--pj-orange-text);
}

.adb-open {
  display: inline-flex;
  align-items: center;
  justify-self: start;
  min-height: 44px;
  color: var(--pj-orange-text);
  font-weight: 600;
}

.adb-rest {
  margin-top: 12px;
}

/* ── Les deux courbes ──────────────────────────────────────────────────── */

.adb-charts,
.adb-lists {
  display: grid;
  gap: 20px;
}

/* Une seule serie par cadre, donc une seule couleur et aucune legende a lire :
   le titre nomme la serie. Trait de 2 px, aire tres pale dessous. */
.adb-chart {
  display: block;
  width: 100%;
  height: 160px;
  overflow: visible;
}

.adb-chart__line {
  fill: none;
  stroke: var(--pj-orange-fill);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

.adb-chart__area {
  fill: var(--pj-orange-fill);
  opacity: 0.12;
  stroke: none;
}

/* Les reperes sont du HTML sous le cadre : dans le SVG ils seraient etires par
   `preserveAspectRatio="none"` avec le trace. */
.adb-chart__axis {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  color: var(--pj-muted);
  font-size: 0.78rem;
}

/* ── Les quatre listes ─────────────────────────────────────────────────── */

/* Un tableau large defile dans sa propre boite plutot que d'elargir la page. */
.adb-table {
  overflow-x: auto;
}

.adb-table table {
  width: 100%;
  border-collapse: collapse;
}

.adb-table th,
.adb-table td {
  padding: 9px 10px 9px 0;
  border-bottom: 1px solid var(--pj-line);
  text-align: left;
  white-space: nowrap;
}

.adb-table th {
  color: var(--pj-muted);
  font-size: 0.78rem;
  font-weight: 600;
}

.adb-table tbody tr:last-child td {
  border-bottom: 0;
}

.adb-table__date {
  color: var(--pj-muted);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 768px) {
  .adb-item {
    grid-template-columns: 28px minmax(0, 1fr) auto auto;
    gap: 4px 14px;
  }

  .adb-item__text,
  .adb-badge,
  .adb-open {
    grid-column: auto;
  }
}

@media (min-width: 1200px) {
  .adb-charts,
  .adb-lists {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* =====================================================================
   Console des assignations (`.assign-`)
   ===================================================================== */

/* Trois boutons par ligne — historique, modifier, revoquer. Sans cela ils
   s'empilent en pleine largeur des que la colonne se resserre. */
.assign-actions {
  white-space: nowrap;
}

.assign-actions .btn {
  width: auto;
  margin-left: 4px;
}

/* Le fil d'une assignation : une entree par acte, la plus recente en haut. */
.assign-history {
  margin: 0;
  padding: 0;
  list-style: none;
}

.assign-history li {
  padding: 10px 0;
  border-bottom: 1px solid var(--pj-line);
}

.assign-history li:last-child {
  border-bottom: 0;
}

/* =====================================================================
   Revue des comptes en double (`.dup-`)
   =====================================================================
   Dans `prosyjob-app.css` pour la meme raison que `.alv-` et `.adb-` :
   `.layout-admin` eteint `prosyjob.css`, donc c'est la seule feuille chargee
   sur cette pile. */

/* La barre de selection. Collante en haut : sur l'onglet « Sans identifiant »
   une page porte vingt-cinq lignes, et cocher en bas pour remonter chercher le
   bouton est exactement le geste qu'on fait de travers. Elle n'existe que
   lorsqu'il y a une selection — l'attribut `hidden` la retire, la regle
   `[hidden]{display:none!important}` de cette feuille s'en chargeant. */
.dup-selection {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
  padding: 10px 14px;
  border: 1px solid var(--pj-line);
  border-radius: var(--pj-radius-sm);
  background: var(--pj-card);
}

.dup-selection__count {
  flex: 1 1 auto;
  margin: 0;
  font-weight: 600;
}

/* `.layout-admin` neutralise deja le `button { width: 100% }`, mais compter
   dessus est un pari sur une feuille du vendeur. */
.dup-selection .btn {
  width: auto;
  min-height: 38px;
}

/* La case vit dans la cellule du nom : une case a cocher seule en tete de
   ligne ne dit pas ce qu'elle coche, et une colonne de plus faisait defiler la
   boite. Le `padding-left` de la variante sans case aligne le nom sur ceux qui
   en portent une, pour que la colonne reste une colonne. */
.dup-name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  cursor: pointer;
}

.dup-name input[type="checkbox"] {
  width: 18px;
  height: 18px;
  margin: 0;
  flex: 0 0 auto;
  cursor: pointer;
}

.dup-name--fixed {
  padding-left: 26px;
  cursor: default;
}

/* Neuf colonnes — nom, login d'origine, les deux identifiants, pays, statut,
   les deux dates, actions — ne tiennent pas dans la boite ; six n'y tenaient
   deja pas (1124px pour 1001). La derniere se collait donc
   au-dela du bord et « Rendre principal » arrivait coupe, ce qui se lit comme
   un defaut d'affichage plutot que comme un tableau a faire defiler. Elle reste
   au bord droit ; le reste glisse dessous. Meme correctif que `.alv-col-actions`
   sur la liste des comptes, et pour la meme raison : l'action est le point de
   l'ecran, elle ne se cherche pas. Reservee au grand format — sous 768px le
   `.table-responsive` du theme defile deja et coller n'aurait plus de sens. */
@media (min-width: 768px) {
  .dup-col-actions {
    position: sticky;
    right: 0;
    z-index: 1;
    background: var(--pj-card);
    box-shadow: -10px 0 10px -10px rgba(0, 0, 0, 0.28);
  }
}

/* =====================================================================
   Listes d'administration (`.alv-`)
   =====================================================================
   Le gabarit unique des quatre ecrans que le tableau de bord ouvre. Dans
   `prosyjob-app.css` pour la meme raison que `.adb-` : `.layout-admin` eteint
   `prosyjob.css`, donc c'est la seule feuille chargee sur cette pile. */
.alv-count {
  color: var(--pj-muted);
  font-weight: 600;
}

.alv-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin-bottom: 16px;
}

.alv-field {
  display: grid;
  flex: 1 1 180px;
  gap: 4px;
  margin: 0;
}

.alv-field--search {
  flex: 2 1 240px;
}

.alv-field > span {
  color: var(--pj-muted);
  font-size: 0.8rem;
  font-weight: 600;
}

.alv-field .form-control {
  min-height: 44px;
}

/* `.layout-admin` neutralise le `button { width: 100% }`, mais on le declare
   quand meme : la regle est la, et compter dessus est un pari sur une feuille
   du vendeur. */
.alv-submit {
  width: auto;
  min-height: 44px;
}

.alv-badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--pj-card-2);
  color: var(--pj-muted);
  font-size: 0.78rem;
  font-weight: 600;
}

.alv-badge--ok {
  background: var(--pj-card-2);
  color: var(--color-success, #1e8e5a);
}

.alv-badge--warn {
  background: var(--pj-orange-soft);
  color: var(--pj-orange-text);
}

/* Deux canaux de verification cote a cote : la puce porte le canal, le signe
   porte l'etat. Elles se suivent sur une ligne et retombent l'une sous l'autre
   dans la carte mobile plutot que de deborder. */
.alv-badge--chan {
  margin-right: 6px;
  white-space: nowrap;
}

/* Les actions d'une ligne. `.layout-admin` neutralise deja le
   `button { width: 100% }`, mais une rangee de boutons le declare quand meme :
   compter sur une feuille du vendeur est un pari. */
.alv-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.alv-actions .btn {
  width: auto;
}

/* La vignette d'une publication (E33) : toujours visible dans la liste plutot
   qu'un clic a part, pour qu'un contenu problematique porte par l'image (et
   non le titre) ne puisse pas etre saute. Le clic n'agrandit — meme motif
   qu'une case tactile 44px minimum ici trop petite pour vraiment juger une
   image, jamais une deuxieme facon d'agir dessus. */
/* Specificite `.alv-table .alv-thumb` deliberee : `.layout-admin button` (une
   classe + un element) impose deja `width: auto`, et une simple `.alv-thumb`
   (une classe) perd face a elle quel que soit l'ordre des feuilles — la
   vignette prenait alors la largeur de l'image au lieu de 48px fixes. */
.alv-table .alv-thumb {
  display: block;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  border-radius: var(--pj-radius-sm);
  overflow: hidden;
  cursor: zoom-in;
  flex: 0 0 auto;
}

.alv-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.alv-thumb--empty {
  background: var(--pj-card-2);
}

/* Une colonne qui reste au bord droit de la boite qui defile. Une liste a dix
   colonnes tient dans 1740px pour 1000 visibles : sans cela, le bouton d'action
   est hors champ sur chaque ligne et il faut defiler pour chaque compte.
   Reservee au grand format : sous 768px la table devient des cartes, ou tout
   est deja visible et ou coller n'aurait plus de sens. La teinte est celle de
   la carte, sinon les colonnes glissent visiblement dessous. */
@media (min-width: 768px) {
  .alv-table .alv-col-actions {
    position: sticky;
    right: 0;
    z-index: 1;
    background: var(--pj-card);
    box-shadow: -10px 0 10px -10px rgba(0, 0, 0, 0.28);
  }
}

/* Une table large defile dans sa propre boite plutot que d'elargir la page. */
.alv-table {
  overflow-x: auto;
}

/* ── Sous 768 px : la table devient des cartes ─────────────────────────────
   Le contrat l'impose (§9) et une liste d'administration est exactement
   l'ecran ou on l'oublie. Un seul rendu, retourne en CSS : chaque cellule
   porte son intitule en `data-label`, pose ici a gauche de la valeur. */
@media (max-width: 767.98px) {
  .alv-table table,
  .alv-table tbody,
  .alv-table tr,
  .alv-table td {
    display: block;
    width: 100%;
  }

  .alv-table thead {
    /* Pas `display: none` : l'en-tete disparait a l'oeil mais reste lu par une
       synthese vocale, qui en a toujours besoin pour nommer les colonnes. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .alv-table tr {
    margin-bottom: 12px;
    padding: 6px 12px;
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius-sm);
  }

  .alv-table td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 7px 0;
    border: 0;
    border-bottom: 1px solid var(--pj-line);
    text-align: right;
    /* Les deux lignes qui font tenir la carte dans 360px.

       Le theme pose `white-space: nowrap` sur les cellules du tableau, ce qui
       est juste tant qu'on est un tableau : une colonne s'elargit. Repliee en
       carte, la cellule fait toute la largeur, et une valeur qui refuse de se
       couper elargit alors la boite `.alv-table` — un quartier saisi en texte
       libre (« Route de Foumbot, face grand Bazar… », l'heritage l'a garde
       tel quel) reclamait 382px contre 269 disponibles, et lire la ligne
       demandait un geste lateral sur l'ecran ou arrivent ~90% du public.

       `normal` seul ne suffit pas : un email n'a aucune espace ou se couper,
       et reste un seul mot. `anywhere` le coupe — et il agit bien sur la
       taille min-content, la ou `break-word` ne l'aurait pas fait, ce qui est
       exactement ce dont un item flex a besoin pour accepter de retrecir. */
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .alv-table tr td:last-child {
    border-bottom: 0;
  }

  /* La ou le doigt agit : 44px, mesures plutot que supposees. Inutile de
     l'imposer au clavier et a la souris, ou la rangee serait deux fois trop
     haute pour rien. */
  .alv-actions .btn {
    min-height: 44px;
  }

  /* Comme toute autre valeur de la carte : contre le bord droit, en face de
     son intitule. */
  .alv-actions {
    justify-content: flex-end;
  }

  .alv-table td::before {
    content: attr(data-label);
    /* L'intitule ne se replie pas, la valeur si. Le `white-space: normal` pose
       sur la cellule s'herite ici aussi, et « Email » revenait coupe en
       « Em / ail » : c'est un libelle fixe, court, dont la largeur est connue
       — c'est a la valeur de ceder la place, pas a lui. */
    flex: none;
    white-space: nowrap;
    color: var(--pj-muted);
    font-size: 0.8rem;
    font-weight: 600;
    text-align: left;
  }
}

/* =====================================================================
   Lanceur de rubriques (`.pj-search__*`)
   =====================================================================
   Le champ de la barre du haut n'etait qu'un decor ; il propose desormais les
   rubriques que la barre laterale montre. Les regles vivent ici parce que la
   barre du haut existe sur les trois coquilles, et que `prosyjob-app.css` est
   la seule feuille que les trois chargent. */
.pj-search {
  position: relative;
}

.pj-search__results {
  position: absolute;
  z-index: 1075;
  top: calc(100% + 6px);
  left: 0;
  width: 100%;
  min-width: 260px;
  padding: 6px;
  border: 1px solid var(--pj-line);
  border-radius: var(--pj-radius-sm);
  background: var(--pj-card);
  box-shadow: var(--pj-shadow);
}

/* Le fond de fermeture : un element reel plutot qu'un ecouteur sur `document`,
   qui survivrait a la barre du haut (§9) et s'accumulerait a chaque rendu. */
.pj-search__backdrop {
  position: fixed;
  z-index: 1074;
  inset: 0;
}

.pj-search__row {
  display: flex;
  width: 100%;
  min-height: 44px;
  align-items: center;
  gap: 10px;
  padding: 0 10px;
  border: 0;
  border-radius: var(--pj-radius-sm);
  background: none;
  color: var(--pj-ink);
  text-align: left;
  cursor: pointer;
}

/* Le survol et la selection au clavier partagent le meme etat visible : sans
   cela, flecher dans la liste ne se voit pas. */
.pj-search__row:hover,
.pj-search__row.is-active {
  background: var(--pj-card-2);
}

.pj-search__row i {
  width: 18px;
  color: var(--pj-muted);
  text-align: center;
}

.pj-search__empty,
.pj-search__more {
  margin: 0;
  padding: 10px;
  color: var(--pj-muted);
  font-size: 0.85rem;
}

/* La derniere ligne du lanceur : chercher le terme dans les contenus. Separee
   des rubriques par un filet, parce que c'est un geste d'une autre nature —
   on quitte la navigation pour interroger la base. */
.pj-search__row--global {
  margin-top: 4px;
  border-top: 1px solid var(--pj-line);
  border-radius: 0;
  color: var(--pj-orange-text);
  font-weight: 600;
}


/* ── Un seul survol pour les trois menus (2026-08-12) ──────────────────
   Trois listes de navigation, trois survols écrits par trois mains :

     .pj-nav a        (sidebar)          fond pâle + icône orange
     .pj-shortcut     (menu contextuel)  fond pâle, et rien d'autre
     .dropdown-item   (menu avatar)      le défaut Bootstrap, et rien d'autre

   Mesuré : le fond du thème (#F5F7FB) contre le blanc de la carte est un
   écart de 2 % — invisible seul. Ce qui rend le survol de la sidebar
   *lisible*, c'est l'accent orange, et lui n'existait que là. Le menu
   contextuel était même en dessous de rien du tout : son carré d'icône est
   peint plus SOMBRE (#EEF1F6) que le fond de survol, donc la tuile se
   dissolvait dans sa propre ligne au lieu d'en ressortir.

   L'état est donc écrit une fois pour les trois : fond `--pj-card-2`,
   libellé et icône `--pj-orange-text`. La variante *texte* de la marque et
   non `--pj-orange`, qui mesure 3,03:1 sur ce fond — sous le plancher AA
   pour un libellé, et à 0,03 du plancher 1.4.11 pour un glyphe, ce qui est
   une conformité qu'un futur réglage de `--pj-card-2` casse sans prévenir.
   `--pj-orange-text` donne 4,69:1 en clair et 6,19:1 en sombre, mesurés.
   C'est la sidebar qui était fautive là (elle héritait de `.pj-app a:hover`,
   d'où le `--pj-orange`), pas les deux copies : on unifie vers le jeton qui
   passe plutôt que de propager le défaut. Et `--pj-orange-fill`, le troisième
   candidat, n'est PAS redéfini par le bloc sombre — c'est un fond sous du
   blanc, pas une encre : il tombe à 2,85:1 en sombre.

   Rien n'est scopé à un écran : c'est la même question posée trois fois. */

/* Le fond. `--pj-card-2` en clair comme en sombre, ce qui rend inutile de
   savoir quel thème est actif — et les deux surcharges sombres qui
   existaient déjà pour .pj-nav et .pj-shortcut disent la même valeur. */
.pj-app .pj-nav a:hover,
.pj-app .pj-shortcut:hover:not(.is-active) {
    background: var(--pj-card-2);
}

/* Le libellé. Sur la tuile du menu contextuel c'est `__t` qui porte la
   couleur, pas l'ancre — `.pj-shortcut__t` déclare la sienne. */
.pj-app .pj-nav a:hover,
.pj-app .pj-shortcut:hover:not(.is-active) .pj-shortcut__t {
    color: var(--pj-orange-text);
}

/* L'icône, du même jeton que le libellé : quand les deux portent l'accent,
   les voir dans deux oranges différents se remarque. */
.pj-app .pj-nav a:hover i,
.pj-app .pj-nav a:hover svg,
.pj-app .pj-shortcut:hover:not(.is-active) .pj-shortcut__ico,
.pj-app .dropdown-item:hover > i,
.pj-app .dropdown-item:focus > i {
    color: var(--pj-orange-text);
}

/* Le carré d'icône du menu contextuel : il doit s'éclairer, pas se fondre.
   Teinte douce plutôt que le plein orange, que `.is-active` occupe déjà —
   sans quoi survoler ressemblerait à sélectionner. */
.pj-app .pj-shortcut:hover:not(.is-active) .pj-shortcut__ico {
    background: var(--pj-orange-soft);
}

/* Les transitions manquantes : le thème n'anime que `background` sur la
   tuile, et rien du tout sur une entrée de menu. */
.pj-app .pj-shortcut,
.pj-app .pj-shortcut__ico,
.pj-app .dropdown-item,
.pj-app .dropdown-item > i {
    transition: background-color .18s, color .18s;
}

/* ── Le menu avatar, et la langue (même composant) ─────────────────────
   Ici on ne se bat pas sur la spécificité : Bootstrap lit ses propres
   jetons pour peindre `.dropdown-item:hover`, alors on les redéfinit sur le
   conteneur — le même geste que `.btn-primary` plus haut. Une règle
   `:root[data-theme=dark] .pj-app .dropdown-item { color }` aurait pesé
   (0,4,0) et battu le `:hover` en (0,3,1) : l'accent aurait disparu en
   sombre exactement là où il manquait déjà.

   Le fond, lui, doit rester une déclaration : `user-template/css/style.css`
   force `.dropdown-menu { background-color: #fff }`, ce qui court-circuite
   `--bs-dropdown-bg` et laissait, en thème sombre, une boîte BLANCHE avec
   des libellés `#dee2e6` — 1,3:1 mesuré, c'est-à-dire illisible — et un
   survol `#2b3035`, bandeau noir à l'envers du reste du menu. */
.pj-app .dropdown-menu {
    --bs-dropdown-bg: var(--pj-card);
    --bs-dropdown-border-color: var(--pj-line);
    --bs-dropdown-link-color: var(--pj-ink);
    --bs-dropdown-link-hover-color: var(--pj-orange-text);
    --bs-dropdown-link-hover-bg: var(--pj-card-2);
    --bs-dropdown-link-active-color: var(--pj-orange-text);
    --bs-dropdown-link-active-bg: var(--pj-orange-soft);
    --bs-dropdown-header-color: var(--pj-muted);
    --bs-dropdown-divider-bg: var(--pj-line);
    background-color: var(--pj-card);
    border-color: var(--pj-line);
    border-radius: var(--pj-radius-sm);
    /* La pastille de survol a besoin d'une marge pour ne pas toucher les
       bords : sans elle, un coin arrondi sur une entrée pleine largeur ne
       se voit pas. */
    padding: 8px;
}

.pj-app .dropdown-item {
    border-radius: var(--pj-radius-sm);
}

.pj-app .dropdown-header {
    padding-left: 8px;
    padding-right: 8px;
}

/* ------------------------------------------------------------------ *
 * M22 « Vos annonces » — les quatre emplacements
 *
 * Le catalogue vend quatre placements. Jusqu'ici un seul était dessiné :
 * une campagne achetée sur les trois autres était payée, modérée, mise
 * en ligne par le moteur — et peinte nulle part. Ces règles sont l'autre
 * moitié de la vente.
 *
 * Un emplacement sans annonce reste `hidden`, et non « vide » : une bande
 * vide occupe quand même sa ligne et repousse le pied de page sur toutes
 * les pages du site, c'est-à-dire presque toujours.
 * ------------------------------------------------------------------ */

/* Le lien lui-même, commun aux quatre emplacements (AdSlotComponent). */
.pj-ad__link {
    display: block;
    position: relative;
    color: inherit;
    text-decoration: none;
}

/* L'image porte le ratio de son emplacement et ne doit jamais déborder de
   sa colonne : le rail est étroit sur mobile, et une bannière fournie en
   600px de large y ferait défiler toute la page. */
.pj-ad__link img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--pj-radius-sm);
}

/* ── L'annonce texte ──────────────────────────────────────────────────
   La forme demandée par le propriétaire du produit (annonce_texte.png) :
   une tuile d'icône à gauche, un titre en gras, un paragraphe gris.
   L'icône est fixe et décorative — une campagne texte ne téléverse rien —
   d'où `aria-hidden` côté markup : le lecteur d'écran reçoit le titre et
   le texte, pas un porte-voix. */
.pj-ad__link--text {
    align-items: flex-start;
    display: flex;
    gap: 14px;
}

.pj-ad__icon {
    align-items: center;
    background: var(--pj-orange-soft);
    border-radius: var(--pj-radius-sm);
    color: var(--pj-orange-text);
    display: flex;
    /* Ni rétrécie ni étirée : c'est un carré, quelle que soit la longueur
       du texte à côté. */
    flex: 0 0 64px;
    font-size: 24px;
    height: 64px;
    justify-content: center;
}

.pj-ad__copy {
    display: block;
    min-width: 0;
}

.pj-ad__copy strong,
.pj-ad__copy span {
    display: block;
}

.pj-ad__copy strong {
    color: var(--pj-ink);
    font-size: 15px;
    line-height: 1.3;
    margin-bottom: 4px;
}

.pj-ad__copy span {
    color: var(--pj-muted);
    font-size: 13px;
    line-height: 1.5;
}

/* Le passage d'une annonce à la suivante. Une apparition, pas un
   glissement : la carte ne bouge pas de place, seul son contenu change,
   ce qui évite de déplacer sous le doigt ce que quelqu'un s'apprêtait à
   toucher. */
@keyframes pj-ad-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

.pj-ad--inline .pj-ad__link {
    animation: pj-ad-in 320ms ease-out;
}

/* Qui demande moins de mouvement n'en reçoit aucun. La rotation, elle,
   continue : la supprimer priverait purement et simplement d'audience le
   deuxième annonceur de la liste. */
@media (prefers-reduced-motion: reduce) {
    .pj-ad--inline .pj-ad__link {
        animation: none;
    }
}

/* Sous 480px la tuile et le texte se marchent dessus — 64px de carré plus
   14 de gouttière sur une colonne de 297, il ne reste pas de quoi lire
   trois mots par ligne. Mesuré, pas supposé. */
@media (max-width: 480px) {
    .pj-ad__icon {
        flex-basis: 48px;
        font-size: 20px;
        height: 48px;
    }
}

/* Le rail (300x250) garde le cadre du thème : une annonce doit ressembler
   à l'emplacement, pas le remplacer. Seul le contenu change.
 *
 * Et il garde surtout ses **couleurs**. Le bloc `.pj-promo` du thème est
 * peint en bleu, alors que la carte texte est écrite pour un fond blanc :
 * `--pj-ink` sur ce bleu-là, c'est du bleu nuit sur bleu nuit. Mesuré à
 * l'écran — la tuile d'icône s'affichait, le titre et le texte non. Le
 * `inherit` rend la typographie au bloc porteur. */
.pj-promo--ad .pj-ad__copy strong,
.pj-promo--ad .pj-ad__copy span {
    color: inherit;
}

/* La tuile suit, pour la même raison : un aplat pêche sur le bleu du
   thème est une pièce rapportée. */
.pj-promo--ad .pj-ad__icon {
    /* Dérivée de la couleur du texte plutôt qu'écrite : pas de valeur
       brute (règle du projet), et la tuile suit le bloc quel que soit le
       thème, sans qu'un jeton de plus soit à miroiter dans les deux
       copies de l'échelle. */
    background: color-mix(in srgb, currentColor 16%, transparent);
    color: inherit;
}

/* Les hôtes que la coquille pose elle-même — la bande de pied de page et
   la verticale. Ils portent leur propre carte, n'ayant pas de bloc thème
   sous eux. */
.pj-ad--band,
.pj-ad--side {
    background: var(--pj-card);
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius);
    box-shadow: var(--pj-shadow-sm);
    display: grid;
    gap: 8px;
    padding: 12px;
}

/* La bande 728x90. Elle est un frère de `.pj-shell` dans `#app`, qui est
   un flex en colonne : sans `width: 100%` une marge auto sur l'axe
   transverse annule le `stretch` et la bande se dimensionne sur son
   propre contenu — le défaut que §2 corrige pour la coquille et le pied
   de page. Même cause, même remède. */
.pj-ad--band {
    width: 100%;
    max-width: var(--pj-max);
    margin: 0 auto 16px;
}

/* ── Tableau de bord : tuiles de profil et fil (recette 2026-08) ───────

   « Les pavés Entreprise, Candidat, Organisme de formation et Commercial
   peuvent rester, mais doivent devenir plus petits, sous forme d'icônes de
   50 x 50, afin de libérer de l'espace. »

   L'icône fait donc exactement 50×50 et la tuile tient sur une ligne. C'est
   la hauteur ainsi rendue qui accueille le fil en dessous : avant, deux
   sections titrées de grandes cartes occupaient à elles seules la hauteur
   d'un écran de téléphone.

   `auto-fit` plutôt qu'un nombre de colonnes fixe : quatre tuiles tiennent
   sur une ligne au large, deux à 768px et une à 360px, sans point de rupture
   à écrire. */
.dash-tiles {
    display: grid;
    /* 160px, et la valeur vient d'une mesure qui a corrigé une estimation.
       À 360px la piste centrale ne fait pas 345px mais **297** — la coquille
       prend le reste — donc deux colonnes n'y tiendraient qu'en descendant
       sous 143px, largeur à laquelle « Organisme de formation » se casse en
       quatre lignes. Une tuile par ligne à 360px est donc le bon résultat et
       non un échec : elle mesure 72px de haut contre ~200 pour l'ancienne
       carte, et le bloc entier 324px contre plus de 900.
       Ce que 160px change vraiment est le palier suivant : deux colonnes dès
       ~395px de fenêtre, au lieu de ~495px avec 210px. */
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 12px;
}

.dash-tile {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: var(--pj-card);
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius-sm);
    color: var(--pj-ink);
    text-decoration: none;
}

.dash-tile:hover {
    border-color: var(--pj-orange);
}

/* Les 50×50 demandés. `flex: none` parce qu'un flex item se laisse comprimer
   par défaut : sans lui l'icône rétrécit dès que le nom passe à la ligne, et
   la taille demandée ne serait vraie qu'à certaines largeurs. */
.dash-tile__ico {
    flex: none;
    width: 50px;
    height: 50px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 12px;
    background: var(--pj-card-2);
    font-size: 20px;
}

.dash-tile__body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.dash-tile__name {
    font-weight: 700;
    font-size: 14px;
}

.dash-tile__stats {
    font-size: 12px;
    color: var(--pj-muted);
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

/* Le profil non activé se lit à la teinte, pas à un intertitre : c'est ce qui
   permet aux quatre tuiles de tenir sur une seule ligne. */
.dash-tile--muted .dash-tile__ico {
    background: color-mix(in srgb, var(--pj-muted) 14%, transparent);
    color: var(--pj-muted);
}

.dash-tile--muted .dash-tile__name {
    font-weight: 600;
    color: var(--pj-muted);
}

/* ── « Quoi de neuf » (recette 2026-08) ────────────────────────────────
   Un champ, un compteur, un bouton. Volontairement pauvre : la recette
   demande « un court texte », pas un éditeur. */
.dash-post {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.dash-post__pick {
    align-self: flex-start;
    /* `width: auto` : le sélecteur d'entreprise n'a aucune raison de barrer
       la carte sur toute sa largeur, et `.field select` le ferait. */
    width: auto;
    max-width: 100%;
    min-height: 44px;
}

.dash-post__text {
    width: 100%;
    min-height: 66px;
    padding: 10px 12px;
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius-sm);
    background: var(--pj-card);
    color: var(--pj-ink);
    font: inherit;
    resize: vertical;
}

.dash-post__text::placeholder {
    color: var(--pj-muted);
}

.dash-post__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.dash-post__count {
    font-size: 12px;
    color: var(--pj-muted);
}

/* `width: auto` : dans la coquille connectée un bouton nu prendrait toute la
   largeur, et le pied du composeur deviendrait deux lignes empilées. */
.dash-post__send {
    width: auto;
    min-height: 44px;
    flex: none;
}

.dash-feed__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.dash-feed__title {
    margin: 0;
    font-size: 16px;
}

/* 44px de cible, comme partout : mesuré à 18,8px au premier jet, parce
   qu'un lien texte ne fait que la hauteur de sa ligne. C'est pourtant l'un
   des deux gestes que la recette nomme dans ce bloc (« puis un lien pour en
   voir davantage »). `align-items: baseline` sur l'en-tête aligne toujours le
   texte sur le titre ; seule la zone cliquable grandit. */
.dash-feed__more {
    flex: none;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 4px;
    font-size: 13px;
    font-weight: 700;
    color: var(--pj-orange-text);
    text-decoration: none;
}

.dash-feed__more:hover {
    text-decoration: underline;
}

.dash-feed__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 12px;
}

.dash-feed__item {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    /* 44px de cible garantis quel que soit le contenu : une entrée sans
       sous-titre ni méta n'est qu'une ligne de texte. */
    min-height: 44px;
    padding: 10px;
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius-sm);
    color: var(--pj-ink);
    text-decoration: none;
}

.dash-feed__item:hover {
    border-color: var(--pj-orange);
    background: var(--pj-card-2);
}

.dash-feed__img {
    flex: none;
    width: 44px;
    height: 44px;
    border-radius: 8px;
    overflow: hidden;
    background: var(--pj-card-2);
}

.dash-feed__img img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* `min-width: 0` : sans lui un titre sans espace (une URL collée dans un
   intitulé d'offre) élargit la piste de la grille et fait déborder la page. */
.dash-feed__body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.dash-feed__name {
    font-weight: 700;
    font-size: 14px;
}

.dash-feed__sub,
.dash-feed__meta {
    font-size: 12px;
    color: var(--pj-muted);
}

/* ── Le bandeau des actualités diffusées (recette 2026-08) ─────────────
   Frère de `.pj-shell` dans `#app`, donc soumis exactement aux mêmes règles
   de largeur que la bande publicitaire juste au-dessus : sans `width: 100%`
   la marge auto sur l'axe transverse annule le `stretch` et le bandeau se
   dimensionne sur son propre contenu (§2). Même cause, même remède. */
.pj-news {
    width: 100%;
    max-width: var(--pj-max);
    margin: 0 auto 16px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.pj-news__item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 16px;
    background: var(--pj-card);
    border: 1px solid var(--pj-line);
    /* Le filet orange porte tout le sens : c'est ce qui distingue une
       annonce d'une carte ordinaire, sans recourir à un fond coloré qui
       demanderait sa propre paire de jetons clair/sombre. */
    border-left: 3px solid var(--pj-orange);
    border-radius: var(--pj-radius-sm);
}

.pj-news__ico {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    background: var(--pj-orange-soft);
    color: var(--pj-orange-text);
    font-size: 13px;
}

.pj-news__body {
    flex: 1 1 auto;
    /* `min-width: 0` : sans lui un titre sans espace (une URL collée) élargit
       le flex item et fait déborder toute la page horizontalement. */
    min-width: 0;
}

.pj-news__title {
    display: block;
    font-weight: 700;
    color: var(--pj-ink);
    text-decoration: none;
}

.pj-news__title:hover {
    color: var(--pj-orange-text);
}

.pj-news__excerpt {
    margin: 2px 0 0;
    font-size: 13px;
    color: var(--pj-muted);
}

/* 44px de cible, mesurés et non déduits du padding — c'est la règle du
   projet et `.btn--sm` est resté cinq jalons à 38px faute de la mesurer. */
.pj-news__close {
    flex: none;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: -6px -6px 0 0;
    background: none;
    border: 0;
    border-radius: 999px;
    color: var(--pj-muted);
    cursor: pointer;
}

.pj-news__close:hover,
.pj-news__close:focus-visible {
    background: var(--pj-card-2);
    color: var(--pj-ink);
}

.pj-news__all {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 0 4px;
    font-size: 13px;
    font-weight: 700;
    color: var(--pj-orange-text);
    text-decoration: none;
}

.pj-news__all:hover {
    text-decoration: underline;
}

.pj-ad--band .pj-ad__link img {
    /* 728x90 : on borne la hauteur plutôt que la largeur, sinon un visuel
       fourni en 1456px étire la bande sur les grands écrans. */
    max-height: 90px;
    width: auto;
    max-width: 100%;
    margin: 0 auto;
}

/* L'unité texte, dans le corps de la page.
 *
 * Une classe à elle plutôt que `.pj-ad--band`, pour une raison mesurée et
 * une seule : la bande porte `margin: 0 auto 16px`, faite pour se centrer
 * sous `--pj-max` entre deux frères de `#app`. Dans `#content`, qui est
 * un flex en colonne avec son propre `gap`, ces 16 px **s'ajoutent** à
 * l'espacement au lieu de le remplacer — la cumulation que le §9 de
 * CLAUDE.md interdit, « une seule mécanique d'espacement par page ».
 * Mesuré : 16 px de trop sous l'annonce avec `--band`, 0 avec celle-ci.
 * La largeur, elle, ne pose pas de problème dans les deux cas (625 px de
 * colonne, 625 px d'annonce) : le `width: 100%` de la bande suffit à
 * battre le rétrécissement qu'une marge auto provoquerait autrement. */
.pj-ad--inline {
    background: var(--pj-card);
    border: 1px solid var(--pj-line);
    border-radius: var(--pj-radius);
    box-shadow: var(--pj-shadow-sm);
    display: grid;
    gap: 8px;
    padding: 12px;
    width: 100%;
}

.pj-ad--inline .pj-ad__link img {
    max-height: 120px;
    width: auto;
    max-width: 100%;
}

/* La verticale 140x420, dans le rail connecté sous le bloc promo. */
.pj-ad--side {
    margin-top: 16px;
}

.pj-ad--side .pj-ad__link img {
    max-width: 140px;
    margin: 0 auto;
}

/* …et sa propre colonne dans la coquille visiteur, ajoutée seulement
   quand une annonce a été servie — `AppShell` pose la classe. Sans elle
   la grille reste à une colonne, donc une page sans annonce garde
   exactement la largeur qu'elle avait. */
.pj-shell--guest.has-adside {
    grid-template-columns: minmax(0, 1fr) 164px;
}

/* Sous 1100px la colonne n'existe pas : `AdSlotComponent.when()` refuse
   alors la requête, pour qu'aucune impression ne soit comptée pour une
   bannière que personne n'a vue. La règle ci-dessous n'est donc qu'un
   filet — elle rattrape le redimensionnement d'une fenêtre déjà peinte. */
@media (max-width: 1100px) {
    .pj-shell--guest.has-adside {
        grid-template-columns: minmax(0, 1fr);
    }

    .pj-shell--guest > .pj-ad--side {
        display: none;
    }
}
