/*
Version: 1.0.11
Version date: 09-08-2026
*/

/**
 * The player.
 *
 * Loaded only on pages that actually carry a slider.
 *
 * ## Everything themeable is a custom property
 *
 * The renderer sets them per instance in a `style` attribute, so one stylesheet
 * serves every slider on the site and a theme overrides any of it with one rule.
 * No CSS is generated at runtime.
 *
 * ## ⚠ Only `transform` and `opacity` are ever animated
 *
 * Nothing here animates `left`, `top`, `width` or `height`. On a mid-range phone
 * that is the difference between sixty frames a second and a visible stutter.
 * `will-change` is set by the transition and dropped again — a permanent one on
 * twenty slides is a large amount of GPU memory for no benefit.
 */

/* ⚠ The variables are declared for `.pcsl-outside` as well as `.pcsl`.
 *
 * A background slider's controls are rendered *outside* its root — they have to
 * be, or they are stuck under the page content; see `Renderer::outside_controls()`.
 * Outside it they inherit nothing, so `var(--pcsl-control)` resolves to nothing
 * and every control loses its backdrop. Declaring them on both is why that layer
 * looks identical to the one inside.
 *
 * The layout properties below stay on `.pcsl` alone: the outside layer is a fixed
 * full-viewport sheet and has no business with `max-width` or `margin-inline`. */
.pcsl,
.pcsl-outside {
	--pcsl-speed: 600ms;
	--pcsl-radius: 0px;
	--pcsl-backdrop: #000;
	--pcsl-aspect: 16/9;
	--pcsl-control: rgb(0 0 0 / 45%);
	--pcsl-control-hover: rgb(0 0 0 / 70%);
	--pcsl-control-ink: #fff;
	--pcsl-arrow-size: 44px;
	--pcsl-dot-size: 10px;
	--pcsl-focus: #fff;
}

.pcsl {
	position: relative;
	max-width: var(--pcsl-max-width, none);
	margin-inline: 0;
}

.pcsl-align-center { margin-inline: auto; }
.pcsl-align-right  { margin-inline: auto 0; }

/* ---------------------------------------------------------------------------
 * The frame
 * ------------------------------------------------------------------------ */

.pcsl-frame {
	position: relative;
	overflow: hidden;
	border-radius: var(--pcsl-radius);
	background: var(--pcsl-backdrop);
}

/* ⚠ A ratio is always set, from the setting or from the first image, so the
   frame has a height before any picture arrives. Without it every slider is a
   layout shift on every page load. */
.pcsl-size-aspect .pcsl-frame,
.pcsl-size-natural .pcsl-frame {
	aspect-ratio: var(--pcsl-aspect);
}

.pcsl-size-height .pcsl-frame {
	height: var(--pcsl-height, 480px);
}

.pcsl-track {
	position: relative;
	width: 100%;
	height: 100%;
	margin: 0;
	padding: 0;
	list-style: none;
}

/* ---------------------------------------------------------------------------
 * ⚠⚠ `touch-action` is the single most important line in this file on a phone
 *
 * It tells the browser, before any script runs, which gestures belong to the
 * page and which to us. A horizontal slider claims horizontal drags and leaves
 * `pan-y` to the page — so scrolling *through* a slider works exactly as it does
 * everywhere else. Get this wrong and the page feels broken in a way readers
 * cannot describe and will not report.
 *
 * It cannot be set from JavaScript on boot: by the time `pointerdown` fires, the
 * browser has already decided whose gesture it is. Hence a server-rendered class.
 * ------------------------------------------------------------------------ */

.pcsl-swipe .pcsl-frame {
	touch-action: pan-y;
	cursor: grab;
	/* A drag that starts on a photograph must not begin a native image drag or a
	   text selection instead. */
	-webkit-user-select: none;
	user-select: none;
	-webkit-user-drag: none;
}

.pcsl-swipe.pcsl-axis-v .pcsl-frame {
	touch-action: pan-x;
}

.pcsl-swipe.is-dragging .pcsl-frame {
	cursor: grabbing;
}

.pcsl-swipe .pcsl-img {
	-webkit-user-drag: none;
	pointer-events: none;
}

.pcsl-slide {
	margin: 0;
	padding: 0;
	list-style: none;
	/* ⚠⚠ Load-bearing. The caption and hotspot layers are `position: absolute;
	   inset: 0`, so they resolve against the nearest POSITIONED ancestor — and
	   without this that is `.pcsl-track`, not the slide they belong to.

	   The stacked effects happen to hide the bug, because they make every slide
	   `position: absolute` themselves (below). The sliding ones do not: their
	   slides are plain flex items, so on `slide` / `slide-v` all N caption
	   layers and all N hotspot layers pile up at the track's origin. What you
	   see is one caption on the first picture reading the LAST picture's text —
	   because the last layer in the DOM paints on top — and nothing at all on
	   any other picture, because the whole pile translates away with the track.

	   It reads as «captions are broken», which is why it is worth the comment:
	   the markup was right the entire time. */
	position: relative;
}

.pcsl-img {
	display: block;
	width: 100%;
	height: 100%;
	/* The focal point is written inline per slide, so the part of a photograph
	   that matters survives being cropped into a phone-shaped frame. */
	object-position: center center;
}

.pcsl-fit-cover .pcsl-img   { object-fit: cover; }
.pcsl-fit-contain .pcsl-img { object-fit: contain; }

