/*
 * Globalny arkusz motywu Zakątek Urody / O! Rzęsy.
 *
 * Spis sekcji:
 *   1. Aliasy tokenów
 *   2. Reset
 *   3. Typografia
 *   4. Layout i kontenery
 *   5. Powłoka strony
 *   6. Przyciski
 *   7. Formularze
 *   8. Komunikaty i okruszki
 *   9. Interakcje i dostępność
 *
 * ŹRÓDŁEM PRAWDY dla kolorów, rozmiarów pisma i odstępów jest theme.json.
 * Aliasy poniżej wskazują na presety — nie definiują wartości od nowa.
 * Druga, niezależna definicja tej samej wartości jest w tym projekcie błędem:
 * nie rozjedzie się od razu, tylko przy pierwszej zmianie koloru.
 */

/* ==========================================================================
   1. Aliasy tokenów
   ========================================================================== */

:root {
	/* Kolory */
	--zu-bg: var(--wp--preset--color--bg);
	--zu-surface: var(--wp--preset--color--surface);
	--zu-surface-2: var(--wp--preset--color--surface-2);
	--zu-blush: var(--wp--preset--color--blush);
	--zu-line: var(--wp--preset--color--line);
	--zu-ink: var(--wp--preset--color--ink);
	--zu-ink-soft: var(--wp--preset--color--ink-soft);
	--zu-ink-muted: var(--wp--preset--color--ink-muted);
	--zu-accent: var(--wp--preset--color--accent);
	--zu-accent-deep: var(--wp--preset--color--accent-deep);
	--zu-deep: var(--wp--preset--color--deep);
	--zu-deep-line: var(--wp--preset--color--deep-line);
	--zu-deep-muted: var(--wp--preset--color--deep-muted);
	--zu-gold-text: var(--wp--preset--color--gold-text);

	/*
	 * Jedyny kolor definiowany poza theme.json, i to celowo. Kontrast 4.01:1
	 * na tle --zu-bg nie spełnia WCAG AA dla tekstu podstawowego, więc nie
	 * udostępniamy go w palecie edytora. Dozwolony dla obramowań, linii
	 * i tekstu w rozmiarze large. Do tekstu służy --zu-gold-text (5.48:1).
	 */
	--zu-gold-decor: #94714f;

	/*
	 * Kolor komunikatu o powodzeniu. Również poza theme.json i z tego samego
	 * powodu: to token interfejsu, a nie kolor treści — klientka nie powinna
	 * móc pomalować nim akapitu. Prototyp go nie zawierał, bo nie miał
	 * formularzy; dobrany do ciepłej palety i zmierzony: 6.24:1 na --zu-bg,
	 * 6.89:1 dla białego tekstu na nim.
	 *
	 * Dla błędu świadomie NIE wprowadzamy drugiej czerwieni — burgund marki
	 * (--zu-accent-deep, 7.30:1) już czyta się jako ostrzeżenie, a druga
	 * czerwień obok niego tylko zamuliłaby paletę.
	 */
	--zu-success: #37634a;

	/* Typografia */
	--zu-font-display: var(--wp--preset--font-family--display);
	--zu-font-sans: var(--wp--preset--font-family--sans);

	--zu-text-label: var(--wp--preset--font-size--x-small);
	--zu-text-sm: var(--wp--preset--font-size--small);
	--zu-text-base: var(--wp--preset--font-size--medium);
	--zu-text-lead: var(--wp--preset--font-size--large);
	--zu-text-h2: var(--wp--preset--font-size--x-large);
	--zu-text-h1: var(--wp--preset--font-size--xx-large);
	--zu-text-display: var(--wp--preset--font-size--display);

	/* Odstępy */
	--zu-space-1: var(--wp--preset--spacing--20);
	--zu-space-2: var(--wp--preset--spacing--30);
	--zu-space-3: var(--wp--preset--spacing--40);
	--zu-space-4: var(--wp--preset--spacing--50);
	--zu-space-5: var(--wp--preset--spacing--60);
	--zu-space-6: var(--wp--preset--spacing--70);

	/*
	 * Layout. Szerokości są aliasami na settings.layout z theme.json —
	 * WordPress emituje je jako zmienne globalne. Wcześniej te same liczby
	 * stały wpisane wprost tutaj, czyli w dwóch niezależnych miejscach:
	 * zmiana jednego nie ruszała drugiego, a bloki i nasze kontenery
	 * rozjechałyby się po pierwszej korekcie szerokości.
	 */
	--zu-container: var(--wp--style--global--wide-size);
	--zu-container-text: var(--wp--style--global--content-size);
	--zu-gutter: clamp(1rem, 4vw, 2.5rem);

	/*
	 * Próg przełączenia nawigacji na układ szeroki. Wartości nie da się użyć
	 * wprost w @media — składnia tego nie pozwala — więc liczba pojawia się
	 * w regule media poniżej. Skrypt nawigacji czyta ją STĄD, a nie ma wpisanej
	 * własnej kopii, żeby JS i CSS nie mogły się rozjechać.
	 */
	--zu-bp-nav: 62rem;

	/* Kształt — obie wartości odczytane z prototypu */
	--zu-radius: 18px;
	--zu-radius-sm: 8px;
	--zu-radius-pill: 999px;

	/* Ruch */
	--zu-transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);

	/* Stan skupienia — jeden kontrakt dla całego interfejsu */
	--zu-focus-color: var(--zu-accent-deep);
	--zu-focus-width: 2px;
	--zu-focus-offset: 2px;
}

/* ==========================================================================
   2. Reset
   ========================================================================== */

*,
*::before,
*::after {
	box-sizing: border-box;
}

body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
	margin: 0;
}

html {
	-webkit-text-size-adjust: 100%;
}

/*
 * Tło, kolor, rodzina i rozmiar pisma oraz interlinia body pochodzą z
 * theme.json — WordPress emituje je w global-styles. Nie powtarzamy ich tutaj.
 * Zostaje wyłącznie to, czego theme.json nie wyraża: układ powłoki strony.
 */
body {
	min-height: 100vh;
	display: flex;
	flex-direction: column;
	text-rendering: optimizeSpeed;
}

/* Obraz bez wymiarów to gwarantowany CLS — dlatego wymuszamy proporcje. */
img,
picture,
svg,
video {
	display: block;
	max-width: 100%;
	height: auto;
}

input,
button,
textarea,
select {
	font: inherit;
	color: inherit;
}

ul,
ol {
	margin: 0;
}

/* ==========================================================================
   3. Typografia
   ========================================================================== */

/*
 * Rodzina, waga, interlinia, tracking i rozmiary h1-h4 pochodzą z theme.json
 * (styles.elements.heading oraz styles.elements.h1..h4). Dzięki temu edytor
 * i front czytają jedno mapowanie, a nie dwa, które trzeba pamiętać zsynchronizować.
 * Tutaj zostaje tylko to, czego theme.json nie potrafi wyrazić.
 */
h1,
h2,
h3,
h4 {
	text-wrap: balance;
}

p {
	text-wrap: pretty;
}

/*
 * „Eyebrow" — mała, rozstrzelona etykieta wersalikami nad nagłówkiem.
 * Charakterystyczny element języka wizualnego prototypu; tracking 0.22em
 * i wersaliki odczytane z jego CSS.
 */
.zu-eyebrow {
	font-family: var(--zu-font-sans);
	font-size: var(--zu-text-label);
	font-weight: 500;
	letter-spacing: 0.22em;
	text-transform: uppercase;
	color: var(--zu-ink-muted);
}

/* Tytuł w skali „display" — zarezerwowany dla hero i tytułów stron. */
.zu-display {
	font-size: var(--zu-text-display);
	line-height: 1.08;
}

.zu-lead {
	font-size: var(--zu-text-lead);
	color: var(--zu-ink-soft);
	line-height: 1.5;
}

.zu-muted {
	color: var(--zu-ink-muted);
}

/* Kolor odnośnika i jego stan hover pochodzą z theme.json (styles.elements.link). */
a {
	text-underline-offset: 0.2em;
	transition: color var(--zu-transition);
}

/* ==========================================================================
   4. Layout i kontenery
   ========================================================================== */

/*
 * Jeden mechanizm kontenera dla całej strony. min() zamiast max-width
 * z marginesami: szerokość i rynna liczą się w jednej deklaracji, więc nie ma
 * gdzie powstać rozjazd między sekcjami.
 */
.zu-container {
	width: min(100% - (2 * var(--zu-gutter)), var(--zu-container));
	margin-inline: auto;
}

