/* ékla — foundation. Identity-free primitives shared by every skin.
   Colour NEVER appears as a literal here; it always comes from a preset var, so an
   identity reskins by swapping theme.json alone. */

/* --- accessibility ------------------------------------------------------- */
/* The focus ring is TWO TONES, and that is the point: it is the one rule in this file
   that has to hold for a palette nobody has chosen yet.
   A single-token ring cannot. Measured under Éclat: `primary` #FF4D3D is 3.29:1 on
   `base` but 2.96:1 on the `tertiary` track the skin switcher and the période toggle
   both sit on [6C 2026-08-24: "the skin switcher" is present tense but it was removed in
   Plan 6b Task 3 -- the tertiary track this measured against is the période toggle's alone
   now. The ratio and the ring rule are unaffected: `tertiary` did not change.] — under the
   3:1 WCAG 1.4.11 / 2.4.11 ask, i.e. the ring shipped failing on
   a surface this project already renders. Swapping the token for `contrast` inverts the
   problem instead of solving it: #2B1409 is 17.40:1 on `base` and 1.09:1 on the #12100F
   agenda ground [6C 2026-08-24: that literal is also historical -- the agenda ground is
   token-driven since 6b4 (`color-mix(accent 45%, contrast)`, identity.css §13) and computes
   to rgb(93,29,12) under today's palette, not #12100F. The 1.09:1 figure and "invisible for
   a whole page" conclusion still hold: encre-on-encre is illegible regardless of the exact
   dark tone.], where it would be invisible for a whole page.
   Two concentric bands fix it structurally: a `contrast` ring FRAMED on both sides by a
   `base` halo — halo 0–6px out from the border box, ring 2–5px, so 2px of halo inside
   the ring and 1px outside it. The indicator supplies its own adjacent colour, so
   1.4.11 is satisfied by the base/contrast pair alone, whatever the page behind is doing.
   WHY IT CANNOT FAIL FOR AN ARBITRARY FUTURE PALETTE. Against a ground G the indicator
   disappears only if BOTH tones fall under 3:1 of G. If G sits between them that needs
   the pair ratio under 9 (the worst G is the geometric mean, where each tone reads
   √pair); if G sits outside them, the nearer tone is at least the pair ratio itself. So
   the rule holds for every identity whose base/contrast pair clears 9:1. Brute-forced
   over the whole sRGB cube rather than argued: the worst adjacent colour that exists is
   rgb(99,135,45) and the better tone still reads 4.17:1 there. All four palettes in this
   project clear the bound — Éclat 17.40:1, Papier 17.73:1, Jardin 13.08:1, parent
   17.40:1. An identity that drops that pair below 9:1 has a body-text problem before it
   has a focus problem, but it must re-measure this rule.
   `!important` on the halo, and ONLY on the halo: box-shadow is the one property here
   that identities also set, and they do — `.wp-element-button { box-shadow: … }` in
   Éclat's identity.css is (0,1,0), ties this selector and wins on source order, which
   would strip the halo off the most-focused element on the site. The outline needs no
   such guard: no identity sets `outline`. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--wp--preset--color--contrast);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--wp--preset--color--base) !important;
  border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* NAV CLOSE BUTTON -- no rule needed here, measured 2026-07-28.
   Core's `.wp-block-navigation__responsive-close:focus{outline:none}`
   (style.css:3060) lands on a `tabindex="-1"` WRAPPER DIV
   (navigation.php:805), never in the tab order. The real close button
   is `.wp-block-navigation__responsive-container-close`
   (navigation.php:795) and has ZERO focus/outline rules in core, so it
   already receives the ring above via `:where(..., button, ...)`.
   Handoff item 6b described a gap that does not exist on the real
   control. Still worth one manual Tab-through of the mobile menu. */

/* --- images -------------------------------------------------------------- */
.wp-block-post-featured-image img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* NO-IMAGE FALLBACK -- the mechanism is `:not(:has(img))` on the identity's own card,
   not a shared `ekla-noimg` class.
   26 of 232 shows have no featured image and used to punch holes in every grid. (This
   comment read "31" from Plan 3 until the final review of Plan 5b; 26 is the re-derived
   count -- 206 of 232 carry a thumbnail -- and it now agrees with the three identity files.) A
   parameterisable shared primitive was specified for that (spec 4.1.4) and written here,
   but nothing ever emitted it: wp:post-featured-image renders NO ELEMENT when there is
   no thumbnail, so there is nothing to hang the class on. Eclat solved it with
   `.eclat-tile:not(:has(img))`, which needs no emitter. Retired in Plan 5b Task 1; spec
   4.1.4 amended to match. Each identity dresses its own empty card. */

/* --- wordmark ------------------------------------------------------------ */
/* ékla has no logo file and no charte. Each identity draws the wordmark in its own
   display face; this is the shared skeleton. A real logo drops in without rework. */
.ekla-wordmark {
  font-family: var(--wp--preset--font-family--display);
  font-weight: 800; letter-spacing: -.045em; line-height: 1;
  text-decoration: none; display: inline-block;
}

/* --- agenda filters ------------------------------------------------------- */
/* Classes emitted by ekla_render_agenda_filters(). Structure only — identities
   restyle. Until now these rendered as bare underlined links under Éclat.

   Read out of the live render callback (`ekla_render_agenda_filters()` in
   ekla/functions.php) rather than out of a class list, Plan 5a Task 3: it also emits
   `.ekla-filters__quand` (the "période" toggle group, a role="group"
   wrapper around the "À venir" / "Passés" links) and an `.is-active` state class
   alongside `aria-current="true"` on whichever filter link is current. Neither
   name was in the class list this file was specified from; both are styled below
   because they are real, currently emitted, currently unstyled markup — exactly
   the problem this stylesheet exists to fix. */
.ekla-filters { display: flex; flex-direction: column; gap: var(--wp--preset--spacing--30); }
.ekla-filters__quand { display: flex; align-items: center; gap: var(--wp--preset--spacing--20); }
.ekla-filters__group { display: flex; align-items: baseline; gap: var(--wp--preset--spacing--30); flex-wrap: wrap; }
/* `flex: 1` is structural, not skin, which is why it lives here and not in an identity.
   Measured on the live archive: without it the Âge and Lieu lists render on a line of
   their OWN, below their label, because flex line-breaking is decided on each item's
   hypothetical main size — and the ul's is its max-content width, all fourteen chips in
   one row. A zero flex-basis keeps the list beside its label and lets it wrap internally.
   Every identity meets this, so it is the parent's to own. */
.ekla-filters__group ul { display: flex; flex-wrap: wrap; gap: .5rem; list-style: none; margin: 0; padding: 0; flex: 1; }
.ekla-filters__label { flex: none; }
.ekla-filter { text-decoration: none; display: inline-block; }
/* The underline stays in this identity-free layer on purpose. The rule above strips the
   browser's own underline off every filter link, so weight alone would be the whole
   visible current-state cue; `aria-current="true"` (which the callback emits) carries it
   programmatically, and this carries it visually, with no colour and no geometry — the
   two things that would make it a skin decision. An identity that draws filters as pills
   undoes it in one declaration, as Éclat does; an identity that has not styled its
   filters yet — the state 5b starts from — still has a visible active filter. */
.ekla-filter.is-active { font-weight: 700; text-decoration: underline; text-underline-offset: .25em; }
.ekla-filters__count { margin: 0; }
/* Unlike a pill (nested in .ekla-filters__group ul > li), this is a direct child of
   the column-flex nav, so it would stretch full-width by default; keep it content-sized. */
.ekla-filter--clear { align-self: flex-start; }

/* --- sessions / dates ------------------------------------------------------ */
.ekla-sessions { list-style: none; margin: 0; padding: 0; }
.ekla-daterange { margin: 0; }

