/*
 * pm-showcase-panel — the project detail panel (WO-35, frame 92:883).
 *
 * Source frames: docs/design/frames/35-panel.json for the panel's inside —
 * the ink editorial column left and the fixed-724px media column right, the
 * close button top right inside 48px, the dots bottom centre — and
 * docs/design/frames/92-gallery-scroll.json (frame 92:629) for its box: from
 * the `wide` breakpoint up the dialog is 1239 × 900 at x = 201, NOT
 * full-viewport, with the gallery visible in that 201px strip behind a 25%
 * scrim and the panel's shadow falling onto it. Below `wide` it stays
 * full-bleed.
 *
 * WO-92 REVERSED a recorded client instruction, deliberately. ADR 0008 §13
 * point 6 records the client's feedback of 27 August 2026 ("the panel takes
 * the full viewport width; the Figma's 1239px gap goes"), which is what WO-35
 * built here as `inset: 0`. On 24 September 2026 the same client, citing this
 * frame, asked for the gap back. The frame never changed; the instruction
 * did. Do not "restore" the full-bleed panel as a bug fix — read
 * 92-gallery-scroll.json#contradictions[0] and ADR 0008 §13 point 6's
 * amendment first.
 *
 * Compiled to assets/css/components/showcase-panel.css by
 * src/build-tokens.mjs, which substitutes the breakpoint markers (a comment
 * holding `@bp <name>`) with the pixel value from tokens/tokens.json. Never
 * type a breakpoint width here.
 *
 * Every colour is a --pm-color-* token. No hex value and no literal
 * breakpoint width may appear in this file. Values the token file does not
 * hold (the 724px media column, the 6px meta gap, the 40px swipe threshold's
 * touch-action) are typed with a comment citing the extract key they come
 * from — the same technique as the grid's 898px heading measure.
 *
 * The two-column layout is the ≥ desktop rule (WO-35 AC 2: at laptop a
 * fixed 724px media column would leave almost nothing for the editorial);
 * below it the panel is one column, media first, and the whole panel
 * scrolls — developer judgement, MISTAKES.md §C7 (no mobile design exists;
 * the decisions are recorded in the work order's report).
 *
 * WO-52: the panel now slides in from the right and back out, over the
 * gallery — 52-panel-slide.json (frame 108:1331). The panel's own opacity
 * stays 1 (the still shows no fade on the panel itself); only `transform`
 * transitions. What fades is the scrim behind it, and that is `html::before`,
 * not the panel — see the scrim block below. Open uses --pm-motion-duration-slow /
 * --pm-motion-ease-enter; close uses --pm-motion-duration-base /
 * --pm-motion-ease-exit, selected by the `--closing` modifier the JS adds
 * before removing `--visible` (showcase-panel.js's async close). A
 * direct-URL open needs no transition at all: the server renders
 * `--visible` already applied (ShowcasePanel.php), so the first paint is in
 * place before any script runs — a CSS transition never fires on an
 * element's very first computed style.
 * TODO(open-question): duration and easing are ADR 0008 §3's provisional
 * motion tokens, not a designer spec (Eleonor) — 52-panel-slide.md "Open
 * questions".
 */

/*
 * The dialog. Fixed, and full-bleed (`inset: 0`) only BELOW the `wide`
 * breakpoint — from `wide` up the ≥ wide block at the end of this file
 * insets it 201px from the left, which is the frame's box (WO-92 AC 1).
 * z-index 100: above the header's z-index 10 (header.css), so the header is
 * not visible above the panel (WO-35 test 2), and above the scrim's 90.
 */
.pm-showcase-panel {
	position: fixed;
	inset: 0;
	z-index: 100; /* above the header (header.css z-index 10) */
	display: grid;
	grid-template-columns: 1fr;
	background-color: var(--pm-color-ink);
	color: var(--pm-color-surface);
	overflow-y: auto; /* below desktop the whole panel scrolls (WO-35 AC 2) */
	opacity: 1;
	transform: translateX(100%);
	transition: transform var(--pm-motion-duration-slow) var(--pm-motion-ease-enter);
}