.zu-container--text {
	width: min(100% - (2 * var(--zu-gutter)), var(--zu-container-text));
	margin-inline: auto;
}

/* Sekcja — pionowy rytm strony. */
.zu-section {
	padding-block: var(--zu-space-5);
}

.zu-section--tight {
	padding-block: var(--zu-space-4);
}

.zu-stack > * + * {
	margin-block-start: var(--zu-space-2);
}

/*
 * Rytm pionowy treści. Reset zeruje marginesy nagłówków i akapitów, więc bez
 * tego sąsiadujące nagłówki nachodzą na siebie — didone z ciasną interlinią
 * i polskimi ogonkami zderza się dosłownie.
 *
 * Odstęp wynika z sąsiedztwa, a nie z samego elementu: margines dostaje
 * element NASTĘPUJĄCY po innym, więc pierwszy element bloku nigdy nie wnosi
 * niechcianego odstępu u góry.
 */
.zu-prose > * + * {
	margin-block-start: var(--zu-space-2);
}

.zu-prose > * + :is(h2, h3, h4) {
	margin-block-start: var(--zu-space-4);
}

.zu-prose :is(ul, ol) {
	padding-inline-start: var(--zu-space-3);
}

.zu-prose li + li {
	margin-block-start: var(--zu-space-1);
}

/*
 * Tables in page content, and they arrived with the legal pages.
 *
 * Two of them exist: the delivery cost table the plugin renders from WooCommerce's
 * shipping zones, and the purpose-and-basis table in the privacy notice. Until they
 * were published nothing in page content had ever been a table, so nothing styled one
 * — the browser default would have put an unstyled grid in the middle of the terms.
 *
 * Hairlines between rows rather than a box around every cell, which is the same
 * decision the footer groups and the brand milestones made: a document, not a
 * spreadsheet.
 *
 * `display: block` with `overflow-x: auto` on the wrapper is what stops a wide table
 * pushing the whole page sideways on a phone. This repository has shipped a horizontal
 * overflow before and the suites now assert against it at five widths, so the table
 * scrolls inside its own box instead.
 */
.zu-prose :is(table, .wp-block-table) {
	display: block;
	overflow-x: auto;
	max-width: 100%;
}

.zu-prose table {
	border-collapse: collapse;
	width: 100%;
	font-size: var(--zu-text-sm);
}

/*
 * `border: 0` first, and it is load-bearing rather than tidy.
 *
 * `wp-block-library` puts `border: 1px solid` on every `.wp-block-table` cell and a
 * 3 px rule under `thead`. Setting only `border-block-end` overrides one edge of four,
 * so the privacy notice rendered as a **full black grid with a heavy black header
 * rule** — measured `rgb(34, 27, 26)` on top, left and right — under a comment in this
 * file claiming "hairlines between rows rather than a box around every cell". The
 * comment described the intention; the browser painted core's default.
 */
.zu-prose :is(th, td) {
	padding: var(--zu-space-2) var(--zu-space-2) var(--zu-space-2) 0;
	text-align: start;
	vertical-align: top;
	border: 0;
	border-block-end: 1px solid var(--zu-line);

	/*
	 * Long unbroken strings — "art. 6 ust. 1 lit. f) RODO" and the like — set the
	 * column width and pushed 64 px of the privacy table out of its own box at 320 px.
	 * Letting them break keeps the table inside the phone.
	 */
	overflow-wrap: break-word;
}

.zu-prose thead {
	border-block-end: 1px solid var(--zu-line);
}

/*
 * A caption, because a screen reader listing the tables on a page otherwise announces
 * an unnamed one. Set as a label rather than as a title: it names the table for someone
 * who cannot see that the heading above it already does.
 */
.zu-prose caption {
	margin-block-end: var(--zu-space-2);
	font-size: var(--zu-text-label);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	text-align: start;
	color: var(--zu-ink-muted);
}

/*
 * The separator exists for one job: the terms page carries TWO regulations, and the
 * second one — the account terms — rendered as an `h2` identical to "Rozdział 11"
 * immediately above it. Twelve sibling headings, the last of which is a different legal
 * document, with nothing but a list restarting at 1 to say so.
 */
.zu-prose hr {
	margin-block: var(--zu-space-5) var(--zu-space-4);
	border: 0;
	border-block-start: 1px solid var(--zu-line);
}

.zu-prose thead th {
	font-weight: 500;
	font-size: var(--zu-text-label);
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--zu-ink-muted);
}

.zu-prose tbody th {
	font-weight: 400;
}

/* Narrow screens: the padding is width the content needs more than the rhythm does. */
@media (max-width: 26rem) {
	.zu-prose :is(th, td) {
		padding-inline-end: var(--zu-space-1);
		font-size: var(--zu-text-label);
	}
}

/*
 * A note under a table or a section: the same size as the small print it is, and
 * muted, because it qualifies the thing above it rather than competing with it.
 *
 * The delivery page's copy is the reason this exists — "the cart is what you pay" has
 * to be readable and has to not look like another row of the table.
 *
 * **The token was `--zu-muted` and that token does not exist.** The alias block at the
 * top of this file defines `--zu-ink-muted`; the declaration was invalid at
 * computed-value time, so both this rule and the table head fell back to inherited ink
 * and rendered in the **darkest** colour on the page — darker than the lead paragraph
 * they were supposed to sit quietly beneath. A frontend review measured it resolving to
 * an empty string. Exactly the failure this repository keeps catching: a comment
 * describing what a declaration was expected to do, never checked against what it did.
 */
.zu-prose .zu-note {
	font-size: var(--zu-text-sm);
	color: var(--zu-ink-muted);
}

/*
 * Preformatted blocks, and this one was caught by measuring rather than by reading.
 *
 * The returns page carries the statutory withdrawal form as a `<pre>`. Preformatted
 * text does not wrap, so its longest line set the width of the document: **321 px of
 * horizontal overflow at 320 px wide, 251 px at 390 px**. The whole page slid sideways,
 * on a phone, on the page a customer opens when something has gone wrong.
 *
 * Two changes. The form itself lost the rows of dots that paper forms use to leave
 * room for handwriting — they are pure width on a screen and nobody writes into them.
 * And this rule keeps any future `<pre>` inside its own box: scrolls itself, never the
 * page. Tabular figures because the form is meant to be copied into an e-mail and
 * should keep its shape when it gets there.
 */
.zu-prose pre {
	overflow-x: auto;
	max-width: 100%;
	padding: var(--zu-space-3);
	background: var(--zu-surface-2);
	font-size: var(--zu-text-sm);
	line-height: 1.6;
	tab-size: 2;
}

/*
 * A signature at the end of a page written in the first person.
 *
 * Set in the display face because that is what a signature is — a name, not a
 * sentence — and given more space above it than the prose rhythm provides, so it
 * reads as the end of the letter rather than as one more paragraph.
 *
 * The class is applied in the content, not by the template. That was the point while
 * `/o-firmie/` existed: it was the owner's page and she could remove the signature by
 * deleting a block, where a template forcing one onto the last paragraph would be a
 * template deciding how she signs off.
 *
 * She withdrew that page on 2026-09-21, so nothing on the site carries this class
 * today — the only markup that uses it is `tools/content/o-firmie.html`, which is kept
 * but no longer seeded. The rule stays for the same reason the seed file does: putting
 * the page back is one command, and deleting the style would make that command produce
 * a page that renders wrong.
 */
.zu-prose .zu-signature {
	margin-block-start: var(--zu-space-4);
	font-family: var(--zu-font-display);
	font-size: var(--zu-text-lead);
	line-height: 1.2;
	color: var(--zu-ink);
}

/* --- Nagłówek strony i wpisy --- */

.zu-page-header {
	margin-block-end: var(--zu-space-4);
}

.zu-page-header__description {
	margin-block-start: var(--zu-space-2);
	color: var(--zu-ink-soft);
	max-width: var(--zu-container-text);
}

.zu-entry + .zu-entry {
	margin-block-start: var(--zu-space-4);
	padding-block-start: var(--zu-space-4);
	border-block-start: 1px solid var(--zu-line);
}

.zu-entry__title a {
	color: inherit;
	text-decoration: none;
}

.zu-entry__title a:hover {
	color: var(--zu-accent-deep);
}

.zu-entry__content,
.zu-entry__excerpt {
	margin-block-start: var(--zu-space-2);
}

/* --- Stan pusty --- */

.zu-empty {
	max-width: var(--zu-container-text);
	padding-block: var(--zu-space-4);
}

