/*
Theme Name: Michael Pawluk
Author: Michael Pawluk
Description: Personal portfolio theme for michaelpawluk.com.
Requires at least: 7.0
Requires PHP: 8.1
Version: 0.13.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: michaelpawluk
*/

/*
 * Everything that follows is theme-level, not page-level: the colour scheme, the skip link,
 * one focus ring and one screen-reader utility. Anything that belongs to a block or a layout
 * is a preset in theme.json instead. See docs/decisions.md, 2026-09-16.
 */

/*
 * Every palette value is a light-dark() pair, so this one declaration decides which half of
 * each pair resolves. It has to live here because color-scheme belongs on <html>, which
 * theme.json cannot reach.
 *
 * The site is light for everyone, whatever their device prefers, until they press the dark mode
 * switch; that sets data-theme="dark" on <html>, and the dark half of every pair takes over.
 * docs/decisions.md, "2026-09-21, dark mode".
 */
html {
	color-scheme: light;
}

html[data-theme="dark"] {
	color-scheme: dark;
}

/*
 * A browser without light-dark() would render every palette value as an invalid colour, which
 * means unstyled black on white in the best case. Writing the light half out again keeps those
 * browsers on the real design. html:root rather than :root so this wins wherever in the head
 * the theme stylesheet lands relative to the global styles.
 */
