/*
 * Only what theme.json cannot express.
 *
 * ⛔ No colour literals. Every colour comes through a token — var(--wp--preset--color--*)
 * or a color-mix() derived from one. A hex here is a defect, not a shortcut.
 */

/* Global box-sizing: without it a 100%-wide field plus its padding overflows its column (the forms did). */
*,
*::before,
*::after {
	box-sizing: border-box;
}

/*
 * NO skip-link CSS here, deliberately.
 *
 * WordPress core injects its own for block themes -- _block_template_add_skip_link()
 * in wp-includes/block-template.php emits <a href="#wp--skip-link--target"> and gives
 * <main> that id. This file previously styled an .bwp-skip-link that no markup emitted,
 * under a comment claiming core does not provide one. Both halves were wrong: dead CSS,
 * and a false statement that would have sent the next reader to fix a non-problem.
 *
 * Verified against core source, not documentation, on 2026-08-20.
 */

/*
 * Whole-box clickable cards.
 *
 * Standing rule: a card with ONE destination must be clickable across its whole box, not
 * only on the title. The title link is stretched to cover the card, and the card itself
 * carries the hover and focus-within states so keyboard users see the same affordance.
 */
.bwp-card {
	position: relative;
	transition: transform 120ms ease, box-shadow 120ms ease;
}

.bwp-card:hover,
.bwp-card:focus-within {
	transform: translateY(-2px);
	box-shadow: 0 6px 20px color-mix(in srgb, var(--wp--preset--color--contrast) 12%, transparent);
}

.bwp-card .wp-block-post-title a::after {
	content: "";
	position: absolute;
	inset: 0;
}

@media (prefers-reduced-motion: reduce) {
	.bwp-card {
		transition: none;
	}
	.bwp-card:hover,
	.bwp-card:focus-within {
		transform: none;
	}
}

/*
 * Current-page state on the navigation.
 *
 * The menu is custom links (patterns/nav.php), which core never marks as current. The
 * theme's nav filter (functions.php, bwp_theme_nav_current_state) sets aria-current="page"
 * on the matching link, and this styles that attribute, so what a screen reader announces
 * and what the visitor sees cannot disagree. 500, not 600: the 600 face was one file
 * downloaded on every page for this one rule.
 */
.wp-block-navigation a[aria-current="page"] {
	text-decoration: underline;
	text-underline-offset: 0.3em;
	font-weight: 500;
}

/*
 * Header -- a DELIBERATE departure from FSE standards Rule 13 (opaque sticky header).
 *
 * Bill's current site floats the menu OVER the hero photo, and Task 2 keeps the design, so
 * the header is `position: fixed` and transparent at the top (recorded in the project's
 * architecture map). Rule 13's sticky trap still applies to anyone changing this back: sticky
 * must go on the <header class="wp-block-template-part"> wrapper, never on a group inside
 * parts/header.html, where it silently does nothing.
 *
 * A fixed header takes no space, so the page must leave room for it. That is
 * --bwp-header-clearance, and it is USED in three places below: the hero cover's top padding,
 * <main> on any page without a .bwp-hero, and scroll-padding-top so the
 * skip link and in-page anchors do not land under the bar.
 *
 * Solid on scroll: a CSS scroll-driven animation fades a ::before layer in, no JavaScript.
 * Opacity rather than background-color, so it can run on the compositor instead of
 * repainting the bar each frame. Browsers without animation-timeline show the layer from the
 * start, so the menu is always legible.
 *
 * The admin-bar offsets are not optional: without them a logged-in reviewer (the client,
 * during review) sees the header pinned underneath the admin bar and reports it as broken.
 *
 * ⚠️ Cannot be confirmed by grep. Screenshot top-of-page and mid-scroll, logged out,
 * desktop and mobile.
 */
:root {
	--bwp-header-clearance: 9rem;
	/* Plus the sign-up bar when it is showing, or focus and anchors land under the header (WCAG 2.4.11). */
	scroll-padding-top: calc(var(--bwp-header-clearance) + var(--bwp-topbar, 0px));
}

/*
 * --bwp-topbar: the Mailchimp sign-up bar's height when it is on the page (assets/js/topbar-offset.js),
 * else 0. <body> gets the same padding the plugin applies a frame later, so nothing moves when it
 * does. ⛔ No !important: that overrides the plugin's close/reopen transition and each one then
 * counts as layout shift (performance review, 2026-09-29).
 */
body {
	padding-block-start: var(--bwp-topbar, 0px);
}

header.wp-block-template-part {
	position: fixed;
	inset-inline: 0;
	inset-block-start: var(--bwp-topbar, 0px);
	z-index: 100;
}

header.wp-block-template-part::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-color: color-mix(in srgb, var(--wp--preset--color--base-3) 92%, transparent);
}

@supports (animation-timeline: scroll()) {
	header.wp-block-template-part::before {
		/* A page with nothing to scroll (Home: one-screen slideshow) leaves the timeline inactive and the animation unapplied, so the base must be the transparent start state. */
		opacity: 0;
		animation: bwp-header-solid linear both;
		animation-timeline: scroll(root);
		animation-range: 0 12rem;
	}

	@keyframes bwp-header-solid {
		from {
			opacity: 0;
		}
		to {
			opacity: 1;
		}
	}
}

/*
 * No hero photo to float over (posts, search, 404, the shop): the bar is solid from the top,
 * as the current site shows a dark band behind the menu on those pages.
 */
/* 0,1,2 like the rule above it; wins by coming after it. */
:where(body:not(:has(.bwp-hero))) header.wp-block-template-part::before {
	animation: none;
	opacity: 1;
}