.zu-empty__text {
	margin-block: var(--zu-space-2) var(--zu-space-3);
	color: var(--zu-ink-soft);
}

.zu-empty .zu-search {
	max-width: 26rem;
}

.zu-empty__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--zu-space-2);
	margin-block-start: var(--zu-space-3);
}

/* --- Paginacja --- */

.zu-pagination {
	margin-block-start: var(--zu-space-5);
}

.zu-pagination .nav-links {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--zu-space-1);
}

.zu-pagination .page-numbers {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 2.5rem;
	min-height: 2.5rem;
	padding-inline: 0.6rem;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-pill);
	color: var(--zu-ink);
	text-decoration: none;
	font-size: var(--zu-text-sm);
}

.zu-pagination .page-numbers:hover {
	border-color: var(--zu-accent-deep);
	color: var(--zu-accent-deep);
}

.zu-pagination .page-numbers.current {
	background-color: var(--zu-accent-deep);
	border-color: var(--zu-accent-deep);
	color: var(--zu-surface);
}

.zu-pagination .page-numbers.dots {
	border-color: transparent;
}

/* ==========================================================================
   5. Powłoka strony
   ========================================================================== */

.site-main {
	flex: 1;
	padding-block: var(--zu-space-4);
}

.site-header {
	background-color: var(--zu-bg);
	border-block-end: 1px solid var(--zu-line);
}

/*
 * Nagłówek jest siatką, nie rzędem flexa. Na wąskich ekranach znak słowny
 * i akcje stoją w jednym rzędzie, a wyszukiwarka schodzi pod nie na całą
 * szerokość — w sklepie szukanie jest zbyt ważne, żeby chować je za przyciskiem.
 */
.site-header__inner {
	display: grid;
	grid-template-areas:
		'brand  actions'
		'toggle search'
		'nav    nav';
	grid-template-columns: auto 1fr;
	align-items: center;
	gap: var(--zu-space-2) var(--zu-space-2);
	padding-block: var(--zu-space-3);
}

.site-branding {
	grid-area: brand;
	font-family: var(--zu-font-display);
	font-size: var(--zu-text-lead);
	line-height: 1;
	margin: 0;
}

.site-branding__link {
	display: inline-block;
	color: var(--zu-ink);
	text-decoration: none;
}

/*
 * Znak graficzny w nagłówku.
 *
 * Width rather than height, because the mark is wider than it is tall (1.52:1) and
 * sizing by height makes the header jump when the file is swapped for one with a
 * different ratio. The intrinsic `width`/`height` attributes in the markup reserve
 * the box; this only decides how big it ends up.
 *
 * 108 px on a phone and 132 px above the navigation breakpoint: the script wordmark
 * under the lash stops being legible much below that, and the header has no room
 * for much more.
 */
.site-branding__logo {
	display: block;
	width: 108px;
	height: auto;
}

@media (min-width: 48rem) {
	.site-branding__logo {
		width: 132px;
	}
}

.site-header__actions {
	grid-area: actions;
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: var(--zu-space-2);
}

.zu-icon {
	flex: none;
}

/* --- Wyszukiwarka --- */

/*
 * ONE component, ONE border, ONE ring — and the version before this had two of each.
 *
 * The owner's words were that the focus state "looks like an extra, ill-fitting outline
 * around the component", and it was exactly that. The wrapper is a pill; on focus its
 * border turned burgundy **and** a 2 px burgundy outline appeared 2 px outside it. Two
 * concentric burgundy rings around a pill that already contains a burgundy circle, with
 * a 2 px gap of page background between them — which is what `outline-offset` is for and
 * what makes it wrong here. An offset ring reads as "something around the thing"; this
 * control needs "the thing, lit".
 *
 * So the ring is a `box-shadow` instead. It follows `border-radius` with no gap, so the
 * pill simply gains a 2 px burgundy edge and stops being two shapes.
 *
 * The project's focus contract (`--zu-focus-*`, `ARCHITECTURE.md` §13) asks for 2 px at
 * 3:1 or better against what it sits on. This keeps both numbers and changes only where
 * the 2 px is drawn: `--zu-accent-deep` is 7.30:1 on `--zu-bg`. `forced-colors` falls
 * back to a real outline, because a `box-shadow` is not painted in that mode at all.
 */
.zu-search {
	grid-area: search;
	display: flex;
	align-items: center;
	gap: 0.25rem;
	background-color: var(--zu-surface);
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-pill);
	padding-inline: 0.25rem;
	transition: border-color var(--zu-transition), box-shadow var(--zu-transition);
}

.zu-search:hover {
	border-color: var(--zu-gold-decor);
}

/*
 * `.zu-search .zu-search__input`, and the extra selector is the whole fix.
 *
 * The rule below used to be `.zu-search__input` alone — one class, specificity (0,1,0)
 * — and the shop's shared field styling is `input[type='search']` at (0,1,1), later in
 * this file. So every reset here lost, silently, and the browser painted the generic
 * field *inside* the pill. Measured on the live header:
 *
 *     declared  border: 0        radius: —      padding: 9.6px 8px 9.6px 13.6px
 *     computed  border: 1px      radius: 8px    padding: 11.2px 16px
 *
 * An 8 px rounded rectangle, permanently, inset a few pixels inside a 999 px pill —
 * and on hover the generic `input:hover` turned that inner rectangle gold while the
 * wrapper turned gold too, giving two concentric outlines in the one control the owner
 * had already complained about. The previous round's claim that this is "one component,
 * one border" was true in the source and false in the render, which is the worse of the
 * two ways to be wrong.
 *
 * Found by an independent frontend review, which photographed it rather than reading
 * the stylesheet — the only way this class of defect is ever found.
 */
.zu-search .zu-search__input {
	flex: 1;
	/*
	 * `width: 100%` is load-bearing, not inherited clutter.
	 *
	 * The generic field rule sets it, and raising this selector's specificity meant
	 * deciding what to keep. Dropping it to `auto` let the input fall back to an
	 * input's intrinsic ~20-character width — measured 196 px — which pushed the whole
	 * search form to 246 px inside a 288 px container and put **47 px of horizontal
	 * overflow on every page at 320**. Caught by the suite, on the check that exists
	 * for exactly this.
	 */
	width: 100%;
	min-width: 0;
	border: 0;
	border-radius: 0;
	background: transparent;
	padding: 0.6rem 0.5rem 0.6rem 0.85rem;
	font-size: var(--zu-text-sm);
	/* Safari draws its own rounded inner shadow on type=search otherwise. */
	appearance: none;
}

/*
 * And the generic hover has to be excluded by name, or it re-draws the inner edge the
 * moment the pointer arrives — the doubling returns on hover only, which is harder to
 * notice and no less wrong.
 */
.zu-search .zu-search__input:hover {
	border-color: transparent;
}

/* The ring belongs to the whole field, not to the bare input inside it. */
.zu-search .zu-search__input:focus-visible {
	outline: none;
}

/*
 * Chromium paints its own search-clear control inside the field once there is text in
 * it — a blue × on a cream pill, in the shop's own header. `appearance: none` on the
 * input does not suppress it; the pseudo-element has to be addressed directly.
 */
.zu-search .zu-search__input::-webkit-search-cancel-button {
	appearance: none;
	display: none;
}

.zu-search:focus-within {
	border-color: var(--zu-focus-color);
	box-shadow: 0 0 0 var(--zu-focus-width) var(--zu-focus-color);
}

@media (forced-colors: active) {
	.zu-search:focus-within {
		outline: var(--zu-focus-width) solid Highlight;
	}
}

/*
 * The button is an end-cap, not a sticker.
 *
 * It was a 2 rem circle floating inside a taller pill with 0.35 rem of air on one side
 * and a flex gap on the other, which is how it came to look "accidentally pasted on".
 * Now it is inset by the same 0.25 rem the field carries on every side, so the two
 * radii are concentric and the circle sits in the pill rather than on it.
 */
.zu-search__submit {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 2.25rem;
	height: 2.25rem;
	border: 0;
	border-radius: var(--zu-radius-pill);
	background-color: var(--zu-accent-deep);
	color: var(--zu-surface);
	cursor: pointer;
	transition: background-color var(--zu-transition);
}

.zu-search__submit:hover {
	background-color: var(--zu-accent);
}

/*
 * And the button's own ring goes inward.
 *
 * Tabbing to it inside an already-focused field drew a second ring outside the first —
 * the same doubling, one level down. `inset` keeps the indicator on the button, where
 * the focus actually is, and `--zu-surface` reads at better than 7:1 on the burgundy it
 * sits on.
 */
