/*
 * NEWKID V2 — shared interface components.
 *
 * Global header and footer, and the custom video player
 * (docs/wordpress-architecture.md §37).
 */

/* ─── Opening intro loader ───────────────────────────────────────────────── */

/* Shared, session-once fullscreen intro (docs/wordpress-architecture.md,
   "Opening Intro Loader"). Markup (template-parts/intro-loader.php) is
   server-rendered on every top-level V2 template and always present in the
   DOM; whether it is ever visible is decided pre-paint by the inline <head>
   script (newkid_v2_intro_bootstrap(), inc/intro-loader.php) toggling
   `nk-intro-show`/`nk-intro-skip` on <html> before <body> parses. Hidden by
   default here, shown only via the explicit opt-in class — never the
   reverse — so a page that somehow loads with neither class (e.g. this
   stylesheet failing to load at all) never gets stuck showing an overlay
   nothing can dismiss. z-index sits above the header (20) and everything
   else on the page. */
.v2-intro-loader {
	display: none;
	position: fixed;
	inset: 0;
	z-index: 9999;
	align-items: center;
	justify-content: center;
	background: var(--color-bg);
	opacity: 1;
	transition: opacity 750ms var(--ease-premium);
}

html.nk-intro-show .v2-intro-loader {
	display: flex;
}

.v2-intro-loader--out {
	opacity: 0;
	pointer-events: none;
}

/* Intrinsic 774×562 — centered, aspect preserved, never cropped/stretched
   (brief: no object-fit:cover, no fullscreen video). A compact mark, not a
   hero element, matching the source clip's own restrained scale. */
.v2-intro-loader__video {
	width: clamp(140px, 20vw, 280px);
	height: auto;
	object-fit: contain;
}

@media (prefers-reduced-motion: reduce) {
	.v2-intro-loader {
		transition: opacity 200ms ease;
	}
}

/* ─── Global header ──────────────────────────────────────────────────────── */

:root {
	/* Header-specific spacing — not promoted to base.css tokens since no
	   other component needs them yet (design-guidelines.md §9 gives the page
	   margin; the top offset here is a component-level visual judgement
	   against the approved mockup, to be tuned during visual review). */
	--header-top-spacing: clamp(20px, 2.2vw, 32px);
	--header-nav-gap: clamp(16px, 1.2vw, 24px);
}

.v2-header {
	/* Fixed: stays pinned to the viewport while the page scrolls, on every
	   page (confirmed visual decision — supersedes the earlier "overlay,
	   not sticky" default). Still has no background, so it keeps overlaying
	   full-bleed media on Home/Contact exactly as before. */
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 20;
	background: transparent;
}

.v2-header__inner {
	/* position:relative + z-index:1 (bug fix, mobile-menu-veil pass): gives
	   the actual nav content its OWN stacking context, explicitly above the
	   dedicated .v2-header__menu-veil sibling (mobile media query below,
	   z-index:0) — without this, a backdrop-filter element sharing the same
	   stacking context as this content could end up blurring it too (the
	   exact regression this fixed: logo/nav text visibly blurred instead of
	   only the page behind the menu). */
	position: relative;
	z-index: 1;
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: var(--header-top-spacing) var(--page-margin) 0;
}

.v2-header__logo {
	display: flex;
	align-items: center;
	line-height: 0;
}

.v2-header__logo-img {
	/* Image replaces the previous text wordmark (logo-asset swap) — same
	   fluid clamp() value the wordmark used, now driving the image height
	   instead of a font-size, so on-page scale is unchanged. width:auto
	   preserves the asset's intrinsic aspect ratio; the width/height
	   attributes on the <img> itself (template-parts/header-global.php)
	   give the browser that same ratio before CSS loads, avoiding layout
	   shift. One fluid rule, no separate mobile override — already scales
	   across every breakpoint and every page that includes the shared
	   header. */
	display: block;
	height: clamp(18px, 1.6vw, 23px);
	width: auto;
}

.v2-header__right {
	display: flex;
	align-items: center;
	gap: var(--header-nav-gap);
}

.v2-header__nav {
	display: flex;
	align-items: center;
	gap: var(--header-nav-gap);
}

.v2-header__nav-link,
.v2-header__lang-link {
	font-family: var(--font-base);
	font-size: var(--type-nav-size);
	font-weight: var(--type-nav-weight);
	letter-spacing: var(--type-nav-tracking);
	line-height: 1.2;
	color: var(--color-fg);
	text-decoration: none;
	opacity: var(--opacity-muted);
	transition: opacity var(--transition-ui);
	white-space: nowrap;
}

