/**
 * Folder Flow Gallery - frontend styles (SPEC §8.3).
 *
 * Responsive from 320px up. Tiles are a fixed aspect ratio so the grid never
 * reflows as images load (CLS ~0) regardless of intrinsic dimensions. Per-
 * gallery appearance comes in as CSS custom properties set by frontend.js
 * (--ffg-tile-min-set / --ffg-gap-set / --ffg-radius); the values below are the
 * free-tier defaults, used until the client hydrates.
 *
 * THE TILE MINIMUM HAS ONE DECIDER, AND IT IS THIS STYLESHEET (0.27.1). A
 * writer (frontend.js applyConfig(), FFG_Render's preview markup) puts the
 * gallery's SETTING on --ffg-tile-min-set and never touches --ffg-tile-min;
 * every reader (the grid tracks, the skeletons, the aligned flex basis, the
 * justified packer) reads --ffg-tile-min, which is derived below and in the
 * small-screen block. Before 0.27.1 the script wrote --ffg-tile-min inline,
 * and an inline declaration beats every stylesheet rule, so the 140px phone
 * rule never took effect and a 375px phone got one 220px tile per row.
 *
 * WHAT the tile shape is comes in the same way: --ffg-tile-aspect (a CSS
 * <ratio>) plus --ffg-tile-aspect-num (the same value as a bare number, for the
 * no-aspect-ratio fallback). D35: the gallery's Tile shape setting, or in
 * Automatic mode the snapped median shape of the images in the folder on
 * screen. Everything that must share the tile footprint - image thumbs, folder
 * tiles, skeletons - reads those two vars and nothing else.
 */

/*
 * The two numbers in the tile-minimum rule, and who mirrors them:
 *   220px - the free tile minimum, FFG_Gallery_CPT::appearance_defaults()
 *           ['tile_min'] (also frontend.js's px() fallback).
 *   140px - the phone cap below 480px, FFG_Gallery_CPT::PHONE_TILE_MIN,
 *           which the free Display tab's "(140px on phones)" note prints.
 * This stylesheet is the canonical home of the rule (a media query cannot read
 * a variable, and the block editor and the preview drawer load this file with
 * no page-level inline style); tests/defaults-resolution.php section 16 parses
 * this file and fails if either PHP number disagrees with it.
 */

.ffg-gallery {
	/* The EFFECTIVE tile minimum: the gallery's setting, else the free default.
	 * Read this; write --ffg-tile-min-set. */
	--ffg-tile-min: var( --ffg-tile-min-set, 220px );
	/* The same for the gap (0.27.2): read --ffg-gap, write --ffg-gap-set. */
	--ffg-gap: var( --ffg-gap-set, 10px );
	--ffg-radius: 6px;
	/* The gallery's own colour tokens (D42, F-251). Everything the component
	 * paints for itself - tile and thumb backgrounds, the loading shimmer, the
	 * hairline on a notice - reads one of these, so the Dark theme below is a
	 * block of token values rather than a second copy of the rules. The light
	 * values are the exact colours the stylesheet carried before 0.22.0, so a
	 * gallery at its defaults renders identically. */
	--ffg-tile-bg: rgba( 0, 0, 0, 0.04 );
	--ffg-thumb-bg: rgba( 0, 0, 0, 0.06 );
	--ffg-line: rgba( 0, 0, 0, 0.2 );
	/* The tile hairline: the ink colour at a quarter, so the premium tile border
	 * needs no colour control of its own (D42 - one control per concept) and reads
	 * on either theme. A token rather than a literal because the DARK block below
	 * is the only place its value changes. */
	--ffg-tile-line: rgba( 29, 35, 39, 0.25 );
	--ffg-shimmer-a: rgba( 0, 0, 0, 0.06 );
	--ffg-shimmer-b: rgba( 0, 0, 0, 0.12 );
	margin: 1.5em 0;
}

/* Dark theme (D42, F-251 - FREE). Scoped to the gallery's own descendants and
 * to the gallery's own surfaces: the tiles, the thumb backgrounds, the shimmer,
 * the hairlines. Text that sits on the PAGE (the breadcrumb, the description,
 * an empty or error notice) inherits the page's own colour and is never set
 * here: on a dark site the theme's text is already light, and on a light site
 * a forced light colour made the breadcrumb invisible (measured 2026-09-22).
 * The page itself is never touched - a plugin does not get to decide a site's
 * background (the footprint rule) - so this is the choice for a gallery sitting
 * on a dark page, not a dark mode for the site.
 *
 * The folder overlay is deliberately absent from this block: it is already
 * white-on-a-dark-gradient over a photograph, and it looks the same on either
 * theme. */
.ffg-gallery.ffg-theme-dark {
	--ffg-tile-bg: #2c3338;
	--ffg-thumb-bg: #2c3338;
	--ffg-line: rgba( 255, 255, 255, 0.24 );
	--ffg-tile-line: rgba( 255, 255, 255, 0.25 );
	--ffg-shimmer-a: rgba( 255, 255, 255, 0.07 );
	--ffg-shimmer-b: rgba( 255, 255, 255, 0.14 );
}

/* The folder, Back and broken-image glyphs are single-colour SVG data URIs
 * (#50575e), which is invisible on #2c3338. Inverting the pseudo-element is one
 * declaration and keeps ONE copy of each icon: a second set of data URIs in a
 * lighter fill would be the same paths written twice, free to drift. */
.ffg-gallery.ffg-theme-dark .ffg-thumb-folder::after,
.ffg-gallery.ffg-theme-dark .ffg-thumb-up::after,
.ffg-gallery.ffg-theme-dark .ffg-thumb-error::after {
	filter: invert( 1 );
	opacity: 0.45;
}

/* ------------------------------------------------------------------ Breadcrumb */

.ffg-breadcrumb {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.35em;
	margin-bottom: 1em;
	font-size: 0.95em;
	line-height: 1.4;
}

/* A crumb is a LINK, and link decoration is the theme's to decide (0.27.3, D49,
 * Steve's ruling on the theme audit's fault 6). The rule used to say
 * `text-decoration: none` with an underline on hover, and lost on the themes
 * that underline content links (Astra's `.ast-single-post .entry-content a`
 * 0,2,1, OceanWP's 0,6,1): we were fighting a theme over a choice that is the
 * theme's. So no text-decoration here at all, in any state - the trail is
 * underlined where the theme underlines links, and plain where it does not. */
.ffg-breadcrumb .ffg-crumb {
	color: inherit;
	opacity: 0.75;
}

.ffg-breadcrumb a.ffg-crumb:hover,
.ffg-breadcrumb a.ffg-crumb:focus {
	opacity: 1;
}

.ffg-breadcrumb .ffg-crumb-current {
	font-weight: 600;
	opacity: 1;
}

.ffg-breadcrumb .ffg-crumb-sep {
	opacity: 0.4;
	user-select: none;
}

/* ------------------------------------------------------------------ Grid */

.ffg-grid {
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( var( --ffg-tile-min ), 1fr ) );
	gap: var( --ffg-gap );
	/* Belt-and-braces: a tile must never stretch to a taller track (0.14.3) -
	 * an overlay would float in the stretched gap. Tile heights are equal by
	 * construction (see the folder-tile note below), so there is no gap left
	 * to stretch into; this only stops a future outlier doing damage. */
	align-items: start;
}

/* Grid alignment (D42, F-251 - FREE): where a part-filled last row sits.
 *
 * MEASURED TWICE, because the obvious implementations do not do what the setting
 * says (staging.woodbarnfarm.co.uk, 12 tiles in 8 columns, 2026-09-22):
 *
 *   1. `justify-content` on the grid ALONE is inert. With
 *      `minmax( var( --ffg-tile-min ), 1fr )` tracks the columns always consume
 *      the whole content box, so there is nothing left to distribute: the
 *      property computes to the chosen value and not one pixel moves.
 *   2. Fixed tracks - `repeat( auto-fill, var( --ffg-tile-min ) )` - plus
 *      `justify-content` moves the whole BLOCK of columns (measured: the 32px
 *      remainder went to the left at `end`), but the last row still fills tracks
 *      from the start of that block, so the part-filled row stayed on the left.
 *      `justify-content` aligns TRACKS, not the items of one row, and no grid
 *      property aligns a single row's items.
 *
 * So Centre and Right lay the tiles out as wrapping FLEX rows instead, where
 * `justify-content` applies to each line - including the last one, which is the
 * whole point of the control. The trade is that a tile is then exactly the tile
 * size rather than stretching to fill the row, which is what the field's own help
 * says. LEFT is the default and carries no rule at all: it is the stretch-to-fill
 * grid this plugin has always drawn, which is why nothing about an existing
 * gallery changes. The skeletons take the same treatment so the layout does not
 * shift when the real tiles replace them. */
.ffg-gallery.ffg-align-centre .ffg-grid,
.ffg-gallery.ffg-align-centre .ffg-skeletons,
.ffg-gallery.ffg-align-right .ffg-grid,
.ffg-gallery.ffg-align-right .ffg-skeletons {
	display: flex;
	flex-wrap: wrap;
}

.ffg-gallery.ffg-align-centre .ffg-grid > .ffg-tile,
.ffg-gallery.ffg-align-centre .ffg-skeletons > .ffg-skeleton,
.ffg-gallery.ffg-align-right .ffg-grid > .ffg-tile,
.ffg-gallery.ffg-align-right .ffg-skeletons > .ffg-skeleton {
	flex: 0 0 var( --ffg-tile-min );
}

/* A notice or an error is a child of the grid and must still own its whole row. */
.ffg-gallery.ffg-align-centre .ffg-grid > .ffg-notice,
.ffg-gallery.ffg-align-centre .ffg-grid > .ffg-error,
.ffg-gallery.ffg-align-right .ffg-grid > .ffg-notice,
.ffg-gallery.ffg-align-right .ffg-grid > .ffg-error {
	flex: 0 0 100%;
}

.ffg-gallery.ffg-align-centre .ffg-grid,
.ffg-gallery.ffg-align-centre .ffg-skeletons {
	justify-content: center;
}

.ffg-gallery.ffg-align-right .ffg-grid,
.ffg-gallery.ffg-align-right .ffg-skeletons {
	justify-content: flex-end;
}