@supports not (color: light-dark(#000, #fff)) {
	html:root {
		--wp--preset--color--base: #ffffff;
		--wp--preset--color--contrast: #1c1c1c;
		--wp--preset--color--muted: #6b6b6b;
		--wp--preset--color--rule: #d6d6d6;
		--wp--preset--color--surface: #f4f4f4;
		--wp--preset--color--accent: #14c6d6;
		--wp--custom--overlay: rgb(28 28 28 / 0.74);
		--wp--custom--audio--wave: #c9c9c9;
		--wp--custom--audio--on-accent: #1c1c1c;
		--wp--custom--audio--pill: #e6e6e6;
		--wp--custom--audio--blue: #4aa3f0;
		--wp--custom--audio--green: #35c98a;
		--wp--custom--audio--amber: #f0b429;
		--wp--custom--audio--coral: #f2794a;
		--wp--custom--audio--pink: #ef6fa6;
		--wp--custom--audio--violet: #9b8cf5;
		--wp--preset--shadow--panel: 0 40px 90px -30px rgb(0 0 0 / 0.6);
		--wp--preset--shadow--dock: 0 10px 30px -12px rgb(0 0 0 / 0.5);
		--wp--preset--shadow--lift: 0 18px 34px -16px rgb(0 0 0 / 0.45);
	}
}

/*
 * One focus ring for the whole site, in the ink colour, replacing the copies that had grown
 * inside individual blocks. :focus-visible so a mouse click never draws it, only keyboard use.
 */
:focus-visible {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: 2px;
}

/*
 * Placeholder text, such as "Search" in the search field, in the muted colour: the browser's own
 * grey was about 4:1 on the field's surface tone. Firefox also fades it to half by default.
 */
::placeholder {
	color: var(--wp--preset--color--muted);
	opacity: 1;
}

/*
 * The footer's "Back to top" and the skip link glide rather than jump, unless the visitor has
 * asked their device for less motion. docs/decisions.md, "2026-09-21, footer".
 */
@media (prefers-reduced-motion: no-preference) {
	html {
		scroll-behavior: smooth;
	}
}

/* A 24px target for the footer's small mono link, the WCAG 2.5.8 minimum, without changing how it reads. */
.mp-back-to-top a {
	display: inline-block;
	min-height: 24px;
	line-height: 24px;
}

/*
 * Defined once here so any block can use it without shipping its own copy.
 */
.screen-reader-text {
	border: 0;
	clip-path: inset(50%);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute !important;
	white-space: nowrap;
	width: 1px;
	word-break: normal !important;
	word-wrap: normal !important;
}

/*
 * WordPress writes the skip link itself and points it at the main element's id. Only its
 * looks are ours. The selector carries the a to outrank core's own :focus rule, which would
 * otherwise paint it grey on grey whichever stylesheet happens to load second.
 */
a.skip-link.screen-reader-text:focus {
	background-color: var(--wp--preset--color--base);
	box-shadow: var(--wp--preset--shadow--dock);
	clip-path: none;
	color: var(--wp--preset--color--contrast);
	display: block;
	font-family: var(--wp--preset--font-family--host-grotesk);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	height: auto;
	left: 8px;
	line-height: 1;
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: 2px;
	padding: 14px 20px;
	text-decoration: none;
	top: 8px;
	width: auto;
	z-index: 100000;
}

/*
 * The footer sits at the bottom of the window on a short page (a 404, Contact, an album on a
 * phone) instead of halfway up it with blank page below. The page is at least the window's
 * height, less the admin bar when signed in, and the main area takes up the slack. Only the
 * space below the content changes; everything above keeps its place.
 */
.wp-site-blocks {
	display: flex;
	flex-direction: column;
	min-height: calc(100svh - var(--wp-admin--admin-bar--height, 0px));
}

.wp-site-blocks > main {
	flex-grow: 1;
}

/*
 * In a flex column neighbouring margins no longer merge, so the page's own gap between header,
 * main and footer would add to the title's top margin and the footer's, where before the larger
 * one won. The gap goes, and so does the last block's bottom margin, which the footer's larger
 * top margin always outweighed: the spacing is what it was before the flex column.
 */
.wp-site-blocks > * + * {
	margin-block-start: 0;
}

.wp-site-blocks > main > :last-child {
	margin-block-end: 0;
}

/*
 * A project's info line: the details (year, client) and the disciplines, read as one line with
 * the " · " the rest of the site uses between parts. They are two blocks side by side, so the
 * dot goes before whichever part isn't first; a part with nothing in it renders nothing and
 * takes its dot with it.
 */
.mp-facts-line > * + *::before {
	content: "\00a0·\00a0";
}

/*
 * Read as one line of text, so on a phone it wraps where words do, not a dot to a line. The
 * line's own size is 0 so the space between its two blocks in the markup doesn't show; each
 * part carries its own size.
 */
.mp-facts-line.is-layout-flex {
	display: block;
	font-size: 0;
}

.mp-facts-line > * {
	display: inline;
}

/*
 * The project header (docs/decisions.md, "2026-09-22, the project page: how it opens"): the
 * Description keeps to the text column's width, so a sentence at this size reads as a line, not
 * a banner; and a tall Lead image or Cover is held to the window, centred, never cropped.
 */
.mp-project-description {
	max-width: var(--wp--style--global--content-size, 720px);
	line-height: 1.4;
}

.mp-project-lead-image img {
	width: auto;
	max-width: 100%;
	max-height: calc(100svh - 8rem);
	margin-inline: auto;
}

/*
 * Tables and dividers in the site's hairlines (docs/decisions.md, "2026-09-22, the design pass").
 * Core draws every table cell in the text colour and the header with a 3px rule, and the
 * divider 2px thick, against the 1px grey lines everywhere else on the site. Cells take the rule
 * grey, the header one line in the text colour, the divider 1px.
 */
:root .wp-block-table th,
:root .wp-block-table td {
	border: 1px solid var(--wp--preset--color--rule);
}

:root .wp-block-table thead {
	border-bottom: 1px solid var(--wp--preset--color--contrast);
}

:root .wp-block-table th {
	font-weight: 500;
}

:root .wp-block-separator {
	border-top-width: 1px;
	border-bottom-width: 0;
}

/*
 * Previous and Next "With picture" (inc/hierarchy.php, mp_step_card()): the neighbour's picture
 * beside a small mono label and its name, which turns grey under the pointer like other titles.
 */
.mp-step-card {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--30);
	color: inherit;
	text-decoration: none;
}

.mp-step-card__image {
	flex: none;
	width: 132px;
	height: auto;
	aspect-ratio: 3 / 2;
	object-fit: cover;
	border-radius: var(--wp--preset--border-radius--small);
}

.mp-step-card__text {
	display: grid;
	gap: 4px;
}

.mp-step-card__label {
	font-family: var(--wp--preset--font-family--geist-mono);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	color: var(--wp--preset--color--muted);
}

.mp-step-card__title {
	font-size: var(--wp--preset--font-size--large);
	font-weight: 500;
	line-height: 1.2;
}

.mp-step-card:hover .mp-step-card__title {
	color: var(--wp--preset--color--muted);
}

/* With cards, "All …" gets its own line above them, and Previous and Next take the two sides. */
nav:has(> .mp-foot-steps .mp-step-card) {
	flex-direction: column;
	align-items: stretch;
}

.mp-foot-steps:has(.mp-step-card) {
	justify-content: space-between;
	gap: var(--wp--preset--spacing--40);
}