.v2-header__nav-link:hover,
.v2-header__nav-link.is-active,
.v2-header__lang-link:hover,
.v2-header__lang-link.is-active {
	opacity: 1;
}

.v2-header__nav-link--pending {
	pointer-events: none;
}

.v2-header__lang {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: var(--type-nav-size);
}

.v2-header__lang-dot,
.v2-header__lang-sep {
	opacity: var(--opacity-muted);
}

.v2-header__toggle {
	display: none;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	gap: 5px;
	width: 28px;
	height: 28px;
	padding: 0;
	background: none;
	border: none;
	cursor: pointer;
}

.v2-header__toggle span {
	display: block;
	width: 100%;
	height: 1px;
	background: var(--color-fg);
	transition: transform 220ms ease;
}

.v2-header__mobile {
	display: none;
}

/* Hidden by default (desktop) — no mobile menu, no backdrop needed. Given
   real position/z-index/blur only inside the mobile media query below. */
.v2-header__menu-veil {
	display: none;
}

/* ─── Mobile header ──────────────────────────────────────────────────────── */

@media (max-width: 767px) {
	:root {
		/* Bug-fix + intensity pass: this is now a deliberately MORE PRONOUNCED
		   state than the passive scroll-blur veil (--sticky-veil-bg, base.css,
		   7px / 0.18) — an OPEN navigation overlay should read as an
		   unmistakable "you are now in the menu" state, not just a readability
		   nudge. Still translucent ("smoked glass", not opaque) — 0.72 keeps
		   the page faintly perceptible underneath. Its own dedicated blur
		   token, moderately stronger than --sticky-veil-blur (7px), not a
		   different intensity tier entirely. */
		--mobile-menu-veil-blur: 12px;
		--mobile-menu-veil-bg: rgba(0, 0, 0, 0.72);

		/* Neutral divider language for this menu only (brief §7: "Do NOT use
		   the colorful Info gradient divider system here") — same flat
		   off-white-at-low-opacity already used for Work List row separators
		   and Info's own non-gradient fallback (pages.css), not a new value. */
		--mobile-menu-divider-color: rgba(240, 242, 228, 0.12);
	}

	.v2-header__right > .v2-header__nav,
	.v2-header__right > .v2-header__lang {
		display: none;
	}

	.v2-header__toggle {
		display: inline-flex;
		/* Comfortable touch target (brief §6) without enlarging the visual
		   icon itself — the icon stays 18px (span width below), centered in
		   a bigger tappable box via flex alignment, no extra markup needed. */
		width: 44px;
		height: 44px;
	}

	.v2-header__toggle span {
		width: 18px;
	}

	/* Closed → open hamburger/cross morph (brief §24). Tied to the
	   structural --menu-open state (not the animated --menu-visible one) so
	   the icon reads "cross" for as long as the panel is actually present,
	   including while it fades out on close — see header.js. translateY
	   (gap 5px + span height 1px) / 2 = 3px puts both bars on the same
	   center line before rotating into an X. */
	.v2-header--menu-open .v2-header__toggle span:nth-child(1) {
		transform: translateY(3px) rotate(45deg);
	}

	.v2-header--menu-open .v2-header__toggle span:nth-child(2) {
		transform: translateY(-3px) rotate(-45deg);
	}

	/* Fullscreen navigation overlay (mobile-menu redesign brief). Structural
	   only — .v2-header switches from its normal auto-height fixed strip to
	   a fixed-height flex column filling the viewport, so .v2-header__mobile
	   below can flex:1 into the remaining space instead of needing its own
	   independent fixed-positioning/height math. Kept applied for the whole
	   open+closing-animation duration (header.js only removes it once the
	   close animation finishes) so nothing about this layout shifts out from
	   under the content while it's still visually fading out. */
	.v2-header--menu-open {
		bottom: 0;
		height: 100dvh;
		display: flex;
		flex-direction: column;
	}

	.v2-header--menu-open .v2-header__inner {
		flex: 0 0 auto;
		/* Supplements the existing top spacing with the notch/status-bar
		   safe area (brief §16) — only added in the fullscreen state so the
		   normal (non-menu) header bar is completely unaffected. */
		padding-top: calc(var(--header-top-spacing) + env(safe-area-inset-top));
	}

	.v2-header__mobile {
		/* position:relative + z-index:1 — same reasoning as .v2-header__inner
		   above: guarantees this real nav content composites above
		   .v2-header__menu-veil (z-index:0) as its own stacking context,
		   never swept into that element's backdrop-filter sampling. */
		position: relative;
		z-index: 1;
		flex: 1 1 auto;
		min-height: 0; /* lets this scroll internally instead of overflowing .v2-header (brief §17) */
		display: flex;
		flex-direction: column;
		/* Explicit, not just the implicit default: content packs from the top
		   (spacing-refinement brief §1/§7/§8) — deliberately NOT `space-between`
		   or `flex-end`, which would spread/anchor children across the full
		   available height again. Whatever vertical space this flex column has
		   left over after its (content-sized, non-growing) children stays
		   unused at the BOTTOM, which is the desired empty zone. */
		justify-content: flex-start;
		overflow-y: auto;
		padding: 0 var(--page-margin) calc(24px + env(safe-area-inset-bottom));
	}

	.v2-header__mobile[hidden] {
		display: none;
	}

	.v2-header__mobile-divider {
		flex: 0 0 auto;
		height: 1px;
		background: var(--mobile-menu-divider-color);
	}

	/* Top divider (brief §9: unchanged — the gap ABOVE it, i.e. below the
	   Header bar, is untouched) → primary nav. Spacing-refinement brief §2:
	   roughly 40-70px of breathing room, not the ~half-viewport gap the old
	   flex-centering produced. */
	.v2-header__mobile-divider:first-child {
		margin-top: clamp(16px, 4vw, 24px);
		margin-bottom: clamp(40px, 10vw, 56px);
	}

	/* Contact → lower divider (brief §4: ~30-50px, close to Contact, not
	   pushed down by leftover flex space) → EN/FR (brief §5: small gap —
	   utilities read as directly connected to the nav group, not a separate
	   block anchored near the bottom). */
	.v2-header__mobile-nav + .v2-header__mobile-divider {
		margin-top: clamp(28px, 7vw, 40px);
		margin-bottom: clamp(18px, 4vw, 22px);
	}

	/* Large editorial primary links (brief §8, unchanged typography/gap —
	   only look "centered" because this used to flex:1 + justify-content:
	   center into whatever space was left between the two dividers, which is
	   exactly what pushed the whole group toward mid-screen. Content-sized
	   now (no flex-grow), so it sits directly after the top divider instead;
	   see the spacing-refinement brief for why. Reuses .v2-header__nav-link
	   for color/opacity/active-state (shared with the desktop nav) — only
	   size/weight/spacing are overridden here for this context. */
	.v2-header__mobile-nav {
		display: flex;
		flex-direction: column;
		gap: clamp(20px, 5vw, 32px);
	}

	.v2-header__mobile-nav .v2-header__nav-link {
		font-size: clamp(34px, 10vw, 44px);
		font-weight: 400;
		line-height: 1.15;
		letter-spacing: -0.025em;
	}

	.v2-header__mobile-secondary {
		flex: 0 0 auto;
		display: flex;
		flex-direction: column;
		/* One uniform token for both internal gaps (lang→social, social→legal)
		   rather than per-row values (spacing-refinement brief §6) — was 14px,
		   nudged up slightly to read as a deliberate compact group rather than
		   a slightly cramped fallback. */
		gap: clamp(16px, 4vw, 22px);
	}

	.v2-header__mobile-lang {
		display: flex;
		align-items: center;
		gap: 6px;
		font-size: var(--type-nav-size);
	}

	.v2-header__mobile-social {
		display: flex;
		align-items: center;
		gap: 16px;
	}

	.v2-header__mobile-legal {
		display: flex;
		align-items: center;
		gap: clamp(12px, 4vw, 16px);
	}

	/* ─── Opening/closing motion (brief §19-20) ──────────────────────────── */
	/*
	 * The transition itself is declared unconditionally on the resting
	 * (closed) rule, not inside the .is-open/--menu-visible rule below — a
	 * transition property only takes effect using whichever rule matches
	 * AFTER a change, so declaring it only on the "visible" state would
	 * animate the fade in but snap instantly on close (the "visible" rule,
	 * and its transition, would simply stop matching). Small stagger across
	 * the three primary links; the secondary block fades in as one unit,
	 * slightly after (brief §19 "SECONDARY LINKS: slightly delayed").
	 */
	.v2-header__mobile-nav .v2-header__nav-link,
	.v2-header__mobile-secondary {
		opacity: 0;
		transform: translateY(18px);
		transition: opacity 0.4s var(--ease-premium), transform 0.4s var(--ease-premium);
	}

	.v2-header__mobile-nav .v2-header__nav-link:nth-child(1) { transition-delay: 20ms; }
	.v2-header__mobile-nav .v2-header__nav-link:nth-child(2) { transition-delay: 70ms; }
	.v2-header__mobile-nav .v2-header__nav-link:nth-child(3) { transition-delay: 120ms; }
	.v2-header__mobile-secondary { transition-delay: 160ms; }

	.v2-header--menu-visible .v2-header__mobile-nav .v2-header__nav-link,
	.v2-header--menu-visible .v2-header__mobile-secondary {
		opacity: 1;
		transform: translateY(0);
	}

	@media (prefers-reduced-motion: reduce) {
		.v2-header__mobile-nav .v2-header__nav-link,
		.v2-header__mobile-secondary {
			transition: none;
			transition-delay: 0ms;
		}
	}

	/* Mobile menu backdrop — bug fix: this used to be a `.v2-header::before`
	   pseudo-element, sharing `.v2-header`'s own box/stacking context with
	   the real nav content (.v2-header__inner/.v2-header__mobile). In
	   practice, a backdrop-filter element and later-painted same-context
	   siblings can end up sharing one compositing/backdrop-root, which is
	   exactly what caused the regression: the NEWKID logo and several nav
	   links rendered visibly blurred instead of staying sharp. The fix is a
	   genuinely separate, real DOM element (template-parts/header-global.php)
	   — not a pseudo-element of the box that also contains the nav — with
	   its OWN explicit z-index (0), while .v2-header__inner/.v2-header__mobile
	   above each get their own explicit z-index (1) and stacking context.
	   That gives an unambiguous, real z-index hierarchy (page → veil → nav)
	   instead of relying on implicit pseudo-element paint order, which is
	   what actually leaked through here.

	   Now sized via `inset: 0` to .v2-header's OWN box: since --menu-open
	   above makes that box exactly the viewport (100dvh) rather than an
	   auto-height wrapper, the veil is a true fullscreen layer with no
	   fade-taper/mask needed — the previous mask-image existed only to fade
	   the veil out below a small, shrink-wrapped dropdown's real content,
	   which no longer applies now that the menu fills the screen edge to
	   edge.

	   Opacity driven by --menu-visible, deliberately NOT --menu-open: the
	   structural fullscreen state (above) is applied first and torn down
	   last so nothing about the layout shifts mid-animation, while this
	   visible/animated state is what actually toggles in sync with the nav
	   content's own fade (header.js). */
	.v2-header__menu-veil {
		display: block;
		position: absolute;
		inset: 0;
		z-index: 0;
		background: var(--mobile-menu-veil-bg);
		backdrop-filter: blur(var(--mobile-menu-veil-blur));
		-webkit-backdrop-filter: blur(var(--mobile-menu-veil-blur));
		pointer-events: none;
		opacity: 0;
		transition: opacity 220ms ease;
	}

	.v2-header--menu-visible .v2-header__menu-veil {
		opacity: 1;
	}
}