.zu-search__submit:focus-visible {
	outline: none;
	box-shadow: inset 0 0 0 var(--zu-focus-width) var(--zu-surface);
}

@media (forced-colors: active) {
	.zu-search__submit:focus-visible {
		outline: var(--zu-focus-width) solid Highlight;
		outline-offset: calc(-1 * var(--zu-focus-width));
	}
}

/* --- Koszyk i konto --- */

.zu-cart-link,
.zu-account-link {
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	color: var(--zu-ink);
	text-decoration: none;
	font-size: var(--zu-text-sm);
	padding: 0.4rem;
	border-radius: var(--zu-radius-sm);
}

.zu-cart-link:hover,
.zu-account-link:hover {
	color: var(--zu-accent-deep);
}

/*
 * Clipped, not hidden — and the difference is the whole accessible name.
 *
 * `display: none` removes the word from the accessibility tree, and it is the only
 * text in this link: the icon is `aria-hidden` and the badge does not exist when the
 * cart is empty. Measured on the Chromium accessibility tree by an independent
 * review in stage 4: at 390 px with an empty cart the link announced as
 * `{"role":"link","name":""}`. The account link beside it was already doing this
 * correctly, which is why only one of the two was broken.
 */
.zu-cart-link__label {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* The badge is the element WooCommerce's cart fragment replaces — see inc/cart-state.php. */
.zu-cart-link__badge {
	display: inline-flex;
	align-items: center;
}

.zu-cart-link__count {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 1.35rem;
	height: 1.35rem;
	padding-inline: 0.35rem;
	border-radius: var(--zu-radius-pill);
	background-color: var(--zu-accent-deep);
	color: var(--zu-surface);
	font-size: var(--zu-text-label);
	font-weight: 500;
	line-height: 1;
}

/* --- Nawigacja --- */

.zu-nav-toggle {
	grid-area: toggle;
	justify-self: start;
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	padding: 0.45rem 0.75rem;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-pill);
	background-color: transparent;
	color: var(--zu-ink);
	font-size: var(--zu-text-sm);
	cursor: pointer;
}

.zu-nav-toggle:hover {
	border-color: var(--zu-gold-decor);
}

.site-nav {
	grid-area: nav;
	display: none;
}

.site-nav.is-open {
	display: block;
}

.site-nav__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-1);
	padding-block: var(--zu-space-2);
	border-block-start: 1px solid var(--zu-line);
}

.site-nav__list a {
	display: block;
	padding: 0.5rem 0;
	color: var(--zu-ink);
	text-decoration: none;
	font-size: var(--zu-text-sm);
}

.site-nav__list a:hover,
.site-nav__list .current-menu-item > a {
	color: var(--zu-accent-deep);
}

/* --- Stopka --- */

.site-footer {
	background-color: var(--zu-deep);
	color: var(--zu-surface);
	margin-block-start: var(--zu-space-5);
	padding-block: var(--zu-space-5) var(--zu-space-3);
}

.site-footer__inner {
	display: grid;
	gap: var(--zu-space-4);
}

.site-footer__wordmark {
	font-family: var(--zu-font-display);
	font-size: var(--zu-text-lead);
	margin: 0;
}

.site-footer__tagline {
	color: var(--zu-deep-muted);
	margin-block-start: var(--zu-space-1);
}

/*
 * The link groups.
 *
 * Two columns on a phone rather than one, and four once there is room. A single
 * column of fourteen links is a scroll with no shape to it; two columns of short
 * lists read as sections, which is what they are.
 *
 * `align-content: start` because the groups have different lengths — without it a
 * three-item list stretches its gaps to match a six-item one and the headings stop
 * lining up.
 */
/*
 * Columns, not a grid — and the grid left a hole in the footer at every narrow width.
 *
 * Three groups in two grid columns means the third starts a new *row*, below the
 * tallest group in the first. "Zakupy" carries seven links, so column one ended after
 * "Kontakt i dane firmy" and stayed empty until the next row began: measured, a
 * **212 px void** at every width from 320 to 820. `align-content: start` aligns the
 * grid's rows as a block; it does not pack items into columns, which is what was
 * actually wanted.
 *
 * CSS multi-column flows the groups into the columns instead, so the second column
 * starts where the first runs out and the hole closes. It keeps DOM order, so the tab
 * order is unchanged, and it needs no JavaScript — which matters here more than
 * usual, because this is the block that will hold the link to the terms of sale.
 *
 * `break-inside: avoid` keeps a group's heading with its links.
 */
.site-footer__nav {
	columns: var(--zu-footer-columns-narrow, 2);
	column-gap: var(--zu-space-3);
}

.site-footer__group {
	break-inside: avoid;
	/* A column break cannot fall inside a group, so the gap has to come from the item. */
	margin-block-end: var(--zu-space-4);
}

.site-footer__heading {
	margin: 0 0 var(--zu-space-2);
	font-family: var(--zu-font-sans);
	font-size: var(--zu-text-label);
	font-weight: 500;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	/*
	 * `--zu-surface-2`, and the token this replaced is a lesson about measuring.
	 *
	 * It was `--zu-gold-text`, with a comment claiming 5.9:1 against the deep ground.
	 * That figure is real and it belongs to a different background: `#7d5c3d` measures
	 * **5.52:1 on the cream page** — which is what `DESIGN_REFERENCE.md` records and
	 * what the token exists for — and **2.78:1 on `--zu-deep`**, well under AA, at
	 * 12 px. A number was carried across a background change without being re-taken,
	 * and the comment asserting it made the mistake look checked.
	 *
	 * `--zu-surface-2` measures **12.85:1** here and is already the on-deep eyebrow
	 * colour in `shop.css`, so the footer now agrees with the brand band rather than
	 * inventing a third treatment. Found by an independent frontend review.
	 */
	color: var(--zu-surface-2);
}

.site-footer__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.55rem;
}

.site-footer__list a {
	color: var(--zu-surface);
	text-decoration: none;
	font-size: var(--zu-text-sm);
	/*
	 * An underline that is there and quiet, rather than one that appears on hover.
	 * A link list on a dark ground with no underline at all reads as a column of
	 * labels; this keeps it recognisably clickable and still calm.
	 */
	text-decoration: underline;
	text-decoration-color: transparent;
	text-underline-offset: 0.25em;
	transition: color var(--zu-transition), text-decoration-color var(--zu-transition);
}

.site-footer__list a:hover {
	color: var(--zu-surface-2);
	text-decoration-color: currentColor;
}

/* --- Kto sprzedaje --- */

.site-footer__company {
	display: flex;
	flex-wrap: wrap;
	gap: var(--zu-space-2) var(--zu-space-4);
	margin-block-start: var(--zu-space-4);
	padding-block-start: var(--zu-space-3);
	border-block-start: 1px solid var(--zu-deep-line);
	font-size: var(--zu-text-label);
	color: var(--zu-deep-muted);
}

.site-footer__address {
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem var(--zu-space-2);
	font-style: normal;
	margin: 0;
}

.site-footer__company-name {
	color: var(--zu-surface-2);
}

.site-footer__contact {
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem var(--zu-space-2);
	margin: 0;
}

/*
 * The two things a customer clicks to reach a human, so they are the one part of this
 * strip that is not muted.
 *
 * 44 px of vertical target on touch, from padding rather than from a font size: these
 * sit at label size by design and a phone still has to be able to hit them.
 */
.site-footer__contact a {
	display: inline-flex;
	align-items: center;
	min-height: 2.75rem;
	color: var(--zu-surface);
	text-decoration: underline;
	text-underline-offset: 0.25em;
	text-decoration-color: var(--zu-deep-line);
	transition: text-decoration-color var(--zu-transition);
}

.site-footer__contact a:hover {
	text-decoration-color: currentColor;
}

