/**
 * academy-archives.css: the card-loop style layer. Unit D owns this file.
 *
 * Loaded by includes/assets.php as `ezy-academy-archives`, after `ezy-academy-chrome`, on every
 * academy request. It is not archive-only in scope: it holds what the CARDS need, and cards appear
 * on the hub, on the three archives, and anywhere Unit E places a related-content section.
 *
 * EXTRACTED, NOT PORTED (contract Addendum 10). Every block below names the 8882 file it came from.
 * Oxygen's compiled `post-NN.css` files are deliberately NOT shipped: they are one page's compiled
 * output, named for an Oxygen post id that means nothing here, and unregenerable because Oxygen is
 * not installed on 8883.
 *
 * WHAT WAS AUDITED AND DELIBERATELY LEFT OUT
 * ------------------------------------------
 * The four per-template Oxygen files behind this unit's pages are, in full:
 *
 *   post-65.css, post-119.css, post-120.css, post-121.css   (115 bytes, identical, one md5)
 *       .ezy-cards > .bde-loop,
 *       .ezy-cards .bde-loop-item,
 *       .ezy-cards .bde-loop-item > [class*="oxy-html-code"] { display: contents }
 *
 *   post-65-defaults.css, post-119-defaults.css,
 *   post-120-defaults.css, post-121-defaults.css            (41 bytes, identical, one md5)
 *       .bde-post-loop { max-width: 100%; width: 100% }
 *
 * NEITHER is ported, and neither is a judgement call.
 *
 * The first exists only to undo Oxygen. Its own builder comment on 8882 reads: "Loop unwrap:
 * PostsLoop emits .bde-loop > .bde-loop-item wrappers between the Tailwind grid/list and the card
 * markup. Flatten all three so the reference card element is the effective grid/list child." Our
 * templates render the cards as direct children of the grid, so there is nothing between them to
 * flatten and no element on the page that the selector can match.
 *
 * The second is inert twice over: it loads BEFORE tailwind.css so `max-width` loses to `max-w-2xl`
 * at equal specificity, and `width: 100%` only restates what a block-level element already
 * computed. Our grid carries no `bde-post-loop` class in any case.
 *
 * `global-settings.css` is excluded by ruling (Addendum 10): it carries the heading-font override
 * the owner overruled in Addendum 4, and porting it would reinstate a system-ui heading stack and
 * defeat the Jost ruling.
 *
 * It does however also carry THREE COLOUR declarations that do reach an academy page and that we are
 * currently missing, which is a real and visible fidelity gap. It is raised as a carryable for Unit
 * C in .pipeline/academy/D-driver.md and deliberately NOT written here, because it is document
 * chrome rather than a card-loop concern and `academy-chrome.css` is Unit C's file. Two units both
 * declaring a body background is how a conflict gets shipped. The measured values, so nobody has to
 * re-derive them:
 *
 *     body       { background-color: #f9fafb }   --bde-background-color -> --grey-50
 *     body       { color:            #374151 }   --bde-body-text-color  -> --grey-700
 *     h1 ... h6  { color:            #111827 }   --bde-headings-color   -> --grey-900
 *
 * The custom properties resolve inside `global-settings.css` itself, not in `variables.css`, which
 * is why a grep of `variables.css` alone reports them as undefined. They are not. Verified against
 * screenshots: 8882 paints the page #f9fafb behind the white `rounded-t-2xl` card panel and we paint
 * it white, so on 8883 the panel is currently invisible.
 *
 * The fourth declaration, `body{font-family:var(--bde-body-font-family)}`, resolves to a system-ui
 * stack and is exactly what Addendum 4 overruled in favour of Inter. It must NOT be ported.
 */

/* ---------------------------------------------------------------------------
 * Tag chips on webinar cards.
 *
 * Extracted verbatim from 8882:/wp-content/uploads/oxygen/css/post-93.css, the compiled CSS of
 * Oxygen loop template 93 (the webinar card). That file is loaded on /academy/webinars/ and on
 * /academy/ and is the ONLY per-template Oxygen CSS behind this unit's four pages that applies to
 * markup we actually emit: Unit C's ezy_academy_card_webinar() renders the
 * `.ezy-chips` container and its `rel="tag"` links.
 *
 * Original, minified, 187 bytes:
 *   .ezy-chips a{position:relative;z-index:10;border-radius:9999px;background-color:#f9fafb;
 *   padding:0.375rem 0.75rem;font-weight:500;color:#4b5563}
 *   .ezy-chips a:hover{background-color:#f3f4f6}
 *
 * Rewritten here as readable first-party CSS with the Tailwind palette names the hex values are,
 * so it is editable rather than opaque. Values unchanged: #f9fafb is gray-50, #f3f4f6 is gray-100
 * and #4b5563 is gray-600, all three straight out of the Tailwind scale the rest of the design uses.
 *
 * `position:relative` plus `z-index:10` lifts each chip above the card's absolutely positioned ring
 * overlay, which would otherwise swallow the clicks.
 * ------------------------------------------------------------------------ */

.ezy-chips a {
	position: relative;
	z-index: 10;
	padding: 0.375rem 0.75rem;
	border-radius: 9999px;
	background-color: #f9fafb; /* gray-50 */
	color: #4b5563;            /* gray-600 */
	font-weight: 500;
}

.ezy-chips a:hover {
	background-color: #f3f4f6; /* gray-100 */
}