.pm-showcase-panel--visible {
	transform: translateX(0);
}

/* WO-52: the JS adds this before removing --visible, so the outgoing slide
   reads the base/exit pair instead of the slow/enter pair it opened with. */
.pm-showcase-panel--closing {
	transition: transform var(--pm-motion-duration-base) var(--pm-motion-ease-exit);
}

/* The scroll lock the JS applies while the panel is open (WO-35 AC 5). */
html.pm-has-open-panel {
	overflow: hidden;
}

/*
 * WO-92 AC 10: the scrollbar gutter, UNCONDITIONALLY on html — never keyed to
 * the lock class above. Until WO-92 nothing was visible behind the panel, so
 * the ~15px reflow when `overflow: hidden` drops the page's scrollbar did not
 * matter; now it would shift the visible strip sideways on open. Keyed to the
 * lock it would reserve the gutter only while the panel is open and cause the
 * very jump AC 10 forbids. Invisible on macOS's overlay scrollbars — this is
 * for classic-scrollbar platforms.
 */
html {
	scrollbar-gutter: stable;
}

/*
 * WO-92: THE SCRIM. A full-viewport dim under the panel and over the page —
 * Texte 25% (92-gallery-scroll.json#measured.scrim.fill → the scrim-panel
 * role), which is what dims the gallery cards in the frame's visible strip.
 *
 * It is `html::before`. Read this before changing it — every other option was
 * tried and rejected across five review rounds:
 *
 * - NOT a new element and NOT a wrapper layer. The panel's markup lives
 *   inside a <template> written by ShowcaseGrid.php, so a layer could not be
 *   server-rendered onto the archive at all, and it broke five things in a
 *   lifecycle that is already delicate (applyModalState's reparent-to-body,
 *   buildPanel's appendChild, template()'s firstElementChild, beginClose's
 *   inert sweep, and inert's own inheritance) — besides creating a stacking
 *   context that would let the stuck header paint over the panel's top 64px.
 *   A pseudo-element is not a DOM node: the inert sweep cannot touch it,
 *   nothing is reparented, no stacking context is created.
 * - NOT keyed to the scroll-lock class (html.pm-has-open-panel). Three
 *   separate reasons: a pseudo-element that first EXISTS when a class lands
 *   has no before-change style and cannot fade in; the lock is released only
 *   in the JS's finish(), after the 250ms slide, so the scrim could not fade
 *   out without editing a forbidden function; and on the direct URL the lock
 *   class arrives from JS while `--visible` is server-rendered, so the first
 *   paint would miss it (AC 9).
 *
 * So it is declared UNCONDITIONALLY here at opacity 0 — it exists before the
 * panel opens and therefore has a style to animate from — and driven by the
 * panel's own `--visible` / `--closing` pair, exactly as the panel is. On the
 * direct URL that is correct on the FIRST paint, because ShowcasePanel.php
 * server-renders `--visible` and single-showcase.php renders the header, the
 * grid content and the footer behind the panel: there IS a page to dim.
 *
 * z-index 90 sits alone between the theme's z-index 10 declarations (the
 * header) and the panel's 100, both in the root stacking context. CAVEAT for
 * the next person: Elementor's Advanced tab writes a per-element z-index into
 * _elementor_data, where Git cannot see it. Nothing in the database sets one
 * today, but any value >= 90 an editor sets on an element in the root
 * stacking context paints THROUGH the scrim, and no code review would show it.
 *
 * It renders at every width: below the panel's gap breakpoint it is simply
 * invisible behind a full-bleed panel, which costs nothing and keeps one code
 * path (AC 5).
 */
html::before {
	content: '';
	position: fixed;
	inset: 0;
	z-index: 90; /* between the header's 10 and the panel's 100 */
	background-color: var(--pm-color-scrim-panel);
	opacity: 0;
	pointer-events: none; /* the strip's click is a window listener, not this (showcase-panel.js) */
	transition: opacity var(--pm-motion-duration-slow) var(--pm-motion-ease-enter);
}