/* Folder / Back tiles are structurally IDENTICAL to image tiles: a thumb
 * that fills the tile, with the name (and count) as a bottom overlay on top
 * of it - see ".ffg-folder .ffg-tile-meta" in the Tile meta section. The
 * tile's height is therefore its thumb's height, exactly as an image tile's
 * is, and equal heights need no rule at all.
 *
 * History (do not re-introduce): 0.14.3 tried align-items alone (the row
 * TRACK was still sized by the taller folder card); 0.14.4 kept the name
 * strip below the preview and shrank the thumb to fit the tile's footprint,
 * which equalised heights but - once D35 made tiles 16:9 rather than square -
 * squeezed the preview to a ~53px sliver above a grey slab. Overlaying the
 * strip removes the height it was consuming instead of paying for it out of
 * the preview. */

.ffg-tile {
	position: relative;
	display: block;
	color: inherit;
	text-decoration: none;
	border-radius: var( --ffg-radius );
	overflow: hidden;
	background: var( --ffg-tile-bg, rgba( 0, 0, 0, 0.04 ) );
}

.ffg-tile:focus-visible {
	outline: 2px solid currentColor;
	outline-offset: 2px;
}

/* Fixed-aspect thumb area - a known tile shape keeps the grid stable while
 * images load. The shape is --ffg-tile-aspect (D35), set on the gallery root by
 * frontend.js: the gallery's Tile shape setting, or - in Automatic mode - the
 * snapped median shape of the images in the folder being viewed. 1 / 1 until
 * the client hydrates, and for any folder whose images have no recorded
 * dimensions yet. Selector raised to .ffg-gallery .ffg-thumb so a theme's own
 * single-class rules cannot outrank the tile geometry. */
.ffg-gallery .ffg-thumb {
	position: relative;
	width: 100%;
	aspect-ratio: var( --ffg-tile-aspect, 1 / 1 );
	background: var( --ffg-thumb-bg, rgba( 0, 0, 0, 0.06 ) );
	overflow: hidden;
}

/* Fallback for browsers without aspect-ratio: the same shape as a
 * padding-bottom percentage, from the numeric companion var
 * (--ffg-tile-aspect-num, a bare width/height number - a `16 / 9` token cannot
 * be divided by inside calc()). */
@supports not ( aspect-ratio: 1 / 1 ) {
	.ffg-gallery .ffg-thumb {
		height: 0;
		padding-bottom: calc( 100% / var( --ffg-tile-aspect-num, 1 ) );
	}
}

/* Tile image geometry, defended.
 *
 * MEASURED (staging.woodbarnfarm.co.uk, BeTheme 28.5.6, 2026-09-21): BeTheme
 * ships `.content_wrapper img { height: auto }` and
 * `img:not([src$=".svg"]) { height: auto }` - both specificity 0,1,1, so both
 * beat a bare `.ffg-img` (0,1,0). The 400x225 thumb inside a 224px-wide tile
 * rendered 224x126 with a computed `height: 126px`, leaving a blank band under
 * every image. It looked right before load only because a replaced element with
 * no intrinsic size stretches to inset: 0. Salient has no theme-wide img reset
 * reaching content, which is why it never showed there.
 *
 * So: component specificity (.ffg-gallery .ffg-thumb > .ffg-img = 0,3,0) AND
 * !important on the geometry properties. The !important is deliberate and
 * scoped to this plugin's own layout primitive - it has to beat both a theme
 * reset at equal-or-higher specificity and any inline style a theme's or
 * plugin's JS writes onto the img. Colour, spacing and typography are the
 * site's business and stay unforced; the box the tile draws in is ours. */
.ffg-gallery .ffg-thumb > .ffg-img {
	position: absolute !important;
	inset: 0 !important;
	width: 100% !important;
	height: 100% !important;
	max-width: none !important;
	max-height: none !important;
	min-width: 0 !important;
	min-height: 0 !important;
	object-fit: cover !important;
	display: block;
	margin: 0;
	padding: 0;
}

.ffg-img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: 0;
	transition: opacity 0.35s ease;
}

.ffg-img.is-loaded {
	opacity: 1;
}

/* ------------------------------------------------------------------ Folder / state icons
 *
 * Inline SVG data URIs, deliberately NOT dashicons: the dashicons font is an
 * admin asset WordPress does not load for logged-out visitors, so glyph
 * codepoints render as tofu on the public front end (and the original
 * codepoints were wrong anyway - \f335 is dashicons-no-alt, the X, which is
 * how "folder with no cover yet" tiles ended up showing an error cross).
 * Three tiny SVGs cost a few hundred bytes and depend on nothing.
 */

.ffg-thumb-folder::after,
.ffg-thumb-up::after,
.ffg-thumb-error::after {
	content: "";
	position: absolute;
	inset: 0;
	background-repeat: no-repeat;
	background-position: center;
	background-size: 2.6em 2.6em;
	opacity: 0.4;
}

/* Folder */
.ffg-thumb-folder::after {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2350575e'%3E%3Cpath d='M4 5h5.6l2 2H20a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z'/%3E%3C/svg%3E");
}

/* Back (chevron left) */
.ffg-thumb-up::after {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2350575e'%3E%3Cpath d='M14.7 5.3a1 1 0 0 1 0 1.4L9.42 12l5.3 5.3a1 1 0 1 1-1.42 1.4l-6-6a1 1 0 0 1 0-1.4l6-6a1 1 0 0 1 1.42 0z'/%3E%3C/svg%3E");
}

.ffg-thumb-error {
	background: var( --ffg-thumb-bg, rgba( 0, 0, 0, 0.06 ) );
}

.ffg-thumb-error .ffg-img {
	display: none;
}

/* Broken image (frame + slash) */
.ffg-thumb-error::after {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2350575e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='5' width='18' height='14' rx='1.5'/%3E%3Cpath d='M3 16.5l5-5 4 4 3-3 6 6'/%3E%3Cpath d='M4.5 3.5l16 17'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------------------ Tile meta */

.ffg-tile-meta {
	display: flex;
	flex-direction: column;
	gap: 0.15em;
	padding: 0.5em 0.6em;
}

/* LINE CLAMP, tiles only (D45, F-228). A tile is a fixed box (D35) and text is
 * the one thing in it that can be any length, so the title is held to one line
 * and the description to two: a 40-character webcam name grows the strip over
 * the picture otherwise, and on a folder tile it would push the count out of the
 * gradient. The clamp is the `-webkit-box` construct - still the only thing that
 * clamps to N lines in every browser this plugin supports - with the
 * standardised `line-clamp` beside it so the prefixed property can eventually
 * go; a browser that supports neither falls back to `overflow: hidden`, which
 * clips at the element's own height.
 *
 * The lightbox is deliberately NOT clamped: it is a full-screen view of one
 * picture, the caption is the only text in it, and there is room for it. */
.ffg-tile-name {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 1;
	line-clamp: 1;
	font-weight: 600;
	font-size: 0.95em;
	overflow: hidden;
	text-overflow: ellipsis;
}

.ffg-tile-desc {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	font-size: 0.85em;
	line-height: 1.35;
	opacity: 0.8;
	overflow: hidden;
	text-overflow: ellipsis;
}

.ffg-tile-count {
	font-size: 0.82em;
	opacity: 0.65;
}

/* Title overlay on image tiles (free default: bottom gradient).
 *
 * Colours are driven by the appearance-pack CSS vars set by frontend.js:
 * --ffg-overlay-rgb is a BARE 'r,g,b' triplet (so it can feed rgba() with an
 * alpha), --ffg-overlay-text is a hex. The free build never sets custom values,
 * so the var() FALLBACKS below reproduce the old hardcoded colours pixel for
 * pixel. Trap: in `var( --ffg-overlay-rgb, 0, 0, 0 )` everything after the
 * FIRST comma is the fallback, i.e. the fallback is the triplet `0, 0, 0` -
 * which is exactly why passing it to rgba() is valid; do not "fix" it to a
 * single 0. */
.ffg-tile-overlay {
	position: absolute;
	inset: auto 0 0 0;
	padding: 1.4em 0.6em 0.5em;
	color: var( --ffg-overlay-text, #fff );
	background: linear-gradient( to top, rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0.65 ), rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0 ) );
	opacity: 0;
	transition: opacity 0.2s ease;
}

.ffg-file:hover .ffg-tile-overlay,
.ffg-file:focus-visible .ffg-tile-overlay {
	opacity: 1;
}

.ffg-tile-overlay .ffg-tile-name,
.ffg-tile-overlay .ffg-tile-desc {
	color: var( --ffg-overlay-text, #fff );
}

/* WHEN the strip shows (D45, F-228): a class on the gallery root, like every
 * other styling choice since D42. `hover` is the default and carries no rule at
 * all - it IS the two rules above, which is what makes the default a no-op by
 * construction rather than by agreement. */
.ffg-gallery.ffg-caption-always .ffg-tile-overlay {
	opacity: 1;
}

.ffg-gallery.ffg-caption-never .ffg-tile-overlay {
	display: none;
}

/* No hover to wait for (Steve's ruling 2, 0.31.0; Use-your-Drive documents the
 * same rule): on a touch screen an "On hover" strip would never be seen, so it
 * is shown all the time. "Never" still hides it (display). */
@media ( hover: none ) {
	.ffg-gallery .ffg-tile-overlay {
		opacity: 1;
	}
}

/* Folder + Back tile names: the SAME bottom overlay, over a preview that
 * fills the whole tile.
 *
 * ONE rule set for BOTH layouts (0.16.2). The justified layout has rendered
 * folder tiles this way since 0.13.0; the grid kept a name strip BELOW the
 * preview, which 0.14.4 then had to shrink the preview to make room for -
 * tolerable on a square tile, ridiculous once D35 made tiles 16:9 (a ~53px
 * preview sliver above a grey slab). So the treatment moves here, to the
 * FREE part of the stylesheet, because it is now free grid behaviour; the
 * justified-scoped copy that used to live in the premium block below is
 * gone. The appearance-pack colour vars are read identically by both
 * layouts, which is the point of sharing the rule.
 *
 * Unlike the image-title overlay above this one is ALWAYS visible, not
 * hover-revealed: folder names drive navigation and must be readable at a
 * glance. Same var()-fallback trap as above - the fallback in
 * var(--ffg-overlay-rgb, 0, 0, 0) IS the triplet `0, 0, 0`. */
.ffg-folder .ffg-tile-meta {
	position: absolute;
	inset: auto 0 0 0;
	padding: 1.4em 0.6em 0.5em;
	color: var( --ffg-overlay-text, #fff );
	background: linear-gradient( to top, rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0.65 ), rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0 ) );
}

.ffg-folder .ffg-tile-name,
.ffg-folder .ffg-tile-count {
	color: var( --ffg-overlay-text, #fff );
}

/* The count sits on a gradient, not on the page background: 0.65 of white is
 * legible where 0.65 of the theme's body colour was. Kept slightly muted so
 * the name still leads. */
.ffg-folder .ffg-tile-count {
	opacity: 0.8;
}

/* ------------------------------------------------------------------ Skeletons */

.ffg-skeletons {
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( var( --ffg-tile-min ), 1fr ) );
	gap: var( --ffg-gap );
}