/* ---------------------------------------------------------------------------
 * WHOLE-CARD CLICK. Contract Addendum 28 item 3, owner ruling, Gate 1 exception.
 *
 * 8882 makes the image and the title clickable and nothing else. The owner reported that as a
 * usability defect from using the site, and the addendum rules for the STRETCHED-LINK pattern
 * rather than wrapping the card in an anchor. Markup side is includes/partials.php: the article
 * carries `ezy-card`, the title anchor carries `ezy-card-link`, and the old media anchor is gone
 * so there is exactly one link and one accessible name per card.
 *
 * Written as first-party CSS rather than Tailwind utilities on purpose. `relative`, `absolute`,
 * `inset-0` and `z-10` all do exist in our purged build, but a stretched link needs a `::after`
 * on the anchor and Tailwind has no utility for that, so the rule has to live somewhere anyway.
 * Putting all four declarations in one place beats splitting them across markup and stylesheet.
 * (Addendum 21: `group-hover:opacity-50` was checked and is ABSENT from the build, which is why
 * the media fade below is written out rather than expressed as a utility.)
 *
 * WHY THERE IS ALSO A JS HALF. Measured, not assumed: an overlay that covers the card takes every
 * mousedown, so no text under it can be selected by dragging. Verified in headless Chrome against
 * a reduced test page and then against the real cards: with the overlay alone,
 * window.getSelection() came back "" for both the title and the author line. Objective 5 of this
 * unit requires selection to keep working, and no placement of the overlay can satisfy both: the
 * element that receives the mousedown is the one that decides. So:
 *
 *   no JS  -> the overlay is live. Whole card clickable, card text not selectable.
 *   JS on  -> academy-behaviour.js stamps `ezy-card-js` on <html>, the overlay goes
 *             pointer-transparent, text is selectable again, and a delegated click handler
 *             navigates on a click that did not select anything.
 *
 * The pattern is still the ruled one: the stretched link is the mechanism, and it is what works
 * with JavaScript off. The class is only ever ADDED by script, so a failed or blocked script
 * degrades to a fully clickable card rather than to a broken one.
 *
 * Stacking. The overlay sits at z-index 1, which is above the card's ring overlay and above the
 * author row (both positioned at z-index auto) and below `.ezy-chips a`, which the block at the
 * top of this file already lifts to z-index 10. So the tag chips on a webinar card stay clickable
 * and win over the card link, which is the behaviour their own extracted CSS was written for.
 * ------------------------------------------------------------------------ */

.ezy-card {
	position: relative;
}

.ezy-card-link::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
}

.ezy-card-js .ezy-card-link::after {
	pointer-events: none;
}

/* ---------------------------------------------------------------------------
 * CURSOR. Contract Addendum 33 item 1, owner ruling. This is a REGRESSION FIX for the block
 * above, not a new feature: the card became clickable but stopped looking clickable.
 *
 * MEASURED CAUSE, .verification/unit-v6/before-cursor.json. With JavaScript ON the overlay is
 * `pointer-events: none`, so the element under the pointer at a card corner is the <article>
 * itself (or the guide card's ring div), whose computed cursor is `auto`. With JavaScript OFF
 * the overlay is live and the element under the pointer IS the anchor, whose UA cursor is
 * already `pointer`. So the defect existed in exactly one of the two states, which is why it
 * needs a rule that does not depend on either:
 *
 *   before, JS on   corner -> cursor auto     (ARTICLE / ring DIV)
 *   before, JS off  corner -> cursor pointer  (A.ezy-card-link)
 *
 * `cursor` inherits, so one declaration on the card covers every descendant that does not set
 * its own, in both states. The chips and the title anchor still compute `pointer` from the UA
 * sheet, so nothing that was already right changes.
 *
 * WHY NOT THE `cursor-pointer` UTILITY. It does exist in the purged build
 * (`grep -c '\.cursor-pointer{' tailwind.css` = 1, Addendum 21 discipline), but the markup is
 * built in includes/partials.php, which this unit does not own and which a Navigator is auditing.
 * The rule also belongs beside the overlay it repairs. So it is written here.
 *
 * TRADE-OFF, stated rather than hidden: a pointer cursor over card text replaces the I-beam, so
 * the text no longer LOOKS selectable even though it still IS selectable with JS on (verified,
 * .verification/unit-v6/after-select.json). The owner asked for pointer on the card surface and
 * a card that lies about being clickable is the worse of the two.
 * ------------------------------------------------------------------------ */

.ezy-card {
	cursor: pointer;
}

/* The 50% image fade 8882 hangs off the media div's own :hover. The overlay shadows that div in
 * the no-JS path, so the same fade is re-hung off the card, which is now the thing being hovered. */
.ezy-card:hover .ezy-card-media {
	opacity: 0.5;
}

/* ===========================================================================
 * UNIT V4. Card grid: equal heights, aligned internal rows, tag overflow,
 * video thumbnail slot, and the hub band "see all" call to action.
 *
 * Contract Addendum 30, owner ruling. EVERY rule below is a deliberate GATE 1
 * EXCEPTION: 8882 does none of it. Do not "restore fidelity" here.
 *
 * ---------------------------------------------------------------------------
 * MISSING UTILITIES. Contract Addendum 21, checked before writing a line.
 *
 * `tailwind.css` is a PURGED build byte-identical to 8882, so a utility 8882
 * never used is not in the file and does nothing at all, silently. Measured on
 * 2026-08-25 with `grep -c '.CLASS{' wp-content/uploads/ezy-experiences/css/tailwind.css`:
 *
 *   ABSENT (0 hits), so written as first-party CSS below:
 *     .mt-auto          the auto-margin push that bottom-aligns the tag row.
 *                       This is THE mechanism objective 1 needs; without a rule
 *                       here the tag row would silently stay under the author row.
 *     .items-stretch    not needed in the end: grid items already stretch by
 *                       default, which is what equalises the card heights.
 *     .items-baseline   the band heading / CTA row uses it, so it is written out.
 *     .bottom-full      the tag popover anchor.
 *     .w-max .w-fit .self-start   (already recorded by Unit C)
 *     .flex-grow .basis-0 .min-h-0 .self-stretch .gap-y-2 .focus-within:*
 *     .group-hover:opacity-100    so the reveal is NOT expressed as a group hover.
 *     .tracking-widest .hover:text-orange-600
 *
 *   PRESENT and used as utilities in the markup:
 *     .h-full .flex-1 .grow .aspect-video .aspect-square .relative .absolute
 *     .inset-0 .object-cover .w-full .sr-only .z-20 .shadow-lg .justify-between
 *     .gap-4 .gap-3 .flex-wrap .items-center .truncate .min-w-0
 *
 *   PRESENT BUT INERT, the nastiest case found in this unit:
 *     .line-clamp-2  ->  `.line-clamp-2{-webkit-line-clamp:2}`  and NOTHING else.
 *     .line-clamp-3  ->  same shape.
 *   The purge kept the declaration but dropped the `display:-webkit-box`,
 *   `-webkit-box-orient:vertical` and `overflow:hidden` that make it work, so the
 *   class looks correct in the markup, greps as "present", and clamps nothing.
 *   That is why `.ezy-card-title` writes all four declarations itself.
 * ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
 * 1. Equal heights and aligned internal rows.
 *
 * NOT a fixed card height, by ruling: a pixel height makes a long title either
 * overflow or truncate unpredictably. Two mechanisms instead:
 *
 *   a. the grid equalises. `.ezy-cards` is `display:grid` and grid items default
 *      to `align-self: stretch`, so every card in a row is already the height of
 *      the tallest one. Nothing to add.
 *   b. the card distributes. The article is `flex flex-col`, so an `auto` top
 *      margin on the LAST row pushes it to the bottom of whatever height the grid
 *      handed the card, and the rows above keep their natural size.
 *
 * So: title top edges align because the media slot above them is a fixed ratio;
 * tag rows align because they are pushed to a common bottom.
 * ------------------------------------------------------------------------ */