/* ─── Global footer ──────────────────────────────────────────────────────── */

:root {
	/* Footer-specific spacing — kept separate from --header-top-spacing even
	   though the values currently match, since the two are conceptually
	   different edges that may need to be tuned independently later. */
	--footer-bottom-spacing: clamp(20px, 2.2vw, 32px);
}

.v2-footer {
	/* Normal document flow by default — a page-level modifier can change
	   this later for pages that need the footer integrated into a
	   fullscreen viewport (Home, Contact). Not built ahead of that need. */
	position: relative;
}

/* Grid, not flex: the center mark needs to be a real, in-flow grid item
   (see .v2-footer__mark below) so `align-items: center` centers it against
   the row's own content height, same as the left/right text — rather than
   against .v2-footer__inner's full padded box (which includes
   --footer-bottom-spacing below the row and nothing above it), the
   asymmetry that previously made the mark sit visibly lower than the
   text. The two `1fr` outer tracks keep the center column truly centered
   on the row regardless of how wide the left/right content is. */
.v2-footer__inner {
	position: relative;
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	padding: 0 var(--page-margin) var(--footer-bottom-spacing);
}

.v2-footer__left {
	display: flex;
	align-items: baseline;
	gap: 16px;
	justify-self: start;
}

/* Desktop only (see mobile block below) — restored per the original
   validated desktop Footer; the simplified Instagram/LinkedIn +
   Legal/Privacy-only layout is a mobile-only reduction, not a global
   redesign. */