.ffg-skeleton {
	/* The tile shape the real tiles will have (D35), so the grid does not
	 * resize when the skeletons are replaced. */
	aspect-ratio: var( --ffg-tile-aspect, 1 / 1 );
	border-radius: var( --ffg-radius );
	background: linear-gradient( 100deg, var( --ffg-shimmer-a, rgba( 0, 0, 0, 0.06 ) ) 30%, var( --ffg-shimmer-b, rgba( 0, 0, 0, 0.12 ) ) 50%, var( --ffg-shimmer-a, rgba( 0, 0, 0, 0.06 ) ) 70% );
	background-size: 200% 100%;
	animation: ffg-shimmer 1.3s ease-in-out infinite;
}

@supports not ( aspect-ratio: 1 / 1 ) {
	.ffg-skeleton {
		height: 0;
		padding-bottom: calc( 100% / var( --ffg-tile-aspect-num, 1 ) );
	}
}

@keyframes ffg-shimmer {
	0% {
		background-position: 200% 0;
	}
	100% {
		background-position: -200% 0;
	}
}

@media ( prefers-reduced-motion: reduce ) {
	.ffg-skeleton {
		animation: none;
	}
	.ffg-img {
		transition: none;
	}
}

/* ------------------------------------------------------------------ The gallery bar, the pager (0.28.0) */

/* The gallery's control tokens (0.28.0, D50): the sort select, the Load more
 * button, the page navigator and the premium search box are the gallery's own
 * SURFACES, like its tiles, so they take the gallery's colour theme (D42) and
 * never the page's. A control drawn in the page's colours would be a white box
 * on a dark site whose gallery is Dark, or the reverse. */
.ffg-gallery {
	--ffg-control-bg: #fff;
	--ffg-control-fg: #1d2327;
	--ffg-control-line: rgba( 0, 0, 0, 0.25 );
	--ffg-control-hover: #f0f0f1;
}
.ffg-gallery.ffg-theme-dark {
	--ffg-control-bg: #2c3338;
	--ffg-control-fg: #f0f0f1;
	--ffg-control-line: rgba( 255, 255, 255, 0.3 );
	--ffg-control-hover: #3c434a;
}

/* ------------------------------------------------------------------ Back to top (F-282, 0.28.1) */

/* Where "back to the top of the gallery" lands: clear of the WordPress admin
 * bar for a logged-in viewer (core sets the variable only while the bar shows)
 * and a little air above the gallery's first line. A theme's own fixed header
 * is the theme's; its `scroll-padding-top` on html, where it sets one, is
 * honoured by the same scroll. */
.ffg-gallery {
	scroll-margin-top: calc( var( --wp-admin--admin-bar--height, 0px ) + 16px );
}

/* The button (frontend.js setupBackToTop()): fixed, placed by the script in the
 * viewport's bottom band at the gallery's own right edge, and shown only two
 * screens into a long gallery. A control FFG draws, so it is an "author chose
 * this" rule (D49): (0,4,0) - the gallery root with its data attribute, our
 * class, and the `type` the button carries - above Twenty Twenty-One's (0,3,1)
 * button rule, declaring every property a theme's button rule was measured
 * touching. Hidden is `visibility: hidden`, so a hidden button is out of the
 * tab order and the accessibility tree as well as out of sight. Its colours are
 * the gallery's control tokens, so it follows the Dark theme. z-index 90: over
 * the tiles and their overlays, under a theme's sticky header and far under
 * GLightbox (999999) - and the script hides it while a lightbox is open. */
.ffg-gallery[data-gallery] > .ffg-top[type="button"] {
	position: fixed;
	top: 0;
	left: 0;
	z-index: 90;
	display: flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	width: 44px;
	height: 44px;
	min-width: 0;
	min-height: 0;
	margin: 0;
	padding: 0;
	border: 1px solid var( --ffg-control-line );
	border-radius: 50%;
	box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.18 );
	background: var( --ffg-control-bg );
	color: var( --ffg-control-fg );
	font: inherit;
	line-height: 0;
	letter-spacing: normal;
	text-decoration: none;
	text-transform: none;
	cursor: pointer;
	visibility: hidden;
	opacity: 0;
	transition: opacity 0.2s ease, visibility 0s linear 0.2s;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery[data-gallery] > .ffg-top.is-shown[type="button"] {
	visibility: visible;
	opacity: 1;
	transition: opacity 0.2s ease, visibility 0s linear 0s;
}
.ffg-gallery[data-gallery] > .ffg-top[type="button"]:hover {
	background: var( --ffg-control-hover );
	color: var( --ffg-control-fg );
	border-color: var( --ffg-control-fg );
}
.ffg-gallery[data-gallery] > .ffg-top[type="button"]:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 2px;
}
/* The chevron: two borders of a small square, turned to point up. */
.ffg-gallery .ffg-top .ffg-top-glyph {
	display: block;
	width: 11px;
	height: 11px;
	margin-top: 5px;
	border-top: 2px solid currentColor;
	border-left: 2px solid currentColor;
	transform: rotate( 45deg );
}
@media ( prefers-reduced-motion: reduce ) {
	.ffg-gallery[data-gallery] > .ffg-top[type="button"],
	.ffg-gallery[data-gallery] > .ffg-top.is-shown[type="button"] {
		transition: none;
	}
}

/* The bar: the breadcrumb on the left, the visitor's tools on the right, one
 * row that wraps on a narrow screen. Drawn only when there is a tool in it; a
 * gallery with none keeps its bare breadcrumb exactly as before. ALWAYS
 * visible - never revealed on hover (the competitor review's do-not-copy). */
.ffg-gallery .ffg-bar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5em 1em;
	margin: 0 0 1em;
}
.ffg-gallery .ffg-bar > .ffg-breadcrumb {
	flex: 1 1 auto;
	min-width: 0;
	margin-bottom: 0;
}
.ffg-gallery .ffg-bar-tools {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5em;
	margin-left: auto;
}

/* Words for a screen reader only (the sort select's name). */
.ffg-gallery .ffg-sr {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect( 0, 0, 0, 0 );
	white-space: nowrap;
	border: 0;
}
.ffg-gallery .ffg-sort {
	display: inline-flex;
	margin: 0;
}

/* THE CONTROLS ARE "THE AUTHOR CHOSE THIS" RULES (D49): a theme's form and
 * button rules reach a <select>, a <button> and a link in the content, so each
 * rule sits at (0,4,0) or above - the gallery root, the region, our class, and
 * a type or href attribute - and declares every property a theme was measured
 * touching on a control (background, border, radius, shadow, padding, sizes,
 * box-sizing, colour, font, line-height, letter-spacing, text-transform,
 * text-decoration, appearance). No !important: none was measured necessary.
 *
 * The sort select. appearance: none and our own chevron, because every theme
 * that styles a select draws its own arrow as a background image, and a native
 * arrow beside a theme's painted one is two arrows. */
.ffg-gallery .ffg-bar .ffg-sort .ffg-sort-select {
	box-sizing: border-box;
	width: auto;
	min-width: 0;
	max-width: 100%;
	height: 34px;
	min-height: 0;
	margin: 0;
	padding: 0 30px 0 10px;
	border: 1px solid var( --ffg-control-line );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background-color: var( --ffg-control-bg );
	/* A chevron in a mid grey that reads on either colour theme. */
	background-image: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%23808080' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E" );
	background-repeat: no-repeat;
	background-position: right 10px center;
	background-size: 12px 8px;
	color: var( --ffg-control-fg );
	font-family: inherit;
	font-size: 0.9em;
	font-weight: 400;
	line-height: 32px;
	letter-spacing: normal;
	text-transform: none;
	text-decoration: none;
	cursor: pointer;
	-webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
}
.ffg-gallery .ffg-bar .ffg-sort .ffg-sort-select:hover {
	border-color: var( --ffg-control-fg );
}
.ffg-gallery .ffg-bar .ffg-sort .ffg-sort-select:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 1px;
	box-shadow: none;
}
.ffg-gallery .ffg-bar .ffg-sort .ffg-sort-select:disabled {
	cursor: default;
	opacity: 0.7;
}

/* Load more (F-276): one centred button under the grid. */
.ffg-gallery .ffg-more {
	display: flex;
	justify-content: center;
	margin: 1.25em 0 0;
}
.ffg-gallery .ffg-more .ffg-more-btn[type="button"] {
	box-sizing: border-box;
	width: auto;
	min-width: 0;
	height: 40px;
	min-height: 0;
	margin: 0;
	padding: 0 22px;
	border: 1px solid var( --ffg-control-line );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background: var( --ffg-control-bg );
	color: var( --ffg-control-fg );
	font-family: inherit;
	font-size: 0.95em;
	font-weight: 600;
	line-height: 38px;
	letter-spacing: normal;
	text-transform: none;
	text-decoration: none;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery .ffg-more .ffg-more-btn[type="button"]:hover {
	background: var( --ffg-control-hover );
	color: var( --ffg-control-fg );
	border-color: var( --ffg-control-fg );
}
.ffg-gallery .ffg-more .ffg-more-btn[type="button"]:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 2px;
}
.ffg-gallery .ffg-more .ffg-more-btn[type="button"]:disabled {
	cursor: default;
	opacity: 0.7;
}

/* The numbered page navigator (F-276). Links, not list items: a theme's
 * content-list rules would reach a list here. A page number is a boxed control
 * (the author chose this), so its underline and colour are ours - unlike the
 * breadcrumb's, which are the theme's (D49). */
.ffg-gallery .ffg-pager {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 6px;
	margin: 1.25em 0 0;
}
.ffg-gallery .ffg-pager a.ffg-page[href],
.ffg-gallery .ffg-pager span.ffg-page[class] {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	min-width: 36px;
	height: 36px;
	margin: 0;
	padding: 0 10px;
	border: 1px solid var( --ffg-control-line );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background: var( --ffg-control-bg );
	color: var( --ffg-control-fg );
	font-family: inherit;
	font-size: 0.9em;
	font-weight: 400;
	line-height: 1;
	letter-spacing: normal;
	text-transform: none;
	text-decoration: none;
	text-underline-offset: auto;
	cursor: pointer;
}
.ffg-gallery .ffg-pager a.ffg-page[href]:hover,
.ffg-gallery .ffg-pager a.ffg-page[href]:focus {
	background: var( --ffg-control-hover );
	color: var( --ffg-control-fg );
	border-color: var( --ffg-control-fg );
	text-decoration: none;
	box-shadow: none;
}
.ffg-gallery .ffg-pager a.ffg-page[href]:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 2px;
}
/* The current page is the one filled box; a dead Previous or Next is faded. */
.ffg-gallery .ffg-pager span.ffg-page.is-current {
	background: var( --ffg-control-fg );
	border-color: var( --ffg-control-fg );
	color: var( --ffg-control-bg );
	font-weight: 600;
	cursor: default;
}
.ffg-gallery .ffg-pager span.ffg-page[aria-disabled="true"] {
	opacity: 0.45;
	cursor: default;
}
.ffg-gallery .ffg-pager .ffg-page-gap {
	min-width: 1.5em;
	text-align: center;
	opacity: 0.6;
}