.ezy-cards > .ezy-card {
	/* Belt and braces. The grid stretches its items already, but a card is also
	 * rendered outside a grid in the no-context placeholder path, and `h-full`
	 * on the article would be a no-op there. */
	height: 100%;
}

/* The tag row, bottom-anchored. `.mt-auto` does not exist in our build. */
.ezy-card .ezy-chips {
	margin-top: auto;
}

/* Title block: exactly N lines tall, with a real ellipsis.
 *
 * 8882 uses `h-[46px] text-ellipsis overflow-hidden`, which is two lines of
 * `leading-6` minus 2px, so the second line is clipped 2px into its own glyphs
 * and `text-ellipsis` never fires (it only applies to a single-line
 * `overflow:hidden` box). Replaced with a genuine multi-line clamp at a whole
 * number of lines, which is both aligned and legible. Line count is per card
 * shape, because a collection's titles are as long as they are: webinar titles
 * run to 78 characters and need three lines, guide and book titles fit two.
 */
.ezy-card-title {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: var( --ezy-card-title-lines, 2 );
	overflow: hidden;
}

.ezy-card--webinar {
	--ezy-card-title-lines: 3;
}

/* ===========================================================================
 * UNIT V7, contract Addendum 37, owner ruling: THE CARD'S VERTICAL RHYTHM.
 *
 *   "title is too close to the thumbnail / too much space between title and author"
 *
 * Two gaps, one place, every card type, every page that renders a card. Measured before the
 * change with a real browser at 1440 / 1024 / 390 on the hub and all three archives
 * (.verification/unit-v7/before-rhythm.json):
 *
 *                          thumbnail -> title      title text -> author row
 *   webinar card                  0px               15px, 39px or 63px
 *   guide card                   60px (32 to the    11px
 *                                date line, 12 more
 *                                to the title)
 *   book card                     n/a               n/a
 *
 * So the defect is on the WEBINAR card in both directions. The guide card already carries 32px
 * under its thumbnail and 12px above its byline, which is the rhythm being asked for, and the
 * owner did not report it. The book card has no thumbnail above its title at all: its
 * `.ezy-card-media` is 8882's empty 0-height ring and the cover image sits INSIDE the title
 * link, below the h3 (measured mediaH 0 at all three viewports). There is nothing there to
 * space away from, so gap one does not apply to it and is not forced on it.
 *
 * ---------------------------------------------------------------------------
 * GAP 1: under the thumbnail. 0px -> 1.5rem.
 *
 * Scoped to the RESERVED slot, `.ezy-card-media.aspect-video`, which is the marker
 * ezy_academy_card_media() adds when a set reserves a 16:9 thumbnail box (Unit V6). Written
 * against the slot rather than against `.ezy-card--webinar` on purpose: any card type that
 * reserves a thumbnail inherits the gap, which is what "make it uniform" has to mean for a
 * feature the guides will grow into. It also means the no-thumbnails fallback path in
 * ezy_academy_loop() still renders compact cards with no dead space, exactly as before.
 *
 * 1.5rem is not a new number: it is the value Unit V6 landed on for the single's sidebar rhythm
 * and it is `gap-6` / `mt-6` in the scale this design is built from. 1rem was rendered and
 * looked at first (.verification/unit-v7/cand-webinars-c1-thumb16-meta12-noresv-1440.png) and
 * is still tight under an 18px semibold title; 2rem
 * (cand-webinars-c3-thumb32-meta12-noresv-1440.png) pushes the tag row off a 1400px viewport
 * for no gain.
 *
 * ---------------------------------------------------------------------------
 * GAP 2: title to author row. 16px -> 0.75rem, and the reservation that dominated it is GONE.
 *
 * The margin was never the whole story, and a margin tweak alone would have been a fake fix.
 * V4 reserved `height: calc( lines * 1.5rem )` on the title block above 1024px so that every
 * author row in a row of cards landed on one line. It worked, and it is exactly what the owner
 * reported: a webinar title clamped at three lines but using only one leaves TWO EMPTY LINES
 * between the title and the byline. That is where 39px and 63px came from, against a 16px
 * margin. No margin value can fix it, because the slack is INSIDE the title box.
 *
 * So the reservation is removed and the slack moves to where nobody reads across it: below the
 * author row, absorbed by the `margin-top: auto` already on the tag row. The gap becomes a
 * single value on every card of every type, and 0.75rem is the guide card's own existing `mt-3`,
 * so the three card shapes converge on a number that was already in the design.
 *
 * WHAT STILL ALIGNS, which is everything Addendum 30 asked for:
 *   titles start on one line  -> the media slot above them holds a fixed ratio (Unit V6),
 *                                untouched here.
 *   tags sit on one baseline  -> `.ezy-chips { margin-top: auto }` plus the reserved chip band
 *                                above, untouched here.
 * WHAT NO LONGER ALIGNS, stated rather than hidden: the AUTHOR ROW now travels with its own
 * title instead of sitting on a shared line, so bylines are staggered by up to two line heights
 * across a row of cards. That is the trade the owner's note asks for, it reads as a byline
 * attached to its title, and it is one declaration to put back if the owner prefers the line.
 *
 * The CLAMP itself stays, unconditional, so an overlong title is still cut with an ellipsis
 * rather than pushing the rest of the card down. Only the reserved HEIGHT is gone. The chip
 * band's own lg-only reservation below is a separate mechanism and is deliberately untouched.
 *
 * Both values are plain CSS lengths rather than utilities: the margins they replace live in
 * includes/partials.php, and Addendum 21's purge trap makes a stylesheet rule the safer half of
 * the pair in any case. The markup keeps `mb-4` on the byline, which IS in the build
 * (`grep -cF '.mb-4{'` = 1), so the space between the byline and the tag row is provably
 * unchanged.
 * ------------------------------------------------------------------------ */