/* --- show blocks ------------------------------------------------------------ */
/* ekla_render_show_compagnie/booking/after emit .ekla-compagnie, .ekla-booking and
   .ekla-after. STRUCTURE ONLY -- every identity supplies its own ground, ink, scale and
   rhythm, the same split `.ekla-sessions` above already uses (foundation strips the list
   chrome; each identity styles the rows).

   WHY THIS RULE EXISTS. .ekla-after carries `alignfull` in the markup, which is how it
   gets its own ground (see ekla_render_show_after()'s docblock for the mechanism and why
   it is not `width: 100vw`). Dropping the contentSize cap drops it for the TEXT too --
   without this the paragraphs would run the full viewport, 3425px on a 3440 screen,
   which is worse than the unstyled state this plan set out to fix. So the measure is
   handed back here.

   THIS IS THE FLOOR, NOT THE RULING. An identity restates it where its own measure
   differs from the shared default. Identity stylesheets declare `['ekla-foundation']` as a dependency, so
   an identical (0,1,0) selector there wins on source order by construction, not by luck. */
.ekla-after > * {
  max-width: var(--wp--style--global--content-size);
  margin-inline: auto;
}

/* --- headings & anchors ---------------------------------------------------- */
/* Ragged multi-line headings and an anchor target landing flush under a sticky
   header are both small structural annoyances every identity shares, independent
   of skin. `:where()` keeps the specificity at zero so an identity can override
   either without a fight. */
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
:where(h1, h2, h3, h4, h5, h6, section)[id] { scroll-margin-top: var(--wp--preset--spacing--40); }

/* [6C 2026-08-24] Smooth in-page anchor travel, site-wide. The reduced-motion
   override above (:39-46) collapses this to auto — measured, universal selector
   + !important wins. No theme JS scrolls programmatically (grep-verified). */
html { scroll-behavior: smooth; }

/* --- layout utilities ------------------------------------------------------ */
/* `.ekla-bleed` and `.ekla-grid` WERE HERE, AND ARE RETIRED — final review of Plan 5b.
   Both were speculative primitives written for consumers that never arrived: measured across
   the whole of wp-content, ZERO consumers each, in any theme, template, pattern, part or
   stylesheet. Three identities shipped complete without either.
   `.ekla-bleed` is the reason this note is longer than "unused". Its technique was
   `width: 100vw` with a negative margin, which this project already established is wrong
   on this site: 100vw counts the scrollbar, so it reintroduces the horizontal overflow
   §9 spent a declaration removing. The retired Jardin identity needed a full-bleed band,
   looked at this, rejected it, and used core's own `.has-global-padding > .alignfull`
   instead. A primitive with no consumers AND a written-down defect is worse than dead code
   in a file every identity loads — it is a trap for whoever reaches for it next.
   The retirement is `.ekla-noimg`'s and `.pa-idx`'s, for the third time in this plan: a
   primitive nothing emits is not a foundation, it is a claim. `.ekla-rail` below was kept
   because it had a real consumer — the retired Jardin identity's archive-spectacle.html,
   five lanes. That was its ONLY consumer anywhere in wp-content (verified 2026-08-19,
   Plan 6b Task 3): with Jardin deleted, `.ekla-rail` is now itself a zero-consumer
   primitive by this note's own criterion. Retiring it is a separate decision this task
   did not make — flagged here rather than actioned. */

/* Horizontal scroll-snap rail (5b: upcoming-shows strip, galleries). Children of
   the rail should be `flex: none` at their own intrinsic or declared width. */
.ekla-rail {
  display: flex;
  gap: var(--wp--preset--spacing--30);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
.ekla-rail > * { scroll-snap-align: start; flex: none; }

/* ===========================================================================
   Pillars -- the four activities (Plan 5c, Task 2)
   ---------------------------------------------------------------------------
   TOKENS ONLY. Not one literal colour: this single component has to read as
   three different designs, and it can only do that if every value resolves to
   a preset slot that each child theme redefines.

   The icon uses `primary`, NOT `accent`. Measured against each identity's own
   base, accent gives 1.37:1 on Papier and 2.28:1 on Jardin -- both below the
   WCAG 1.4.11 3:1 floor for a graphical object, i.e. an icon you cannot see.
   primary is 3.29 / 4.83 / 4.64:1. verify-5c.php re-derives those numbers.
   =========================================================================== */
.ekla-pillars {
  padding-block: var(--wp--preset--spacing--60);
}

.ekla-pillars__row {
  gap: var(--wp--preset--spacing--50);
}

.ekla-pillars__item {
  display: flex;
  flex-direction: column;
  gap: var(--wp--preset--spacing--20);
  padding: var(--wp--preset--spacing--40);
  background-color: var(--wp--preset--color--base);
  border-top: 3px solid var(--wp--preset--color--primary);
}

.ekla-pillars__icon {
  color: var(--wp--preset--color--primary);
  flex: none;
}

.ekla-pillars__title {
  font-family: var(--wp--preset--font-family--display);
  font-size: var(--wp--preset--font-size--large);
  color: var(--wp--preset--color--primary);
  margin: 0;
}

.ekla-pillars__tagline {
  font-size: var(--wp--preset--font-size--small);
  color: var(--wp--preset--color--contrast);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0;
}

.ekla-pillars__keywords {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--wp--preset--font-size--small);
}

.ekla-pillars__keywords li + li {
  margin-top: 0.25em;
}

.ekla-pillars__note {
  font-size: var(--wp--preset--font-size--small);
  font-style: italic;
  margin: 0;
}

.ekla-pillars__more {
  margin-block-start: auto;   /* pins the link to the bottom so the four cards align */
  font-size: var(--wp--preset--font-size--small);
}

/* ===========================================================================
   Timeline -- ékla dans ses grandes lignes (Plan 5c, Task 3)
   ---------------------------------------------------------------------------
   Mobile first: ONE column, year above card, no axis. The alternating two-column
   form with a central axis is layered on at 48em, because at narrow widths an
   alternating layout produces unreadable ~140px text columns.

   The axis is `primary`. NOT `accent`: accent measures 1.37:1 on Papier and
   2.28:1 on Jardin against their own base -- below the WCAG 1.4.11 3:1 floor,
   i.e. an axis nobody can see. primary is 3.29 / 4.83 / 4.64:1.

   NOTHING here may key on a specific entry index. Only :nth-child(even/odd) is
   allowed, because ékla is expected to extend this past ten entries and any
   :nth-child(7) style rule would silently mis-render the moment they do.
   =========================================================================== */
.ekla-timeline {
  --ekla-timeline-axis: var(--wp--preset--color--primary);
  display: flex;
  flex-direction: column;
  gap: var(--wp--preset--spacing--50);
  padding-block: var(--wp--preset--spacing--60);
  position: relative;
}

.ekla-timeline__entry {
  display: flex;
  flex-direction: column;
  gap: var(--wp--preset--spacing--20);
}

.ekla-timeline__year {
  font-family: var(--wp--preset--font-family--display);
  font-size: var(--wp--preset--font-size--x-large);
  color: var(--ekla-timeline-axis);
  line-height: 1;
  margin: 0;
}

.ekla-timeline__card {
  /* Opaque on purpose. A translucent card over a scrolling column is how Éclat's
     filter panel shipped invisible in 5b; verify-5c.php asserts the computed
     background is not transparent. */
  background-color: var(--wp--preset--color--base);
  color: var(--wp--preset--color--contrast);
  padding: var(--wp--preset--spacing--40);
  border-left: 3px solid var(--ekla-timeline-axis);
}

.ekla-timeline__title {
  font-family: var(--wp--preset--font-family--display);
  font-size: var(--wp--preset--font-size--large);
  margin: 0 0 0.4em;
}

.ekla-timeline__figure {
  margin: 0;
}

.ekla-timeline__figure img {
  width: 100%;
  height: auto;
  display: block;
}