/* A flat list shown in place of a folder (0.28.0 - the premium search's
 * results): always the stretch grid, whatever the layout or alignment, with a
 * muted caption under each tile naming where it lives. (0,4,0) so the justified
 * layout's flex rows and the aligned layouts' flex rows do not apply to it. */
.ffg-gallery[data-layout] .ffg-grid.ffg-list {
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( var( --ffg-tile-min ), 1fr ) );
	gap: var( --ffg-gap );
	align-items: start;
}
.ffg-gallery .ffg-list .ffg-list-item {
	min-width: 0;
}
.ffg-gallery .ffg-list .ffg-list-note {
	margin: 0.35em 0 0;
	padding: 0;
	font-size: 0.8em;
	line-height: 1.35;
	opacity: 0.7;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.ffg-gallery .ffg-list-status {
	margin: 0 0 0.75em;
	font-size: 0.9em;
	opacity: 0.8;
}

/* ------------------------------------------------------------------ Notices / error / sentinel */

.ffg-notice,
.ffg-error {
	grid-column: 1 / -1;
	padding: 1.5em;
	text-align: center;
	opacity: 0.8;
}

.ffg-error {
	opacity: 1;
}

.ffg-retry {
	margin-top: 0.6em;
	cursor: pointer;
}

.ffg-sentinel {
	height: 1px;
}

.ffg-gallery-notice {
	padding: 1.2em;
	border: 1px dashed var( --ffg-line, rgba( 0, 0, 0, 0.2 ) );
	border-radius: var( --ffg-radius );
	opacity: 0.85;
}

/* The no-JS baseline only shows when scripts are off. */
.ffg-baseline {
	list-style: disc;
	padding-left: 1.4em;
}

/* ------------------------------------------------------------------ Small screens */

/* Below 480px a tile's minimum is the SMALLER of the gallery's setting and
 * 140px (Steve, 2026-09-24): two tiles a row on a 375px phone, and a gallery
 * set smaller than 140 keeps its own smaller number. The justified packer reads
 * the same resolved value as its target row height. Only the stylesheet decides
 * this, which is why the setting arrives on --ffg-tile-min-set (see the
 * docblock at the top). */
@media ( max-width: 480px ) {
	.ffg-gallery {
		--ffg-tile-min: min( var( --ffg-tile-min-set, 220px ), 140px );
		/* The gap follows the tile minimum's rule (0.27.2): the smaller of the
		 * setting and 8px, so two 140px tiles fit a phone with a desktop-style
		 * scrollbar (two tiles + a 10px gap needed 290px of a 288px box). */
		--ffg-gap: min( var( --ffg-gap-set, 10px ), 8px );
	}
}

/* ---- Embeds (shortcode + block, M7) ---- */
.ffg-embed-heading {
	margin: 0 0 0.5em;
}
.ffg-embed-description {
	margin: 0 0 1em;
}

/* The current folder's description (sidecar folder.txt body - premium
 * writes it, the free build renders whatever the payload carries). */
.ffg-folder-description {
	margin: 0.25em 0 1em;
	color: #50575e;
	max-width: 60em;
	white-space: pre-line; /* folder.txt line breaks survive */
}

/* Logged-in views: the WP admin bar overlays the viewport top (32px, 46px on
 * small screens) at z-index 99999, and GLightbox does not compensate - its
 * close button (top: 15px) ends up half-hidden behind the bar for admins and
 * editors. Push it clear. Visitors (no admin bar) are untouched. */
body.admin-bar .glightbox-container .gclose {
	top: 47px;
}
@media screen and ( max-width: 782px ) {
	body.admin-bar .glightbox-container .gclose {
		top: 61px;
	}
}

/* ---- GLightbox's own controls, defended (0.27.3, D49; theme audit fault 3) ----
 *
 * We ship GLightbox, so its close / next / previous buttons are ours to keep
 * looking like GLightbox. The vendor sets their size, background and radius
 * (`.glightbox-clean .gclose`, 0,2,0) but no padding, border, shadow or
 * box-sizing, so a theme's `button` rule landed INSIDE the vendor's fixed box
 * and squashed the icon. Measured 2026-09-24 (docs/THEME-AUDIT-2026-09-24.md):
 *
 *   Twenty Twenty-One `button:not(:hover):not(:active):not(.has-background)`
 *     (0,3,1) painted them rgb(40, 48, 61), and its `button` padding made the
 *     arrows 60px wide;
 *   Astra `button` (0,0,1) padding .6em 1em .4em and a box-shadow;
 *   BeTheme, Neve, GeneratePress, Kadence `button` (0,0,1) padding.
 *
 * So the vendor's own values are restated here, and the properties it left
 * open are closed, at (0,4,0) under GLightbox's container and skin classes -
 * above the highest theme rule measured, (0,3,1). No !important: no theme was
 * measured forcing any of these. The background follows the vendor's own
 * breakpoint and hover (glightbox.min.css, @media (min-width: 769px)). */
.glightbox-container.glightbox-clean .gcontainer > .gbtn {
	box-sizing: border-box;
	min-width: 0;
	min-height: 0;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 4px;
	box-shadow: none;
	background-color: rgba( 0, 0, 0, 0.75 );
	color: #fff;
	text-decoration: none;
	text-transform: none;
	letter-spacing: normal;
}

.glightbox-container.glightbox-clean .gcontainer > .gclose {
	width: 35px;
	height: 35px;
}

.glightbox-container.glightbox-clean .gcontainer > .gnext,
.glightbox-container.glightbox-clean .gcontainer > .gprev {
	width: 40px;
	height: 50px;
}

@media ( min-width: 769px ) {
	.glightbox-container.glightbox-clean .gcontainer > .gbtn {
		background-color: rgba( 0, 0, 0, 0.32 );
	}
	.glightbox-container.glightbox-clean .gcontainer > .gbtn:hover {
		background-color: rgba( 0, 0, 0, 0.7 );
	}
}

/* The picture's box, defended (0.27.3, D49; theme audit fault 4). Twenty
 * Twenty-One caps every child of anything whose class CONTAINS
 * "inner-container" - `[class*="inner-container"] > :not(.entry-content)
 * :not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright)
 * :not(.wp-block-separator)` (0,7,0) { max-width:
 * var(--responsive--aligndefault-width) } - and GLightbox's `ginner-container`
 * matches the substring, so the picture drew 610 x 343 in a 1440 x 900 window
 * (every other theme: 1440 x 810). The selector below is GLightbox's own path
 * to the picture's box, eight classes, (0,8,0); the declarations are the
 * vendor's (no max-width, `margin: 0 auto` from `.desc-bottom .gslide-media`).
 * No !important: TT21 uses none. */
.glightbox-container.glightbox-clean .gcontainer .gslider .gslide .gslide-inner-content .ginner-container > .gslide-media {
	max-width: none;
	margin: 0 auto;
}

/* ---- The lightbox caption bar (R8, D45, F-228 - FREE) ----
 *
 * GLightbox's own caption is a white description slot UNDER the picture, sized
 * for paragraphs: on a webcam frame it was a large white box holding one line of
 * file name (the UI review's R8). We do not style that box - we never build it.
 * frontend.js leaves GLightbox's `data-title` and `data-description` unset,
 * which is its own signal to remove `.gslide-description` from the slide
 * altogether, and renders this bar from its own attributes instead: one compact
 * bar over the picture's own edge, title left, description right, and the
 * download / share / play buttons at its right end so a lightbox has ONE bar.
 *
 * The bar lives inside the slide's `.gslide-media`, which is the picture's box,
 * so it sits on the picture rather than at the bottom of the viewport with black
 * between. That needs a containing block, and the ONE third-party selector we
 * scope under is GLightbox's own container class. */
.glightbox-container .gslide.current .gslide-media {
	position: relative;
}

/*
 * "A preview of this image is not available on this server" (D54): the
 * lightbox slide of an image with no picture to show - a private bucket on
 * the free build, or an image too large for the server's image engine.
 * GLightbox shows it as inline content in its own white box; this only sets
 * the words' size and spacing.
 */
.ffg-lb-nopreview {
	margin: 0;
	padding: 24px 16px;
	font-size: 16px;
	line-height: 1.5;
	text-align: center;
}

.ffg-lb-nopreview .ffg-lb-nopreview-text {
	margin: 0 0 12px;
}

.ffg-lb-nopreview .ffg-lb-nopreview-link {
	text-decoration: underline;
}

.ffg-lb-bar {
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	z-index: 1200;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px 12px;
	padding: 6px 8px 6px 14px;
	color: #fff;
	font-size: 14px;
	line-height: 1.35;
	background: linear-gradient( to top, rgba( 0, 0, 0, 0.72 ), rgba( 0, 0, 0, 0 ) );
}

/* Top: the gradient turns over, and the bar keeps clear of GLightbox's close
 * button, which is 35px wide and 20px from the container's right edge. Where the
 * picture is narrower than the viewport that padding is simply whitespace. */
.ffg-lb-bar-top {
	top: 0;
	bottom: auto;
	padding: 6px 62px 6px 14px;
	background: linear-gradient( to bottom, rgba( 0, 0, 0, 0.72 ), rgba( 0, 0, 0, 0 ) );
}

body.admin-bar .ffg-lb-bar-top {
	padding-top: 38px;
}

.ffg-lb-text {
	display: flex;
	flex: 1 1 auto;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 2px 12px;
	min-width: 0;
}

.ffg-lb-title {
	font-weight: 600;
}

/* Right-hand end of the text, not of the bar: the buttons keep that. */
.ffg-lb-desc {
	margin-left: auto;
	font-size: 0.92em;
	opacity: 0.78;
}

.ffg-lb-actions {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	gap: 2px;
	margin-left: auto;
}

/* "Hidden until tapped": the words go, the buttons stay, and so does the bar -
 * the toggle has to be somewhere. With nothing to shade, the gradient goes too. */
.ffg-lb-bar.is-collapsed {
	background: none;
}

.ffg-lb-bar.is-collapsed .ffg-lb-text {
	display: none;
}

/* Zooming a picture is asking to see the picture. GLightbox hides its own
 * description slot on the same signal. */
.zoomed .ffg-lb-bar {
	display: none;
}

/* A button in the bar. Free because the caption toggle is free; the download,
 * share and play buttons the premium pack builds carry the same class, which is
 * what makes one bar look like one bar.
 *
 * "The author chose this" rule, so it WINS, by specificity (0.27.3, D49; theme
 * audit faults 2 and 8). At `.ffg-lb-btn` (0,1,0) it lost to Hello Elementor's
 * `[type="button"]` (0,1,0, reset.css, loaded after us: a 60px button with a
 * pink border) and to every theme `button` rule that set a radius or a shadow.
 * The highest theme rule measured reaching a button is Twenty Twenty-One's
 * `button:not(:hover):not(:active):not(.has-background)` (0,3,1), so the rule is
 * (0,4,0): GLightbox's container and skin classes (the bar and the premium
 * toolbar both live inside it) plus our class and the `type` every one of these
 * buttons carries. Every property a theme's button rule was measured touching
 * is declared, so none is left for a theme to fill in. The states are raised
 * with it, or the base rule would beat our own :hover. No !important: no theme
 * was measured forcing one here. */
.glightbox-container.glightbox-clean .ffg-lb-btn[type="button"] {
	position: relative;
	box-sizing: border-box;
	width: 44px;
	height: 44px;
	min-width: 0;
	min-height: 0;
	padding: 11px;
	margin: 0;
	border: 0;
	border-radius: 0;
	box-shadow: none;
	background: transparent;
	color: #fff;
	font: inherit;
	line-height: 0;
	letter-spacing: normal;
	text-decoration: none;
	text-transform: none;
	cursor: pointer;
	opacity: 0.7;
	transition: opacity 0.2s ease;
	-webkit-appearance: none;
	appearance: none;
}

.glightbox-container.glightbox-clean .ffg-lb-btn[type="button"]:hover,
.glightbox-container.glightbox-clean .ffg-lb-btn[type="button"]:focus-visible,
.glightbox-container.glightbox-clean .ffg-lb-cap-btn[type="button"][aria-pressed="true"] {
	opacity: 1;
	background: transparent;
	color: #fff;
}

.glightbox-container.glightbox-clean .ffg-lb-btn[type="button"]:focus-visible {
	outline: 2px solid rgba( 255, 255, 255, 0.7 );
	outline-offset: -2px;
}

.ffg-lb-btn svg {
	display: block;
	width: 100%;
	height: 100%;
}

/* Phone: the bar stays where it is and the two texts stack, so a title and a
 * date read as two lines instead of one line ending in an ellipsis. */
@media ( max-width: 600px ) {
	.ffg-lb-text {
		flex-direction: column;
	}
	.ffg-lb-desc {
		margin-left: 0;
	}
}

@media ( prefers-reduced-motion: reduce ) {
	.glightbox-container.glightbox-clean .ffg-lb-btn[type="button"] {
		transition: none;
	}
}

/* <fs_premium_only> */
/* ---- Tile styling: border, shadow, loaded effect, hover set (D42, F-252 - PREMIUM) ----
 *
 * Every rule here keys off a class the premium appearance pack appends to the
 * gallery root through `ffg_gallery_config` (`$config['classes']`), so both the
 * class and the rule are absent from the free build: there is nothing to tamper
 * a free gallery into. The DEFAULT of every one of these controls carries no
 * rule at all (ffg-border-none, ffg-shadow-none, ffg-load-fade,
 * ffg-hover-icon-none, ffg-hover-fx-none, ffg-hover-scale-none,
 * ffg-hover-title-fade), which is what makes a gallery that has chosen nothing
 * render exactly as it did before 0.22.0.
 *
 * The BORDER is an absolutely-positioned ring, NOT a border property, and that
 * is deliberate: D35's whole point is that the tile's box is the component's
 * layout primitive and the image box must equal it. A real border - on the tile
 * or on the thumb - would take its width out of that box (or add to it, without
 * border-box), so the img would no longer be the tile's size and the geometry
 * assertion D35 was built on would stop holding. A ring inset to the tile's own
 * edges costs no layout at all, inherits the corner radius, and needs no
 * !important. */

.ffg-gallery.ffg-border-thin .ffg-tile::after,
.ffg-gallery.ffg-border-medium .ffg-tile::after,
.ffg-gallery.ffg-border-thick .ffg-tile::after {
	/* The ring itself; each weight class below gives it its width. Listing the
	 * three classes rather than matching the prefix keeps "None" with no rule and
	 * no pseudo-element at all. */
	content: "";
	position: absolute;
	inset: 0;
	z-index: 2;
	border: 0 solid var( --ffg-tile-line, rgba( 29, 35, 39, 0.25 ) );
	border-radius: inherit;
	pointer-events: none;
}

.ffg-gallery.ffg-border-thin .ffg-tile::after {
	border-width: 1px;
}

.ffg-gallery.ffg-border-medium .ffg-tile::after {
	border-width: 2px;
}

.ffg-gallery.ffg-border-thick .ffg-tile::after {
	border-width: 3px;
}

/* Shadow: three fixed depths. Not a number, because "how far off the page" is a
 * judgement with three good answers and no need for a fourth. */
.ffg-gallery.ffg-shadow-small .ffg-tile {
	box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.12 );
}