.v2-footer__copy {
	font-family: var(--font-base);
	font-size: 14px;
	font-weight: var(--weight-body);
	line-height: 1.2;
	color: var(--color-fg);
	opacity: var(--opacity-muted);
}

.v2-footer__social {
	font-family: var(--font-base);
	font-size: 14px;
	font-weight: var(--weight-body);
	line-height: 1.2;
	color: var(--color-fg);
	text-decoration: none;
	opacity: var(--opacity-secondary);
	transition: opacity var(--transition-ui);
}

.v2-footer__social:hover,
.v2-footer__social:focus-visible {
	opacity: 1;
}

/* Desktop only (see mobile block below). The middle track of
   .v2-footer__inner's 1fr/auto/1fr grid centers this true-center,
   independent of how wide the left/right content blocks are — deliberately
   NOT a `position: absolute` + `transform: translate(-50%, -50%)` centering
   hack: this element also carries the shared `.v2-reveal` motion class
   (base.css / home-entrance.js / work-reveal.js etc.), which animates via
   `transform: translateY(...)`. A static centering transform on the same
   property would be replaced outright (transform doesn't merge across
   rules), snapping the mark to the row's top-left corner for the whole
   reveal — the previous cause of the "glitchy"/jumping entrance. Letting
   grid placement own centering leaves `transform` entirely free for the
   reveal system, so it only ever moves the mark by its intended few px.
*/
.v2-footer__mark {
	justify-self: center;
	display: block;
	width: clamp(21px, 1.6vw, 26px);
	aspect-ratio: 538.97 / 382.05;
	background-color: var(--color-fg);
	-webkit-mask-image: url('../images/mark.svg');
	        mask-image: url('../images/mark.svg');
	-webkit-mask-repeat: no-repeat;
	        mask-repeat: no-repeat;
	-webkit-mask-position: center;
	        mask-position: center;
	-webkit-mask-size: contain;
	        mask-size: contain;
}