/* ---------------------------------------------------------------------------
 * Stacked effects — fade, cut, zoom, kenburns
 *
 * Every slide sits on top of the others; only the active one is painted. This
 * is the layout the *transition* uses; the sliding effects below replace it.
 * ------------------------------------------------------------------------ */

.pcsl-fx-fade .pcsl-slide,
.pcsl-fx-cut .pcsl-slide,
.pcsl-fx-zoom .pcsl-slide,
.pcsl-fx-kenburns .pcsl-slide {
	position: absolute;
	inset: 0;
	opacity: 0;
	/* ⚠ `visibility` as well as `opacity`: a fully transparent slide is still
	   in the tab order and still hit-tested, so links on slide 7 would be
	   reachable while slide 1 is showing. */
	visibility: hidden;
	transition: opacity var(--pcsl-speed) ease, visibility 0s linear var(--pcsl-speed);
}

.pcsl-fx-fade .pcsl-slide.is-active,
.pcsl-fx-cut .pcsl-slide.is-active,
.pcsl-fx-zoom .pcsl-slide.is-active,
.pcsl-fx-kenburns .pcsl-slide.is-active {
	opacity: 1;
	visibility: visible;
	transition-delay: 0s;
}

/* No animation at all — the honest option, and what everything falls back to
   when the reader has asked for reduced motion. */
.pcsl-fx-cut .pcsl-slide {
	transition: none;
}

.pcsl-fx-zoom .pcsl-slide .pcsl-img {
	transform: scale(1.06);
	transition: transform var(--pcsl-speed) ease;
}

.pcsl-fx-zoom .pcsl-slide.is-active .pcsl-img {
	transform: scale(1);
}

/* A slow drift, only meaningful while the slider is running on its own. */
.pcsl-fx-kenburns .pcsl-slide.is-active .pcsl-img {
	animation: pcsl-kenburns 12s ease-out forwards;
}

@keyframes pcsl-kenburns {
	from { transform: scale(1) translate3d(0, 0, 0); }
	to   { transform: scale(1.12) translate3d(-1.5%, -1%, 0); }
}

/* ---------------------------------------------------------------------------
 * Sliding effects
 *
 * A real track: the slides sit side by side and the whole strip is moved by one
 * transform. It is the layout a finger-following swipe needs, which is why the
 * sliding effects are built this way rather than as stacked transforms — M3
 * only has to change the number in `--pcsl-index`.
 * ------------------------------------------------------------------------ */

.pcsl-fx-slide .pcsl-track,
.pcsl-fx-slide-v .pcsl-track {
	display: flex;
	transition: transform var(--pcsl-speed) cubic-bezier(.4, 0, .2, 1);
	will-change: transform;
}

/* ⚠ `--pcsl-drag` is added to the same transform rather than being a second one.
   During a swipe the player writes pixels into it and the strip follows the
   finger; the rest of the time it is `0px` and costs nothing. Two separate
   transforms would fight each other the moment a drag was released mid-flight. */
.pcsl-fx-slide .pcsl-track {
	transform: translate3d(calc(var(--pcsl-index, 0) * -100% + var(--pcsl-drag, 0px)), 0, 0);
}

.pcsl-fx-slide-v .pcsl-track {
	flex-direction: column;
	transform: translate3d(0, calc(var(--pcsl-index, 0) * -100% + var(--pcsl-drag, 0px)), 0);
}

/* While a finger is down the strip must track it exactly, so the easing that
   makes a released swipe feel right is precisely what must not apply. */
.pcsl.is-dragging .pcsl-track {
	transition: none;
}

.pcsl-fx-slide .pcsl-slide,
.pcsl-fx-slide-v .pcsl-slide {
	flex: 0 0 100%;
	height: 100%;
}

/* ---------------------------------------------------------------------------
 * ⚠ Reduced motion wins over every effect
 *
 * Not a softer animation — none. Someone who has asked their system to reduce
 * motion has usually done so because motion makes them ill.
 * ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
	.pcsl .pcsl-track,
	.pcsl .pcsl-slide,
	.pcsl .pcsl-img {
		transition: none !important;
		animation: none !important;
	}
}

.pcsl-reduced .pcsl-track,
.pcsl-reduced .pcsl-slide,
.pcsl-reduced .pcsl-img {
	transition: none !important;
	animation: none !important;
}

/* ---------------------------------------------------------------------------
 * Controls
 *
 * Everything in the frame sits over an unknown photograph, so each control
 * carries its own backdrop rather than trusting the picture beneath it.
 * ------------------------------------------------------------------------ */

.pcsl-slot-frame {
	position: absolute;
	inset: 0;
	/* The layer is transparent to the pointer; the controls inside it are not.
	   Otherwise it would swallow every click meant for the picture — and, from
	   M5, every hotspot. */
	pointer-events: none;
}

.pcsl-slot-frame > * {
	pointer-events: auto;
}

/* ⚠ The `:where()` is load-bearing, and this cost a debugging cycle.
 *
 * A slider is dropped into an unknown theme, and themes style bare `button`
 * heavily — Twenty Seventeen gives every button a dark background and its own
 * padding. So the reset has to beat a plain `button` selector, which means it
 * cannot be a bare element selector itself.
 *
 * But `.pcsl button` is specificity (0,1,1) and `.pcsl-arrow` is (0,1,0), so the
 * reset then beats the *component* too — and the arrows render with no backdrop
 * and the theme's ink colour, which is exactly what happened.
 *
 * `:where()` contributes nothing, so this is (0,1,0): higher than the theme's
 * `button` (0,0,1), equal to every component class — and equal means source
 * order decides. **Every component rule must therefore stay below this block.** */