.ffg-gallery.ffg-shadow-medium .ffg-tile {
	box-shadow: 0 3px 8px rgba( 0, 0, 0, 0.16 );
}

.ffg-gallery.ffg-shadow-large .ffg-tile {
	box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.22 );
}

/* The loaded effect. The free stylesheet fades an image in when it loads
 * (opacity 0 -> 1 on .is-loaded); these add a transform to that same moment, or
 * take the fade away.
 *
 * "No effect" is one declaration and no transition override: with opacity
 * already 1 there is no change for the free transition to animate, so nothing
 * has to be switched off. */
.ffg-gallery.ffg-load-none .ffg-img {
	opacity: 1;
}

/* Slide up settles from below AND from slightly oversize, so the image covers
 * the thumb for the whole of the movement - a bare translate would show a strip
 * of the thumb background at the leading edge. */
.ffg-gallery.ffg-load-slide .ffg-img {
	transform: translateY( 12px ) scale( 1.08 );
}

.ffg-gallery.ffg-load-slide .ffg-img.is-loaded {
	transform: none;
}

/* Scale up genuinely grows (0.92 -> 1). The fade runs with it, so the thumb
 * background showing at the edges is part of the reveal rather than a gap. */
.ffg-gallery.ffg-load-scale .ffg-img {
	transform: scale( 0.92 );
}

.ffg-gallery.ffg-load-scale .ffg-img.is-loaded {
	transform: none;
}

/* Hover: the image effect. Greyscale stays grey; Colour on hover greys at rest
 * and hands the colour back under the pointer (or to the keyboard, via
 * :focus-within on the tile - the tile IS the link, so focus lands inside it). */
.ffg-gallery.ffg-hover-fx-greyscale .ffg-img,
.ffg-gallery.ffg-hover-fx-colourise .ffg-img {
	filter: grayscale( 1 );
}

.ffg-gallery.ffg-hover-fx-colourise .ffg-tile:hover .ffg-img,
.ffg-gallery.ffg-hover-fx-colourise .ffg-tile:focus-within .ffg-img {
	filter: none;
}

/* Hover: the zoom. Inside the tile, which never moves - the tile keeps its box
 * and its overflow clip, so the grid is untouched and D35 still holds. */
.ffg-gallery.ffg-hover-scale-scaled .ffg-tile:hover .ffg-img,
.ffg-gallery.ffg-hover-scale-scaled .ffg-tile:focus-within .ffg-img {
	transform: scale( 1.03 );
}

.ffg-gallery.ffg-hover-scale-zoomed .ffg-tile:hover .ffg-img,
.ffg-gallery.ffg-hover-scale-zoomed .ffg-tile:focus-within .ffg-img {
	transform: scale( 1.08 );
}

/* ONE transition declaration for the image, listing every class that animates
 * something beyond the free opacity fade. Two rules each owning half of the
 * transition property is how a loaded effect and a hover effect would come to
 * cancel each other. */
.ffg-gallery.ffg-load-slide .ffg-img,
.ffg-gallery.ffg-load-scale .ffg-img,
.ffg-gallery.ffg-hover-fx-colourise .ffg-img,
.ffg-gallery.ffg-hover-scale-scaled .ffg-img,
.ffg-gallery.ffg-hover-scale-zoomed .ffg-img {
	transition: opacity 0.35s ease, transform 0.35s ease, filter 0.3s ease;
}

/* Hover: the LAYER - the tint and the icon, one pseudo-element (D58, 0.31.0).
 *
 * Both sit over the PICTURE only: the thumb's ::before, inset to the thumb's own
 * box, so a caption bar below the image is never tinted and the icon is centred
 * on the picture rather than on picture-plus-bar. The tint is the layer's
 * background COLOUR and the icon is two background IMAGES (the glyph, and the
 * 48px dark disc it sits on, drawn as a hard-edged radial gradient), so the two
 * controls set different properties of one box and compose with no rule for
 * each pair. The thumb's ::after is taken (the folder, Back and broken-image
 * glyphs) and the tile's ::after is the border ring, so this is the one free
 * pseudo-element with the picture's box.
 *
 * Folder tiles take both since 0.31.0 (Steve's ruling 2: every hover effect
 * applies to folders too). The Back tile takes the tint but not the icon: its
 * picture IS a centred arrow, and a second mark on top of it would cover it.
 *
 * z-index 2 lifts the layer over the image (a later positioned sibling). The
 * thumb is made a stacking context (isolation) so that 2 stays INSIDE the
 * picture and an over-the-image caption strip, a later sibling of the thumb,
 * paints above the tint instead of being darkened by it. */
.ffg-gallery.ffg-hover-tint-darken .ffg-tile .ffg-thumb,
.ffg-gallery.ffg-hover-tint-lighten .ffg-tile .ffg-thumb,
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile .ffg-thumb,
.ffg-gallery.ffg-hover-icon-eye .ffg-tile .ffg-thumb,
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile .ffg-thumb {
	isolation: isolate;
}

.ffg-gallery.ffg-hover-tint-darken .ffg-tile .ffg-thumb::before,
.ffg-gallery.ffg-hover-tint-lighten .ffg-tile .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 2;
	background-repeat: no-repeat;
	background-position: center;
	opacity: 0;
	transition: opacity 0.2s ease;
	pointer-events: none;
}

