/*!
 * Photo Gallery by 256SOF — core frontend styles.
 *
 * All rules are scoped under .sofpg to avoid leaking into the theme.
 * Layout-specific blocks follow: .sofpg--masonry, .sofpg--justified, etc.
 *
 * CSS custom properties (set inline by templates, overridable in theme):
 *   --sofpg-cols-desktop, --sofpg-cols-tablet, --sofpg-cols-mobile
 *   --sofpg-gap
 *   --sofpg-radius
 *   --sofpg-caption-bg, --sofpg-caption-color
 *
 * TODO (implementation tickets):
 *   - Masonry: column-based flow or Masonry.js targets (.sofpg__item).
 *   - Justified: flex row with precomputed widths from justified-layout.js.
 *   - Grid: CSS Grid with repeat(var(--sofpg-cols-desktop), 1fr).
 *   - Album: cover image + thumbnail strip below / hover reveal.
 *   - Carousel: Swiper container (.swiper, .swiper-wrapper, .swiper-slide).
 */

.sofpg {
	--sofpg-cols-desktop: 3;
	--sofpg-cols-tablet: 2;
	--sofpg-cols-mobile: 1;
	--sofpg-gap: 12px;
	--sofpg-radius: 4px;
	--sofpg-caption-bg: rgba(0, 0, 0, 0.55);
	--sofpg-caption-color: #fff;

	box-sizing: border-box;
	width: 100%;
}

.sofpg *,
.sofpg *::before,
.sofpg *::after {
	box-sizing: inherit;
}

.sofpg__item {
	display: block;
	position: relative;
	overflow: hidden;
	border-radius: var(--sofpg-radius);
	text-decoration: none;
}

.sofpg__img {
	display: block;
	width: 100%;
	height: auto;
}

.sofpg__caption {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: 8px 12px;
	background: var(--sofpg-caption-bg);
	color: var(--sofpg-caption-color);
	font-size: 0.875rem;
	line-height: 1.3;
	opacity: 0;
	transition: opacity 200ms ease;
}

.sofpg__item:hover .sofpg__caption,
.sofpg__item:focus-visible .sofpg__caption {
	opacity: 1;
}

/*
 * Masonry layout.
 *
 * Masonry.js runs in percentPosition mode, so each .sofpg__item must have a
 * width expressed in percent of the container. We compute that width from the
 * --sofpg-cols-* custom properties (set inline by templates/masonry.php) and
 * the inter-item --sofpg-gap, so the last column sits flush against the right
 * edge. Vertical gap is margin-bottom; Masonry reads the full box height.
 */
.sofpg--masonry .sofpg__item {
	width: calc( ( 100% - ( var(--sofpg-cols-desktop) - 1 ) * var(--sofpg-gap) ) / var(--sofpg-cols-desktop) );
	margin-bottom: var(--sofpg-gap);
}

@media (max-width: 768px) {
	.sofpg--masonry .sofpg__item {
		width: calc( ( 100% - ( var(--sofpg-cols-tablet) - 1 ) * var(--sofpg-gap) ) / var(--sofpg-cols-tablet) );
	}
}

@media (max-width: 480px) {
	.sofpg--masonry .sofpg__item {
		width: calc( ( 100% - ( var(--sofpg-cols-mobile) - 1 ) * var(--sofpg-gap) ) / var(--sofpg-cols-mobile) );
	}
}

/*
 * Editor-only Masonry fallback.
 *
 * In Elementor's preview iframe, Masonry's internal item collection
 * silently rejects every direct child of the container (likely an
 * `instanceof HTMLElement` mismatch between the lib's loading context
 * and the iframe's own HTMLElement constructor). Rather than fight
 * that, frontend.js detects editor mode and adds the
 * `sofpg--editor-fallback` class, which switches the container to a
 * CSS multi-column flow. It produces a true masonry-like rendering
 * (items pack into balanced columns, vary in height, no overlap)
 * without any JavaScript. The published front-end keeps using real
 * Masonry — these rules only fire when the fallback class is present.
 *
 * `column-count` (and `column-gap`) read the same custom properties
 * Masonry uses, so column count and spacing stay consistent with what
 * the gallery is configured for. Each item carries `break-inside:
 * avoid` so it isn't split across columns, and we override the
 * percent-based width from the rules above with `width: 100%` so each
 * item fills the column it lands in.
 */