.pcsl :where(button),
.pcsl-outside :where(button) {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	font: inherit;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
}

.pcsl :focus-visible,
.pcsl-outside :focus-visible {
	outline: 3px solid var(--pcsl-focus);
	outline-offset: 2px;
	/* A white ring alone disappears against a pale photograph; the dark shadow
	   under it keeps the focus visible on any image. */
	box-shadow: 0 0 0 5px rgb(0 0 0 / 55%);
}

/* ---- arrows ------------------------------------------------------------ */

.pcsl-arrow {
	position: absolute;
	display: grid;
	place-items: center;
	/* 44px is the smallest target most guidance considers reliable with a
	   thumb, and this plugin is aimed at thumbs. */
	width: var(--pcsl-arrow-size);
	height: var(--pcsl-arrow-size);
	border-radius: 50%;
	background: var(--pcsl-control);
	color: var(--pcsl-control-ink);
	transition: background .15s ease, opacity .15s ease;
}

.pcsl-arrow:hover { background: var(--pcsl-control-hover); }

.pcsl-arrow[disabled] {
	opacity: .35;
	cursor: default;
}

.pcsl-arrow svg {
	width: 60%;
	height: 60%;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.4;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.pcsl-arrows-sides .pcsl-arrow-prev { top: 50%; left: 10px; transform: translateY(-50%); }
.pcsl-arrows-sides .pcsl-arrow-next { top: 50%; right: 10px; transform: translateY(-50%); }

.pcsl-arrows-ends .pcsl-arrow-prev { left: 50%; top: 10px; transform: translateX(-50%); }
.pcsl-arrows-ends .pcsl-arrow-next { left: 50%; bottom: 10px; transform: translateX(-50%); }

/* ---- dots -------------------------------------------------------------- */

.pcsl-dots {
	display: flex;
	gap: 8px;
	justify-content: center;
	align-items: center;
	padding: 10px 0;
}

.pcsl-dots-over {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 6px;
	padding: 6px 0;
}

.pcsl-dot {
	display: grid;
	place-items: center;
	/* The button is a comfortable target; the visible dot inside it is small.
	   A 10px tap target is not a control, it is a dare. */
	width: 28px;
	height: 28px;
	border-radius: 50%;
}

.pcsl-dot-mark {
	width: var(--pcsl-dot-size);
	height: var(--pcsl-dot-size);
	border-radius: 50%;
	background: currentColor;
	opacity: .35;
	transition: opacity .15s ease, transform .15s ease;
}

.pcsl-dots-over .pcsl-dot-mark {
	background: #fff;
	box-shadow: 0 0 3px rgb(0 0 0 / 60%);
}

.pcsl-dot:hover .pcsl-dot-mark { opacity: .7; }

.pcsl-dot.is-active .pcsl-dot-mark {
	opacity: 1;
	transform: scale(1.25);
}

/* ---- counter ----------------------------------------------------------- */

.pcsl-counter {
	margin: 0;
	padding: 10px 0;
	text-align: center;
	font-size: .85em;
	font-variant-numeric: tabular-nums;
	opacity: .8;
}

.pcsl-counter-sep { margin: 0 .35em; }

/* ---- play / pause ------------------------------------------------------ */

.pcsl-playpause {
	position: absolute;
	display: grid;
	place-items: center;
	width: 36px;
	height: 36px;
	border-radius: 50%;
	background: var(--pcsl-control);
	color: var(--pcsl-control-ink);
}

.pcsl-playpause:hover { background: var(--pcsl-control-hover); }

/* ⚠⚠ Scoped to the button. These were written unqualified — a bare
   `.pcsl-at-top-right { right: 10px; top: 10px }` — inside the play/pause
   section, so they matched **every caption and every hotspot** in that corner as
   well. Nothing broke, because `.pcsl-caption.pcsl-at-top-right` is a class more
   specific and wins, but the whole zone system rests on that accident holding.
   A zone class says *where a thing is*; only the component's own class may say
   what that means for it. */
.pcsl-playpause.pcsl-at-bottom-right { right: 10px; bottom: 10px; }
.pcsl-playpause.pcsl-at-bottom-left  { left: 10px; bottom: 10px; }
.pcsl-playpause.pcsl-at-top-right    { right: 10px; top: 10px; }
.pcsl-playpause.pcsl-at-top-left     { left: 10px; top: 10px; }

.pcsl-playpause svg {
	width: 55%;
	height: 55%;
	fill: currentColor;
}

/* The icon shows what pressing it will do. While the slider is playing, the
   button offers a pause. */
.pcsl-playpause .pcsl-icon-play { display: none; }
.pcsl .pcsl-playpause .pcsl-icon-pause { display: none; }
.pcsl.is-playing .pcsl-playpause .pcsl-icon-pause { display: block; }
.pcsl.is-playing .pcsl-playpause .pcsl-icon-play { display: none; }
.pcsl:not(.is-playing) .pcsl-playpause .pcsl-icon-play { display: block; }

/* ---- progress ---------------------------------------------------------- */

.pcsl-progress {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 3px;
	background: rgb(255 255 255 / 25%);
}

.pcsl-progress-bar {
	display: block;
	height: 100%;
	background: #fff;
	transform-origin: left center;
	transform: scaleX(0);
}

.pcsl.is-playing .pcsl-progress-bar {
	animation: pcsl-progress var(--pcsl-progress-duration, 5000ms) linear forwards;
}

@keyframes pcsl-progress {
	from { transform: scaleX(0); }
	to   { transform: scaleX(1); }
}

/* ---------------------------------------------------------------------------
 * Captions
 *
 * Rendered inside each slide, so a caption fades when its picture fades and
 * slides when it slides — no JavaScript involved in any of that.
 * ------------------------------------------------------------------------ */

.pcsl-captions {
	position: absolute;
	inset: 0;
	/* The layer is transparent to the pointer; a caption is words, not a
	   control, and it must not swallow a swipe or a hotspot beneath it. */
	pointer-events: none;
	/* ⚠ A caption sits over an unknown photograph. Without a shadow, white text
	   on a pale sky is invisible — and pale skies are common. */
	text-shadow: 0 1px 3px rgb(0 0 0 / 55%);
	color: var(--pcsl-layer-ink, #fff);
	font-size: var(--pcsl-layer-size, clamp(13px, 1.6vw, 17px));
	line-height: 1.35;
}

.pcsl-caption {
	position: absolute;
	max-width: min(80%, 46ch);
	padding: 6px 10px;
}

/* The nine places. `translate` rather than a margin so a caption is centred on
   its zone rather than butting against it. */
.pcsl-caption.pcsl-at-top-left     { top: var(--pcsl-inset-top, 4%); left: var(--pcsl-inset-left, 4%); }
.pcsl-caption.pcsl-at-top          { top: var(--pcsl-inset-top, 4%); left: 50%; transform: translateX(-50%); text-align: center; }
.pcsl-caption.pcsl-at-top-right    { top: var(--pcsl-inset-top, 4%); right: var(--pcsl-inset-right, 4%); text-align: right; }
.pcsl-caption.pcsl-at-left         { top: 50%; left: var(--pcsl-inset-left, 4%); transform: translateY(-50%); }
.pcsl-caption.pcsl-at-center       { top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
.pcsl-caption.pcsl-at-right        { top: 50%; right: var(--pcsl-inset-right, 4%); transform: translateY(-50%); text-align: right; }
.pcsl-caption.pcsl-at-bottom-left  { bottom: var(--pcsl-inset-bottom, 4%); left: var(--pcsl-inset-left, 4%); }
.pcsl-caption.pcsl-at-bottom       { bottom: var(--pcsl-inset-bottom, 4%); left: 50%; transform: translateX(-50%); text-align: center; }
.pcsl-caption.pcsl-at-bottom-right { bottom: var(--pcsl-inset-bottom, 4%); right: var(--pcsl-inset-right, 4%); text-align: right; }

/* ---- the three looks, shared by captions AND hotspots -------------------
 *
 * ⚠ Keyed on `.pcsl-layer-{style}`, which both components emit. A caption and a
 * hotspot set to «on a rounded panel» must be indistinguishable — a reader has
 * no idea which of the two an operator reached for, and should not be able to
 * tell. Writing these rules twice is how the two drift apart.
 *
 * ⚠ `--pcsl-panel` arrives as a FINISHED colour (`rgb(r g b / a%)`) composed in
 * PHP from the operator's colour and strength. CSS cannot add an alpha to a
 * `#rrggbb` custom property without `color-mix()`, whose browser floor this
 * plugin already declined once for the scrim.
 * --------------------------------------------------------------------- */

.pcsl-layer-chip {
	background: var(--pcsl-panel, rgb(0 0 0 / 55%));
	border-radius: 6px;
	backdrop-filter: blur(2px);
	padding: 4px 10px;
}

/* The fully rounded one. This is what a hotspot's old `label` look was, so it is
   what `label` migrates to — anything else would square off corners on sites
   that never asked for a change. */
.pcsl-layer-pill {
	background: var(--pcsl-panel, rgb(0 0 0 / 55%));
	border-radius: 999px;
	backdrop-filter: blur(2px);
	padding: 7px 14px;
}

/* A band ignores its zone's left/right and takes the whole width — only the row
   it was placed in still means anything. */
.pcsl-layer-bar {
	left: 0 !important;
	right: 0 !important;
	max-width: none;
	padding: 10px 5%;
	background: linear-gradient(to top, var(--pcsl-panel, rgb(0 0 0 / 72%)), var(--pcsl-panel-fade, rgb(0 0 0 / 0%)));
	transform: none !important;
	text-align: inherit;
}

.pcsl-layer-bar.pcsl-at-top,
.pcsl-layer-bar.pcsl-at-top-left,
.pcsl-layer-bar.pcsl-at-top-right {
	background: linear-gradient(to bottom, var(--pcsl-panel, rgb(0 0 0 / 72%)), var(--pcsl-panel-fade, rgb(0 0 0 / 0%)));
}

/* A band across the middle has no edge to fade towards. */
.pcsl-layer-bar.pcsl-at-left,
.pcsl-layer-bar.pcsl-at-center,
.pcsl-layer-bar.pcsl-at-right {
	background: var(--pcsl-panel, rgb(0 0 0 / 55%));
}

/* «A solid band» — the operator's choice, so it beats every gradient above. */
.pcsl-bar-solid .pcsl-layer-bar {
	background: var(--pcsl-panel, rgb(0 0 0 / 55%));
}

/* ---- making room for a control in the same corner -----------------------
 *
 * ⚠ A caption and a button placed in the same corner were each correct on their
 * own and together read «Photo 4 of ▮». The component says which corner it took
 * (`Component::root_class()`) and the caption reserves the width here — neither
 * class knows anything about the other.
 * --------------------------------------------------------------------- */

.pcsl-pp-bottom-right .pcsl-caption.pcsl-at-bottom-right { padding-right: 52px; }
.pcsl-pp-bottom-left  .pcsl-caption.pcsl-at-bottom-left  { padding-left: 52px; }
.pcsl-pp-top-right    .pcsl-caption.pcsl-at-top-right    { padding-right: 52px; }
.pcsl-pp-top-left     .pcsl-caption.pcsl-at-top-left     { padding-left: 52px; }

/* A band spans the whole width, so it clears the button by ending sooner. */
.pcsl-pp-bottom-right .pcsl-caption-bar.pcsl-at-bottom { padding-right: 58px; }
.pcsl-pp-bottom-left  .pcsl-caption-bar.pcsl-at-bottom { padding-left: 58px; }

/* ⚠ The ⤢ and ✕ live in the top-right — but only when the Expand component drew
   one. This used to be unconditional, so a slider with the full-screen button
   switched off still lost 52px of its top-right caption to a button that was not
   there. `pcsl-has-expand` comes from `Expand::root_class()`, the same seam the
   play/pause button uses above. */
.pcsl-has-expand .pcsl-caption.pcsl-at-top-right { padding-right: 52px; }

/* ⚠ A visible hotspot is in exactly the same position as a caption here, and it
   matters more: a marker half under the pause button is a *control* that cannot
   be pressed, not just words that are hard to read.

   The invisible `area` ones are deliberately left alone. They fill a whole cell
   of the frame and the controls sit above them in the stacking order already —
   nudging one would move a region the operator chose, to dodge something they
   cannot see. */
.pcsl-pp-bottom-right .pcsl-hotspot-marker.pcsl-at-bottom-right,
.pcsl-pp-bottom-right .pcsl-hotspot-words.pcsl-at-bottom-right   { right: calc(var(--pcsl-inset-right, 4%) + 44px); }
.pcsl-pp-bottom-left  .pcsl-hotspot-marker.pcsl-at-bottom-left,
.pcsl-pp-bottom-left  .pcsl-hotspot-words.pcsl-at-bottom-left    { left: calc(var(--pcsl-inset-right, 4%) + 44px); }
.pcsl-pp-top-right    .pcsl-hotspot-marker.pcsl-at-top-right,
.pcsl-pp-top-right    .pcsl-hotspot-words.pcsl-at-top-right      { right: calc(var(--pcsl-inset-right, 4%) + 44px); }
.pcsl-pp-top-left     .pcsl-hotspot-marker.pcsl-at-top-left,
.pcsl-pp-top-left     .pcsl-hotspot-words.pcsl-at-top-left       { left: calc(var(--pcsl-inset-right, 4%) + 44px); }

/* ⚠ Conditional for the same reason as the caption rule above — see there. */
.pcsl-has-expand .pcsl-hotspot-marker.pcsl-at-top-right,
.pcsl-has-expand .pcsl-hotspot-words.pcsl-at-top-right { right: calc(var(--pcsl-inset-right, 4%) + 44px); }

/* ---------------------------------------------------------------------------
 * Hotspots — the parts of a picture that go somewhere
 *
 * Inside the slide, like the captions, so a hotspot travels with its own
 * photograph. Drawn after them, so a link is never buried under words.
 *
 * ⚠ The layer is a 3×3 grid, which is what makes an invisible region be a
 * *third of the frame* rather than a point: `area` hotspots are placed by
 * `grid-area` and the visible ones are taken out of flow and anchored to their
 * zone. One container, two placement schemes, because the two answer different
 * questions — "which part of the picture is this" and "where does the marker
 * sit".
 * ------------------------------------------------------------------------ */

.pcsl-hotspots {
	position: absolute;
	inset: 0;
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	grid-template-rows: repeat(3, 1fr);
	/* Transparent to the pointer; each hotspot inside it is not. Otherwise the
	   layer would swallow every swipe that began on a picture with no hotspot
	   under that finger. */
	pointer-events: none;
}

/* ⚠⚠ These are written at (0,2,1), and every other component in this file is at
   (0,1,0). That is not carelessness — it is trap 9 again, one notch harder.

   A hotspot is the only `<a>` this plugin renders, and a theme's content-link
   styling is `.entry-content a { color; text-decoration; box-shadow }`, which is
   (0,2,1) and beats anything at component strength. A blue underline through a
   marker sitting on a photograph is not a cosmetic loss, it is the control
   becoming unreadable.

   So the hotspot's *appearance* is written to match that strength, and only its
   appearance. Placement stays at component strength below, because no theme has
   an opinion about `grid-area` on a link. */
.pcsl a.pcsl-hotspot {
	pointer-events: auto;
	text-decoration: none;
	color: inherit;
	border: 0;
	/* ⚠ The player decides whether a gesture was a tap or a swipe. The browser
	   must not also decide to drag the link itself, which is what it does with an
	   anchor by default and which cancels the pointer stream mid-swipe. */
	-webkit-user-drag: none;
}

.pcsl a.pcsl-hotspot:hover,
.pcsl a.pcsl-hotspot:focus {
	text-decoration: none;
}

/* ---- an invisible region: the zone's own cell -------------------------- */

.pcsl-hotspot-area { grid-area: 2 / 2; }

.pcsl-hotspot-area.pcsl-at-top-left     { grid-area: 1 / 1; }
.pcsl-hotspot-area.pcsl-at-top          { grid-area: 1 / 2; }
.pcsl-hotspot-area.pcsl-at-top-right    { grid-area: 1 / 3; }
.pcsl-hotspot-area.pcsl-at-left         { grid-area: 2 / 1; }
.pcsl-hotspot-area.pcsl-at-center       { grid-area: 2 / 2; }
.pcsl-hotspot-area.pcsl-at-right        { grid-area: 2 / 3; }
.pcsl-hotspot-area.pcsl-at-bottom-left  { grid-area: 3 / 1; }
.pcsl-hotspot-area.pcsl-at-bottom       { grid-area: 3 / 2; }
.pcsl-hotspot-area.pcsl-at-bottom-right { grid-area: 3 / 3; }

/* The whole picture is a link. ⚠ Rendered first, so the zone hotspots that may
   sit on top of it keep their own taps — see the Hotspots component. */
.pcsl-hotspot-area.pcsl-at-full { grid-area: 1 / 1 / 4 / 4; }

/* ---- the visible ones: anchored to their zone --------------------------- */

.pcsl-hotspot-marker,
.pcsl-hotspot-words {
	position: absolute;
	display: grid;
	place-items: center;
}

.pcsl-hotspot-marker.pcsl-at-top-left,
.pcsl-hotspot-words.pcsl-at-top-left     { top: var(--pcsl-inset-top, 4%); left: var(--pcsl-inset-left, 4%); }
.pcsl-hotspot-marker.pcsl-at-top,
.pcsl-hotspot-words.pcsl-at-top          { top: var(--pcsl-inset-top, 4%); left: 50%; transform: translateX(-50%); }
.pcsl-hotspot-marker.pcsl-at-top-right,
.pcsl-hotspot-words.pcsl-at-top-right    { top: var(--pcsl-inset-top, 4%); right: var(--pcsl-inset-right, 4%); }
.pcsl-hotspot-marker.pcsl-at-left,
.pcsl-hotspot-words.pcsl-at-left         { top: 50%; left: var(--pcsl-inset-left, 4%); transform: translateY(-50%); }
.pcsl-hotspot-marker.pcsl-at-center,
.pcsl-hotspot-words.pcsl-at-center       { top: 50%; left: 50%; transform: translate(-50%, -50%); }
.pcsl-hotspot-marker.pcsl-at-right,
.pcsl-hotspot-words.pcsl-at-right        { top: 50%; right: var(--pcsl-inset-right, 4%); transform: translateY(-50%); }
.pcsl-hotspot-marker.pcsl-at-bottom-left,
.pcsl-hotspot-words.pcsl-at-bottom-left  { bottom: var(--pcsl-inset-bottom, 4%); left: var(--pcsl-inset-left, 4%); }
.pcsl-hotspot-marker.pcsl-at-bottom,
.pcsl-hotspot-words.pcsl-at-bottom       { bottom: var(--pcsl-inset-bottom, 4%); left: 50%; transform: translateX(-50%); }
.pcsl-hotspot-marker.pcsl-at-bottom-right,
.pcsl-hotspot-words.pcsl-at-bottom-right { bottom: var(--pcsl-inset-bottom, 4%); right: var(--pcsl-inset-right, 4%); }

/* ---- the marker --------------------------------------------------------- */

.pcsl a.pcsl-hotspot-marker {
	width: 30px;
	height: 30px;
	border-radius: 50%;
	/* ⚠ It sits on an unknown photograph, so it carries its own contrast in both
	   directions: a light disc with a dark ring reads on a black wall and on a
	   white sky alike. */
	background: rgb(255 255 255 / 92%);
	box-shadow: 0 0 0 2px rgb(0 0 0 / 30%), 0 2px 10px rgb(0 0 0 / 40%);
}

.pcsl-hotspot-marker::before {
	content: "";
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: #1d2327;
}

.pcsl-hotspot-marker::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: 50%;
	box-shadow: 0 0 0 0 rgb(255 255 255 / 70%);
	animation: pcsl-hotspot-pulse 2.4s ease-out infinite;
}

@keyframes pcsl-hotspot-pulse {
	70%  { box-shadow: 0 0 0 14px rgb(255 255 255 / 0%); }
	100% { box-shadow: 0 0 0 0 rgb(255 255 255 / 0%); }
}

/* ⚠ A pulse that never stops is exactly the thing reduced-motion is asked for.
   The marker keeps its ring; it stops breathing. */
@media (prefers-reduced-motion: reduce) {
	.pcsl-hotspot-marker::after { animation: none; }
}

.pcsl-reduced .pcsl-hotspot-marker::after { animation: none; }

.pcsl a.pcsl-hotspot-marker:hover {
	background: #fff;
	transform: scale(1.08);
}

/* The zone-anchored ones already carry a transform, so hover keeps it. */
.pcsl a.pcsl-hotspot-marker.pcsl-at-top:hover,
.pcsl a.pcsl-hotspot-marker.pcsl-at-bottom:hover    { transform: translateX(-50%) scale(1.08); }
.pcsl a.pcsl-hotspot-marker.pcsl-at-left:hover,
.pcsl a.pcsl-hotspot-marker.pcsl-at-right:hover     { transform: translateY(-50%) scale(1.08); }
.pcsl a.pcsl-hotspot-marker.pcsl-at-center:hover    { transform: translate(-50%, -50%) scale(1.08); }

/* ---- the labelled one --------------------------------------------------- */

/* ⚠ Only what makes it a hotspot rather than a caption: how wide it may grow,
   what size the words are, and that it is white on a photograph. The BACKGROUND
   and the corners come from `.pcsl-layer-{style}` above, shared with the
   captions — a copy here would be the thing that lets the two drift apart. */
.pcsl a.pcsl-hotspot-words {
	max-width: min(70%, 34ch);
	color: var(--pcsl-layer-ink, #fff);
	/* ⚠ The same size as a caption. They were 15px and 17px, to two different
	   ratios, for no reason anybody had written down. */
	font-size: var(--pcsl-layer-size, clamp(13px, 1.6vw, 17px));
	line-height: 1.3;
	text-align: center;
	text-shadow: 0 1px 3px rgb(0 0 0 / 55%);
}

/* A band is the full width, so it must not be capped or centred like a pill. */
.pcsl a.pcsl-hotspot-words.pcsl-layer-bar {
	max-width: none;
}

/* ⚠ A hotspot is a link and must answer the pointer. The panel colour is the
   operator's, so the hover cannot be a fixed darker black — it brightens the
   words and lifts the panel instead, which works on any colour they choose. */
.pcsl a.pcsl-hotspot-words:hover,
.pcsl a.pcsl-hotspot-words:focus-visible {
	color: var(--pcsl-layer-ink, #fff);
	filter: brightness(1.25);
}

/* ---- which screens ------------------------------------------------------ */

/* ⚠ A long caption is fine on a laptop and covers half the picture on a phone,
   and a row of markers is the same story. One block for both, because they
   answer with the same three words — see the Screens class.

   These use the same 782px the WordPress admin uses, deliberately: a slider's
   own breakpoint is about whether the *slider* appears, which is a different
   question from whether this caption fits. */
@media (max-width: 782px) {
	.pcsl .pcsl-on-desktop { display: none; }
}

@media (min-width: 783px) {
	.pcsl .pcsl-on-mobile { display: none; }
}

/* ---------------------------------------------------------------------------
 * Expand and close
 * ------------------------------------------------------------------------ */

.pcsl-expand,
.pcsl-close {
	position: absolute;
	top: 10px;
	right: 10px;
	display: grid;
	place-items: center;
	width: 36px;
	height: 36px;
	border-radius: 50%;
	background: var(--pcsl-control);
	color: var(--pcsl-control-ink);
}

.pcsl-expand:hover,
.pcsl-close:hover { background: var(--pcsl-control-hover); }

.pcsl-expand svg,
.pcsl-close svg {
	width: 55%;
	height: 55%;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.2;
	stroke-linecap: round;
}

/* The close button is bigger than everything else on the overlay: it is the one
   control a reader must be able to hit without looking for it. */
.pcsl.is-open .pcsl-close {
	width: 48px;
	height: 48px;
	top: 14px;
	right: 14px;
}

/* ---------------------------------------------------------------------------
 * The overlay
 *
 * ⚠ "Full screen" here means covering the browser window and hiding the page —
 * Panos's own definition. No device or browser Fullscreen API is involved, which
 * is also why this behaves identically on iOS Safari, where that API does not
 * exist for anything but video.
 *
 * ⚠ The player MOVES the root element to `<body>` before opening it. `position:
 * fixed` is relative to the viewport only while no ancestor has a transform,
 * filter or containment — and themes put those on wrappers all the time. Moving
 * the element is the only way to be sure; listeners and loaded images survive it.
 * ------------------------------------------------------------------------ */

.pcsl.is-open {
	position: fixed;
	inset: 0;
	z-index: 999999;
	max-width: none;
	margin: 0;
	background: var(--pcsl-backdrop, #000);
	animation: pcsl-fade-in .2s ease;
}

/* ⚠ `position: absolute; inset: 0`, not `height: 100%`.
 *
 * A percentage height resolves against the parent's *definite* height, and any
 * layout that sizes the parent to its content — a centred grid, for one — makes
 * that circular: the frame asks the row how tall it is, the row asks the frame.
 * The first version of this centred the overlay with `place-items: center` and
 * the picture came out at its natural size in the top half of a black window.
 * Insetting to the edges has no such conversation to have.
 *
 * It also handles a phone's disappearing browser chrome for free, because the
 * fixed parent already does. */
.pcsl.is-open .pcsl-frame {
	position: absolute;
	inset: 0;
	width: auto;
	height: auto;
	max-height: none;
	border-radius: 0;
	/* The window is the frame now, so the slider's own shape no longer applies —
	   a 16:9 setting must not letterbox a portrait phone. */
	aspect-ratio: auto;
}

/* Over the whole window a cropped photograph loses too much. Whatever the slider
   does inline, the overlay shows the whole picture. */
.pcsl.is-open .pcsl-img {
	object-fit: contain;
}

.pcsl.is-open .pcsl-slot-below {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 8px;
	z-index: 2;
	color: #fff;
}

@keyframes pcsl-fade-in {
	from { opacity: 0; }
	to   { opacity: 1; }
}

/* ⚠ Scroll lock. `overflow: hidden` alone is ignored by iOS Safari, so the page
   is pinned as well and its scroll position restored on close — without the
   `top` offset the reader is returned to the top of the page, which is the
   classic version of this bug. */
html.pcsl-locked,
html.pcsl-locked body {
	overflow: hidden;
}

html.pcsl-locked body {
	position: fixed;
	width: 100%;
}

/* ---------------------------------------------------------------------------
 * The page background
 *
 * ⚠ Printed on `wp_footer` as a direct child of `<body>` — see the Background
 * class. Everything below assumes that: the element has no layout parent worth
 * speaking of, and `position: fixed` can be trusted because nothing wraps it.
 *
 * ⚠ Specificity is `.pcsl.pcsl-mode-background` (0,2,0) so it beats the plain
 * `.pcsl` and the `.pcsl-size-*` rules near the top of this file without an
 * `!important` anywhere. The order of these two classes in the markup does not
 * matter; the count does.
 * ------------------------------------------------------------------------ */

.pcsl.pcsl-mode-background {
	position: fixed;
	inset: 0;
	/* ⚠ `0`, not a large number. This sits *behind* the page, and the page's own
	   content is lifted to `z-index: 1` by the settings stylesheet. A background
	   that competes on z-index with the theme's header is a background covering
	   the header. */
	z-index: 0;
	max-width: none;
	margin: 0;
	/* ⚠ The whole point: the reader is meant to click *through* this to the page.
	   `.pcsl-slot-frame > *` restores `auto` on the controls, which is what keeps
	   the play/pause button pressable — and an autoplaying full-page background
	   that cannot be paused is a WCAG 2.2.2 failure, not a style choice. */
	pointer-events: none;
}

/* Same reasoning as the overlay: the window is the frame, so the slider's own
   shape and height settings no longer mean anything. */
.pcsl.pcsl-mode-background .pcsl-frame {
	position: absolute;
	inset: 0;
	width: auto;
	height: auto;
	max-height: none;
	border-radius: 0;
	aspect-ratio: auto;
}

/* ⚠ Cropped, always. `contain` would letterbox the viewport and paint bands of
   the backdrop colour across somebody's page — never what a background is for.
   (0,3,0), so it beats `.pcsl-fit-contain .pcsl-img` at (0,2,0).) */
.pcsl.pcsl-mode-background .pcsl-img {
	object-fit: cover;
}

/* ⚠ The readability veil. Text over an unknown photograph is the reason
   background sliders usually look amateurish; this is the difference. It is a
   setting (`scrim`) rather than a constant, and zero turns it off.

   ⚠ The colour is the slider's own **backdrop** setting, not a hardcoded black.
   A dark veil is right for a theme with light text and exactly wrong for one
   with dark text — and half the themes in the world have dark text. Reusing the
   backdrop means «make the veil white» is a colour picker the operator already
   has, rather than a second setting nobody finds.

   `opacity` takes a percentage directly, so `--pcsl-scrim` is used as-is. That
   also keeps this working in every browser, which `color-mix()` would not. */
.pcsl.pcsl-mode-background .pcsl-frame::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
	background: var(--pcsl-backdrop, #000);
	opacity: var(--pcsl-scrim, 40%);
	pointer-events: none;
}

/* ⚠⚠ The controls layer, and why it is a separate element.
 *
 * `position: fixed` creates a stacking context unconditionally. The slider is
 * fixed at `z-index: 0` and the page's content is lifted to `z-index: 1`, so
 * NOTHING inside the slider can paint above that content — the whole subtree is
 * pinned to the root's level whatever z-index it is given.
 *
 * The pictures have to be under the content and the controls have to be over it.
 * That is two layers, and one element cannot be two layers. So the renderer puts
 * the frame slot in a sibling of the root, and this is that sibling.
 *
 * Found the honest way: a live check showed the play/pause button unclickable,
 * sitting under the page's text exactly as the stacking rules require — which on
 * an autoplaying full-page background is a WCAG 2.2.2 failure.
 *
 * The z-index is high because a theme's own sticky header is routinely at 100 or
 * 1000, and a pause button behind one is a pause button that does not exist. It
 * stays below the fullscreen overlay's 999999, which must beat everything. */
.pcsl-outside {
	position: fixed;
	inset: 0;
	z-index: 999998;
	/* Transparent to the pointer, exactly like the in-frame slot: this sheet
	   covers the entire viewport and would otherwise swallow every click on the
	   page it is meant to sit over. `.pcsl-slot-frame > *` restores `auto` on the
	   controls themselves. */
	pointer-events: none;
}

/* ⚠ A background slider is behind the text of the page, so the ordinary
   `is-active` fade is all it may do. A Ken Burns drift is fine; a slide that
   moves the strip is not, because the reader's eye tracks motion and the words
   they are trying to read are sitting on top of it. Left to the operator — this
   note is here so nobody "fixes" the absence of a rule. */

/* ---------------------------------------------------------------------------
 * A slider with one image is a picture
 *
 * ⚠ Nothing is hidden here, deliberately. Every navigation component already
 * refuses to render below two slides, and a blanket `display: none` on the
 * frame slot would take the ✕ with it — leaving a single-image slider that can
 * be opened over the whole window and not closed. The class stays because a
 * theme may want it.
 * ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------------------
 * Screen-reader-only text
 *
 * Themes usually ship this, but a slider must not depend on the theme having
 * done so — the labels are the difference between a usable carousel and an
 * unusable one.
 * ------------------------------------------------------------------------ */

.pcsl .screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---------------------------------------------------------------------------
 * Phones
 * ------------------------------------------------------------------------ */

@media (max-width: 600px) {
	.pcsl {
		--pcsl-arrow-size: 40px;
	}

	.pcsl-arrows-sides .pcsl-arrow-prev { left: 6px; }
	.pcsl-arrows-sides .pcsl-arrow-next { right: 6px; }
}