.ezy-card-media.aspect-video {
	margin-bottom: 1.5rem;
}

.ezy-card-author {
	margin-top: 0.75rem;
}

/* ===========================================================================
 * UNIT V7, SECOND PASS: THE CARD'S INTERNAL ROWS ALIGN STRUCTURALLY, VIA SUBGRID.
 *
 * The orchestrator rejected the first pass, correctly. Removing V4's title-height reservation
 * gave every card the tight byline the owner asked for, but it also unpinned the byline LINE:
 * on the hub's webinar row at 1440, a one-line title put its byline ~36px above its two-line
 * neighbours'. Addendum 30 is explicit that "content aligned with other boxes (so titles in the
 * same lines, tags etc)", and "the cards are all the same height" is NOT that claim. Card height
 * is equalised by the grid and says nothing about where a row sits inside the card.
 *
 * The two requirements only look contradictory. Alignment means every card in a row spends the
 * same vertical space on its title, so a short title must leave slack somewhere; the owner's
 * complaint is that the slack was FIXED at three lines, so a one-line title paid 48px whether or
 * not any card beside it needed the room. The fix is to make the slack the ROW's actual maximum
 * instead of the page's theoretical one.
 *
 * WHY SUBGRID AND NOT A COMPUTED HEIGHT. The alternative was to keep a reserved title block and
 * size it to the tallest title in the set. Rejected on merit, three reasons:
 *   1. "Tallest in the set" is not measurable where it matters. Wrapping depends on the rendered
 *      column width, the Jost/Inter metrics and the breakpoint, so PHP would be guessing from
 *      character counts, and JavaScript would mean a layout pass that shifts the page after paint
 *      and does nothing at all with scripts off, which this project treats as a supported state.
 *   2. Even done perfectly it is the SET maximum, so a row whose titles all fit two lines still
 *      pays for the one three-line title elsewhere on the page. Subgrid charges each row only
 *      what that row needs: measured, the hub row now spends 48px on titles, not 72px.
 *   3. It reintroduces a magic number that has to be re-derived every time a title is edited.
 *
 * Subgrid needs no number at all: the four card rows become tracks of the OUTER grid, so the
 * media, title, byline and tag rows of every card in a row are literally the same tracks, and
 * each track is as tall as its own tallest content. Support was checked in the browser rather
 * than assumed: `CSS.supports( 'grid-template-rows', 'subgrid' )` is true in the Chrome this was
 * verified in, and Grid Level 2 has shipped in all three engines since 2023.
 *
 * There is no Tailwind utility for any of this. `subgrid` is not in Tailwind v3 at all, and
 * Addendum 21's purge trap makes a documented rule the only honest option:
 *   grep -cF '.grid-rows-subgrid{'  0      grep -cF '.row-span-5{'  0
 *   grep -cF '.grid{' 1   grep -cF '.grid-cols-1{' 1   grep -cF '.lg\:grid-cols-4{' 1
 *
 * FIVE TRACKS, PLACED EXPLICITLY, not four placed automatically. The three card shapes happen to
 * have four rows each today, but the guide card's is a DATE row where the webinar's is its title,
 * and an author row is skipped entirely for a post with no author. Auto-placement would then slide
 * the tag row up a track and misalign the very thing this rule exists to align. Explicit
 * `grid-row` per element makes a missing row leave an empty 0px track instead, so nothing shifts:
 *
 *   1 media  2 date (guide only)  3 title  4 byline  5 tags
 *
 * `row-gap: 0` on the card is load-bearing and was measured, not assumed: a subgrid uses its OWN
 * gutters between the tracks it spans, while the parent's `gap-y-20` still separates one row of
 * cards from the next. Verified in a reduced test page before touching the site (parent row-gap
 * 80px, card row-gap 4px, measured 4px inside the card and 80px between card rows). So all the
 * internal rhythm stays where it already was, in margins: the 1.5rem under the reserved
 * thumbnail, the 0.75rem above the byline, the guide's `mt-8` date offset and the `mb-4` above
 * the tag row are all unchanged and all still do exactly what they did.
 *
 * `display: contents` on `.ezy-card-body` is what lets the markup stay as it is. 8882 wraps the
 * title and byline in one `max-w-xl` div; that wrapper cannot be a single track, because the
 * title and the byline are two of the rows being aligned. `display: contents` drops its box and
 * promotes its children to grid items while leaving the DOM, the class list and the crawler's
 * view untouched. The lost `max-width: 36rem` is inert here in any case: the widest card this
 * site renders is 342px, measured at 390, and 36rem is 576px.
 *
 * WHAT THIS DOES NOT CHANGE, all re-verified after the change:
 *   the tag row is still pinned to the bottom by `.ezy-chips { margin-top: auto }`, which now
 *     bottom-aligns inside the last track instead of the flex column, same result;
 *   the media slot's ratio-independent fix (Unit V6) is untouched and still measures 1.78 on the
 *     640x400 card;
 *   `.ezy-card-link::after` still stretches over the whole card, because it is positioned against
 *     `.ezy-card`, so whole-card click and text selection are unaffected;
 *   `cursor: pointer` inherits exactly as before.
 *
 * `items-start justify-start` on the article are flex utilities that also mean something to a
 * grid, and both are wanted: `align-items: flex-start` keeps each row's content at the top of its
 * track (which is the alignment), and `justify-content: flex-start` is neutralised by the
 * explicit single `minmax(0, 1fr)` column, which is there so the media box still fills the card's
 * width rather than shrinking to its own content.
 *
 * DEGRADATION. Where `subgrid` is not supported the declaration is dropped and everything else
 * still applies: the card is its own five-row grid, the rows keep their margins, and only the
 * cross-card alignment is lost. That is the first pass's behaviour, which the owner accepted on
 * spacing, so the floor is a state we have already shipped and looked at.
 * ------------------------------------------------------------------------ */

