/* =====================================================================
   Shared card-level link + hover-zoom image — CUHK 2026 style guide.

   Verified against the actual style guide CSS: the mechanism is
   identical everywhere it's used — the whole card is wrapped in a
   single <a>, and its image scales up on hover — but the zoom amount
   varies per pattern:
     a.card:hover   .card__img img   { transform: scale(1.1);  }
     a.event:hover  .event__img img  { transform: scale(1.05); }
     a.result:hover .result__img img { transform: scale(1.05); }

   Confirmed consumers (reference guide shows a real <a>-wrapped card
   with this hover effect): News List, Card List 1, Card List 2,
   Search Results List.

   NOT yet applied to Contact List / Avatar List 1 / Avatar List 2 —
   the reference guide's own sample markup for those uses plain <div>
   items with no href and no hover-zoom CSS, so there's no confirmed
   link destination to point at. Apply this utility to them only after
   confirming what (if anything) each item should link to.

   Usage: wrap the whole card in <a class="cuhk-card-link ...">, put
   class "cuhk-hover-zoom-img" on the <img> (or its wrapper), and set
   --cuhk-hover-zoom on the card if it needs something other than the
   1.05 default (e.g. 1.1 to match .card__img above).
   ===================================================================== */
.cuhk-card-link {
	display: block;
	text-decoration: none;
	color: inherit;
}
.cuhk-hover-zoom-img {
	overflow: hidden;
}
.cuhk-hover-zoom-img img {
	transition: transform 0.3s;
}
.cuhk-card-link:hover .cuhk-hover-zoom-img img {
	transform: scale(var(--cuhk-hover-zoom, 1.05));
}

/* Same effect, triggered by hovering the card WITHOUT requiring a
   .cuhk-card-link wrapper (internal comments #4.1, 2026-07-26) - needed
   for cards that want hover-zoom without becoming a full clickable card
   (e.g. "International Rankings", which per spec should stay
   non-clickable). The .cuhk-card-link rule above still exists and still
   applies for cards that ARE link-wrapped; this is additive, not a
   replacement.

   Requires a DEDICATED class (cuhk-hover-card) on the card wrapper, not a
   generic WP block class like .wp-block-column or .wp-block-group - both
   were tried and both failed the same way in testing: WordPress reuses
   those exact classes for page-layout-scale wrappers too (a whole page's
   content-width column, an entire multi-card section's outer group), with
   no way to distinguish "this wraps one card" from "this wraps everything"
   by class name alone. Confirmed concretely: with .wp-block-column
   included, hovering one Programme card zoomed EVERY card in the grid at
   once, because a page-layout .wp-block-column further up the tree also
   :has() (and now :hover-ed) all of them. A dedicated class has no such
   ambiguity - it only ever means "this is a card," so whoever applies it
   (a migration, or by hand via Additional CSS Class(es) alongside
   cuhk-hover-zoom-img) is making that boundary explicit. */
.cuhk-hover-card:hover .cuhk-hover-zoom-img img {
	transform: scale(var(--cuhk-hover-zoom, 1.05));
}

/* BOTH classes on the SAME element. Some block types have no separate
   image sub-block to tag - a core/cover's background IS the image, and a
   core/media-text's image is part of the block rather than a nested image
   block - so for those, both classes land on one element. That breaks the
   rule above outright: a descendant combinator requires
   .cuhk-hover-zoom-img to be a genuinely separate, nested element from
   whatever matched :hover, and a node is never its own descendant
   (confirmed by testing - with both classes on one element, the hover did
   nothing at all).

   Deliberately generic (any descendant <img>) rather than one rule per
   block type keyed to its internal class (.wp-block-cover__image-background,
   .wp-block-media-text__media, ...) - those cards only ever contain the
   one image, and a generic rule means the client's own self-service use
   (per internal comments #4.1's remark: "create a css class... so I can
   use this class for the image block, when I need the effect") works on
   whatever block they put both classes on, without needing a new rule
   shipped each time. */
.cuhk-hover-card.cuhk-hover-zoom-img:hover img {
	transform: scale(var(--cuhk-hover-zoom, 1.05));
}