.v2-footer__right {
	display: flex;
	align-items: center;
	gap: var(--header-nav-gap);
	justify-self: end;
}

.v2-footer__legal-link {
	font-family: var(--font-base);
	font-size: 14px;
	font-weight: var(--weight-body);
	line-height: 1.2;
	color: var(--color-fg);
	text-decoration: none;
	opacity: var(--opacity-muted);
	transition: opacity var(--transition-ui);
	white-space: nowrap;
}

.v2-footer__legal-link:hover,
.v2-footer__legal-link:focus-visible {
	opacity: 1;
}

.v2-footer__legal-link--pending {
	pointer-events: none;
}

/* ─── Mobile footer ──────────────────────────────────────────────────────── */
/*
 * Mobile-only reduction (scope correction: an earlier pass mistakenly made
 * this the global Footer — it must stay mobile-only, desktop keeps the
 * original copyright + centered mark). Same breakpoint the rest of the
 * theme already uses for its mobile/desktop split (header, Work Grid,
 * Single Work).
 *
 * Copyright and the centered mark are hidden outright — not just visually
 * reduced — so only Instagram / LinkedIn (left) and Legal Notice / Privacy
 * Policy (right) remain, on one row: two short groups fit at every
 * supported width (375px+) without wrapping, so a stacked/column layout is
 * not "necessary" (design-guidelines.md §16/§59).
 *
 * .v2-footer__inner switches from the desktop 1fr/auto/1fr grid to flex +
 * space-between here because a `display: none` .v2-footer__mark is not a
 * grid item at all — it drops out of the auto-placement order entirely, so
 * .v2-footer__right would land in the grid's middle `auto` track (sized to
 * content) instead of the last `1fr` track, landing right next to
 * .v2-footer__left instead of at the right edge. That mispositioning, not
 * the font size alone, was the cause of Legal Notice colliding with
 * LinkedIn. Flex + space-between has no such ordering dependency: the two
 * remaining visible children simply take the outer edges with the leftover
 * space between them.
 */
@media (max-width: 767px) {
	.v2-footer__copy,
	.v2-footer__mark {
		display: none;
	}

	.v2-footer__inner {
		display: flex;
		justify-content: space-between;
		align-items: center;
	}

	.v2-footer__left {
		gap: clamp(6px, 2vw, 8px);
	}

	.v2-footer__right {
		gap: clamp(6px, 2vw, 8px);
	}

	.v2-footer__social,
	.v2-footer__legal-link {
		font-size: 12px;
	}
}