@media (min-width: 48rem) {
	/*
	 * The wordmark keeps its own row rather than its own column.
	 *
	 * It used to reserve `minmax(0, 14rem)` beside the links, and the column held one
	 * 30 px wordmark and nothing else — `blogdescription` is empty, so the tagline
	 * under it never renders. Measured at 768: a **224 x 460 px empty column** next to
	 * link groups squeezed narrow enough that "Poznaj O! Rzęsy" and "Lakiery hybrydowe"
	 * both wrapped onto two lines. The footer was taller and emptier at once, which is
	 * the opposite of the note that prompted rebuilding it.
	 *
	 * Full width for the links instead. Nothing is invented to fill a column that had
	 * no content to give it.
	 */

	/*
	 * Up to four columns where there is room, and the number comes from the template.
	 *
	 * This rule said `columns: 4` and claimed that flowing rather than placing meant
	 * "three groups fill three columns and four fill four, with no empty cell either
	 * way". That was a prediction and it was wrong: a balanced multi-column with a
	 * fixed count sizes every track at 1/4 of the width regardless of what it has to
	 * put in them, so three groups fill three and leave the fourth empty.
	 *
	 * And there have only ever been three. `zakatek_footer_group()` drops a group with
	 * no live link in it, and "Informacje" has none — all five of its slugs are
	 * unpublished, and the one draft among them is refused by `zakatek_page_link()`.
	 * So the footer rendered Zakupy, Moje konto and O marce, and **a quarter of it has
	 * been empty at every width from 48rem up since `ea0473a` introduced this rule** —
	 * measured at the boundary: `column-count` is 2 at 767px and 3 at 768px. The
	 * prediction was never true; it was simply never checked.
	 *
	 * `footer.php` counts the groups it is about to render and sets the property. Four
	 * remains the ceiling — five groups would make five columns too narrow to read.
	 */
	.site-footer__nav {
		columns: var(--zu-footer-columns, 4);
	}

	.site-footer__company {
		justify-content: space-between;
	}
}

.site-footer__baseline {
	margin-block-start: var(--zu-space-4);
	padding-block-start: var(--zu-space-2);
	border-block-start: 1px solid var(--zu-deep-line);
	font-size: var(--zu-text-label);
	color: var(--zu-deep-muted);
}

/* --- Układ szeroki --- */

/*
 * Wartość progu odpowiada --zu-bp-nav. @media nie przyjmuje custom properties,
 * więc liczba musi tu wystąpić dosłownie; skrypt nawigacji czyta ją z tokenu,
 * żeby nie powstała trzecia kopia.
 */
@media (min-width: 62rem) {
	/*
	 * Two rows on desktop, and the arithmetic is why.
	 *
	 * The categories moved out of a hover panel and into the bar, which is five items
	 * instead of two: 562 px of them. On one row with the mark, the search and the
	 * cart, that is about 1 070 px of content inside a 944 px container at 1024 —
	 * and CSS grid does not overflow when you ask it to, it *shrinks the auto column*.
	 * Measured before this: the mark was rendered at **0 px wide** from 992 to 1024,
	 * and at 116 px instead of 132 everywhere above it. The shop's own logo had been
	 * silently squeezed out of its header and nothing reported an overflow, because
	 * there was none.
	 *
	 * So the category bar takes a row of its own, under a hairline. It is the ordinary
	 * shape of a shop header for a reason: the brand row stays calm, the catalogue gets
	 * the full width, and both survive 1024 without anything being negotiated away.
	 */
	.site-header__inner {
		grid-template-areas:
			'brand search actions'
			'nav   nav    nav';
		grid-template-columns: auto minmax(12rem, 26rem) auto;
		gap: var(--zu-space-2) var(--zu-space-3);
		padding-block: var(--zu-space-2);
	}

	.site-nav {
		padding-block-start: var(--zu-space-2);
		border-block-start: 1px solid var(--zu-line);
	}

	.zu-nav-toggle {
		display: none;
	}

	.site-nav {
		display: block;
	}

	.site-nav__list {
		flex-direction: row;
		align-items: center;
		gap: var(--zu-space-3);
		padding-block: 0;
		border-block-start: 0;
	}

	.site-nav__list > li {
		position: relative;
	}

	.site-nav__list a {
		padding: 0;
	}

	/*
	 * Top-level items: a little more weight and tracking than body text, and an
	 * underline that draws rather than appears.
	 *
	 * They were plain ink at body size with only a colour change on hover, which is
	 * why they read as afterthoughts rather than as the shop's navigation. This is the
	 * smallest change that gives them presence without shouting: the same Lexend, one
	 * step up in weight, 0.04em of tracking, and a rule that grows from the left.
	 *
	 * `white-space: nowrap`, because these are category names and two of them are three
	 * words long. A wrapped navigation item reads as a mistake.
	 */
	.site-nav__list > li > a {
		position: relative;
		padding-block: 0.4rem;
		font-weight: 500;
		letter-spacing: 0.04em;
		white-space: nowrap;
	}

	.site-nav__list > li > a::after {
		content: '';
		position: absolute;
		inset-inline: 0;
		bottom: 0;
		height: 1px;
		background-color: currentColor;
		transform: scaleX(0);
		transform-origin: left center;
		transition: transform var(--zu-transition);
	}

	.site-nav__list > li > a:hover::after,
	.site-nav__list > li:focus-within > a::after,
	.site-nav__list > li.current-menu-item > a::after,
	.site-nav__list > li.current-menu-parent > a::after,
	.site-nav__list > li.current-menu-ancestor > a::after {
		transform: scaleX(1);
	}

	/*
	 * The category the customer is currently in stays marked, and it is marked by
	 * colour as well as by the rule — the rule alone is a 1px line that a glance at a
	 * busy header will miss.
	 *
	 * `current-menu-parent` and `-ancestor` matter on a product page: WooCommerce sets
	 * them from the product's own terms, so "Rzęsy" stays lit while the customer is
	 * reading a lash product rather than only on the archive itself.
	 */
	.site-nav__list > li.current-menu-item > a,
	.site-nav__list > li.current-menu-parent > a,
	.site-nav__list > li.current-menu-ancestor > a {
		color: var(--zu-accent-deep);
	}

	/*
	 * ---- What used to be here, and why it is gone ----
	 *
	 * The categories hung under "Sklep" in a panel that opened on hover. It sat below
	 * the link with a gap, and `display: none` removed it the instant the cursor left
	 * the `<li>` on the way down — which the owner reported, exactly, as "the dropdown
	 * disappears when I try to enter it". The round before this one fixed it properly:
	 * a transparent `::before` bridging the gap, `visibility` instead of `display` so
	 * the leave could carry a 180 ms grace, `:focus-within` for the keyboard, a chevron
	 * hidden under `(hover: none)` because a tablet at 1024 gets the desktop bar with
	 * no hover at all, and `transition: none` on focus so a tabbed-to link did not
	 * arrive at 0.017 opacity.
	 *
	 * Six rules to make one hover panel behave. The categories now sit in the bar
	 * beside "Sklep", so the catalogue is one click from anywhere instead of a hover, a
	 * traverse and a click — and every one of those rules is deleted rather than
	 * carried. `wp_nav_menu` renders at `depth => 1`, so the markup they styled cannot
	 * appear either.
	 *
	 * This also closes the accessibility gap the round before recorded and carried:
	 * WCAG 2.1 SC 1.4.13 asks that content appearing on hover be dismissible without
	 * moving the pointer or focus, and the panel took no Escape. There is no panel.
	 */

	/* Above the wide breakpoint there is room for the word, so it comes back on screen. */
	.zu-cart-link__label {
		position: static;
		width: auto;
		height: auto;
		margin: 0;
		overflow: visible;
		clip-path: none;
	}

	/*
	 * Left over from the footer that was one wordmark and a row of links, where the
	 * brand took the width and the nav sat narrow on the right. The proportions are
	 * the other way round now, and this rule was quietly winning at every desktop
	 * width: measured at 1024 and 1440, the tracks came out **727 px for the brand and
	 * 152 px for four columns of links**, so the whole link area collapsed to a single
	 * column while the 48rem rule above it looked correct in the source.
	 *
	 * A reminder that a rule is not dead because the thing it was written for is: it
	 * is dead when it stops matching, and this one never did.
	 */
	.site-footer__inner {
		align-items: start;
	}
}

/* ==========================================================================
   6. Przyciski
   ========================================================================== */

/*
 * Kształt pigułki odczytany z prototypu (border-radius: 2147483647px, czyli
 * Tailwindowe rounded-full). Selektor obejmuje też klasy WooCommerce, żeby
 * przyciski sklepu wyglądały jak nasze bez nadpisywania szablonów wtyczki.
 */
/*
 * Selektory WooCommerce celowo BEZ przodka .woocommerce. Klasa .woocommerce
 * trafia na body tylko na stronach wtyczki, a karty produktów pojawiają się
 * także poza nimi — na stronie głównej przyciski renderowały się jako zwykłe
 * podkreślone odnośniki. Klasa .button należy w tym motywie wyłącznie do
 * WooCommerce, więc szerszy selektor nie łapie niczego obcego.
 */