/* THE ROW GAP HAD TO MOVE OFF THE GRID AND ONTO THE CARD. Measured, three times, because the
 * first attempt shipped a broken layout to my own screenshots and the second moved the footer.
 *
 * A subgrid does NOT get to choose the gutters between the tracks it spans: it inherits the
 * parent's. `.ezy-cards` carries `gap-y-20`, so the card's five internal tracks were separated by
 * **80px each**, which put 104px between the thumbnail and the title instead of 24px. `row-gap: 0`
 * on the card was computed as `0px` and ignored, exactly as Grid Level 2 says it should be. Chrome
 * also stopped propagating per-track size contributions in that state, so the parent's resolved
 * tracks read `161.875px 0px 0px 0px 39px` with the title and byline overflowing zero-height
 * tracks. Both symptoms have one cause and one fix.
 *
 * So the 80px separation between rows of cards now lives on the CARD as `margin-bottom`, and the
 * grid's own `row-gap` is zero. That leaves the parent's gutters at 0 inside the card, where the
 * margins on the individual rows do the spacing, and it puts the row separation somewhere a
 * subgrid cannot inherit it. Re-verified in a reduced page first (parent row-gap 0, card margin
 * 80: tracks resolved to `73 0 36 46 125`, four cards' rows all level, 80px between card rows),
 * then on the site.
 *
 * `5rem` is `gap-y-20`, the exact value being replaced, not a new one.
 *
 * NO COMPENSATING NEGATIVE MARGIN, and that was measured rather than reasoned. A negative
 * `margin-bottom` on the container looked obviously necessary to cancel the last row's card
 * margin; it is not, because `.ezy-cards > .ezy-card { height: 100% }` (Unit V4, above) makes the
 * card fill its track, so the trailing margin overflows the container instead of growing it. Added
 * on reasoning, the negative margin pulled the footer up 80px on every one of the four pages at
 * all three viewports. Removed, the clear space between the grid's bottom edge and the footer is
 * 88px at 1024 and 1440 and 80px at 390 -- identical to before this unit, on every page.
 *
 * Scoped to `.ezy-cards.grid`, deliberately. `.ezy-cards` is also on the book single's inert
 * wrapper and on the chapter rail, which is a `flex flex-col gap-3` column where zeroing
 * `row-gap` would collapse a 12px gap that has nothing to do with cards. The compound requires
 * the `grid` class that only the card loop's container carries.
 */
.ezy-cards.grid {
	row-gap: 0;
}

.ezy-cards.grid > .ezy-card {
	margin-bottom: 5rem;
}

.ezy-cards > .ezy-card {
	display: grid;
	grid-template-columns: minmax( 0, 1fr );
	grid-template-rows: subgrid;
	grid-row: span 5;
}

.ezy-card-body {
	display: contents;
}

.ezy-card > .ezy-card-media {
	grid-row: 1;
}

.ezy-card .ezy-card-date {
	grid-row: 2;
}

.ezy-card .ezy-card-link {
	grid-row: 3;
}

.ezy-card .ezy-card-author {
	grid-row: 4;
}

.ezy-card > .ezy-chips {
	grid-row: 5;
}

/* THE TAG ROW: the track pins it now, so the auto margin has to stand down INSIDE the grid.
 *
 * `.ezy-chips { margin-top: auto }` is Unit V4's bottom-pin and it is kept above, because it is
 * what a card rendered outside a grid still needs. Inside the subgrid it fights the alignment it
 * used to provide: an auto margin makes the box shrink-wrap and sit at the BOTTOM of the last
 * track, so a card whose tags wrap to two rows and a card whose tags fit one row bottom-align
 * their boxes and their FIRST chip lines end up 34px apart. Measured at 1024 before this
 * declaration: first-chip spread 34px and 68px on three of the five rows.
 *
 * With the margin at 0 the box stretches to the track, `align-content: flex-start` (V4, above)
 * keeps the chips on the track's first line, and every card in the row starts its tags on one
 * line: spread 0 at all three viewports.
 *
 * The tag row is STILL pinned to the bottom of the card, structurally rather than by a margin:
 * track 5 is the last track, so nothing can come after it, and the card's height is the sum of
 * the five shared tracks. This is the one place where the orchestrator's "keep the auto-margin
 * push" and "the top of the tag row must be equal across the row" cannot both hold, and the
 * alignment is the owner's actual requirement, so it wins. The mechanism it replaces is one
 * declaration away in either direction. */
.ezy-cards > .ezy-card > .ezy-chips {
	margin-top: 0;
}