html:has(.pm-showcase-panel--visible)::before {
	opacity: 1;
}

/* The panel's own timings, not a third pair (AC 11): in over `slow`, out over
   `base` — the JS adds --closing before removing --visible, so the outgoing
   fade reads this rule exactly as the panel's transform does. */
html:has(.pm-showcase-panel--closing)::before {
	transition: opacity var(--pm-motion-duration-base) var(--pm-motion-ease-exit);
}

/* The JS toggles hidden on fillable elements (meta items, spec block,
   dots) — never let a display override resurrect them. */
.pm-showcase-panel [hidden] {
	display: none !important;
}

/*
 * The editorial column (35-panel.json#measured.editorial): ink, padding
 * 64, a column with space-between so the action footer sits at the bottom
 * of the viewport-height column.
 */
.pm-showcase-panel__editorial {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	gap: var(--pm-space-10);
	padding: var(--pm-space-16); /* 64 — 35-panel.json#measured.editorial.padding */
	background-color: var(--pm-color-ink);
}

/*
 * The content block above the footer: eyebrow, title, meta row, rule,
 * story, spec block, rule — gap 40 (35-panel.json#measured.editorial.layout).
 */
.pm-showcase-panel__content {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--pm-space-10); /* 40 */
}

/*
 * The eyebrow — the section-header pattern's accent rule + Caption Meta
 * accent (35-panel.json#measured.editorial.eyebrow), the same structure
 * the grid's heading block uses.
 */
.pm-showcase-panel__eyebrow {
	display: inline-flex;
	align-items: center;
	gap: var(--pm-space-4);
	margin: 0;
}

.pm-showcase-panel__eyebrow-rule {
	flex: none;
	width: 40px; /* 35-panel.json#measured.editorial.eyebrow — the 40px accent rule */
	height: 1px;
	background-color: var(--pm-color-accent);
}

.pm-showcase-panel__eyebrow-text {
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-caption);
	font-weight: var(--pm-font-weight-400);
	line-height: var(--pm-leading-normal);
	letter-spacing: var(--pm-tracking-caption);
	text-transform: uppercase;
	color: var(--pm-color-accent);
}

/*
 * The title — the H1 style (Roboto Light 56 / 1 / -3) in surface, NOT
 * uppercase (35-panel.json#measured.editorial.title: the panel title keeps
 * the content's casing, unlike the page's uppercase H1s). An <h2>: the
 * dialog sits inside the page's heading outline, under the grid's H1.
 */
.pm-showcase-panel__title {
	margin: 0;
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-h1);
	font-weight: var(--pm-font-weight-300);
	line-height: var(--pm-leading-tight);
	letter-spacing: var(--pm-tracking-h1);
	color: var(--pm-color-surface);
}

/*
 * The meta row (35-panel.json#measured.editorial.meta_row): gap 24; each
 * item the 14px pin icon + gap 6 + Caption Meta uppercase in on-ink-soft
 * (Blush Linen @80 — the role WO-35 added); the "•" separator in accent.
 */
.pm-showcase-panel__meta {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--pm-space-6); /* 24 */
}

.pm-showcase-panel__meta-item {
	display: inline-flex;
	align-items: center;
	gap: 6px; /* 35-panel.json#measured.editorial.meta_row.location — icon-to-text gap 6 */
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-caption);
	font-weight: var(--pm-font-weight-400);
	line-height: var(--pm-leading-normal);
	letter-spacing: var(--pm-tracking-caption);
	text-transform: uppercase;
	color: var(--pm-color-on-ink-soft);
}

.pm-showcase-panel__meta-icon {
	flex: none;
	width: 14px; /* 35-panel.json#assets.map-pin.svg — 14x14 */
	height: 14px;
	color: var(--pm-color-accent); /* the pin's Terra Sienna stroke, currentColor in the SVG */
}