/*
 * Any page without a hero starts below the bar; a hero carries the clearance itself. One rule
 * on <main>, keyed to the same "is there a hero" test as the solid bar above, so it covers
 * every template, including WooCommerce's own (shop category, product, cart, checkout), which
 * never carried a theme class. (Replaced the per-template .bwp-clear-header class, 2026-09-22.)
 */
:where(body:not(:has(.bwp-hero))) main {
	padding-block-start: var(--bwp-header-clearance);
}

/*
 * Project pages (the Projects post type, templates/single-ajr_content.html). Live's
 * Blacksilver project layout differs from the other pages, and Andrew asked for these to look
 * exactly like live (2026-09-29): the header is a SOLID band with the hero below it (not the
 * photo running up behind it), the hero is 550px tall, and the type is larger (70/52/38px, from
 * theme.json custom.project.*). The hero title's size is set on the block itself
 * (fse-conversion/project-parity.mjs); these rules cover the rest.
 *
 * Two tokens are USED by the projects' own block markup, not by a rule in this file, so they
 * can look dead and are not: custom.project.title (the hero title's size; this file only
 * re-points it on phones, below) and custom.width.project-photo (the content width of a
 * "Photo" group, live's 960px for a photo standing on its own; functions.php
 * bwp_theme_project_photo_sizes() reads it to describe that photo).
 *
 * Live changes all of it in ONE step at 767px, not gradually (measured at six widths,
 * 2026-09-30): type drops to 40/30/26px, the hero loses its fixed height and its spacing shrinks.
 * So below 768px the project tokens are pointed at their "-phone" values here, once, and every
 * rule (and the title's own inline size) follows.
 */
@media (max-width: 47.9375em) {
	.single-ajr_content {
		--wp--custom--project--title: var(--wp--custom--project--title-phone);
		--wp--custom--project--heading: var(--wp--custom--project--heading-phone);
		--wp--custom--project--subheading: var(--wp--custom--project--subheading-phone);
		--wp--custom--project--hero-height: 0px;
		--wp--custom--project--hero-pad-top: var(--wp--custom--project--hero-pad-top-phone);
		--wp--custom--project--hero-pad-bottom: var(--wp--custom--project--hero-pad-bottom-phone);
		--wp--custom--project--hero-gutter: var(--wp--custom--project--hero-gutter-phone);
	}
}

:where(body.single-ajr_content) header.wp-block-template-part::before {
	animation: none;
	opacity: 1;
}

:where(body.single-ajr_content) main {
	padding-block-start: var(--bwp-header-clearance);
}

.single-ajr_content h2.wp-block-heading {
	font-size: var(--wp--custom--project--heading);
}

.single-ajr_content h3.wp-block-heading {
	font-size: var(--wp--custom--project--subheading);
}

/*
 * Page hero: a full-width cover, the photo running up behind the transparent header.
 * 0,2,0 so it outranks core's `.wp-block-cover` padding wherever the files land in the cascade.
 */
.wp-block-cover.bwp-hero {
	/* Height scales with width like the current heroes: ~350px on a phone, ~580px at 1440. */
	min-block-size: clamp(22rem, 40vw, 40rem);
	padding-block-start: var(--bwp-header-clearance);
}

/*
 * Project pages: the hero sits below the solid header, so no clearance. Live's spacing: 260px
 * above the words and 130px below in a band at least 550px tall, the words centred in what is
 * left (so a two-line title makes the band taller, as on live), and 130px clear each side; on a
 * phone 150px and 70px, 30px each side, with no minimum. The Cover is border-box and centres its
 * content, so padding gives the same result.
 */
:where(body.single-ajr_content) .wp-block-cover.bwp-hero {
	min-block-size: var(--wp--custom--project--hero-height);
	padding-block: var(--wp--custom--project--hero-pad-top) var(--wp--custom--project--hero-pad-bottom);
	padding-inline: var(--wp--custom--project--hero-gutter);
}

/* The title wraps in live's 614px box ("LockDown / StarTrails"), and the line under it sits
   directly beneath. 0,1,1 and 0,1,2: enough to outrank the layout's max-width and block gap. */
:where(.single-ajr_content) .bwp-hero h1 {
	max-inline-size: var(--wp--custom--width--project-title);
}

:where(.single-ajr_content) .bwp-hero h1 + p {
	margin-block-start: 0;
}

/*
 * A gallery given the "One photo per row on phones" style, on a phone: one photo per row, as live
 * shows them. Core forces two columns below 600px with an ID-weight selector
 * (`:not(#individual-image)`), so this has to match it; recorded in .fse-preflight.json
 * outrankCore.
 */
@media (max-width: 37.4375em) {
	.is-style-bwp-stack.wp-block-gallery.has-nested-images figure.wp-block-image:not(#individual-image) {
		width: 100%;
	}
}

/*
 * Project galleries follow live's steps (counted at twenty widths, 2026-09-30): the photos sit
 * 10px apart below 1025px (theme.json custom.project.tight-gap; 40px above it, the block's own
 * gap); a gallery of three or more per row drops to two per row below 960px; every gallery is
 * one per row below 480px. Core keeps N per row from 600px (1,4,1 with its ID-weight
 * `:not(#individual-image)`) and two per row below that, so the two-per-row rule has to outrank
 * it; recorded in .fse-preflight.json outrankCore. `.columns-default` is a gallery whose count
 * was never chosen: core shows it three per row, so it is in the list.
 * functions.php bwp_theme_project_gallery_sizes() states these same steps in each photo's
 * `sizes`: change one and change the other.
 */
@media (max-width: 64em) {
	.single-ajr_content .wp-block-gallery.has-nested-images {
		--wp--style--unstable-gallery-gap: var(--wp--custom--project--tight-gap);
		gap: var(--wp--custom--project--tight-gap);
	}
}

