/**
 * Portfolio Hub rows — STRUCTURE ONLY.
 *
 * This file contains no design values. No type, no colour, no spacing,
 * no rhythm, no proportions, no hover treatment. All of that lives in
 * Appearance > Editor > Styles > Additional CSS, in the AI DESIGN TOKENS
 * block, and applies to every section of the site at once.
 *
 * What belongs here: how many columns a row has, when that column count
 * changes, and the boxes the cards are built from. Anything you could
 * change without changing how the page is *built* is design and does not
 * belong in this file.
 *
 * There are no var() fallbacks anywhere on purpose. A fallback is a design
 * value in hiding: it is the number that silently takes over when a token
 * goes missing, and that is how this plugin drifted out of sync with the
 * writing section while its own header claimed it was token-driven.
 *
 * v1.9.2 — 2026-08-12.
 */

.aph-row {
	--aph-columns: 4;
}

/* ---------------------------------------------------------------- header */

.aph-row__header {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
}

.aph-row__title,
.aph-row__dek {
	margin: 0;
}

.aph-row__dek {
	flex: 1 1 12rem;
}

.aph-row__seeall {
	margin-inline-start: auto;
	text-decoration: none;
	white-space: nowrap;
}

.aph-row__seeall:hover {
	text-decoration: underline;
	text-underline-offset: 0.25em;
}

/* ----------------------------------------------------------------- items */

.aph-row__items {
	display: grid;
	grid-template-columns: repeat( var( --aph-columns ), minmax( 0, 1fr ) );
}

.aph-row--hero .aph-row__items {
	grid-template-columns: minmax( 0, 1fr );
}

/* A row that resolved to a single card must not run the full page width as
   one enormous card. It takes the width it would have had in a two-up row,
   so a category with one project sits flush with the ones that have two.
   Grid geometry, not a typographic measure. */
.aph-row--grid[ data-cols='1' ] .aph-card,
.aph-row--feature[ data-cols='1' ] .aph-card {
	max-width: calc( ( 100% - var( --ai-gap ) ) / 2 );
}

/* ----------------------------------------------------------------- cards */