/* ─── Video player (Single Work) ─────────────────────────────────────────── */
/*
 * Adapted from V1's custom player (newkid/assets/css/... video-player rules)
 * with the credits overlay/button removed entirely (brief §11, §25-26).
 * Minimal, small utility typography, controls overlaid at the bottom edge of
 * the frame (design-guidelines.md §36-37) — the video itself stays dominant.
 */

:root {
	--player-controls-padding: clamp(12px, 1.4vw, 20px);
	--player-btn-gap: 6px;
}

.v2-player {
	position: relative;
	width: 100%;
	max-width: 100%;
	/* .v2-player__controls-wrapper is a sibling of .v2-player__frame, not
	   nested inside it, so the frame's own overflow:hidden never constrained
	   it — this is the actual positioning context for the controls, and the
	   clip that keeps them (and the mobile control row below, in particular)
	   from ever visually escaping the player and causing page-level
	   horizontal scroll. Harmless on desktop, nothing there currently
	   overflows. */
	overflow: hidden;
	/* Same subtle radius as Work's media thumbnails (base.css
	   --v2-media-radius). This element's own box is the true outer visual
	   frame — .v2-player__frame (video) fills it exactly and the absolutely-
	   positioned .v2-player__controls-wrapper is anchored to its edges too,
	   so rounding + clipping here once is enough for the video, poster and
	   controls overlay together without separately rounding each inner
	   layer. Reset to 0 in fullscreen below — a real screen-filling surface
	   must never show an inset rounded frame. */
	border-radius: var(--v2-media-radius);
	background: rgba(240, 242, 228, 0.04);
	/* Single reveal owner for the ENTIRE visible player — frame, poster,
	   video and the desktop controls overlay all fade in together as one
	   unit (assets/js/single-work-player.js adds `is-ready` once a real
	   frame exists, or once a failure-fallback poster has actually
	   finished loading — see that file). Previously only .v2-player__video
	   had its own opacity/transition while this element (with its
	   border-radius + the tint above) was visible unconditionally from
	   first paint — against the page's pure black background that faint
	   tint's rounded rectangle was itself a visible "player outline"
	   before the video ever appeared, independent of whatever readiness
	   state the video was in. One owner removes that: nothing about the
	   player is perceivable until everything is. Not gated behind
	   html.motion-enabled (contrast base.css's `.v2-reveal` system) — same
	   reasoning as the old video-only rule this replaces: every source
	   this player ever plays is JS-attached, so there is no no-JS path
	   where a progressive-enhancement gate would matter here. */
	opacity: 0;
	transition: opacity 600ms var(--ease-premium);
}

.v2-player.is-ready {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.v2-player {
		transition: none;
	}
}

/* Fullscreen requests the native API on .v2-player itself (assets/js/
   single-work-player.js enterFullscreen()) — reset the radius there so the
   fullscreen surface is a true edge-to-edge rectangle, not an inset rounded
   frame floating inside the OS fullscreen black background. */
.v2-player:fullscreen,
.v2-player:-webkit-full-screen {
	border-radius: 0;
}

.v2-player__frame {
	position: relative;
	width: 100%;
	aspect-ratio: 16 / 9;
	/* Deliberately NOT rounded here on desktop — .v2-player above is the
	   ONE authoritative clipping/masking surface for the whole player
	   (media + controls together), and this plain rectangular clip is a
	   harmless secondary safety net, not a second visual corner. An
	   earlier pass gave this its own matching border-radius too (same
	   value, same box, so no dimension ever changed), reasoning it would
	   defend against sub-pixel video-layer corner bleed — it didn't
	   (verified: identical either way under a solid-fill pixel test at the
	   time). What it did do, invisibly until this player started fading as
	   one opacity unit, was leave two independently-anti-aliased rounded
	   masks stacked on top of each other: WebKit compositing the opacity
	   fade could rasterize .v2-player's and .v2-player__frame's corner
	   masks with a fractional-pixel mismatch, letting a sliver of the
	   video's own corner pixels show between them — exactly the "tiny
	   stray corner pixels" this replaces. One mask, applied once, removes
	   the seam. Mobile reintroduces a radius here on purpose (below) — see
	   that rule's own comment for why the ownership genuinely differs
	   there. */
	overflow: hidden;
}