@media (min-width: 30em) and (max-width: 59.9375em) {
	.single-ajr_content .wp-block-gallery.has-nested-images:is(.columns-default, .columns-3, .columns-4, .columns-5, .columns-6, .columns-7, .columns-8) figure.wp-block-image:not(#individual-image) {
		width: calc(50% - var(--wp--style--unstable-gallery-gap) / 2);
	}
}

@media (max-width: 29.9375em) {
	.single-ajr_content .wp-block-gallery.has-nested-images figure.wp-block-image:not(#individual-image) {
		width: 100%;
	}
}

/*
 * News cards: the date sits over a hairline rule, and "Continue reading" carries the short bar
 * beneath it, as on the current news page.
 */
.bwp-news-date {
	inline-size: 100%;
	padding-block-end: var(--wp--preset--spacing--20);
	border-block-end: 1px solid var(--wp--preset--color--border);
	font-size: var(--wp--custom--news--date-size);
	letter-spacing: var(--wp--custom--news--date-tracking);
	line-height: 1.3;
}

/* Live's clock before the date: an SVG drawn in the text colour through a mask. */
.bwp-news-date time::before {
	content: "";
	display: inline-block;
	inline-size: 1.2em;
	block-size: 1.2em;
	margin-inline-end: 0.35em;
	vertical-align: -0.25em;
	background-color: currentColor;
	-webkit-mask: url("../img/icon-clock.svg") center / contain no-repeat;
	mask: url("../img/icon-clock.svg") center / contain no-repeat;
}

/*
 * Single posts (News sign-off 2026-09-29, "Two layouts, as live"; custom.post tokens).
 * The date sits under the title in small italic, as live's older posts show it.
 */
.bwp-post-date {
	font-size: var(--wp--custom--post--date-size);
	font-style: italic;
	letter-spacing: var(--wp--custom--post--date-tracking);
}

/* Wide layout (template "Post: wide"): live's bold 34px title. */
.wp-block-post-title.bwp-post-title-wide {
	font-size: var(--wp--custom--post--title-wide-size);
	font-weight: 800;
	line-height: 1.05;
	letter-spacing: normal;
}

/*
 * Wide posts: a photo standing on its own fills the column, as live's did (the saved size is
 * often 768px; srcset still picks the right file for the wider box).
 */
:where(.bwp-post-wide) .wp-block-post-content > .wp-block-image img {
	inline-size: 100%;
	block-size: auto;
}

/* Previous/next: the neighbour's photo beside "PREV"/"NEXT" and its title (functions.php). */
.bwp-post-nav .wp-block-post-navigation-link a {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--30);
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
}

.bwp-post-nav .post-navigation-link-next a {
	flex-direction: row-reverse;
	text-align: end;
}

:where(.bwp-post-nav) a:hover .post-navigation-link__title {
	text-decoration: underline;
}

.bwp-post-nav__thumb {
	flex-shrink: 0;
	inline-size: var(--wp--custom--post--nav-thumb);
	block-size: var(--wp--custom--post--nav-thumb);
	border-radius: var(--wp--custom--post--nav-thumb-radius);
	object-fit: cover;
}

.bwp-post-nav--wide .bwp-post-nav__thumb {
	inline-size: var(--wp--custom--post--nav-thumb-wide);
	block-size: var(--wp--custom--post--nav-thumb-wide);
}

.bwp-post-nav__text {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--10);
}

.bwp-post-nav .post-navigation-link__label {
	font-size: var(--wp--custom--post--nav-label-size);
	font-weight: 600;
	letter-spacing: var(--wp--custom--post--nav-label-tracking);
	text-transform: uppercase;
}

.bwp-post-nav .post-navigation-link__title {
	line-height: 1.15;
}

/*
 * The card photo spans the card. The card is a centred vertical flex group, so without this the
 * figure shrank to its content and stayed 0x0 until the photo arrived: the first two cards
 * jumped 238px (CLS 0.06; review, 2026-09-29).
 */
.bwp-news-card > .wp-block-post-featured-image {
	inline-size: 100%;
}

/* Card title and link at live's size and tracking (custom.news tokens). */
.bwp-news-card .wp-block-post-title {
	font-size: var(--wp--custom--news--title-size);
	font-weight: 600;
	letter-spacing: var(--wp--custom--news--title-tracking);
}

.bwp-news-card .bwp-read-more {
	font-size: var(--wp--custom--news--link-size);
	letter-spacing: var(--wp--custom--news--link-tracking);
}

.bwp-read-more::after {
	content: "";
	display: block;
	inline-size: 2.5rem;
	margin: var(--wp--preset--spacing--20) auto 0;
	border-block-start: 2px solid currentColor;
}

/*
 * Small pages (sign-off, 2026-09-30).
 *
 * 404: the search box is the width of an article and centred under its sentence, not the full
 * 1080px column (patterns/error-404.php; no wrapper, so the width is set here).
 */
:where(.error404) main .wp-block-search {
	max-inline-size: var(--wp--custom--width--article);
}

/*
 * Search results and archives (patterns/blog-list.php, its section's class bwp-post-list): the
 * page links stand a section's space below the last result, which ends in its "Continue
 * reading" bar. The Pagination block takes no margin of its own, so it is set here, for this
 * list only; 0,2,0 to outrank the layout's block gap.
 */
.bwp-post-list > .wp-block-query-pagination {
	margin-block-start: var(--wp--preset--spacing--50);
}