.pm-showcase-panel__meta-sep {
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-caption);
	line-height: var(--pm-leading-normal);
	color: var(--pm-color-accent);
}

/*
 * The 1px rules — the new divider-on-ink role (White @20,
 * 35-panel.json#measured.editorial.rule; the role landed with WO-38, which
 * recorded that WO-35 adds the same one). A div, not an <hr>: Elementor's
 * `.elementor hr` reset (MISTAKES.md §C15) must not reach it.
 */
.pm-showcase-panel__rule {
	width: 100%;
	height: 1px;
	background-color: var(--pm-color-divider-on-ink);
}

/*
 * The story — Body Light 16 / 1.6 in on-ink-soft. The frame reads 85%
 * opacity; 80 vs 85 is below the token grain, so the story shares the
 * on-ink-soft role (WO-35 AC 3, recorded).
 */
.pm-showcase-panel__story {
	width: 100%;
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: 16px; /* Body 16 — no token at this size; 35-panel.json#measured.editorial.story */
	font-weight: var(--pm-font-weight-300);
	line-height: var(--pm-leading-body); /* 1.6 */
	color: var(--pm-color-on-ink-soft);
}

.pm-showcase-panel__story p {
	margin: 0;
}

.pm-showcase-panel__story p + p {
	margin-top: var(--pm-space-4);
}

/*
 * The spec block (35-panel.json#measured.editorial.spec): gap 12, the
 * Caption Meta accent label and the Body 16 / 1.6 text in full surface.
 */
.pm-showcase-panel__spec {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--pm-space-3); /* 12 */
}

.pm-showcase-panel__spec-label {
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-caption);
	font-weight: var(--pm-font-weight-400);
	line-height: var(--pm-leading-normal);
	letter-spacing: var(--pm-tracking-caption);
	text-transform: uppercase;
	color: var(--pm-color-accent);
}

.pm-showcase-panel__spec-text {
	width: 100%;
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: 16px; /* Body 16 — no token at this size; 35-panel.json#measured.editorial.spec.text */
	font-weight: var(--pm-font-weight-300);
	line-height: var(--pm-leading-body);
	color: var(--pm-color-surface);
}

.pm-showcase-panel__spec-text p {
	margin: 0;
}

.pm-showcase-panel__spec-text p + p {
	margin-top: var(--pm-space-4);
}

/*
 * The action footer (35-panel.json#measured.editorial.action_footer): gap
 * 24, the H3 Subtitle 24 Light closing line in surface and the inverted
 * primary button — the pm-button component's --primary --on-dark compound
 * (WO-26 AC 4), geometry the system's, recoloured.
 */
.pm-showcase-panel__footer {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--pm-space-6); /* 24 */
}

.pm-showcase-panel__closing {
	margin: 0;
	font-family: var(--pm-font-family-base), system-ui, sans-serif;
	font-size: var(--pm-font-size-h3);
	font-weight: var(--pm-font-weight-300);
	line-height: var(--pm-leading-tight);
	letter-spacing: var(--pm-tracking-h3);
	color: var(--pm-color-surface);
}

/*
 * The media column (35-panel.json#measured.media): ink behind the photo,
 * the current image object-fit cover filling the column, the close button
 * top right inside 48px, the dots bottom centre. The project navigation
 * that used to sit below the dots is WITHDRAWN, not pending: it was never
 * designed, and ADR 0016 retires it on the client's ruling that the approved
 * frame governs this panel. Node 92:884 holds the image container, the close
 * control and the dots, and nothing else.
 */
.pm-showcase-panel__media {
	position: relative;
	order: -1; /* below desktop the media comes first (WO-35 AC 2) */
	height: 60vh; /* developer judgement §C7 — no mobile design; a photo hero above the editorial */
	background-color: var(--pm-color-ink);
	overflow: hidden;
}

.pm-showcase-panel__stage {
	position: absolute;
	inset: 0;
	touch-action: pan-y; /* horizontal swipes go to the carousel; vertical drags scroll the panel */
	cursor: pointer;
	user-select: none; /* a drag is the carousel's gesture, not a selection */
	-webkit-user-drag: none; /* the native image drag must not hijack the pointer stream */
}