.sofpg--masonry.sofpg--editor-fallback {
	column-count: var(--sofpg-cols-desktop);
	column-gap: var(--sofpg-gap);
}

.sofpg--masonry.sofpg--editor-fallback .sofpg__item {
	width: 100%;
	display: block;
	break-inside: avoid;
	page-break-inside: avoid;
	-webkit-column-break-inside: avoid;
	margin-bottom: var(--sofpg-gap);
}

@media (max-width: 768px) {
	.sofpg--masonry.sofpg--editor-fallback {
		column-count: var(--sofpg-cols-tablet);
	}
}

@media (max-width: 480px) {
	.sofpg--masonry.sofpg--editor-fallback {
		column-count: var(--sofpg-cols-mobile);
	}
}

/*
 * Grid layout.
 *
 * Rigid CSS Grid: uniform tiles, identical aspect ratio, JS-free.
 * Column count comes from --sofpg-cols-* custom properties (set inline by
 * templates/grid.php). Each tile uses `aspect-ratio` + `object-fit: cover`
 * to crop the image to the chosen ratio without server-side processing.
 * The href still points to the full-size image so PhotoSwipe opens it
 * uncropped at its real dimensions.
 */
.sofpg--grid {
	display: grid;
	grid-template-columns: repeat( var(--sofpg-cols-desktop), 1fr );
	gap: var(--sofpg-gap);
}

.sofpg--grid .sofpg__item {
	aspect-ratio: var(--sofpg-aspect-ratio, 1 / 1);
	overflow: hidden;
}

.sofpg--grid .sofpg__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center;
}

@media (max-width: 768px) {
	.sofpg--grid {
		grid-template-columns: repeat( var(--sofpg-cols-tablet), 1fr );
	}
}

@media (max-width: 480px) {
	.sofpg--grid {
		grid-template-columns: repeat( var(--sofpg-cols-mobile), 1fr );
	}
}

/*
 * Justified (Flickr-style) layout.
 *
 * The per-row height and per-item width are computed in JS at runtime
 * (assets/js/justified.js) by reading data-sofpg-ratio on each item and
 * the container's measured clientWidth. The algorithm writes inline
 * width/height/margin styles on each .sofpg__item. The rules below are
 * the pre-JS placeholder sizing plus the overflow/object-fit bits that
 * crop the image inside the computed box.
 *
 * No media queries here: the JS reacts to container width changes, so
 * breakpoints would fight the algorithm.
 */
.sofpg--justified {
	position: relative;
	font-size: 0;  /* Kill whitespace between inline-block items. */
}

.sofpg--justified .sofpg__item {
	display: inline-block;
	vertical-align: top;
	overflow: hidden;
	/* Actual width and height are set inline by the JS algorithm. */
	/* Default placeholder dims avoid a flash before JS runs. */
	height: var(--sofpg-target-height, 240px);
	/* Pull the focus ring back inside the overflow:hidden box so it remains visible. */
	outline-offset: -2px;
}

.sofpg--justified .sofpg__img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center;
}

/*
 * Tight rows can produce very small thumbnails where the default caption
 * size overflows. Shrink both font-size and padding; images below ~100px
 * wide will still clip the caption but the alternative (hiding it) loses
 * information.
 */
.sofpg--justified .sofpg__item .sofpg__caption {
	font-size: 0.75rem;
	padding: 4px 6px;
}

/*
 * Carousel layout (Swiper v11).
 *
 * Slide height is fixed via --sofpg-slide-height (set inline by
 * templates/carousel.php). Images use object-fit: contain to preserve the
 * full frame; the black background acts as a letterbox for portraits in a
 * landscape slide. This matches what photographers expect when presenting
 * work — `cover` would crop and lose content.
 */
.sofpg--carousel {
	position: relative;
	width: 100%;
}

.sofpg--carousel .sofpg-swiper {
	width: 100%;
	height: var(--sofpg-slide-height, 480px);
}

.sofpg--carousel .swiper-slide {
	overflow: hidden;
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--sofpg-letterbox-bg, #000);
}

.sofpg--carousel .sofpg__item {
	display: block;
	width: 100%;
	height: 100%;
}

.sofpg--carousel .sofpg__img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	object-position: center;
}

/* Caption stays visible in carousel — each image gets full attention,
   no hover trigger like in the grid/masonry layouts. */
.sofpg--carousel .sofpg__caption {
	font-size: 1rem;
	padding: 12px 16px;
	opacity: 1;
}