/*
 * My Account: WooCommerce's own headings inside the page ("Login", "Billing address") are h2s,
 * which this theme draws at 48px for page sections. In a half-width address column "Shipping
 * address" broke over two lines and pushed its "Add" link out of line with its neighbour. They
 * are labels for a form, so they take the large text size. Only headings WooCommerce prints
 * (inside its own .woocommerce wrapper): a heading typed into the page itself keeps its size.
 */
.woocommerce-account .woocommerce h2 {
	font-size: var(--wp--preset--font-size--large);
}

/* Present for screen readers and search engines, not drawn (the Home page's h1). */
.bwp-visually-hidden {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/*
 * Home slideshow: a full-screen crossfade of the Home page's photos, as Blacksilver's fullscreen
 * slideshow was. The photos are plain Image blocks in the "Slides" group (.bwp-slides), so they
 * can be added, removed and reordered in the editor, and no core Gallery CSS has to be fought.
 * CSS only: every photo is stacked in the same place and crossfades, 8s each with 1s fades, as
 * live. The cycle, the keyframes and each photo's delay depend on how many photos there are, so
 * they are written per page by functions.php bwp_theme_slideshow_css() (any number works). The
 * first photo is visible from the start (it is the LCP image).
 * Reduced motion: no animation, the first photo only.
 * In the EDITOR the slides are laid out as a grid instead (below), so each one can be seen,
 * replaced, reordered or removed.
 */
.bwp-slideshow {
	position: relative;
	/* One screen, less the sign-up bar when it is open (it pads <body> above the slideshow). */
	block-size: calc(100svh - var(--bwp-topbar, 0px));
	min-block-size: 30rem;
	overflow: hidden;
	background-color: var(--wp--preset--color--base-3);
}

/* 0,2,0: flow layout gives the group a block gap after the hidden h1, which shifted every slide 20px down. */
.bwp-slideshow > .bwp-slides {
	position: absolute;
	inset: 0;
	margin: 0;
}

.bwp-slides > .wp-block-image {
	position: absolute;
	inset: 0;
	margin: 0;
	opacity: 0;
	animation-timing-function: linear;
	animation-iteration-count: infinite;
}

.bwp-slides > .wp-block-image img {
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
}

/* "Whole photo on phones" (functions.php): on a portrait screen the slide is shown whole on the dark background, not cropped. */
@media (max-aspect-ratio: 1 / 1) {
	:where(.bwp-slides) > .wp-block-image.is-style-bwp-whole img {
		object-fit: contain;
	}
}

/* 0,2,0 like the rule above; wins by coming after it. The first photo shows before (and without) the timing. */
:where(.bwp-slides) > .wp-block-image:first-child {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.bwp-slides > .wp-block-image {
		animation: none;
	}
}

/*
 * Editor: the slides as a grid of thumbnails. Stacked and faded as on the site, Bill would see
 * only the first photo and a click would land on an invisible one (Home sign-off, 2026-09-29).
 * The hidden page h1 is shown too, so he knows it is there. .editor-styles-wrapper exists only
 * in the editor canvas.
 */
:where(.editor-styles-wrapper) .bwp-slideshow {
	block-size: auto;
	min-block-size: 0;
	padding: var(--wp--preset--spacing--30);
	overflow: visible;
}

:where(.editor-styles-wrapper) .bwp-slideshow > .bwp-slides {
	position: static;
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
	gap: var(--wp--preset--spacing--20);
}

:where(.editor-styles-wrapper) .bwp-slides > .wp-block-image {
	position: static;
	aspect-ratio: 3 / 2;
	opacity: 1;
	animation: none;
}

:where(.editor-styles-wrapper) .bwp-slideshow .bwp-visually-hidden {
	position: static;
	inline-size: auto;
	block-size: auto;
	margin-block-end: var(--wp--preset--spacing--20);
	overflow: visible;
	clip-path: none;
	font-size: var(--wp--preset--font-size--medium);
	white-space: normal;
}

/*
 * Forms (ajr-forms: Contact, Workshops), styled like the current site's forms: white fields
 * with dark text, square corners, regular-weight labels, a full-width Send button. Set through
 * the plugin's own custom properties, so its layout and states are untouched.
 */
.ajr-form {
	--ajr-gap: var(--wp--preset--spacing--20);
	--ajr-surface: var(--wp--preset--color--contrast);
	--ajr-radius: 0;
	font-size: var(--wp--preset--font-size--small);
}

.ajr-form :is(input, select, textarea) {
	color: var(--wp--preset--color--base-3);
}

.ajr-form .ajr-field__label {
	font-weight: 400;
}

.ajr-form .ajr-form__submit {
	inline-size: 100%;
}

/*
 * A required field shows live's red asterisk. AJR Forms writes "(required)" in words, on
 * purpose, for screen readers; the words stay in the page (font-size 0 keeps them in the
 * accessibility tree) and the asterisk is drawn with empty alt text, so nothing is read twice.
 */
.ajr-form .ajr-field__required {
	font-size: 0;
	opacity: 1;
}

.ajr-form .ajr-field__required::after {
	padding-inline-start: 0.2em;
	color: var(--wp--preset--color--alert);
	font-size: var(--wp--preset--font-size--small);
	content: "*";
	content: "*" / "";
}

/* Choices side by side, wrapping on a narrow screen, as live's checkboxes sit. */
.ajr-form .ajr-choices {
	flex-direction: row;
	flex-wrap: wrap;
	column-gap: var(--wp--preset--spacing--30);
}

/*
 * Boxed tabs (About → Photography Background / Awards / Acting Awards), as on the current
 * site: the panel is a ruled box and the active tab is joined to it, its bottom edge painted
 * in the page colour over the panel's top rule. Core's underline indicator is removed.
 * Measurements are theme.json tokens (custom.tabs, custom.width.tabs), taken from live.
 *
 * The base button rule is kept at 0,1,1 so the active rule (0,2,0) wins: at 0,2,1 it reset
 * the active tab's border to transparent and the joined box never showed (About sign-off,
 * 2026-09-29). Core styles these buttons inside :where(), so neither needs more.
 */
/*
 * Phones (base): the titles stack as full-width ruled rows above the text, as live's do (live
 * opens each one's text directly under it; core's Tabs block keeps one panel, below all three).
 */
.bwp-tabs .wp-block-tab-list {
	flex-direction: column;
	gap: 0;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	letter-spacing: var(--wp--custom--tabs--tab-letter-spacing);
}

:where(.bwp-tabs) .wp-block-tab-list button {
	z-index: 1;
	inline-size: 100%;
	padding: var(--wp--custom--tabs--tab-padding-phone);
	border: 1px solid var(--wp--preset--color--contrast-2);
	border-block-end-width: 0;
}

/*
 * On phones the open title keeps core's underline (the text sits below all three rows, so its
 * position cannot show which one is open; a background shift alone was 1.17:1).
 */
:where(.bwp-tabs) .wp-block-tab-list [aria-selected="true"] {
	background-color: var(--wp--preset--color--base);
}

/* The site's focus ring, inside the button because the rows touch. */
:where(.bwp-tabs) .wp-block-tab-list button:focus-visible {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: -2px;
}

.bwp-tabs > .wp-block-tab-panels {
	margin-block-start: 0; /* the panel's top rule closes the last title row */
	padding: var(--wp--custom--tabs--panel-padding-phone);
	border: 1px solid var(--wp--preset--color--contrast-2);
}

/* Wider screens: tabs in a row, the active one joined to the ruled panel below it. */
@media (min-width: 768px) {
	.bwp-tabs .wp-block-tab-list {
		flex-direction: row;
	}

	:where(.bwp-tabs) .wp-block-tab-list button {
		inline-size: max-content;
		padding: var(--wp--custom--tabs--tab-padding);
		line-height: 1;
		border-color: transparent;
		border-block-end-width: 1px;
	}

	:where(.bwp-tabs) .wp-block-tab-list [aria-selected="true"] {
		border-color: var(--wp--preset--color--contrast-2);
		border-block-end-color: var(--wp--preset--color--base-2);
		background-color: var(--wp--preset--color--base-2);
	}

	/* The joined box marks the open tab here; core's underline would cut across it. */
	:where(.bwp-tabs) .wp-block-tab-list [aria-selected="true"]::before {
		content: none;
	}

	.bwp-tabs > .wp-block-tab-panels {
		margin-block-start: -1px;
		padding: var(--wp--custom--tabs--panel-padding);
	}
}

/*
 * A block pulled up over the hero (About's portrait, via a negative top margin). The cover is
 * positioned, so an unpositioned block that follows it would paint underneath it.
 */
.bwp-overlap {
	position: relative;
	z-index: 1;
	/* Scales with the hero: a fixed -10rem covered the page title on a phone. */
	margin-block-start: clamp(-10rem, -12vw, -3rem);
}

/* "—— PHOTO ——": the kicker with a short rule either side, as on the current heroes. */
.bwp-kicker--ruled {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1.25em;
}

.bwp-kicker--ruled::before,
.bwp-kicker--ruled::after {
	content: "";
	inline-size: 2rem;
	border-block-start: 1px solid currentColor;
	opacity: 0.6;
}

/*
 * Linked photo tiles (Galleries, Projects). The image is the whole link; hover and keyboard
 * focus dim it slightly, as the current site does. Opacity only, and none at all for reduced motion.
 */
.bwp-tiles .wp-block-image,
.bwp-tiles .wp-block-post-featured-image {
	margin: 0;
}

/* Block, or the inline <a> adds a line box under each photo and the rows drift 9px apart. */
.bwp-tiles .wp-block-image a,
.bwp-tiles img {
	display: block;
	inline-size: 100%;
	transition: opacity 200ms ease-out;
}

:where(.bwp-tiles) a:is(:hover, :focus-visible) img {
	opacity: 0.8;
}

@media (prefers-reduced-motion: reduce) {
	.bwp-tiles img {
		transition: none;
	}
}

/*
 * "Snug" tiles (the Projects page): below 600px they sit 10px apart, as live's project grid
 * does, not a full 40px (theme.json custom.project.tight-gap). 0,2,0: the grid's own gap is a
 * 0,1,0 rule printed after this file. functions.php bwp_theme_project_tile_sizes() counts on
 * this gap and this 600px step.
 */
@media (max-width: 37.4375em) {
	.bwp-tiles.bwp-tiles--snug {
		gap: var(--wp--custom--project--tight-gap);
	}
}

/*
 * Photo tiles with a name: the Galleries page (a Terms Query, fse-conversion/pages/galleries.js)
 * and the Projects page (a Query Loop, pages/projects.js). The Term Template prints a list, so
 * the bullets and indent go. The name sits over the photo and appears on hover or keyboard
 * focus, as the live site's albums and project grid did, and is always shown where there is no
 * hover (touch). The name's link is stretched over the whole tile, so the photo is clickable
 * too, with a single link.
 */
.wp-block-term-template.bwp-tiles {
	padding-inline-start: 0;
	list-style: none;
}

.bwp-photo-tile {
	position: relative;
}

.bwp-photo-tile :is(.wp-block-image, .wp-block-post-featured-image) {
	margin: 0;
}

.bwp-photo-tile__name {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	margin: 0;
	padding: var(--wp--preset--spacing--20);
	background-color: var(--wp--preset--color--overlay);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 500;
	text-align: center;
	opacity: 0;
	transition: opacity 200ms ease-out;
}

.bwp-photo-tile__name a {
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
}

.bwp-photo-tile__name a::after {
	position: absolute;
	inset: 0;
	content: "";
}

:where(.bwp-photo-tile:is(:hover, :focus-within)) .bwp-photo-tile__name {
	opacity: 1;
}

.bwp-photo-tile__name a:focus-visible {
	outline: none;
}

:where(.bwp-photo-tile__name) a:focus-visible::after {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: -4px;
}

/* Touch: the name always shows, on a strip at the foot of the photo. The box itself still covers
   the tile, so the stretched link does too. */
@media (hover: none) {
	.bwp-photo-tile__name {
		place-items: end stretch;
		padding: 0;
		background-color: transparent;
		font-size: var(--wp--preset--font-size--medium);
		opacity: 1;
	}

	.bwp-photo-tile__name a {
		padding: var(--wp--preset--spacing--10) var(--wp--preset--spacing--20);
		background-color: var(--wp--preset--color--overlay);
	}
}

@media (prefers-reduced-motion: reduce) {
	.bwp-photo-tile__name {
		transition: none;
	}
}

body.admin-bar header.wp-block-template-part {
	inset-block-start: calc(32px + var(--bwp-topbar, 0px));
}

@media (max-width: 782px) {
	body.admin-bar header.wp-block-template-part {
		inset-block-start: calc(46px + var(--bwp-topbar, 0px));
	}
}

/* The menu reads as white on the photo, not the 50% link colour used in body copy. */
.bwp-site-header .wp-block-navigation a {
	color: var(--wp--preset--color--contrast);
	opacity: 0.9;
}

.bwp-site-header a:is(:hover, :focus-visible) {
	opacity: 1;
}

/*
 * The logo at live's size (112px wide; set on the Site Logo block in parts/header.html, so it
 * can be changed in the editor). It keeps that size when the menu needs the room: as a flex item
 * it used to shrink to 41px at 1024. At 1024px and below it is live's smaller mark.
 */
.bwp-site-header .wp-block-site-logo {
	flex-shrink: 0;
}

@media (max-width: 1024px) {
	.bwp-site-header .wp-block-site-logo img {
		inline-size: var(--wp--custom--header--logo-small);
	}
}

/*
 * 600–1024px: the menu button, as live (live swaps to its mobile menu at 1024px). Core only
 * does that below 600px, and in between the ten items wrapped to three or four rows and ran
 * over the hero text, because the header is fixed and the pages leave it 9rem. These two rules
 * undo core's own `min-width: 600px` pair (0,3,0 and 0,2,0), so they carry one more class to
 * win wherever the files land in the cascade; recorded in .fse-preflight.json outrankCore.
 */
@media (min-width: 600px) and (max-width: 1024px) {
	.bwp-site-header .wp-block-navigation__responsive-container:not(.hidden-by-default):not(.is-menu-open) {
		display: none;
	}

	.bwp-site-header .wp-block-navigation__responsive-container-open:not(.always-shown) {
		display: flex;
	}
}

/*
 * Basket in the menu: a cart icon with the number of prints as a badge (Andrew, 2026-09-30).
 * The markup is an ordinary menu link: functions.php (bwp_theme_basket_link) hides the word
 * "Basket" from sight and adds the number. The icon is an SVG drawn in the link colour through
 * a mask, like the news clock.
 *
 * The link keeps core's own `display` (core sets it at the same weight, 0,2,0, so a value here
 * would win or lose by file order): the icon is an inline-block on ::before instead.
 */
.bwp-basket .wp-block-navigation-item__content {
	position: relative;
}

.bwp-basket .wp-block-navigation-item__content::before {
	content: "";
	display: inline-block;
	vertical-align: middle;
	inline-size: var(--wp--custom--header--basket-icon);
	block-size: var(--wp--custom--header--basket-icon);
	background-color: currentColor;
	-webkit-mask: url("../img/icon-cart.svg") center / contain no-repeat;
	mask: url("../img/icon-cart.svg") center / contain no-repeat;
}

.bwp-basket__count {
	position: absolute;
	inset-block-start: calc(var(--wp--custom--header--basket-badge) / -2);
	inset-inline-start: calc(var(--wp--custom--header--basket-icon) - var(--wp--custom--header--basket-badge) / 2);
	box-sizing: border-box;
	min-inline-size: var(--wp--custom--header--basket-badge);
	block-size: var(--wp--custom--header--basket-badge);
	padding-inline: 0.25em;
	border-radius: var(--wp--custom--header--basket-badge);
	background-color: var(--wp--preset--color--contrast);
	color: var(--wp--preset--color--base-3);
	font-size: var(--wp--custom--header--basket-badge-text);
	font-weight: 500;
	line-height: var(--wp--custom--header--basket-badge);
	letter-spacing: 0;
	text-align: center;
}

/*
 * On the basket page the other items show the current page with an underline, and the icon has
 * no words to underline: a rule under the icon instead, so sight and aria-current still agree.
 * Absolutely placed, so the icon does not move.
 */
:where(.bwp-basket) a[aria-current="page"]::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	inset-block-end: -0.4em;
	border-block-end: 1px solid currentColor;
}