/* Failure fallback ONLY (assets/js/single-work-player.js) — no longer the
   normal loading state. Ships with no `src` (see player.php's `data-poster`),
   so it never paints and the browser never requests the thumbnail image
   during a normal successful load; JS sets `src` from `data-poster` only if
   the video genuinely fails to become playable. When that happens it simply
   needs to fill the frame — the video on top of it stays permanently at
   opacity 0 in that case, so no reveal/crossfade logic applies to this
   layer. */
.v2-player__poster {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	/* Always covers the fixed 16:9 frame, whatever the source thumbnail's
	   own aspect ratio (square/portrait included) — this is the one layer
	   that's allowed to crop; the video below it is not (see .v2-player__
	   video). Same object-fit: cover precedent as Home/Info's own poster
	   layers. */
	object-fit: cover;
}

/* Until JS assigns a `src` (failure fallback only) this <img> is an empty,
   src-less image. Browsers still paint its box as a broken-image outline
   (light silver hairline); the video on top hides it except at the
   anti-aliased edge pixels when the frame lands on a fractional device
   pixel, which showed up as a thin grey line on the frame's edges on real
   phones. Not rendered at all until it actually has an image to show. */
.v2-player__poster:not([src]) {
	display: none;
}

.v2-player__video {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	/* Single Work shows the actual video frame, not a crop (brief §28) —
	   unlike Work's thumbnails/previews, which use cover. Only the poster
	   layer above uses cover; this never changes. */
	object-fit: contain;
	background: #000;
	cursor: pointer;
	/* No opacity/transition of its own any more — .v2-player above is now
	   the single reveal owner for the whole player, so the video (like the
	   poster and the controls) is simply visible-or-not exactly as much as
	   its ancestor is. Two independent fades on nested elements (this one
	   previously keyed off `loadeddata`, the ancestor's off the page
	   entrance timeline) was exactly what could show one without the
	   other — a bare frame/outline before the video, or vice versa. */
}

.v2-player__controls-wrapper {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 1;
	padding: 28px var(--player-controls-padding) var(--player-controls-padding);
	/* Protects control legibility over bright footage without recoloring the
	   video itself (design-guidelines.md "Media overlays"). */
	background: linear-gradient(to top, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 100%);
	pointer-events: none;
}

.v2-player__controls-wrapper > * {
	pointer-events: auto;
}

.v2-player__bar {
	margin-bottom: 10px;
}

.v2-player__progress {
	position: relative;
	height: 14px;
	cursor: pointer;
}

/* Intentionally NOT --weight-body (typography-lightening pass): this is a
   functional control label sitting at 10-12px directly over arbitrary,
   unpredictable video footage (often bright/busy), not decorative copy on
   a guaranteed solid-black background — Light 300 at this size/contrast
   context is a real legibility risk for a control the visitor actually
   needs to read to operate the player, so it keeps Regular 400. */
.v2-player__time {
	position: absolute;
	top: 0;
	left: 0;
	font-family: var(--font-base);
	font-size: var(--type-utility-size);
	font-weight: 400;
	color: var(--color-fg);
	opacity: var(--opacity-secondary);
	white-space: nowrap;
}

.v2-player__controls {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
}

.v2-player__col {
	display: flex;
	align-items: center;
	gap: var(--player-btn-gap);
}

.v2-player__col--center {
	margin: 0 auto;
}

/* Same reasoning as .v2-player__time above — kept at Regular 400, not
   --weight-body. */
.v2-player__btn {
	font-family: var(--font-base);
	font-size: var(--type-utility-size);
	font-weight: 400;
	letter-spacing: 0.02em;
	color: var(--color-fg);
	background: none;
	border: none;
	padding: 0;
	margin: 0;
	cursor: pointer;
	opacity: var(--opacity-muted);
	transition: opacity var(--transition-ui);
	white-space: nowrap;
}

.v2-player__btn:hover,
.v2-player__btn.is-active {
	opacity: 1;
}

.v2-player__sep {
	font-size: var(--type-utility-size);
	color: var(--color-fg);
	opacity: 0.3;
}

.v2-player__video-placeholder,
.v2-single-work__video-placeholder {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	aspect-ratio: 16 / 9;
	background: rgba(240, 242, 228, 0.04);
	font-family: var(--font-base);
	font-size: var(--type-nav-size);
	color: var(--color-fg);
	opacity: var(--opacity-muted);
}

.v2-single-work__iframe {
	display: block;
	width: 100%;
	aspect-ratio: 16 / 9;
	border: none;
}