.mp-foot-steps .post-navigation-link-next:has(.mp-step-card) {
	margin-inline-start: auto;
}

/*
 * The Outline button drawn like the filled one: core gives it a 2px edge and its own padding;
 * this one has the site's 1px edge and the filled button's size, so the two sit level.
 */
:root .wp-block-button.is-style-outline > .wp-block-button__link {
	border-width: 1px;
	padding: var(--wp--preset--spacing--20) var(--wp--preset--spacing--30);
}

/*
 * Transparent artwork in dark mode (docs/decisions.md, "2026-09-22, the optional list"): a logo
 * or poster with a transparent background (marked mp-has-alpha by inc/images.php) gets a light
 * card behind it, so a dark mark doesn't sink into the charcoal. The card is the light surface
 * tone in both modes on purpose: it stands in for the paper the artwork was designed on. Drawn
 * with a shadow rather than padding, so the picture keeps its size and place.
 */
html[data-theme="dark"] img.mp-has-alpha {
	background-color: #f4f4f4;
	box-shadow: 0 0 0 clamp(8px, 1.5vw, 20px) #f4f4f4;
	border-radius: var(--wp--preset--border-radius--small);
}

/*
 * Printed, or saved as a PDF: the content, black on white whatever the mode, without the parts
 * that only work on a screen (the menu, the dark mode switch, the players' controls, embeds, the
 * foot links and the footer), and with each link's address written after it.
 */
@media print {
	html,
	html[data-theme="dark"] {
		color-scheme: light;
	}

	body {
		background: #fff !important;
		color: #000 !important;
	}

	.wp-site-blocks > header nav,
	.wp-block-michaelpawluk-theme-switch,
	.wp-site-blocks > footer,
	main nav[aria-label],
	.wp-block-michaelpawluk-more-projects,
	.mp-audio__lane,
	.mp-pause,
	.mp-viewer__controls,
	.mp-viewer__strip,
	.wp-block-embed,
	.wp-block-search {
		display: none !important;
	}

	.wp-block-post-content a[href^="http"]::after {
		content: " (" attr(href) ")";
		font-size: 0.8em;
		color: #555;
		word-break: break-all;
	}

	img,
	figure {
		break-inside: avoid;
	}
}

/*
 * A linked title — a project, album, journal post or playlist in a list — turns grey under the
 * pointer (Michael, 2026-09-22: subtler than an underline). The link's own underline, where it
 * has one, is unchanged.
 */
.mp-project-grid__title a:hover,
.mp-featured-projects__title a:hover,
.mp-featured-project__title a:hover,
.wp-block-post-title a:hover,
.mp-playlist-tile:hover .mp-playlist-tile__name {
	color: var(--wp--preset--color--muted);
}

@media (prefers-reduced-motion: no-preference) {
	.mp-project-grid__title a,
	.mp-featured-projects__title a,
	.mp-featured-project__title a,
	.wp-block-post-title a,
	.mp-playlist-tile__name {
		transition: color 0.15s ease;
	}
}

/*
 * The excerpts in a list of posts (archives, search) keep to the text width; across the wide
 * width they ran to about 170 characters a line.
 */
.mp-row-excerpt {
	max-width: var(--wp--style--global--content-size, 720px);
}

/*
 * A search box set to a share of the width (the search page's is 45%) never narrows below 22rem,
 * or the whole width where that is less: on a phone 45% left the field two letters wide beside
 * its button (the stable check, 2026-09-22). min-width outranks the width core writes inline.
 */
.wp-block-search__inside-wrapper {
	min-width: min(100%, 22rem);
}

/*
 * Core's password form, on a protected project, album or playlist, drawn like the Contact form:
 * the label above its field, the field with the same muted edge (3:1 against the page, as a
 * control's boundary needs), and the button, which core already gives the theme's button class,
 * beside the field. It stays in the text column, as the rest of the page's writing does.
 *
 * Core's markup is fixed, so this reaches it by its own class. The label wraps its field, so the
 * row is a two-row grid the label shares through subgrid: its words in the first row, the field
 * in the second, and the button in the second row too, as tall as the field whatever the font
 * size. The empty paragraph is left by the paragraph filter, which closes one the form never
 * opened.
 */
.post-password-form > p:empty {
	display: none;
}

.post-password-form > p:has(label) {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-rows: auto auto;
	gap: var(--wp--preset--spacing--10) var(--wp--preset--spacing--20);
	max-width: 36rem;
}