/* ---------------------------------------------------------------------------
 * 2. Video thumbnail slot.
 *
 * The wrapper carries `aspect-video` from the markup (that utility IS in the
 * build), so the slot is a 16:9 box whether or not an image arrived. That is
 * what keeps the title top edges on one line across a row.
 *
 * The slot is only reserved when at least one card in the SET has a featured
 * image, decided in ezy_academy_loop(). With none attached the cards render
 * compact exactly as before, so a thumbnail job that has not run yet, or has
 * failed outright, cannot leave seventeen empty grey boxes on the page.
 * ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
 * UNIT V6, contract Addendum 35 objective 9. THE RESERVED SLOT NOW ACTUALLY ENFORCES ITS RATIO.
 *
 * The V4/V5 Navigator rejected the slot on one card: "Understanding credit risks" carries a
 * 640x400 thumbnail (1.6:1, not 1.778:1, because Unit T1 correctly cropped letterbox bars off an
 * sddefault fallback), and its media box rendered TALLER than its row-mates, pushing its title
 * down. Reproduced here before touching anything, .verification/unit-v6/before-cards.json:
 *
 *   1440   row 4: media heights 160.88 and 178.75, spread 17.87px, title offset spread 17.87px
 *   1024   row 4: media heights 102.38 and 113.75, spread 11.37px, title offset spread 11.37px
 *   390    one card per row, so the defect is invisible there and always was
 *
 * ROOT CAUSE, and it is not the image. `aspect-video` (aspect-ratio: 16/9) was on the wrapper and
 * was being OVERRIDDEN. The wrapper is a flex item in the card's `flex flex-col`, and a flex item
 * has `min-height: auto`, so it cannot be shorter than its content's minimum. The image was in
 * normal flow, so its intrinsic height at the column width (286 / 1.6 = 178.75px) became a floor
 * and beat the ratio's 160.88px. With the other sixteen sources at exactly 16:9 the two numbers
 * agreed, which is why the slot LOOKED like it was working and why V4's own measurement passed:
 * it compared cards that all happened to share a source ratio.
 *
 * THE FIX IS RATIO-INDEPENDENT, so the next odd-sized thumbnail cannot break a row either:
 *   min-height: 0   removes the flex floor, so aspect-ratio decides the height, always.
 *   overflow: hidden + an absolutely positioned image  crops to fit instead of growing the box.
 *   object-fit: cover  keeps the crop centred rather than squashing the picture. `object-cover`
 *                      is already on the img in markup; it is restated here so this block stands
 *                      on its own if that class is ever dropped. (`object-center` is ABSENT from
 *                      the purged build, Addendum 21, and is not needed: centre is the default.)
 *
 * SCOPED TO THE RESERVED SLOT, deliberately. `.ezy-card-media` is also used by the guide cards and
 * the book card, where the slot is NOT reserved and has no ratio. Absolutely positioning the image
 * there would collapse the wrapper to zero height and take those cards' art off the page, so both
 * rules require `.aspect-video`, which is exactly the marker `ezy_academy_card_media()` adds when
 * it reserves the slot. The old unscoped `height: 100%` was a no-op outside the reserved slot
 * anyway, because there the parent height is auto.
 * ------------------------------------------------------------------------ */

.ezy-card-media.aspect-video {
	min-height: 0;
	overflow: hidden;
	border-radius: 1rem; /* rounded-2xl, so the crop follows the card art's corners */
}

.ezy-card-media.aspect-video > img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Missing image inside a reserved slot. Written here rather than as utilities so
 * the whole placeholder is one documented thing: a flat gray-100 ground and a
 * low-contrast play glyph, which reads as "video, art pending" rather than as a
 * broken image. */
.ezy-card-media-empty {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 1rem; /* rounded-2xl, matching the ring above it */
	background-color: #f3f4f6; /* gray-100 */
}

.ezy-card-media-empty svg {
	width: 2.5rem;
	height: 2.5rem;
	color: #d1d5db; /* gray-300 */
}

/* ---------------------------------------------------------------------------
 * 3. Tag overflow: first few, then "+N" revealing the rest.
 *
 * CSS ONLY, and the full list is in the served HTML at all times. Same rule as
 * the transcript panel: reveal with CSS, never inject with JavaScript, so a
 * crawler reads every tag and its link.
 *
 * The toggle is a real `<input type="checkbox">` kept in the accessibility tree
 * by `sr-only` (which clips it rather than removing it, so it stays focusable),
 * with a `<label for>` as the visible "+N" chip. That gives three independent
 * ways in, which is what the ruling asks for:
 *
 *   hover  ->  `.ezy-chips-of:hover`, a pointer over the chip or the panel.
 *   focus  ->  `.ezy-chips-toggle:focus`, i.e. Tab to it. Reveal on focus alone,
 *              so a keyboard user does not have to press anything to read it.
 *   tap    ->  `.ezy-chips-toggle:checked`, i.e. tapping the label toggles the
 *              checkbox. This is the touch path, where :hover never fires, and
 *              it is also Space on the keyboard for a sticky reveal.
 *
 * `.ezy-chips` loses 8882's `overflow-hidden` in the markup, because the panel is
 * absolutely positioned above the row and an ancestor clip would eat it.
 * ------------------------------------------------------------------------ */

/* Reserve the tag band so the first row of tags lands on the same line across a
 * row of cards even when one card's tags wrap. `align-content: flex-start` is
 * load-bearing: `.ezy-chips` carries `items-center`, and without this a single
 * row of chips would be centred in the reserved band instead of sitting on its
 * first line. `:not(:empty)` so the eight untagged webinars reserve nothing. */
.ezy-card .ezy-chips:not(:empty) {
	align-content: flex-start;
	row-gap: 0.375rem;
}

/* Multi-column only. Below 1024px `.ezy-cards` is `grid-cols-1`, so there is exactly one card
 * per grid row and nothing to align a chip band with: reserving one there would only print dead
 * space into every card on a phone. (Unit V4 wrote this note against the title reservation as
 * well; Unit V7 removed that one, so the reasoning now lives here, where the last reservation
 * is.) */
@media ( min-width: 1024px ) {
	.ezy-card .ezy-chips:not(:empty) {
		min-height: calc( ( var( --ezy-chip-rows, 2 ) * 1.75rem ) + ( ( var( --ezy-chip-rows, 2 ) - 1 ) * 0.375rem ) );
	}
}