.zu-button,
.wp-element-button,
a.button,
button.button,
input.button,
#respond input#submit {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--zu-space-1);
	padding: 0.75rem 1.5rem;
	border: 1px solid transparent;
	border-radius: var(--zu-radius-pill);
	background-color: var(--zu-accent-deep);
	color: var(--zu-surface);
	font-family: var(--zu-font-sans);
	font-size: var(--zu-text-sm);
	font-weight: 500;
	line-height: 1.2;
	text-align: center;
	text-decoration: none;
	cursor: pointer;
	transition: background-color var(--zu-transition), border-color var(--zu-transition), color var(--zu-transition);
}

.zu-button:hover,
.wp-element-button:hover,
a.button:hover,
button.button:hover,
input.button:hover,
#respond input#submit:hover {
	background-color: var(--zu-accent);
	color: var(--zu-surface);
}

/* Wariant drugorzędny — obrys zamiast wypełnienia. */
.zu-button--secondary {
	background-color: transparent;
	border-color: var(--zu-accent-deep);
	color: var(--zu-accent-deep);
}

.zu-button--secondary:hover {
	background-color: var(--zu-accent-deep);
	color: var(--zu-surface);
}

/* Wariant tekstowy — dla akcji pobocznych, np. „usuń z koszyka". */
.zu-button--quiet {
	background-color: transparent;
	color: var(--zu-ink-soft);
	padding-inline: var(--zu-space-1);
}

.zu-button--quiet:hover {
	background-color: transparent;
	color: var(--zu-accent-deep);
}

/*
 * Wariant obrysowy na jasnym tle — dla drugiej akcji obok przycisku głównego.
 *
 * Distinct from `--secondary`, which is an accent-coloured outline and reads as
 * another call to action. This one is ink on a hairline: present, clearly
 * clickable, and visibly the lesser of the two when it stands beside a filled
 * button in the same row.
 */
.zu-button--ghost {
	background-color: transparent;
	border-color: var(--zu-line);
	color: var(--zu-ink);
}

.zu-button--ghost:hover {
	background-color: var(--zu-surface);
	border-color: var(--zu-accent-deep);
	color: var(--zu-accent-deep);
}

/*
 * Wariant na ciemnym tle.
 *
 * The burgundy fill is **2.09:1** against `--zu-deep` and disappears into it.
 * Inverting to a light fill with dark text puts the button at **12.85:1** against
 * that background, with **6.16:1** for the label on the fill, and keeps the brand
 * colour where it is legible — in the text.
 *
 * Measured rather than eyeballed, along with the rest of the deep band: white title
 * 16.84:1, `--zu-deep-muted` body 7.95:1, `--zu-surface-2` eyebrow and award year
 * 12.85:1. The lowest ratio anywhere in the new sections is 5.17:1, on the small
 * muted labels over blush.
 */
.zu-button--on-deep {
	background-color: var(--zu-surface-2);
	border-color: var(--zu-surface-2);
	color: var(--zu-accent-deep);
}

.zu-button--on-deep:hover {
	background-color: var(--zu-surface);
	border-color: var(--zu-surface);
	color: var(--zu-accent-deep);
}

.zu-button[disabled],
.zu-button[aria-disabled='true'],
.wp-element-button[disabled] {
	opacity: 0.55;
	cursor: not-allowed;
}

/*
 * The two buttons that move money took a WordPress-admin **blue** keyboard focus ring.
 *
 * "Przejdź do płatności" and "Kupuję i płacę" are WooCommerce block buttons, and the
 * plugin draws their focus with a transparent outline plus
 * `box-shadow: #fff 0 0 0 1px inset, rgb(6,117,196) 0 0 0 2px` — `#0675C4`, the admin
 * accent. Every other control in this shop uses `--zu-focus-color` (`#7f3b40`), so the
 * single highest-stakes moment in the checkout was the one place that looked like
 * wp-admin. Visible, so never a WCAG failure; an unstyled-WooCommerce leak, which is what
 * stage 9 was asked to find. Measured by an independent review across 34 real Tab stops.
 *
 * The shadow is replaced rather than removed, because the white inset ring is what keeps
 * the outline legible against the burgundy fill.
 */
.wc-block-components-button.wp-element-button:focus-visible {
	outline: var(--zu-focus-width) solid var(--zu-focus-color);
	outline-offset: var(--zu-focus-offset);
	box-shadow: 0 0 0 1px var(--zu-surface) inset;
}

/* ==========================================================================
   7. Formularze
   ========================================================================== */

.zu-field {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-1);
}

.zu-label,
.woocommerce form .form-row label {
	font-size: var(--zu-text-sm);
	font-weight: 500;
	color: var(--zu-ink-soft);
}

input[type='text'],
input[type='email'],
input[type='url'],
input[type='password'],
input[type='search'],
input[type='number'],
input[type='tel'],
input[type='date'],
textarea,
select,
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce form .form-row select {
	width: 100%;
	padding: 0.7rem 1rem;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-sm);
	background-color: var(--zu-surface);
	color: var(--zu-ink);
	font-size: var(--zu-text-base);
	transition: border-color var(--zu-transition);
}

input::placeholder,
textarea::placeholder {
	color: var(--zu-ink-muted);
	opacity: 1;
}

input:hover,
textarea:hover,
select:hover {
	border-color: var(--zu-gold-decor);
}

/*
 * Stan błędu. Kolor nie może być jedynym nośnikiem informacji — komunikat
 * tekstowy jest obowiązkowy, a pole dostaje aria-invalid.
 *
 * The second selector list is a stage 5 repair, and the defect it fixes is the
 * reason the rule above is keyed on the attribute at all.
 *
 * `input[aria-invalid='true']` is (0,1,1) and the field rule immediately above is
 * (0,2,2) on WooCommerce's classic forms, so for two stages this rule simply lost:
 * a refused field rendered the ordinary pale border, byte-identical to an untouched
 * one. Measured by an independent review on an address form submitted with five
 * fields cleared — every one of them announced correctly and none of them visibly
 * marked, so a sighted customer who scrolled past the summary could not tell which
 * of nine fields to fix.
 *
 * It did not show earlier because stage 4's checkout is a block, where
 * `commerce.css` matches the plugin's own selector shape (ARCHITECTURE.md §18.8).
 * Stage 5 is the first stage that renders a classic WooCommerce form a customer can
 * have refused.
 */
input[aria-invalid='true'],
textarea[aria-invalid='true'],
select[aria-invalid='true'],
.woocommerce form .form-row input.input-text[aria-invalid='true'],
.woocommerce form .form-row textarea[aria-invalid='true'],
.woocommerce form .form-row select[aria-invalid='true'] {
	border-color: var(--zu-accent-deep);
	border-width: 2px;
}

.zu-field__error {
	font-size: var(--zu-text-sm);
	color: var(--zu-accent-deep);
	font-weight: 500;
}

.zu-field__hint {
	font-size: var(--zu-text-sm);
	color: var(--zu-ink-muted);
}

/* ==========================================================================
   8. Komunikaty i okruszki
   ========================================================================== */

/*
 * Powiadomienia WooCommerce. Domyślne są niebieskie i wyglądają jak wklejone
 * z innej strony — to dokładnie ten efekt, którego §69 zabrania.
 *
 * Selektory obejmują dwie rodziny klas, bo WooCommerce ma dwie: klasyczną
 * (.woocommerce-info) oraz blokową (.wc-block-components-notice-banner),
 * używaną przez bloki Koszyka i Zamówienia.
 *
 * Tła wyprowadzamy z koloru wiodącego przez color-mix, zamiast wpisywać drugą
 * wartość — dzięki temu zmiana tokenu przestawia komunikat w całości.
 */
.woocommerce-info,
.woocommerce-message,
.woocommerce-error,
.wc-block-components-notice-banner {
	display: flex;
	align-items: flex-start;
	gap: var(--zu-space-2);
	margin-block: var(--zu-space-3);
	padding: var(--zu-space-2) var(--zu-space-3);
	border: 1px solid var(--zu-line);
	border-inline-start: 3px solid var(--zu-accent-deep);
	border-radius: var(--zu-radius-sm);
	background-color: var(--zu-blush);
	color: var(--zu-ink);
	font-size: var(--zu-text-sm);
	list-style: none;
}

