/**
 * Catalog product grid — 16:10 preview cards.
 *
 * Marker class: .nou-catalog-home
 * Depends on:   assets/css/catalog.css  (registered as a style dependency, so
 *               the shell always prints first — chip, mono font and the
 *               neutral scale come from there)
 *
 * Ported from design-lab/css/wf-c-cards.css with the light overrides from
 * wf-c-light.css already folded in. Markup comes from
 * inc/catalog.php → [nouthemes_catalog_grid].
 *
 * ─────────────────────────────────────────────────────────────────────
 * NO MATTE FRAME. The dark wireframe wrapped each preview in a 10px inset on
 * its own surface, because on #0b0f15 a near-white screenshot needs an
 * intermediate step or eight cards read as a light box. That device does not
 * transfer to the light mode, and the reason is arithmetic:
 *
 *   - page #ffffff with frame #f8fafc → the frame is DARKER than the page,
 *     a 1.046:1 step. The product sits in a dent instead of lifting.
 *   - Inverting it (off-white page, white frame) is capped by text contrast:
 *       page #f7f8fa → muted and accent both 4.55:1  (only just AA)
 *       page #f4f6f8 → both 4.46:1                   (fails)
 *     The lightest page tint that still passes AA yields a 1.063:1 step —
 *     still invisible.
 *
 * With this palette the light mode cannot build depth from tone, because
 * `muted` and `accent` already sit on the AA threshold against white and
 * there is no room left to darken the page. So it separates with a LINE: no
 * inset, no frame fill, the image inside a single 1px border.
 *
 * If a tinted page is ever wanted here, `primary` has to move darker than
 * #0066ff to buy back the AA margin. That is a brand-colour decision and
 * belongs in theme.json, not in this file.
 * ─────────────────────────────────────────────────────────────────────
 *
 * Three ratio decisions carried over unchanged:
 *  1. The 16:10 box lives on the WRAPPER, never on <img>. The wrapper exists
 *     even when a product has no image, so the grid cannot go ragged.
 *     assets/css/home.css got this right; assets/css/woocommerce.css still
 *     puts the ratio on the img.
 *  2. `object-position: top` — centre-cropping a screenshot loses its hero.
 *  3. 16:10 is the browser-window ratio, so the image reads as "a website".
 */

/* ================================================================== *
 * GRID
 * ================================================================== */