/* The panel is anchored to the CHIPS ROW, not to the "+N" chip, which is why this row is the
 * positioned ancestor and `.ezy-chips-of` deliberately is NOT.
 *
 * MEASURED, not assumed. Anchored to the chip, the panel opened from wherever the "+N" happened to
 * sit and ran off the right of the page: with every panel forced open, `scrollWidth` was 1450 on a
 * 1440 viewport and 458 on a 390 one, i.e. the page grew a horizontal scrollbar. Anchored to the
 * row at `left: 0` with `max-width: 100%`, the panel can never be wider than the card's own content
 * box, so it cannot overflow at any width. Re-measured after the change: 1440/1440 and 390/390.
 *
 * `:hover` still reaches the panel from the chip because :hover applies to an element AND its DOM
 * ancestors, and the panel is still a DOM child of `.ezy-chips-of` however it is positioned. */
.ezy-card .ezy-chips {
	position: relative;
}

.ezy-chips-of {
	display: inline-flex;
	align-items: center;
}

/* The "+N" chip. Same shape as a tag chip, deliberately quieter: it is an
 * affordance, not a destination, so it is not a link and does not compete with
 * the real tags for a crawler's attention. */
.ezy-chips-more {
	position: relative;
	z-index: 10;
	padding: 0.375rem 0.75rem;
	border-radius: 9999px;
	background-color: #f3f4f6; /* gray-100 */
	color: #6b7280;            /* gray-500 */
	font-weight: 500;
	cursor: pointer;
	user-select: none;
}

.ezy-chips-of:hover .ezy-chips-more,
.ezy-chips-toggle:focus ~ .ezy-chips-more,
.ezy-chips-toggle:checked ~ .ezy-chips-more {
	background-color: #e5e7eb; /* gray-200 */
	color: #374151;            /* gray-700 */
}

/* Keyboard focus has to be visible on a chip whose input is clipped out of
 * sight, or a Tab stop appears to do nothing. */
.ezy-chips-toggle:focus ~ .ezy-chips-more {
	outline: 2px solid #1f2937; /* gray-800 */
	outline-offset: 2px;
}

/* The panel. `bottom: 100%` with its own bottom padding gives the visual gap
 * without a dead zone that would break the hover chain on the way in.
 * `.bottom-full` is absent from the build, hence the longhand. */
.ezy-chips-rest {
	position: absolute;
	bottom: 100%;
	left: 0;
	z-index: 30;
	display: none;
	flex-wrap: wrap;
	gap: 0.375rem;
	width: max-content;
	max-width: 100%;
	margin-bottom: 0.5rem;
	padding: 0.625rem;
	border: 1px solid #e5e7eb; /* gray-200 */
	border-radius: 0.5rem;
	background-color: #fff;
	box-shadow: 0 10px 15px -3px rgb( 0 0 0 / 0.1 ), 0 4px 6px -4px rgb( 0 0 0 / 0.1 );
}

.ezy-chips-of:hover .ezy-chips-rest,
.ezy-chips-toggle:focus ~ .ezy-chips-rest,
.ezy-chips-toggle:checked ~ .ezy-chips-rest {
	display: flex;
}

/* `.ezy-chips` sets `whitespace-nowrap`, which is right for a chip in the row: a tag name must not
 * break mid-name there. Inside the panel it is wrong, because a tag longer than a narrow card
 * would push the panel past `max-width: 100%` and put the overflow back. */
.ezy-chips-rest a {
	min-width: 0;
	overflow-wrap: break-word;
	white-space: normal;
}

/* Lift the card being read above its neighbours. Every `.ezy-card` is
 * `position: relative` at `z-index: auto`, so without this a card painted later
 * in the grid would cover the panel of a card painted earlier. `:focus-within`
 * covers the keyboard path, where there is no :hover. */
.ezy-cards > .ezy-card:hover,
.ezy-cards > .ezy-card:focus-within {
	z-index: 2;
}

/* ---------------------------------------------------------------------------
 * 4. Hub band heading row and its "see all" call to action.
 *
 * Type-agnostic on purpose: templates/page-academy.php renders this for EVERY
 * band from the registered academy post types, so guides pick it up the moment
 * there are more guides than a band shows, with no edit to markup or CSS.
 * `.items-baseline` is absent from the build, hence the longhand.
 * ------------------------------------------------------------------------ */

.ezy-band-head {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: 0.5rem 1rem;
}

.ezy-band-cta {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	color: #374151; /* gray-700, the body colour */
	font-size: 0.875rem;
	text-underline-offset: 3px;
	text-decoration: underline;
}

.ezy-band-cta:hover {
	color: #ea580c; /* orange-600, the academy accent */
}

.ezy-band-cta svg {
	width: 1rem;
	height: 1rem;
	transition: transform 200ms;
}

.ezy-band-cta:hover svg {
	transform: translateX( 2px );
}

@media ( prefers-reduced-motion: reduce ) {
	.ezy-band-cta svg {
		transition: none;
	}

	.ezy-band-cta:hover svg {
		transform: none;
	}
}