.ffg-gallery.ffg-hover-tint-darken .ffg-tile:hover .ffg-thumb::before,
.ffg-gallery.ffg-hover-tint-darken .ffg-tile:focus-within .ffg-thumb::before,
.ffg-gallery.ffg-hover-tint-lighten .ffg-tile:hover .ffg-thumb::before,
.ffg-gallery.ffg-hover-tint-lighten .ffg-tile:focus-within .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent):hover .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent):focus-within .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent):hover .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent):focus-within .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent):hover .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent):focus-within .ffg-thumb::before {
	opacity: 1;
}

/* The tint: FooGallery's dark and light hover themes, at the strength of the
 * approved mockup (D1: black at 35%). */
.ffg-gallery.ffg-hover-tint-darken .ffg-tile .ffg-thumb::before {
	background-color: rgba( 0, 0, 0, 0.35 );
}

.ffg-gallery.ffg-hover-tint-lighten .ffg-tile .ffg-thumb::before {
	background-color: rgba( 255, 255, 255, 0.35 );
}

/* The icon: a 22px stroke glyph on a 48px disc of black at 55%, the sizes of
 * the approved mockup D1 (Steve's ruling of 2026-09-25; 40px and 20px until
 * then, the download button's size in the extras block below). The disc is the
 * only way a white glyph is legible over an arbitrary photograph. Glyph first,
 * so it paints over its disc. */
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
	background-size: 22px 22px, 48px 48px;
}

/* Magnifier */
.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M16.5 16.5L21 21'/%3E%3C/svg%3E"), radial-gradient( circle closest-side, rgba( 0, 0, 0, 0.55 ) calc( 100% - 1px ), rgba( 0, 0, 0, 0 ) 100% );
}

/* Eye */
.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1.8 12S5.7 5.5 12 5.5 22.2 12 22.2 12 18.3 18.5 12 18.5 1.8 12 1.8 12z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E"), radial-gradient( circle closest-side, rgba( 0, 0, 0, 0.55 ) calc( 100% - 1px ), rgba( 0, 0, 0, 0 ) 100% );
}

/* Arrow */
.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12h14'/%3E%3Cpath d='M12 5l7 7-7 7'/%3E%3C/svg%3E"), radial-gradient( circle closest-side, rgba( 0, 0, 0, 0.55 ) calc( 100% - 1px ), rgba( 0, 0, 0, 0 ) 100% );
}

/* Hover: how the image title strip arrives. Slide up starts it below the tile's
 * own clip, so it rises into view instead of appearing in place. */
.ffg-gallery.ffg-hover-title-slide .ffg-tile-overlay {
	transform: translateY( 100% );
	transition: opacity 0.2s ease, transform 0.2s ease;
}

.ffg-gallery.ffg-hover-title-slide .ffg-file:hover .ffg-tile-overlay,
.ffg-gallery.ffg-hover-title-slide .ffg-file:focus-visible .ffg-tile-overlay {
	transform: none;
}

/* A strip at the TOP of the picture (D58) slides DOWN into view, from above the
 * tile's clip. (0,4,0): above the parked rule, below the hover rule (0,5,0) and
 * before "Always" below, which wins on source order at equal weight. */
.ffg-gallery.ffg-hover-title-slide.ffg-cap-pos-top .ffg-tile-overlay {
	transform: translateY( -100% );
}

.ffg-gallery.ffg-hover-title-instant .ffg-tile-overlay {
	transition: none;
}

/* "Show tile captions: Always" (free, D45) and "how the strip arrives" (premium)
 * are answers to two different questions, and both have to hold: the strip is on
 * screen AND it is not still parked 100% below the tile's own clip. Stated here
 * rather than in the free half because a premium rule may name a free class -
 * the reverse is what the seam forbids - and because this rule has to outrank the
 * slide treatment above, which a free rule of equal specificity could not do
 * from earlier in the file. */
.ffg-gallery.ffg-caption-always.ffg-hover-title-slide .ffg-tile-overlay {
	transform: none;
}

/* Reduced motion. The free stylesheet already takes the image transition off,
 * but these classes are more specific than the rule that does it, so each one
 * has to say so itself - and the loaded-effect transforms have to be neutralised
 * too, or an image would simply arrive offset and stay there. Hover motion goes
 * with them: a movement triggered by the pointer is exactly what this setting
 * asks not to see. The greyscale and colour treatments stay - a colour change is
 * not motion - they simply stop animating. The parked title strip is un-parked at
 * (0,4,0) because the top strip's own parked rule (D58) is (0,4,0), earlier. */
@media ( prefers-reduced-motion: reduce ) {
	.ffg-gallery.ffg-load-slide .ffg-img,
	.ffg-gallery.ffg-load-scale .ffg-img,
	.ffg-gallery.ffg-hover-fx-colourise .ffg-img,
	.ffg-gallery.ffg-hover-scale-scaled .ffg-img,
	.ffg-gallery.ffg-hover-scale-zoomed .ffg-img,
	.ffg-gallery.ffg-hover-title-slide .ffg-tile-overlay,
	.ffg-gallery.ffg-hover-tint-darken .ffg-tile .ffg-thumb::before,
	.ffg-gallery.ffg-hover-tint-lighten .ffg-tile .ffg-thumb::before,
	.ffg-gallery.ffg-hover-icon-zoom .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
	.ffg-gallery.ffg-hover-icon-eye .ffg-tile:not(.ffg-parent) .ffg-thumb::before,
	.ffg-gallery.ffg-hover-icon-arrow .ffg-tile:not(.ffg-parent) .ffg-thumb::before {
		transition: none;
	}
	.ffg-gallery.ffg-load-slide .ffg-img,
	.ffg-gallery.ffg-load-scale .ffg-img,
	.ffg-gallery.ffg-hover-title-slide .ffg-tile > .ffg-tile-overlay,
	.ffg-gallery.ffg-hover-scale-scaled .ffg-tile:hover .ffg-img,
	.ffg-gallery.ffg-hover-scale-scaled .ffg-tile:focus-within .ffg-img,
	.ffg-gallery.ffg-hover-scale-zoomed .ffg-tile:hover .ffg-img,
	.ffg-gallery.ffg-hover-scale-zoomed .ffg-tile:focus-within .ffg-img {
		transform: none;
	}
}

/* ---- Caption styling (D58, 0.31.0 - PREMIUM) ----
 *
 * HOW the caption strip looks, on image and folder tiles alike: the strip is the
 * same `.ffg-tile-meta` child of the tile in both, so one selector serves both.
 * WHEN an image tile's strip shows stays the free `ffg-caption-*` class; nothing
 * here touches `display`, so "Never" still hides every style in every position.
 *
 * The DEFAULT of every control carries no rule (ffg-cap-style-fade,
 * ffg-cap-pos-bottom, ffg-cap-align-left, ffg-cap-size-medium,
 * ffg-cap-weight-bold, and the opacity, which only the solid bar reads), so a
 * gallery that has chosen nothing is drawn by the free rules alone.
 *
 * Sizes are in em so the strip scales with the title: the title size sets the
 * STRIP's font size and the title keeps its own 0.95em inside it, so the
 * padding, the description and the count grow with it. No height is set
 * anywhere - a bar is as tall as its words. */

/* Background opacity: one value per class, read by the solid bar only. */
.ffg-gallery.ffg-cap-bg-100 {
	--ffg-cap-alpha: 1;
}

.ffg-gallery.ffg-cap-bg-90 {
	--ffg-cap-alpha: 0.9;
}

.ffg-gallery.ffg-cap-bg-80 {
	--ffg-cap-alpha: 0.8;
}

.ffg-gallery.ffg-cap-bg-70 {
	--ffg-cap-alpha: 0.7;
}

.ffg-gallery.ffg-cap-bg-60 {
	--ffg-cap-alpha: 0.6;
}

.ffg-gallery.ffg-cap-bg-50 {
	--ffg-cap-alpha: 0.5;
}

.ffg-gallery.ffg-cap-bg-40 {
	--ffg-cap-alpha: 0.4;
}

.ffg-gallery.ffg-cap-bg-30 {
	--ffg-cap-alpha: 0.3;
}

.ffg-gallery.ffg-cap-bg-20 {
	--ffg-cap-alpha: 0.2;
}

.ffg-gallery.ffg-cap-bg-10 {
	--ffg-cap-alpha: 0.1;
}

.ffg-gallery.ffg-cap-bg-0 {
	--ffg-cap-alpha: 0;
}

/* Solid bar: one flat band of the background colour at the chosen opacity. */
.ffg-gallery.ffg-cap-style-solid .ffg-tile > .ffg-tile-meta {
	padding: 0.6em 0.8em;
	line-height: 1.35;
	background: rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), var( --ffg-cap-alpha, 1 ) );
}

/* Text only: no background, and a shadow close and dark enough to hold white
 * words on a bright sky (the mockup's D5). */
.ffg-gallery.ffg-cap-style-text .ffg-tile > .ffg-tile-meta {
	padding: 0.6em 0.8em;
	line-height: 1.35;
	background: none;
	text-shadow: 0 1px 3px rgba( 0, 0, 0, 0.85 ), 0 0 1px rgba( 0, 0, 0, 0.9 );
}

/* Over the image, top: the strip pins to the top edge; Fade turns its gradient
 * and its lead-in padding the other way up. */
.ffg-gallery.ffg-cap-pos-top .ffg-tile > .ffg-tile-meta {
	top: 0;
	bottom: auto;
}

.ffg-gallery.ffg-cap-pos-top.ffg-cap-style-fade .ffg-tile > .ffg-tile-meta {
	padding: 0.5em 0.6em 1.4em;
	background: linear-gradient( to bottom, rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0.65 ), rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0 ) );
}

/* Below the image: the strip leaves the picture and becomes the tile's second
 * block, so the tile is picture + bar. ROW MATCHING is the layout's own
 * stretch, not a measurement: the grid stretches every tile to its row's
 * tallest (align-items, which the free grid sets to start for the overlay
 * layouts), the tile is a column, and the bar takes whatever height the row
 * gives it, with its words centred in it - so every bar in a row ends on one
 * line. The picture keeps its shape (D35's thumb box is untouched), which is
 * also why the tile image geometry assertion still holds.
 *
 * A caption below the image is always on screen (D58): it has no picture to
 * hide behind, and a bar that appeared on hover would open a gap under every
 * tile at rest. "Never" still removes it (display, which is not set here). */
.ffg-gallery.ffg-cap-pos-below .ffg-grid {
	align-items: stretch;
}