.post-password-form label {
	display: grid;
	grid-row: 1 / 3;
	grid-template-rows: subgrid;
	font-weight: 500;
}

.post-password-form input[type="password"] {
	box-sizing: border-box;
	width: 100%;
	min-height: 48px;
	padding: 10px 14px;
	border: 1px solid var(--wp--preset--color--muted);
	border-radius: var(--wp--preset--border-radius--small);
	background: var(--wp--preset--color--base);
	color: var(--wp--preset--color--contrast);
	font: inherit;
	font-weight: 400;
}

.post-password-form .wp-block-button {
	display: grid;
	grid-row: 2;
	grid-column: 2;
}

/* The field's height, which is more than the 44px a target needs. */
.post-password-form input[type="submit"] {
	min-width: 44px;
	min-height: 44px;
	font-family: inherit;
	cursor: pointer;
}

/* A wrong password is said in words and marked by weight and a heavier edge, as on Contact. */
.post-password-form-invalid-password p {
	font-weight: 500;
}

.post-password-form.password-form-error input[type="password"] {
	border-width: 2px;
	border-color: var(--wp--preset--color--contrast);
}

/*
 * Core's "Stripes" table colours every other row a fixed #f0f0f0, which in dark mode put light
 * text on a light row. The rows take the palette's surface instead, which has a dark twin. Here
 * rather than in theme.json, whose table styles reach the <table>, not the figure that carries
 * the style's class; :root outranks core's rule of the same shape.
 */
:root .wp-block-table.is-style-stripes {
	border-bottom-color: var(--wp--preset--color--rule);
}

:root .wp-block-table.is-style-stripes tbody tr:nth-child(odd) {
	background-color: var(--wp--preset--color--surface);
}

/*
 * A playlist with no cover yet.
 *
 * Its page is two columns, the cover on the left and the tracklist on the right. With no cover
 * the featured image block renders nothing, and an empty 40% column would hold the tracklist
 * into two thirds of the page for no reason. The empty column goes, and the tracklist takes the
 * width — !important because the column's own basis is written inline by the block.
 *
 * Here rather than in a block's stylesheet because it has to hold on a playlist with no songs
 * either, when no block of ours is on the page to enqueue one.
 */
.mp-playlist-cover:not(:has(img)) {
	display: none;
}

.mp-playlist-cover:not(:has(img)) + .wp-block-column {
	flex-basis: 100% !important;
}

/*
 * The Journal's lead post with no picture.
 *
 * Same move as the playlist cover above: the featured image renders nothing, so its column
 * goes and the writing takes the width, with the title set a size larger to hold the space the
 * picture would have. Never an empty frame.
 */
.mp-journal-lead-picture:not(:has(img)) {
	display: none;
}

.mp-journal-lead-picture:not(:has(img)) + .wp-block-column {
	flex-basis: 100% !important;
	max-width: 48rem;
}

.mp-journal-lead-picture:not(:has(img)) + .wp-block-column .wp-block-post-title {
	font-size: var(--wp--preset--font-size--xx-large) !important;
}

/*
 * The Journal's cards.
 *
 * Each card holds both a picture and a cover: the title set on the surface tone, for a post with
 * no featured image. Only one ever shows. A card with a picture hides the cover; a card without
 * one hides its second title, since the cover already carries it. display: none takes the hidden
 * one out of the accessibility tree too, so a screen reader hears each title once.
 */
.mp-journal-cover {
	aspect-ratio: 3 / 2;
	box-sizing: border-box;
}

.mp-journal-card:has(.wp-block-post-featured-image img) .mp-journal-cover,
.mp-journal-card:not(:has(.wp-block-post-featured-image img)) .mp-journal-card-title {
	display: none;
}

/*
 * The Journal's archive: tiles on the surface tone, flowing in columns so that a tile with a
 * picture and a tile without one can sit side by side at their own heights. The picture keeps
 * its own proportions, never cropped, with the tile's padding around it like a mat. Columns
 * rather than a grid, because a grid would square every row to its tallest tile. They read
 * down each column in date order, which is also the order in the markup, so the keyboard and a
 * screen reader follow the dates.
 */
.mp-journal-archive .wp-block-post-template {
	column-gap: var(--wp--preset--spacing--30);
	columns: 3 16rem;
}

.mp-journal-archive .wp-block-post-template > li {
	break-inside: avoid;
	margin: 0 0 var(--wp--preset--spacing--30);
}