/*
 * WooCommerce dokłada do komunikatu ikonę z własnego fontu przez ::before,
 * w swoim niebieskim. Nasz lewy pasek niesie ten sam sygnał, a treść niesie
 * znaczenie — ikona zostawała jako obca plamka koloru.
 *
 * `::after` is here for a second reason, found in stage 5. WooCommerce also declares
 * `.woocommerce-info::after { content: " "; display: table }` as a clearfix, and the
 * rule above turns the notice into a flex container — so that pseudo-element was an
 * empty flex item at the end of every notice in the shop, taking a `gap` with it.
 * Invisible while the notice is a row and a whole empty line once it is a column,
 * which is what the account pages make it below the tablet breakpoint.
 */
.woocommerce-info::before,
.woocommerce-message::before,
.woocommerce-error::before,
.woocommerce-info::after,
.woocommerce-message::after,
.woocommerce-error::after {
	content: none;
}

/*
 * The error notice is a LIST, and that changes what "flex" has to mean.
 *
 * WooCommerce renders a refusal as `<ul class="woocommerce-error">` with one `<li>`
 * per problem, so the flex row above turned every message into a column. Measured by
 * an independent review on an address form with five errors at 1280 px: five boxes
 * across one row at x = 371 / 527 / 706 / 864 / 1065, each wrapping to two lines,
 * with ragged baselines. A customer who missed five fields got a five-column band
 * instead of five problems.
 *
 * It was correct below the tablet breakpoint only because `account.css` happened to
 * stack account notices there — so the case that read worst was the one with the most
 * room. This is not account-only: `.woocommerce-error` is the classic error notice
 * everywhere in the shop.
 *
 * `.woocommerce-info` and `.woocommerce-message` stay rows: they are a sentence and
 * sometimes a button, which is what the row was designed for.
 */
.woocommerce-error {
	flex-direction: column;
	gap: var(--zu-space-1);
}

.woocommerce-error li {
	margin: 0;
}

.woocommerce-error .button {
	margin-inline-start: 0;
	align-self: flex-start;
}

.woocommerce-message,
.wc-block-components-notice-banner.is-success {
	border-inline-start-color: var(--zu-success);
	border-color: color-mix(in srgb, var(--zu-success) 25%, var(--zu-line));
	background-color: color-mix(in srgb, var(--zu-success) 8%, var(--zu-surface));
}

.woocommerce-error,
.wc-block-components-notice-banner.is-error {
	border-inline-start-color: var(--zu-accent-deep);
	border-color: color-mix(in srgb, var(--zu-accent-deep) 25%, var(--zu-line));
	background-color: color-mix(in srgb, var(--zu-accent-deep) 8%, var(--zu-surface));
}

/* Przycisk wewnątrz komunikatu nie powinien rozpychać go na pełną wysokość. */
.woocommerce-info .button,
.woocommerce-message .button,
.woocommerce-error .button {
	margin-inline-start: auto;
	flex: none;
}

/* --- Okruszki --- */

/*
 * The `.woocommerce` ancestor is needed on the CONTAINER too, not only on the
 * anchors below.
 *
 * Stage 2 fixed this for links and stopped there, and the fix's own comment
 * described the trap it then walked into: the plugin sets `#767676` through
 * `.woocommerce:where(body:not(.woocommerce-uses-block-theme)) .woocommerce-breadcrumb`,
 * and `:where()` contributes no specificity, so that rule is (0,2,0) and the simple
 * selector lost. An independent review measured the consequence on the product page:
 * the trailing crumb — the product name, the longest text in the trail — rendered at
 * 4.11:1 on the cream ground, below WCAG AA, while `PROJECT_STATE` recorded the
 * problem as fixed.
 */
.woocommerce .woocommerce-breadcrumb,
.woocommerce-breadcrumb {
	margin-block-end: var(--zu-space-3);
	color: var(--zu-ink-muted);
	font-size: var(--zu-text-sm);
}

/*
 * The selector needs the .woocommerce ancestor because the plugin sets #767676
 * through .woocommerce .woocommerce-breadcrumb a. The simpler selector lost, and
 * that grey on the cream background measures 4.11:1 — below WCAG AA. The theme
 * token measures 8.73:1.
 */
/*
 * `padding-block` and `inline-block` are here for the tap, not for the look.
 *
 * Measured at 390 px during the stage 9 accessibility pass: the trail's links were
 * 99 × 18 px. WCAG 2.2's target-size criterion exempts a link whose height is set by the
 * line-height of the text around it, and a breadcrumb separated by "/" is exactly that —
 * so this was never a failure. It was still an 18 px tap target on a phone, and four
 * pixels of padding either side fixes it for nothing.
 *
 * The container's line-height is raised with it. Without that, the taller links in a
 * trail that wraps to two lines would come within a pixel of each other, which trades a
 * small target for overlapping ones.
 */
.woocommerce .woocommerce-breadcrumb,
.woocommerce-breadcrumb {
	line-height: 1.9;
}

.woocommerce .woocommerce-breadcrumb a,
.woocommerce-breadcrumb a {
	display: inline-block;
	padding-block: 0.25rem;
	color: var(--zu-ink-soft);
	text-decoration: none;
}

.woocommerce .woocommerce-breadcrumb a:hover,
.woocommerce-breadcrumb a:hover {
	color: var(--zu-accent-deep);
	text-decoration: underline;
}

/* ==========================================================================
   9. Interakcje i dostępność
   ========================================================================== */

/*
 * Widoczny focus jest wymagany. Nie usuwamy outline bez podania zamiennika.
 * :focus-visible, żeby wskaźnik nie pojawiał się przy kliknięciu myszą.
 */
/*
 * No `border-radius` here, and removing it is the fix rather than an omission.
 *
 * This rule used to set `border-radius: var(--zu-radius-sm)` so that the ring around a
 * square element was not a hard rectangle. At specificity (0,1,0) and late in the file
 * it also won against every pill in the theme: measured, `.zu-button`,
 * `.zu-button--ghost`, `.zu-button--secondary`, `.zu-button--on-deep`,
 * `.zu-nav-toggle` and `.zu-search__submit` all went **999px -> 8px on focus**. The
 * primary call to action changed shape when a keyboard reached it, and the search
 * button turned from a circle into a rounded square.
 *
 * An outline follows the element's own `border-radius`, so nothing is needed here for
 * the ring to fit: a pill gets a pill-shaped ring and a square field gets a square one,
 * which is what each should have had all along. Present since stage 1 and caught by the
 * fourth frontend review.
 */
:focus-visible {
	outline: var(--zu-focus-width) solid var(--zu-focus-color);
	outline-offset: var(--zu-focus-offset);
}

/*
 * Na ciemnym tle burgund nie jest widoczny — tam obrys jest jasny.
 *
 * `.zu-brand` joined the footer here after a frontend review measured the ring on the
 * brand band's one button at **2.09:1** against `--zu-deep` — the same number this
 * stylesheet already quotes a few hundred lines above, where it explains why the
 * burgundy *fill* was rejected for that background. The fill was reasoned about and
 * the ring was not, on the same band, in the same round.
 *
 * `ARCHITECTURE.md` §13 makes ≥ 3:1 a binding owner decision, and `outline-offset`
 * puts the ring on the deep ground on both sides, so no adjacent colour rescues it.
 * `--zu-surface` there measures 16.84:1.
 */
.site-footer :focus-visible,
.zu-brand :focus-visible {
	outline-color: var(--zu-surface);
}

.skip-link {
	position: absolute;
	left: var(--zu-space-2);
	top: var(--zu-space-2);
	z-index: 100;
	padding: var(--zu-space-2) var(--zu-space-3);
	background-color: var(--zu-surface);
	color: var(--zu-accent-deep);
	border-radius: var(--zu-radius-sm);
	transform: translateY(calc(-100% - var(--zu-space-3)));
}

.skip-link:focus {
	transform: translateY(0);
}

/*
 * The skip link targets <main tabindex="-1"> so activating it moves FOCUS, not
 * just the scroll position. Without the attribute a screen reader was left with
 * its cursor on body, with no guarantee of reaching the content.
 *
 * The outline is removed because this element is not in the tab order: focus
 * lands here programmatically only, and a ring around the whole content would
 * be misleading.
 *
 * Scoped to `main`, and that scoping is the whole point. Written as a bare
 * `[tabindex='-1']:focus`, it out-specified `:focus-visible` (0,2,0 against 0,1,0)
 * for EVERY element given a programmatic focus target. Stage 3 moves focus onto
 * refusal notices when a purchase is blocked, and an independent review measured
 * the result: focus relocated to a message with no indicator at all, so a keyboard
 * user's next Tab continued from an invisible position. A rule that takes something
 * away must not apply more widely than the case it was written for.
 */
main[tabindex='-1']:focus {
	outline: none;
}

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