.ffg-gallery.ffg-cap-pos-below .ffg-tile {
	display: flex;
	flex-direction: column;
}

.ffg-gallery.ffg-cap-pos-below .ffg-tile > .ffg-thumb {
	flex: 0 0 auto;
}

.ffg-gallery.ffg-cap-pos-below .ffg-tile > .ffg-tile-meta {
	position: relative;
	inset: auto;
	flex: 1 0 auto;
	justify-content: center;
	padding: 0.6em 0.8em;
	line-height: 1.35;
	opacity: 1;
	transform: none;
}

/* Fade has no picture to fade over below the image: it draws a flat bar in its
 * colour at the strength its gradient starts from. */
.ffg-gallery.ffg-cap-pos-below.ffg-cap-style-fade .ffg-tile > .ffg-tile-meta {
	background: rgba( var( --ffg-overlay-rgb, 0, 0, 0 ), 0.65 );
}

/* Text only below the image sits on the page, where a shadow is a smudge. */
.ffg-gallery.ffg-cap-pos-below.ffg-cap-style-text .ffg-tile > .ffg-tile-meta {
	text-shadow: none;
}

/* Below the image nothing is covered, so neither line is clamped: a long name
 * wraps, and the row's bars grow together. Over the image the free clamps stand
 * (the title one line, the description two, with an ellipsis). */
.ffg-gallery.ffg-cap-pos-below .ffg-tile-meta .ffg-tile-name,
.ffg-gallery.ffg-cap-pos-below .ffg-tile-meta .ffg-tile-desc {
	display: block;
	overflow: visible;
	overflow-wrap: anywhere;
}

/* Justified rows: the row height is the PICTURE's height (the packer writes it
 * on the thumb in this position, not on the tile), the tile grows by its bar,
 * and each flex line stretches its tiles to the tallest - the same row match as
 * the grid. (0,4,0) and (0,5,0) to outrank the justified block's own rules. */
.ffg-gallery.ffg-cap-pos-below[data-layout="justified"] .ffg-grid {
	align-items: stretch;
}

.ffg-gallery.ffg-cap-pos-below[data-layout="justified"] .ffg-grid > .ffg-tile {
	align-self: stretch;
}

.ffg-gallery.ffg-cap-pos-below[data-layout="justified"] .ffg-thumb {
	position: relative;
	inset: auto;
	height: auto;
}

/* Text alignment. Left is the default and carries no rule: the strip keeps the
 * text direction it has always had. */
.ffg-gallery.ffg-cap-align-centre .ffg-tile > .ffg-tile-meta {
	text-align: center;
}

.ffg-gallery.ffg-cap-align-right .ffg-tile > .ffg-tile-meta {
	text-align: right;
}

/* Title size. Medium is the default and carries no rule: the title's own 0.95em
 * of the tile's text (14.25px on a 15px theme). Small and Large scale the whole
 * strip, so the padding in em scales with them. */
.ffg-gallery.ffg-cap-size-small .ffg-tile > .ffg-tile-meta {
	font-size: 0.875em;
}

.ffg-gallery.ffg-cap-size-large .ffg-tile > .ffg-tile-meta {
	font-size: 1.125em;
}

/* Title weight. Bold is the default: the 600 the title has always had. */
.ffg-gallery.ffg-cap-weight-normal .ffg-tile-meta .ffg-tile-name {
	font-weight: 400;
}

/* Touch screens (no hover): an "On hover" strip is always shown (the free rule),
 * so the slide-up reveal must not leave it parked off the picture. (0,4,0), after
 * the top strip's parked rule. */
@media ( hover: none ) {
	.ffg-gallery.ffg-hover-title-slide .ffg-tile > .ffg-tile-overlay {
		transform: none;
	}
}

/* ---- Page layouts (SPEC §10, §14 - PREMIUM: full-width + canvas) ---- */

/*
 * Full width (0.27.3, D49; theme audit fault 1): the wrapper carries core's
 * `alignfull` class (FFG_Layouts::wrap_full_width()) and THIS STYLESHEET SETS
 * NO WIDTH OR MARGIN ON IT. The width is the theme's full width - exactly the
 * box a full-width image block gets on the same page - because only the theme
 * knows its own layout.
 *
 * Until 0.27.2 the wrapper broke out with its own arithmetic (`width: 100vw;
 * margin: calc( 50% - 50vw )`), and that failed three ways, all measured:
 *   - Twenty Twenty-Five: core's `.is-layout-constrained > :where(:not(
 *     .alignleft):not(.alignright):not(.alignfull))` (0,1,0) capped it at the
 *     content size and forced `margin-left/right: auto !important`;
 *   - Twenty Twenty-One: `.entry-content > :not(.alignwide):not(.alignfull)...`
 *     (0,6,0) capped it at 610px;
 *   - GeneratePress (right sidebar) and OceanWP (right sidebar): the calc()
 *     assumes the content column is centred in the window, so the gallery
 *     landed at x = -190 / -183.5, clipped at the left edge and drawn over the
 *     sidebar. CSS cannot know where a column sits; the theme does.
 * Every one of those rules exempts `.alignfull` by name, and the sidebar
 * themes ship an `.alignfull` rule that keeps full-width content beside the
 * sidebar, so the class alone answers all three with no !important.
 *
 * What that means on a theme page layout WITH a sidebar, or a BOXED one (Astra's
 * separate container): the gallery is as wide as the theme lets full-width
 * content be there, which is not the window. That is the documented behaviour
 * (Steve's ruling, 2026-09-24): Full width needs a theme page layout without a
 * sidebar; Canvas is the option with no theme chrome at all.
 */
.ffg-layout-full .ffg-gallery {
	/* Comfortable side gutters so tiles never touch the viewport edge. */
	padding-left: clamp( 12px, 4vw, 48px );
	padding-right: clamp( 12px, 4vw, 48px );
}

/*
 * Canvas: the plugin owns the whole page (no theme header/footer). Give the
 * gallery breathing room and let it fill the screen.
 */
body.ffg-canvas {
	margin: 0;
	background: #fff;
}
.ffg-canvas-main {
	box-sizing: border-box;
	min-height: 100vh;
	padding: clamp( 16px, 4vw, 56px );
}
@media ( prefers-color-scheme: dark ) {
	body.ffg-canvas {
		background: #111;
		color: #eee;
	}
}

/* ---- Justified rows layout (SPEC §14 appearance pack - PREMIUM) ----
 *
 * Activation surface: the free frontend.js applyConfig() stamps
 * data-layout="{layout}" on the .ffg-gallery root from payload.gallery.layout,
 * which the ffg_gallery_config filter (FFG_Appearance) raises to 'justified'.
 * The packer (assets/js/justified__premium_only.js) writes an explicit
 * width/height onto every .ffg-tile; the rules below turn the grid into flowing
 * rows and let each thumb fill its (packer-sized) tile, so every row is a clean
 * band of images - image rows and folder rows alike.
 */
.ffg-gallery[data-layout="justified"] .ffg-grid {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	gap: var( --ffg-gap );
	/* The packer decides where every tile goes, so the free Grid alignment (D42)
	 * has nothing to align here: a justified row is full by construction and the
	 * last one is packed to the same rule. Stated rather than left to the cascade,
	 * because `justify-content` from an alignment class WOULD otherwise reach this
	 * flex container. */
	justify-content: flex-start;
}

/* The packer owns each tile's box; keep flex from resizing it. The selector
 * carries `.ffg-grid >` so it matches the free Grid alignment rule's specificity
 * (D42 gives the aligned layouts a flex basis of their own) and wins on source
 * order - the packer's measured width must beat any basis. */
.ffg-gallery[data-layout="justified"] .ffg-grid > .ffg-tile {
	flex: 0 0 auto;
	align-self: flex-start;
}

/* Smooth reflow on resize - but only after the first pack (JS adds the class),
 * so tiles never animate up from their unpacked size on load. */
.ffg-gallery[data-layout="justified"] .ffg-grid.ffg-justified-ready .ffg-tile {
	transition: width 0.2s ease, height 0.2s ease;
}

/* The thumb fills the whole tile (the packer sized the tile to the image), so
 * the fixed 1:1 aspect ratio is turned off here. */
.ffg-gallery[data-layout="justified"] .ffg-thumb {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	aspect-ratio: auto;
}
@supports not ( aspect-ratio: 1 / 1 ) {
	.ffg-gallery[data-layout="justified"] .ffg-thumb {
		height: 100%;
		padding-bottom: 0;
	}
}

/* Folder + Back tile name overlay: NOT here any more. Since 0.16.2 the grid
 * renders folder tiles the same way, so the one shared rule set
 * (".ffg-folder .ffg-tile-meta" and the name/count colours) lives in the FREE
 * Tile meta section above and covers both layouts - which also guarantees the
 * appearance-pack colour vars apply identically to each. Only the
 * justified-SPECIFIC sizing below stays premium-scoped. */

/* Notices/errors are flex children of the grid here; span the whole row. */
.ffg-gallery[data-layout="justified"] .ffg-grid > .ffg-notice,
.ffg-gallery[data-layout="justified"] .ffg-grid > .ffg-error {
	flex: 0 0 100%;
}

/* Reduced motion: never animate a repack. */
@media ( prefers-reduced-motion: reduce ) {
	.ffg-gallery[data-layout="justified"] .ffg-grid.ffg-justified-ready .ffg-tile {
		transition: none;
	}
}
/* </fs_premium_only> */

/* <fs_premium_only> */
/* ---- Frontend extras: downloads / sharing / autoplay (SPEC §14 - PREMIUM) ----
 *
 * All added by assets/js/extras__premium_only.js. Icons are inline SVG that
 * inherit currentColor, so each control's colour is set once on the button.
 */

/* THE BUTTONS ON THE GRID ARE "THE AUTHOR CHOSE THIS" RULES (0.27.3, D49; theme
 * audit faults 2, 7 and 8). At (0,1,0) they lost to theme button rules,
 * measured 2026-09-24 (docs/THEME-AUDIT-2026-09-24.md):
 *   Hello Elementor `[type="button"]` (0,1,0, reset.css, loaded AFTER us, so it
 *     wins the tie): a pink 1px border, 3px radius, transparent background,
 *     padding .5rem 1rem - the share button 50px wide, the download 48px;
 *   Twenty Twenty-One `button:not(:hover):not(:active):not(.has-background)`
 *     (0,3,1): both painted rgb(40, 48, 61);
 *   Astra `button` (0,0,1): a faint box-shadow.
 * So both rules are (0,4,0) - the gallery root and its state class, our class,
 * and the `type` both buttons carry - above the highest theme rule measured,
 * and declare every property a theme's button rule was measured touching. The
 * states are raised with them, or the base rule would beat our own :hover. No
 * !important: no theme was measured forcing one here. */