/*
 * Shop collections (archive-product.html → WooCommerce Product Categories block), as tiles like
 * the current shop: the collection photo, 3:2, with its name on a dark strip along the bottom.
 * The block lists new collections by itself, so nothing here needs editing when one is added.
 */
/*
 * Each collection photo at its own shape, as the current shop shows them (a square book cover,
 * a near-square Somerset), tops aligned in each row. It was cropped to 3:2 (Shop sign-off,
 * 2026-09-29). The image is WooCommerce's full photo, so its width/height carry the real shape.
 */
.bwp-collections .wc-block-product-categories-list {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
	align-items: start;
	gap: var(--wp--preset--spacing--30);
	margin: 0;
	padding: 0;
	list-style: none;
}

.bwp-collections .wc-block-product-categories-list-item {
	position: relative;
	margin: 0;
}

.bwp-collections .wc-block-product-categories-list-item a {
	display: block;
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
}

.bwp-collections .wc-block-product-categories-list-item__image {
	display: block;
	inline-size: 100%;
	max-inline-size: none;
	margin: 0;
	border: 0;
}

.bwp-collections .wc-block-product-categories-list-item__image img {
	display: block;
	inline-size: 100%;
	block-size: auto;
}

.bwp-collections .wc-block-product-categories-list-item__name {
	position: absolute;
	inset-inline: 0;
	inset-block-end: 0;
	padding: var(--wp--preset--spacing--10) var(--wp--preset--spacing--20);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 500;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	background-color: color-mix(in srgb, var(--wp--preset--color--base-3) 80%, transparent);
}