@media (min-width: 48em) {
  .ekla-timeline::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    background-color: var(--ekla-timeline-axis);
  }

  .ekla-timeline__entry {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--wp--preset--spacing--60);
    align-items: start;
  }

  /* Badge on the axis, one per entry, centred on the row's first line. */
  .ekla-timeline__entry::before {
    content: "";
    position: absolute;
    left: 50%;
    width: 0.9rem;
    height: 0.9rem;
    margin-left: -0.45rem;
    border-radius: 50%;
    background-color: var(--wp--preset--color--base);
    border: 2px solid var(--ekla-timeline-axis);
  }

  .ekla-timeline__entry { position: relative; }

  /* ODD entries: card left, year right. EVEN: mirrored. */
  .ekla-timeline__entry:nth-child(odd)  .ekla-timeline__card   { grid-column: 1; text-align: right; border-left: 0; border-right: 3px solid var(--ekla-timeline-axis); }
  .ekla-timeline__entry:nth-child(odd)  .ekla-timeline__year   { grid-column: 2; grid-row: 1; }
  .ekla-timeline__entry:nth-child(odd)  .ekla-timeline__figure { grid-column: 2; }

  .ekla-timeline__entry:nth-child(even) .ekla-timeline__year   { grid-column: 1; grid-row: 1; text-align: right; }
  .ekla-timeline__entry:nth-child(even) .ekla-timeline__card   { grid-column: 2; grid-row: 2; }
  .ekla-timeline__entry:nth-child(even) .ekla-timeline__figure { grid-column: 1; }
}

/* ===========================================================================
   Page sections and the team grid (Plan 5c, Task 4). Tokens only.
   =========================================================================== */
.ekla-section {
  padding-block: var(--wp--preset--spacing--60);
  scroll-margin-top: var(--wp--preset--spacing--70);   /* anchors must clear the sticky header */
}

.ekla-section__heading {
  font-family: var(--wp--preset--font-family--display);
  font-size: var(--wp--preset--font-size--xx-large);
  color: var(--wp--preset--color--primary);
  margin-block-end: var(--wp--preset--spacing--40);
}

.ekla-team {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wp--preset--spacing--40);
}