.nou-catalog-grid {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	/*
	 * Asymmetric rhythm: a tight column gutter so the previews read as one
	 * set, a generous row gap so the text block under each card breathes.
	 * No border-top here — the section head already draws the rule, and two
	 * parallel lines 50px apart read as a mistake.
	 */
	gap: clamp(34px, 3.4vw, 46px) 24px;
	align-items: start;
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (max-width: 900px) {
	.nou-catalog-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 560px) {
	.nou-catalog-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.nou-catalog-empty {
	margin: 0;
	padding-block: var(--wp--preset--spacing--medium, 60px);
	font-family: var(--wp--preset--font-family--mono);
	font-size: 12px;
	letter-spacing: var(--wp--custom--tracking--wide);
	text-transform: uppercase;
	text-align: center;
	color: var(--wp--custom--neutral--600);
}

/* ================================================================== *
 * CARD
 * ================================================================== */

.nou-catalog-card {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

/*
 * CONCENTRIC RADII: outer radius = inner radius + padding. Padding is 0 in
 * light mode, so the two radii must be EQUAL. Give the frame a larger radius
 * than the image and the frame's curve cuts inside the image's corner, leaving
 * four nubs that look like a render bug.
 */
.nou-catalog-card__frame {
	position: relative;
	display: block;
	padding: 0;
	/*
	 * neutral 300, not 150. This is now the only thing separating a white
	 * screenshot from a white page, so it carries the whole job.
	 */
	border: 1px solid var(--wp--custom--neutral--300);
	border-radius: var(--wp--custom--radius--md);
	background: transparent;
	transition: transform 320ms var(--wp--custom--easing--out),
		border-color 240ms var(--wp--custom--easing--base),
		box-shadow 320ms var(--wp--custom--easing--base);
}

/*
 * `overflow: hidden` keeps the hover zoom INSIDE the frame: the image grows
 * while the box does not, so there is no reflow and neighbouring cards hold
 * still.
 *
 * No inset hairline here, unlike the dark mode. The frame border already sits
 * flush against the image edge; a second 1px line on top of it just renders as
 * an uneven 2px edge.
 */
.nou-catalog-card__media {
	position: relative;
	display: block;
	width: 100%;
	aspect-ratio: 16 / 10;
	overflow: hidden;
	border-radius: var(--wp--custom--radius--md);
	background: var(--wp--custom--neutral--100);
}

.nou-catalog-card__media img {
	display: block;
	width: 100%;
	height: 100%;
	max-width: none;
	object-fit: cover;
	object-position: top;
	border-radius: inherit;
	transition: transform 520ms var(--wp--custom--easing--out);
}

/* ------------------------------------------------------------------ *
 * PRODUCT TYPE CHIP — top-left, over the image.
 * ------------------------------------------------------------------ */

/*
 * COLOURS HERE ARE HARD-CODED ON PURPOSE, both fill and text.
 *
 * The chip's backdrop is a user-uploaded image plus a fixed black overlay. It
 * has nothing to do with the page palette, so it must not read from it. An
 * earlier pass used the top of the neutral scale for the text; that scale
 * inverts between modes, so in light mode it resolved to #1f2937 — dark text
 * on a dark overlay, measured at 1.74:1 on a white screenshot and 1.58:1 over
 * a pale hero, against the 4.5:1 floor for small text.
 *
 * Hard white gives 8.45:1 over a white screenshot and 9.29:1 over a pale hero
 * — correct in both modes with no override layer.
 *
 * The overlay is 70%, not 62%: at 62% even neutral-800 text only reaches
 * 4.15:1.
 */
/*
 * inset 18px, the same figure in both colour modes. In the dark mode that is
 * the frame's 10px padding plus 8px of margin inside the image; here the
 * padding is 0, so all 18px sits inside the image. Kept identical on purpose —
 * the chip should not shift position when the mode changes.
 */
.nou-catalog-card__platform {
	position: absolute;
	z-index: 2;
	top: 18px;
	left: 18px;
	border-color: transparent;
	background: color-mix(in srgb, #000 70%, transparent);
	backdrop-filter: blur(6px);
	-webkit-backdrop-filter: blur(6px);
	color: #ffffff;
}

/* ------------------------------------------------------------------ *
 * TEXT
 * ------------------------------------------------------------------ */

.nou-catalog-card__body {
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.nou-catalog-card__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
}

/*
 * 18px, not 16px. The page title runs to 58px and this meta line is 11px; at
 * 16px the product name has no rung of its own on the ladder. 18px separates
 * it from the meta line and makes it the page's second anchor.
 */
/*
 * MUST NOT be positioned. The whole-card click target is
 * `.nou-catalog-card__name a::after { position: absolute; inset: 0 }`, which
 * resolves against the nearest positioned ancestor. That has to be
 * `.nou-catalog-card` (position: relative), so the overlay covers the image and
 * the body — not just the title. Give `__name` `position: relative` and the
 * overlay shrinks to the title box: the image silently stops being clickable.
 */
.nou-catalog-card__name {
	display: flex;
	align-items: baseline;
	gap: 0.28em;
	margin: 0;
	font-size: var(--wp--preset--font-size--small, 18px);
	font-weight: var(--wp--custom--font-weight--medium);
	letter-spacing: var(--wp--custom--tracking--snug);
	line-height: 1.3;
	color: var(--wp--custom--neutral--900);
}

.nou-catalog-card__name a {
	color: inherit;
	text-decoration: none;
}

/* Stretched link: the whole card is clickable, but assistive tech still sees
 * exactly one link. */
.nou-catalog-card__name a::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 3;
}

/*
 * The third appearance of the caret motif: wordmark `nouthemes_`, page title
 * `…templates▮`, and now each card on hover. It is 0 wide at rest so the name
 * never shifts when it appears.
 */
.nou-catalog-card__caret {
	flex: none;
	width: 0;
	height: 0.92em;
	background: var(--wp--custom--accent--strong);
	transition: width 220ms var(--wp--custom--easing--out);
}

/*
 * Price is not metadata. An earlier pass styled it like the updated-on date;
 * on a product card the price is what decides the purchase, so it sits at the
 * top colour step and does not brighten on hover — there is no reason a price
 * should need a hover to be legible.
 *
 * `get_price_html()` is used verbatim so currency, sale and range formatting
 * stay WooCommerce's job; the nested spans inherit from here.
 */
.nou-catalog-card__price {
	font-family: var(--wp--preset--font-family--mono);
	font-size: 14px;
	color: var(--wp--custom--neutral--900);
	white-space: nowrap;
}

.nou-catalog-card__price del {
	margin-right: 6px;
	color: var(--wp--custom--neutral--600);
}

.nou-catalog-card__price ins {
	text-decoration: none;
}

.nou-catalog-card__price .woocommerce-Price-amount,
.nou-catalog-card__price bdi {
	font-family: inherit;
	font-size: inherit;
	color: inherit;
}

/*
 * Two to four words, no sentence, no full stop — the answer to "what is this
 * for". Fixed at one line so the card rhythm stays exact. The marketing
 * sentence it replaced was a variable-height block nobody reads while scanning
 * thirty products.
 *
 * The platform is deliberately absent: the section heading already says
 * WordPress, so repeating it in every card of the WordPress section is noise.
 * Three signals, no overlap — platform in the section head, product type in
 * the chip, use case here.
 */
.nou-catalog-card__meta {
	margin: 0;
	font-family: var(--wp--preset--font-family--mono);
	font-size: 11px;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--wp--custom--neutral--600);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* ================================================================== *
 * HOVER / FOCUS
 *
 * Three movements at THREE DIFFERENT SPEEDS — that mismatch is what makes it
 * feel handled rather than mechanical: the frame lifts in 320ms, the image
 * zooms over 520ms, the caret catches up at 220ms.
 *
 * The frame is never scaled, only the image inside it: the box keeps its size
 * so the border stays crisp and the grid does not shift.
 *
 * On white, the darker border is the primary signal and the shadow is
 * secondary. The dark mode uses black at 55%; the same value here draws a pool
 * of ink and makes the card look glued on. 10% is the ceiling.
 * ================================================================== */

@media (hover: hover) {
	.nou-catalog-card:hover .nou-catalog-card__frame {
		transform: translateY(-6px);
		border-color: var(--wp--custom--neutral--500);
		box-shadow: 0 18px 40px rgb(0 0 0 / 0.1);
	}

	.nou-catalog-card:hover .nou-catalog-card__media img {
		transform: scale(1.04);
	}

	.nou-catalog-card:hover .nou-catalog-card__caret {
		width: 0.14em;
	}
}

/* Keyboard gets the same signal as the mouse, plus an accent border. */
.nou-catalog-card:focus-within .nou-catalog-card__frame {
	border-color: var(--wp--custom--accent--strong);
	box-shadow: 0 18px 40px rgb(0 0 0 / 0.1);
}

.nou-catalog-card:focus-within .nou-catalog-card__caret {
	width: 0.14em;
}

.nou-catalog-card__name a:focus-visible {
	outline: 2px solid var(--wp--custom--accent--strong);
	outline-offset: 3px;
	border-radius: var(--wp--custom--radius--xs);
}

/* ================================================================== *
 * PRODUCT WITH NO PREVIEW YET
 * The 16:10 box is held by the wrapper, so the grid does not move a pixel —
 * the direct payoff of putting aspect-ratio on the wrapper, not the <img>.
 * ================================================================== */

.nou-catalog-card__media--empty {
	display: grid;
	place-items: center;
	background-image: linear-gradient(135deg, var(--wp--custom--neutral--200) 0 1px, transparent 1px 100%),
		linear-gradient(45deg, var(--wp--custom--neutral--200) 0 1px, transparent 1px 100%);
	background-size: 14px 14px;
}

.nou-catalog-card__media--empty span {
	font-family: var(--wp--preset--font-family--mono);
	font-size: 10px;
	letter-spacing: var(--wp--custom--tracking--wide);
	text-transform: uppercase;
	color: var(--wp--custom--neutral--600);
}

/* ================================================================== *
 * REVEAL ON SCROLL
 *
 * THE GATE IS `.nou-catalog-reveal`, NOT `.nou-animations-ready`.
 *
 * This is a bug caught during the port, worth spelling out because the wrong
 * version looks correct. In the wireframe the reveal script set
 * `.nou-animations-ready` itself, so a script that failed to load never set the
 * class and the cards stayed visible. In the theme that class belongs to
 * assets/js/animations.js, which runs whether or not this section's script
 * does — so keying the hidden state off it would hide every card permanently
 * the moment catalog-cards.js was blocked, errored, or bailed out because the
 * browser has no IntersectionObserver.
 *
 * `.nou-catalog-reveal` is set by catalog-cards.js only after all of its
 * early returns have passed and immediately before it starts observing. No
 * script, no class, nothing hidden. That escape hatch is the whole reason
 * docs/animations.md asks for a gate in the first place.
 * ================================================================== */

.nou-catalog-reveal .nou-catalog-card {
	opacity: 0;
	translate: 0 14px;
	transition: opacity 520ms var(--wp--custom--easing--out),
		translate 520ms var(--wp--custom--easing--out);
	/*
	 * Delay follows POSITION IN THE ROW, not absolute index: assets/js/
	 * catalog-cards.js writes --i = index % columns, reading the column count
	 * off the live grid. The value therefore self-caps at (columns - 1) — two
	 * beats on a three-column grid — so the last card never waits eight beats
	 * and no hard ceiling is needed in CSS.
	 */
	transition-delay: calc(var(--i, 0) * 70ms);
}

.nou-catalog-reveal .nou-catalog-card.is-in {
	opacity: 1;
	translate: none;
}

@media (prefers-reduced-motion: reduce) {
	/*
	 * Belt and braces. catalog-cards.js already returns before setting the gate
	 * when reduced motion is on, so this should be unreachable — it is here in
	 * case the class is ever set from somewhere else.
	 */
	.nou-catalog-reveal .nou-catalog-card {
		opacity: 1;
		translate: none;
		transition: none;
	}

	.nou-catalog-card__frame,
	.nou-catalog-card__media img,
	.nou-catalog-card__caret {
		transition: none;
	}
}

/* ================================================================== *
 * CORE QUERY LOOP — the same card, built from core blocks
 *
 * patterns/posts.php renders the post grid with core/query so that every blog
 * route (index, category, tag, author, date) inherits the query WordPress
 * already resolved. Core's wrappers differ from the hand-rendered card in three
 * ways, and the rules below absorb each of them rather than duplicating the
 * card's styling:
 *
 *   post-template       <ul>/<li>  instead of  <div>/<article class="…card">
 *   post-terms          <div><a>   instead of  <span>
 *   post-featured-image <figure>   instead of  <span>
 *
 * Everything else already matches, because the pattern reuses the card's own
 * class names.
 * ================================================================== */

/*
 * The <li> IS the card. `position: relative` is load-bearing, not cosmetic:
 * `.nou-catalog-card__name a::after` stretches to `inset: 0` of the nearest
 * positioned ancestor, so without this the whole-card click target collapses
 * onto the title and the image quietly stops being clickable.
 */
.nou-catalog-grid > li.wp-block-post {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 14px;
	margin: 0;
}

/*
 * Core emits the term as `<div class="…"><a>Guide</a></div>`, where the card
 * used a bare `<span>`. The chip styling lands on the div; the link inside
 * inherits so the pill does not sprout an underline.
 *
 * `z-index` above the stretched link, otherwise the card-wide overlay swallows
 * the chip and the category is no longer reachable.
 */
.nou-catalog-grid .wp-block-post-terms.nou-catalog-card__platform {
	z-index: 4;
	width: max-content;
	max-width: calc(100% - 36px);
}

.nou-catalog-grid .wp-block-post-terms.nou-catalog-card__platform a {
	color: inherit;
	text-decoration: none;
}

.nou-catalog-grid .wp-block-post-terms.nou-catalog-card__platform a:hover {
	text-decoration: underline;
	text-underline-offset: 2px;
}

/* Core separates multiple terms with a comma; one chip is the design. */
.nou-catalog-grid .wp-block-post-terms.nou-catalog-card__platform .wp-block-post-terms__separator {
	display: none;
}

.nou-catalog-grid .wp-block-post-terms.nou-catalog-card__platform a:not(:first-of-type) {
	display: none;
}

/*
 * `aspectRatio` and `scale` on the block give the 16:10 box and object-fit, so
 * only object-position is left — and it is the one that matters here: these are
 * screenshots, and the top of a screenshot is the part worth keeping.
 */
.nou-catalog-grid figure.nou-catalog-card__media {
	margin: 0;
	border-radius: var(--wp--custom--radius--md);
	overflow: hidden;
	background: var(--wp--custom--neutral--100);
}

.nou-catalog-grid figure.nou-catalog-card__media img {
	object-position: top;
	border-radius: inherit;
	transition: transform 520ms var(--wp--custom--easing--out);
}

.nou-catalog-grid > li.wp-block-post:hover figure.nou-catalog-card__media img {
	transform: scale(1.03);
}

/*
 * The featured image is not a link (see patterns/posts.php): the title's
 * stretched link already covers the card. Nothing to style here beyond the box
 * itself — this rule stays so a future `isLink` does not silently break the
 * fill.
 */
.nou-catalog-grid figure.nou-catalog-card__media a {
	display: block;
	height: 100%;
}

/* Core's flow layout inserts margins between siblings; the card is a flex
 * column with its own gap, and the two together double the spacing. */
.nou-catalog-grid > li.wp-block-post > *,
.nou-catalog-grid .nou-catalog-card__body > *,
.nou-catalog-grid .nou-catalog-card__frame > * {
	margin-block: 0;
}

/* post-date renders `<div><time>`; the card's meta styling expects the text
 * node directly, so the div takes it and the time inherits. */
.nou-catalog-grid .wp-block-post-date.nou-catalog-card__meta time {
	color: inherit;
	text-decoration: none;
}

/* ------------------------------------------------------------------ *
 * QUERY PAGINATION — core's, wearing the theme's pager
 * ------------------------------------------------------------------ */

.wp-block-query-pagination.nou-catalog-pager {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 8px;
	margin-top: clamp(36px, 4vw, 52px);
}

.wp-block-query-pagination.nou-catalog-pager .wp-block-query-pagination-numbers {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
}

.wp-block-query-pagination.nou-catalog-pager a,
.wp-block-query-pagination.nou-catalog-pager .page-numbers {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 36px;
	min-height: 36px;
	padding: 0 12px;
	border: 1px solid var(--wp--custom--line--strong);
	border-radius: var(--wp--custom--radius--xs);
	font-family: var(--wp--preset--font-family--mono);
	font-size: 11px;
	letter-spacing: var(--wp--custom--tracking--wide);
	text-transform: uppercase;
	text-decoration: none;
	color: var(--wp--custom--neutral--700);
	transition: border-color 160ms var(--wp--custom--easing--base),
		color 160ms var(--wp--custom--easing--base);
}

.wp-block-query-pagination.nou-catalog-pager a:hover {
	border-color: var(--wp--preset--color--primary);
	color: var(--wp--preset--color--primary);
}

/* The page you are on is not a link, and should not look like one you can go to. */
.wp-block-query-pagination.nou-catalog-pager .page-numbers.current {
	border-color: var(--wp--preset--color--contrast);
	background: var(--wp--preset--color--contrast);
	color: var(--wp--preset--color--base);
}

/* The gap marker is punctuation, not a control: no box, no target size. */
.wp-block-query-pagination.nou-catalog-pager .page-numbers.dots {
	min-width: 0;
	min-height: 0;
	padding: 0 4px;
	border: 0;
	color: var(--wp--custom--neutral--400);
}

@media (prefers-reduced-motion: reduce) {
	.nou-catalog-grid figure.nou-catalog-card__media img,
	.wp-block-query-pagination.nou-catalog-pager a,
	.wp-block-query-pagination.nou-catalog-pager .page-numbers {
		transition: none;
	}

	.nou-catalog-grid > li.wp-block-post:hover figure.nou-catalog-card__media img {
		transform: none;
	}
}