:where(.bwp-collections) a:is(:hover, :focus-visible) img {
	opacity: 0.8;
}

/*
 * A print (single-product.html): one large photo, no zoom-lens clutter, never taller than the
 * screen, as on the current shop. WooCommerce's gallery floats half-width by default.
 */
:where(.bwp-print) .bwp-print-photo .woocommerce-product-gallery {
	/*
	 * ⛔ !important is justified here: WooCommerce prints an INLINE style="opacity: 0" on the
	 * gallery and only its script turns it back on, after jQuery. That kept the print photo (the
	 * page's LCP image) invisible until every script ran. An inline style can only be beaten by
	 * !important. The zoom and slider it waits for are switched off in functions.php.
	 */
	opacity: 1 !important;
	float: none;
	inline-size: 100%;
	margin-block-end: 0;
}

/*
 * The print-size choice: a white box with dark text, 612px wide (60% of the column), as live's.
 * WooCommerce left it transparent, so its dark text sat unreadable on the dark page (Andrew,
 * 2026-09-29: "this should be white").
 */
:where(.bwp-print) .variations,
:where(.bwp-print) .variations td.value {
	inline-size: 100%;
}

/*
 * 0,4,4 to out-rank WooCommerce's `.woocommerce div.product form.cart .variations select` (0,4,3),
 * whose `background` shorthand resets the colour to transparent. WooCommerce is not core, so it is
 * recorded as a reasoned cssBaseline entry in .fse-preflight.json, not outrankCore. Only the colour
 * longhands, so Woo's dropdown arrow stays, and the end padding keeps room for that arrow.
 */