@media (min-width: 40em) { .ekla-team { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64em) { .ekla-team { grid-template-columns: repeat(3, 1fr); } }

.ekla-team__member {
  background-color: var(--wp--preset--color--tertiary);
  padding: var(--wp--preset--spacing--30);
}

.ekla-team__name {
  font-family: var(--wp--preset--font-family--display);
  font-size: var(--wp--preset--font-size--medium);
  margin: 0 0 0.2em;
}

.ekla-team__role,
.ekla-team__mail {
  font-size: var(--wp--preset--font-size--small);
  margin: 0;
}

/* --- consent banner: STRUCTURE ONLY ------------------------------ (Plan 5f) --- */
/* Appearance lives in each identity.css, the same split .ekla-sessions and the show blocks use.
   Four structural facts belong here because they are identical everywhere and none of them is a
   colour:

   1. THE WIDTH -- CORRECTED, FIX ROUND 1. This rule is a NARROW-VIEWPORT GUARD, not the primary
      width control; an earlier version of this comment claimed "a real max-width is the only
      lever", and that was backwards. Per the CSS width-resolution algorithm, min-width ALWAYS
      wins over a conflicting max-width -- a fixed later step, not a specificity contest, so no
      selector weight and no !important in this file could ever have shrunk the banner past the
      plugin's own min-width floor. That floor is not a fixed default: bottom-left.css applies
      `min-width: var(--cmplz_banner_width)`, and generate_css() writes that variable straight
      from the banner RECORD's own `banner_width` column (class-cookiebanner.php declares it at
      :191, save() persists it at :608, get_css_settings() emits it at :1144). The plan's cited
      measurement ("setting it to 430px left the banner at 1713px") was taken BEFORE Task 1
      created the banner record, when the front end served the defaults/ fallback unconditionally
      and no generated CSS applied at all -- that state no longer exists. The real fix is
      `scripts/complianz-setup.php` setting `banner_width = 430` on the record (Plan 5f Task 5 fix
      round 1); with min-width also at 430 the two now agree at the wide end, and the
      calc(100% - 20px) half below is what still does real work, capping the banner below
      ~450px viewports where 100% - 20px undercuts 430px.
   2. THE BULLETS. The plugin's .cmplz-links / .cmplz-documents lists are plain <ul>/<li> with no
      list-style reset of their own, so an unstyled browser default bullets every entry -- ordinary
      styling for a plugin-rendered list inside a themed card. THIS IS NOT A FIX FOR THE CLIENT'S
      EARLIER "three empty bullets" REPORT, and an earlier pass at this plan wrongly said it was.
      That symptom's real cause was unconfigured page_links (all four cmplz_*_page options were
      false), which left three links permanently class="cmplz-hidden" while their <li> stayed
      bulleted; Task 2 configured the pages and the symptom was already gone before this rule was
      written (see progress.md's Task 3 log). Recorded here, corrected in place, so the wrong
      causal claim made earlier in this same plan is not repeated.
   3. THE FLOATING TAB. Removed in favour of a footer link. This originally also avoided a
      77x22px overlap with the skin switcher, a second floating control that used to sit at
      right:28px (bottom-left.css moves the tab to left:40px, measured 2026-08-02). The skin
      switcher itself was removed in Plan 6b Task 3, so the footer-link choice is the only
      floating-control decision left on the page; nothing remains to overlap.
   4. THE FOOTER BUTTON'S RESET. Controller research, not the original plan text -- the plan's own
      footer markup (<a href="#cmplz-manage-consent">) turned out to be dead three ways over: the
      plugin's one reopen binding is `cmplz_add_event('click', 'button.cmplz-manage-consent', ...)`
      (cookiebanner/js/complianz.js:1903-1906), which fires only for a <button> ELEMENT carrying
      that exact class. A bare <button> inherits the browser's own control chrome (background,
      border, padding, font), which would look wrong in all three identities before Task 6 ever
      restyles it. Structure only here: no per-identity colour CHOICE, no identity look -- but see
      the Critical below: the reset still has to make the inherited colour legible, which is a
      structural obligation, not a per-identity one.

   ⚠ CRITICAL, FIX ROUND 2 (task-5 review, not caught by the original implementation or by fix
      round 1's browser pass -- contrast was never among the things measured). `font: inherit` was
      here from fix round 1; `color` was not, and browsers do NOT inherit `color` into form
      controls the way they inherit most other properties -- a bare <button> keeps the UA's own
      text colour (black) regardless of what colour its surrounding text uses. Measured live before
      this fix, WCAG relative luminance: Éclat (the ACTIVE DEFAULT identity) rendered the button at
      black-on-rgb(43,20,9) = 1.21:1 -- functionally invisible, on the one control that exists so a
      visitor can withdraw cookie consent. `color: inherit` fixes it structurally, with no
      per-identity rule: the button now takes each footer's own AMBIENT text colour (the
      body-prose ink the footer already sets, which `color: inherit` picks up from the nearest
      ancestor that declares one) -- which is what makes it legible on all three identities and is
      the whole point of this reset.
      ⚠ CORRECTED, FIX ROUND 3 (task-5 re-review). This comment used to also claim that colour is
      "the same colour the sibling Mentions légales / Vie privée links use" -- checked and FALSE on
      Papier: `.pa-footer` (identity.css:918) sets the ambient body-prose colour via --pa-body,
      which this rule inherits, but Papier's links take a separate, MORE SPECIFIC colour from
      theme.json's styles.elements.link.color.text (--contrast, near-black ink) -- so the button
      reads as body text while the links read as ink, two different colours. (True on Jardin,
      cosmetically true on Éclat where the link additionally carries opacity:.82 the button does
      not.) Whether the button should visually MATCH the links is a per-identity styling choice,
      not this file's -- left to Task 6.
      MEASURED LIVE AFTER THE FIX (getComputedStyle button `color` vs footer `background-color`,
      WCAG relative luminance, all three consent-cleared):
        Éclat  (default) : rgb(255,255,255) on rgb(43,20,9)   = 17.40:1
        Papier           : rgb(74,64,56)    on rgb(239,233,221) = 8.34:1
        Jardin           : rgb(231,240,221) on rgb(29,81,54)    = 7.84:1
      All three clear WCAG AA's 4.5:1 floor with wide margin; none is the accidental-only pass a
      light ground would otherwise give (Papier and Jardin are measured, not assumed safe because
      their grounds looked lighter/darker). Per-identity colour/font/treatment choices BEYOND this
      inherited value remain Task 6's, not this file's.

   ⚠ DEVIATION FROM THE PLAN'S LITERAL STEP 3 TEXT. The brief specified the narrow-viewport
      fallback as a calc() against the full-viewport-width unit; verify-5e.php's own A3 FORBIDS
      that exact unit anywhere in this file ("the retired technique -- it counts the scrollbar and
      reintroduces overflow", the same reason identity.css retired .ekla-bleed). Confirmed live:
      adding it back reddened A3 on a full suite run. Replaced with the equivalent percentage form
      below -- for a position:fixed element (which .cmplz-cookiebanner is, from the plugin's own
      base rule) a percentage resolves against the initial containing block exactly as the
      viewport-width unit would, WITHOUT the scrollbar-inclusion defect, and the plugin's own
      bottom-left.css already uses the same percentage form for this same element's mobile width.

      [6B5 2026-08-23, client item 3: the Éclat row above is STALE. That identity's footer no
      longer has a dark ground with white text -- it takes the palette's `primary` (coral) with
      `contrast` ink, measured 4.28:1, which is BELOW the 4.5:1 floor this comment claims all
      three clear. It is a knowingly shipped client-approved residual (ruling R2), not an
      oversight, but the sentence "all three clear WCAG AA's 4.5:1 floor with wide margin" is no
      longer true of Éclat and must not be read as current. Papier and Jardin no longer exist as
      child themes; their rows are historical.] */
.cmplz-cookiebanner { max-width: min(430px, calc(100% - 20px)); }
.cmplz-cookiebanner .cmplz-links ul,
.cmplz-cookiebanner .cmplz-documents ul { list-style: none; margin: 0; padding: 0; }
#cmplz-manage-consent { display: none; }
.ekla-cookie-link {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* 5G-ORPHAN-BEGIN */
/* =====================================================================
   ORPHAN SURFACE -- editorial base (Plan 5g, spec 2026-08-04).
   STRUCTURE ONLY, the same contract as .ekla-sessions/.ekla-after:
   foundation gives skeleton, spacing and figure behaviour; each identity
   supplies ground, ink, type scale and rhythm from its own identity.css,
   scoped by BODY CLASSES because the markup is shared (spec section 5).
   NO HEX HERE -- test-orphan-page.php B3 rejects the whole slice on one.
   ===================================================================== */

/* Head skeleton: page + article + utility titles share one rhythm floor. */
.ekla-page__head,
.ekla-article__head {
  padding-block: var(--wp--preset--spacing--50) var(--wp--preset--spacing--40);
}
.ekla-page__title,
.ekla-article__title,
.ekla-news__title,
.ekla-search__title,
.ekla-tax__title {
  /* THE FLOOR, NOT THE RULING -- each identity restates scale with its
     own display face; this only guarantees a hierarchy exists unskinned. */
  font-size: clamp(2rem, 3.5vw, 3rem);
  line-height: 1.1;
}

/* Article meta row. */
.ekla-article__label {
  font-size: .8125rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.ekla-article__label a { text-decoration: none; }
.ekla-article__date { font-size: .9375rem; }
.ekla-article__back { margin-block-start: var(--wp--preset--spacing--50); }

/* Featured figure: core renders NOTHING when no thumbnail exists, so no
   empty-box defence is needed; this only disciplines the one that exists. */
.ekla-article__figure img { width: 100%; height: auto; }

/* Prose defence: migrated content still carries raw <img> and stray widths. */
.ekla-page .wp-block-post-content img,
.ekla-article .wp-block-post-content img {
  max-width: 100%;
  height: auto;
}
.ekla-page .wp-block-post-content figcaption,
.ekla-article .wp-block-post-content figcaption {
  font-size: .875rem;
  opacity: .75;
  margin-block-start: calc(var(--wp--preset--spacing--20) / 2);
}

/* News cards (index.html, Task 3). Grid geometry comes from the block's own
   {"layout":{"type":"grid","columnCount":3}}; this is the card interior. */
.ekla-news__card { display: flex; flex-direction: column; gap: var(--wp--preset--spacing--20); }
.ekla-news__card figure { margin: 0; }
/* [6C3 2026-08-25, client: "the actualités images are cropped"] 16/9 + contain, mirroring
   the block attribute index.html now carries (`"aspectRatio":"16/9","scale":"contain"`) --
   core inlines both on the <img>, so the stylesheet value is the documented twin, not the
   deciding one. Was 3/2 + cover: the news images are wide event banners (1.91 and 2.67),
   which lost 21% of their width, text included. Letterbox on the identity's ground instead
   (identity.css gives the figure its tertiary background). */
.ekla-news__card img { width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: contain; }
.ekla-news__card-date { font-size: .875rem; }
.ekla-news__card-title { font-size: 1.25rem; line-height: 1.25; }
.ekla-news__card-title a { text-decoration: none; }

/* Search hits. Core's post-template <li> carries post_class(), so the
   post-type tag is CSS-generated from classes core already emits --
   French labels are user-facing copy and belong in the content channel,
   but ::before content is the one CSS exception (matches the pattern of
   query-no-results copy living in templates). */
.ekla-search__results .wp-block-post {
  padding-block: var(--wp--preset--spacing--30);
  border-block-start: 1px solid currentColor;
}
.ekla-search__results .wp-block-post::before {
  content: "Page";
  display: inline-block;
  font-size: .75rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  opacity: .7;
}
.ekla-search__results .type-post::before      { content: "Actualité"; }
.ekla-search__results .type-spectacle::before { content: "Spectacle"; }

/* 404. */
.ekla-404__title { font-size: clamp(2.5rem, 6vw, 4.5rem); line-height: 1; }
.ekla-404__lead { font-size: 1.125rem; }
.ekla-404__search, .ekla-search__form { max-width: 36rem; }
/* 5G-ORPHAN-END */

/* 5G2-FORMS-BEGIN */
/* =========================================================================
   PLAN 5G2 -- Forminator forms, token override layer.
   Spec: docs/superpowers/specs/2026-08-04-plan-5g2-forms-presentation-design.md
   The plugin's stylesheets stay loaded (row/col grid + validation plumbing
   for 97 live fields); this fence overrides ONLY the cosmetic layer. The
   selector prefix mirrors the skin's own strongest prefix, so every rule
   here ties-or-beats it on specificity and wins on source order (theme CSS
   enqueues after plugin CSS) -- measured 2026-08-04: the whole skin holds
   ONE !important, and the 47 KB generated per-form stylesheets restyle the
   form CONTAINER only, never .forminator-input/-button/-label.
   Identity-variable details ride the --ekla-form-* contract: children set
   the custom properties in their 5G2 fences and never write competing
   selectors. Values below are the parent's neutral defaults -- a child is
   always active in practice, so these only need to be sane, not seen.
   The ONE hex in this fence is --ekla-form-error: no identity palette
   contains a usable red (Papier/Jardin `accent` fail 3:1 outright, the
   plugin's own #E04562 is 3.9:1 on white), and #B3261E measures 6.54 /
   6.16 / 6.02 on the three field grounds (#FFFFFF / #FAF8F3 / #F3F7EC)
   and 5.38 as actually rendered on the plugin's shared pink error strip
   -- all >= 4.5, re-measured composited in the Task 4 browser pass.
   The gate allowlists exactly this custom-property line. */

.forminator-ui.forminator-custom-form[data-design=default] {
  --ekla-form-field-bg: var(--wp--preset--color--base);
  --ekla-form-field-fg: var(--wp--preset--color--contrast);
  --ekla-form-field-border: var(--wp--preset--color--contrast);
  --ekla-form-field-radius: 4px;
  --ekla-form-check: var(--wp--preset--color--primary);
  --ekla-form-btn-bg: var(--wp--preset--color--contrast);
  --ekla-form-btn-fg: var(--wp--preset--color--base);
  --ekla-form-btn-radius: 4px;
  --ekla-form-btn-ring: transparent;
  --ekla-form-btn-bg-hover: var(--ekla-form-btn-bg);
  --ekla-form-btn-fg-hover: var(--ekla-form-btn-fg);
  --ekla-form-btn-ring-hover: transparent;
  --ekla-form-btn-shadow: none;
  --ekla-form-btn-shadow-hover: none;
  --ekla-form-error: #B3261E;
}

/* Form controls do NOT inherit font or color -- the recorded lesson (this
   file's Complianz section; ekla-vivant identity.css ~1973). Everything
   text-like gets both, explicitly. 1rem also keeps iOS from zooming on
   focus. Padding/height are left to the plugin: structure, not cosmetics.
   !important on color/background-color/border -- Task 4 browser pass
   (2026-08-04) measured live and found EVERY generated per-form stylesheet
   (uploads/forminator/<ID>_<hash>/css/style-<ID>.css) carries its own
   ID-anchored rule for these exact properties (Forminator's stock,
   never-customised "Appearance" preset, baked in at form-creation time),
   e.g. `.forminator-ui#forminator-module-5936.forminator-design--default
   .forminator-input`. An ID beats any number of classes regardless of
   source order, so without !important this whole rule silently lost and
   every form rendered the plugin's factory grey/white, not the identity's
   tokens -- contradicting the "generated stylesheets restyle the
   container only" note this comment used to carry. font-family/font-size
   are left un-important (typography, not contrast; out of this task's
   narrow fix authority, deferred). */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-input,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-textarea,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-select--field,
.forminator-ui.forminator-custom-form[data-design=default] .select2-selection {
  font-family: var(--wp--preset--font-family--body);
  font-size: 1rem;
  color: var(--ekla-form-field-fg) !important;
  background-color: var(--ekla-form-field-bg) !important;
  border: 1px solid var(--ekla-form-field-border) !important;
  border-radius: var(--ekla-form-field-radius);
}
/* The generated stylesheet ALSO sets .select2-selection__rendered's own
   color (ID-scoped), which otherwise defeats plain inheritance from the
   rule above. Re-asserted explicitly -- pair 9 of the Task 4 matrix. */
.forminator-ui.forminator-custom-form[data-design=default] .select2-selection__rendered {
  color: var(--ekla-form-field-fg) !important;
}

/* Rhythm and room: a fixed inter-row beat (overriding the skin's own, so it
   is an override, not an addition), and enough textarea to draft a real
   message in -- 14 of the 97 live fields are textareas. */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-row {
  margin-bottom: 1.5rem;
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-textarea {
  min-height: 8rem;
}

.forminator-ui.forminator-custom-form[data-design=default] .forminator-label {
  font-family: var(--wp--preset--font-family--body);
  font-size: .9375rem;
  font-weight: 600;
  color: var(--wp--preset--color--contrast) !important; /* generated stylesheet ID-scopes this too */
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-required {
  color: var(--ekla-form-error);
}

/* Checkbox/radio: native controls tinted from the palette. Éclat overrides
   --ekla-form-check to encre in its fence -- its primary (#FF4D3D-class
   coral) sits at the 3:1 boundary on white; Papier (~5.6:1) and Jardin
   (~4.7:1) keep primary. All three pairs are re-measured at Task 4. */
.forminator-ui.forminator-custom-form[data-design=default] input[type="checkbox"],
.forminator-ui.forminator-custom-form[data-design=default] input[type="radio"] {
  accent-color: var(--ekla-form-check);
}
/* accent-color above is inert for THIS skin: the plugin visually clips the
   native input to 1x1px (the standard sr-only pattern) and paints a custom
   .forminator-checkbox-box span instead, whose tick uses `color: inherit`.
   Found live in the Task 4 browser pass; fixed at the element that is
   actually rendered, !important for the same ID-scoped-generated-CSS
   reason as the rules above. */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-checkbox-box {
  border-color: var(--ekla-form-check) !important;
  color: var(--ekla-form-check) !important;
}

/* THE FOCUS RE-ASSERT. The skin writes `outline:0` twelve times (incl.
   `.forminator-input:focus`) at class specificity, which silently defeats
   foundation's :where()-scoped ring (zero specificity) on every control in
   every form. Same ring values as the global rule above; border-radius is
   the one deviation -- the global rule's 2px would visibly square the
   focused field's corners, so the field's own radius is kept. The halo
   keeps its !important for the same reason the global rule holds it.
   outline ALSO needed !important -- Task 4 browser pass (2026-08-04)
   measured live and found the skin's real `:focus` outline-killer is
   `[data-design="default"][data-color-option="default"] .forminator-input:focus`,
   ONE MORE attribute selector than this rule's own prefix, so it outranks
   us on specificity even though it targets :focus and we target the more
   correct :focus-visible (a tie would go to source order, but this was
   never a tie). Without !important the ring's outline silently rendered
   `outline-style: none` while its box-shadow halo (already !important)
   painted fine -- caught only by focusing the control live and reading
   computed style, not by curl. */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-input:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-textarea:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-select--field:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] .select2-selection:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] input[type="checkbox"]:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] input[type="radio"]:focus-visible,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-button-submit:focus-visible {
  outline: 3px solid var(--wp--preset--color--contrast) !important;
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--wp--preset--color--base) !important;
  border-radius: var(--ekla-form-field-radius);
}

/* The submit button. `background` (not -color) so a gradient var works --
   Éclat's soleil ramp rides this. [6B4 2026-08-22: the 2px transparent
   resting ring is GONE (border: 0). Under the `background` shorthand the
   gradient tile sizes to the padding box but paints to the border box, so
   the ring filled with a REPEAT of the ramp -- the #FFEC80 end butting the
   #E7411C start as a visible 2px outline (client item 1). Site buttons
   carry border: 0 and never showed it. Hover ring tokens stay defined
   (verify-5g2 B2) but nothing consumes them while the border is 0.
   The three font declarations carry !important because the generated
   per-form stylesheet (style-<ID>.css, ID-anchored, specificity (1,3,0))
   otherwise wins: measured 14px/500/inherit on forms 5936 and 5810 --
   same mechanism, same fix as the background/color pair below.] */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-button.forminator-button-submit {
  font-family: var(--wp--preset--font-family--body) !important;
  font-size: .9375rem !important;
  font-weight: 700 !important;
  line-height: 1.2;
  padding: .9rem 1.75rem;
  background: var(--ekla-form-btn-bg) !important; /* generated stylesheet ID-scopes background-color+color
                                                       on THIS exact selector -- measured live rendering
                                                       white ink on Éclat's button, the forbidden case its
                                                       own ruling exists to prevent (2.07:1). !important
                                                       restores the intended token on every identity. */
  color: var(--ekla-form-btn-fg) !important;
  border: 0;
  border-radius: var(--ekla-form-btn-radius);
  box-shadow: var(--ekla-form-btn-shadow);
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease,
              box-shadow .2s ease, transform .2s ease;
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-button.forminator-button-submit:hover {
  background: var(--ekla-form-btn-bg-hover) !important;
  color: var(--ekla-form-btn-fg-hover) !important;
  border-color: var(--ekla-form-btn-ring-hover);
  box-shadow: var(--ekla-form-btn-shadow-hover);
}

/* Validation + result states. Vocabulary measured from the skin:
   .forminator-has_error on the field wrapper, .forminator-error-message
   for the inline message, .forminator-response-message (+ .forminator-
   success/.forminator-error) for the AJAX banner. The plugin's #E04562
   fails AA on every identity's ground; --ekla-form-error replaces it. */
.forminator-ui.forminator-custom-form[data-design=default] .forminator-has_error .forminator-input,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-has_error .forminator-textarea,
.forminator-ui.forminator-custom-form[data-design=default] .forminator-has_error .select2-selection {
  border-color: var(--ekla-form-error) !important; /* generated stylesheet ID-scopes #E51919 here too */
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-error-message {
  font-family: var(--wp--preset--font-family--body);
  font-size: .875rem;
  color: var(--ekla-form-error) !important; /* generated stylesheet ID-scopes #333333 here too */
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-response-message {
  font-family: var(--wp--preset--font-family--body);
  color: var(--wp--preset--color--contrast);
  border-inline-start: 4px solid var(--wp--preset--color--primary);
  padding: .75rem 1rem;
}
.forminator-ui.forminator-custom-form[data-design=default] .forminator-response-message.forminator-error {
  border-inline-start-color: var(--ekla-form-error);
}

/* The select2 FLOATING dropdown can mount outside the form element, where
   the --ekla-form-* scope does not reach -- hence the literal-token
   fallbacks on these two rules and nowhere else.
   Task 4 finding: this Forminator build never emits a plain .select2-dropdown
   class on the floating wrapper -- the real class is
   .forminator-select-dropdown (confirmed live: 0 matches for .select2-dropdown,
   the wrapper's actual classList is
   "forminator-select-dropdown forminator-custom-form-<ID> forminator-dropdown--default …").
   .select2-dropdown is kept, harmlessly, as a fallback for any Forminator
   context that does emit it; .forminator-select-dropdown is the one that
   actually matches here. Generated per-form CSS also styles this wrapper
   (class-scoped, not ID-scoped, but still enough classes to outrank ours),
   hence !important -- same defeat family as the rest of this fence. */
.forminator-select-dropdown-container--open .select2-dropdown,
.forminator-select-dropdown-container--open .forminator-select-dropdown,
.forminator-ui.forminator-custom-form[data-design=default] .select2-dropdown {
  font-family: var(--wp--preset--font-family--body);
  color: var(--wp--preset--color--contrast) !important;
  background: var(--wp--preset--color--base) !important;
  border: 1px solid var(--wp--preset--color--contrast) !important;
  border-radius: var(--ekla-form-field-radius, 4px);
}
/* The option list inherits from the dropdown wrapper in principle, but the
   generated stylesheet ID-scopes each option's own color too -- re-asserted
   so the dropdown reads as themed, not "stock white-and-blue" (Task 4 brief). */
.forminator-select-dropdown-container--open .select2-results__option {
  color: var(--wp--preset--color--contrast) !important;
}
/* 5G2-FORMS-END */

/* =========================================================================
   Plan 6a, Task 2 -- per-séance venue & audience inside .ekla-sessions.
   ---------------------------------------------------------------------------
   NOTE ON NUMBERING: this file has no "§N" scheme of its own (that convention
   belongs to the per-identity identity.css files -- see e.g. ekla-vivant's
   §1..§17). Sections here are named banners instead (Pillars, Timeline, the
   consent banner, ...), so this one follows that convention rather than
   inventing a numbering this file has never used.
   ENGINE-LEVEL MINIMUM ONLY -- identity styling belongs to 6b, the same split
   `.ekla-sessions` above and the show blocks already use: foundation strips
   list chrome and gives structure, each identity supplies ground/ink/scale.
   =========================================================================== */
.ekla-session__lieu,
.ekla-session__public { display: inline-block; margin-left: .5rem; font-size: .85em; opacity: .8; }

/* Plan 6a, Task 6 -- "Infos pratiques" (ekla/show-infos). Same engine-level-minimum split:
   foundation gives structure only, identity styling is 6b's. */
.ekla-infos { margin-top: 1.5rem; }
/* [6D 2026-09-04, client item 3] the credits block reads as a footnote: .8rem (12.8px on the
   16px root) with a slightly tighter leading. Ink unchanged, so the 9.75:1 pair stands. */
.ekla-infos__distribution { font-size: .8rem; line-height: 1.45; }

/* --- 6B2 COVER GRID (bibliotheque numerique, tracker 2026-08-13: "prevoir une grille ?") ---
   ---------------------------------------------------------------------------
   Sarah's complaint was "PDFs displayed too big". Both halves of that are a layout
   problem, not an image problem: /bibliotheque-numerique/ stacks 68 cover figures one
   per row inside a flow layout, 13 of the sources are 983-2560px wide (attachment 605
   is 1707x2560, i.e. ~1280x1919 at the 1280px contentSize -- one cover taller than the
   viewport), and the other 40 are under 400px wide yet still each take a whole row.
   scripts/migrate/37-biblio-pdf-grid.php wraps ALL FIVE entry runs on that page as
   `.ekla-cover-grid` groups of `.ekla-cover-grid__item` tiles -- 5 grids, 68 tiles.

   WHY `cover` AND NOT `pdf` (ruling R15, fix round 1, 2026-08-20). The first cut of this
   fence was `.ekla-pdf-grid` and it gridded only the four runs that carry a `.pdf` href,
   leaving "Les publications en vente" -- 10 physical books, no downloads -- as the one
   stacked section between four grids. The class name was what forced that: it made the
   scope question a naming question. Only 43 of the 68 tiles link a PDF at all; what they
   ALL are is cover images. Renaming fixed the scope. The old class name is gone entirely,
   and the migration script hard-aborts on any page still carrying it.

   ENGINE-LEVEL MINIMUM, like `.ekla-sessions` and `.ekla-infos` above: structure only.
   The tiles inherit their type, ground and rhythm from whichever identity is active.

   WHY THE TILE WRAPPER EXISTS AND IS STYLED HERE. An entry on that page is a RUN of
   sibling blocks -- a cover figure plus a `core/html` description or a `core/buttons`
   download button -- never a single block (the 6b2 design doc records the covers as
   figures wrapping the `.pdf` link; measured 2026-08-20, not one of them contains an
   `<a>` at all). CSS Grid places DIRECT CHILDREN, so the per-entry wrapper is what keeps
   a cover and its own download button in the same cell. Those wrappers are `core/group`
   blocks, so core's layout stylesheet gives them block-gap margins; the reset below is
   what stops the grid reading as a stack with gutters.
   =========================================================================== */
.ekla-cover-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
  /* CORRECTED 2026-08-21 (6b3 fix round 1): was `align-items: start`. No rule in this
     fence ever depended on that (grep of the whole suite finds zero needles pinning
     align-items), and it silently defeated the bottom-alignment this task adds below --
     with `start`, grid items are never stretched to their row's shared height, so a
     flex item's own box is exactly its own content height with no free space left, and
     `margin-block-start: auto` on its last child has nothing to consume (measured:
     browser getBoundingClientRect on a 4-tile row showed item heights of 654/657/573/
     573px, not a shared row height, before this fix). `stretch` is grid's own default
     for block-level children and is what the flex-column + margin-auto idiom below
     actually needs: with it, every tile in a row shares that row's height and each
     tile's own last child can be pushed to ITS OWN bottom edge, which lands all of them
     on a common line. */
  align-items: stretch;
}
@media (min-width: 64rem) {
  /* 6b3 (§F.2): 64-80rem was 4 columns -> 226px description tiles (~27 chars a
     line). Intermediate 3-col band; 4 columns from 80rem. */
  .ekla-cover-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 80rem) {
  .ekla-cover-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Core's flow layout puts `margin-block-start` on every child after the first, and a
   constrained layout adds `max-width: <contentSize>` + auto inline margins. Inside a grid
   cell both are wrong: the first tile of each row would sit higher than the rest, and the
   auto margins would centre a narrow tile in its column. */
.ekla-cover-grid > .ekla-cover-grid__item {
  margin: 0;
  max-width: none;
  /* 6b3 item 6: column so mixed-height tiles align their download control at
     the bottom edge (the .ekla-pillars__item idiom). */
  display: flex;
  flex-direction: column;
}
/* GUARDED 2026-08-21 (6b3 fix round 2). 12 of the 68 tiles (Art à l'École x5, Spectacle
   x5, Turbulences x2) carry ONLY a cover -- no description, no download button -- so
   their cover `<figure>` IS `:last-child`. Unguarded, fix round 1's `align-items:
   stretch` (above) made those tiles fill their row's shared height and this rule then
   pushed the lone cover to the tile's BOTTOM, leaving an empty gap above it while the
   full tiles beside it (image + text, correctly guarded by having a second child) kept
   their covers at the top -- measured live on grid 2/3's row 3 (F,F,I,I at 4 columns)
   and grid 4's row 2 (F,I,I at 3 columns). `:not(:only-child)` disables the auto margin
   exactly on those 12 single-child tiles and leaves their cover at the flex column's
   natural start position; verified against the rendered post that all 56 remaining
   tiles have >= 2 children (40 have 2, 15 have 3, 1 has 4), so the guard never disables
   the wanted alignment on a full tile.

   [6B5 2026-08-23: the per-count breakdown above is superseded. Converting 22 inline
   bracketed links into sibling core/buttons blocks (client item 5) moved many tiles from two
   children to three; re-measured by parsing the live post, the distribution is now 12 tiles
   with 1 child, 24 with 2, 25 with 3 and 7 with 4. THE TWO NUMBERS THIS RULE ACTUALLY DEPENDS
   ON DID NOT CHANGE: there are still exactly 12 single-child tiles for `:not(:only-child)` to
   exempt, and still 56 tiles with two or more children, so the guard's premise holds untouched
   and only the parenthetical census needed correcting; note this census counts BLOCKS in
   post_content, not rendered DOM children, which differ -- what the selector actually depends
   on and what was re-verified is that exactly 12 tiles render a single child (so
   :not(:only-child) exempts them) and 56 render two or more] */
/* 6B6 TASK 2 (2026-08-23): A FLOOR UNDER THE BOTTOM ALIGNMENT ABOVE. margin-block-start:auto
   bottom-aligns each tile's last child by CONSUMING free space -- but on the tallest tile in
   a grid row (every item is stretch-sized to the row's height, see align-items:stretch above)
   that tile's own content already fills the row, so there is no free space to consume and
   auto resolves to 0: its download control sits flush against the text above it. Measured
   live on /bibliotheque-numerique/ before this fix, of the 56 tiles this rule targets (the
   12 single-child tiles are exempted by the guard above): 42/56 flush at 2 columns, 40/56 at
   3 columns, 36/56 at 4 columns -- worse, not better, at the widest layout.

   TWO CANDIDATES WERE MEASURED (task-2 brief, live at 2/3/4 columns via iframe sweep).
   Candidate A added `row-gap: 1rem` to the flex column on the .ekla-cover-grid__item rule
   above: a clean 16px floor, but row-gap inserts 1rem between EVERY pair of children, not
   only above the last one. 16 of the 68 tiles carry a `core/spacer` (mostly 10px) between
   cover and description, always in that fixed order (37-biblio-pdf-grid.php's own doc
   comment: an entry "OPENS at a core/image and absorbs the following core/spacer"), so the
   two new row-gaps land the same way on all 16; 3 of them (tiles 0, 2, 22 in DOM order)
   were directly measured live and, on all 3, row-gap pushed cover-to-text from 66px to
   98px at 2, 3 AND 4 columns alike -- a new 16px gap on each side of the 10px spacer --
   changing the client's existing rhythm. Rejected on that measurement.

   CANDIDATE B, KEPT: `padding-block-start: 1rem`, added to this SAME last-child rule rather
   than a new one. Padding is scoped to the element it is declared on and cannot touch the
   gap between earlier siblings -- measured live on the same 3 sample tiles, cover-to-text
   held at exactly 66px at 2, 3 and 4 columns, unchanged.

   COST, ADDED HERE (6B6 fix wave, MINOR 4 -- this fence's house rule is that the comment
   carries the trade-off): `padding-block-start: 1rem` lands on all 56 qualifying tiles
   unconditionally, including the ones that were already NOT flush, so it adds 16px to their
   gap too, not only to the previously-flush ones -- and because grid `align-items: stretch`
   sizes every tile in a row to its tallest member, whose own slack is tautologically 0, that
   tallest tile's row grows by 16px as well; a tile that had a comfortable 82px gap now has
   98px.

   ⚠ THE BRIEF'S OWN DEFECT SCRIPT STILL READS THIS AS UNFIXED -- MEASURED, NOT A GAP IN THE
   FIX. Padding sits INSIDE an element's own border box; unlike margin, it never moves that
   box's own top edge. A script comparing `lastChild.getBoundingClientRect().top` against
   the previous sibling's `.bottom` (Step 1/6 of the task brief -- the obvious way to look
   for this defect) therefore still reads 42/40/36 zeros at 2/3/4 columns after this fix,
   identical to the pre-fix counts, because the wrapper box's own top position never
   changes. The floor is real but lives inside that box: confirmed instead by computed
   style, where `padding-block-start` is unconditionally 16px on all 56 qualifying tiles at
   every column count, and by the rendered `<a>`/`<button>` inside each `.wp-block-buttons`
   last-child, measured exactly 16px below the previous sibling on every tile the box-edge
   script reports as 0. Content-level floor (box gap + padding) measured >= 16px on all 56
   tiles at 2, 3 and 4 columns, minimum exactly 16px. A future check for this floor must
   measure content position, or grep this declaration (as Task 7 does) -- not wrapper-box
   position, which cannot see this fix.

   Shared bottom baseline (6b5 item 5, above) verified undisturbed: box bottoms and the
   inner <a>/<button> tops and bottoms are equal within every grid row at 2, 3 and 4
   columns (23/14/13 rows respectively, 0 rows failing). */
.ekla-cover-grid > .ekla-cover-grid__item > :last-child:not(:only-child) {
  margin-block-start: auto;
  padding-block-start: 1rem;
}
/* 6b5 item 5 (2026-08-23): "put buttons everywhere and center it in the bottom of their card,
   for now its on the left inside the card". The BOTTOM half of that ask already shipped -- it is
   the margin-block-start:auto rule directly above, from 6b3 item 6. What was missing is the
   centring: core emits .wp-block-buttons as a flex row with no justification, i.e. flex-start,
   which is the "on the left" the client is describing.
   ONE rule for all 41 controls rather than a justifyContent attribute on the 17 that script 43
   converts: the attribute would emit is-content-justification-center on those alone and leave
   the 24 pre-existing buttons without it, giving two markup shapes for one visual result.

   ⚠ CORRECTION, fix round 1 (2026-08-23). "41 controls" / "the 17 that script 43 converts" above
   predate the discovery that script 43's original premise (17 bracketed links, one nesting shape)
   missed 5 more links in a second shape. Script 43 now converts 22, giving 46 controls total
   (24 pre-existing + 22 converted), not 41. The rule's own reasoning is unchanged: still one
   shared rule rather than a per-block justifyContent attribute, now covering 22 converted
   controls instead of 17. */
.ekla-cover-grid .wp-block-buttons { justify-content: center; }
/* The cover fills its cell so tiles line up on a common left edge and a common width --
   the "grille" that was asked for. Capping is the point: `width: 100%` inside a
   ~284px column is what brings the 2560px covers down. It does mean the smallest covers
   (190px wide) are scaled UP by about 1.5x and will look soft; that is a source-asset
   problem (low-resolution scans), recorded rather than hidden, and it is fixed by
   replacing those files, not by this rule. */
.ekla-cover-grid .wp-block-image { margin: 0; }
.ekla-cover-grid .wp-block-image img { width: 100%; height: auto; }
/* JUSTIFICATION OFF INSIDE A TILE -- a regression THIS fence introduced, measured and then
   repaired rather than reported. The descriptions are migrated Joomla markup carrying an
   inline `style="text-align: justify;"`, which was set for a 313px-wide measure. Inside a
   tile the measure is 145px at a 360px viewport -- 19 characters per line, measured -- and
   justifying 19 characters produces the rivers and half-width word gaps visible in
   task-6b2-3-mobile-360-justify.jpg. `!important` is required and is the narrow reason this
   rule is allowed one: an inline style loses only to an author `!important` declaration, and
   foundation.css is the file permitted to carry them (5g2 precedent). Éclat's identity.css
   is NOT touched by this fence -- it carries exactly ONE deliberate `!important` in its
   declarations (§13's tile focus halo; the other 14 occurrences in that file are prose), and
   verify-5e.php:182-193 goes red if a second one ever appears anywhere in it.
   THE ATTRIBUTE SELECTOR IS THE SCOPE, not decoration. Measured in the rendered page: 18
   paragraphs are justified and 81 are CENTRED -- the "[Télécharger]" links and the copyright
   lines -- and all 99 sit inside a tile. A blanket `.ekla-cover-grid__item p` would
   left-align every one of them and quietly restyle the client's own copy; this rule leaves
   all 81 centred lines alone, verified computed `center` at six viewport widths. (There is
   one further centred element page-wide, an `h3`, which no `p` selector can reach either.)

   ⚠ CORRECTION, fix round 1 (2026-08-20). The round-0 version of this comment said the rule
   covered 8 justified paragraphs and that "the 10 justified paragraphs elsewhere on the page
   (the video blurbs and the whole publications-en-vente section)" kept their justification.
   THE VIDEO-BLURB HALF WAS FALSE and was never measured: the video section contains ZERO
   justified paragraphs (and zero centred ones). All 10 were in "Les publications en vente",
   which round 1 now grids as well -- so the split is no longer 8 inside / 10 outside but
   **18 inside / 0 outside**, and every one of them sits in a narrow tile with the same rivers
   problem. That is the ruled outcome, not an accident of the wider scope.
   test-biblio-grid.php asserts 18/0 so this cannot drift back unnoticed.
   Revert by deleting this one rule if ékla prefers the justified setting.

   ⚠ CORRECTION, 6b5 item 5 (2026-08-23). The "81 are CENTRED" / "all 99" counts above predate
   scripts/migrate/43-biblio-buttons.php, which lifts 17 of those centred "[Télécharger]" /
   "[Découvrir]" `<p style="text-align: center;">` paragraphs out of the page entirely and
   re-emits them as core/buttons blocks (see the .ekla-cover-grid .wp-block-buttons rule below).
   Those 17 paragraphs no longer exist, so the measured population is now 64 centred (81 - 17)
   and 82 total (18 + 64), not 81 / 99. THE RULE ITSELF NEEDED NO CHANGE: p[style*="justify"]
   never matched the bracketed links -- they carried text-align:center, not justify -- so this
   selector's own scope is unaffected by the conversion.

   ⚠ CORRECTION, 6b5 item 5 fix round 1 (2026-08-23). The "64 centred / 82 total" figures directly
   above were themselves wrong within hours: they were computed from a premise (script 43 converts
   17 links total) that the SAME fix round found incomplete -- 5 more centred bracket-links existed
   in a second nesting shape (`<strong><a>[Label]</a></strong>` instead of `<a><strong>[Label]
   </strong></a>`) that neither the original measurement nor the original conversion regex saw.
   Script 43 now converts all 22, so the measured population is 59 centred (81 - 22) and 77 total
   (18 + 59), not 64 / 82. Re-measured live via wp.sh eval after the fix-round-1 run, not derived
   by arithmetic alone. The rule's scope is, again, unaffected -- see the paragraph above. */
.ekla-cover-grid__item p[style*="justify"] { text-align: start !important; }

/* --- 6B6 TASK 6 (2026-08-23): VIDEO GRID (videotheque) -------------------------------
   `.ekla-video-grid` groups of `.ekla-video-grid__item` -- 4 sections, 14 videos.
   ENGINE-LEVEL MINIMUM, like `.ekla-cover-grid` above: structure only, with type, ground
   and rhythm left to the active identity.

   WHY A SECOND GRID FENCE AND NOT A REUSE OF .ekla-cover-grid. A cover tile is a portrait
   image with prose under it; a video tile is a 16:9 iframe with a caption under it. They
   share a column ladder and nothing else -- the aspect ratio below is the whole point, and
   putting it on the cover grid would distort 68 book covers.

   THE LADDER IS 2 -> 3 AT 64REM -> 4 AT 80REM (ruling R6, 6b6 task-6 brief). The brief's own
   code block implements only 2 -> 3 and makes the 80rem/4-up step conditional on measuring
   that a 4-up card still renders its title legibly. MEASURED LIVE, 2026-08-23, same-origin
   iframe harness at 1280px (80rem) with a temporary 4-column override on the rendered
   /videotheque/ page (all 14 tiles, real DOM, real YouTube embeds, not a mock): card width
   290px, video box 290x163 (a true 16:9 aspect-ratio box, confirmed non-collapsed), caption
   font-size 16.26px (the theme's normal paragraph size, not shrunk). The longest of the 14
   titles ("Dans les coulisses du theatre jeune public : entretien avec Coralie Vanderlinden
   de la Cie3637", 94 characters) wrapped to 3 lines; the other 13 wrapped to 1-2 lines; zero
   truncation, zero overlap, ordinary multi-word-per-line paragraph reflow throughout, zero
   horizontal overflow. This is a materially wider tile than `.ekla-cover-grid`'s own
   REJECTED 4-up-before-80rem measurement two fences above (226px, ~27 characters/line, for
   longer prose descriptions) -- different content, different width, not comparable as a
   pass/fail on its own, but the 290px result was independently measured on this fence's own
   content and read as legible on its own terms. Screenshot and full measurement in the task
   report. */
.ekla-video-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
}
@media (min-width: 64rem) { .ekla-video-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 80rem) { .ekla-video-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.ekla-video-grid > .ekla-video-grid__item { margin: 0; max-width: none; }
/* Core's responsive-embed wrapper already holds the ratio; this guarantees it even when a
   theme or plugin drops that class.

   ⚠ CORRECTION, 6B6 fix wave (2026-08-23). FALSE FOR THIS BLOCK SHAPE, measured live rather
   than assumed. Core's own ratio CSS (wp-includes/css/dist/block-library/style.css) is keyed
   on `.wp-embed-responsive .wp-has-aspect-ratio .wp-block-embed__wrapper::before` and the
   `.wp-embed-aspect-16-9` variant of it. Those two classes exist ONLY inside core's own
   inline <style> tag in the document, never on an element: confirmed live on /videotheque/
   (curl + a class-attribute sweep of the rendered HTML) that <body> does carry
   `wp-embed-responsive`, but all 14 <figure> elements are `class="wp-block-embed
   is-type-video"` and NOTHING else, and zero `class="…"` attributes anywhere in the page
   contain "aspect". The minimal `{"url":…,"type":"video"}` embed shape this content uses
   never emits `wp-has-aspect-ratio` / `wp-embed-aspect-16-9`, so core's mechanism never
   activates here. The declaration below is not a guarantee layered on top of core's ratio —
   on this page it IS the ratio, alone.

   PROVEN by live removal, not merely by the class being absent: overriding this declaration
   via an inline style on a rendered wrapper (which wins over this external class-selector
   rule without needing `!important`) collapsed wrapperH from 169.875px to a FIXED 150px —
   unchanged whether the wrapper was 302px wide (the measured 4-up card width) or forced to
   160px. Reason: an `<iframe>`'s `width`/`height` HTML attributes (500x281 here) do not give
   it a CSS-recognised intrinsic ratio the way an `<img>`'s natural pixels do, so with no
   intrinsic ratio and a resolved width, the UA's CSS default-object-size (300x150) supplies
   the height instead — NOT the iframe's own height="281" attribute. Restored byte-for-byte
   after measuring (169.875px reconfirmed).

   DO NOT DELETE THIS RULE: without it, every one of the 14 cards renders at a fixed ~150px
   height regardless of card width — e.g. letterboxed at the 4-up ~302px width (302x150 is
   WIDER than 16:9, not merely "off ratio"), and disproportionately tall relative to width at
   narrow columns. Nothing in the 55-file suite catches this deletion on its own (G5a in
   verify-6b6.php counts rendered `.ekla-video-grid__item`s, not their shape); G6 in the same
   file now pins this declaration by rule body, proven able to fail (task report). */
.ekla-video-grid__item .wp-block-embed__wrapper { aspect-ratio: 16 / 9; }
.ekla-video-grid__item iframe { width: 100%; height: 100%; display: block; }

/* --- 6C-EVENT-CARDS-BEGIN (Plan 6c-lite A1, 2026-08-24) ---------------------
   /journees-pro/ event cards. Structure rides the existing .ekla-pillars__row /
   __item columns interface (declared above, :195-203); this fence only fits the
   1000x1000 square posters, which every other card fence would crop (news 3/2)
   or letterbox (cover-grid 16rem contain). Tokens only, no hex (house rule). */
.ekla-event-card .wp-block-image { margin: 0; }
.ekla-event-card .wp-block-image img { width: 100%; height: auto; display: block; }
.ekla-event-card > h2 { margin-block: var(--wp--preset--spacing--20) 0; }
/* --- 6C-EVENT-CARDS-END --- */