/* 4.3: whole card clickable, without a link "except the categories button
   inside" (internal comments #4.3). Cards here already contain a real,
   separate <a> - e.g. a post-terms "categories button" pointing somewhere
   else entirely - so literally wrapping the whole card in ANOTHER <a> is
   invalid HTML (browsers silently mangle/auto-close nested anchors).
   Standard "stretched link" technique instead: pick ONE of the card's
   existing links (whichever already points at the card's real
   destination - typically the title) and mark it
   .cuhk-card-stretched-link; its ::after is stretched via position:
   absolute + inset:0 to cover the whole card, so a click ANYWHERE on the
   card activates that one real link. .cuhk-hover-card doubles as the
   positioning container (position: relative) - already applied to every
   card from 4.1, no extra class needed there.

   Anything that must stay independently clickable on top of that overlay
   (the categories button 4.3 explicitly calls out, or any other nested
   link/button) gets .cuhk-card-exempt-link, raised above the stretched
   overlay with a higher z-index - clicks land on the real exempt element,
   not the stretched one, because it's now the topmost thing at that
   point on screen.

   The two ::after selectors below handle the class being on the <a>
   itself OR a wrapping block (e.g. core/post-title's className goes on
   the <h3>, not the <a> inside it) - self-service application (Additional
   CSS Class(es)) might do either. The self-targeting one is scoped to
   `a.cuhk-card-stretched-link` specifically (not a bare
   `.cuhk-card-stretched-link`) - confirmed by testing that a bare version
   also matched the WRAPPING element (the <h3>) when the class landed
   there, generating a second ::after that isn't a link itself. That
   second box paints on top of the real, nested <a>'s ::after (an
   element's own generated content comes after its children in paint
   order), so every click landed on the non-clickable wrapper's copy
   instead of the actual anchor - the whole card was hit-testable but
   fully inert. Requiring the host element to literally be an <a> removes
   that duplicate entirely. */
.cuhk-hover-card {
	position: relative;
}
/* card-click.js's delegated click (Cover-block cards' photo "dead zone")
   is invisible to the browser's own cursor heuristics - it only shows a
   pointer automatically over real <a>/<button> elements, not over a plain
   <img> that JS happens to make clickable. Scoped to :has(a stretched
   link) specifically, not every .cuhk-hover-card - this class is shared
   with hover-only cards (e.g. "International Rankings", deliberately
   non-clickable per spec), which must keep the default cursor. */
.cuhk-hover-card:has(.cuhk-card-stretched-link) {
	cursor: pointer;
}
/* A Cover block's own background layers - the aria-hidden="true" dim
   overlay (.wp-block-cover__background) and the background <img> itself
   (.wp-block-cover__image-background) - are both position:absolute and sit
   as direct siblings of .wp-block-cover__inner-container, the same
   stacking level as our stretched link's ::after once it bubbles up
   through inner-container (which, being position:relative + z-index:auto
   itself, doesn't isolate it into its own context). Confirmed by testing
   (giving .cuhk-hover-card an explicit z-index to force a local stacking
   context did NOT change the outcome - elementFromPoint at a card's center
   hit the dim span, then after excluding that, the image, instead of the
   link), so this isn't a z-index-ordering problem. Both layers are purely
   decorative already (the dim span is aria-hidden; the real, accessible
   content lives in inner-container) - correct fix is letting clicks pass
   through both, not fighting over paint order. Scoped to .cuhk-hover-card
   only, consistent with the rest of this file. */
.cuhk-hover-card .wp-block-cover__background,
.cuhk-hover-card .wp-block-cover__image-background {
	pointer-events: none;
}
/* WP core gives .is-layout-constrained groups position: relative by
   default (supports alignwide/alignfull children breaking out via
   negative margins) - confirmed by testing this is what actually broke
   the stretched link below: the card's title sits inside one of these
   groups, so ::after's "nearest positioned ancestor" resolved to that
   inner group instead of the card, and the overlay only covered the
   group's own (much smaller) box, not the whole card. Neutralized only
   inside .cuhk-hover-card, not site-wide, since alignwide/alignfull
   content elsewhere on the site still needs the real behavior - nothing
   inside these specific cards uses either. */
.cuhk-hover-card .is-layout-constrained {
	position: static;
}
/* NOT neutralized (unlike .is-layout-constrained above), despite the same
   "becomes the ::after's containing block instead of the full card"
   symptom - .wp-block-cover__inner-container's position:relative isn't
   cosmetic default like is-layout-constrained's is. WP core's own cover
   CSS gives it an explicit z-index:1 (core cover/style.min.css) precisely
   so its text/button paint ABOVE the cover's absolutely-positioned photo -
   z-index only takes effect on a positioned element, so forcing
   position:static here (an earlier, reverted version of this rule) silently
   disabled that z-index, dropping the text back to normal in-flow paint
   order, which comes BEFORE (visually behind) the positioned photo -
   confirmed the hard way: shipped to production, made the card's title
   invisible, buried under its own background image.
   inner-container is also deliberately width:auto/shrink-wrapped (WP core,
   for every is-position-* variant, not just bottom-left) so the outer
   cover's flex alignment (align-items/justify-content) can place it in a
   corner - stretching it to the full card to fix the ::after sizing would
   fight that same mechanism for every position variant sitewide.
   Net effect: the stretched-link overlay for Cover-block cards only
   covers the text/button panel, not the whole photo - a smaller but still
   real, working clickable area. Making the whole photo clickable too would
   need a non-CSS approach (a small JS click-delegation on
   .cuhk-hover-card), not attempted here. */