body.single-product div.product form.cart .variations select {
	inline-size: min(100%, calc(var(--wp--custom--width--print) * 0.6));
	max-inline-size: 100%;
	padding-block: var(--wp--preset--spacing--10);
	padding-inline-start: var(--wp--preset--spacing--20);
	padding-inline-end: calc(var(--wp--preset--spacing--20) + 1.75rem);
	border: 1px solid var(--wp--preset--color--border);
	background-color: var(--wp--preset--color--contrast);
	color: var(--wp--preset--color--base-3);
}

/*
 * A collection's prints (patterns/collection-prints.php): a photo grid as the current shop's,
 * in the 1020px print column, one per row on phones and four across from 600px, each photo at
 * its own shape, tops aligned (Shop sign-off 2026-09-29, "Photo grid, as live").
 */
.bwp-print-grid {
	max-inline-size: var(--wp--custom--width--print);
	margin-inline: auto;
}

.bwp-print-grid .wp-block-post-template {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	align-items: start;
	gap: var(--wp--preset--spacing--40);
}

/*
 * Each photo at its own shape, filling its column. The grid's gap spaces the tiles, so the
 * list's flow-layout margin (on every tile but the first) would drop the first row out of line.
 */
.bwp-print-grid .wp-block-post-template > li,
.bwp-print-grid .wp-block-post-featured-image {
	margin-block: 0;
}

.bwp-print-grid .wp-block-post-featured-image img {
	inline-size: 100%;
	block-size: auto;
}