.pm-showcase-panel__stage--empty {
	/* TODO(open-question): the zero-image placeholder is not designed —
	   the AC's surface-alt block is the proposal (WO-35 AC 4). */
	background-color: var(--pm-color-surface-alt);
	cursor: default;
}

/*
 * The carousel's images: stacked, the active one visible — the crossfade
 * is an opacity swap over the fast motion token (WO-35 AC 6), none under
 * reduced motion. Every image carries its src from build time: the pool
 * IS the preload — while image 1 displays, image 2 is fetched.
 */
.pm-showcase-panel__image {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: 0;
	transition: opacity var(--pm-motion-duration-fast) var(--pm-motion-ease-standard);
}

.pm-showcase-panel__image--active {
	opacity: 1;
}

/*
 * The close button (35-panel.json#measured.media.close): a 40px hit area
 * top right inside 48px; the circled-× SVG fills it, its currentColor
 * stroke painted surface.
 */
.pm-showcase-panel__close {
	position: absolute;
	top: var(--pm-space-12); /* 48 — inside 48px */
	right: var(--pm-space-12);
	width: 40px; /* 35-panel.json#measured.media.close — the 40px circled × */
	height: 40px;
	padding: 0;
	background-color: transparent;
	border: 0;
	color: var(--pm-color-surface);
	cursor: pointer;
}

.pm-showcase-panel__close:focus-visible {
	outline: 2px solid var(--pm-color-accent);
	outline-offset: 2px;
}

.pm-showcase-panel__close-icon {
	display: block;
	width: 100%;
	height: 100%;
}

/*
 * The media footer: the dots, bottom centre, sitting on the image
 * container's own 48px padding edge — 35-panel.json
 * #measured.media.dots_geometry_verified_2026_10_01.offset_above_media_column_bottom,
 * the same edge the close control is placed against at the top (WO-105 AC 3).
 * It was space-6 while the retired project navigation shared this column:
 * 24 + that row's 48px button min-height + a space-4 gap put the dots 88px
 * up, and removing the row alone would have dropped them to 24. Neither is
 * the frame's. No gap: the dots are this footer's only child now.
 *
 * The row spans the column width, so it never intercepts a click meant for
 * the image — only its controls do (pointer-events).
 */
.pm-showcase-panel__media-footer {
	position: absolute;
	right: 0;
	bottom: var(--pm-space-12); /* 48 — the padding edge, as the close control's top */
	left: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	pointer-events: none;
}

/*
 * The dots (35-panel.json#measured.media.dots): 16px circles, gap 8, the
 * active one White, the others White at 25% — one colour plus opacity, no
 * token of its own (WO-35 AC 4).
 *
 * White, not surface (WO-105 AC 1). The frame fills the dots White —
 * dots_geometry_verified_2026_10_01.active.fill, and the extract's own
 * carousel-dots-reference.svg export fills them white at the same 25%
 * alpha. Surface is Blush Linen, a different colour, and WO-35 reached for
 * it by mistake. --pm-color-white is the existing token for the White role
 * and lang-switcher.css already consumes it directly; --pm-color-on-accent
 * is NOT the substitute, because build-tokens.mjs also feeds that one into
 * the generated Mail palette. Surface is not redefined either — the close
 * control above keeps it on purpose, since close-circle.svg really is
 * exported with a Blush Linen stroke.
 */
.pm-showcase-panel__dots {
	display: flex;
	gap: var(--pm-space-2); /* 8 */
	pointer-events: auto;
}

.pm-showcase-panel__dot {
	width: 16px;
	height: 16px;
	padding: 0;
	background-color: var(--pm-color-white);
	border: 0;
	border-radius: 50%;
	opacity: 0.25; /* 35-panel.json#measured.media.dots — the inactive dot's 25% */
	cursor: pointer;
	transition: opacity var(--pm-motion-duration-fast) var(--pm-motion-ease-standard);
}