/* ---------------------------------------------------------------------------
 * UNIT V6, contract Addendum 35 objective 10. THE BAND UNDER THE NAVBAR, one rule for every
 * academy archive. Owner: "the gap between menu and content in this page needs to be
 * significantly smaller / do a quick audit in different pages where this could happen and make
 * it uniform." Reported against /academy/guides/.
 *
 * WHAT THE AUDIT ACTUALLY FOUND, and it changes the fix. All five academy page types do share the
 * same spacer, `<div class="relative pt-28 lg:pt-40">` at the end of templates/header.php, 160px
 * at lg. But the EFFECTIVE gap is not shared, because two of the five pull themselves back up
 * under the navbar on purpose and one does not. Measured, .verification/unit-v6/before-gap.json,
 * clear space between the navbar bottom edge and the first content at 1440:
 *
 *   archives (guides, books, webinars)   124px   no compensating negative margin
 *   hub                                   -      hero card is `-mt-20`, DELIBERATELY tucked
 *                                                under the navbar (page-academy.php line 122)
 *   webinar single                        52px   hero is `-mt-[160px]`, Unit V5 tuned this
 *   guide and book singles                68px   same, and also already tuned
 *
 * So the excess is on the archives alone, and shrinking the shared spacer for everyone would drag
 * the hub's hero 64px further under the navbar and undo V5's work on all three singles. ONE rule,
 * scoped to `body.archive`, is both the smallest fix and the uniform one: it covers guides, books,
 * webinars and any academy archive added later, because the class comes from WordPress and not
 * from a template.
 *
 * VALUE: 6rem, which is exactly what V5 landed on for the singles' hero. No second number is
 * invented. It puts the archives at 96 + 40 - 76 = 60px of clear space, between the webinar's 52
 * and the guide and book's 68, so the whole academy now reads the same.
 *
 * lg ONLY, following V5's ruling for the same reason: at 390 the navbar is full width and ends at
 * y=60 against a 112px `pt-28` spacer, so there is no excess to remove and taking any out would
 * crowd the header. 390 is unchanged and deliberately so.
 *
 * WHY IT OVERRIDES A UTILITY. The spacer lives in templates/header.php, which this unit must not
 * edit, and it carries no first-party hook. `.pt-28.lg\:pt-40` as a COMPOUND is unambiguous:
 * exactly one element per academy page matches it, verified on all seven pages measured
 * (spacerCount 1 everywhere). Specificity 0,3,1 beats the utility's 0,1,0 and the academy layers
 * load after tailwind.css, so no !important is needed.
 * ------------------------------------------------------------------------ */

@media ( min-width: 1024px ) {

	body.ezy-academy.archive .pt-28.lg\:pt-40 {
		padding-top: 6rem;
	}
}


/* ------------------------------------------------------------------------
 * UNIT S3: cancel the LAST row's card margin. Corrects a Unit V7 regression.
 *
 * V7 replaced the grid's `row-gap` with a per-card `margin-bottom`, further up this file:
 *
 *     .ezy-cards.grid       { row-gap: 0 }
 *     .ezy-cards.grid > .ezy-card { margin-bottom: 5rem }
 *
 * That is the right way to get an 80px row gap while `grid-template-rows: subgrid` is in play.
 * But a margin is applied to EVERY card, including the ones in the final row, so 80px of it stays
 * inside the grid container with nothing below it to separate. Measured `slack` (grid container
 * bottom minus the lowest card's bottom) was 80px on the hub's two bands and on all three
 * archives, at 1440, 1024, 768 and 390 -- twenty measurements, one value. Consequences a reader
 * sees: 168px between the lowest card and the footer where the pre-V7 spacing was 88, and on the
 * hub 112px between the guides band's last card and the next band's heading where it was 32.
 *
 * A negative bottom margin of the same size on the CONTAINER removes exactly the overhang and
 * nothing else. The container is the right place: the cards must keep their margin, because that
 * margin IS the row gap between rows 1..n-1.
 *
 * WHY THIS FIX WAS ONCE REJECTED, so it is not reverted a third time. V7 found it, applied it,
 * saw "the footer moved up 80px" and backed it out as an overcorrection. Its gate measured the
 * distance from the GRID CONTAINER's bottom to the footer, which is the parent's padding and
 * reads the same 88px whether the overhang is there or not. That gate could not fail, so it could
 * not see that the 80px it was protecting was the defect. Gate any change to these three rules on
 * `slack` and on lowest-CARD-to-footer, never on grid-bottom-to-footer.
 *
 * Confirmed against assets/css/academy-archives.css.before: neither of V7's two rules existed
 * before V7, so this corrects V7's own regression rather than an inherited one.
 *
 * Appended rather than edited in place because this file is shared and append-only; overwriting
 * it has already cost this programme once.
 * ------------------------------------------------------------------------ */

.ezy-cards.grid {
	margin-bottom: -5rem;
}

/* ---------------------------------------------------------------------------
 * UNIT U1: the footer column headings are h2 now, so their type comes from a
 * class instead of a tag. APPENDED ONLY; nothing above this line was edited.
 *
 * WHY IT IS HERE AND NOT IN academy-chrome.css. That file holds `.footer h5`
 * (line 181) and it belongs to another unit, which this unit may read and not
 * write. This file is appended to instead, and it is the right file to use for
 * it: `ezy_academy_style_layers()` loads it on EVERY academy request, not only
 * on archives, and it loads AFTER academy-chrome.css. Confirmed served on
 * /academy/, /academy/books/ and a webinar single. The `.footer h5` rule is now
 * dead and is left in place; deleting it is a one-line job for whoever next owns
 * that file.
 *
 * WHY EACH DECLARATION IS HERE. All six are lifted from `.footer h5`, which the
 * h2 no longer matches. `color` is the one that is not merely cosmetic:
 * academy-chrome.css:426 has `h1, h2, h3, h4, h5, h6 { color: #111827 }`, and on
 * the #0D1232 footer that renders near-black on near-black. `.footer h5` used to
 * out-specify it at 0,1,1; `.footer .ezy-footer-heading` out-specifies it at
 * 0,2,0. That is deliberate: the rule must win on SPECIFICITY, because relying
 * on load order would break the moment another layer is registered before this
 * one.
 *
 * `font-family` is NOT repeated. It comes from the `h1..h6` element list in
 * academy-fonts.css, which includes h2, so Jost is inherited unchanged. Adding
 * it here would be a second place to edit the same decision.
 *
 * The tailwind preflight zeroes heading margins (`...h1,...h6...{margin:0}`) and
 * sets `font-size:inherit; font-weight:inherit`, which is why all three of
 * margin, size and weight have to be stated rather than left to the tag.
 *
 * MEASURED, 1440 / 1024 / 768 / 390 on the hub, an archive, a by-tag page and a
 * single: 13px, 700, line-height 19.5px, rgb(255,255,255), letter-spacing
 * 0.52px, margin 0 0 14px 0, uppercase, Jost, box height 19.5. Every value is
 * identical to the h5 before the change.
 * ------------------------------------------------------------------------- */

.footer .ezy-footer-heading {
	color: #fff;
	margin: 0 0 14px;
	font-size: 13px;
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}