a.cuhk-card-stretched-link::after,
.cuhk-card-stretched-link a::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
}
.cuhk-card-exempt-link,
.cuhk-card-exempt-link a {
	position: relative;
	z-index: 2;
}
/* UI Issues v2, #2.2: Programme List cards in a 2-column grid query loop
   (Face-to-Face/Open/Professional Certificate Programmes etc.) don't
   visually match height when one card's title/description wraps to more
   lines than its row sibling's. CSS Grid's own default align-items:stretch
   already makes each <li> itself the full row height (confirmed via
   computed style - this is native grid behavior, not something to add),
   but nothing below that inherits it: every block between the <li> and
   .cuhk-hover-card is a plain div with height:auto, which shrinks to its
   own content regardless of how tall its stretched grid-item ancestor is.
   Cascading height:100% down through each of those levels lets a shorter
   card's grey background - and its image, since wp:post-featured-image
   already sets height:100% of ITS OWN column - stretch to match the
   taller sibling instead of stopping short.
   Scoped to `.is-layout-grid > li` specifically (not every .cuhk-hover-card
   site-wide) - plenty of other cards using this same class intentionally
   aren't in a stretchable grid, e.g. the homepage's Cover-block "Our
   Programmes" cards, which must keep their own content-driven height. */
.wp-block-post-template.is-layout-grid > li {
	height: 100%;
}
.wp-block-post-template.is-layout-grid > li > .wp-block-group,
.wp-block-post-template.is-layout-grid .cuhk-hover-card {
	height: 100%;
}
/* Real remaining gap (client-reported, confirmed via screenshot on
   Face-to-Face/PCP): even with the card and its image COLUMN now
   correctly stretched to the row's full height above, the <img> itself
   still fell a little short. Root cause is WordPress core's own
   post-featured-image render output, not this project's CSS: with
   `{"aspectRatio":"1","width":"100%","height":"100%"}` all set on the
   block, core prints a self-conflicting inline style -
   `aspect-ratio:1;width:100%;height:100%;width:auto;object-fit:cover` -
   confirmed directly on this exact page's HTML. The trailing width:auto
   wins (later wins within one style attribute) and forces the image to a
   SQUARE box computed from its own height (aspect-ratio:1), rather than
   actually filling the column's real (non-square) width - shorter or
   taller than the column depending on which is smaller, however that
   happens to resolve. object-fit:cover already crops/fills correctly on
   its own once both dimensions are genuinely 100% - the aspect-ratio is
   redundant here and actively fighting it. Overriding both dimensions
   with !important (needed - inline styles otherwise win) restores the
   intended fill-the-column behavior regardless of that core quirk. */
.wp-block-post-template.is-layout-grid .cuhk-hover-card img {
	width: 100% !important;
	height: 100% !important;
}
/* The rule above alone still left a small (~8px) gap at 2-column desktop
   widths, though it happened to look right at 1-column tablet widths by
   coincidence - confirmed by measuring the actual box tree: img's
   height:100% needs a DEFINITE ancestor height to resolve against, but
   its parents in between (<figure class="wp-block-post-featured-image">
   and the <a> inside it) are plain block boxes with no height of their
   own (height:auto, sized to the image's content) - a circular
   dependency CSS resolves by falling back to `auto` on the img's
   height:100%, which is why aspect-ratio:1 (from the image's own
   attributes) kept winning and forcing a square. At the single-column
   tablet width, the image's own natural square size happened to already
   be the row's tallest item, so this same bug was invisible there by
   coincidence, not because it was actually fixed. Cascading height:100%
   through figure and its inner <a> too gives the img a genuinely
   definite basis at every width. */
.wp-block-post-template.is-layout-grid .cuhk-hover-card figure,
.wp-block-post-template.is-layout-grid .cuhk-hover-card figure > a {
	height: 100%;
}