/* Tile download button: top-right of an image tile, a dark scrim circle with a
 * white glyph. Hidden until the tile is hovered or keyboard-focused; always
 * shown where there is no hover (touch). */
.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"] {
	position: absolute;
	top: 6px;
	right: 6px;
	z-index: 2;
	box-sizing: border-box;
	width: 32px;
	height: 32px;
	min-width: 0;
	min-height: 0;
	padding: 6px;
	margin: 0;
	border: 0;
	border-radius: 50%;
	box-shadow: none;
	background: rgba( 0, 0, 0, 0.55 );
	color: #fff;
	font: inherit;
	line-height: 0;
	letter-spacing: normal;
	text-decoration: none;
	text-transform: none;
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.2s ease, background 0.2s ease;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"]:hover,
.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"]:focus-visible {
	background: rgba( 0, 0, 0, 0.75 );
	color: #fff;
}
.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"]:focus-visible {
	outline: 2px solid #fff;
	outline-offset: 1px;
}
.ffg-dl-btn svg {
	display: block;
	width: 100%;
	height: 100%;
}
.ffg-gallery .ffg-tile:hover .ffg-dl-btn[type="button"],
.ffg-gallery .ffg-tile:focus-within .ffg-dl-btn[type="button"] {
	opacity: 1;
}
@media ( hover: none ) {
	.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"] {
		opacity: 1;
	}
}

/* Folder share button: pinned to the gallery's top-right corner (the JS adds
 * .ffg-has-share, which makes the gallery the positioning context). */
.ffg-gallery.ffg-has-share {
	position: relative;
}
.ffg-gallery.ffg-has-share > .ffg-share-folder[type="button"] {
	position: absolute;
	top: 0;
	right: 0;
	z-index: 3;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	width: 34px;
	height: 34px;
	min-width: 0;
	min-height: 0;
	padding: 7px;
	margin: 0;
	border: 1px solid rgba( 0, 0, 0, 0.15 );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background: rgba( 255, 255, 255, 0.9 );
	color: inherit;
	font: inherit;
	line-height: 0;
	letter-spacing: normal;
	text-decoration: none;
	text-transform: none;
	cursor: pointer;
	opacity: 0.75;
	transition: opacity 0.2s ease;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery.ffg-has-share > .ffg-share-folder[type="button"]:hover,
.ffg-gallery.ffg-has-share > .ffg-share-folder[type="button"]:focus-visible {
	opacity: 1;
	background: rgba( 255, 255, 255, 0.9 );
	color: inherit;
}
.ffg-share-folder svg {
	display: block;
	width: 100%;
	height: 100%;
}

/* The breadcrumb makes room for the share button (0.27.3, D49; theme audit
 * fault 5). The button is absolutely positioned in the gallery's top-right
 * corner, and the trail reserved nothing for it, so a long trail ran
 * underneath: 48.6px of "2026-09 - September" under the button on Hello
 * Elementor at 375, 30.1px on Astra. Not a theme leaking in - the theme only
 * decides the font size, and so where the trail ends. The reserve is the
 * button's own footprint: its 34px plus a gap on the inline end, and at least
 * its height, so the trail wraps BEFORE the button and the row is always tall
 * enough to hold it (the crumbs centre on it). `padding-right` because the
 * button is pinned with `right: 0`. */
.ffg-gallery.ffg-has-share .ffg-breadcrumb {
	min-height: 34px;
	padding-right: calc( 34px + 0.5em );
}
/* With a gallery bar (0.28.0) the TOOLS are what reach the right-hand edge, so
 * the bar reserves the button's footprint and the trail inside it does not
 * reserve it twice. */
.ffg-gallery.ffg-has-share .ffg-bar {
	min-height: 34px;
	padding-right: calc( 34px + 0.5em );
}
.ffg-gallery.ffg-has-share .ffg-bar > .ffg-breadcrumb {
	min-height: 0;
	padding-right: 0;
}

/* The search box (F-272, 0.28.0): added to the gallery bar by
 * assets/js/search__premium_only.js. A text input in the content is reached by
 * every theme's form rules (input[type="search"] at (0,1,1) is common, and some
 * themes style it far higher), so this is the D49 pattern at (0,5,1): declare
 * every property a theme touches on a field. */
.ffg-gallery .ffg-bar .ffg-search {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin: 0;
}
.ffg-gallery .ffg-bar .ffg-search input.ffg-search-input[type="search"] {
	box-sizing: border-box;
	width: 14em;
	min-width: 0;
	max-width: 100%;
	height: 34px;
	min-height: 0;
	margin: 0;
	padding: 0 10px;
	border: 1px solid var( --ffg-control-line );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background: var( --ffg-control-bg );
	color: var( --ffg-control-fg );
	font-family: inherit;
	font-size: 0.9em;
	font-weight: 400;
	line-height: 32px;
	letter-spacing: normal;
	text-transform: none;
	text-decoration: none;
	outline: 0;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery .ffg-bar .ffg-search input.ffg-search-input[type="search"]:focus {
	border-color: var( --ffg-control-fg );
	box-shadow: none;
}
.ffg-gallery .ffg-bar .ffg-search input.ffg-search-input[type="search"]:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 1px;
}
.ffg-gallery .ffg-bar .ffg-search input.ffg-search-input[type="search"]::placeholder {
	color: var( --ffg-control-fg );
	opacity: 0.6;
}
/* Clear search: a text button beside the box, shown while results are. */
.ffg-gallery .ffg-bar .ffg-search .ffg-search-clear[type="button"] {
	box-sizing: border-box;
	width: auto;
	min-width: 0;
	height: 34px;
	min-height: 0;
	margin: 0;
	padding: 0 12px;
	border: 1px solid var( --ffg-control-line );
	border-radius: var( --ffg-radius );
	box-shadow: none;
	background: var( --ffg-control-bg );
	color: var( --ffg-control-fg );
	font-family: inherit;
	font-size: 0.9em;
	font-weight: 400;
	line-height: 32px;
	letter-spacing: normal;
	text-transform: none;
	text-decoration: none;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
}
.ffg-gallery .ffg-bar .ffg-search .ffg-search-clear[type="button"]:hover,
.ffg-gallery .ffg-bar .ffg-search .ffg-search-clear[type="button"]:focus-visible {
	background: var( --ffg-control-hover );
	color: var( --ffg-control-fg );
	border-color: var( --ffg-control-fg );
}
.ffg-gallery .ffg-bar .ffg-search .ffg-search-clear[type="button"]:focus-visible {
	outline: 2px solid var( --ffg-control-fg );
	outline-offset: 1px;
}
.ffg-gallery .ffg-bar .ffg-search .ffg-search-clear[hidden] {
	display: none;
}
@media ( max-width: 480px ) {
	.ffg-gallery .ffg-bar .ffg-search input.ffg-search-input[type="search"] {
		width: 100%;
	}
	.ffg-gallery .ffg-bar .ffg-search {
		flex: 1 1 100%;
	}
}

/* Lightbox toolbar: the download, share and play buttons for the current slide.
 *
 * Since 0.24.0 (R8, D45) its home is the free caption bar's `.ffg-lb-actions`
 * slot - ONE bar, with the words at the left and the controls at the right,
 * instead of a caption box under the picture and a row of icons above it. The
 * free code moves this element into the bar on every slide, so the element (and
 * the play button's own state) survives the move.
 *
 * The absolute positioning below is the FALLBACK for the case there is no bar to
 * move into, which is where these buttons sat until 0.24.0: top-right, just left
 * of .gclose (35px wide at right: 20px, so its box ends 55px from the edge -
 * measured overlap at 44px, 2026-07-07). `.ffg-lb-bar .ffg-lb-tools` unsets it.
 * The button styling itself is in the free half of this stylesheet, because the
 * bar's own caption toggle is a free button of the same kind. */
.ffg-lb-tools {
	position: absolute;
	top: 10px;
	right: 66px;
	z-index: 1200;
	display: flex;
	align-items: center;
	gap: 2px;
}
/* Same admin-bar clearance as the close button (see the free rule above). */
body.admin-bar .ffg-lb-tools {
	top: 42px;
}
@media screen and ( max-width: 782px ) {
	body.admin-bar .ffg-lb-tools {
		top: 56px;
	}
}
/* In the bar it is simply a row of buttons at the end of a flex line. */
.ffg-lb-bar .ffg-lb-tools {
	position: static;
	top: auto;
	right: auto;
}

/* The slideshow's hold indicator (0.29.0, D52): shown only while the slideshow
 * waits for the next picture to arrive instead of moving onto a blank slide. A
 * button-sized box so the row does not shift when it appears, holding a small
 * ring that turns; the words are for a screen reader (it is a live region). */
.ffg-lb-wait {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
}
.ffg-lb-wait[hidden] {
	display: none;
}
.ffg-lb-wait::before {
	content: "";
	box-sizing: border-box;
	width: 18px;
	height: 18px;
	border: 2px solid rgba( 255, 255, 255, 0.35 );
	border-top-color: #fff;
	border-radius: 50%;
	animation: ffg-lb-spin 0.9s linear infinite;
}
.ffg-lb-wait .ffg-lb-wait-text {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect( 0, 0, 0, 0 );
	white-space: nowrap;
	border: 0;
}
@keyframes ffg-lb-spin {
	to {
		transform: rotate( 360deg );
	}
}
@media ( prefers-reduced-motion: reduce ) {
	.ffg-lb-wait::before {
		animation-duration: 3s;
	}
}

/* Copied-link confirmation: a transient dark chip below the button. */
.ffg-tip {
	position: absolute;
	top: 100%;
	right: 0;
	margin-top: 4px;
	padding: 3px 7px;
	border-radius: var( --ffg-radius );
	background: rgba( 0, 0, 0, 0.85 );
	color: #fff;
	font-size: 12px;
	line-height: 1.3;
	white-space: nowrap;
	pointer-events: none;
	opacity: 0;
	transform: translateY( -2px );
	transition: opacity 0.15s ease, transform 0.15s ease;
}
.is-copied .ffg-tip {
	opacity: 1;
	transform: translateY( 0 );
}

@media ( prefers-reduced-motion: reduce ) {
	.ffg-gallery .ffg-tile .ffg-dl-btn[type="button"],
	.ffg-gallery.ffg-has-share > .ffg-share-folder[type="button"],
	.ffg-tip {
		transition: none;
	}
}
/* </fs_premium_only> */