/* ─── Video player: mobile control layout ───────────────────────────────── */
/*
 * Desktop's 3-column `justify-content: space-between` row relies on the row
 * having more width than its (nowrap) content needs. On narrow viewports
 * that stops being true and the row overflows instead of wrapping — flex
 * items get a default min-width equal to their nowrap content, so
 * `flex-shrink` can't compress them — which is what pushed controls/time
 * outside the player. Fixed with a dedicated mobile layout rather than
 * trying to force the desktop row into less space:
 *   - a 3-column CSS grid (`minmax(0,1fr) auto minmax(0,1fr)`) for
 *     Mute/Unmute – Play/Pause – Fullscreen, so each column can only ever
 *     take its own track's space, never the next column's;
 *   - the time display drops out of the absolutely-positioned "slides along
 *     the track" behaviour (desktop-only) and becomes a small static,
 *     centered row of its own — still inside the same click-to-seek
 *     .v2-player__progress element, so seeking still works;
 *   - smaller utility type (10-11px) and larger invisible hit-padding on
 *     each button (~44px tap target) per this pass's brief §7-8.
 *
 * Revised per mobile-refinement feedback: controls no longer overlay the
 * video on mobile — they sit in normal document flow immediately below the
 * rounded 16:9 frame, on the page's own black background, so they never
 * cover the picture. The desktop overlay (absolute-positioned over the
 * frame, translucent gradient for legibility over footage) is completely
 * unchanged above; everything below is additive, scoped to this same
 * breakpoint.
 *
 * .v2-player__frame is where the rounded corners/clipping now live at this
 * breakpoint, since .v2-player__controls-wrapper is no longer a clipped
 * overlay sibling relying on .v2-player's own radius+overflow:hidden (see
 * .v2-player's desktop comment above) — that box becomes a plain, unclipped
 * flow container instead, so the (now outside-the-frame) controls are never
 * accidentally rounded/cut off.
 */

@media (max-width: 767px) {
	.v2-player {
		overflow: visible;
		border-radius: 0;
		background: none;
	}

	.v2-player__frame {
		border-radius: var(--v2-media-radius);
		/* .v2-player no longer carries this fallback tint on mobile (it's a
		   plain unclipped flow container now) — kept here instead so a
		   project with no poster image still shows the same subtle neutral
		   fill, not a sudden plain black frame, before the video paints. */
		background: rgba(240, 242, 228, 0.04);
	}

	.v2-player:fullscreen .v2-player__frame,
	.v2-player:-webkit-full-screen .v2-player__frame {
		/* Same reasoning as .v2-player's own fullscreen reset above — a real
		   screen-filling surface must never show an inset rounded frame. */
		border-radius: 0;
	}

	.v2-player__controls-wrapper {
		position: static;
		z-index: auto;
		/* No horizontal inset: the row spans exactly .v2-player__frame's
		   width (both are 100% of the same unpadded .v2-player parent), so
		   Mute aligns with the video's left edge and Fullscreen with its
		   right edge, per brief §4. Top padding is the video→controls gap
		   (brief §5's ~12-18px target); bottom padding is trailing space
		   before .v2-single-work__grid's own mobile gap (pages.css) takes
		   over ahead of the project title — not doubled with it. */
		padding: 16px 0 12px;
		background: none;
	}

	.v2-player__controls {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
		align-items: center;
		gap: 6px;
	}

	.v2-player__col--left {
		justify-content: flex-start;
		overflow: hidden;
	}

	.v2-player__col--center {
		margin: 0;
		justify-content: center;
	}

	.v2-player__col--right {
		justify-content: flex-end;
		overflow: hidden;
	}

	.v2-player__btn {
		font-size: 10px;
		/* Invisible hit-area padding: expands the tap target toward ~44px
		   without changing the button's visual size or the row's spacing —
		   the matching negative margin cancels the padding's layout effect. */
		padding: 14px 6px;
		margin: -14px -6px;
	}

	.v2-player__sep {
		font-size: 10px;
	}

	/* Drops the desktop "slides along the track" positioning (JS keeps
	   setting an inline `left`, but a static element ignores left/top
	   entirely) in favour of a small static, centered time row that can
	   never sit outside the player. */
	.v2-player__bar {
		margin-bottom: 8px;
	}

	.v2-player__progress {
		display: flex;
		align-items: center;
		justify-content: center;
		height: auto;
		min-height: 32px; /* touch target for seeking, brief §8 */
	}

	.v2-player__time {
		position: static;
		text-align: center;
		font-size: 10px;
	}
}