/* Swiper UI theming. */
.sofpg--carousel .swiper-button-prev,
.sofpg--carousel .swiper-button-next {
	color: #fff;
	filter: drop-shadow( 0 0 4px rgba( 0, 0, 0, 0.5 ) );
}

.sofpg--carousel .swiper-pagination-bullet {
	background: #fff;
	opacity: 0.5;
	filter: drop-shadow( 0 0 2px rgba( 0, 0, 0, 0.5 ) );
}

.sofpg--carousel .swiper-pagination-bullet-active {
	opacity: 1;
}

/* Lazy-loading spinner shown while a slide's image is being fetched.
   Swiper's bundle ships a default; we only override the color token to
   match the white-on-letterbox palette used elsewhere in the carousel. */
.sofpg--carousel .swiper-lazy-preloader {
	--swiper-preloader-color: #fff;
}

@media (max-width: 768px) {
	.sofpg--carousel .sofpg-swiper {
		height: calc( var(--sofpg-slide-height, 480px) * 0.75 );
	}
}

@media (max-width: 480px) {
	.sofpg--carousel .sofpg-swiper {
		height: calc( var(--sofpg-slide-height, 480px) * 0.5 );
	}
}

/*
 * Album layout.
 *
 * Cover stage stacks every .sofpg__cover absolutely; the active one is
 * opacity:1/visibility:visible, the rest are faded out. The visibility
 * transition is delayed by 200ms on fade-out (see inactive rule) so the
 * leaving cover stays click-targetable for the duration of the fade; the
 * incoming active cover flips to visible immediately (no delay) so it can
 * receive the lightbox click the instant the user triggers the swap.
 *
 * This matters for PhotoSwipe: it binds to every a.sofpg__cover (they all
 * have the .sofpg__item selector class), but only the visible one is
 * clickable, so the lightbox opens at the currently displayed cover.
 */
.sofpg--album {
	position: relative;
	width: 100%;
}

.sofpg--album .sofpg__cover-stage {
	position: relative;
	width: 100%;
	height: var(--sofpg-cover-height, 520px);
	background: var(--sofpg-letterbox-bg, #000);
	overflow: hidden;
}

.sofpg--album .sofpg__cover {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	opacity: 0;
	visibility: hidden;
	transition: opacity 200ms ease-in-out, visibility 0s linear 200ms;
	cursor: zoom-in;
}

.sofpg--album .sofpg__cover.is-active {
	opacity: 1;
	visibility: visible;
	transition: opacity 200ms ease-in-out, visibility 0s;
	z-index: 2;
}

.sofpg--album .sofpg__cover .sofpg__img {
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
}

.sofpg--album .sofpg__cover .sofpg__caption {
	font-size: 1rem;
	padding: 12px 16px;
	opacity: 1;
}

/* Thumbnail strip. */
.sofpg--album .sofpg__thumbs-strip {
	display: flex;
	gap: var(--sofpg-thumb-gap, 4px);
	padding: var(--sofpg-thumb-gap, 4px) 0;
	overflow-x: auto;
	overflow-y: hidden;
	scroll-behavior: smooth;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: thin;
}

.sofpg--album .sofpg__thumb {
	flex: 0 0 auto;
	width: var(--sofpg-thumb-size, 80px);
	height: var(--sofpg-thumb-size, 80px);
	padding: 0;
	margin: 0;
	border: 2px solid transparent;
	background: none;
	cursor: pointer;
	overflow: hidden;
	transition: border-color 150ms ease;
	outline-offset: 2px;
}

.sofpg--album .sofpg__thumb img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.sofpg--album .sofpg__thumb:hover,
.sofpg--album .sofpg__thumb:focus-visible {
	border-color: #666;
}

.sofpg--album .sofpg__thumb.is-active {
	border-color: #2271b1;
}

@media (max-width: 768px) {
	.sofpg--album .sofpg__cover-stage {
		height: calc( var(--sofpg-cover-height, 520px) * 0.7 );
	}
}

@media (max-width: 480px) {
	.sofpg--album .sofpg__cover-stage {
		height: calc( var(--sofpg-cover-height, 520px) * 0.5 );
	}

	.sofpg--album .sofpg__thumb {
		width: calc( var(--sofpg-thumb-size, 80px) * 0.8 );
		height: calc( var(--sofpg-thumb-size, 80px) * 0.8 );
	}
}