@media (min-width: 37.5em) {
	.bwp-print-grid .wp-block-post-template {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

/* A print's breadcrumb, as the current shop's: small grey capitals, wide tracking, » between. */
.bwp-crumbs {
	color: var(--wp--preset--color--contrast-2);
	font-size: var(--wp--preset--font-size--small);
	letter-spacing: var(--wp--custom--post--date-tracking);
	text-transform: uppercase;
}

.bwp-crumbs a {
	color: inherit;
	text-decoration: none;
}

.bwp-crumbs a:is(:hover, :focus-visible) {
	text-decoration: underline;
}

/*
 * No quantity box on a print (functions.php): its wrapper takes no space; the hidden 1 still
 * posts. 0,4,1 to out-rank WooCommerce's add-to-cart-form block rule
 * `.wp-block-woocommerce-add-to-cart-form form.cart .quantity { display: inline-flex }` (0,3,0);
 * at 0,2,1 it silently lost (standards review, 2026-09-29).
 */
.wp-block-woocommerce-add-to-cart-form form.cart .quantity:has(> input[type="hidden"]) {
	display: none;
}

:where(.bwp-print) .variations :is(tbody, tr, td.value) {
	display: block;
	inline-size: 100%;
}

/*
 * Basket: each print at half the product column (Andrew, Shop sign-off 2026-09-29), not
 * WooCommerce's 80px. Its fixed widths sit at 0,3,1 (the column header on a wide basket, the
 * row grid on a narrow one), so these are 0,4,1 (recorded in .fse-preflight.json). The file is
 * sized to match in functions.php (bwp_theme_basket_image).
 */
.wp-block-woocommerce-cart table.wc-block-cart-items .wc-block-cart-items__header .wc-block-cart-items__header-image {
	width: 50%;
}

.wp-block-woocommerce-cart .wc-block-cart table.wc-block-cart-items .wc-block-cart-items__row {
	grid-template-columns: 50% minmax(0, 1fr);
}

/*
 * The terms line and its two texts under the checkout button (bwp-migrate Setup::basket_info),
 * small and soft as the current shop's. Inside the filled basket only, so an empty one has none.
 */
.bwp-basket-info {
	margin-block: var(--wp--preset--spacing--20) 0;
	color: var(--wp--preset--color--contrast-2);
	font-size: var(--wp--preset--font-size--small);
}

/*
 * "From £30.00" under the size dropdown (functions.php bwp_theme_print_price_from). Hidden once a
 * size is chosen and WooCommerce shows that size's own price.
 */
.bwp-price-from {
	margin-block: 0 var(--wp--preset--spacing--30);
}

/*
 * Keep it close to the dropdown: WooCommerce pads the size cell with the block gap (0,2,3, over
 * the cap), so the gap variable is zeroed on that cell instead; the table's own 1em margin is
 * the space left above the price.
 */
:where(.bwp-print) .variations td.value {
	--wp--style--block-gap: 0;
}

:where(.variations_form:has(.woocommerce-variation-add-to-cart-enabled)) .bwp-price-from {
	display: none;
}

/*
 * The print-size choice has no visible label on the current shop (its prompt "Select a size"
 * says what it is); the label stays in the page for screen readers.
 */
:where(.bwp-print) .variations th.label {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* WooCommerce's Product Image Gallery block caps itself at 512px; the print photo spans the column. */
:where(.bwp-print) .bwp-print-photo.wp-block-woocommerce-product-image-gallery {
	max-inline-size: none;
}

/*
 * The photo fills the 1020px column as live's does (custom.width.print), never taller than 85% of
 * the screen. WooCommerce pins the image to `width: 100%` (0,3,3, out-ranking this), so a portrait
 * print keeps the column width and would squash to 85vh tall: `object-fit: contain` draws it at
 * its own proportions inside that box instead (performance review, 2026-09-29: portraits showed
 * 2.3x stretched). Its `sizes` hint is per photo (functions.php bwp_theme_print_photo_sizes_for).
 */
:where(.bwp-print) .bwp-print-photo img {
	object-fit: contain;
	inline-size: auto;
	block-size: auto;
	max-inline-size: 100%;
	max-block-size: 85vh;
	margin-inline: auto;
}

/*
 * Basket and checkout fields (WooCommerce blocks): white fields with deep text, as the contact
 * form. WooCommerce floats each label inside its white field with `color: inherit` (0,2,1), so
 * inheriting the page's white it disappeared; the field wrapper passes it the deep text instead.
 * Validation messages keep WooCommerce's own red. Shop sign-off 2026-09-29.
 */
:is(.wc-block-components-text-input, .wc-block-components-textarea, .wc-blocks-components-select) {
	color: var(--wp--preset--color--base-3);
}

/* Small uppercase label with wide tracking: "PHOTO", "BUY A PRINT", the copyright line. */
.bwp-kicker {
	letter-spacing: 0.35em;
	text-transform: uppercase;
}

/* ─────────────────────────────────────────────────────────────────────────────
 * SITE CHROME HAS NO GAP. Header, main and footer butt together.
 *
 * ⛔ THIS RULE IS LOAD-BEARING. Do not delete it as "an override that shouldn't
 * be needed" — it exists because WordPress conflates two different decisions
 * under one setting.
 *
 * `styles.spacing.blockGap` in theme.json sets the default gap BETWEEN BLOCKS IN
 * CONTENT (paragraph to heading, card to card) — which a client typing a page
 * genuinely wants. But WordPress also emits it as:
 *
 *     :where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--XX); }
 *
 * whose children are the HEADER, MAIN and FOOTER. So the same number that spaces
 * two paragraphs also drives a band of blank page between the header and the hero.
 * On a full-bleed banded site that reads as a broken layout, and it has now shown
 * up on more than one theme built from this starter.
 *
 * Setting the root blockGap to 0 "fixes" it and takes the content spacing with it,
 * which is why that is the wrong fix. Zeroing only the site-level children keeps
 * both behaviours correct and independent.
 *
 * Any band that genuinely needs breathing room sets its own padding — every
 * section in this theme already does.
 * ───────────────────────────────────────────────────────────────────────────── */
:where(.wp-site-blocks) > * {
	margin-block-start: 0;
	margin-block-end: 0;
}

/* ⛔ AND AGAIN, WITH SPECIFICITY THAT CANNOT LOSE A SOURCE-ORDER FIGHT.
 *
 * The rule above uses the SAME selector WordPress does, so it has the same (zero)
 * specificity and only wins by coming later in the cascade. That held locally and FAILED on
 * staging: SiteGround's Speed Optimizer concatenates every stylesheet into one combined
 * file, and in that file our CSS landed BEFORE core's global styles — so core won and the
 * gap came back. Identical code, different result, purely because of an optimiser.
 *
 * Naming the actual children is specificity 0,0,2 against core's 0,0,0. It wins wherever it
 * ends up, on any host, with any combiner. Do not "simplify" this back into one rule. */
.wp-site-blocks > header,
.wp-site-blocks > main,
.wp-site-blocks > footer,
.wp-site-blocks > .wp-block-template-part {
	margin-block-start: 0;
	margin-block-end: 0;
}