.pm-showcase-panel__dot--active {
	opacity: 1;
}

.pm-showcase-panel__dot:focus-visible {
	outline: 2px solid var(--pm-color-accent);
	outline-offset: 2px;
}

/*
 * ≥ desktop (1280): two columns — the editorial 1fr, the media fixed at
 * the extract's 724px (WO-35 AC 2). The panel itself stops scrolling; the
 * editorial column scrolls internally when a long story exceeds the
 * viewport height.
 */
@media (min-width: 1280px) {
	.pm-showcase-panel {
		grid-template-columns: 1fr 724px; /* 724 — 35-panel.json#measured.media.width */
		grid-template-rows: minmax(0, 1fr);
		overflow-y: hidden;
	}

	.pm-showcase-panel__media {
		order: 0;
		height: auto;
	}

	.pm-showcase-panel__editorial {
		overflow-y: auto;
	}
}

/*
 * ≥ wide (1440): the frame's box. The dialog stops covering the viewport and
 * sits 201px from the left edge — 201 + 1239 = 1440, the frame's own width —
 * leaving the gallery visible in that strip behind the scrim, with the panel's
 * shadow falling onto it (WO-92 AC 1, 3).
 *
 * The gap starts at `wide`, NOT at the `desktop` marker where the two-column
 * rule starts: at 1280 a 201px gap leaves 1079 − 724 = 355px for an editorial
 * column whose content needs 387 (92-gallery-scroll.json#measured
 * .editorial_content.inset.width) — it would crush the column. Below `wide`
 * `inset: 0` stands. The frame gives 1440 only; the gap at other widths is
 * Eleonor's open question, recorded in the extract and in WO-92.
 *
 * At 1440 the grid's `1fr 724px` makes the editorial column exactly the
 * frame's 515 (1239 − 724); above 1440 the editorial column absorbs the extra
 * width and the gap holds at 201.
 *
 * TODO(open-question): the gap at widths other than 1440 (Eleonor) — the
 * frame gives 1440 only. Shipping the Architect's rule: a fixed 201px from
 * `wide` up, full-bleed below. 92-gallery-scroll.json "open_questions".
 */
@media (min-width: 1440px) {
	.pm-showcase-panel {
		inset: 0 0 0 201px; /* 201 — 92-gallery-scroll.json#measured.panel.gap_left */

		/* The shadow is real from WO-92: at full width it fell outside the
		   viewport, so WO-35 recorded it and built nothing. The offsets cannot
		   be tokens — build-tokens.mjs emits only roles.color — so they are
		   typed here with their extract key, the same convention as the 724px
		   media column above; only the colour is a role (shadow-panel, AC 7). */
		box-shadow: 0 -8px 80px 36px var(--pm-color-shadow-panel); /* 35-panel.json#measured.panel.shadow */
	}
}

/*
 * Reduced motion: the slide (WO-52), the carousel crossfade and the scrim's
 * fade (WO-92) collapse to instant swaps. The JS closes synchronously under
 * this query too — it never adds --closing, so this rule is the only thing
 * neutralising the transform transition on open.
 *
 * `html::before` is listed EXPLICITLY, and so is its --closing form. The three
 * panel selectors above cannot reach a pseudo-element on html, and this theme
 * has no global reduced-motion reset, so without this line a reduced-motion
 * close would fade the scrim out over 400ms with the panel ALREADY REMOVED
 * from the DOM — a dimmed page with no panel, the one broken state this design
 * can produce (WO-92 AC 6, 11). The --closing form is here for specificity,
 * not for behaviour: `html:has(…--closing)::before` outranks a bare
 * `html::before`, so listing only the latter would leave the outgoing
 * transition in force if anything ever added --closing under this query.
 */
@media (prefers-reduced-motion: reduce) {
	.pm-showcase-panel,
	.pm-showcase-panel__image,
	.pm-showcase-panel__dot,
	html::before,
	html:has(.pm-showcase-panel--closing)::before {
		transition: none;
	}
}