/*
 * Touch targets — at least 44 px for interactive controls on mobile.
 *
 * The list deliberately covers the header controls too. It used to apply to
 * buttons only, so the cart icon, the account icon and the search submit measured
 * around 32 px on a phone — the shop's primary controls on a small screen.
 */
@media (pointer: coarse) {
	.zu-button,
	.wp-element-button,
	a.button,
	button.button,
	.zu-nav-toggle,
	.woocommerce-ordering select {
		min-height: 44px;
	}

	.zu-search__submit,
	.zu-cart-link,
	.zu-account-link {
		min-width: 44px;
		min-height: 44px;
	}

	/*
	 * The mobile menu was left out of this block, and it is the shop's primary
	 * navigation on a phone. Measured at 320, 390, 768 and 900 with a coarse pointer:
	 * **38 px tall with no gap between consecutive items**, so two adjacent category
	 * links share an edge and a thumb near it can take either.
	 *
	 * The height comes from padding rather than from a min-height so the links stay
	 * left-aligned to their text; `align-items` keeps the label centred in the taller
	 * box. Desktop is untouched — the bar there is a pointer target, not a thumb one.
	 */
	.site-nav__list a {
		display: flex;
		align-items: center;
		min-height: 44px;
	}
}

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

/* ==========================================================================
   10. Reveal on scroll
   ========================================================================== */

/*
 * Nothing here applies until `assets/js/reveal.js` puts `.zu-reveal-ready` on the
 * document, and that is the whole safety of it: a browser with no JavaScript, a
 * crawler that does not wait, or a script that fails to load all leave the page
 * finished rather than blank. The hidden state is never the default.
 *
 * `translateY(1rem)` and 0.6s — far enough to be noticed, short enough not to be
 * waited for. The owner asked for a page that lives, and the difference between
 * elegant and cheap here is entirely distance and duration.
 *
 * `opacity` and `transform` only: both composite on the GPU, so a long page of
 * revealing sections costs no layout and no paint.
 */
.zu-reveal-ready .zu-reveal {
	opacity: 0;
	transform: translateY(1rem);
	transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.zu-reveal-ready .zu-reveal.is-revealed {
	opacity: 1;
	transform: none;
}

/*
 * Children arrive one after another rather than together.
 *
 * Keyed off an **ancestor** that reveals, not off itself: the stagger container is
 * never observed. Marking both a section and the grid inside it would hide the grid
 * twice and animate the same content in two passes — the grid fading in as a block
 * and then its children arriving inside it. One observer per section, and the grid
 * simply reacts.
 *
 * Six steps, 70ms apart: the last lands 350ms after the first, which reads as one
 * gesture rather than six events. Beyond six the delay is capped — a grid of twenty
 * products staggered to the twentieth would keep a customer waiting 1.4s for the
 * last card, which is an animation charging rent.
 */
.zu-reveal-ready .zu-reveal--stagger > * {
	opacity: 0;
	transform: translateY(0.75rem);
	transition: opacity 0.5s ease-out, transform 0.5s ease-out;
}

.zu-reveal-ready .is-revealed .zu-reveal--stagger > *,
.zu-reveal-ready .zu-reveal--stagger.is-revealed > * {
	opacity: 1;
	transform: none;
}

.zu-reveal-ready .is-revealed .zu-reveal--stagger > :nth-child(2) { transition-delay: 0.07s; }
.zu-reveal-ready .is-revealed .zu-reveal--stagger > :nth-child(3) { transition-delay: 0.14s; }
.zu-reveal-ready .is-revealed .zu-reveal--stagger > :nth-child(4) { transition-delay: 0.21s; }
.zu-reveal-ready .is-revealed .zu-reveal--stagger > :nth-child(5) { transition-delay: 0.28s; }
.zu-reveal-ready .is-revealed .zu-reveal--stagger > :nth-child(n + 6) { transition-delay: 0.35s; }

/*
 * Belt and braces. `reveal.js` returns before touching anything when reduced motion
 * is asked for, so `.zu-reveal-ready` is never added and these rules never match —
 * but if that check is ever lost, this keeps the page readable rather than animated.
 */
@media (prefers-reduced-motion: reduce) {
	.zu-reveal-ready .zu-reveal,
	.zu-reveal-ready .zu-reveal--stagger > * {
		opacity: 1 !important;
		transform: none !important;
	}
}

/*
 * Paper does not scroll, and that is not a joke about the medium.
 *
 * `IntersectionObserver` reveals a section when it enters the *viewport*. Printing a
 * page nobody has scrolled through means most sections are still at `opacity: 0`, and
 * they print exactly that way: a frontend review put the front page through Chrome's
 * print pipeline and got the hero, the categories, then **five blank A4 pages**.
 * Measured under `emulateMediaType('print')`: 6 of 7 sections and 16 to 19 of 22
 * stagger children invisible, depending on how far the observer had got.
 *
 * The owner is the first person who will print this page or save it as a PDF, and
 * five sheets of empty cream reads as a broken site rather than as an animation.
 */
/*
 * Printing, and the six legal pages are why this block exists.
 *
 * These are documents people print: terms of sale, a returns procedure with an address
 * to post a form to, a privacy notice. Rendered to A4 the returns page came out as
 * **four sheets**, the top quarter of the first being the logo, the menu button, the
 * search field and the cart icons, and the fourth being nothing but the footer's
 * fifteen navigation links. "Adres do zwrotów" sat at the foot of page one with two of
 * its four address lines below the break.
 *
 * None of that is navigable on paper. What stays is what a printed document needs: the
 * content, and the company's address and contact details from the footer — on a
 * printed regulation those are the most useful lines on the page.
 */
@media print {
	.site-header,
	.site-footer__nav,
	.skip-link {
		display: none;
	}

	/* A heading at the foot of a sheet with its clause overleaf is a broken document. */
	.zu-prose :is(h2, h3) {
		break-after: avoid;
	}

	.zu-prose :is(li, pre, tr, .wp-block-table) {
		break-inside: avoid;
	}

	/*
	 * Backgrounds are off by default in every browser's print dialogue, so the deep
	 * footer band prints pale ink on white. The company details have to survive that.
	 */
	.site-footer,
	.site-footer__company,
	.site-footer__address,
	.site-footer__contact a {
		color: #000;
		background: transparent;
	}
}

@media print {
	.zu-reveal-ready .zu-reveal,
	.zu-reveal-ready .zu-reveal--stagger > * {
		opacity: 1 !important;
		transform: none !important;

		/*
		 * `transition: none` as well, and it is not belt-and-braces.
		 *
		 * Without it the switch to print media starts a 0.6s fade from 0 to 1 rather
		 * than arriving at 1 — measured mid-flight at **0.654**. A print job that
		 * snapshots before it finishes prints a half-faded section, which is a subtler
		 * version of the same blank page.
		 */
		transition: none !important;
	}
}

/*
 * Focus reveals instantly, because a keyboard can arrive before the observer does.
 *
 * Tabbing into an unrevealed section scrolls it into view, and the reveal then takes
 * up to 0.6s — during which the focused element sits at low opacity and, because the
 * opacity is on an *ancestor*, so does its focus ring. Measured with real Tab presses,
 * on two separate reviews: **between a quarter and half of the front page's ~40 tab
 * stops received focus below 0.99 opacity, the lowest reading 0.000** — a keyboard
 * user with no visible focus indicator at all (WCAG 2.4.7). The exact count is a
 * property of how fast the keys arrive; that some stops land invisible is not.
 *
 * It healed itself in half a second, which is exactly what makes it the kind of
 * defect nobody reports and everybody feels.
 */
.zu-reveal-ready .zu-reveal:focus-within,
.zu-reveal-ready .zu-reveal--stagger > *:focus-within {
	opacity: 1;
	transform: none;
	transition: none;
}

/*
 * The breadcrumb separator, and why it is an element rather than a pair of
 * non-breaking spaces.
 *
 * WooCommerce's default delimiter is `&nbsp;/&nbsp;`, which makes the trail one
 * unbreakable run — fine on a flat taxonomy, and the cause of horizontal scrolling at
 * 320 px once the catalogue went three levels deep. `zakatek_breadcrumb_defaults()`
 * replaces it with ordinary spaces around this span, so the trail wraps between
 * crumbs. `overflow-wrap` is the backstop for a single crumb longer than the screen.
 */
.woocommerce .woocommerce-breadcrumb,
.woocommerce-breadcrumb {
	overflow-wrap: anywhere;
}

.zu-breadcrumb__sep {
	color: var(--zu-line-strong, var(--zu-ink-muted));
}