/*
 * A square or upright picture would otherwise fill a phone's width, so height is capped: it stays
 * whole and sits centred on more of the mat instead.
 */
.mp-journal-tile .wp-block-post-featured-image img {
	display: block;
	height: auto;
	margin-inline: auto;
	max-height: 16rem;
	max-width: 100%;
	width: auto;
}

/*
 * The Photo essay template's lead picture: full width, square-cornered as any photo filling the
 * window is, and short enough that the date and title below it are on screen on arrival. The
 * 18rem is the header above plus the date, title and space below; 85% of the window alone left
 * the title just under the fold at 2560 × 1440. A 3:2 picture at 2560 wide would be 1700px tall.
 */
.mp-essay-lead img {
	border-radius: 0;
	max-height: calc(100svh - 18rem);
}

/*
 * The 404's photo (templates/404.html, inc/bindings.php): a wide band, 16:5 as the Image block
 * sets it, holding a photo from Home's showcase cropped around its own focal point. On a phone
 * 16:5 is barely 110px tall, so the band is never shorter than 11rem: about 2:1 at 375px, and
 * 16:5 again from about 620px. A floor rather than a second shape, so the block's own Aspect
 * ratio setting still decides at every other width. inc/bindings.php counts the same 11rem
 * when it chooses the file.
 *
 * And never more than 45% of the window's height, so on a short laptop window the search box
 * is still on screen on arrival, as it is at 1440 × 900, where 45% is exactly the 16:5 band.
 */
.mp-photo-band img {
	min-height: 11rem;
	max-height: 45svh;
}

/*
 * Contact: the form beside a photo (patterns/contact-page.php, the `mp-contact-split` class).
 *
 * The row is core's Media & Text, photo on the right and set to fill, so the photo takes the
 * row's height, and the row's height is the form's: the two start and end level. Core draws the
 * block for writing beside a picture, and some of its defaults don't suit a form, so this class,
 * which the pattern puts on the block, changes them. Michael can still drag the photo's width
 * and set its focal point in the editor; those are the block's own.
 */

/* Core sets the writing in from its column by 0 8%, which put the form's edge inside the title's
   and the email address's. No padding; the playlist page's gap between the two instead, and a
   smaller one under the photo when they stack. */
.wp-block-media-text.mp-contact-split {
	gap: var(--wp--preset--spacing--40) var(--wp--preset--spacing--50);
}

.wp-block-media-text.mp-contact-split > .wp-block-media-text__content {
	padding: 0;
}

/* The photo is never shorter than 3:2 at its width. Beside the full form that never shows, the
   form being taller; after sending, when the form becomes one line of thanks, it keeps the photo
   a picture rather than core's 250px strip. On a phone it is the whole height: a 3:2 band. The
   width is written out because a frame with a height and a ratio would otherwise take its width
   from them and run off the page. */
.wp-block-media-text.mp-contact-split > .wp-block-media-text__media {
	aspect-ratio: 3 / 2;
	width: 100%;
}

/* The medium radius, like the site's other large pictures. */
.wp-block-media-text.mp-contact-split > .wp-block-media-text__media img {
	border-radius: var(--wp--preset--border-radius--medium);
}

/* With no photo chosen, the block leaves an empty frame: blank half a row, and on a phone a band of
   blank page above the form. It goes, with the gap under it; the form keeps its half of the row.
   Not in the editor, where the empty frame is itself the placeholder with the Upload and Media
   Library buttons. (:has() can't hold another :has(), hence the two ways of saying "empty".) */
.mp-contact-split > .wp-block-media-text__media:not(.components-placeholder, :has(img, video)) {
	display: none;
}

.wp-block-media-text.mp-contact-split:not(:has(
	> .wp-block-media-text__media img,
	> .wp-block-media-text__media video,
	> .components-placeholder
)) {
	row-gap: 0;
}

/* Core stacks the two at 600px, the photo on top at a fixed 250px; the ratio above sizes it. */
@media (max-width: 600px) {
	.wp-block-media-text.mp-contact-split.is-stacked-on-mobile > .wp-block-media-text__media {
		height: auto;
		min-height: 0;
	}
}

/*
 * Image protection on phones: the long-press menu that offers "Save image" is the touch
 * version of the context menu blocked in inc/images.php. A picture that links to a page keeps
 * it, like any link; one inside a link to the image file itself doesn't.
 */
main img:not(a:not([href*="/wp-content/uploads/"]) img),
.mp-lightbox img {
	-webkit-touch-callout: none;
}