.aph-card {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.aph-card__media {
	display: block;
	overflow: hidden;
	line-height: 0;
}

.aph-card__image {
	width: 100%;
	height: auto;
}

/* A card video occupies the same box as the still it replaces, so a row
   mixing the two keeps its grid. The poster is the featured image, which is
   what shows before it plays and wherever it cannot. */
.aph-card__video {
	display: block;
	background-color: #000;
	pointer-events: none;
}

/* No gap. Card rhythm is owned by --ai-card-gap in the tokens block. A flex
   gap here stacks on top of the margins those rules set and silently doubles
   the spacing, which is how this section drifted away from the writing
   section in the first place. */
.aph-card__body {
	display: flex;
	flex-direction: column;
	gap: 0;
	min-width: 0;
}

.aph-card__eyebrow,
.aph-card__title,
.aph-card__dek,
.aph-card__meta {
	margin: 0;
}

.aph-card__eyebrow a,
.aph-card__title a {
	text-decoration: none;
	color: inherit;
}

.aph-card__eyebrow a:hover {
	text-decoration: underline;
}

.aph-card__title a:hover {
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

/* ------------------------------------------------------------ hero variant */

.aph-row--hero .aph-card--hero {
	display: grid;
	grid-template-columns: minmax( 0, 1.35fr ) minmax( 0, 1fr );
	align-items: center;
}

/* ---------------------------------------------------------- slider variant */

/* Everything below is geometry and state: how the frame is proportioned,
   which slide is showing, and where the panel and controls sit inside it.
   The custom properties are set as inline styles by the block from the
   editor's own settings — they are the editor's values being carried, not
   design values declared here. Nothing in this section picks a colour, a
   type size, a scrim or a border. */

.aph-row--slider .aph-slider {
	position: relative;
	isolation: isolate;
}

.aph-slider__viewport {
	display: grid;
	grid-template-areas: 'frame';
	position: relative;
	overflow: hidden;
}

.aph-slide {
	grid-area: frame;
	display: grid;
	min-width: 0;
	visibility: hidden;
	pointer-events: none;
}

.aph-slide.is-active {
	visibility: visible;
	pointer-events: auto;
	z-index: 1;
}

/* ------------------------------------------------------------ slide frame */

/* The still spans the whole frame and the panel sits over it, which is what
   lets the photograph run to the page edge while the text still has somewhere
   to be read. Both children occupy the same grid area. */

.aph-slide__media,
.aph-slide__panel {
	grid-area: 1 / 1 / 2 / 2;
	min-width: 0;
}

/* The proportion belongs to the frame, not to the photograph. Those are the
   same thing until the photograph is set into a field narrower than the frame,
   and then they are not: a 21:9 frame can hold a 3:2 picture with the rest
   given over to the matte. */
.aph-row--slider .aph-slide {
	aspect-ratio: var( --aph-ratio );
}

.aph-row--slider[ data-ratio='viewport' ] .aph-slide {
	aspect-ratio: auto;
	height: var( --aph-frame-height );
}

.aph-row--slider[ data-ratio='auto' ] .aph-slide {
	aspect-ratio: auto;
}

.aph-slide__media {
	position: relative;
	display: block;
	line-height: 0;
	overflow: hidden;
	height: 100%;
}

.aph-slide__image {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: var( --aph-image-fit );
}

.aph-row--slider[ data-ratio='auto' ] .aph-slide__media,
.aph-row--slider[ data-ratio='auto' ] .aph-slide__image {
	height: auto;
}

/* ------------------------------------------------------------ slide matte */

/* The photograph takes a share of the frame and the rest is left as a field
   for the text. Nothing overlaps: the picture is smaller than the frame
   rather than being covered up, which is what lets a wide frame hold an
   uncropped picture. */

.aph-row--slider[ data-matte='true' ] .aph-slide__media {
	width: var( --aph-matte-width );
	justify-self: start;
}

.aph-row--slider[ data-matte='true' ][ data-panel='left' ] .aph-slide__media {
	justify-self: end;
}

/* The inner edge of the picture dissolving into the field, so the two do not
   meet at a hard line. Above the loop as well as the still. */
.aph-row--slider[ data-matte='true' ] .aph-slide__media::after {
	content: '';
	position: absolute;
	inset: 0;
	z-index: 2;
	pointer-events: none;
	background: linear-gradient(
		to right,
		transparent calc( 100% - var( --aph-matte-fade ) ),
		var( --ai-slide-matte ) 100%
	);
}

.aph-row--slider[ data-matte='true' ][ data-panel='left' ] .aph-slide__media::after {
	background: linear-gradient(
		to left,
		transparent calc( 100% - var( --aph-matte-fade ) ),
		var( --ai-slide-matte ) 100%
	);
}

/* The loop sits over the still, invisible until it can play. Every failure
   path — a slow download, a refused autoplay, Low Power Mode, reduced motion,
   no JavaScript — leaves the photograph showing, because the photograph was
   never replaced. */
.aph-slide__video {
	position: absolute;
	inset: 0;
	z-index: 1;
	width: 100%;
	height: 100%;
	object-fit: var( --aph-image-fit );
	opacity: 0;
	pointer-events: none;
}

.aph-slide__video.is-ready {
	opacity: 1;
}

/* ------------------------------------------------------------ slide panel */

.aph-slide__panel {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
	min-width: 0;
}

.aph-slide__body {
	width: var( --aph-panel-width );
	min-width: 0;
}

.aph-row--slider[ data-panel='right' ] .aph-slide__panel {
	justify-content: flex-end;
}

.aph-row--slider[ data-panel='left' ] .aph-slide__panel {
	justify-content: flex-start;
}

.aph-row--slider[ data-panel-align='start' ] .aph-slide__panel {
	align-items: flex-start;
}

.aph-row--slider[ data-panel-align='center' ] .aph-slide__panel {
	align-items: center;
}

.aph-row--slider[ data-panel-align='end' ] .aph-slide__panel {
	align-items: flex-end;
}

.aph-row--slider[ data-text-align='center' ] .aph-slide__body {
	text-align: center;
}

.aph-row--slider[ data-text-align='end' ] .aph-slide__body {
	text-align: end;
}

/* A panel below the frame is not an overlay at all: it leaves the shared grid
   area and takes its own row, so the photograph is never covered. */
.aph-row--slider[ data-panel='below' ] .aph-slide {
	grid-template-rows: auto auto;
}

.aph-row--slider[ data-panel='below' ] .aph-slide__panel {
	grid-area: 2 / 1 / 3 / 2;
	align-items: flex-start;
}

.aph-row--slider[ data-panel='below' ] .aph-slide__body {
	width: 100%;
}

.aph-slide__kicker,
.aph-slide__title,
.aph-slide__dek,
.aph-slide__meta,
.aph-slide__cta {
	margin: 0;
}

.aph-slide__kicker a,
.aph-slide__title a,
.aph-slide__cta a {
	text-decoration: none;
	color: inherit;
}

.aph-slide__kicker a:hover {
	text-decoration: underline;
}

.aph-slide__title a:hover,
.aph-slide__cta a:hover {
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

/* ---------------------------------------------------------- slider motion */

/* No transition until the script is running. A slide swapping on first paint
   is a flash, not a transition. */

.aph-row--slider.is-enhanced[ data-transition='fade' ] .aph-slide,
.aph-row--slider.is-enhanced[ data-transition='slide' ] .aph-slide {
	opacity: 0;
	transition:
		opacity var( --aph-transition-ms ) var( --aph-easing ),
		transform var( --aph-transition-ms ) var( --aph-easing ),
		visibility 0s linear var( --aph-transition-ms );
}

.aph-row--slider.is-enhanced[ data-transition='fade' ] .aph-slide.is-active,
.aph-row--slider.is-enhanced[ data-transition='slide' ] .aph-slide.is-active {
	opacity: 1;
	transition:
		opacity var( --aph-transition-ms ) var( --aph-easing ),
		transform var( --aph-transition-ms ) var( --aph-easing ),
		visibility 0s;
}

.aph-row--slider.is-enhanced[ data-transition='slide' ] .aph-slide {
	transform: translate3d( var( --aph-slide-offset, 3% ), 0, 0 );
}

.aph-row--slider.is-enhanced[ data-transition='slide' ][ data-direction='prev' ] .aph-slide {
	--aph-slide-offset: -3%;
}

.aph-row--slider.is-enhanced[ data-transition='slide' ] .aph-slide.is-active {
	transform: translate3d( 0, 0, 0 );
}

/* Only ever runs when the editor switched it on. */
@keyframes aph-ken-burns {
	from {
		transform: scale( 1 );
	}

	to {
		transform: scale( 1.06 );
	}
}

.aph-row--slider[ data-ken-burns='true' ] .aph-slide.is-active .aph-slide__image {
	animation: aph-ken-burns var( --aph-kenburns-ms ) linear both;
}

/* -------------------------------------------------------- slider controls */

.aph-slider__controls {
	position: relative;
	display: flex;
	align-items: center;
	min-width: 0;
}

.aph-row--slider[ data-controls='overlay' ] .aph-slider__controls {
	position: absolute;
	inset-inline: 0;
	inset-block-end: 0;
	z-index: 2;
}

/* Wraps rather than overflows. Arrows, a counter and a dot per slide is more
   than a phone-width frame holds in one line, and an overflowing control
   group lands on top of the slide text. */
.aph-slider__nav-group {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-end;
	min-width: 0;
	margin-inline-start: auto;
}

.aph-slider__nav,
.aph-slider__dot {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	background: none;
	border: 0;
	cursor: pointer;
	color: inherit;
	font: inherit;
}

.aph-slider__counter {
	margin: 0;
	white-space: nowrap;
	font-variant-numeric: tabular-nums;
}

.aph-slider__dots {
	display: flex;
	align-items: center;
}

.aph-slider__dot {
	line-height: 0;
}

.aph-slider__dot > span {
	display: block;
	border-radius: 50%;
	background-color: currentColor;
}

/* Anchored to the foot of the frame rather than to the top of the controls,
   so it reads as the slide's own clock rather than as a rule above the
   arrows. Controls set below the frame put it under them for the same
   reason. */
.aph-slider__progress {
	position: absolute;
	inset-inline: 0;
	inset-block-end: 0;
	z-index: 2;
	overflow: hidden;
}

.aph-slider__progress-bar {
	display: block;
	width: 100%;
	height: 100%;
	transform: scaleX( 0 );
	transform-origin: left center;
	background-color: currentColor;
}

/* ---------------------------------------------------------------- editor */

/* Admin-side only. Never rendered on the front end, so it is not part of the
   design system and is left as plain values. */

.aph-placeholder {
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
	padding: 1.25rem 1.5rem;
	border: 1px dashed color-mix( in srgb, currentColor 35%, transparent );
	border-radius: 2px;
	font-size: 0.875rem;
	opacity: 0.75;
}

.aph-editor-preview {
	pointer-events: none;
}

.aph-placement-panel__note {
	margin: 0.75rem 0 0;
	font-size: 12px;
	opacity: 0.7;
}

.aph-video-field {
	margin-top: 1rem;
	padding-top: 1rem;
	border-top: 1px solid rgba( 0, 0, 0, 0.1 );
}

.aph-video-field__label {
	margin: 0 0 0.5rem;
	font-size: 11px;
	text-transform: uppercase;
}

.aph-video-field__buttons {
	display: flex;
	gap: 0.5rem;
	align-items: center;
	flex-wrap: wrap;
}

/* ------------------------------------------------------------ responsive */

/* These override grid-template-columns directly rather than reassigning
   --aph-columns. The custom property is set as an inline style on the
   section, and an inline declaration beats a stylesheet rule even inside a
   media query, so redefining the variable here would be silently ignored. */
@media ( max-width: 1024px ) {
	.aph-row[ data-cols='3' ] .aph-row__items,
	.aph-row[ data-cols='4' ] .aph-row__items,
	.aph-row[ data-cols='5' ] .aph-row__items,
	.aph-row[ data-cols='6' ] .aph-row__items {
		grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
	}
}

/* Phones hold two across. A four-card row becomes a 2x2 block rather than a
   long single-file scroll. */
@media ( max-width: 640px ) {
	.aph-row .aph-row__items {
		grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
	}

	/* Rows that resolved to a single card keep it full width rather than
	   sitting in a half-width column beside nothing. Half a phone column is
	   a thumbnail. */
	.aph-row[ data-cols='1' ] .aph-row__items {
		grid-template-columns: minmax( 0, 1fr );
	}

	.aph-row--grid[ data-cols='1' ] .aph-card,
	.aph-row--feature[ data-cols='1' ] .aph-card {
		max-width: none;
	}

	.aph-row--hero .aph-card--hero {
		grid-template-columns: minmax( 0, 1fr );
	}

	.aph-row__header {
		flex-direction: column;
		align-items: flex-start;
	}

	.aph-row__dek {
		flex-basis: auto;
	}

	.aph-row__seeall {
		margin-inline-start: 0;
	}

	/* A 34% panel on a phone is a column two words wide. Below this width the
	   panel always takes the full frame, whichever side it was set to. */
	.aph-row--slider .aph-slide__body {
		width: 100%;
	}

	/* The frame takes its own proportion on a phone. A 2:1 lead is a
	   letterbox slot on a 390px screen. */
	.aph-row--slider:not( [ data-ratio-mobile='match' ] ):not( [ data-ratio-mobile='auto' ] ) .aph-slide {
		aspect-ratio: var( --aph-ratio-mobile );
	}

	.aph-row--slider[ data-ratio-mobile='auto' ] .aph-slide {
		aspect-ratio: auto;
	}

	/* The matte turns through ninety degrees. A field beside the picture on a
	   phone is a strip too narrow to hold either, so the picture takes the top
	   of the frame and the field takes the bottom — the same idea, stacked.
	   Nothing overlaps: the text sits in the field, below the photograph,
	   against the fade rather than on top of the picture. */
	.aph-row--slider[ data-matte='true' ] .aph-slide {
		grid-template-rows: var( --aph-matte-height ) 1fr;
	}

	.aph-row--slider[ data-matte='true' ] .aph-slide__media {
		width: 100%;
		grid-area: 1 / 1 / 2 / 2;
		justify-self: stretch;
	}

	.aph-row--slider[ data-matte='true' ] .aph-slide__panel {
		grid-area: 2 / 1 / 3 / 2;
		align-items: flex-start;
	}

	/* The foot of the picture dissolving down into the field it sits in. */
	.aph-row--slider[ data-matte='true' ] .aph-slide__media::after {
		background: linear-gradient(
			to bottom,
			transparent calc( 100% - var( --aph-matte-fade ) ),
			var( --ai-slide-matte ) 100%
		);
	}

	/* With the text in its own row there is nothing to clear — the controls
	   sit in the field beside it rather than over the words. */
	.aph-row--slider[ data-matte='true' ][ data-controls='overlay' ] .aph-slide__panel {
		padding-block-end: var( --ai-slide-panel-pad );
	}

	.aph-row--slider .aph-slide__panel {
		align-items: flex-end;
		justify-content: flex-start;
	}
}

/* The stacked slides are all in the layout at once, so the frame is as tall as
   the tallest of them. Reduced motion keeps the still and drops the movement,
   not the slider. */
@media ( prefers-reduced-motion: reduce ) {
	.aph-row--slider .aph-slide,
	.aph-row--slider .aph-slide.is-active {
		transition: none;
		transform: none;
	}

	.aph-row--slider[ data-ken-burns='true' ] .aph-slide.is-active .aph-slide__image {
		animation: none;
	}
}

@media ( prefers-reduced-motion: reduce ) {
	.aph-card__image,
	.aph-card__video {
		transition: none;
	}
}
