/*
 * GENERATED FILE — do not hand-edit. Edit the relevant source file under
 * wp-content/themes/classroompros/assets/css/ instead, then rebuild with
 * `php tools/build-css.php`.
 *
 * Produced by tools/build-css.php from main.css's own component list
 * (assets/css/main.css), which remains the only file a human edits and
 * the only thing that decides this file's concatenation order.
 *
 * D-09-15/D-09-16: this build step runs on the developer's machine only,
 * never on Active24 — see tools/build-css.php's own docblock for why
 * that is structurally true, not merely a convention. It lives at
 * assets/dist/ rather than beside its sources because several existing
 * test harnesses read assets/css/ directly, some of them recursively
 * with per-file allow-lists a second copy of every declaration would
 * break; assets/dist/ sits at the same directory depth as assets/css/,
 * so every relative url() reference in the bundled files below resolves
 * identically with zero rewriting.
 */
@layer reset, base, components, utilities, overrides;

/* SOURCE: reset.css */
/*
 * reset.css — modern minimal reset, wrapped entirely in the `reset` layer
 * declared by style.css:9 (`@layer reset, base, components, utilities,
 * overrides;`). This is the lowest-priority layer, so anything in
 * base/components/utilities/overrides always wins regardless of
 * selector specificity — that ordering guarantee is the whole point of
 * the cascade-layer architecture (CLAUDE.md § Research Findings Area 1).
 */

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

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

	ul,
	ol {
		padding: 0;
		list-style: none;
	}

	h1,
	h2,
	h3,
	h4,
	h5,
	h6 {
		text-wrap: balance;
	}

	p {
		text-wrap: pretty;
	}

	img,
	picture,
	svg {
		display: block;
		max-width: 100%;
	}

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

	button {
		background: none;
		border: none;
		cursor: pointer;
	}

	a {
		color: inherit;
		text-decoration-skip-ink: auto;
	}

	/*
	 * 02-07 fix: [hidden] is only a UA-stylesheet `display: none` — ANY
	 * author-layer `display` declaration on the same element (e.g.
	 * components/hero.css's `.c-hero__track { display: flex; }`) silently
	 * overrides it, because the UA stylesheet has no cascade-layer
	 * membership and always loses to a layered author rule. This is
	 * exactly backwards from TRACK-04's contract (the inactive track must
	 * be hidden by the real `hidden` attribute, never a CSS substitute) —
	 * the failure mode nobody had guarded against was CSS *un*-hiding an
	 * attribute-hidden element. Re-assert it here, at the reset layer, so
	 * it protects every current and future component in one place rather
	 * than requiring every `[data-track]`-bearing component to remember
	 * its own `[hidden]` override. `!important` is the standard
	 * normalize/reset idiom for this exact rule, precisely because
	 * `[hidden]` is otherwise trivially overridden by any component-layer
	 * `display` declaration, layered or not.
	 */
	[hidden] {
		display: none !important;
	}

	/*
	 * :focus-visible is intentionally left untouched — no
	 * `outline: none` reset anywhere in this file. Every interactive
	 * control this project ships relies on the browser's native
	 * focus-visible ring (or the accent-colored override components.css
	 * adds later); stripping it here would silently break keyboard
	 * navigation for every later plan in this phase.
	 */
}

/* SOURCE: base.css */
/*
 * base.css — @font-face declarations, design tokens and element-level
 * typography, wrapped entirely in the `base` layer declared by
 * style.css:9. Sits above `reset` in cascade priority, below
 * `components`/`utilities`/`overrides`.
 *
 * Font lock — Phase 02.5 (D-15) replaces the 02.3 five-face system with
 * the approved split design's own two-face pair: Instrument Serif 400
 * (headlines, stat figures, pull-quotes, prices) and Archivo variable
 * (UI, body, labels, buttons). Space Mono, Syne and Anybody's @font-face
 * blocks are deleted, not repointed — their .woff2 files stay on disk
 * for one wave; plan 02.5-05 owns their removal from tools/subset-fonts.sh
 * (D-23). The ď/ť caron gate is not re-run here — 02.3-VERIFICATION.md
 * already passed it for both surviving faces at the subsetted-artefact
 * level (D-23); this plan confirms that result rather than repeating it.
 */

@layer base {
	/*
	 * Grotesk / display / UI face — Archivo (variable). `wght` stays live
	 * across 400-700 (the project's two-weight budget); `wdth` was pinned
	 * to its own default at subset time since nothing varies Archivo's
	 * width. Hierarchy comes from size, weight, tracking and family —
	 * never a font-variation-settings rule against a pinned axis.
	 */
	@font-face {
		font-family: 'Archivo';
		src: url('../fonts/archivo-variable.woff2') format('woff2');
		font-weight: 400 700;
		font-style: normal;
		font-display: swap;
		unicode-range: U+0000-00FF, U+010C-010F, U+011A-011B, U+0147-0148, U+0158-0161, U+0164-0165, U+016E-016F, U+017D-017E;
	}

	/*
	 * Headline / figure / pull-quote / price face — Instrument Serif.
	 * Public release ships Regular only; it never carries the bold slot,
	 * consistent with the project rule that the serif role is reading
	 * copy, never a button, so it never needed emphasis in the first
	 * place. `unicode-range` is carried verbatim from the 02.3-verified
	 * subset — this range IS the Czech subset contract; narrowing it
	 * breaks the caron gate.
	 */
	@font-face {
		font-family: 'Instrument Serif';
		src: url('../fonts/instrument-serif-400.woff2') format('woff2');
		font-weight: 400;
		font-style: normal;
		font-display: swap;
		unicode-range: U+0000-00FF, U+010C-010F, U+011A-011B, U+0147-0148, U+0158-0161, U+0164-0165, U+016E-016F, U+017D-017E;
	}

	/*
	 * Condensed display face — Phase 07.1 (D-07.1-02). Archivo instanced at
	 * `wdth 62` and fully pinned to `wght 700` — a separate font FILE, not a
	 * `font-variation-settings` rule against the axis this file's own lines
	 * 21-24 above explain was pinned away at subset time for the body cut.
	 * It exists for the mobile hero's huge scrolling type (07.1-04) and is a
	 * DISPLAY face only — never body or UI text. `unicode-range` is carried
	 * verbatim from the two blocks above; this range IS the Czech subset
	 * contract and narrowing it breaks the caron gate (tools/subset-fonts.sh).
	 */
	@font-face {
		font-family: 'Archivo Condensed';
		src: url('../fonts/archivo-condensed-700.woff2') format('woff2');
		font-weight: 700;
		font-style: normal;
		font-display: swap;
		unicode-range: U+0000-00FF, U+010C-010F, U+011A-011B, U+0147-0148, U+0158-0161, U+0164-0165, U+016E-016F, U+017D-017E;
	}

	:root {
		/* Spacing scale — multiples of 4, per 02-UI-SPEC.md § Spacing Scale */
		--space-xs: 4px;
		--space-sm: 8px;
		--space-md: 16px;
		--space-lg: 24px;
		--space-xl: 32px;
		--space-2xl: 48px;
		--space-3xl: 64px;
		/*
		 * New this phase — 02.3-UI-SPEC.md § Spacing Scale. Full-bleed
		 * section dividers and the hero-to-first-section gap on the
		 * mobile brutalist rhythm. The only new step; the scale is not
		 * extended further, since nothing in this project's eleven-
		 * section single-page layout currently needs a larger gap.
		 */
		--space-4xl: 96px;

		/*
		 * --tap-target-min is the ONE sanctioned exception to the
		 * 4px-multiple spacing rule (44 is a multiple of 4, not 8, and
		 * smaller than --space-lg) — flagged explicitly in Phase 1 and
		 * confirmed in 02-UI-SPEC.md § Spacing Scale. Applies to every
		 * interactive control this phase ships: in-hero track-choice
		 * buttons, the floating bar's compact toggle, admin ↑/↓
		 * reorder buttons, the admin drag handle, the header's
		 * tap-to-call link, and repeater add/remove controls.
		 */
		--tap-target-min: 44px;

		/*
		 * Ground / ink / accent — 02.3-UI-SPEC.md § Ground, Ink, Accent.
		 * This directly inverts the prior phase's green monochrome-ink
		 * ramp: one literal white ground, one literal carbon ink for all
		 * text everywhere, and the accent as a ground/fill colour only —
		 * never a text colour. #2EE168 is declared as a literal in
		 * exactly this one place; every other reference reaches it
		 * through this token. Computed WCAG figures (02.3-UI-SPEC.md §
		 * Computed contrast): carbon ink on white ≈ 19.8:1 (AAA at every
		 * size); the accent on white ≈ 1.74:1 (fails AA at every text
		 * size — structurally invalid as text, independent of taste).
		 */
		--color-ground: #FFFFFF;
		--color-ink: #0A0A0A;
		--color-accent: #2EE168;
		--color-destructive: #D64545; /* unchanged — admin-only, out of the public-page ground/ink/accent contract */

		/*
		 * Five panel grounds plus paper (Phase 02.5, D-12/D-14) — every
		 * pair below is verified by computing the WCAG relative-luminance
		 * ratio, never by eye (02.5-CONTRAST.md). --color-accent above is
		 * unchanged and IS --ground-green's value; declared as its own
		 * literal here rather than aliased, because the accent's contract
		 * (never used as text — test-no-accent-as-text.php) is narrower
		 * than a panel ground's (used as a background for large panels of
		 * body text) and the two must stay independently editable.
		 *
		 * green:  9.81:1 at full opacity — AA pass.
		 * coral:  02.5-CONTRAST.md Option B — the hue ships exactly as
		 *         approved (#FF5A36); the text flips dark instead of the
		 *         sign-off's literal "darken the hue" wording, because
		 *         darkening the hue still fails once the mockup's opacity
		 *         fades are accounted for (worst case 2.46:1) while
		 *         #1B0500 clears 4.5:1 at every opacity in the design
		 *         (4.84:1 worst case). This supersedes D-13.
		 * indigo: 6.23:1 at full opacity, 4.53:1 at the design's lowest
		 *         opacity — passes, but with only 0.03 to spare; treat as
		 *         at-risk, not as comfortably safe.
		 * amber:  11.98:1 at full opacity — AA pass.
		 * night:  16.60:1 at full opacity — AA pass.
		 * paper:  17.74:1 at full opacity — AA pass; this is the fixed
		 *         left pane's own ground/ink pair (glass.css), opaque and
		 *         never tinted or blurred (D-02).
		 */
		--ground-green: #2EE168;
		--ink-green: #06210E;
		--ground-coral: #FF5A36;
		--ink-coral: #1B0500;
		--ground-indigo: #4B3BFF;
		--ink-indigo: #FFFFFF;
		--ground-amber: #FFC83D;
		--ink-amber: #1B1200;
		--ground-night: #0F140F;
		--ink-night: #EFF3ED;
		--paper: #F7F7F3;
		--paper-ink: #0D110B;

		/*
		 * Two computed tonal ink steps per ground/pane (Phase 02.5 Task 1,
		 * 02.5-CONTRAST.md recommendation 2) — what body copy, fact/service
		 * descriptions and footnotes use INSTEAD OF the mockup's decorative
		 * `opacity` fades. Opacity composites text toward its own
		 * background, which is exactly what let coral's real 2.46:1 worst
		 * case hide behind a declared 3.10:1 in the sign-off — a computed
		 * colour has no such gap between declared and effective value.
		 * Every ratio below is computed with the same WCAG relative-
		 * luminance formula as the primary ink pairs above, so 4.5:1 is a
		 * property of the shipped hex value, not of an opacity that could
		 * later drift. `-2` is the lede/fact/service tone (still close to
		 * full ink); `-3` is the smallest, most receded tone this system
		 * ships (fnote/caption/label) and is deliberately never faded
		 * further than its own printed ratio.
		 *
		 * green:  full 9.81 · -2 #0C3C1B 7.20 · -3 #115426 5.21
		 * coral:  full 6.33 · -2 #320E06 5.63 · -3 #47160B 4.88
		 * indigo: full 6.23 · -2 #F0EEFF 5.46 · -3 #E2DFFF 4.82 — indigo's
		 *         own full-ink budget is already tight (D-14), so its two
		 *         fade steps are the shallowest of the six; treat both as
		 *         at-risk the way the primary pair already is.
		 * amber:  full 11.99 · -2 #40300A 8.26 · -3 #624A13 5.41
		 * night:  full 16.60 · -2 #BBBFB9 9.99 · -3 #8D918C 5.82
		 * paper:  full 17.74 · -2 #393C36 10.44 · -3 #60625D 5.75
		 */
		--ink-green-2: #0C3C1B;
		--ink-green-3: #115426;
		--ink-coral-2: #320E06;
		--ink-coral-3: #47160B;
		--ink-indigo-2: #F0EEFF;
		--ink-indigo-3: #E2DFFF;
		--ink-amber-2: #40300A;
		--ink-amber-3: #624A13;
		--ink-night-2: #BBBFB9;
		--ink-night-3: #8D918C;
		--paper-ink-2: #393C36;
		--paper-ink-3: #60625D;

		/*
		 * The `--rule`/`--rule-strong`/`--rule-faint`/`--rule-w` carbon
		 * hairline system that lived here through Phase 02.3 is REMOVED,
		 * not repointed (Phase 02.5, D-02/D-20/D-24) — every one of those
		 * four tokens is a 1px-hairline primitive, and "no 1px hairlines"
		 * is the first of the approved design's six hard negatives. Their
		 * only fourteen consumers were `components/sections.css` (13) and
		 * `components/header.css` (1), both deleted whole in this same
		 * commit, so the tokens and the borders they drew leave the tree
		 * together and the tree is never half-migrated. Where the approved
		 * design still shows a dividing line — the panel head rule — it is
		 * a 0.14rem bar at reduced opacity (components/panels.css), which
		 * is a shape, not a border, and needs no token of its own.
		 * utilities.css's `.u-visually-hidden` clip-rect `1px` is a false
		 * positive of a hairline sweep (D-24) and is untouched — it lives
		 * in a file this plan does not import here.
		 */

		/*
		 * Focus ring — never the accent. The accent measures ≈1.74:1 on
		 * white and fails WCAG 2.4.11's 3:1 non-text minimum; carbon ink
		 * measures ≈19.8:1, clearing the floor by a wide margin at every
		 * zoom level.
		 */
		--color-focus-ring: var(--color-ink);

		/*
		 * Role-named type families — Phase 02.5 (D-15) narrows this to the
		 * approved design's two faces: Archivo for UI/body/labels/buttons,
		 * Instrument Serif for headlines/figures/pull-quotes/prices.
		 * --font-mono/--font-stamp/--font-note (Space Mono/Syne/Anybody,
		 * the three retired 02.3 faces) are removed — the only live
		 * consumer was this file's own `label, .c-label` rule below,
		 * repointed to --font-grotesk in the same commit; every other
		 * reference lives in components/hero.css and components/sections.css,
		 * neither of which main.css imports any more (dead, unparsed CSS —
		 * plan 02.5-05 owns their removal from disk). Never hardcode a
		 * family name in a component file — always reach through one of
		 * these two tokens.
		 */
		--font-grotesk: 'Archivo', ui-sans-serif, system-ui, sans-serif;
		--font-serif: 'Instrument Serif', ui-serif, Georgia, serif;

		/*
		 * Condensed display face — Phase 07.1 (D-07.1-02). Consumed only by
		 * plan 07.1-04's mobile hero; nothing in this plan applies it to any
		 * selector. The `'Archivo'` second fallback step is LOAD-BEARING, not
		 * decorative: if archivo-condensed-700.woff2 fails to load, the
		 * fallback is the family this project has already caron-gated end to
		 * end, never a system condensed face whose ď/ť construction nobody
		 * has inspected — a wrong caron on a Czech company's own site is
		 * exactly the failure mode this project's gate exists to prevent.
		 */
		--font-condensed: 'Archivo Condensed', 'Archivo', ui-sans-serif, system-ui, sans-serif;

		/*
		 * Weight tokens — Archivo's variable range covers both; Instrument
		 * Serif ships Regular only and never carries the bold slot.
		 */
		--weight-regular: 400;
		--weight-bold: 700;

		/*
		 * Motion — the base three durations and both spring easings are
		 * inherited unchanged from Phase 1/2.2 and remain declared with
		 * their values unchanged. They are still unconsumed: Phase 4's
		 * audio accordion is their reserved first consumer.
		 */
		--duration-fast: 150ms;
		--duration-base: 250ms;
		--duration-slow: 400ms;
		--ease-spring-smooth: linear(0, 0.35, 0.62, 0.82, 0.94, 0.99, 1);
		--ease-spring-bounce: linear(0, 0.18, 0.42, 0.72, 0.95, 1.06, 1.04, 1.01, 0.99, 1);

		/*
		 * Phase 2.2's floating-bar motion language — kept, unchanged in
		 * value. --ease-graphic is a standard "expo-out" curve — fast
		 * initial movement, hard stop, no overshoot. --duration-snap is
		 * shorter than --duration-fast: a harder cut, not a softened one.
		 */
		--ease-graphic: cubic-bezier(0.16, 1, 0.3, 1);
		--duration-snap: 100ms;

		/*
		 * New this phase — 02.3-UI-SPEC.md § Motion. --e-out is for
		 * entrances only (the sticky track bar's slide-in); --e-hard
		 * must never be used for an entrance because it starts too flat.
		 * --d-snap and --d-bar are the two new durations these easings
		 * pair with.
		 */
		--e-out: cubic-bezier(0.16, 1, 0.3, 1);
		--e-hard: cubic-bezier(0.76, 0, 0.16, 1);
		--d-snap: 0.12s;
		--d-bar: 0.26s;
	}

	/*
	 * Global focus-visible ring — 02.3-UI-SPEC.md § Focus-Visible Ring.
	 * outline-offset is 3px, not the prior phase's 2px, so the ring reads
	 * clearly against the hard square edges everywhere else on the page.
	 */
	:focus-visible {
		outline: 2px solid var(--color-focus-ring);
		outline-offset: 3px;
	}

	/*
	 * html's `hyphens: auto` and `lang="cs"` already exist from Phase 1
	 * (style.css:11 and functions.php's language_attributes filter) —
	 * restated here as a no-op comment, not a duplicate declaration.
	 */

	body {
		font-family: var(--font-grotesk);
		font-size: 1.0625rem;
		font-weight: var(--weight-regular);
		line-height: 1.4;
		letter-spacing: 0;
		color: var(--color-ink);
		background-color: var(--color-ground);
	}

	/*
	 * Label / metadata role — the only place text-transform: uppercase is
	 * permitted in this file, licensed by locked decision 7's short-labels
	 * carve-out (02.3-UI-SPEC.md § Typography). Repointed to --font-grotesk
	 * (Phase 02.5, D-15) now that the mono role face is retired.
	 */
	label,
	.c-label {
		font-family: var(--font-grotesk);
		font-size: 0.75rem;
		font-weight: var(--weight-regular);
		line-height: 1.5;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: var(--color-ink);
	}

	h1,
	h2,
	h3,
	h4,
	h5,
	h6 {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
	}

	h2,
	h3 {
		font-size: clamp(1.5rem, 1rem + 2.4vw, 3rem);
		line-height: 1.05;
		letter-spacing: -0.02em;
	}

	/*
	 * Display role — H1 and .c-display-text. line-height: 1.16 is the
	 * measured knockout-leading lock 02.3-UI-SPEC.md § "The
	 * knockout-leading rule" derived for Archivo specifically: an inline
	 * background box for Archivo is exactly 1.1713em tall regardless of
	 * declared line-height, and 1.16 is what makes consecutive knockout
	 * bars fuse into one slab instead of gapping or overlapping. This is
	 * a measured value to be re-verified in-browser against the shipped
	 * subsetted archivo-variable.woff2, not a taste call.
	 */
	h1,
	.c-display-text {
		font-size: clamp(2.5rem, 1rem + 8vw, 6rem);
		line-height: 1.16;
		letter-spacing: -0.03em;
	}

	/*
	 * .ser — the approved design's one-word serif-italic accent (the
	 * mockup's own `.ser` class name, kept verbatim so a reader can trace
	 * it back to 02.4-MOCKUP-v9-split.html directly). Used to mark a
	 * single word inside an otherwise-Archivo headline, e.g. the fixed
	 * pane's `h1` pitch (glass.css, item 1 of 02.5-FIDELITY-WATCH.md) —
	 * "Naučíme vaše dítě <span class='ser'>mluvit</span>." in the mockup.
	 * Declared once, globally, because it is a character-level emphasis
	 * device usable wherever copy calls for it, not a component. The span
	 * itself belongs inside the copy that calls for it (a template
	 * decision, plan 02.5-06's file) — this rule only exists so that span
	 * has somewhere to land.
	 */
	.ser {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-style: italic;
		letter-spacing: -0.005em;
	}

	/*
	 * Cut-motion override — 02.3-UI-SPEC.md § Motion, rule 1. The
	 * browser's DEFAULT View Transition behaviour is an opacity
	 * cross-fade of roughly 250ms; left alone, the existing
	 * document.startViewTransition() call in track-toggle.js would
	 * silently ship exactly the fade the track-swap contract forbids.
	 * This is the whole fix and it is CSS-only — track-toggle.js is not
	 * touched by this phase; its hidden-attribute swap, pushState and
	 * localStorage sequencing are inherited structural plumbing.
	 */
	::view-transition-old(root),
	::view-transition-new(root) {
		animation: none;
		mix-blend-mode: normal;
	}
}

/* SOURCE: components/glass.css */
/*
 * glass.css — the fixed left paper pane (Phase 02.5, 02.4-SIGNOFF.md,
 * D-02). Hand-authored, native nesting, no preprocessor, no bundler
 * (D-16). Wrapped entirely in the `components` layer declared by main.css.
 *
 * `.glass` is opaque `var(--paper)` — NOT frosted, NOT tinted, and
 * carrying no translucent backdrop filtering of any kind. This is a hard
 * negative learned from an explicit rejection (D-02) — do not add
 * `backdrop-filter` to this selector or any of its descendants under any
 * circumstance. Depth comes from flat tonal steps, overlap and
 * box-shadow only, never a gradient (D-02).
 *
 * Task 3 (02.5-CONTRAST.md recommendation 2, criterion 6): no `opacity`
 * on any body-text selector in this file — the pull-quote, its
 * attribution, the form's reply note and the footer line all read one of
 * the two computed `--paper-ink-2`/`--paper-ink-3` steps base.css
 * declares instead of a fade.
 *
 * No @keyframes and no transition live here either — motion is
 * components/motion.css.
 */

@layer components {
	/*
	 * Phase 07.1, plan 06 (D-07.1-06), Task 2. `--pane-inset` and
	 * `--pane-radius` used to be declared ONLY inside the
	 * `min-width: 1000px` fit-ladder block near the end of this file — a
	 * phone could never read either, since a media query that never
	 * matches never contributes a value. Both are now declared
	 * unconditionally here, with values chosen for a PHONE (the desktop
	 * fit-ladder's own declarations, further down this file, are
	 * unedited and byte-identical to what shipped before this task —
	 * confirmed by reading them, not assumed — and WIN at desktop widths
	 * because they come later in this same file at equal `:root`
	 * specificity, the same "later wins" rule this file's own fit-ladder
	 * comment already documents).
	 *
	 * Desktop's own inset (`clamp(10px, 1.1vw, 20px)`) is viewport-relative
	 * and resolves under 4px on a 320px phone — a value tuned for a
	 * 1440px screen, not this one. The phone gets its own fixed value from
	 * the `--space-*` scale instead: `--space-md` (16px), matching this
	 * project's own established phone edge margin elsewhere (mobile.css's
	 * `.m-lead` padding, mobile-actions.css's `.m-actions` padding-inline).
	 *
	 * `--pane-radius` is 22px on desktop, sized for a ~600px-tall column;
	 * `16px` here is one visual step down, proportioned for a card that is
	 * both narrower and shorter — same TOKEN name reused through both
	 * rules (never a hardcoded new literal at either call site), a
	 * DIFFERENT resolved value per breakpoint by design, exactly like
	 * `--pane-gap`/`--pane-pad` already vary across this file's own height
	 * ladder.
	 *
	 * `--pane-shadow` is new: the desktop tile's exact box-shadow value,
	 * promoted to a token so both the phone and the desktop `.glass` rules
	 * reference the SAME declared shadow VALUES (not just the same
	 * mechanism) — "the same edge the computer website has" becomes a fact
	 * about this stylesheet rather than a coincidence between two
	 * hand-typed copies of the same six numbers.
	 */
	:root {
		--pane-inset: var(--space-md);
		--pane-radius: 16px;
		--pane-shadow: 0 30px 70px -22px rgba(13, 17, 11, 0.42), 0 2px 8px rgba(13, 17, 11, 0.10);
	}

	.glass {
		background: var(--paper);
		color: var(--paper-ink);
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
		padding: clamp(var(--space-md), 4vw, var(--space-xl));
		overflow-y: auto;
		/*
		 * The phone tile (D-07.1-06 Task 2): a floating, inset, rounded,
		 * shadowed paper card — the desktop tile's own treatment, one
		 * size down, sharing its tokens rather than copying its numbers.
		 * No `1px solid` hairline anywhere (D-02's first hard negative) —
		 * `.glass__form`'s own comment already states the form carries "no
		 * ground/border of its own beyond the paper it sits on"; this
		 * margin/radius/shadow trio IS that paper's own visible edge, not
		 * a border added to it. Reset to `margin: 0` inside the desktop
		 * query below — desktop is `position: fixed` with its own explicit
		 * `inset-block`/`left` offsets already doing this job; a margin
		 * left un-reset there would silently double the desktop inset.
		 */
		margin: var(--pane-inset);
		border-radius: var(--pane-radius);
		box-shadow: var(--pane-shadow);
	}

	/*
	 * Phase quick-260831-v19 — the owner's standing requirement: "the left
	 * pannel should never scroll", and "I like the rounded corners".
	 *
	 * The pane becomes a floating, rounded tile inset from the viewport
	 * edge. Measured before this change at 1440x900 it held 1434px of
	 * content in a 900px box — a 534px overflow. Making it fit is the
	 * whole job, and the ORDER in which things give is the design:
	 * chrome sheds first, content never. The five enquiry fields, the
	 * submit and the tap-to-call link survive every tier, because the
	 * form is this project's core value (02.7 D-01: the form always
	 * wins) and CONV-07 owns the phone link.
	 *
	 * The door is COMPACTED at short heights, never removed — it is the
	 * only route to the pane overlay, and test-content-reachability.php
	 * fails outright if that route disappears.
	 */
	@media (min-width: 1000px) {
		.glass {
			position: fixed;
			/*
			 * D-07.1-06 Task 2: the phone's own base rule now sets
			 * `margin: var(--pane-inset)`. Desktop is `position: fixed`
			 * with its own explicit `inset-block`/`left` offsets doing the
			 * identical job below — a margin left un-reset here would
			 * ADD to those offsets (a fixed element's margin box is still
			 * offset from its containing block by its own margin, even
			 * with explicit inset properties set), silently doubling the
			 * desktop inset. Reset to zero so desktop's own inset maths,
			 * unchanged below, stays the only thing positioning it.
			 */
			margin: 0;
			inset-block: var(--pane-inset);
			left: var(--pane-inset);
			width: calc(46vw - var(--pane-inset) * 2);
			border-radius: var(--pane-radius);
			overflow: hidden;
			/* Both .glass and .panel are positioned elements with no
			   z-index declared elsewhere; per CSS2.1 painting order,
			   same-layer positioned siblings paint in DOM order. Until
			   Phase 07.1, plan 06 (D-07.1-06), `.panel` came AFTER
			   `.glass` in front-page.php's markup, so without an explicit
			   stacking priority here every scrolling panel would silently
			   paint on top of the fixed pane, covering the pitch and the
			   enquiry form entirely — the defect this z-index was added
			   to fix, found by rendering and reading the screenshot
			   (D-19), not by assertion.

			   D-07.1-06 relocated `.glass`'s own dispatch to fire INSIDE
			   the panel loop, between the last programme panel and the
			   first non-programme one — so `.panel` now comes both before
			   AND after `.glass` in the markup, depending on which panel.
			   The premise above ("panel always comes after glass") is no
			   longer wholly true; the declaration below is now
			   belt-and-braces rather than the only thing preventing the
			   defect, and is left exactly as it was — removing it would
			   still leave the pane painting correctly today by document
			   order alone for panels after it, but under it for panels
			   before it, on any future markup change that is not
			   guaranteed to preserve painting order this rule already
			   states explicitly. */
			z-index: 10;
			box-shadow: var(--pane-shadow);
			overscroll-behavior: contain;
			gap: var(--pane-gap);
			padding: var(--pane-pad);
		}
	}

	.glass__brand {
		display: none;
		align-items: center;
		gap: var(--space-sm);
	}

	.glass__logo {
		display: inline-flex;
		width: 2rem;
		height: 2rem;
		flex-shrink: 0;
	}

	.glass__logo svg,
	.glass__logo img {
		width: 100%;
		height: 100%;
	}

	.glass__wordmark {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 1.0625rem;
		letter-spacing: -0.01em;
	}

	.glass__pitch {
		display: none;
		gap: var(--space-md);
	}

	/*
	 * Item 1 (02.5-FIDELITY-WATCH.md #1) — the approved design's H1 role
	 * for THIS element is Archivo, not Instrument Serif: the sign-off's
	 * "Instrument Serif — headlines" line describes the programme-panel
	 * titles (panels.css `.panel__heading`) and the dock title, not the
	 * pane's own pitch statement, which the mockup sets in its page-level
	 * `h1` rule (Archivo, weight 800, letter-spacing -.038em) with a
	 * SINGLE word carrying a `.ser` (Instrument Serif italic) accent span.
	 * Font-family/weight/letter-spacing are fixed here — the CSS half of
	 * this item. The `.ser` span itself is a per-word markup decision
	 * inside the pitch copy, which lives in template-parts/glass-pane.php
	 * (plan 02.5-06's file, not this plan's) — .ser is declared once,
	 * below, ready for that span to use.
	 *
	 * One documented gap: the approved weight is 800. base.css's Archivo
	 * @font-face declares `font-weight: 400 700` (the project's
	 * established two-weight subset budget, D-15/02.3) — 800 is outside
	 * that descriptor's range, so a browser matching `font-weight: 800`
	 * against this face resolves to its nearest boundary, 700. This ships
	 * at 700 rather than a silently-wrong "800" that the face cannot
	 * actually render; widening the subsetted weight axis is a font-file
	 * change (tools/subset-fonts.sh, plan 02.5-09's file), not a CSS one.
	 */
	.glass__pitch-heading {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: clamp(2rem, 1.3rem + 3vw, 3.05rem);
		line-height: 1.03;
		letter-spacing: -0.038em;
	}

	.glass__pitch-body {
		font-size: 1rem;
		line-height: 1.55;
		max-width: 38ch;
		color: var(--paper-ink-2);
	}

	/*
	 * `display: none` on the phone (2026-09-07, owner: "this image and
	 * quote is supposed to be right under the hero on the green
	 * background"). The quote now has TWO render sites reading the one
	 * stored `hero.quote` / `hero.quote_author` / `hero.quote_image` —
	 * `mobile-lead.php`'s `.m-quote`, which paints under the hero on the
	 * brand green, and this one, which is the desktop pane's.
	 *
	 * Exactly one is ever in the accessibility tree: `.m-lead` is already
	 * `display: none` above 1000px (mobile.css's own desktop query), and
	 * this rule takes the other half below it. `display: none`, not
	 * `visibility`/`opacity`, precisely because it must leave the a11y
	 * tree and not merely stop painting — the same reasoning, and the same
	 * idiom, as `.glass__footer`'s phone hide + desktop restore directly
	 * below (D-07.1-10, plan 07.1-05).
	 */
	.glass__quote {
		display: none;
		padding-inline-start: var(--space-md);
		border-inline-start: 0.14rem solid currentColor;
		color: var(--paper-ink-2);
	}

	/*
	 * Phase 07.1, plan 06 (D-07.1-08). Sits ABOVE the blockquote, inside
	 * the figure's own `border-inline-start` bar (unchanged, kept
	 * deliberately — see glass-pane.php's own comment on why the bar
	 * stays and reads as one testimonial unit, not two ideas). No new
	 * ground/colour of its own (D-02) — a flat rounded rectangle on the
	 * pane's own paper, matching this file's existing `0.6rem` radius
	 * vocabulary (`.glass__form-submit`) rather than a new literal.
	 */
	.glass__quote-image {
		display: block;
		width: 100%;
		max-width: 11rem;
		/*
		 * `height: auto` is load-bearing, not decoration: the rendered
		 * `<img>` carries HTML `width`/`height` attributes from
		 * `wp_get_attachment_image()`'s own intrinsic-size output, and
		 * those attribute values resolve to a definite (non-`auto`)
		 * `height` unless CSS says otherwise — at that point
		 * `aspect-ratio` has nothing left to compute, since it only
		 * derives a dimension that is actually `auto`. Found by
		 * rendering: without this line the box measured 176x300 (the
		 * attachment's own 240x300 crop, scaled), not 176x132 (4:3).
		 */
		height: auto;
		aspect-ratio: 4 / 3;
		object-fit: cover;
		border-radius: 0.6rem;
		margin-block-end: var(--space-sm);
	}

	.glass__quote blockquote {
		font-family: var(--font-serif);
		font-style: italic;
		font-size: 1.125rem;
		line-height: 1.4;
		color: var(--paper-ink);
	}

	.glass__quote figcaption {
		font-size: 0.8125rem;
		margin-top: var(--space-xs);
		color: var(--paper-ink-3);
	}

	/*
	 * Item 2 (02.5-FIDELITY-WATCH.md #2) — "a real object with weight,"
	 * per the mockup's own comment, not a plain form on transparent
	 * ground: white card background, no ground/border of its own beyond
	 * the paper it sits on, spacing that reads as a card rather than a
	 * stack of bare fields.
	 */
	.glass__form {
		display: flex;
		flex-direction: column;
		/*
		 * One step down the `--space-*` scale on a phone (D-07.1-06
		 * Task 2, "smaller") — `--space-md` (16px) to `--space-sm` (8px).
		 * Desktop's own `min-width: 1000px` fit-ladder tier already
		 * overrides this to `--space-sm` unconditionally above 1000px
		 * (further down this file, unedited); phone now reads the same
		 * value by its own base rule instead of inheriting nothing.
		 */
		gap: var(--space-sm);
	}

	.glass__form-heading {
		display: flex;
		align-items: baseline;
		justify-content: space-between;
		gap: var(--space-sm);
		flex-wrap: wrap;
		font-family: var(--font-serif);
		font-style: italic;
		font-weight: var(--weight-regular);
		font-size: 1.5rem;
		letter-spacing: -0.01em;
	}

	/*
	 * The amber "1. hodina zdarma" tag (02.5-FIDELITY-WATCH.md #3) — its
	 * COLOUR is fixed here since colour is presentation, but its actual
	 * copy is a `kontakt.form_tag` field value (CONT-01) that plan
	 * 02.5-06's template wiring supplies; until then this class renders
	 * nothing (glass-pane.php's own empty-state guard). #8A5D00 on
	 * paper's #F7F7F3 ground computes to 5.36:1 (WCAG relative-luminance
	 * formula) — clears 4.5:1. This is a one-off accent-as-text use on the
	 * pane, outside the six-surface ground/ink system base.css declares,
	 * so it is computed here rather than added as an eighth token pair;
	 * tools/tests/test-contrast-ratios.php's scope is the declared
	 * ground/ink token pairs (Task 3), so this literal is verified by the
	 * arithmetic in this comment rather than by that harness — recorded
	 * in the plan's SUMMARY as a documented, deliberate boundary, not a
	 * silent gap.
	 */
	/*
	 * "1. hodina zdarma" is the strongest single thing this page can say to
	 * a hesitant parent, and it was set as a 15px serif italic footnote
	 * under the form heading, where it read as a caption. It is a chip now.
	 *
	 * The colours are the locked amber ground/ink pair rather than a new
	 * value, so this needs no fresh contrast maths: #1B1200 on #FFC83D is
	 * 11.99:1, and test-contrast-ratios.php already computes that pair.
	 * The retired #8A5D00 was a one-off hex with no token behind it.
	 */
	.glass__form-tag {
		align-self: flex-start;
		font-family: var(--font-grotesk);
		font-style: normal;
		font-weight: var(--weight-bold);
		font-size: 0.8125rem;
		letter-spacing: 0.06em;
		text-transform: uppercase;
		padding: 0.4rem 0.7rem;
		border-radius: 0.4rem;
		background: var(--ground-amber);
		color: var(--ink-amber);
	}

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

	.glass__form-field label {
		font-size: 0.8125rem;
		letter-spacing: 0.05em;
		color: var(--paper-ink-2);
	}

	.glass__form-optional {
		font-weight: var(--weight-regular);
		letter-spacing: 0;
		text-transform: none;
		color: var(--paper-ink-3);
	}

	.glass__form-hint {
		font-size: 0.75rem;
		color: var(--paper-ink-3);
	}

	/*
	 * The per-field validation message (CONV-08).
	 *
	 * `--paper-ink` — 17.74:1 on the paper ground — and NOT the declared
	 * destructive red. base.css annotates that red as admin-only and
	 * outside the public contract, and it computes to roughly 4.10:1 here,
	 * under the 4.5:1 body-text floor this project's own harness enforces
	 * elsewhere. An error message is the one string on the page that must
	 * be readable by someone who is already frustrated; it does not get to
	 * be the least legible thing in the form.
	 *
	 * Weight and position carry the emphasis instead of hue, which also
	 * means the message does not depend on colour vision to be found.
	 */
	/*
	 * The honeypot's hiding place.
	 *
	 * Off-canvas, NOT `display: none` and NOT `visibility: hidden`. A bot
	 * sophisticated enough to be worth defending against checks whether a
	 * field is in the layout at all and skips the ones that are not; a
	 * field that is merely positioned a long way off-screen still reads as
	 * fillable. The one-pixel box keeps it from affecting anything.
	 *
	 * `aria-hidden` on the wrapper already removes it from the
	 * accessibility tree and `tabindex="-1"` from the tab order, so no real
	 * visitor — sighted, keyboard or screen reader — ever meets it.
	 */
	.glass__form-trap {
		position: absolute;
		left: -9999px;
		width: 1px;
		height: 1px;
		overflow: hidden;
	}

	/*
	 * Plan 03-04: the per-field error paragraph is now ALWAYS rendered
	 * (never conditionally, so enquiry-form.js can fill an element it did
	 * not create, D-18) — this rule is what keeps an empty one silent and
	 * invisible, the exact same pattern .glass__form-status:empty already
	 * established below.
	 */
	.glass__form-error:empty {
		display: none;
	}

	.glass__form-error {
		font-family: var(--font-grotesk);
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		line-height: 1.4;
		color: var(--paper-ink);
	}

	/*
	 * The failing field's own edge, deepened from --paper-ink-3 to
	 * --paper-ink. Same 0.14rem inset bar every input already carries —
	 * "a shape, not a border", the precedent panel__head::after set and
	 * D-02 requires — just at full ink, so the eye lands on the control
	 * that needs attention without a new colour entering the system.
	 */
	.glass__form-field--invalid input,
	.glass__form-field--invalid select,
	.glass__form-field--invalid textarea {
		box-shadow: inset 0 -0.14rem 0 var(--paper-ink);
	}

	/*
	 * The consent row is the one field whose control is a checkbox, and
	 * the generic rule above does not reach it (D-03-A forbids touching
	 * that control at all). Mark the row instead.
	 */
	.glass__form-field--consent.glass__form-field--invalid .glass__form-error {
		margin-block-start: var(--space-xs);
	}

	/*
	 * Empty until Phase 3 has something to announce. `role="status"` on an
	 * empty element is silent, so this costs a screen-reader user nothing
	 * in the meantime.
	 */
	.glass__form-status:empty {
		display: none;
	}

	.glass__form-status {
		font-family: var(--font-grotesk);
		font-size: 0.875rem;
		font-weight: var(--weight-bold);
		color: var(--paper-ink);
	}

	/*
	 * Item 2's chrome fix — approved chrome is `background:#EBEBE3;
	 * border:0; border-radius:.55rem`, replacing the retired 02.3 Berlin
	 * system's white fill + 1px black border + square corners this
	 * selector previously carried (a live D-02 violation). #EBEBE3 is a
	 * neutral field fill, not one of the five panel grounds, so it has no
	 * ink-step token of its own; `color: var(--paper-ink)` (17.74:1 on
	 * paper, and #EBEBE3 is lighter than paper so the ratio only improves)
	 * covers the typed value.
	 */
	/*
	 * ONE height for every control (quick task 260831-wey). The <select>
	 * was visibly shorter than the inputs beside it — a UA default nobody
	 * had overridden, and the kind of inconsistency that makes a form feel
	 * unfinished no matter how good the rest of it is. `block-size`, not
	 * `min-block-size`, so the select cannot shrink back to its intrinsic
	 * height; the textarea opts out below because it must be able to grow.
	 */
	.glass__form-field input,
	.glass__form-field select,
	.glass__form-field textarea {
		min-height: var(--control-h, 46px);
		padding-inline: var(--space-md);
		border: 0;
		border-radius: 0.55rem;
		background: #EBEBE3;
		color: var(--paper-ink);
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;

		/*
		 * #EBEBE3 on #F7F7F3 paper computes to 1.12:1 — WCAG 1.4.11 asks
		 * 3:1 of a non-text UI boundary, so these fields read as faint
		 * smudges rather than as things you type into, on the one form the
		 * business depends on.
		 *
		 * D-02 forbids the reflexive 1px border, so this borrows the
		 * system's own precedent instead: `panel__head::after` is a 0.14rem
		 * bar its docblock calls "a shape, not a border". #60625D on
		 * #EBEBE3 is 5.4:1.
		 */
		box-shadow: inset 0 -0.14rem 0 var(--paper-ink-3);
	}

	.glass__form-field input:not([type='checkbox']),
	.glass__form-field select {
		block-size: var(--control-h, 46px);
	}

	.glass__form-field textarea {
		padding-block: var(--space-sm);
		resize: vertical;
		line-height: 1.5;
	}

	.glass__form-field input:focus-visible,
	.glass__form-field select:focus-visible,
	.glass__form-field textarea:focus-visible {
		outline: 2px solid var(--color-focus-ring);
		outline-offset: 2px;
		box-shadow: inset 0 -0.14rem 0 var(--paper-ink);
	}

	.glass__form-field--consent label {
		display: flex;
		align-items: flex-start;
		gap: var(--space-sm);
		text-transform: none;
		font-family: var(--font-grotesk);
		font-size: 0.75rem;
		letter-spacing: 0;
		color: var(--paper-ink-2);
	}

	/*
	 * The generic `.glass__form-field input { min-height: 44px }` rule above
	 * caught the checkbox too, stretching a 13px-wide box to 44px tall while
	 * `align-items: flex-start` pinned it to the top — so the tick floated
	 * well below the first line of the sentence it belongs to. The label
	 * wraps the input, so the hit area was always fine; the optics were not,
	 * on the one control that is a legal gate.
	 */
	.glass__form-field--consent input {
		flex-shrink: 0;
		inline-size: 1.25rem;
		block-size: 1.25rem;
		min-height: 0;
		margin-top: 0.1rem;
		box-shadow: none;
		accent-color: var(--ground-green);
	}

	/*
	 * The cascading second choice hides itself only once a script has
	 * decided the chosen programme has no courses of its own. A CLASS,
	 * not the `hidden` property — panels.js is the only file permitted
	 * to write that, and test-nojs-content.php check 3 enforces it. With
	 * no script the field stays visible carrying every course, which is
	 * a complete form rather than a broken one.
	 *
	 * This rule used to live inside the `@media (min-width: 1000px)` fit
	 * ladder below, so below that width nothing hid the field — a phone
	 * visitor whose chosen programme had no courses saw the field, tapped
	 * it, and found it empty. Unconditional here, it hides at every width.
	 */
	.glass__form-cascade.is-empty {
		display: none;
	}

	/*
	 * This was 44px tall at 15px type — the accessibility FLOOR, which is not
	 * a design decision — while `.panel__cta` ("Více informací", a mere
	 * disclosure) was 60px at 19px and `.overlay__close` ("go back") was a
	 * 57px full-width black slab. The site's one revenue action was the
	 * smallest and quietest of the three.
	 *
	 * The two-layer drop shadow is deleted rather than tuned. It was the one
	 * element in the build importing another system's vocabulary — a
	 * soft-shadowed rounded pill — into a design whose own docblocks say its
	 * depth vocabulary is flat tonal steps and overlap, and which refuses
	 * gradients and hairlines everywhere else.
	 */
	.glass__form-submit {
		min-height: calc(var(--tap-target-min, 44px) + var(--space-md));
		border: 0;
		border-radius: 0.6rem;
		padding-inline: var(--space-lg);
		background: var(--ground-green);
		color: var(--ink-green);
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 1.0625rem;
		letter-spacing: 0.01em;
	}

	/*
	 * The sending affordance (Plan 03-04). enquiry-form.js adds
	 * `.is-sending` to the `<form>` element itself — never to the button
	 * directly, so any future descendant rule can key on it without the
	 * script needing to know which control carries the busy look — and
	 * sets the button's own `disabled` property, which already switches it
	 * to the neutral `.glass__form-submit[disabled]` fill declared above.
	 * `cursor: wait` is the one thing added here rather than relying on
	 * `[disabled]` alone: it is the one signal that reads as "in
	 * progress" rather than "unavailable" without introducing an
	 * `opacity` declaration test-contrast-ratios.php's check 2 would
	 * reject on a non-decorative selector, or a new colour token.
	 */
	.is-sending .glass__form-submit {
		cursor: wait;
	}

	/*
	 * The enquiry card's call line (CONV-07, D-P3-15) — ONE element, two
	 * Czech readings by gate state (see glass-pane.php's own docblock at
	 * its render site). When the gate is closed this IS the launch-gate
	 * closed state (Phase 3, D-P3-01 / 03-CONTEXT.md D-03-B) — the
	 * INTENDED production appearance for the whole interval between Phase
	 * 3 and Phase 5, not an error state — so it reads as a quiet
	 * redirection to the phone and the mailbox, at the same weight as the
	 * reply note beneath it, and never as an alarm. No accent as ink
	 * (DESIGN-10): #393C36 on paper is 10.44:1.
	 */
	.glass__form-call {
		font-size: 0.8125rem;
		line-height: 1.5;
		color: var(--paper-ink-2);
	}

	.glass__form-call a {
		color: var(--paper-ink);
	}

	/*
	 * Disabled submit: the same #EBEBE3 neutral field fill the inputs
	 * already use, so the button visibly stops being the green thing you
	 * press. #393C36 on #EBEBE3 is 9.35:1 — a disabled control is exempt
	 * from WCAG 1.4.3 but this one still has to be READABLE, because it is
	 * the label explaining what the form would do if it were open.
	 */
	.glass__form-submit[disabled] {
		background: #EBEBE3;
		color: var(--paper-ink-2);
		cursor: not-allowed;
	}

	.glass__form-reply-note {
		text-align: center;
		font-size: 0.75rem;
		font-weight: var(--weight-bold);
		color: var(--paper-ink-2);
	}

	/*
	 * D-07.1-10: on a phone this block is HIDDEN — the address, the
	 * `tel:` link and the statutory disclosure are carried by
	 * template-parts/site-footer.php's own `site-footer__contact`/
	 * `site-footer__disclosure` instead (front-page.php's dispatch,
	 * `with_contact => true`/`with_disclosure => true`). `display: none`
	 * here — the base (mobile-first) rule — is restored to `display: flex`
	 * inside the `@media (min-width: 1000px)` block below, where this
	 * pair's OWN markup is still the one that renders: footer.css hides
	 * `site-footer__contact` at the SAME 1000px boundary, so the two
	 * halves are exact complements and exactly one copy of the address/
	 * phone is ever in the accessibility tree. The pane's own markup
	 * (glass-pane.php) is untouched by this move — only which copy paints
	 * changed. See glass-pane.php's own rewritten comment for the reason
	 * this pair's disclosure sub-rule (`.glass__footer-legal`, further
	 * down this file) stays `display: none` at every desktop height
	 * regardless: this rule's own desktop restore does not reach it.
	 */
	.glass__footer {
		display: none;
		margin-top: auto;
		padding-top: var(--space-lg);
		flex-direction: column;
		gap: var(--space-xs);
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		color: var(--paper-ink-2);
	}

	.glass__footer-phone {
		color: var(--paper-ink);
	}

	/*
	 * ---------------------------------------------------------------
	 * THE FIT LADDER LIVES AT THE END OF THIS FILE ON PURPOSE.
	 *
	 * It overrides base rules declared above at EQUAL specificity, so
	 * source order is the only thing deciding the winner. Placed before
	 * them (as it was) every tier silently lost: the form kept its 16px
	 * gap while the tier asked for 4px, and the pane overflowed by 64px
	 * that nothing in the ladder could reclaim. Found by reading the
	 * computed gap, not this file. Do not move it back up.
	 * ---------------------------------------------------------------
	 */
	/*
	 * The fit ladder.
	 *
	 * Measured first, then written. At full chrome the pane holds ~1480px
	 * of content, which fits no viewport anyone actually has — so the
	 * compact state is the DEFAULT for the fixed pane, not a short-screen
	 * exception. The height queries below only handle what is still too
	 * tall after that.
	 *
	 * Order of sacrifice, and it never varies: decoration, then supporting
	 * copy, then secondary chrome. The five enquiry controls, the consent
	 * checkbox, the submit and the tap-to-call link are never touched at
	 * any height — the form is the core value (02.7 D-01), consent is a
	 * legal requirement, and CONV-07 owns the phone route.
	 */
	@media (min-width: 1000px) {
		:root {
			--pane-inset: clamp(10px, 1.1vw, 20px);
			--pane-radius: 22px;
			/*
			 * The pane's four groups — brand, headline, form, footer — are
			 * separated by real air. Its FIELDS stay tight to each other.
			 *
			 * The previous pass set both to the same small value and the
			 * result read as crammed rather than editorial: an enormous
			 * headline sitting on top of a dense block with no hierarchy
			 * of distance. Editorial spacing is a CONTRAST between gaps,
			 * not more of them everywhere, so the group gap goes up while
			 * the field gap stays where it is.
			 */
			--pane-gap: var(--space-2xl);
			--pane-pad: var(--space-xl);
		}

		/*
		 * D-07.1-10: the desktop restore. Above 1000px the pane's own
		 * address/phone pair renders exactly as it always has — this move
		 * is phone-scoped for the CONTACT block; the statutory disclosure
		 * sub-rule (`.glass__footer-legal`, further down this file) is
		 * NOT restored here and stays `display: none` at every desktop
		 * height, which is the pre-existing, unedited fit-ladder decision
		 * this task is not re-litigating — the disclosure's one visible
		 * copy above 1000px is the site footer's, not this one.
		 */
		.glass__footer {
			display: flex;
		}

		/*
		 * The quote's desktop restore, paired with its phone hide above.
		 * `block` is a `<figure>`'s own initial display, so this restores
		 * exactly what the pane rendered before the phone copy existed —
		 * it does not impose a new layout on the desktop pane. The
		 * `max-height: 1040px` fit tier further down still hides it on
		 * short desktop viewports; that pre-existing decision is untouched.
		 */
		.glass__quote {
			display: block;
		}

		/*
		 * Field PAIRS share a row. Name and phone are asked together in
		 * every form a person has ever filled in, and programme and its
		 * course are one question in two parts — pairing them is how the
		 * form reads, and it also buys back ~140px of pane height, which
		 * is what pays for the editorial air everywhere else.
		 */
		.glass__form-row {
			display: grid;
			grid-template-columns: 1fr 1fr;
			gap: var(--space-sm);
			align-items: start;
		}

		/* Default compact state — applies at every height, because the
		   uncompacted pane fits none of them. */
		.glass__pitch-body { display: none; }

		/*
		 * The headline now carries a whole sentence when a programme's
		 * claim is on screen, not just the hero's short pitch — it went
		 * from two lines to four and pushed the form down the pane. It is
		 * capped at three lines and set a step smaller so the form's
		 * position is stable no matter which claim is showing, and so a
		 * client writing a long one cannot shove the submit button out of
		 * the tile.
		 */
		/*
		 * The headline is retyped character by character as the section
		 * changes, so its height would otherwise grow line by line and
		 * shove the whole form down the pane while the animation ran —
		 * "as the title gets written, it moves the rest of the page under
		 * it". Its box is now RESERVED at the clamped three lines, so the
		 * text fills a space that was already there and nothing below it
		 * ever moves. Three lines is the same number the clamp caps at, so
		 * no claim can outgrow the reservation either.
		 */
		.glass__pitch-heading {
			font-size: clamp(1.3rem, 2vw, 1.7rem);
			line-height: 1.14;
			display: -webkit-box;
			-webkit-line-clamp: 3;
			-webkit-box-orient: vertical;
			overflow: hidden;
			min-height: calc(1.14em * 3);
		}
		.glass__form { gap: var(--space-sm); }
		/*
		 * The heading and its tag sit on one line where there is room and
		 * wrap where there is not. `nowrap` plus a negative bottom margin
		 * pulled the amber tag up INTO the serif heading — they collided.
		 * Found by rendering.
		 */
		.glass__form-heading {
			flex-wrap: wrap;
			align-items: baseline;
			gap: var(--space-sm);
			margin-block-end: var(--space-xs);
		}
		.glass__form-tag { white-space: nowrap; }
		.glass__form-field textarea {
			min-height: var(--tap-target-min, 44px);
			height: var(--tap-target-min, 44px);
		}
		.glass__footer-legal { display: none; }

		/*
		 * Each tier is sized so that it FITS its own range. Editorial air
		 * is not free: measured on the worst-case claim, the generous
		 * default needs ~1300px of viewport, --space-xl needs ~1210 and
		 * --space-lg needs ~1140. Giving every height the generous value
		 * would have pushed the no-scroll boundary from 1140 to 1300,
		 * which is a height almost nobody has — the air would have been
		 * paid for by everyone losing the behaviour.
		 */
		@media (max-height: 1299px) {
			:root { --pane-gap: var(--space-xl); --pane-pad: var(--space-lg); }
		}

		@media (max-height: 1205px) {
			:root { --pane-gap: var(--space-lg); }
		}

		@media (max-height: 1180px) {
			:root { --pane-gap: var(--space-md); }
		}

		@media (max-height: 1040px) {
			.glass__form-reply-note { display: none; }
			.glass__quote { display: none; }
			.glass__footer-address { display: none; }
			:root { --pane-gap: var(--space-lg); --pane-pad: var(--space-md); }
			.glass__form { gap: var(--space-xs); }
		}

		/* below this the air is gone and only the fit ladder is left */
		@media (max-height: 900px) {
			:root { --pane-gap: var(--space-sm); }
		}

		@media (max-height: 940px) {
			/* labels sit tight to their controls; the 44px tap target on
			   every control is untouched */
			.glass__form-field { gap: 1px; }
			.glass__form-field label { font-size: 0.625rem; }
			.glass__form-submit { min-height: var(--tap-target-min, 44px); }
			.glass__pitch-heading { font-size: clamp(1.2rem, 1.9vw, 1.55rem); }
		}

		/*
		 * The release: on the closing panel the pane slides out to the left
		 * and hands the viewport over. `visibility` follows the transform
		 * so the form is not a focusable ghost sitting off-screen.
		 */
		/*
		 * The release (quick task 260901c). The owner asked for the pane to
		 * "get stuck on the page with the last split section, no animation
		 * needed, just stays on there with the scroll" — so it stops being
		 * fixed and simply scrolls away with the last programme, instead of
		 * sliding out from under the reader.
		 *
		 * `position: absolute` against the panel stack does exactly that: it
		 * parks where it last was and the page carries it up. No transform,
		 * no transition, nothing to watch — which is the point.
		 */
		body.is-released .glass {
			/*
			 * A parked pane must never eat the wheel. `.glass` carries
			 * `overscroll-behavior: contain` for its fixed state, which
			 * deliberately stops scroll chaining to the page — correct
			 * while it is a scrollable panel pinned over the content, and
			 * exactly wrong once it is an ordinary block the page is
			 * carrying: the reader hits it on the way past and the page
			 * stops. Reported as "it stops me from scrolling".
			 */
			overflow: hidden;
			overscroll-behavior: auto;
			position: absolute;
			inset-block-start: var(--pane-park, 0);
			inset-block-end: auto;
			block-size: calc(100dvh - var(--pane-inset) * 2);
		}

		/*
		 * The safety valve, at the MEASURED fit boundary.
		 *
		 * !!! THIS NUMBER IS A MEASUREMENT, NOT A PREFERENCE. !!!
		 * It must be re-derived every time anything is added to or removed
		 * from the pane. `overflow: hidden` on a pane that overflows CLIPS
		 * the submit button off the bottom of the tile, and a form whose
		 * end you cannot reach is worse than a scrollbar.
		 *
		 * It has gone stale three times: 1085 -> 1139 -> 1145, each time
		 * because something was added to the pane and nothing re-measured.
		 * The failure is silent by construction — `overflow: hidden` on a
		 * pane that overflows crops the submit button off the bottom of the
		 * tile with no visible symptom.
		 *
		 * This pass moved it the other way, and a long way: pairing name
		 * with phone onto one row, giving every control one height, and
		 * (the big one) moving this ladder to the END of the file so its
		 * tiers stop losing on source order. The form's own gap had been
		 * computing 16px while the tier asked for 4px, which alone was 64px
		 * of unreclaimable overflow.
		 *
		 * Re-derived against the worst case the content can produce — the
		 * longest stored claim AND the cascade open: 860px -> 0, 820px -> 0,
		 * 800px -> 10px over. The valve sits at 819px.
		 */
		@media (max-height: 819px) {
			.glass { overflow-y: auto; }
		}

		/*
		 * Phase 07, Plan 01 (D-07-01/resolved_discretion): the pane's own
		 * brand row and pitch are hidden below 1000px (base rules above,
		 * `.glass__brand`/`.glass__pitch` at `display: none;`) — the
		 * mobile lead (template-parts/mobile-lead.php) carries the same
		 * logo through the same classroompros_render_logo() call, and its
		 * own `mobil.pitch` field replaces `.glass__pitch`'s role, so
		 * nothing here vanishes, it moves. Restored to their CURRENT
		 * computed display values (read from this file, not guessed):
		 * `flex` for `.glass__brand`, `grid` for `.glass__pitch`. Placed
		 * HERE — inside this same min-width:1000px block, at the END of
		 * the file — rather than beside the earlier `.glass { position:
		 * fixed; ... }` min-width block above, for the identical reason
		 * the fit ladder itself lives here and not there: at equal
		 * specificity, source order alone decides the winner, and the
		 * base `.glass__brand`/`.glass__pitch` rules (declared further
		 * down THIS file, around line 120) would otherwise win over an
		 * earlier restore rule even inside a matching media query,
		 * silently hiding the brand and pitch at every desktop width too.
		 */
		.glass__brand {
			display: flex;
		}

		.glass__pitch {
			display: grid;
		}
	}

}

/* SOURCE: components/field.css */
/*
 * field.css — the fixed background photographic field (quick task
 * 260831-v19, ported from `.planning/sketches/004-hloubka/`).
 *
 * Owner: "I like the idea of the images in the background ... the
 * background paralax effect".
 *
 * Every card here is a photograph one of the detail docks already shows.
 * The field sits BEHIND the panel stack, and each panel carries its own
 * ground at less than full opacity so the field reads through it. That
 * layering is what lets a photograph and a Czech headline share the same
 * space without the photograph ever landing on the type — the sketch
 * proved by rendering that a masked-beside arrangement does not survive a
 * full-width panel.
 *
 * Contrast note: panel alpha changes, the ground/ink PAIRS do not. Behind
 * every semi-transparent panel sits an opaque base of that same ground
 * (`.panels::before`), so the composited colour under the type is the
 * ground itself and every ratio computed in 02.5-CONTRAST.md still holds
 * unchanged. test-contrast-ratios.php reads the token pairs, which are
 * untouched.
 *
 * No @keyframes here — the drift is one rAF in field.js that parks when
 * idle, per this project's zero-idle-cost rule.
 */

@layer components {
	.photo-field {
		position: fixed;
		inset: 0;
		z-index: 0;
		overflow: hidden;
		pointer-events: none;
		/* the opaque base plate — field.js sets --live-ground from the
		   live panel's own --panel-bg, so this is always the same colour
		   the semi-transparent panel above it is painting */
		background: var(--live-ground, var(--ground-green));
		transition: background 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
		/* below the panel stack (.panels is positioned) and far below the
		   pane (.glass, z-index 10) */
	}

	.photo-field__card {
		position: absolute;
		inset-inline-start: var(--x);
		inset-block-start: var(--y);
		inline-size: var(--w);
		margin: 0;
		border-radius: 0.7rem;
		overflow: hidden;
		/* a solid plate under each photograph so a card reads as an object
		   floating in depth rather than a ghost tinted by whatever ground
		   happens to be behind it */
		background: #0b0f0b;
		transform: translate3d(var(--px, 0), var(--py, 0), 0)
			scale(var(--s, 1)) rotate(var(--r, 0deg));
		opacity: var(--o, 0.3);
		filter: blur(var(--b, 2px));
		box-shadow: 0 24px 60px -18px rgb(0 0 0 / 0.55);
		transition: opacity 0.38s cubic-bezier(0.22, 0.61, 0.36, 1),
			filter 0.38s cubic-bezier(0.22, 0.61, 0.36, 1);
		will-change: transform, opacity;
	}

	.photo-field__card img {
		inline-size: 100%;
		block-size: 100%;
		aspect-ratio: 4 / 3;
		object-fit: cover;
		opacity: var(--io, 0.7);
		transition: opacity 0.38s cubic-bezier(0.22, 0.61, 0.36, 1);
	}

	/*
	 * Dormant cards are genuinely FAR: dim and heavily blurred. The
	 * separation between dormant and live has to be large — the sketch
	 * rendered a dozen cards at 0.42 and the whole field read as coloured
	 * fog rather than as depth.
	 */
	.photo-field__card[data-band='far'] {
		--s: 0.5;
		--o: 0.28;
		--b: 4px;
		--io: 0.6;
	}

	.photo-field__card[data-band='mid'] {
		--s: 0.7;
		--o: 0.38;
		--b: 2.4px;
		--io: 0.7;
	}

	.photo-field__card[data-band='near'] {
		--s: 0.86;
		--o: 0.48;
		--b: 1.4px;
		--io: 0.8;
	}

	/* the programme currently on screen has its own cards lifted forward */
	.photo-field__card.is-live[data-band='far'] {
		--o: 0.62;
		--b: 1px;
		--s: 0.58;
		--io: 0.9;
	}

	.photo-field__card.is-live[data-band='mid'] {
		--o: 0.78;
		--b: 0.3px;
		--s: 0.8;
		--io: 1;
	}

	.photo-field__card.is-live[data-band='near'] {
		--o: 0.92;
		--b: 0;
		--s: 1;
		--io: 1;
	}

	/*
	 * Below the split the pane is a header band and the panels flow under
	 * it, so a fixed field would sit behind body copy rather than beside
	 * it. It is dropped outright rather than dimmed: on a phone this is
	 * pure cost — bytes, decode time and compositing — for an effect the
	 * layout can no longer stage.
	 */
	@media (max-width: 999px) {
		.photo-field {
			display: none;
		}
	}

	/*
	 * On the closing panel's cream ground the dormant cards stop being
	 * depth and start being smudges: a dark plate at low opacity behind
	 * warm paper reads as a printing fault, not as distance. Photography
	 * there has to be deliberate or absent, so all but the closing panel's
	 * own images recede to nothing.
	 */
	body.is-released .photo-field__card:not(.is-live) {
		/* through --o, the depth property every other rule in this file
		   uses — a literal `opacity` here is what test-contrast-ratios.php
		   check 2 forbids on a non-pseudo selector, and it is right to:
		   that is how faded body text gets shipped by accident */
		--o: 0.03;
	}

	@media (prefers-reduced-motion: reduce) {
		.photo-field__card {
			--px: 0 !important;
			--py: 0 !important;
		}
	}
}

/* SOURCE: components/button.css */
@layer components {
	/*
	 * THE button (quick task 260831-wey). The owner picked the programme
	 * panel's own control as the house style — a filled slab in the
	 * ground's ink with the ground's colour as the label — and asked for
	 * every other primary control to match it: "Zpět na programy button and
	 * the mam zajem button should be styled like this ... and always on the
	 * same spot."
	 *
	 * So it is defined ONCE here and worn by all of them, rather than three
	 * near-identical rules drifting apart in three files. Each surface
	 * supplies its own ink/ground pair through --btn-bg/--btn-fg, which are
	 * the SAME already-computed pairs test-contrast-ratios.php proves at
	 * >= 4.5:1 for every ground — the roles are swapped, and relative
	 * luminance contrast is symmetric, so no new pair needs computing.
	 */
	.c-btn {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: var(--space-sm);
		min-height: calc(var(--tap-target-min, 44px) + var(--space-md));
		padding-block: var(--space-md);
		padding-inline: var(--space-2xl);
		border: 0;
		border-radius: 0.75rem;
		background: var(--btn-bg, var(--panel-fg, var(--ink-green)));
		color: var(--btn-fg, var(--panel-bg, var(--ground-green)));
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold, 700);
		font-size: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);
		letter-spacing: 0.005em;
		line-height: 1.15;
		text-align: center;
		text-decoration: none;
		cursor: pointer;
		transition: transform 0.16s cubic-bezier(0.2, 0.7, 0.3, 1),
			box-shadow 0.16s ease;
	}

	.c-btn:hover {
		transform: translateY(-1px);
		box-shadow: 0 8px 22px -10px rgb(10 14 9 / 0.5);
	}

	.c-btn:active {
		transform: translateY(0);
	}

	/*
	 * Phase 04 (A11Y-03 / T-04-15): this used to fall back to the element's
	 * own current-text-colour keyword (CSS's "inherit my own `color`"
	 * value) — the button's own LABEL colour, which resolves through the mirror
	 * chain `var(--btn-fg, var(--panel-bg, var(--ground-green)))` and on
	 * every panel is that panel's own GROUND. A ring painted 3px outside
	 * the button, in the colour of the ground it sits on, is invisible —
	 * the same class of defect as the 1.06:1 night-ground ring Phase 02.6
	 * found by computing (panels.css:529-545's own docblock). It never
	 * showed up in test-contrast-ratios.php's check 1 because it never
	 * appeared there as a declared ground/ink PAIR — only as a fallback
	 * chain a human would have had to trace by hand.
	 *
	 * Repaired to the SAME fallback chain `background` above already uses.
	 * The ring is now always the slab's own fill colour — a value check 1
	 * already proves clears >=4.5:1 against its ground for every one of
	 * the six surfaces, which mathematically also clears the 3:1 non-text
	 * floor test-contrast-ratios.php's new check 5 computes directly.
	 */
	.c-btn:focus-visible {
		outline: 3px solid var(--btn-bg, var(--panel-fg, var(--ink-green)));
		outline-offset: 3px;
	}

	.c-btn__arrow {
		font-size: 1em;
		line-height: 1;
	}

	/*
	 * Every surface declares the pair EXPLICITLY rather than letting the
	 * button guess. It used to fall back through
	 * `var(--panel-bg, var(--ground-green))`, and the dock ground
	 * modifiers set `--panel-fg` but never `--panel-bg` — so on an amber
	 * detail view both buttons came out with GREEN text. The label must
	 * always be the colour of the ground it is sitting on; a fallback that
	 * silently reaches for another ground's colour is the bug, not the
	 * missing token.
	 */
	.panel {
		--btn-bg: var(--panel-fg);
		--btn-fg: var(--panel-bg);
	}

	.dock {
		--btn-bg: var(--dock-fg);
		--btn-fg: var(--dock-bg);
	}

	.overlay--pane {
		--btn-bg: var(--paper-ink);
		--btn-fg: var(--paper);
	}

	/* the paper pane's own pair, since it has no --panel-* to inherit */
	.glass .c-btn {
		--btn-bg: var(--paper-ink);
		--btn-fg: var(--paper);
	}
}

/* SOURCE: components/sitemap.css */
/*
 * sitemap.css — the floating section control (quick task 260901).
 *
 * It sits on PAPER, not on the live ground. That is the whole reason it is
 * always legible: --paper/--paper-ink is a computed 17.74:1 pair, so the
 * control reads identically over green, coral, indigo, amber, night and
 * paper without a single ground-dependent value and without one opacity
 * declaration for test-contrast-ratios.php to compute.
 *
 * The same control serves desktop and mobile. On a phone the split is gone
 * and this is the page's only navigation, which is why it names where you
 * are instead of marking it.
 *
 * Phase 07.1, Plan 02 (D-07.1-04): the mobile action bar
 * (components/mobile-actions.css's `.m-actions`) moves off the TOP edge
 * (Phase 07, Plan 02's D-07-10 arrangement) and now shares this control's
 * own BOTTOM edge — TWO floating controls stack there at the owner's
 * request (D-07.1-10, Phase 07.1 plan 05, reduced this from three), the
 * exact geometry that got an earlier dot-map control scrapped on
 * 2026-09-01 for overlapping tap targets. `--secnav-inset` below is
 * promoted to a token specifically so `.m-actions` can compute its own
 * clearance from the SAME value this file declares, rather than a second
 * copy of the literal. cookie-banner.css's own stacking-offset comment
 * (derived from this file's z-index and bottom clearance) still reads
 * exactly this clamp() value and this element's toggle height — now
 * reached through a token rather than a literal — so that file needs no
 * change; see tools/tests/browser/mobile-ladder.mjs for the two-state
 * (open/closed) adjacency proof this arrangement now requires.
 *
 * D-07.1-07 (Phase 07.1, plan 05, Task 3): `.secnav__toggle` carries the
 * same small idle drift `.m-actions__act` does, driven by the ONE shared
 * `@keyframes cp-float-drift` components/mobile-actions.css declares —
 * referenced here by name rather than redeclared, since a `@keyframes`
 * at-rule is document-scoped and needs no particular import order. This
 * control's own `--float-period`/`--float-delay` are DIFFERENT values
 * from `.m-actions__act`'s own (the owner's own discretion note: the two
 * must not pulse in unison). PHONE ONLY: `.secnav` serves the desktop
 * layout too, so the drift is cancelled inside this file's own existing
 * `@media (min-width: 1000px)` block, beside the rule that already
 * re-positions `.secnav` clear of the fixed pane.
 */

@layer components {
	/*
	 * Declared once, here, and read by both this file and
	 * components/mobile-actions.css — the literal clamp() value must never
	 * be duplicated (D-07.1-04); see this file's own docblock.
	 */
	:root {
		--secnav-inset: clamp(14px, 2.2vh, 26px);
	}

	.secnav {
		position: fixed;
		z-index: 40;
		inset-block-end: var(--secnav-inset);
		inset-inline: 0;
		/*
		 * D-07-10: clears the iOS home indicator. Additive to the clamp()
		 * above, not a replacement for it — cookie-banner.css's own
		 * stacking-offset arithmetic reads ONLY the clamp() value and
		 * this element's toggle height, so this safe-area padding must
		 * never be folded into that clamp() itself.
		 */
		padding-block-end: env(safe-area-inset-bottom, 0px);
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--space-sm);
		pointer-events: none;
	}

	/* clear of the fixed pane, so it centres on the content it describes */
	@media (min-width: 1000px) {
		.secnav {
			inset-inline-start: 46vw;
		}

		/*
		 * D-07.1-07: PHONE-ONLY. `.secnav` (unlike `.m-actions`) serves the
		 * desktop layout too, so its drift — declared in the mobile-first
		 * base above — is explicitly cancelled here rather than relying on
		 * a `display: none` this control never gets above this width.
		 */
		.secnav__toggle {
			animation: none;
		}
	}

	.secnav__toggle,
	.secnav__list {
		pointer-events: auto;
		background: var(--paper);
		color: var(--paper-ink);
		border: 0;
		border-radius: 999px;
		box-shadow: 0 10px 30px -8px rgb(10 14 9 / 0.42),
			0 2px 6px rgb(10 14 9 / 0.16);
	}

	.secnav__toggle {
		display: inline-flex;
		align-items: center;
		gap: var(--space-sm);
		min-height: var(--tap-target-min, 44px);
		padding: 0 var(--space-lg);
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;
		font-weight: var(--weight-bold, 700);
		cursor: pointer;
		max-inline-size: min(88vw, 30rem);
		/*
		 * D-07.1-07: the idle drift, phone-only (cancelled below this
		 * file's own `@media (min-width: 1000px)` block). A different
		 * period and a different delay from `.m-actions__act`'s own
		 * (components/mobile-actions.css) — declared as tokens, not
		 * repeated numbers, so "the two do not pulse in unison" is a fact
		 * about this file rather than a claim about how it looked once.
		 * Same 2px amplitude, same reasoning: components/mobile-actions.css's
		 * own `.m-actions__act` rule records the measured 8px gap between
		 * the two controls and the margin this amplitude leaves against it.
		 */
		--float-amp: 2px;
		--float-period: 7.2s;
		--float-delay: 1.3s;
		animation: cp-float-drift var(--float-period) ease-in-out var(--float-delay) infinite alternate;
	}

	.secnav__pos {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: 1.25rem;
		line-height: 1;
		color: var(--paper-ink-2);
	}

	.secnav__name {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.secnav__chev {
		margin-inline-start: auto;
		padding-inline-start: var(--space-sm);
		font-size: 0.7rem;
		line-height: 1;
		color: var(--paper-ink-2);
		transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
	}

	.secnav__chev::before {
		content: '▲';
	}

	.secnav.is-open .secnav__chev {
		transform: rotate(180deg);
	}

	.secnav__toggle:focus-visible,
	.secnav__go:focus-visible,
	.secnav__more:focus-visible {
		outline: 3px solid var(--paper-ink);
		outline-offset: 2px;
	}

	/* ---- the list ---------------------------------------------------- */

	/*
	 * Closed by SCALE and visibility rather than `display`, so the open has
	 * something to animate, and by `visibility` rather than opacity so
	 * nothing here is a fade for check 2 to refuse. Closed it is also
	 * removed from the tab order, which `visibility: hidden` does for free.
	 */
	.secnav__list {
		border-radius: 1.25rem;
		padding: var(--space-sm);
		inline-size: min(92vw, 26rem);
		/*
		 * D-07-10: `svh` (small viewport height) rather than `vh`, so a
		 * phone's dynamic toolbar (which shrinks the LARGE viewport as it
		 * hides/shows) cannot push this list's own max size past what is
		 * actually visible at the toolbar's largest extent.
		 */
		max-block-size: min(60svh, 30rem);
		overflow-y: auto;
		visibility: hidden;
		transform: translateY(10px) scale(0.97);
		transform-origin: 50% 100%;
		transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1),
			visibility 0s linear 0.24s;
	}

	.secnav.is-open .secnav__list {
		visibility: visible;
		transform: none;
		transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1),
			visibility 0s;
	}

	.secnav__item {
		display: flex;
		align-items: stretch;
		gap: 2px;
		border-radius: 0.7rem;
	}

	.secnav__item + .secnav__item {
		margin-block-start: 2px;
	}

	.secnav__go {
		flex: 1 1 auto;
		display: flex;
		align-items: center;
		gap: var(--space-md);
		min-height: var(--tap-target-min, 44px);
		padding: var(--space-sm) var(--space-md);
		border: 0;
		border-radius: 0.7rem;
		background: none;
		color: inherit;
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;
		font-weight: var(--weight-bold, 700);
		text-align: start;
		cursor: pointer;
	}

	.secnav__n {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: 1.0625rem;
		line-height: 1;
		color: var(--paper-ink-2);
		flex: none;
	}

	.secnav__t {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.secnav__more {
		flex: none;
		inline-size: var(--tap-target-min, 44px);
		min-height: var(--tap-target-min, 44px);
		border: 0;
		border-radius: 0.7rem;
		background: none;
		color: var(--paper-ink-2);
		font-size: 1rem;
		cursor: pointer;
	}

	.secnav__more::before {
		content: '→';
	}

	.secnav__go:hover,
	.secnav__more:hover {
		background: rgb(13 17 11 / 0.07);
	}

	/*
	 * The section you are in is marked by a real fill, not a fade — the
	 * same ink/paper pair inverted, which is the pair the pane already
	 * uses and which needs no new contrast computation.
	 */
	.secnav__item.is-live .secnav__go {
		background: var(--paper-ink);
		color: var(--paper);
	}

	.secnav__item.is-live .secnav__n {
		color: var(--paper);
	}

	@media (prefers-reduced-motion: reduce) {
		.secnav__list,
		.secnav__chev {
			transition-duration: 0.01ms;
		}

		/*
		 * D-07.1-07 / Phase 07 success criterion §6: silences the idle
		 * drift completely — `.secnav__toggle` named verbatim, per
		 * tools/tests/test-a11y-structure.php check 5, which treats a
		 * silencing rule that does not name the selector it silences as
		 * absent. The control is still present, still tappable, still in
		 * the same resting place; it simply does not move.
		 */
		.secnav__toggle {
			animation: none;
		}
	}
}

/* SOURCE: components/mobile.css */
/*
 * mobile.css — the mobile-only lead (Phase 07, Plan 01, D-07-01/D-07-03/
 * D-07-06/D-07-07; hero rebuilt Phase 07.1, Plan 04, D-07.1-01/D-07.1-02
 * — the owner's own phone walk). Hand-authored, native nesting, no
 * preprocessor, no bundler (D-16). Wrapped entirely in the `components`
 * layer declared by main.css, imported immediately after
 * components/sitemap.css: it reads `--ground-green`/`--ink-green`,
 * `--paper`/`--paper-ink`, `--font-condensed` and the `--space-*` tokens
 * from base.css (imported first), and it must be read BEFORE
 * components/panels.css so nothing in the panel stack can win a
 * specificity tie against this file's own `.m-lead` visibility rule.
 *
 * Authored mobile-first, per D-07-06/MOBILE-02: the base rules ARE the
 * phone rules. A single `@media (min-width: 1000px)` block hides the
 * whole mobile lead — never a `max-width` query fighting the project's
 * established mobile-first `min-width: 1000px` breakpoint.
 *
 * `.m-hero` now sits on `--ground-green`/`--ink-green` (D-07.1-01) — the
 * audited 9.81:1 pair base.css:127-128 already declares, reused by name,
 * never re-minted. This is the FIRST place in this project's mobile
 * layout where the brand neon is load-bearing, and it is legitimate here
 * for exactly one reason: it is a GROUND carrying dark green ink, never
 * text and never a fill. DESIGN-01 separately measures the same hex on
 * WHITE at 1.74:1 and calls it "structurally incapable of carrying a
 * button fill or any text" — that finding is about the accent as
 * foreground, and does not conflict with using it as a background here.
 * `.m-about` stays on `--paper`/`--paper-ink` — the same pair `.secnav`
 * (components/sitemap.css) already uses, a computed 17.74:1 pair with no
 * ground-dependent value — so this file now carries exactly two
 * ground/ink pairs, both audited, both used by name.
 *
 * `.m-lead`'s `--m-bar-h: 0px` stays declared even though nothing in this
 * file's own padding still reads it (Plan 07.1-02 moved the conversion
 * bar to the bottom and deleted its top-clearance redeclaration, so the
 * token now only ever resolves to its own `0px` default) — dock.css
 * reads `var(--m-bar-h, 0px)` as a scroll margin, so deleting the
 * declaration here would silently change dock.css's own fallback
 * behaviour rather than simplify anything.
 *
 * No `opacity` on any text selector in this file. Task 2 (Phase 07, Plan
 * 01) added the flowing name band's own `transform`-only keyframe
 * (`cp-mobile-band-flow`) and this file's own
 * `@media (prefers-reduced-motion: reduce)` branch — required by
 * test-a11y-structure.php check 5 the moment a file declares unguarded
 * `animation`/`transition`, and deliberately NOT deferred to motion.css's
 * own shared branch: motion.css's docblock states it is the one place
 * THAT design's motion is silenced, and this is a Phase 07 addition to a
 * different component. Phase 07.1 Plan 04 scales the band up and moves
 * it behind the logo but does not touch the mechanism itself: still
 * `transform`-only, still the same keyframe, still silenced the same way
 * under reduced motion.
 */

@layer components {
	.m-lead {
		--m-bar-h: 0px;
	}

	/*
	 * D-07.1-01: `100svh`, never `100vh` — the SMALL viewport height, so a
	 * phone's dynamic browser-chrome toolbar (which shrinks the LARGE
	 * viewport as it hides/shows) cannot push the logo out of the centre
	 * while it is showing. This is the same idiom `.secnav__list`'s own
	 * `max-block-size: min(60svh, 30rem)` (components/sitemap.css) already
	 * uses for the identical reason — established here, not invented.
	 *
	 * `position: relative` gives `.m-hero__band` (absolutely positioned,
	 * below) its containing block; `overflow: hidden` keeps the band's
	 * own wider-than-viewport tracks from ever producing real page-level
	 * horizontal scroll, matching `.m-hero__band`'s own `overflow: hidden`
	 * one layer down. `justify-content: center` centres the logo/pitch
	 * column vertically inside the full-height box — with `min-block-size`
	 * doing the sizing, the column's own content is almost always shorter
	 * than the box, so without this the content would sit at the top.
	 *
	 * The padding no longer reads `--m-bar-h` via `calc()`: plan 07.1-02
	 * moved the conversion bar off the top edge and deleted its
	 * `--m-bar-h` redeclaration, so `.m-lead`'s own `0px` default is the
	 * only value this token can resolve to here — a `calc()` that only
	 * ever adds zero is simplified away rather than left as a trap for a
	 * future reader to wonder whether it still does something.
	 */
	.m-hero {
		position: relative;
		overflow: hidden;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: var(--space-lg);
		min-block-size: 100svh;
		padding: var(--space-xl) var(--space-lg);
		background: var(--ground-green);
		color: var(--ink-green);
		text-align: center;
	}

	/*
	 * D-07.1-01: the logo is the dominant element, optically centred. A
	 * fluid `clamp()` rather than a fixed size — judged by rendering at
	 * 320, 390 and 768px (see 07.1-04-SUMMARY.md for the widths and the
	 * judgment) rather than by arithmetic. `aspect-ratio` matches
	 * assets/images/logo.svg's own `viewBox` (355.45 × 258.07) exactly,
	 * so the mark scales without SVG's own `preserveAspectRatio`
	 * letterboxing ever adding empty space inside the box. `z-index: 1`
	 * is explicit even though flex items are already treated as
	 * `position: relative` for z-index purposes (CSS Flexbox §"z-order
	 * for flex items") — stated rather than relied upon, so the stacking
	 * intent (logo above the band) reads directly from this rule.
	 */
	.m-hero__logo {
		display: inline-flex;
		z-index: 1;
		width: clamp(9.5rem, 5.5rem + 22vw, 15rem);
		aspect-ratio: 355.45 / 258.07;
		flex-shrink: 0;
	}

	.m-hero__logo svg,
	.m-hero__logo img {
		width: 100%;
		height: 100%;
	}

	/*
	 * D-07.1-01/D-07.1-02: `.m-hero__pitch` renders only when `mobil.pitch`
	 * is non-empty (mobile-lead.php's own conditional) and is the phone's
	 * only `<h1>` — glass.css hides `.glass__pitch` below 1000px. It
	 * inherits `color: var(--ink-green)` from `.m-hero` (color is an
	 * inherited CSS property) rather than redeclaring it — the ink is the
	 * same token either way, and a redeclaration here would be a second
	 * place for the two to drift apart. `z-index: 1` is explicit for the
	 * same reason `.m-hero__logo`'s is. The typographic scale (Archivo,
	 * not condensed — D-07.1-02 reserves the condensed face for the band)
	 * is unchanged from Phase 07; on the new green ground it still clears
	 * 4.5:1 as the full `--ink-green` step (test-contrast-ratios.php
	 * check 6, this plan's Task 2).
	 */
	.m-hero__pitch {
		z-index: 1;
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: clamp(1.6rem, 1.1rem + 4vw, 2.4rem);
		line-height: 1.1;
		letter-spacing: -0.03em;
		max-width: 26ch;
	}

	/*
	 * Task 2 (Phase 07, D-07-01/D-07-02/D-07-03), scaled and repositioned
	 * Phase 07.1 Plan 04 (D-07.1-01, "text huge scrolling behind"): the
	 * flowing programme-name band — a seamless CSS marquee,
	 * compositor-only, unchanged in mechanism. `.m-hero__band` holds
	 * exactly two identical `.m-hero__track` children side by side
	 * (`display: flex`); translating the BAND by -50% of its own total
	 * width moves it by exactly one track's width, which is what makes
	 * the loop seamless with no JavaScript. `overflow: hidden` clips
	 * whatever sits outside the viewport at any instant — and is now the
	 * "confidently cropped" edge D-07.1-01 asks for: at 390×844 the band
	 * is wider than the viewport by design and a partial letterform is
	 * visible at both edges (confirmed by rendering, see
	 * 07.1-04-SUMMARY.md), not an accidental fit that only ever shows
	 * whole words.
	 *
	 * `.m-hero__band` is now taken OUT OF FLOW (`position: absolute`) so
	 * its own height never enters `.m-hero`'s flex-centring calculation —
	 * the logo centres on `.m-hero`'s own box, not on a box that also has
	 * to make room for the band. It is vertically centred with
	 * `inset-block: 0` plus `margin-block: auto` rather than the more
	 * obvious `top: 50%; transform: translateY(-50%)` — `transform` is
	 * already this rule's own ANIMATED property (the marquee), and a
	 * second, static `transform` on the same rule would be silently
	 * overwritten every frame by the keyframe's own `transform` value,
	 * undoing the centring the instant the animation starts. The
	 * `margin: auto` centring technique for an absolutely positioned
	 * element with `top`/`bottom` both set and an auto (intrinsic) height
	 * needs no `transform` at all (CSS 2.1 §10.6.4).
	 *
	 * `z-index: 0` sits explicitly below the logo/pitch's `z-index: 1`;
	 * `pointer-events: none` — this is decoration in the interaction
	 * sense even though it is real, screen-reader-visible content in the
	 * document sense (mobile-lead.php's own comment on the first,
	 * non-aria-hidden track), and neither track carries an anchor, a
	 * button or a tabindex — a continuously moving tap target is exactly
	 * the defect the scrapped dot-map control was scrapped for (A11Y-02).
	 *
	 * `--m-hero-band-speed` is unchanged in mechanism and stays the one
	 * place this animation's DURATION is tunable; `--m-hero-band-size` is
	 * new (Plan 07.1-04) and is the one place the band's DISPLAY SIZE is
	 * tunable, read by `.m-hero__name`'s `font-size` below, the same
	 * "declare the knob beside the rule it tunes" pattern
	 * `--m-hero-band-speed` already set. `will-change: transform` still
	 * sits on `.m-hero__band` only, never on the individual
	 * `.m-hero__name` items — promoting every name to its own compositor
	 * layer would cost far more than it buys.
	 *
	 * Nothing else here animates: no `width`, no `height`, no `left`/
	 * `top` (the absolute position itself is static, set once, never
	 * animated), no `margin` value change, no `background-position`, no
	 * filter — `transform` is still the only ANIMATED property, matching
	 * this design's own DESIGN-15 discipline (motion.css's own docblock).
	 */
	/*
	 * Fixed 2026-09-07 (owner report: "the text doesnt loop and is cutoff
	 * and moves too slowly"). Three separate faults in this one rule:
	 *
	 * 1. `width: 100%` pinned the band to the VIEWPORT's width. The
	 *    keyframe below translates by `-50%`, and a percentage transform
	 *    resolves against the transformed element's OWN border box — so
	 *    `-50%` moved the band half a viewport, not one track's width.
	 *    The seamless loop only works when the band is exactly as wide as
	 *    its two identical tracks, which is what `max-content` gives it.
	 *    Then `-50%` IS one track, and frame 0 and frame 100% are
	 *    pixel-identical, which is what "seamless" means.
	 *
	 * 2. `overflow: hidden` HERE clipped the band to that same viewport
	 *    width, cutting the second track off entirely — so there was
	 *    never a second copy to wrap into. Clipping belongs one layer up
	 *    on `.m-hero`, which already declares `overflow: hidden` (see its
	 *    own rule above) precisely so the band can be wider than the
	 *    screen without producing page-level horizontal scroll. Removing
	 *    it here changes nothing a visitor can scroll to.
	 *
	 * 3. `inset-inline: 0` set both start and end. With an intrinsic
	 *    width that is over-constrained; only the start edge is wanted,
	 *    so the band can extend past the viewport's end edge.
	 *
	 * Speed: 42s read as almost static. 18s is the same mechanism with
	 * the one tunable turned up — still linear, still compositor-only,
	 * still `transform` as the sole animated property (DESIGN-15).
	 */
	.m-hero__band {
		--m-hero-band-speed: 18s;
		--m-hero-band-size: clamp(3.5rem, 1.4rem + 11.5vw, 6.5rem);
		position: absolute;
		inset-block: 0;
		inset-inline-start: 0;
		margin-block: auto;
		z-index: 0;
		pointer-events: none;
		display: flex;
		width: max-content;
		will-change: transform;
		animation: cp-mobile-band-flow var(--m-hero-band-speed) linear infinite;
	}

	@keyframes cp-mobile-band-flow {
		from {
			transform: translate3d(0, 0, 0);
		}
		to {
			transform: translate3d(-50%, 0, 0);
		}
	}

	.m-hero__track {
		display: flex;
		flex-shrink: 0;
		align-items: center;
		gap: var(--space-2xl);
		padding-inline-end: var(--space-2xl);
	}

	/*
	 * D-07.1-01: "very editorial" — the type IS the composition, not
	 * decoration. `var(--font-condensed)` (plan 07.1-01's face, unused
	 * until this rule) at `--m-hero-band-size` (declared on `.m-hero__band`,
	 * above), tight `line-height` and tight NEGATIVE `letter-spacing`
	 * (Phase 07 shipped a slight positive tracking for a small 1rem
	 * label; a huge condensed display face reads tighter, not looser, at
	 * this scale). `color: var(--ink-green)` is the FULL ink step, never
	 * `--ink-green-2`/`-3` — those two computed fade steps (7.20:1 and
	 * 5.21:1 respectively, base.css:176) still clear 4.5:1 on their own,
	 * but the band is the composition itself here, not a supporting
	 * tone, so it takes the strongest ink this system ships rather than
	 * one of the two steps this project reserves for lede/fact/caption
	 * copy.
	 */
	.m-hero__name {
		white-space: nowrap;
		font-family: var(--font-condensed);
		font-weight: var(--weight-bold);
		font-size: var(--m-hero-band-size);
		line-height: 0.92;
		letter-spacing: -0.02em;
		text-transform: uppercase;
		color: var(--ink-green);
	}

	/*
	 * Task 3 (D-07-07): the mobile-only company block. `--paper`/
	 * `--paper-ink` — `.secnav`'s own pair, and (until Plan 07.1-04)
	 * `.m-hero`'s own pair too. As of D-07.1-01 `.m-hero` moved to
	 * `--ground-green`/`--ink-green`, so this IS now a second ground/ink
	 * pair in this file, not a reuse of the first — both are audited
	 * tokens read by name, and `--paper-ink-2` for the body copy is the
	 * second tone test-contrast-ratios.php check 1 already computes.
	 * "Two grounds, one page": a full-screen green hero running straight
	 * into paper is a visible seam by design — a section change, not a
	 * mistake — see 07.1-04-SUMMARY.md for the rendered judgment of that
	 * transition at 390×844. No `opacity` on either text selector.
	 */
	/*
	 * The quote, directly under the hero, on the brand green (2026-09-07,
	 * owner: "this image and quote is supposed to be right under the hero
	 * on the green background").
	 *
	 * Ground and ink are `--ground-green` / `--ink-green`, the SAME audited
	 * 9.81:1 pair `.m-hero` above uses — not a new colour, and never the
	 * neon carrying text (DESIGN-01 measures `#2EE168` at 1.74:1 on white
	 * and forbids exactly that). Reading it as a continuation of the hero's
	 * own ground is the point: the hero does not end at the fold, it runs
	 * into the quote.
	 *
	 * The rule is the hero's own colours restated through the same tokens,
	 * so a palette change reaches both. `--ink-green-2` for the attribution
	 * is the audited second tone step (7.20:1), not a faded `--ink-green` —
	 * this file's own standing rule is that hierarchy comes from the
	 * computed tone steps, never from `opacity`, because a composited fade
	 * is what made a pair read 3.10:1 nominal and 2.46:1 rendered.
	 */
	/*
	 * `margin: 0 !important` is load-bearing (2026-09-07, owner: "there is
	 * also this small white strip").
	 *
	 * `.m-quote` is a `<figure>`. WordPress core's own global styles ship a
	 * rule in the page head — confirmed by asking Chrome directly, via
	 * `CSS.getMatchedStylesForNode`, not by reading source:
	 *
	 *     :where(figure) { margin: 0 0 1em; }
	 *
	 * `:where()` has ZERO specificity, so the instinct is that `.m-quote`
	 * outranks it easily. It does not, and specificity is not why. That
	 * rule is UNLAYERED, and every unlayered declaration beats every
	 * layered one no matter how specific the layered selector is — this
	 * file's rules all live inside `@layer components` (line 54). `1em`
	 * against this block's inherited 17px font-size is the 17px exactly.
	 *
	 * `.m-lead` carries no padding and no border, so that bottom margin
	 * COLLAPSES THROUGH it and lands outside the green: a 17px band of the
	 * body's white between the quote and the first programme panel,
	 * measured at lead.bottom 1517 against panels.top 1534.
	 *
	 * `!important` is the correct tool here and not laziness — an important
	 * declaration inside a layer outranks an unlayered normal one, which is
	 * the ONLY way a layered rule can win this. `reset.css`'s own
	 * `figure { margin: 0 }` loses for the same reason, which is why the
	 * shared reset is not the place to fix it.
	 */
	.m-quote {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
		margin: 0 !important;
		padding: var(--space-xl) var(--space-lg) var(--space-2xl);
		background: var(--ground-green);
		color: var(--ink-green);
	}

	.m-quote__image {
		display: block;
		inline-size: 100%;
		block-size: auto;
		/*
		 * `--pane-radius` is the project's one declared radius token
		 * (glass.css, promoted to `:root` by plan 07.1-06 so the phone can
		 * read it). Referencing it rather than typing a number keeps the
		 * quote's corner identical to the form tile's directly below and
		 * means a future radius change reaches both — there is no
		 * `--radius-md` in this codebase, and inventing a second radius
		 * scale for one image is how two tokens start disagreeing.
		 */
		border-radius: var(--pane-radius);
	}

	.m-quote__body {
		font-family: var(--font-serif);
		font-size: 1.25rem;
		line-height: 1.4;
	}

	.m-quote__author {
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;
		color: var(--ink-green-2);
	}

	.m-about {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
		padding: var(--space-xl) var(--space-lg);
		background: var(--paper);
		color: var(--paper-ink);
	}

	.m-about__heading {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 1.375rem;
		letter-spacing: -0.02em;
	}

	.m-about__body {
		font-size: 1rem;
		line-height: 1.55;
		color: var(--paper-ink-2);
	}

	@media (min-width: 1000px) {
		.m-lead {
			display: none;
		}
	}

	/*
	 * D-07-03: under `prefers-reduced-motion: reduce` the flow stops and
	 * the names stay — the band is never removed, only its motion. This
	 * is what makes the reduced-motion state a legible composition in its
	 * own right rather than a stripped one.
	 */
	@media (prefers-reduced-motion: reduce) {
		.m-hero__band {
			animation: none;
		}
	}
}

/* SOURCE: components/mobile-actions.css */
/*
 * mobile-actions.css — the mobile-only action bar. Originally built Phase
 * 07, Plan 02 (D-07-08/D-07-09/D-07-10) pinned to the TOP edge; moved to
 * the BOTTOM edge, above the section control, by Phase 07.1, Plan 02
 * (D-07.1-04 — the owner's own walk of the shipped phone layout). Hand-
 * authored, native nesting, no preprocessor, no bundler (D-16). Wrapped
 * in the `components` layer declared by main.css, imported immediately
 * after components/mobile.css — components/sitemap.css is already
 * imported earlier still (main.css's own order), which is where the
 * `--secnav-inset` token this file reads is declared. This file no
 * longer re-declares `--m-bar-h` (main.css's own import-order comment
 * for this file explains why the earlier cross-file dependency is gone).
 *
 * TWO floating controls now share the bottom edge of a phone screen
 * (D-07.1-04, reduced from three by D-07.1-10, Phase 07.1 plan 05):
 * `.secnav` (components/sitemap.css) and `.m-actions__act` ("Kontakt") —
 * the call action ("Zavolat") that used to sit beside it is RETIRED
 * (D-07.1-10, the client's own request that the form be the phone's main
 * route; the number is still reachable, now from the site footer — see
 * this bar's own template's docblock). Two controls at the bottom edge
 * is still the exact geometry that got an earlier dot-map control
 * scrapped on 2026-09-01 for overlapping tap targets, so it is held
 * honest by tools/tests/browser/mobile-ladder.mjs's two-state (list
 * closed, list open) adjacency proof at all seven ladder geometries, not
 * by this comment. `.m-actions` itself paints NOTHING — no `background`, no
 * `color` — declaring neither is deliberate and load-bearing: this file's
 * OWN test-contrast-ratios.php check 6 resolves a rule that declares
 * `color` with no `background` against `--paper` (the one ground both
 * mobile stylesheets are allowed to paint), and that fallback would
 * silently claim a paper ground for a surface that no longer paints one
 * at all if this wrapper declared an ink with no ground of its own. The
 * one `.m-actions__act` pill (D-07.1-10: down from two) is what actually
 * paints — on the SAME `--paper`/`--paper-ink` pair `.secnav__toggle`
 * uses, a computed 17.74:1 pair with no ground-dependent value — so this
 * file still introduces no new colour pair for check 6 to compute; it
 * now also prints a positive line stating the wrapper itself contributes
 * no pair, rather than letting silence stand for proof. `.m-actions__act`
 * redeclares that pair plus its own box-shadow (matching
 * `.secnav__toggle`'s own precedent), so the pill reads as a visibly
 * separate hit area from `.secnav__toggle`, without a 1px hairline (one
 * of the approved design's six hard negatives). Its own `:focus-visible`
 * outline is `var(--paper-ink)`, matching `.secnav`'s own focus rule
 * exactly, so it stays visible against the same paper ground it paints
 * on and test-contrast-ratios.php check 5 resolves it against a ground
 * it already knows.
 *
 * D-07-09's own "equal billing" rule was about TWO actions at equal
 * weight — with `.m-actions__act` down to one (D-07.1-10), what survives
 * of that rule is narrower but still real: the single remaining action is
 * NOT styled as a primary call-to-action of its own. Its treatment still
 * matches `.secnav__toggle`'s exactly (same ground/ink pair, same
 * box-shadow, same border-radius), so it reads as a peer of the section
 * control rather than as an elevated "the" button.
 *
 * `body.is-secnav-open .m-actions { visibility: hidden; }` below is the
 * handshake `sitemap.js` drives (D-07.1-04): `.secnav__list` opens
 * UPWARD from its toggle, straight through the space this bar now
 * occupies, and without this rule the list would paint across the
 * conversion action (D-07.1-10: one now, not two), making it and a list
 * item simultaneously tappable in the same screen position — the
 * dot-map defect, exactly. `visibility`, not
 * `display`, for the same reason `sitemap.css`'s own closed-list rule
 * already gives: the element keeps its box (nothing reflows, and a
 * future drift animation has something to return to) while leaving hit
 * testing and the tab order for free.
 *
 * D-07.1-07 (Phase 07.1, plan 05, Task 3): both of the two remaining
 * floating controls now carry a small idle drift — `@keyframes
 * cp-float-drift` is declared ONCE, below, in THIS file (the file that
 * owns one of the two controls and is the newer of the two), and
 * `components/sitemap.css` references it by name for its own
 * `.secnav__toggle` rather than declaring a second copy — a `@keyframes`
 * at-rule is document-scoped and resolves regardless of declaration
 * order or import order, unlike a custom property, so `main.css`
 * importing `sitemap.css` before this file is not a problem. Each
 * control tunes the shared keyframe through its own `--float-amp`/
 * `--float-period`/`--float-delay` custom properties — two DIFFERENT
 * periods and two DIFFERENT delays (the owner's own discretion note:
 * they must not pulse in unison), and an amplitude bounded by the
 * measured `.m-actions`-to-`.secnav__toggle` gap (see this file's own
 * `.m-actions__act` rule below for the measurement and the value chosen
 * against it). `prefers-reduced-motion: reduce` silences it completely,
 * per this file's own branch further down and Phase 07 success
 * criterion §6.
 */

@layer components {
	/*
	 * D-07.1-07: the ONE shared drift keyframe both floating controls tune
	 * through their own `--float-amp`/`--float-period`/`--float-delay`
	 * (this file's `.m-actions__act` below; components/sitemap.css's
	 * `.secnav__toggle`). `transform` only — no `opacity` — via
	 * `translate3d()`, the same technique `cp-mobile-band-flow`
	 * (components/mobile.css) already uses, so it composites on the GPU
	 * rather than triggering layout. A single `from`/`to` pair with
	 * `animation-direction: alternate` (declared per-control below) is a
	 * genuine rise-and-fall drift, not a loop that snaps back to its start
	 * — alternate reverses the SAME transition rather than jumping.
	 * `ease-in-out` (also declared per-control) is what keeps both ends of
	 * that reversal smooth rather than reading as two back-to-back linear
	 * ramps meeting at a corner.
	 */
	@keyframes cp-float-drift {
		from {
			transform: translate3d(0, 0, 0);
		}
		to {
			transform: translate3d(0, calc(-1 * var(--float-amp, 0px)), 0);
		}
	}

	/*
	 * `.m-lead`'s own `--m-bar-h` (components/mobile.css, declared `0px`
	 * on `.m-lead`) is no longer re-declared here — with nothing at the
	 * top edge any more, `0px` is the correct value on both counts: what
	 * `.m-lead`'s own top padding reads, and what dock.css's
	 * `scroll-margin-block-start: var(--m-bar-h, 0px)` reads. This is a
	 * deliberate deletion (D-07.1-04), not an oversight — neither
	 * mobile.css nor dock.css needed an edit to stay correct.
	 */
	.m-actions {
		position: fixed;
		/*
		 * The section control's own bottom clearance, plus the safe area
		 * it already pads for, plus its toggle's own minimum height, plus
		 * one gap step — this bar sits directly ABOVE `.secnav__toggle`,
		 * never overlapping it. Mirrors `.secnav`'s own construction
		 * (components/sitemap.css) exactly, reading the SAME
		 * `--secnav-inset` token rather than a second copy of the
		 * literal.
		 */
		inset-block-end: calc(var(--secnav-inset) + env(safe-area-inset-bottom, 0px) + var(--tap-target-min, 44px) + var(--space-sm));
		inset-inline: 0;
		z-index: 40;
		display: flex;
		justify-content: center;
		/*
		 * No `background`, no `color` — see this file's own docblock: a
		 * wrapper that paints nothing must not declare an ink either, or
		 * test-contrast-ratios.php check 6's `--paper` inheritance
		 * fallback would silently claim a paper ground for a surface that
		 * no longer paints one.
		 */
		pointer-events: none;
	}

	.m-actions__inner {
		pointer-events: auto;
		display: flex;
		align-items: stretch;
		gap: var(--space-sm);
		width: auto;
		max-inline-size: min(92vw, 30rem);
	}

	/*
	 * D-07.1-04 / MOBILE-03: `.secnav__list` opens UPWARD from its toggle,
	 * through the space this bar now occupies. Without this rule the list
	 * would paint across the conversion action (D-07.1-10: one now, not
	 * two) and both would be simultaneously tappable in the same screen
	 * position — see this file's own docblock. `sitemap.js` adds/removes
	 * `is-secnav-open` on `<body>` on every path that adds/removes
	 * `is-open` on `.secnav`, including Escape and outside-click, so this
	 * class can never desync from the list's own open state.
	 */
	body.is-secnav-open .m-actions {
		visibility: hidden;
	}

	/*
	 * D-07.1-10: with the call action retired there is now exactly ONE
	 * `.m-actions__act`, so `flex: 0 1 auto` (size to content, never grow)
	 * is the unconditional correct value — the D-07-09 concern this used
	 * to need a `.m-actions__inner--single` MODIFIER class to express (one
	 * action must never read as an oversized primary control, only when
	 * the call action happened to be absent) is now simply this rule's own
	 * default, because the modifier's condition ("the call action is
	 * absent") is no longer a variable — it is always true. The modifier
	 * class and the conditional PHP that added it to `.m-actions__inner`
	 * are both gone from mobile-actions.php.
	 */
	.m-actions__act {
		flex: 0 1 auto;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: var(--space-sm);
		min-height: var(--tap-target-min, 44px);
		padding-inline: var(--space-md);
		border: 0;
		border-radius: 999px;
		background: var(--paper);
		color: var(--paper-ink);
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;
		font-weight: var(--weight-bold, 700);
		text-align: center;
		box-shadow: 0 10px 30px -8px rgb(10 14 9 / 0.42),
			0 2px 6px rgb(10 14 9 / 0.16);
		/*
		 * D-07.1-07: the idle drift. `.m-actions` and `.secnav__toggle`
		 * measure an 8px (`--space-sm`) gap between them at rest, at EVERY
		 * one of the seven ladder geometries — measured directly (real
		 * `getBoundingClientRect()` reads via headless Chrome, 320x568
		 * through 768x1024), not assumed, and it holds constant because
		 * `--secnav-inset` cancels out of the arithmetic (both this bar's
		 * own `inset-block-end` and `.secnav`'s own read the SAME token,
		 * and both boxes resolve to exactly `var(--tap-target-min, 44px)`
		 * tall by `min-height`, matching the assumption baked into this
		 * bar's own `inset-block-end` formula above). If both controls
		 * drifted toward each other at amplitude A they would close 2A of
		 * that 8px gap — 2px leaves 4px of that gap as real margin (a 50%
		 * safety factor), comfortably clear even before
		 * tools/tests/browser/mobile-float-motion.mjs's own sampled
		 * worst-case proof (Task 4) confirms it empirically rather than
		 * arithmetically. `animation-fill-mode` is deliberately NOT set —
		 * the keyframe's own `from`/`to` pair already covers the element's
		 * resting `transform: none` state, so no fill mode is needed to
		 * hold a value outside the animation's own active interval.
		 */
		--float-amp: 2px;
		--float-period: 5.6s;
		--float-delay: 0s;
		animation: cp-float-drift var(--float-period) ease-in-out var(--float-delay) infinite alternate;
	}

	/*
	 * `fill="currentColor"` on the inline SVG (mobile-actions.php) means
	 * the icon takes this rule's own `color` — no colour declaration of
	 * its own, nothing for test-contrast-ratios.php check 6 to resolve.
	 * Fixed square size so icon and label never fight for space in the
	 * pill's own inline-flex row.
	 */
	.m-actions__icon {
		flex: none;
		inline-size: 1.125rem;
		block-size: 1.125rem;
	}

	.m-actions__label {
		white-space: nowrap;
	}

	.m-actions__act:focus-visible {
		outline: 3px solid var(--paper-ink);
		outline-offset: 2px;
	}

	/*
	 * D-07.1-07: no explicit desktop cancellation of the drift is needed
	 * here — `.m-actions` is already `display: none` in full below this
	 * width, which removes `.m-actions__act` and its running animation
	 * from the page entirely. This is the asymmetry with
	 * components/sitemap.css's own explicit `animation: none` for
	 * `.secnav__toggle`: that control is NOT hidden above 1000px (it
	 * serves the desktop layout too), so it needs an explicit cancellation
	 * this bar's own control does not.
	 */
	@media (min-width: 1000px) {
		.m-actions {
			display: none;
		}
	}

	/*
	 * D-07.1-07 / Phase 07 success criterion §6: silences the drift
	 * completely — the control is still present, still tappable, still in
	 * the same resting place; it simply does not move.
	 * `.m-actions__act` named verbatim, per this project's own
	 * tools/tests/test-a11y-structure.php check 5, which treats a
	 * silencing rule that does not name the selector it silences as
	 * absent.
	 */
	@media (prefers-reduced-motion: reduce) {
		.m-actions__act {
			animation: none;
		}
	}
}

/* SOURCE: components/panels.css */
/*
 * panels.css — the full-bleed scrolling colour panels behind the fixed
 * left paper pane (Phase 02.5, 02.4-SIGNOFF.md). Hand-authored, native
 * nesting, container queries, no preprocessor, no bundler (D-16). Wrapped
 * entirely in the `components` layer declared by main.css.
 *
 * The five ground modifier classes read their colours from the ground/ink
 * token pairs base.css declares — --ground-{name}/--ink-{name}, plus the
 * two computed fade replacements --ink-{name}-2/-3 — never a raw hex
 * literal here, so the single computed source of truth for D-12/D-13's
 * contrast-checked palette stays in one place.
 *
 * Task 3 (02.5-CONTRAST.md recommendation 2, criterion 6): NO `opacity`
 * declaration applies to any body-text selector in this file. Every place
 * the mockup fades text with opacity, this file names one of the three
 * computed ink steps instead — `currentColor` (full ink, from the
 * `--panel-fg` custom property each ground modifier sets), `--ink-{name}-2`
 * or `--ink-{name}-3`. This is what makes the contrast harness meaningful
 * rather than a moving target: the declared colour IS the effective
 * colour, because nothing composites it toward the ground at paint time.
 *
 * D-02's hard negatives apply throughout: no 1px hairline anywhere in
 * this file, no gradient of any kind, no accumulated effect. The head
 * rule below is a 0.14rem bar at reduced opacity — a deliberately thick,
 * soft mark, not a border; that IS an opacity use, but on a decorative
 * shape, never on text, so it is outside criterion 6's body-text scope
 * and Task 3's harness check 3 (composited ratio of a decorative element)
 * covers it instead of check 1/2. No @keyframes and no transition live
 * here either — motion is components/motion.css so the one genuinely
 * high-risk layer stays easy to find and to disable. The halftone dot
 * texture this file's `.panel` participates in is painted by
 * components/grain.css's `.panel::before` rule, not here — one file per
 * effect, per Task 3's isolation rationale.
 */

@layer components {
	/* .panels is the containing block for the parked pane — see glass.css's
	   release rule, which switches it from fixed to absolute. */
	.panels {
		position: relative;
	}

	.panel {
		container-type: inline-size;
		display: flex;
		flex-direction: column;
		justify-content: flex-start;
		position: relative;
		overflow: hidden;
		padding-block: clamp(var(--space-2xl), 8vw, var(--space-4xl));
		padding-inline: clamp(var(--space-lg), 5vw, var(--space-3xl));
		color: var(--panel-fg, var(--ink-green));
		/*
		 * Quick task 260831-v19 — 0.86 alpha so the fixed photographic
		 * field behind the stack reads through the panel.
		 *
		 * This does NOT weaken any contrast guarantee. The ground/ink
		 * token pair is untouched, and .photo-field paints this same
		 * ground underneath as an opaque base plate (field.js hands it
		 * over from this element's own --panel-bg), so the colour
		 * composited beneath every headline is the ground at full
		 * strength — exactly what test-contrast-ratios.php computes.
		 * With JavaScript off the field never renders a base plate, but
		 * it also never renders a card, so the panel composites against
		 * the page ground and the type is if anything more contrasted.
		 */
		background: color-mix(in srgb, var(--panel-bg, var(--ground-green)) 86%, transparent);
	}

	/* Every panel child paints above the halftone/grain pseudo-elements
	   grain.css adds to `.panel` itself (z-index: 0 there). */
	.panel > * {
		position: relative;
		z-index: 1;
	}

	/* From 1000px up the fixed left pane (46vw, glass.css) sits over the
	   panel's left edge — clear it with padding rather than letting panel
	   content run underneath the opaque pane.
	   `min-height: 100vh` moved here from the base rule above (Phase 07,
	   Plan 03, D-07-11): below 1000px there is no split for a full
	   screen to answer, so a programme section now shrinks to its own
	   content instead — six readable cards rather than six mandatory
	   full screens. Accepted cost, stated to the owner and accepted: the
	   grounds read as bands rather than rooms on a phone. The colour
	   system is unchanged; only the height it is given is. This applies
	   to the full-width sections below the programmes for the same
	   reason (`.panel--pribeh`, further down this file, gets the
	   identical move). */
	@media (min-width: 1000px) {
		.panel {
			min-height: 100vh;
			padding-left: calc(46vw + clamp(var(--space-lg), 4vw, var(--space-2xl)));
			padding-right: clamp(var(--space-lg), 5vw, var(--space-2xl));
		}
	}

	.panel--green {
		--panel-bg: var(--ground-green);
		--panel-fg: var(--ink-green);
		--panel-fg-2: var(--ink-green-2);
		--panel-fg-3: var(--ink-green-3);
	}

	.panel--coral {
		--panel-bg: var(--ground-coral);
		--panel-fg: var(--ink-coral);
		--panel-fg-2: var(--ink-coral-2);
		--panel-fg-3: var(--ink-coral-3);
	}

	.panel--indigo {
		--panel-bg: var(--ground-indigo);
		--panel-fg: var(--ink-indigo);
		--panel-fg-2: var(--ink-indigo-2);
		--panel-fg-3: var(--ink-indigo-3);
	}

	.panel--amber {
		--panel-bg: var(--ground-amber);
		--panel-fg: var(--ink-amber);
		--panel-fg-2: var(--ink-amber-2);
		--panel-fg-3: var(--ink-amber-3);
	}

	.panel--night {
		--panel-bg: var(--ground-night);
		--panel-fg: var(--ink-night);
		--panel-fg-2: var(--ink-night-2);
		--panel-fg-3: var(--ink-night-3);
	}

	/*
	 * Quick task 260831-vod — the sixth ground, for the closing "Náš
	 * příběh" panel. --paper/--paper-ink is base.css's own already-computed
	 * pair (17.74:1, the fixed pane's own ground/ink), so this reuses a
	 * verified surface rather than introducing a colour that would need its
	 * own contrast run.
	 */
	.panel--paper {
		--panel-bg: var(--paper);
		--panel-fg: var(--paper-ink);
	}

	/*
	 * The closing panel reads as a coda, not as an eighth programme: it
	 * gets more air above it and does not force a full viewport, so the
	 * page ends rather than presenting one more thing to scroll past.
	 */
	/*
	 * The closing panel is FULL SCREEN and, once the page has released to
	 * it, full width — the pane slides away and the story gets the whole
	 * viewport. It is the one section that is not competing with the
	 * enquiry form for attention, so it is the one place the split can let
	 * go. (`body.is-released` is set by field.js when this panel owns the
	 * middle of the viewport.)
	 */
	/*
	 * A full-width section sized to its content rather than forced to a
	 * viewport — Reference is one short quote today and would otherwise be
	 * a screen of empty colour.
	 */
	.panel--wide {
		min-height: auto;
		padding-block: clamp(var(--space-3xl), 8vw, var(--space-4xl));
	}

	.panel--pribeh {
		justify-content: center;
		padding-block: clamp(var(--space-3xl), 9vw, var(--space-4xl));
	}

	/* `min-height: 100vh`/`100dvh` moved here (Phase 07, Plan 03, D-07-11)
	   — same reasoning as `.panel`'s own move above: below 1000px there
	   is no split for a full screen to answer, so the closing panel
	   shrinks to its content like every other one. */
	@media (min-width: 1000px) {
		.panel--pribeh {
			min-height: 100vh;
			min-height: 100dvh;
		}
	}

	/*
	 * Full width for every section that is NOT a programme (quick task
	 * 260901b). The split earns its keep beside the offer the visitor is
	 * choosing between; O nás, Reference and Náš příběh are about the
	 * business, so they get the whole viewport and the pane steps aside.
	 *
	 * Selected on `data-section-type`, which front-page.php already emits
	 * from the validated type — so a section the client adds tomorrow is
	 * treated correctly for what it IS, with no list here to keep in sync.
	 *
	 * Owner decision, 2026-09-02 (quick task 260902-kv9 addendum,
	 * ADDITION B): the owner watched these full-width sections travel in
	 * from the right when the pane releases — an 835px eased slide at
	 * 1920px, padding-left dropping from `calc(46vw + ...)` (~931px) to
	 * `clamp(var(--space-2xl), 8vw, var(--space-4xl))` (96px) — and asked
	 * for it gone. The `transition: padding-left` declaration that drove
	 * that slide is REMOVED outright, not shortened or eased differently,
	 * so the released state now SNAPS to full width in one frame, exactly
	 * like footer.css's own `.panels ~ .site-footer` version of this same
	 * rule already does — that file's own docblock has said so since Task
	 * 1: "the footer's left clearance snaps rather than eases ... unlike
	 * the panel stack's own eased version of the same rule." This makes
	 * the panel stack consistent with the footer, which was deliberately
	 * written to snap from the start. Removing the declaration emptied
	 * this selector's own rule block entirely, so the block is removed
	 * too rather than left as a stub — the padding-left VALUE below is
	 * untouched, only the transition that eased into it is gone.
	 *
	 * `.panels > section:not([data-section-type='programme']) .panel,` in
	 * motion.css's `prefers-reduced-motion: reduce` branch
	 * (motion.css:258) is now silencing a transition that no longer
	 * exists — left in place deliberately (motion.css is out of this
	 * task's scope) rather than removed, since a stale no-op silencing
	 * rule is harmless and test-a11y-structure.php's own reduced-motion
	 * coverage check (A11Y-04) matches that selector string literally,
	 * not by whether panels.css still needs it.
	 */
	/*
	 * Quick fix, 2026-09-10 — the boundary of "full width when released"
	 * is the PANE ITSELF, not a type name.
	 *
	 * `:not([data-section-type='programme'])` alone was correct only while
	 * every non-programme panel sat BELOW the split. The permanent
	 * `vitejte` welcome panel (quick task 260909-l4a) is the first section
	 * on the page and is not a programme, so it started matching this rule
	 * too — and released full width at the TOP of the document. Measured
	 * at 1440x950: the moment the pane parked, vitejte grew 1002px -> 1386px
	 * (its map is `inline-size: 100%` on `aspect-ratio: 16/10`, so its
	 * height follows the panel's content width), the document grew
	 * 10975px -> 11296px, and the browser's scroll anchoring lurched the
	 * reader ~396px. The release condition is `lastSplit.bottom <=
	 * innerHeight`, evaluated against that same shifting geometry, so it
	 * flipped back and forth across the handover — reported as the pane
	 * flickering instead of snapping. `--pane-park` was computed from the
	 * pre-jump geometry as well, so the pane also parked ~384px off.
	 *
	 * `.glass ~ section` says the actual invariant structurally: front-
	 * page.php dispatches the pane immediately after the last visible
	 * programme, so a section that FOLLOWS the pane in the document is by
	 * construction a section the split has already ended before — and one
	 * that precedes it scrolls beside the pane and must keep its clearance
	 * at every scroll position. No type list to keep in sync, and a type
	 * added to either band tomorrow is treated correctly for where it is.
	 *
	 * `:not([data-section-type='programme'])` is kept as well, unchanged:
	 * a programme can never follow the pane today, and the rule should
	 * still say out loud that a programme is never full width.
	 */
	@media (min-width: 1000px) {
		body.is-released
			.panels > .glass ~ section:not([data-section-type='programme']) .panel {
			padding-left: clamp(var(--space-2xl), 8vw, var(--space-4xl));
		}
	}

	/*
	 * composed.css hardcodes `color: var(--ink-night-2)` on
	 * `.c-pribeh__body` — correct only inside the dark `.panel--night`
	 * ground it was written for, and that file's own line 327 comment
	 * already flags the hazard. Moving pribeh onto a paper panel walked
	 * straight into it: near-white body text on cream, about 1.2:1, found
	 * by looking at the render.
	 *
	 * Fixed explicitly rather than by redefining --ink-night-2 to mean
	 * something else inside this panel — a token that lies about its own
	 * name is a worse trap than the one being fixed. --paper-ink-2 on
	 * --paper computes to 9.24:1, the same step the fixed pane already
	 * uses for secondary copy. This is the same defect class 02.6 hit on
	 * .overlay--pane, fixed the same way and for the same reason.
	 */
	.panel--paper .c-pribeh__body {
		color: var(--paper-ink-2);
	}

	/* standalone, this is a panel heading and should carry a panel
	   heading's weight — composed it stays the small run-in lead-in */
	.panel--paper .c-pribeh__heading {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(2.4rem, 1rem + 4.6vw, 4.6rem);
		line-height: 0.98;
		letter-spacing: -0.024em;
		margin-block-end: var(--space-lg);
	}

	/*
	 * Full screen, so the composition has to hold the screen: the
	 * photograph and the story share the width as equals and sit on the
	 * optical centre line, rather than the copy floating in the top third
	 * of an otherwise empty viewport.
	 */
	@media (min-width: 1000px) {
		.panel--paper .c-pribeh {
			display: grid;
			grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
			align-items: center;
			gap: clamp(var(--space-2xl), 5vw, var(--space-4xl));
			width: 100%;
		}

		.panel--paper .c-pribeh__media {
			grid-column: 1;
			align-self: stretch;
			display: flex;
		}

		.panel--paper .c-pribeh__media img {
			width: 100%;
			height: 100%;
			max-height: 62vh;
			object-fit: cover;
			border-radius: 0.7rem;
		}

		.panel--paper .c-pribeh__content {
			grid-column: 2;
			max-width: 46ch;
		}
	}

	.panel__head {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-md);
		padding-bottom: var(--space-md);
		margin-bottom: var(--space-xl);
	}

	/* The approved head rule — a thick, softened bar, deliberately never a
	   1px border (D-02). The opacity here is licensed: it is a decorative
	   shape, not text, so Task 3's harness check 3 (not check 1/2) governs
	   it — composited against every ground it still clears 3:1 (WCAG
	   1.4.11 non-text minimum). The mockup's own value (.32) was NOT
	   computed and fails 3:1 on every panel ground when composited
	   (worst case 1.87:1 on green) — 0.6 is the computed replacement,
	   verified by tools/tests/test-contrast-ratios.php check 3 against
	   all five panel grounds (worst case 3.19:1 on indigo). */
	.panel__head::after {
		content: '';
		position: absolute;
		inset-inline: 0;
		bottom: 0;
		height: 0.14rem;
		border-radius: 2px;
		background: currentColor;
		opacity: 0.6;
	}

	.panel__eyebrow,
	.panel__brand {
		font-family: var(--font-grotesk);
		font-size: 0.75rem;
		font-weight: var(--weight-bold);
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	/*
	 * Quick task 260831-vod — spacing refinement. The body used one
	 * --space-xl gap between every block, which set the lede, the stat row
	 * and the price/CTA group all at the same distance from each other and
	 * left the panel reading as four unrelated things stacked up. The
	 * rhythm is now graded: related blocks sit closer than unrelated ones.
	 */
	.panel__body {
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
		flex: 1 1 auto;
	}

	/* Container query, not a viewport media query (D-16) — the split is a
	   property of the panel's own available width, which the fixed pane
	   already changes independently of the viewport. */
	@container (min-width: 640px) {
		.panel__body {
			display: grid;
			grid-template-columns: 1.25fr 0.75fr;
			column-gap: var(--space-2xl);
			row-gap: var(--space-md);
			align-items: start;
		}

		.panel__figure {
			grid-column: 2;
			grid-row: 1 / span 4;
		}
	}

	/*
	 * Quick task 260902-kv9, Task 2. What this fixes, measured: a
	 * non-programme panel puts a single child into the two-column grid
	 * above, which was built for a programme panel's text-plus-figure
	 * pair — at 1920px the second track sat empty at 648px and
	 * `.c-step-list`/`.c-jak-funguje__heading` stopped at 1176px on a
	 * 1920px screen. From commit fd0029f, a Phase 02.7-era quick task.
	 *
	 * Why this selector shape: reused verbatim from this file's own
	 * established non-programme selector (panels.css:176/181 above)
	 * rather than a list of section names, so a section the client adds
	 * tomorrow is treated correctly for what it IS with no list to keep
	 * in sync.
	 *
	 * Why this can live in panels.css at all: main.css imports
	 * composed.css AFTER panels.css and both sit in `@layer components`,
	 * so at EQUAL specificity composed.css wins — the trap the door, the
	 * fit ladder and the testimonial quote all hit (composed.css's own
	 * "Moved here from panels.css" comment). It does not apply here
	 * because this override does not tie on specificity: it scores two
	 * classes plus an attribute selector plus a type selector against
	 * `.panel__body`'s single class, and specificity is resolved before
	 * source order within a layer. A grep of assets/css/ confirms
	 * `.panel__body` is declared in no other file, so there is no later
	 * declaration to lose to. This rule therefore belongs beside the
	 * rule it modifies rather than exiled into composed.css.
	 *
	 * A second container block, not extra declarations inside the first:
	 * the collapse must apply exactly where the two-column grid applies
	 * and nowhere else. Below 640px of panel inline-size `.panel__body`
	 * is still the flex column above (panels.css:320) and there is
	 * nothing to collapse. `minmax(0, 1fr)`, not a bare `1fr` — `1fr` is
	 * `minmax(auto, 1fr)` and lets a wide child (the step grid, a long
	 * unbroken string) blow the track past the panel's content box.
	 *
	 * No other declaration touched: `display`/`column-gap`/`row-gap`/
	 * `align-items`/`flex` are untouched — `column-gap` is inert with one
	 * track, and `.panel__figure`'s `grid-column: 2` only ever matches
	 * inside a programme panel, which this selector excludes by
	 * construction. `.panel__body { display: flex; }` (panels.css:321)
	 * is untouched — tools/tests/test-hidden-guard.php check 4 asserts
	 * its presence by regex.
	 *
	 * No rule capping prose width added here either: `.u-measure`
	 * already sits in the `utilities` layer, declared after `components`
	 * in main.css's `@layer` statement, so its 65ch cap outranks every
	 * components-layer rule regardless of specificity or source order —
	 * and `.c-jak-funguje__intro`, `.c-pribeh__body` and `.panel__lede`
	 * all already carry the class in the markup. measure-layout.mjs's P3
	 * check (Task 1) is what proves that held.
	 */
	@container (min-width: 640px) {
		.panels > section:not([data-section-type='programme']) .panel__body {
			grid-template-columns: minmax(0, 1fr);
		}
	}

	.panel__index {
		display: flex;
		align-items: baseline;
		gap: var(--space-sm);
		color: var(--panel-fg-2, currentColor);
	}

	.panel__index-num {
		font-family: var(--font-serif);
		font-size: clamp(1.75rem, 1rem + 3vw, 2.9rem);
		line-height: 0.72;
		letter-spacing: -0.03em;
	}

	.panel__index-name {
		font-family: var(--font-grotesk);
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		letter-spacing: 0.02em;
	}

	/*
	 * Headline role (02.4-SIGNOFF.md: "Instrument Serif — headlines,
	 * stat figures, pull-quotes, prices"). The programme title is one of
	 * this design's headlines — Instrument Serif Regular, never italic,
	 * never bold (the face ships weight 400 only, per base.css). This is
	 * distinct from the fixed pane's own `h1` pitch, which the approved
	 * mockup sets in Archivo instead (see glass.css) — "headline" is a
	 * role assigned per element by the approved design, not a blanket
	 * face swap.
	 */
	.panel__heading {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(2.2rem, 1.4rem + 4.4vw, 4.5rem);
		line-height: 1;
		letter-spacing: -0.022em;
	}

	.panel__lede {
		font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
		line-height: 1.5;
		max-width: 38ch;
		color: var(--panel-fg-2, currentColor);
	}

	.panel__facts {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-xl);
	}

	.panel__fact {
		display: flex;
		align-items: baseline;
		gap: var(--space-sm);
	}

	.panel__fact-value {
		font-family: var(--font-serif);
		font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem);
		line-height: 0.9;
	}

	.panel__fact-label {
		font-family: var(--font-grotesk);
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		color: var(--panel-fg-2, currentColor);
	}

	.panel__services {
		display: grid;
		gap: var(--space-lg);
	}

	.panel__service-title {
		font-size: 1.0625rem;
		font-weight: var(--weight-bold, 700);
	}

	.panel__service-desc {
		font-size: 0.9375rem;
		line-height: 1.5;
		color: var(--panel-fg-2, currentColor);
	}

	/* Price row — shared by every programme's service list and its own
	   summary price (`inc/render.php`'s classroompros_render_price()),
	   and by Ceník's per-item price line (composed.css). One definition
	   here, read through currentColor, so it works unmodified wherever a
	   `.c-price-row` lands on a coloured ground. */
	.c-price-row {
		display: flex;
		align-items: baseline;
		flex-wrap: wrap;
		gap: var(--space-sm);
		font-family: var(--font-grotesk);
	}

	.c-price__amount {
		font-size: 1.0625rem;
		font-weight: var(--weight-bold);
	}

	.c-price__dph {
		font-size: 0.75rem;
		font-weight: var(--weight-regular);
		letter-spacing: 0.02em;
		color: var(--panel-fg-3, currentColor);
	}

	.panel__figure {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
	}

	.panel__figure img {
		width: 100%;
		height: auto;
		border-radius: 0.4rem;
	}

	.panel__figure-caption {
		font-family: var(--font-grotesk);
		font-size: 0.75rem;
		letter-spacing: 0.02em;
		color: var(--panel-fg-3, currentColor);
	}

	/*
	 * The price block and the control belong together and are the panel's
	 * conversion moment, so they sit as one group with real air above them
	 * separating them from the supporting detail — not the same gap as
	 * everything else.
	 */
	.panel__foot {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: var(--space-md);
		margin-top: var(--space-lg);
	}

	/*
	 * Phase 02.6-02 (D-04): the price row and its note are the CTA's
	 * supporting detail, grouped into one quiet block (tight internal
	 * gap) so they read as a single secondary element beside the control
	 * rather than three siblings of equal visual weight — the control
	 * itself carries the weight below.
	 */
	.panel__price-block {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
	}

	/*
	 * Phase 02.6-02 (D-04, closing 02.5-GATES.md §3 row 7 as superseded —
	 * the owner asked for MORE prominence on this control, not the
	 * mockup's plain-text link, so that row's divergence finding is
	 * settled in the pill's favour rather than left open against a design
	 * decision the owner has since overtaken). Sized well above the
	 * project's tap-target floor with margin on both axes, set in a
	 * primary-control font size rather than body text, and full-width on
	 * its own single-column measure below the same 640px container-query
	 * breakpoint `.panel__body` already switches on, hugging its own
	 * content once the two-column layout has room for it beside the
	 * figure.
	 */
	/*
	 * Quick task 260831-wey: the LOOK now lives in `.c-btn`
	 * (components/button.css) and this control wears it, because the owner
	 * picked this button as the house style and asked every primary control
	 * to match it. Keeping a near-identical copy here is precisely how the
	 * close control and this one ended up at 12px vs 9.6px radius and 60px
	 * vs 76px height while both claimed to be "the same button" — measured,
	 * not assumed.
	 *
	 * panels.css is imported AFTER button.css, so anything still declared
	 * here would silently win. What remains is only what is genuinely this
	 * control's own business: it goes full width on its own single-column
	 * measure below the container-query breakpoint `.panel__body` already
	 * switches on, and hugs its content once there is room beside the
	 * figure.
	 *
	 * Contrast is unchanged: `.c-btn` takes --panel-fg/--panel-bg with the
	 * roles swapped, which is the same pair test-contrast-ratios.php already
	 * proves at >= 4.5:1 on every ground.
	 */
	.panel__cta {
		width: 100%;
	}

	@container (min-width: 640px) {
		.panel__cta {
			width: auto;
		}
	}

	.panel__cta-arrow {
		font-size: 1em;
		line-height: 1;
	}

	/*
	 * Phase 02.6-02 (D-04): the global :focus-visible ring
	 * (base.css — outline-color: var(--color-focus-ring), i.e. the
	 * paper's near-black ink #0A0A0A) is computed, not assumed, to fail
	 * WCAG 1.4.11's 3:1 non-text minimum on the night ground: #0A0A0A
	 * against #0F140F is 1.06:1 — effectively invisible where the ring
	 * sits (outside the element, against the PANEL ground, not the
	 * control's own fill). --panel-fg is exactly the ink token
	 * test-contrast-ratios.php check 1 already proves clears ≥4.5:1
	 * against its own ground for every one of the five panels (see the
	 * ratio table above), so reusing it here — rather than introducing a
	 * new, unverified ring colour — guarantees the 3:1 non-text minimum
	 * with more than a full point of margin on every ground, including
	 * night.
	 */
	.panel__cta:focus-visible {
		outline: 2px solid var(--panel-fg, var(--ink-green));
		outline-offset: 3px;
	}

	.panel__cena-note {
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		color: var(--panel-fg-2, currentColor);
	}

	.panel__fnote {
		font-family: var(--font-serif);
		font-style: italic;
		font-size: 1rem;
		line-height: 1.4;
		max-width: 30ch;
		color: var(--panel-fg-2, currentColor);
	}
}

	/*
	 * The focus ring is invisible on the night ground.
	 *
	 * `--color-focus-ring` is `--color-ink` (#0A0A0A). Against
	 * `--ground-night` (#0F140F) that computes to 1.06:1 — a keyboard user
	 * on a night panel gets no visible ring at all. Indigo is 3.18:1, only
	 * 0.18 over the floor. `.panel__cta` already overrides it locally;
	 * nothing else did, so `.overlay__close` and every audio-player button
	 * inside a night-ground surface were unreachable to the eye.
	 *
	 * `--panel-fg` is the ink each ground modifier sets for itself and is
	 * already proven >= 4.5:1 against all five grounds by
	 * test-contrast-ratios.php, so this needs no new contrast computation —
	 * it reuses a number the suite already guards.
	 */
	.panel :focus-visible,
	.dock :focus-visible,
	.overlay :focus-visible {
		outline-color: var(--panel-fg, var(--color-focus-ring));
	
}

/* SOURCE: components/vitejte.css */
/*
 * vitejte.css — the permanent "Vítejte / kde nás najdete" welcome-and-
 * location panel (Quick task 260909-l4a). The FIRST thing in the
 * right-hand programme deck: a two-column composition on a wide enough
 * panel (copy + address on the left, a decorative arrow, the map on the
 * right), collapsing to a single flex column on a narrow one — the same
 * container-query-gated split section-lektori.php's own `.c-lektori__layout`
 * already establishes for a non-programme panel, never a viewport media
 * query (D-16): `.panel` already declares `container-type: inline-size`
 * (panels.css), so the split is a property of the PANEL's own available
 * width — which the fixed left pane changes independently of the
 * viewport — not of the screen.
 *
 * `.panel__body` collapses every non-programme panel to a single grid
 * column (panels.css's own `.panels > section:not([data-section-type=
 * 'programme']) .panel__body` rule) — this file's own two-column
 * composition therefore lives on `.c-vitejte`'s OWN child wrapper,
 * exactly as `.c-lektori__layout` already does, never as an override of
 * that existing rule.
 */

@layer components {
	.c-vitejte__layout {
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
	}

	.c-vitejte__copy {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
	}

	/*
	 * `.c-vitejte__body`/`.c-vitejte__address` deliberately declare no
	 * `color` of their own. `.panel--paper` (panels.css) sets fewer
	 * custom properties than the five colour grounds — only
	 * `--panel-bg`/`--panel-fg`, no `-fg-2`/`-fg-3` — and both elements
	 * inherit `color` straight from `.panel`'s own
	 * `color: var(--panel-fg, ...)`, never a secondary-ink token. That
	 * resolves to `--paper-ink` on the default ground, a real,
	 * full-contrast ink (17.74:1) — NOT the `.panel--paper
	 * .c-pribeh__body` bug this file's own docblock measured
	 * (composed.css hardcoding a dark-ground secondary-ink token onto
	 * body text, ~1.2:1 on paper). No `.panel--paper .c-vitejte__body`
	 * override is added because there is nothing broken to fix: unlike
	 * pribeh's own body copy, nothing here ever reads
	 * `--panel-fg-2`/`--ink-{name}-2` for its main text.
	 */
	/*
	 * The heading carries the brand name, and base.css sets `hyphens:
	 * auto` on `html[lang="cs"]` for the whole document — which at this
	 * panel's own column width broke it as "Classro-omPros" (measured in
	 * a real render, 1440x950: heading box 277px, font-size 48px). A
	 * proper noun must never be hyphenated, so this one element opts out.
	 * Body copy below keeps `auto` deliberately: Czech hyphenation is
	 * wanted there and reads correctly.
	 */
	.c-vitejte__heading {
		hyphens: none;
		text-wrap: balance;
	}

	/*
	 * The mark after the wordmark. Sized in `em` so it tracks the heading's
	 * own clamped font-size instead of needing its own breakpoint, and
	 * nudged down slightly because the logo's artwork sits high in its
	 * 355x258 viewBox. `inline-block` keeps it on the heading's last line
	 * and lets it wrap with the text rather than stranding on a line alone.
	 */
	/*
	 * Hidden by default and revealed only in the two-column layout below.
	 * The mark exists to fill the empty ground beside the copy; in the
	 * single-column layout there is no such ground, so showing it there
	 * would just push the map further down for no gain.
	 */
	.c-vitejte__mark {
		display: none;
	}

	.c-vitejte__mark svg {
		display: block;
		inline-size: 100%;
		block-size: auto;
	}

	/*
	 * GREEN ACCENTS (owner, 2026-09-09: "I want some green elements on
	 * this new panel ... we need to sell the green a bit more").
	 *
	 * The panel STAYS on the paper ground. Angličtina — the very next
	 * panel — already paints `--ground-green`, so moving this one onto
	 * the green ground would sit two greens back to back, which is the
	 * repeat classroompros_panel_ground()'s own anti-repeat bump exists
	 * to prevent. The brand green arrives as accents on paper instead.
	 *
	 * Which green goes where is a measured decision, not a taste one
	 * (contrast computed against `--paper` #F7F7F3):
	 *   --ground-green #2EE168 → 1.54:1  GRAPHIC ONLY, never text
	 *   --ink-green-3  #115426 → 8.02:1  safe for text (AAA at this size)
	 * So every green that carries words uses --ink-green-3, and
	 * --ground-green appears only as fill or keyline where nothing has to
	 * be read off it.
	 */
	.c-vitejte__address {
		font-weight: var(--weight-bold);
		border-inline-start: 3px solid var(--ground-green);
		padding-inline-start: var(--space-sm);
	}

	/*
	 * A short brand-green rule under the heading. Decorative only — it is
	 * a ::after with no text, so it announces nothing and adds no node to
	 * the accessibility tree.
	 */
	.c-vitejte__heading::after {
		content: '';
		display: block;
		inline-size: 3.5rem;
		block-size: 0.375rem;
		margin-block-start: var(--space-sm);
		border-radius: 999px;
		background: var(--ground-green);
	}

	/*
	 * The hand-drawn arrow (D-5): decorative only, absent below this
	 * panel's own two-column breakpoint rather than overlapping the
	 * reflowed single column. Sized to sit comfortably in the middle
	 * track once that breakpoint is reached, below.
	 */
	.c-vitejte__arrow {
		display: none;
		inline-size: 4rem;
		block-size: auto;
		align-self: center;
		flex-shrink: 0;
		/*
		 * --ink-green-3 (8.02:1 on paper), not --ground-green (1.54:1):
		 * a 3px-stroke line drawing on a pale ground needs the readable
		 * green, and this one is unmistakably green rather than near-ink.
		 */
		color: var(--ink-green-3);
	}

	/*
	 * Container query, not a viewport media query (D-16) — the split is a
	 * property of the panel's own available inline size, matching
	 * panels.css's own `.panel__body` split and section-lektori.php's own
	 * `.c-lektori__layout` split exactly.
	 */
	@container (min-width: 640px) {
		/*
		 * Copy above, map below — NOT two side-by-side columns. Measured
		 * reason, 1440x950: `.panel` spans the full viewport width, but its
		 * body starts to the RIGHT of the fixed paper pane, so the real
		 * content area is ~730px even though the container query sees 1440.
		 * Split three ways that gave each column 277px, which is narrower
		 * than the brand name at this heading size and left the map a
		 * stamp in an 876px-tall panel. Stacking hands the map the whole
		 * 730px and lets the copy hold a comfortable measure.
		 */
		/*
		 * Copy left, mark right, map full width beneath both. The copy is
		 * capped at 46ch for a readable measure, which on this panel's real
		 * ~730px content area leaves a wide empty column to its right — the
		 * mark now fills that instead of the ground sitting bare (owner:
		 * "logo should fill the empty space on the right").
		 *
		 * The map deliberately still spans BOTH columns. Measured reason,
		 * 1440x950: `.panel` runs the full viewport width, but its body
		 * starts to the right of the fixed paper pane, so the real content
		 * area is ~730px even though the container query sees 1440. Giving
		 * the map its own column as well would leave it a stamp; spanning
		 * hands it the whole width.
		 */
		.c-vitejte__layout {
			display: grid;
			grid-template-columns: minmax(0, 46ch) minmax(0, 1fr);
			grid-template-areas:
				'copy  mark'
				'arrow mark'
				'map   map';
			column-gap: var(--space-xl);
			row-gap: var(--space-md);
		}

		.c-vitejte__copy {
			grid-area: copy;
			max-inline-size: 46ch;
		}

		/*
		 * Sized off its own column rather than a fixed width, so it grows
		 * and shrinks with the ground actually available instead of needing
		 * a breakpoint of its own. Capped so it stays a mark beside the
		 * copy and never outgrows the heading it sits next to.
		 */
		.c-vitejte__mark {
			display: block;
			grid-area: mark;
			align-self: center;
			justify-self: center;
			inline-size: min(100%, 15rem);
		}

		/*
		 * Sits BETWEEN the address line and the map, nudged in from the
		 * left edge so it reads as a stroke drawn from the end of the
		 * address down onto the picture — the "we are right here" gesture
		 * (D-5). Rotated because the map is now beneath the copy rather
		 * than beside it. A previous side-by-side placement stranded it
		 * against the panel's right edge, touching neither thing it was
		 * meant to connect (caught in a real render, not from source).
		 * Still decorative and aria-hidden: the address above and the map
		 * link below carry the actual meaning.
		 */
		.c-vitejte__arrow {
			display: block;
			grid-area: arrow;
			justify-self: start;
			margin-inline-start: var(--space-lg);
			rotate: 34deg;
			inline-size: 4.5rem;
		}

		.c-vitejte__map {
			grid-area: map;
		}
	}

	/*
	 * `.panel--paper` (panels.css) sets fewer custom properties than the
	 * five colour grounds — only `--panel-bg`/`--panel-fg`, no `-fg-2`/
	 * `-fg-3` — so the arrow's `--panel-fg-2` fallback above resolves to
	 * plain `currentColor` there, which is already `--paper-ink` and
	 * reads correctly. The address line's bold weight needs no separate
	 * ink step either. Nothing further to override here for the default
	 * ground.
	 */

	.c-vitejte__map {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
	}

	/*
	 * Capped to read as a framed picture rather than a full-bleed hero —
	 * this also keeps the static map from ever becoming this panel's LCP
	 * element. Matches panels.css's own `.panel--paper .c-pribeh__media
	 * img` radius token.
	 */
	/*
	 * `block-size: auto` is load-bearing, not tidiness. wp_get_attachment_image()
	 * emits width="1000" height="625" presentational attributes; with no author
	 * `height` declaration those map to a DEFINITE used height, and a definite
	 * height means `aspect-ratio` never gets to derive one. Measured in a real
	 * render before this line existed: the image came out 277x625 — a portrait
	 * sliver — instead of the 16/10 card declared here. Resetting block-size to
	 * auto hands the ratio back to `aspect-ratio`.
	 *
	 * 16/10 matches the stitched source (1000x625) exactly, so `object-fit:
	 * cover` crops nothing at the declared ratio and the pin stays where the
	 * geocode put it. It is kept anyway for a client who replaces the picture
	 * with a differently-shaped one.
	 */
	.c-vitejte__map img,
	.c-vitejte__map iframe {
		display: block;
		inline-size: 100%;
		block-size: auto;
		aspect-ratio: 16 / 10;
		object-fit: cover;
		border-radius: 0.7rem;
		border: 0;
	}

	/*
	 * The consent-gated live-map swap (Task 3): by default no iframe
	 * exists at all, so this rule only ever matches once map-embed.js has
	 * actually created one. Hiding the static figure only on its own
	 * `load` event (never on mere insertion) is map-embed.js's own job —
	 * this rule only decides what each state LOOKS like once map-embed.js
	 * has set it.
	 */
	/*
	 * Once the live map has actually loaded, the sentence promising it
	 * would load has to go with the static picture it described. Without
	 * this the visitor accepts cookies, watches the interactive map appear
	 * — and is still told "Interaktivní mapa se načte po přijetí cookies"
	 * directly above it. Caught by accepting for real in a browser and
	 * looking; no structural check covers a sentence that is merely no
	 * longer true.
	 */
	.c-vitejte__map[data-cp-map-state='live'] .c-vitejte__map-note {
		display: none;
	}

	.c-vitejte__map[data-cp-map-state='live'] img {
		display: none;
	}

	/*
	 * The brand-green keyline framing the picture. `border: 0` is already
	 * declared on the image itself (it is shared with the live <iframe>,
	 * which must never carry a UA border), so the frame is drawn as a
	 * box-shadow ring rather than a border — that also keeps it outside
	 * the element's own box, so it cannot change the 16/10 ratio or nudge
	 * the layout.
	 */
	.c-vitejte__map img,
	.c-vitejte__map iframe {
		box-shadow: 0 0 0 3px var(--ground-green);
	}

	.c-vitejte__map-link {
		font-weight: var(--weight-bold);
		color: var(--ink-green-3);
		text-decoration-color: var(--ground-green);
		text-decoration-thickness: 0.1875rem;
		text-underline-offset: 0.25rem;
	}

	.c-vitejte__map-note {
		font-size: 0.875rem;
		color: var(--panel-fg-2, currentColor);
	}
}

/* SOURCE: components/composed.css */
/*
 * QUICK TASK 260901c — the hardcoded night ink is gone from this file.
 *
 * Every block here used `var(--panel-fg-2, currentColor)`, correct ONLY inside the
 * dark `.panel--night` ground they were first written for. This file's own
 * line 327 comment already flagged that as a trap, and it sprang the moment
 * these blocks started rendering on other grounds: the story's body copy
 * came out light grey on green, and the step descriptions came out pink on
 * green. Unreadable, and shipped.
 *
 * They now read `--panel-fg-2/-3`, which every ground modifier in
 * panels.css and dock.css already aliases to that ground's OWN verified ink
 * step. The block no longer needs to know which ground it is on, which is
 * the only version of this that cannot break again the next time one moves.
 */
/*
 * composed.css — the folded sections (D-07), written fresh against the
 * approved design, replacing the deleted `sections.css` (Phase 02.5
 * Task 2). Hand-authored, native nesting, no preprocessor, no bundler
 * (D-16). Wrapped entirely in the `components` layer declared by main.css.
 *
 * Placements and treatments, as of Phase 02.6-03 (D-02, D-03: `proc-my`,
 * `reference` and `cenik` all now declare `placement => overlay`,
 * replacing what used to be a three-way `panel`/`pane`/`panel` split):
 *   - `.c-lektori`, `.c-pribeh`, `.c-jak-funguje` (placement: composed) —
 *     sub-blocks `proc-my` folds into its own claim/mechanism/people/
 *     origin reading order. They declare no ground and no ink of their
 *     own — `color: inherit` — and their headings sit one visual step
 *     below `.panel__heading` so the folded sections read as one
 *     argument, not as several panels of equal weight stacked in one.
 *   - `.c-reference` (placement: overlay) — narrower-measure block, no
 *     ground of its own — inherits the overlay's paper ink tokens.
 *     `.c-nahravky` is retired (D-06, Phase 02.6) but its own shared rule
 *     is left in place rather than pruned mid-selector-list; it matches
 *     nothing on the page today.
 *   - `.c-cenik` (placement: overlay as of this plan; composed switch in
 *     section-cenik.php) — its own price list, styled with `currentColor`
 *     the same way panels.css's `.c-price-row` is.
 *   - `.c-kontakt` (placement: pane) — unchanged by this plan.
 *   - The `.overlay--pane`-scoped block near the end of this file (Phase
 *     02.6-03, Task 2) is the FOURTH treatment this docblock's original
 *     three-way split didn't anticipate: every one of the above, once
 *     actually rendered inside the left overlay rather than a full-bleed
 *     panel, needs its own measure, its own reduced heading/fact scale,
 *     and — the one real defect this plan found by rendering rather than
 *     reading — the correct PAPER ink steps rather than the NIGHT ink
 *     steps several of these blocks hardcoded back when they only ever
 *     rendered inside the dark "Proč my" panel. See that block's own
 *     comment for the full reasoning.
 *
 * Every block shares one base rule (`.c-lektori, .c-pribeh, .c-jak-funguje,
 * .c-reference, .c-nahravky, .c-cenik, .c-kontakt`) for the vertical
 * rhythm and heading treatment they all need — the one thing worth
 * carrying across from the deleted sections.css, carried as a decision
 * rather than as copied text (Task 2's own instruction).
 *
 * Same negatives as every other file in this system: no 1px hairline, no
 * gradient of any kind, no accumulated effect (D-02), no `opacity` on any
 * text-bearing selector. No @keyframes and no transition live here —
 * motion is components/motion.css.
 */

@layer components {
	.c-lektori,
	.c-pribeh,
	.c-jak-funguje,
	.c-reference,
	.c-nahravky,
	.c-cenik,
	.c-kontakt {
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
	}

	.c-lektori__heading,
	.c-pribeh__heading,
	.c-jak-funguje__heading,
	.c-reference__heading,
	.c-nahravky__heading,
	.c-cenik__heading,
	.c-kontakt__heading {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: clamp(1.25rem, 1rem + 1.4vw, 1.75rem);
		line-height: 1.1;
		letter-spacing: -0.015em;
	}

	/*
	 * --- Composed sub-blocks: no ground, no ink token of their own ---
	 * `color: inherit` from `.panel--night` (or whichever ground hosts
	 * them) — `.c-pribeh`/`.c-jak-funguje` still fold INSIDE the "Proč my"
	 * panel (D-07); `.c-lektori` no longer does (Phase 06, Plan 07,
	 * D-06-15 — it is now dispatched at top level, its own full-width
	 * panel), but the rule is unchanged either way: a top-level
	 * `.panel--{ground}` sets its own `color` exactly like the composed
	 * host did, so inheriting it needs no ground/ink token here regardless
	 * of which panel this sits inside. Their headings read smaller than
	 * `.panel__heading` (clamp above tops out at 1.75rem vs. the panel
	 * heading's clamp up to 4.5rem) so the panel keeps one dominant voice.
	 */
	.c-lektori,
	.c-pribeh,
	.c-jak-funguje {
		color: inherit;
	}

	/*
	 * Phase 06, Plan 07 (DESIGN-23): `.c-teacher-grid` and
	 * `.c-testimonial-grid` used to share one rule. Split apart here —
	 * DESIGN-23 is scoped to the lector grid only; `.c-testimonial-grid`
	 * (reference cards) keeps its previous `auto-fit`/`1fr` behaviour
	 * unchanged (out of this plan's own scope).
	 *
	 * The defect (verified live, then reproduced and fixed by rendering at
	 * 1/2/3/6 rows, never by reasoning about the rule alone,
	 * tools/tests/browser/lector-card-bounds.mjs): `auto-fit` COLLAPSES
	 * empty tracks to 0 width and lets the remaining, real track(s) grow to
	 * fill the `1fr` maximum — with one lector card, that one track is the
	 * entire `.panel__body` width (~1776px on a 1920px screen), and the
	 * card's own uncapped photo (`width: 100%` below) grows with it.
	 * `auto-fill` does NOT collapse empty tracks — it keeps them present
	 * (invisible, but real, participating in the track-sizing algorithm),
	 * so a single real card sits in its own bounded track and the
	 * remaining tracks stay empty rather than being absorbed into it. Both
	 * halves of the fix are required together, not either alone (a capped
	 * photo inside an uncapped card still produces a stretched card with a
	 * small picture in it): the grid switches to `auto-fill` with a fixed
	 * card basis, AND `.c-teacher-card` itself carries an explicit
	 * `max-width` matching `tools/tests/browser/lector-card-bounds.mjs`'s
	 * own declared `MAX_CARD_WIDTH_PX` (400px; 20rem below clears it with
	 * margin at a 16px root, while staying comfortably above the grid's
	 * own 15rem minimum so a real photo/name/role/bio still fit).
	 */
	.c-teacher-grid {
		display: grid;
		gap: var(--space-lg);
		grid-template-columns: repeat(auto-fill, minmax(15rem, 20rem));
		justify-content: start;
	}

	.c-testimonial-grid {
		display: grid;
		gap: var(--space-lg);
		grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	}

	.c-teacher-card {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
		max-width: 20rem;
	}

	.c-teacher-card__photo img {
		width: 100%;
		height: auto;
		border-radius: 0.4rem;
		margin-bottom: var(--space-sm);
	}

	.c-teacher-card__name {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 1.0625rem;
	}

	.c-teacher-card__role {
		font-size: 0.8125rem;
		font-weight: var(--weight-bold);
		color: var(--panel-fg-2, currentColor);
	}

	.c-teacher-card__bio {
		font-size: 0.9375rem;
		line-height: 1.5;
		color: var(--panel-fg-2, currentColor);
	}

	/*
	 * Phase 06, Plan 07 (CONT-23): the two-column layout `.c-lektori` now
	 * dispatches at top level (D-06-15) — the lector list on the left,
	 * the office gallery on the right, paired at render time from a
	 * sibling instance (section-lektori.php). Container-query, not a
	 * viewport media query — `.panel__body` is already the container-query
	 * root every top-level panel establishes (panels.css), so this reads
	 * the SAME available width panels.css's own 2-column programme layout
	 * reads, not the viewport.
	 *
	 * Single column (the office gallery stacks below the lector list) is
	 * the correct default and the correct fallback for a container too
	 * narrow for two comfortable columns — `.has-gallery` is only ever
	 * absent when section-lektori.php's own buffered check found nothing
	 * to render on the right (CONT-23's "absent, not empty"), in which
	 * case there is no second column to place regardless of width.
	 */
	.c-lektori__layout {
		display: grid;
		gap: var(--space-xl);
	}

	@container (min-width: 48rem) {
		.c-lektori__layout.has-gallery {
			grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
			align-items: start;
		}
	}

	.c-pribeh {
		display: grid;
		gap: var(--space-xl);
	}

	@container (min-width: 640px) {
		.c-pribeh {
			grid-template-columns: 1fr 1fr;
			align-items: center;
		}
	}

	.c-pribeh__media img {
		width: 100%;
		height: auto;
		border-radius: 0.4rem;
	}

	.c-pribeh__body {
		font-size: 1rem;
		line-height: 1.55;
		color: var(--panel-fg-2, currentColor);
	}

	.c-step-list {
		display: grid;
		gap: var(--space-lg);
		grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
		counter-reset: none;
	}

	.c-step-card {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
	}

	.c-step-card__badge {
		font-family: var(--font-serif);
		font-size: 1.75rem;
		line-height: 1;
		color: var(--panel-fg-2, currentColor);
	}

	.c-step-card__icon img {
		width: 2rem;
		height: 2rem;
	}

	.c-step-card__title {
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 1rem;
	}

	.c-step-card__desc {
		font-size: 0.9375rem;
		line-height: 1.5;
		color: var(--panel-fg-2, currentColor);
	}

	/*
	 * --- Pane blocks: fold INTO the fixed left pane, D-07 --- No ground
	 * of their own — they inherit `.glass`'s paper background and read
	 * its `--paper-ink*` tokens, at a narrower measure than the pane's
	 * own 42ch pitch/quote text since they sit lower, in a denser,
	 * more-reference-like position.
	 */
	.c-reference,
	.c-nahravky {
		color: var(--paper-ink, inherit);
	}

	.c-testimonial-card {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
	}

	.c-testimonial-card__quote {
		font-family: var(--font-serif);
		font-style: italic;
		font-size: 1.0625rem;
		line-height: 1.4;
	}

	.c-testimonial-card__attribution {
		display: flex;
		gap: var(--space-xs);
		font-size: 0.8125rem;
	}

	.c-testimonial-card__name {
		font-weight: var(--weight-bold);
	}

	.c-testimonial-card__tag {
		color: var(--paper-ink-2, currentColor);
	}

	.c-lesson-list {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
	}

	.c-lesson-list__item {
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
	}

	.c-lesson-list__title {
		font-weight: var(--weight-bold);
		font-size: 0.9375rem;
	}

	.c-lesson-list__level {
		font-size: 0.75rem;
		font-weight: var(--weight-bold);
		color: var(--paper-ink-2, currentColor);
	}

	.c-lesson-list__desc {
		font-size: 0.875rem;
		line-height: 1.45;
		color: var(--paper-ink-2, currentColor);
	}

	/*
	 * --- Ceník (own panel, placement: panel) and Kontakt (pane,
	 * placement: pane), D-07's addendum --- Ceník is not one of the six
	 * orphaned sections; it keeps its own home, so its price list is
	 * styled with `currentColor` the same way panels.css's `.c-price-row`
	 * is — whatever ground plan 02.5-06 ultimately wraps it in, the price
	 * treatment already matches every other price on the page.
	 */
	.c-price-list {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
	}

	.c-price-list__item {
		display: flex;
		align-items: baseline;
		flex-wrap: wrap;
		gap: var(--space-sm);
	}

	.c-price-list__name {
		font-weight: var(--weight-bold);
	}

	.c-price-list__note {
		font-size: 0.8125rem;
		color: inherit;
	}

	.c-kontakt {
		color: var(--paper-ink, inherit);
	}

	.c-kontakt__body {
		color: var(--paper-ink-2, currentColor);
	}

	.c-contact-block {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
		font-size: 0.9375rem;
	}

	.c-contact-block__address,
	.c-contact-block__hours {
		color: var(--paper-ink-2, currentColor);
	}

	.c-contact-block__phone a,
	.c-contact-block__email a,
	.c-contact-block__map a {
		font-weight: var(--weight-bold);
	}

	/*
	 * --- The left overlay's own reading rhythm (Phase 02.6-03, Task 2,
	 * D-02/D-03) ---
	 *
	 * Keyed on `.overlay--pane` — the overlay's OWN root class
	 * (overlay.css) — rather than on any one instance's class, so a
	 * fourth type given the `overlay` placement later inherits this
	 * measure automatically instead of needing a fourth copy of these
	 * rules. Every selector below is a descendant of `.overlay--pane`,
	 * which gives it enough specificity to win over this file's own
	 * un-scoped rules above regardless of source order.
	 *
	 * THE REAL BUG THIS BLOCK FIXES, found by rendering and reading, not
	 * by reasoning about the CSS (D-19): `.c-teacher-card__role`,
	 * `.c-teacher-card__bio`, `.c-pribeh__body`, `.c-step-card__badge`
	 * and `.c-step-card__desc` (all above, in this same file) hardcode
	 * `color: var(--panel-fg-2, currentColor)` — correct ONLY when they
	 * render inside the dark `.panel--night` ground they were originally
	 * written for. `proc-my` (which folds all three) now ALWAYS renders
	 * inside this overlay instead, on the pane's light paper ground.
	 * `--ink-night-2` (`#BBBFB9`, computed against the night ground at
	 * 9.99:1) painted on `--paper` (`#F7F7F3`) is two light colours
	 * against each other — effectively invisible secondary/body text,
	 * not merely a fade. The fix is two-part:
	 *   1. Every selector elsewhere in this file that reads
	 *      `var(--panel-fg-2, currentColor)` / `var(--panel-fg-3,
	 *      currentColor)` (panels.css's `.panel__lede`,
	 *      `.panel__fact-label`, `.c-price__dph`, and others) resolves
	 *      those custom properties from its nearest ancestor that sets
	 *      them — normally a `.panel--{ground}` modifier. Inside the
	 *      overlay there is no such ancestor, so redeclaring both
	 *      properties once, here, on `.overlay--pane` itself, fixes every
	 *      one of those consumers at once via ordinary custom-property
	 *      inheritance — no per-selector override needed for them.
	 *   2. The five HARDCODED `--ink-night-2` selectors above read a
	 *      literal ground-specific token, not `--panel-fg-2` — custom
	 *      property inheritance cannot fix a literal reference, so each
	 *      is overridden directly below, scoped to `.overlay--pane`.
	 * Every value used is one of base.css's own computed paper-ink
	 * steps — no opacity, no fade, matching the discipline
	 * 02.5-CONTRAST.md established and this plan's own prohibition on
	 * relaxing it.
	 */
	.overlay--pane {
		--panel-fg-2: var(--paper-ink-2);
		--panel-fg-3: var(--paper-ink-3, var(--paper-ink-2));
	}

	.overlay--pane .c-teacher-card__role,
	.overlay--pane .c-teacher-card__bio,
	.overlay--pane .c-pribeh__body,
	.overlay--pane .c-step-card__badge,
	.overlay--pane .c-step-card__desc {
		color: var(--paper-ink-2, currentColor);
	}

	/*
	 * The reading rhythm itself: a shared maximum measure on every
	 * top-level block the overlay dispatches — `proc-my`'s own four
	 * fold wrappers (`.panel__claim`/`.panel__mechanism`/`.panel__people`/
	 * `.panel__origin`, which carry no styling of their own outside a
	 * full-bleed panel's container-query grid — see this theme's own
	 * `.panel__fold` comment), `.c-reference` and `.c-cenik` — so no
	 * grid or paragraph inside the overlay ever runs wider than a
	 * comfortable line length, matching `.panel__lede`'s own 38ch
	 * precedent rather than the full-bleed panel's own much wider
	 * columns. A consistent vertical gap between the THREE composed
	 * instances (about-us, references, prices) beyond the ordinary flex
	 * gap `.overlay--pane` already sets between every direct child — a
	 * visible but unobtrusive separation with no hairline, no border, no
	 * gradient (D-02's own negatives): more breathing room here than
	 * between `proc-my`'s own four internal fold groups, so the three
	 * composed instances still read as three deliberate movements inside
	 * one considered surface.
	 */
	.overlay--pane .panel__claim,
	.overlay--pane .panel__mechanism,
	.overlay--pane .panel__people,
	.overlay--pane .panel__origin,
	.overlay--pane .c-reference,
	.overlay--pane .c-cenik {
		max-width: 38rem;
	}

	.overlay--pane .c-reference,
	.overlay--pane .c-cenik {
		margin-top: var(--space-lg);
		padding-top: var(--space-lg);
	}

	/*
	 * Heading/fact scale, one step below the panel scale (this file's own
	 * shared `.c-*__heading` rule already does this correctly for
	 * `.c-reference__heading`/`.c-cenik__heading` — both are `<h3>` when
	 * composed and already sized off that shared rule, not off
	 * panels.css). `.panel__heading` and `.panel__fact-value` are the two
	 * proc-my selectors still carrying panels.css's OWN full-bleed scale
	 * (up to 4.5rem / 2.25rem) because proc-my's own top-level claim
	 * heading and stat figures were never given a composed-heading-scale
	 * class of their own — they render through the same panels.css rules
	 * a full-bleed programme panel uses. Reduced here to sit clearly
	 * below a genuine panel heading while still reading as the overlay's
	 * own leading voice (the "claim" that opens it).
	 */
	.overlay--pane .panel__heading {
		font-size: clamp(1.75rem, 1.3rem + 2vw, 2.5rem);
	}

	.overlay--pane .panel__fact-value {
		font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem);
		white-space: nowrap;
	}

	/*
	 * About-us: the teacher grid (`lektori`) and the step list
	 * (`jak-funguje`) were both laid out with `auto-fit`/`minmax()` for a
	 * panel roughly two-thirds of the viewport wide (D-07's own fold).
	 * Forced to a single column at the overlay's own, much narrower
	 * measure, so five teacher cards / four step cards read top to
	 * bottom rather than wrapping into an uneven, cramped multi-column
	 * grid.
	 */
	.overlay--pane .c-teacher-grid,
	.overlay--pane .c-step-list {
		grid-template-columns: 1fr;
	}

	/*
	 * References: `section-reference.php` already applies
	 * `c-testimonial-grid--stack` when composed; this is that modifier's
	 * first real definition. Eight testimonials, one (Lenka S., about
	 * Anežka) a full 727-character paragraph — single column, generous
	 * gap between cards, and DELIBERATELY no `max-height`, no
	 * `line-clamp`, no `text-overflow: ellipsis` on the quote: truncating
	 * a client's own written testimonial with CSS is a content decision
	 * dressed as a style, which this project does not make silently. If
	 * eight full quotes read as too long a scroll, that is reported in
	 * the SUMMARY for the owner to decide — a scrolling overlay is an
	 * accepted answer, a clipped sentence is not.
	 */
	.overlay--pane .c-testimonial-grid--stack {
		grid-template-columns: 1fr;
		gap: var(--space-xl);
	}

	/*
	 * Price list: a clean two-column rhythm at the overlay's measure —
	 * name (and, when present, note) on the left, the price row flush
	 * right on the same first line — without touching the existing
	 * name/note/price DOM order in section-cenik.php. `margin-inline-start:
	 * auto` on `.c-price-row` pushes it to the far edge of the flex line;
	 * `.c-price-list__note`'s `flex: 1 1 100%` (with `order: 3`) forces it
	 * onto its own full-width line below, whether or not a price is
	 * present. The DPH clause keeps its OWN metadata styling throughout —
	 * panels.css's `.c-price__dph` already sets its own smaller
	 * `font-size`/`font-weight` independent of `.c-price__amount`'s; this
	 * block does not touch either, and the `--panel-fg-3` fix above (not
	 * a fallback to the amount's ink) is what gives it a genuinely muted
	 * metadata tone here rather than the same full-strength ink as the
	 * amount beside it.
	 */
	.overlay--pane .c-price-list__item {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		column-gap: var(--space-md);
		row-gap: 0.15rem;
	}

	.overlay--pane .c-price-list__name {
		flex: 1 1 auto;
	}

	.overlay--pane .c-price-list__note {
		flex: 1 1 100%;
		order: 3;
	}

	.overlay--pane .c-price-list__item .c-price-row {
		flex: 0 0 auto;
		margin-inline-start: auto;
	}

	/*
	 * ---- Quick task 260901c: the folds read as folds --------------------
	 *
	 * "Proč my" composes four blocks — the claim, how it works, the people,
	 * the story — and they were separated by nothing. "Jak to u nás
	 * funguje" sat directly against the stat row above it and "Lektoři"
	 * against the last step, so the whole section read as one undivided
	 * column of text at four different sizes.
	 *
	 * Written for the panel it is now, not the overlay it used to be: real
	 * air between folds, a hairline where one ends, and the steps on a grid
	 * that uses the full width instead of wrapping 3 + 1.
	 */
	.c-jak-funguje,
	.c-lektori,
	.c-pribeh {
		margin-block-start: clamp(var(--space-2xl), 5vw, var(--space-4xl));
		padding-block-start: clamp(var(--space-xl), 3vw, var(--space-2xl));
		border-block-start: 1.5px solid currentColor;
	}

	.c-jak-funguje__heading,
	.c-lektori__heading,
	.c-pribeh__heading {
		margin-block-end: var(--space-md);
	}

	.c-jak-funguje__intro {
		margin-block-end: clamp(var(--space-xl), 3vw, var(--space-2xl));
	}

	@media (min-width: 1000px) {
		.c-step-list {
			display: grid;
			grid-template-columns: repeat(4, minmax(0, 1fr));
			gap: clamp(var(--space-lg), 2.4vw, var(--space-2xl));
		}

		/* the teacher cards use the width instead of stacking */
		.c-lektori > div:has(.c-teacher-card),
		.c-lektori ul:has(.c-teacher-card) {
			display: grid;
			grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
			gap: clamp(var(--space-lg), 2.4vw, var(--space-2xl));
		}
	}

	/*
	 * Moved here from panels.css (quick task 260901c): main.css imports
	 * composed.css AFTER panels.css, so this file's own
	 * `.c-testimonial-card__quote` rule was winning on source order and the
	 * testimonial stayed small italic body copy. Same defect the door and
	 * the fit ladder both hit — at equal specificity the later file wins,
	 * and in this theme that is almost never the one you are editing.
	 */
	/*
	 * ---- Quick task 260901c: References look like references -----------
	 *
	 * Promoted out of an overlay fold, this was rendering as a heading and
	 * a paragraph — indistinguishable from any other block of copy on the
	 * page. A testimonial has to read as somebody SAYING something, so the
	 * quote leads at display size in the serif and the attribution sits
	 * under it, quiet, where a byline belongs.
	 */
	.panel--paper .c-testimonial-card {
		background: none;
		border: 0;
		padding: 0;
		max-width: 60ch;
	}

	.panel--paper .c-testimonial-card__quote {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(1.5rem, 1rem + 2.2vw, 2.6rem);
		line-height: 1.18;
		letter-spacing: -0.014em;
		margin-block-end: var(--space-lg);
	}

	.panel--paper .c-testimonial-card__attribution {
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		gap: var(--space-sm);
		font-size: 0.9375rem;
	}

	.panel--paper .c-testimonial-card__name {
		font-weight: var(--weight-bold, 700);
	}

	.panel--paper .c-testimonial-card__tag {
		color: var(--paper-ink-2);
	}

	/* more than one testimonial shares the width rather than stacking */
	@media (min-width: 1000px) {
		.panel--paper .c-reference > div:not(.c-reference__heading) {
			display: grid;
			grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
			gap: clamp(var(--space-2xl), 4vw, var(--space-4xl));
		}
	}
}

/* SOURCE: components/grain.css */
/*
 * grain.css — the two texture layers the approved design carries
 * (02.4-SIGNOFF.md "Texture and depth"), deliberately isolated in their
 * own file (Phase 02.5 Task 3). Hand-authored, no preprocessor, no
 * bundler (D-16). Wrapped entirely in the `components` layer declared by
 * main.css.
 *
 * This file holds the phase's single HIGHEST-RISK compositing rule: a
 * fixed, full-viewport, blended pseudo-element sitting above content that
 * scrolls continuously beneath it forces the browser to keep an
 * intermediate compositing surface alive for the life of the page, and
 * that is exactly what the manual CPU-6x/Fast-3G gate in plan 02.5-09 is
 * most likely to catch. Isolating both blended layers here — rather than
 * scattering them across panels.css/glass.css — means the fallback below
 * is a one-file edit, not a hunt across the whole component split.
 *
 * DOCUMENTED FALLBACK, NOT APPLIED (Phase 6 owns remediation if plan
 * 02.5-09's gate fails, per this plan's scope decision): drop
 * `mix-blend-mode: multiply` from both rules below and pre-bake each
 * texture at its target opacity into a static, non-blended PNG/SVG
 * overlay instead — the same visual weight without asking the compositor
 * to blend a moving layer against everything beneath it every frame.
 */

@layer components {
	/*
	 * Grain — fractal-noise SVG data URI on a fixed, full-viewport
	 * pseudo-element at 5% opacity, multiply-blended, exactly as approved.
	 * `body::before` rather than `::after` because `::after` on `body` in
	 * this theme's markup order would otherwise sit behind `.panels`'
	 * later paint order at the same stacking level without an explicit
	 * z-index — `pointer-events: none` keeps it out of the hit-test tree
	 * either way, but a high z-index is still required so it composites
	 * above both `.glass` (z-index 10) and every `.panel`.
	 */
	body::after {
		content: '';
		position: fixed;
		inset: 0;
		z-index: 200;
		pointer-events: none;
		opacity: 0.05;
		mix-blend-mode: multiply;
		background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
	}

	/*
	 * Halftone — a repeating dot pattern on every full-bleed panel, 13%
	 * black (the SVG circle's own `fill-opacity`), multiply-blended, at a
	 * further 55% element opacity exactly as approved — both numbers
	 * ported verbatim from 02.4-MOCKUP-v9-split.html rather than
	 * re-derived, since this is a purely decorative shape (never text) and
	 * the approved design's own composited result is the target, not a
	 * value this plan re-invents. Sits at the panel's own paint order
	 * (`z-index: 0`, implicit) below `.panel > *`'s `z-index: 1`
	 * (panels.css), so every panel's real content always paints above its
	 * own texture.
	 */
	.panel::before {
		content: '';
		position: absolute;
		inset: 0;
		pointer-events: none;
		opacity: 0.55;
		mix-blend-mode: multiply;
		background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11'%3E%3Ccircle cx='2' cy='2' r='1.05' fill='%23000' fill-opacity='0.13'/%3E%3C/svg%3E");
	}
}

/* SOURCE: components/motion.css */
/*
 * motion.css — every keyframe and every transition in the approved design,
 * in one file (Phase 02.5 Task 3), plus the single `prefers-reduced-motion`
 * branch that silences all of it. Hand-authored, no preprocessor, no
 * bundler (D-16). Wrapped entirely in the `components` layer declared by
 * main.css.
 *
 * DESIGN-15: only `transform` and `opacity` are ever the ANIMATED property
 * in this file — no height, no width, no colour, no shadow, no filter.
 * The mockup's own `.btn:hover` also transitions `box-shadow`; that
 * transition is deliberately NOT ported here for exactly this reason —
 * DESIGN-15 is a project rule this plan enforces, not a line-for-line
 * mockup port. No `ease-in` timing function appears anywhere below;
 * entrances use `--e-out` (an ease-out curve, base.css) — the design cuts
 * rather than fades between states.
 *
 * PROGRESSIVE ENHANCEMENT / D-18 CONTRACT: every rule in the "entrance"
 * block below is scoped under a `.js-motion` class on `<body>`, which only
 * a script can add (plan 02.5-08's IntersectionObserver island — the
 * keyframes and the gating class live here; the script only ever toggles
 * `.js-motion` on `<body>` and `.is-visible` — plus `.is-instant`, the
 * entrance's own escape hatch documented at that rule below — on a
 * `.panel`, per this plan's own action text: "the script only toggles
 * the class"). Without
 * that script — JS disabled, JS failed to load, a crawler — NOTHING here
 * applies and every panel is already in its fully visible, untransformed,
 * real-markup state. This is the same "content lives in real markup, JS
 * only enhances" contract section-hero.php's hidden-attribute swap
 * already established (D-18), applied to motion instead of visibility.
 *
 * EFFECT BUDGET (D-02's sixth hard negative — "no accumulation of
 * effects": adding one requires removing one). Six effects ship or are
 * reserved in this file:
 *   1. rise-in entrance per panel, staggered children (IntersectionObserver)
 *   2. bob — ambient idle loop, the panel index figure
 *   3. breathe — ambient idle loop, the brand mark
 *   4. swell — ambient idle loop, the detail dock's swatch bar
 *      (`.dock__swatch::after`, components/dock.css, plan 02.5-08)
 *   5. tick — ambient idle loop, RESERVED for the CTA's arrow glyph
 *      (`.panel__cta` has no arrow glyph of its own yet either — same
 *      reservation as #4, declared and unapplied)
 *   6. the small hover/focus transform nudges on the CTA, the brand row
 *      and the form controls — the one effect in this budget that is not
 *      a `@keyframes` loop, just a `transition` on `transform`
 * A future author adding a seventh removes one of the six above first.
 */

@layer components {
	/* ---------- Keyframes ---------- */

	@keyframes cp-rise {
		from {
			opacity: 0;
			transform: translateY(18px);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	@keyframes cp-bob {
		0%,
		100% {
			transform: translateY(0);
		}
		50% {
			transform: translateY(-4px);
		}
	}

	@keyframes cp-breathe {
		0%,
		100% {
			transform: scale(1);
		}
		50% {
			transform: scale(1.025);
		}
	}

	/*
	 * The detail dock's swatch bar (plan 02.5-08) — applied below, in the
	 * "Ambient idle loops" block, to `.dock__swatch::after`
	 * (components/dock.css). Declared here since Task 1 of that plan
	 * explicitly reserves motion.css as the one file that owns it: "No
	 * @keyframes and no transition here [dock.css] — motion.css owns
	 * motion, including the dock's swell, so the reduced-motion branch
	 * stays in one place."
	 */
	@keyframes cp-swell {
		0%,
		100% {
			transform: scaleX(1);
		}
		50% {
			transform: scaleX(1.3);
		}
	}

	/*
	 * Reserved for the CTA's arrow glyph (plan 02.5-08 or a later plan) —
	 * same reservation as cp-swell above; `.panel__cta` has no arrow glyph
	 * of its own on this plan's tree.
	 */
	@keyframes cp-tick {
		0%,
		100% {
			transform: translateX(0);
		}
		50% {
			transform: translateX(3px);
		}
	}

	/* ---------- Entrance: rise-in per panel, staggered children ---------- */

	/*
	 * Gated on `.js-motion` (added by plan 02.5-08's script) — see this
	 * file's docblock. Without it every child is already visible and
	 * untransformed; this rule only ever HIDES content that a script has
	 * promised to reveal, never content nothing will reveal.
	 *
	 * THAT PROMISE IS A PROMISE ABOUT panels.js, AND IT WAS ONCE ONLY HALF
	 * TRUE. Read narrowly it is a claim about which classes exist: if
	 * `.js-motion` is set then a script is running, and a running script
	 * adds `.is-visible`. But "a script has promised to reveal" is worth
	 * nothing unless the script can actually KEEP the promise, and until
	 * the fix recorded in `.planning/debug/resolved/fast-scroll-panel-reveal.md`
	 * it could not. `.is-visible` came only from an IntersectionObserver
	 * callback, and intersection observations are computed once per
	 * rendered frame — so a panel that crossed the threshold band entirely
	 * between two frames was never sampled, never revealed, and sat at
	 * `opacity: 0` with real Czech copy inside it until the visitor
	 * happened to scroll back over it. Pressing End on this page did it.
	 *
	 * That is the whole lesson of this rule, and it is why the docblock
	 * above is not enough on its own: a hiding rule is only ever as honest
	 * as the reveal is UNCONDITIONAL. panels.js now re-checks every
	 * unrevealed panel's geometry on every observer delivery rather than
	 * trusting that the right frame was sampled, which is what makes the
	 * sentence above true at any scroll speed instead of only at a
	 * comfortable one. Anyone loosening that re-check re-opens this rule.
	 */
	/*
	 * `.panel > *` never matches the `::before` texture pseudo-element
	 * grain.css adds — pseudo-elements are not real DOM children and the
	 * universal child selector cannot select them — so no exclusion is
	 * needed here for that layer to stay untouched by the entrance motion.
	 */
	.js-motion .panel > * {
		opacity: 0;
		transform: translateY(20px);
	}

	.js-motion .panel.is-visible > * {
		animation: cp-rise 0.6s var(--e-out) both;
	}

	.js-motion .panel.is-visible > *:nth-child(3) {
		animation-delay: 0.04s;
	}

	.js-motion .panel.is-visible > *:nth-child(4) {
		animation-delay: 0.11s;
	}

	.js-motion .panel.is-visible > *:nth-child(5) {
		animation-delay: 0.18s;
	}

	.js-motion .panel.is-visible > *:nth-child(6) {
		animation-delay: 0.25s;
	}

	/*
	 * `.is-instant` — revealed, but without the entrance.
	 *
	 * NOT a seventh effect in the budget above: it declares `animation:
	 * none` and adds no motion of any kind. It is the entrance's own
	 * escape hatch, and panels.js sets it in exactly three situations,
	 * all of them the same situation — the animation cannot be seen, so
	 * playing it would cost the content and buy nothing:
	 *
	 *   1. The panel is being revealed while the page is moving faster
	 *      than the entrance can finish. `cp-rise` takes 0.6s and the
	 *      last staggered child starts 0.25s late; a full-viewport panel
	 *      during a fast scroll is on screen for ~50ms and never got
	 *      past 0.43 opacity before it left. That is not an entrance,
	 *      that is a blank panel, and it is the measured cause of the
	 *      "scroll fast and you won't see any text" report.
	 *   2. The panel is already ABOVE the viewport when panels.js
	 *      discovers the observer never sampled it — animating an
	 *      entrance behind the visitor's back is pointless, and the only
	 *      thing that matters is that the copy is really there when they
	 *      scroll back.
	 *   3. No IntersectionObserver at all, where the alternative is ten
	 *      entrance animations firing at once.
	 *
	 * The shorthand is deliberate: `animation: none` also resets the
	 * `animation-delay` set by the four nth-child rules above, which this
	 * rule must follow in source order to beat them (they carry the same
	 * specificity). `opacity` and `transform` are restated because they
	 * are the two properties `.js-motion .panel > *` hides with, and
	 * cancelling the animation without them would leave the panel in the
	 * hidden state permanently — the exact bug this whole block exists to
	 * close.
	 */
	.js-motion .panel.is-visible.is-instant > * {
		animation: none;
		opacity: 1;
		transform: none;
	}

	/* ---------- Ambient idle loops ---------- */

	.js-motion .panel__index-num {
		animation: cp-bob 9s ease-in-out infinite;
	}

	.js-motion .glass__logo svg {
		animation: cp-breathe 7s ease-in-out 1.2s infinite;
	}

	/*
	 * The dock's swatch bar (plan 02.5-08, components/dock.css) — gated on
	 * `.js-motion` exactly like the two ambient loops above it, and further
	 * gated by `[hidden]` naturally: a hidden dock's `::after` still
	 * "runs" the animation per the DOM, but the element computes
	 * `display: none` (reset.css), so nothing paints or costs a frame
	 * until panels.js removes the attribute on open.
	 */
	.js-motion .dock__swatch::after {
		animation: cp-swell 8s ease-in-out 1s infinite;
	}

	/* ---------- Hover / focus transitions (transform only, DESIGN-15) ---------- */

	.panel__cta,
	.glass__brand,
	.glass__form-field input,
	.glass__form-field select {
		transition: transform var(--duration-fast) var(--e-out);
	}

	.panel__cta:hover {
		transform: translateY(-2px);
	}

	.panel__cta:active {
		transform: translateY(0) scale(0.98);
	}

	.glass__brand:hover {
		transform: translateY(-1px);
	}

	.glass__form-field input:focus-visible,
	.glass__form-field select:focus-visible {
		transform: translateY(-1px);
	}

	/* ---------- prefers-reduced-motion: the one branch that silences all of it ---------- */

	/*
	 * Disables every animation and every hover/focus transform transition
	 * declared above, and puts every panel child into its finished,
	 * fully-visible, untransformed state — never a shortened animation,
	 * never an element stuck invisible because its reveal animation was
	 * the thing that got disabled (the classic way this branch breaks).
	 * `.js-motion .panel > *` is included explicitly so a visitor whose
	 * browser both supports IntersectionObserver AND has reduced motion
	 * set never sees the pre-reveal hidden state at all.
	 */
	@media (prefers-reduced-motion: reduce) {
		.js-motion .panel > *,
		.js-motion .panel.is-visible > * {
			opacity: 1;
			transform: none;
			animation: none;
		}

		.js-motion .panel__index-num,
		.js-motion .glass__logo svg,
		.js-motion .dock__swatch::after {
			animation: none;
		}

		/*
		 * Phase 04: three more selectors join this branch, none silenced
		 * in their own file's own name (button.css/panels.css/player.css
		 * carry no branch of their own — motion.css's own docblock states
		 * it is the one place this design's motion is silenced, and three
		 * more files each carrying a branch is exactly how that claim
		 * stops being true). Each shipped in a quick task after this
		 * branch was last reviewed, which is why all three were missed
		 * until test-a11y-structure.php's own reduced-motion coverage
		 * check (Phase 04, A11Y-04) started failing the build over them:
		 *
		 *   - .c-btn (button.css) — the house button's own hover lift.
		 *   - .panels > section:not([data-section-type='programme']) .panel
		 *     (panels.css) — the panel stack's own layout transition, which
		 *     slides the whole column sideways when the pane releases; the
		 *     largest single movement on the page and the one most likely
		 *     to make a motion-sensitive reader ill.
		 *   - .c-player__button (player.css) — the audio player's own play
		 *     control lift.
		 *
		 * Each selector below is written CHARACTER-IDENTICAL to how it
		 * appears in its own file — test-a11y-structure.php's own check
		 * matches selector strings literally, and a silencing rule that
		 * does not name the selector it silences is a rule nobody can
		 * verify.
		 */
		.panel__cta,
		.glass__brand,
		.glass__form-field input,
		.glass__form-field select,
		.c-btn,
		.panels > section:not([data-section-type='programme']) .panel,
		.c-player__button {
			transition: none;
		}

		.panel__cta:hover,
		.panel__cta:active,
		.glass__brand:hover,
		.glass__form-field input:focus-visible,
		.glass__form-field select:focus-visible,
		.c-btn:hover,
		.c-player__button:hover {
			transform: none;
		}
	}

	/*
	 * The pane headline's typing caret (quick task 260831-vod). The
	 * headline itself is server-rendered and always present; this is only
	 * the blinking cursor shown WHILE it is being retyped, so a visitor
	 * with no JavaScript never sees a caret on static text.
	 */
	.glass__pitch-heading.is-typing::after {
		content: "";
		display: inline-block;
		inline-size: 2px;
		block-size: 0.82em;
		margin-inline-start: 3px;
		vertical-align: -0.06em;
		background: currentColor;
		animation: cp-caret 1.05s steps(1) infinite;
	}

	@keyframes cp-caret {
		50% {
			opacity: 0;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.glass__pitch-heading.is-typing::after {
			display: none;
		}
	}

	/*
	 * The detail overlay's entrance (quick task 260831-w37). `hidden`
	 * toggles `display`, and a transition cannot run across a display
	 * change — an animation on the shown state can, and plays once each
	 * time the element is revealed. Short and small: the ground behind it
	 * no longer changes at all (dock.css matches the panel's alpha), so
	 * this only has to carry the CONTENT in, not announce a new surface.
	 */
	.js-motion .dock:not([hidden]),
	.js-motion .overlay--pane:not([hidden]) {
		animation: cp-overlay-in 0.34s cubic-bezier(0.22, 0.61, 0.36, 1) both;
	}

	@keyframes cp-overlay-in {
		from {
			opacity: 0;
			transform: translateY(10px);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.js-motion .dock:not([hidden]),
		.js-motion .overlay--pane:not([hidden]) {
			animation: none;
		}
	}
}

/* SOURCE: components/dock.css */
/*
 * dock.css — the programme detail overlay (Phase 02.5-08, 02.4-SIGNOFF.md).
 * Hand-authored, native nesting, no preprocessor, no bundler (D-16). Wrapped
 * entirely in the `components` layer declared by main.css.
 *
 * TWO RENDERING MODES, ONE MARKUP (see template-parts/programme-dock.php's
 * own docblock for the full reasoning): `.dock` never carries a `hidden`
 * attribute at server-render time, so by default — no JavaScript running —
 * it is an ORDINARY IN-FLOW BLOCK, styled below with plain typography rules
 * and no positioning declaration of its own. `assets/js/panels.js` is the
 * only thing that ever promotes a dock into the approved fixed-overlay
 * chrome, and it does so by adding `.js-motion` to `<body>` — the same flag
 * motion.css already uses as "a script is genuinely running" (that file's
 * own docblock: "which only a script can add"). Reusing it here, rather
 * than inventing a second flag, is deliberate: both concerns share the
 * exact same precondition, and this codebase has exactly one such signal.
 * Every `position: fixed` / overlay-chrome rule below lives under
 * `.js-motion .dock` for exactly this reason — remove `.js-motion` from the
 * selector and this file silently regresses the no-JS reachability gate.
 *
 * `.dock--{ground}` sets BOTH its own `--dock-*` custom properties AND
 * aliases panels.css's `--panel-fg`/`--panel-fg-2`/`--panel-fg-3` to the
 * same computed values, so the dock's "Co je součástí" list can reuse
 * panels.css's `.panel__service`/`.panel__service-title`/
 * `.panel__service-desc`/`.c-price-row`/`.c-price__amount`/`.c-price__dph`
 * rules verbatim — one definition of "a course row with a title, a
 * description and a price," used by the panel's summary sluzby list
 * (historically) and this dock's full one, never two.
 *
 * Task 3 (02.5-CONTRAST.md recommendation 2, criterion 6): no `opacity` on
 * any body-text selector in this file — the kick, the lede, the list label
 * and the price note all read one of the three computed tone steps
 * (`currentColor`/`--panel-fg-2`/`--panel-fg-3`) instead of a fade, exactly
 * like panels.css and glass.css. The swatch bar is the one decorative
 * exception, and it is drawn on a `::after` pseudo-element specifically so
 * it qualifies as the "decorative shape, not text" boundary
 * test-contrast-ratios.php's classifier already draws elsewhere in this
 * system — its opacity (0.6) is not re-derived, it is the SAME value
 * panels.css's `.panel__head::after` already computed and verified against
 * all five panel grounds (worst case 3.19:1 on indigo, clearing the 3:1
 * non-text minimum with margin).
 *
 * No `@keyframes` live here — motion.css owns the dock's ambient swell
 * (its `cp-swell` keyframe was declared RESERVED for exactly this
 * selector by plan 02.5-05; this plan is the one that finally has
 * something to apply it to — see motion.css's own updated docblock).
 *
 * Phase 07.1, Plan 03 (D-07.1-05) is the ONE exception to "no transition
 * lives here": the phone accordion's grow/shrink (`.js-motion .dock`'s
 * `max-block-size`/`opacity` transition, further down, gated off entirely
 * at `min-width: 1000px`) is a Phase-07-era addition to a DIFFERENT
 * component than the one motion.css's shared branch was written for, so
 * — following `components/mobile.css`'s own precedent — it carries its
 * own `@media (prefers-reduced-motion: reduce)` branch in THIS file
 * rather than being deferred to motion.css's.
 *
 * Same hard negatives as everywhere else on the public page (D-02): no 1px
 * hairline, no gradient of any kind, no translucent or blurred surface.
 */

@layer components {
	/* ---------- Ground modifiers — alias panels.css's tone tokens so this
	   dock's reused .panel__service-*/.c-price-row rules pick up the right
	   tone with zero new CSS. ---------- */

	.dock--green {
		--dock-bg: var(--ground-green);
		--dock-fg: var(--ink-green);
		--panel-fg: var(--ink-green);
		--panel-fg-2: var(--ink-green-2);
		--panel-fg-3: var(--ink-green-3);
	}

	.dock--coral {
		--dock-bg: var(--ground-coral);
		--dock-fg: var(--ink-coral);
		--panel-fg: var(--ink-coral);
		--panel-fg-2: var(--ink-coral-2);
		--panel-fg-3: var(--ink-coral-3);
	}

	.dock--indigo {
		--dock-bg: var(--ground-indigo);
		--dock-fg: var(--ink-indigo);
		--panel-fg: var(--ink-indigo);
		--panel-fg-2: var(--ink-indigo-2);
		--panel-fg-3: var(--ink-indigo-3);
	}

	.dock--amber {
		--dock-bg: var(--ground-amber);
		--dock-fg: var(--ink-amber);
		--panel-fg: var(--ink-amber);
		--panel-fg-2: var(--ink-amber-2);
		--panel-fg-3: var(--ink-amber-3);
	}

	.dock--night {
		--dock-bg: var(--ground-night);
		--dock-fg: var(--ink-night);
		--panel-fg: var(--ink-night);
		--panel-fg-2: var(--ink-night-2);
		--panel-fg-3: var(--ink-night-3);
	}

	/* ---------- Base: the no-JS, in-flow rendering ---------- */

	.dock {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
		color: var(--dock-fg, var(--ink-green));
		/*
		 * Same 0.86 alpha as `.panel` (panels.css). The dock opens over a
		 * panel that is already letting the fixed photographic field show
		 * through; at full opacity the field vanished the instant the dock
		 * appeared and the ground visibly flattened, which read as a
		 * coloured slab dropping in rather than a detail view of the panel
		 * you were already looking at. Matching the alpha means the
		 * background does not change at all — only the content does.
		 *
		 * The alpha alone was NOT enough and made things worse before it
		 * made them better: the dock sits on top of the PANEL, not on top
		 * of the field, so at 0.86 the panel's own heading and lede bled
		 * straight through the dock's text. Found by rendering. The
		 * companion rule is `body.is-dock-open` below, which hides the
		 * panel's CONTENT while keeping its ground — so what remains
		 * behind the dock is exactly ground-over-field-over-base, the
		 * identical composite the panel was already showing.
		 *
		 * Contrast is unaffected for the same reason it is unaffected on
		 * .panel: .photo-field paints this same ground underneath as an
		 * opaque base plate, so the colour composited under dock text is
		 * the ground at full strength.
		 */
		background: color-mix(in srgb, var(--dock-bg, var(--ground-green)) 86%, transparent);
		padding: clamp(var(--space-xl), 6vw, var(--space-3xl)) clamp(var(--space-lg), 5vw, var(--space-2xl));
		/*
		 * Phase 07, Plan 03 (D-07-12): below 1000px this is the dock's
		 * WHOLE rendering — an in-flow block right under its own panel,
		 * never a fixed overlay (see `.js-motion .dock` further down,
		 * which now promotes to fixed only inside the desktop query). A
		 * scroll-into-view or an `#dock-{id}` anchor jump must not land
		 * this block's first line under the mobile-only fixed action bar
		 * (components/mobile-actions.css's `.m-actions`) — --m-bar-h is
		 * that bar's own reserved clearance token, 0px wherever it is not
		 * declared, which is exactly the correct fallback outside its
		 * subtree.
		 */
		scroll-margin-block-start: var(--m-bar-h, 0px);
		/*
		 * Same separator idiom `.dock__list > * + *` already uses just
		 * below — below 1000px this reads the inline expansion as
		 * attached to its own panel rather than an unexplained second
		 * block; cancelled in the desktop query beside the fixed-overlay
		 * promotion, matching `.dock__terms`'s own `border-top: 0` idiom.
		 */
		border-block-start: 1.5px solid currentColor;
	}

	.dock > * {
		position: relative;
		z-index: 1;
	}

	/*
	 * Phase 07.1, Plan 03 (D-07.1-05): the phone accordion's own
	 * grow/shrink. Scoped to `.js-motion .dock` — NOT bare `.dock` — for
	 * the identical reason every other script-conditional rule in this
	 * file is (see this file's own docblock, "Every position: fixed /
	 * overlay-chrome rule below lives under .js-motion .dock"): a no-JS
	 * phone visitor never gets `.js-motion` on `<body>`, so this collapse
	 * never applies to them, and the base `.dock` rule above (an
	 * ordinary, un-hidden, in-flow block) is their WHOLE rendering — the
	 * grow is something a script adds, not something a no-JS visitor
	 * loses. `overflow: hidden` and `max-block-size: 0` are what make the
	 * collapse actually invisible rather than merely un-grown; `growOpen`/
	 * `growShut` (assets/js/panels.js) are the only things that ever add
	 * or remove `.is-grown`.
	 *
	 * Duration/curve match `components/sitemap.css`'s own `.secnav__list`
	 * transition (`0.24s cubic-bezier(0.22, 0.61, 0.36, 1)`) — the
	 * project's own motion vocabulary, not a newly invented one.
	 */
	.js-motion .dock {
		overflow: hidden;
		max-block-size: 0;
		opacity: 0;
		/*
		 * The duration is a VARIABLE, not the flat 0.24s this rule used to
		 * carry (2026-09-07, owner: "the animation is more like a flash,
		 * not smooth open and collapse").
		 *
		 * 0.24s is `.secnav__list`'s duration, borrowed here for vocabulary
		 * consistency — but that list is a few hundred pixels tall and a
		 * programme detail measures 1952px at 390×844. The same quarter
		 * second across eight times the distance is ~8100px/second, roughly
		 * two and a half screens in a blink: the motion was not badly eased,
		 * it was simply far too fast to read as opening at all.
		 *
		 * panels.js writes `--dock-grow-dur` from the height it has already
		 * measured for the grow, clamped, so a short detail keeps a brisk
		 * open and a long one takes proportionally longer. The 0.24s
		 * fallback is what applies if the script never sets it — the
		 * no-JS/base state, unchanged.
		 */
		transition: max-block-size var(--dock-grow-dur, 0.24s) cubic-bezier(0.22, 0.61, 0.36, 1),
			opacity var(--dock-grow-dur, 0.24s) cubic-bezier(0.22, 0.61, 0.36, 1);
	}

	/*
	 * `max-block-size: none` is load-bearing, not decoration (fixed
	 * 2026-09-07, owner report: "it opens as it should with an animation
	 * but after that it snaps to hide the content").
	 *
	 * panels.js's `growOpen` animates an INLINE `max-block-size` from 0 to
	 * the measured `scrollHeight`, then — on `transitionend` — releases
	 * that inline style so a late-loading gallery image inside the detail
	 * is not clipped by a height measured before it loaded. Releasing an
	 * inline style hands the property back to the cascade, and the only
	 * declaration left standing was the base rule's `max-block-size: 0`
	 * directly above. So the dock played its full grow and then collapsed
	 * to zero height the instant the animation finished, at `opacity: 1` —
	 * fully "open" and completely empty.
	 *
	 * The open state must therefore declare its own height. `none` rather
	 * than a pixel value keeps the "no magic maximum" property `growOpen`'s
	 * own docblock relies on: the resting height is the element's content
	 * height, decided by layout, never a number typed here that some
	 * future longer detail would silently clip.
	 *
	 * The desktop query further down already sets `max-block-size: none`
	 * on `.dock` unconditionally, so this changes nothing above 1000px.
	 */
	.js-motion .dock.is-grown {
		max-block-size: none;
		opacity: 1;
	}

	/*
	 * Phase 02.6-05 (D-19, found by RENDERING the no-JS base state and
	 * reading the screenshot, not by reading this file): from 1000px up,
	 * glass.css's `.glass` sits FIXED over the page's own left 46vw at
	 * every scroll position, unconditionally — before `.js-motion` ever
	 * promotes THIS dock to its own fixed overlay chrome. Without this
	 * clearance, the dock's own in-flow base rendering (its ordinary,
	 * un-hidden, no-JS state — see this file's own docblock) starts flush
	 * left like an unpositioned block, so roughly the left two-thirds of
	 * every line of its own heading/course-row/price/recording text sits
	 * physically UNDER the opaque, always-fixed pane and is genuinely
	 * unreadable to a sighted desktop visitor with JavaScript off — even
	 * though the same text is present, unhidden, in the raw HTML, which is
	 * why no purely-textual check (test-nojs-content.php's existing
	 * checks, a curl-and-grep) had ever caught it. panels.css's own
	 * `.panel` rule already solved this identical geometry problem for the
	 * six programme panels (see that file's own docblock at this same
	 * media query) — the same fix, the same reasoning, applied here.
	 */
	@media (min-width: 1000px) {
		.dock {
			padding-left: calc(46vw + clamp(var(--space-lg), 4vw, var(--space-2xl)));
			padding-right: clamp(var(--space-lg), 5vw, var(--space-2xl));
			/*
			 * Phase 07, Plan 03 (D-07-12): the base rule's own separator
			 * (added above, the reason a phone's inline expansion reads as
			 * attached to its panel) is cancelled here — the desktop
			 * overlay is a floating fixed surface, not something sitting
			 * directly under its panel, so the same visual join would read
			 * as a stray rule across the top of the overlay instead.
			 * Matches `.dock__terms`'s own `border-top: 0` idiom.
			 */
			border-block-start: 0;
		}
	}

	/* ---------- JS-only promotion: the approved fixed overlay ---------- */

	/*
	 * Phase 07, Plan 03 (D-07-12): every rule that POSITIONS or SIZES the
	 * overlay now lives inside the `@media (min-width: 1000px)` block
	 * below, not here — below 1000px `.js-motion .dock` inherits only the
	 * base, in-flow `.dock` rules above (a relative, flex-column block in
	 * normal document flow). This is the desktop-only half of D-07-12: on
	 * a phone the fixed-overlay problem this rule solved does not exist —
	 * there is no split, no fixed left pane to sit beside, and no reason
	 * for a detail view to cover the whole viewport instead of expanding
	 * where the visitor already is.
	 */
	@media (min-width: 1000px) {
		.js-motion .dock {
			position: fixed;
			inset: 0;
			z-index: 20;
			/*
			 * Phase 07.1, Plan 03 (D-07.1-05): cancels the phone-only
			 * grow/collapse declarations added to the base `.js-motion
			 * .dock` rule above this query. A fixed overlay is never
			 * collapsed and never animates its own height. `overflow:
			 * visible` restores the X axis the base rule's `overflow:
			 * hidden` shorthand set; the explicit `overflow-y: auto`
			 * immediately below restores the Y axis.
			 */
			max-block-size: none;
			opacity: 1;
			transition: none;
			overflow: visible;
			overflow-y: auto;
			/*
			 * Phase 02.6-01 (Task 3): `safe center`, not bare `center` — a
			 * single, deliberate word added to this one declaration, not a
			 * sweep. Bare `center` on an `overflow-y: auto` flex column
			 * vertically centers content taller than the viewport by pushing
			 * its START past the top edge into negative, unreachable scroll
			 * territory — found by rendering and reading the actual computed
			 * position of `.dock__title` (D-19), not by inspecting the rule in
			 * isolation: a programme carrying enough course rows and
			 * recordings, or the left overlay's own much longer "Proč my" fold,
			 * both overflow 900px and were opening with their own heading
			 * scrolled off-screen above an unreachable negative offset. `safe`
			 * (CSS Box Alignment Level 3) keeps the original short-content
			 * centering this rule was written for, and falls back to start
			 * alignment the moment content overflows, so the FIRST child (this
			 * overlay's own close control, then its heading) is always
			 * reachable at `scrollTop: 0`.
			 */
			justify-content: safe center;
			gap: var(--space-lg);
			left: 46vw;
			padding: clamp(var(--space-xl), 3.2vw, var(--space-3xl));
		}
	}

	/*
	 * Above the split the dock stops being one long single column.
	 *
	 * The owner's framing: "the user only writes texts and provides assets,
	 * the formating is on us." So the layout is derived here from what a
	 * programme always has — an identity (kick, title, lede), a commercial
	 * summary (price, CTA, phone), and a list of courses — rather than left
	 * as whatever order the fields happen to be stored in.
	 *
	 * Identity and commercials hold the left column and STAY PUT while the
	 * course list scrolls beside them, so the price and the way to act on
	 * it are never scrolled away from. Course rows become a real list with
	 * rules between them instead of paragraphs separated by whitespace.
	 */
	@media (min-width: 1000px) {
		.js-motion .dock {
			display: grid;
			grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
			align-content: start;
			column-gap: clamp(var(--space-2xl), 4vw, var(--space-4xl));
			row-gap: var(--space-md);
		}

		/*
		 * Rows are pinned, not left to auto-placement. Auto-placement put
		 * the price, the CTA and the phone number BELOW the course list —
		 * a list that is taller than the whole identity column — so the
		 * commercial summary ended up somewhere nobody would scroll to.
		 * The identity column now stacks 2..8 and the list spans all of
		 * them beside it, which is the actual reading order: who this is
		 * for and what it costs on the left, what is in it on the right.
		 */
		.js-motion .dock > .dock__actions {
			grid-column: 1 / -1;
			grid-row: 1;
			margin-block-end: var(--space-lg);
		}

		.js-motion .dock__swatch      { grid-column: 1; grid-row: 2; }
		.js-motion .dock__kick        { grid-column: 1; grid-row: 3; }
		.js-motion .dock__title       { grid-column: 1; grid-row: 4; }
		.js-motion .dock__lede        { grid-column: 1; grid-row: 5; }
		.js-motion .dock__price-block { grid-column: 1; grid-row: 6; margin-block-start: var(--space-lg); }
		.js-motion .dock__call        { grid-column: 1; grid-row: 7; margin-block-start: 0; }

		/* both actions live in the bar; neither is placed on the grid */
		.js-motion .dock__cta {
			margin-block-start: 0;
		}

		/* the courses run the full height of the identity column beside it */
		.js-motion .dock__list  { grid-column: 2; grid-row: 2 / span 7; }
		.js-motion .dock__terms { grid-column: 2; }

		/* recordings and gallery close the view, full width beneath both */
		.js-motion .c-nahravky,
		.js-motion .c-gallery {
			grid-column: 1 / -1;
			margin-block-start: var(--space-2xl);
		}
	}

	/*
	 * A rule between course rows — but never under the list's own label,
	 * which is the list's first child rather than a row.
	 */
	.dock__list > * + *:not(.dock__list-label) {
		border-block-start: 1.5px solid currentColor;
		padding-block-start: var(--space-md);
	}

	/* ---------- Content ---------- */

	.dock__swatch {
		display: block;
		width: 5.5rem;
		height: 0.28rem;
	}

	.dock__swatch::after {
		content: '';
		display: block;
		width: 100%;
		height: 100%;
		border-radius: 2px;
		background: currentColor;
		opacity: 0.6;
		transform-origin: left center;
	}

	.dock__kick {
		font-family: var(--font-grotesk);
		font-size: 0.6875rem;
		font-weight: var(--weight-bold);
		letter-spacing: 0.11em;
		text-transform: uppercase;
		color: var(--panel-fg-3, currentColor);
	}

	.dock__title {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(2.2rem, 1.4rem + 3.4vw, 3.6rem);
		line-height: 0.98;
		letter-spacing: -0.024em;
		max-width: 12ch;
	}

	.dock__lede {
		max-width: 40ch;
		font-size: clamp(1rem, 0.9rem + 0.4vw, 1.0625rem);
		line-height: 1.5;
		color: var(--panel-fg-2, currentColor);
	}

	.dock__list-label,
	.dock__price-label {
		font-family: var(--font-grotesk);
		font-size: 0.6875rem;
		font-weight: var(--weight-bold);
		letter-spacing: 0.11em;
		text-transform: uppercase;
		color: var(--panel-fg-3, currentColor);
		margin-bottom: var(--space-sm);
	}

	.dock__list {
		display: grid;
		gap: var(--space-md);
		max-width: 46ch;
	}

	.dock__price-block {
		display: grid;
		gap: var(--space-xs);
	}

	/* Headline role for the dock's own summary price — Instrument Serif,
	   larger than the panel's own inline price line (panels.css's
	   .c-price__amount), matching 02.4-SIGNOFF.md's "prices" headline role. */
	.dock__price .c-price__amount {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(1.75rem, 1.2rem + 2.2vw, 2.75rem);
		letter-spacing: -0.02em;
		line-height: 1;
	}

	.dock__pricenote {
		font-size: 0.8125rem;
		color: var(--panel-fg-3, currentColor);
	}

	.dock__terms {
		border-top: 0;
		padding-top: var(--space-md);
	}

	/* ---------- The floating close control ---------- */

	/*
	 * The `.back` floating close pill (Phase 02.5-08) was RETIRED here by
	 * hand-edit (Phase 02.6-01, D-05) — a single deliberate removal of this
	 * one rule block, never a regex sweep (a recorded blocking anti-pattern
	 * in this project with two prior incidents). Each overlay now carries
	 * its own large close control instead (`.overlay__close`,
	 * components/overlay.css), rendered as that overlay's own FIRST child —
	 * see programme-dock.php's own docblock. The retired class name was
	 * grepped for across the theme after this edit and the page re-rendered
	 * to confirm nothing else referenced it.
	 */

	/* ---------- Scroll lock (Phase 02.5-08) ---------- */

	/*
	 * The actions bar. Both controls together, pinned to the top of the
	 * dock's own scroll container so the way out and the way forward are
	 * always in the same place — the course list is far taller than the
	 * identity column beside it, and the CTA used to scroll out of reach.
	 */
	.dock__actions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-sm);
		align-items: center;
	}

	/*
	 * In the bar the controls hug their labels rather than carrying the
	 * generous inline padding they use standing alone — two full-size slabs
	 * side by side wrapped onto two rows, which is exactly what putting
	 * them in one bar was meant to stop.
	 */
	.dock__actions .c-btn {
		/* `width: auto` is load-bearing: .panel__cta sets width:100% for its
		   own single-column measure, which made the CTA claim the whole bar
		   and push the close control onto a row of its own — the one thing
		   putting them in a bar was meant to stop. Measured, not assumed. */
		width: auto;
		padding-inline: var(--space-lg);
		min-height: var(--tap-target-min, 44px);
		flex: 0 1 auto;
	}

	/*
	 * No ground on the actions bar (2026-09-07, owner: "the button which is
	 * cutoff has a green background instead of transparent, same mistake is
	 * on desktop").
	 *
	 * This previously painted `--dock-bg` at FULL strength while the dock
	 * itself sits at 86% of that same colour, so the bar read as a distinctly
	 * lighter green band across the detail — the seam the owner is pointing
	 * at. The comment that justified it ("content scrolling under it is
	 * covered") describes the DESKTOP dock, which is `overflow-y: auto` and
	 * really does scroll its own content under a sticky bar. On a phone the
	 * dock is an ordinary in-flow block that grows inline and does not scroll
	 * independently at all, so there was never anything to cover there.
	 *
	 * Removed at both widths, as reported. The controls are legible against
	 * the dock's own ground without it — they are the same `.c-btn` pills the
	 * rest of the page floats on an unpainted ground. If desktop text is ever
	 * seen scrolling visibly behind this bar, the correct repair is to restore
	 * the ground INSIDE the `min-width: 1000px` query only, never to bring it
	 * back to the phone where it is purely a visible seam.
	 */
	.js-motion .dock__actions {
		position: sticky;
		inset-block-start: 0;
		z-index: 3;
		background: transparent;
		padding-block: var(--space-md);
		margin-block-start: calc(var(--space-md) * -1);
	}

	/*
	 * Phase 07, Plan 03 (D-07-12): both rules below moved inside the
	 * desktop-only query — each solves a fixed-overlay problem that does
	 * not exist inline. Below 1000px the visitor must still be able to
	 * scroll the page to read the expanded detail (there is no
	 * independently-scrolling fixed surface to lock the page BEHIND), and
	 * the panel above it must keep its own heading rather than going
	 * invisible above an ordinary in-flow block.
	 */
	@media (min-width: 1000px) {
		body.is-dock-open {
			overflow: hidden;
		}

		/*
		 * While an overlay is open the panel underneath keeps its GROUND and
		 * loses its CONTENT. That is what makes the open seamless: the dock is
		 * semi-transparent so the fixed photographic field keeps running behind
		 * it, and without this the panel's own heading and lede showed through
		 * the dock's text.
		 *
		 * `visibility`, not `display`: the panel keeps its box and its
		 * background, so nothing reflows and the ground does not move. The
		 * panel's own children are hidden from the accessibility tree too,
		 * which is correct — while a modal detail view is open, the page behind
		 * it should not be readable by anyone, sighted or not.
		 */
		body.is-dock-open .panels .panel > * {
			visibility: hidden;
		}
	}

	/*
	 * Phase 07.1, Plan 03 (D-07.1-05): silences the phone accordion's own
	 * grow/shrink transition — this file's own docblock states why the
	 * branch lives here rather than being deferred to motion.css's shared
	 * one. The detail still opens and closes; only the animation stops
	 * (assets/js/panels.js's `growOpen`/`growShut` already take an
	 * immediate, no-inline-style path under the same media query via
	 * `reduceMotionQuery`, so this branch and that script agree).
	 */
	@media (prefers-reduced-motion: reduce) {
		.js-motion .dock {
			transition: none;
		}
	}
}

	/*
	 * The overlay's ending — see programme-dock.php's own docblock for why
	 * it exists at all. It reuses `.panel__cta`, which is already proven
	 * >= 6.23:1 on every one of the five grounds, so this adds placement
	 * only and no new colour.
	 *
	 * `margin-block-start: auto` pushes it to the bottom of the dock's
	 * column when the content is short (the two honestly-empty programmes),
	 * so a thin overlay still ends with an action rather than trailing off.
	 */
	.dock__cta {
		align-self: flex-start;
		margin-block-start: auto;
	}

	/*
	 * CONV-07's third tap-to-call point (Plan 03-04, D-P3-15). Sits on a
	 * panel ground, not paper, so it reads `--panel-fg-2` (the ground
	 * modifiers above alias this to each ground's own verified ink step)
	 * rather than a paper-ink token — the same ground-aware pattern
	 * `.dock__lede` already uses just above. `.dock__cta`'s own
	 * `margin-block-start: auto` already pushes both this and the CTA to
	 * the bottom of the flex column as a pair; this needs only a small gap
	 * from the control above it, and at least `--tap-target-min` of its
	 * own height so the link is comfortably tappable, not just readable.
	 */
	.dock__call {
		align-self: flex-start;
		margin-block-start: var(--space-sm);
		display: inline-flex;
		align-items: center;
		min-height: var(--tap-target-min, 44px);
		font-family: var(--font-grotesk);
		font-weight: var(--weight-bold);
		font-size: 0.875rem;
		color: var(--panel-fg-2, currentColor);
		text-decoration: underline;
		text-underline-offset: 0.2em;
	}

/* SOURCE: components/overlay.css */
/*
 * overlay.css — the chrome BOTH overlays share (Phase 02.6-01, D-03/D-04/
 * D-05). Hand-authored, native nesting, no preprocessor, no bundler (D-16).
 * Wrapped entirely in the `components` layer declared by main.css, imported
 * LAST (after dock.css) — see main.css's own docblock for why: this file's
 * `.overlay--pane` fixed-position rules read the same >=1000px breakpoint
 * dock.css's own `.js-motion .dock` rule already uses, and its close-control
 * styling is shared by both overlay templates.
 *
 * TWO RENDERING MODES, ONE MARKUP — following dock.css's own precedent
 * exactly (see that file's own docblock for the full reasoning this one
 * does not repeat): the base rules below describe the no-JavaScript,
 * in-flow rendering; every `position: fixed` overlay-chrome rule lives
 * behind `.js-motion` (the same flag motion.css/dock.css already use — "a
 * script is genuinely running"), because removing that scope silently
 * regresses the no-JS reachability gate.
 *
 * WHY THIS FILE DOES NOT REDECLARE `.overlay--programme`'s OWN FIXED
 * POSITIONING: `template-parts/programme-dock.php`'s root element carries
 * BOTH `.dock` and `.overlay overlay--programme` (Phase 02.6-01 Task 2).
 * `.js-motion .dock { position: fixed; inset: 0; z-index: 20; }` and its
 * own `@media (min-width: 1000px) { .js-motion .dock { left: 46vw; } }`
 * (both already in dock.css, untouched by this plan except for the single
 * `.back` rule removal named in Task 3's own action text) already put the
 * programme overlay on the RIGHT side, leaving the pane's own 46vw column
 * visible beside it — exactly "each on its own side." Redeclaring the same
 * geometry here under a second selector for the same element would be
 * dead-weight duplication, not a second, independent guarantee — this file
 * only adds what NOTHING else yet provides: `.overlay--pane`'s own fixed
 * geometry (the LEFT side) and the close control BOTH overlays share.
 *
 * Same hard negatives as everywhere else on the public page (D-02): no
 * gradient, no 1px hairline, no translucent surface, no `backdrop-filter`.
 * No `@keyframes` and no `transition` live here — motion.css owns motion;
 * this plan does not need one to prove the state machine, so none is added
 * speculatively. No `opacity` on any body-text selector, matching the
 * discipline `02.5-CONTRAST.md` established (dock.css/glass.css already
 * follow it) — nothing in this file needs one.
 */

@layer components {
	/* ---------- Base: the no-JS, in-flow rendering for the left overlay ---------- */

	/*
	 * `.overlay--programme` (i.e. `.dock`) already carries its own base,
	 * in-flow surface from dock.css's own `.dock { ... }` rule — nothing to
	 * add here for that side. `.overlay--pane` has no other file giving it
	 * one, so this is its equivalent: the same shape dock.css's `.dock`
	 * base uses, on the pane's own paper tokens rather than a ground
	 * colour, since the left overlay is an extension of the paper pane
	 * beside it, not a fifth programme ground.
	 */
	.overlay--pane {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
		color: var(--paper-ink, var(--color-ink));
		background: var(--paper, var(--color-ground));
		padding: clamp(var(--space-xl), 6vw, var(--space-3xl)) clamp(var(--space-lg), 5vw, var(--space-2xl));
	}

	.overlay--pane > * {
		position: relative;
		z-index: 1;
	}

	/*
	 * Phase 02.6-05 (D-19, found by RENDERING the no-JS base state and
	 * reading the screenshot, not by reading this file): the identical
	 * geometry problem dock.css's own `.dock` base rule has, and the
	 * identical fix, for the identical reason — see that file's own
	 * docblock at this same media query for the full explanation.
	 * Summary: from 1000px up, glass.css's `.glass` sits FIXED over the
	 * page's own left 46vw at every scroll position, unconditionally,
	 * before `.js-motion` ever promotes this overlay to its own fixed
	 * chrome. Without this clearance, THIS overlay's own in-flow base
	 * rendering starts flush left, so most of its own heading/claim/
	 * teacher-card/reference/price text sits physically under the opaque,
	 * always-fixed pane — genuinely unreadable to a sighted desktop
	 * visitor with JavaScript off, even though the same text is present,
	 * unhidden, in the raw HTML.
	 */
	@media (min-width: 1000px) {
		.overlay--pane {
			padding-left: calc(46vw + clamp(var(--space-lg), 4vw, var(--space-2xl)));
			padding-right: clamp(var(--space-lg), 5vw, var(--space-2xl));
		}
	}

	/* ---------- JS-only promotion: the left overlay's own fixed geometry ---------- */

	.js-motion .overlay--pane {
		position: fixed;
		inset: 0;
		z-index: 15;
		overflow-y: auto;
		/*
		 * `safe center`, matching dock.css's own fix (see that file's
		 * docblock for the full reasoning) — this overlay's content
		 * (proc-my's full claim/mechanism/people/origin fold) reliably
		 * exceeds a 900px viewport, and bare `center` pushes its own close
		 * control and heading into negative, unreachable scroll territory.
		 */
		justify-content: safe center;
		gap: var(--space-lg);
	}

	@media (min-width: 1000px) {
		.js-motion .overlay--pane {
			left: 0;
			width: 46vw;
			padding: clamp(var(--space-2xl), 4.5vw, var(--space-4xl));
		}
	}

	/* ---------- The close control — shared by both overlays ---------- */

	/*
	 * The FIRST child of whichever overlay it closes (programme-dock.php /
	 * pane-overlay.php both place it there) — a large primary control, not
	 * a floating pill, per D-05. Sticky to the top of the overlay's OWN
	 * scroll container once the overlay becomes the fixed, independently
	 * scrolling surface (`.js-motion`) — never floating over the content
	 * below it. In the base, no-JS, in-flow rendering it is an ordinary
	 * block-level button; sticky positioning is a `.js-motion`-only
	 * enhancement, the same two-mode discipline this whole file follows.
	 */
	/*
	 * Fixed dark-on-light, via the design's own global ink/paper tokens —
	 * the same pattern `.panel__cta` already relies on for its own
	 * ground-independent contrast — rather than `currentColor`, which
	 * would paint the button in whichever ground's ink colour surrounds it
	 * and require a second custom property just to invert it back for the
	 * label. A fixed, solid, maximum-contrast pair reads correctly against
	 * every one of the five programme grounds and the paper pane alike,
	 * with nothing for `test-contrast-ratios.php` to compute per ground.
	 */
	/*
	 * Quick task 260831-wey: the LOOK now comes from `.c-btn`
	 * (components/button.css), which the owner picked as the house style
	 * for every primary control — "Zpět na programy button and the mam
	 * zajem button should be styled like this ... and always on the same
	 * spot." Three near-identical button rules in three files was exactly
	 * how they drifted apart in the first place.
	 *
	 * What stays here is placement, which is this component's business:
	 * full width on a phone where a thumb needs the whole row, hugging its
	 * label above the split where the title should lead.
	 */
	.overlay__close {
		width: 100%;
	}

	@media (min-width: 640px) {
		.overlay__close {
			width: auto;
			align-self: flex-start;
		}
	}

	/*
	 * Sticky to the top of the overlay's OWN scroll container, never
	 * floating over the content below it — a `.js-motion`-only
	 * enhancement, since only once `.js-motion .overlay--pane`/`.dock`
	 * become independently scrolling fixed surfaces does "top of its own
	 * scroll container" mean anything different from "top of the page." In
	 * the base, no-JS, in-flow rendering this is an ordinary block button.
	 */
	/*
	 * In the programme dock the sticky behaviour moved up to `.dock__actions`
	 * (components/dock.css), which carries this control AND the CTA beside
	 * it as one bar. This rule still governs the standalone case.
	 */
	.js-motion .overlay--pane .overlay__close {
		position: sticky;
		top: 0;
		z-index: 2;
	}
}

/* SOURCE: components/player.css */
/**
 * The custom audio player (Phase 02.6, owner request 2026-08-23).
 *
 * Progressive enhancement, in two states:
 *
 *   - WITHOUT JavaScript, `.c-player` contains a real `<audio controls>` and
 *     nothing else. The native player renders and works. This file must never
 *     hide it, because hiding it would leave a no-JS visitor with a recording
 *     they can see listed and cannot play — the exact class of broken promise
 *     that got every audio-less entry removed from the page in the first place.
 *
 *   - WITH JavaScript, panels.js sets `data-player-enhanced` on the wrapper,
 *     removes the `controls` attribute, and injects the chrome below. Only the
 *     enhanced state is styled here; the base state is deliberately left to the
 *     browser.
 *
 * Colour: the player sits inside a programme overlay, so it inherits that
 * overlay's own ground and ink through `--panel-fg` / `--panel-fg-2`, never a
 * literal. A hardcoded hex here would have been wrong on five of the seven
 * grounds — the same defect plan 02.6-03 found when dark-ground ink tokens
 * leaked onto the paper overlay.
 */

@layer components {

	/*
	 * The recordings accordion (Phase 06, plan 06-10 — AUDIO-01/03/06).
	 * Lives in this file rather than composed.css (where `.c-lesson-list__
	 * item`'s own layout rules already sit) because the toggle/panel pair
	 * this plan adds exists specifically to gate this file's own player —
	 * the two are one feature, read together.
	 *
	 * The toggle is a real <button> reset to read as the row's own heading
	 * control — Enter/Space activation, focus and its accessible name are
	 * all free, nothing here changes that. `:focus-visible` reuses
	 * `--panel-fg` at the SAME 3px/3px-offset shape as `.c-player__seek`'s
	 * own rule below, already proven to clear contrast on every ground
	 * (test-contrast-ratios.php) — a new token chain was not needed.
	 *
	 * The panel's collapsed state is gated behind `.js-motion` — the SAME
	 * flag `.c-player__chrome` gates on two rules down — so a no-JS visitor
	 * never sees a collapsed lesson at all: every row ships fully open,
	 * exactly as the server rendered it. No `transition`/`animation`
	 * property is declared anywhere in this block on purpose — the chevron
	 * flips instantly on expand, it does not animate, so this addition
	 * needs no prefers-reduced-motion branch of its own.
	 */
	.c-lesson-list__toggle {
		display: flex;
		align-items: baseline;
		gap: 0.5rem;
		inline-size: 100%;
		margin: 0;
		padding: 0.15rem 0.3rem;
		border: 0;
		border-radius: 0.4rem;
		background: transparent;
		font: inherit;
		color: inherit;
		text-align: left;
		cursor: pointer;
	}

	.c-lesson-list__toggle:hover {
		background: color-mix(in srgb, var(--panel-fg, #0d110b) 8%, transparent);
	}

	.c-lesson-list__toggle:focus-visible {
		outline: 3px solid var(--panel-fg, #0d110b);
		outline-offset: 3px;
	}

	.c-lesson-list__chevron {
		margin-inline-start: auto;
	}

	.c-lesson-list__toggle[aria-expanded="true"] .c-lesson-list__chevron {
		transform: rotate(90deg);
	}

	.js-motion .c-lesson-list__panel:not(.is-lesson-open) {
		display: none;
	}

	/* The chrome is server-rendered but inert until JS confirms it can drive
	   it. Without `.js-motion` a visitor sees the native <audio controls>
	   above and never this dead chrome. */
	.c-player__chrome {
		display: none;
	}

	.js-motion .c-player[data-player-enhanced] .c-player__chrome {
		display: flex;
		align-items: center;
		gap: 0.75rem;
		inline-size: 100%;
	}

	/* Enhanced state only — the bare `.c-player` keeps the native player. */
	.c-player[data-player-enhanced] {
		display: flex;
		align-items: center;
		gap: 0.75rem;
		margin-top: 0.6rem;
		padding: 0.5rem 0.9rem 0.5rem 0.5rem;
		border-radius: 0.65rem;
		background: color-mix(in srgb, var(--panel-fg, #0d110b) 12%, transparent);
		max-width: 32rem;
	}

	.c-player[data-player-enhanced] .c-player__audio {
		/* Present, reachable, but visually replaced by the custom chrome.
		   Not `display:none` — that can drop it from the a11y tree in some
		   engines, and the element is still the real control being driven. */
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.c-player__button {
		flex: none;
		inline-size: 2.75rem;
		block-size: 2.75rem;
		border: 0;
		border-radius: 0.5rem;
		cursor: pointer;
		display: grid;
		place-items: center;
		background: var(--panel-fg, #0d110b);
		color: var(--panel-bg, #fff);
		transition: transform 0.16s cubic-bezier(0.2, 0.7, 0.3, 1);
	}

	.c-player__button:hover {
		transform: translateY(-1px);
	}

	.c-player__button:active {
		transform: translateY(0) scale(0.97);
	}

	.c-player__button svg {
		inline-size: 1.1rem;
		block-size: 1.1rem;
		display: block;
		fill: currentColor;
	}

	/* The play and pause glyphs are both present; the button swaps which one
	   is shown via `aria-pressed`, so no icon is ever created by script.
	 *
	 * These selectors are deliberately qualified with `.c-player__button`.
	 * Written as bare `.c-player__icon--pause` they lose to
	 * `.c-player__button svg { display: block }` above on specificity
	 * (0,1,0 against 0,1,1) and BOTH glyphs render stacked inside the
	 * button — which is exactly what shipped for one render before it was
	 * caught by looking at a screenshot. */
	.c-player__button .c-player__icon--pause {
		display: none;
	}

	.c-player__button[aria-pressed="true"] .c-player__icon--play {
		display: none;
	}

	.c-player__button[aria-pressed="true"] .c-player__icon--pause {
		display: block;
	}

	.c-player__time {
		flex: none;
		font-size: 0.8125rem;
		font-weight: 700;
		font-variant-numeric: tabular-nums;
		color: var(--panel-fg-2, var(--panel-fg, #0d110b));
		min-inline-size: 3.2rem;
	}

	.c-player__time--total {
		text-align: end;
	}

	/* A real range input: keyboard-operable and screen-reader-announced for
	   free, which a div-with-listeners would not be. */
	.c-player__seek {
		flex: 1 1 auto;
		appearance: none;
		background: transparent;
		cursor: pointer;
		block-size: 1.5rem;
		margin: 0;
	}

	.c-player__seek::-webkit-slider-runnable-track {
		block-size: 0.3rem;
		border-radius: 999px;
		background: color-mix(in srgb, var(--panel-fg, #0d110b) 28%, transparent);
	}

	.c-player__seek::-moz-range-track {
		block-size: 0.3rem;
		border-radius: 999px;
		background: color-mix(in srgb, var(--panel-fg, #0d110b) 28%, transparent);
	}

	.c-player__seek::-webkit-slider-thumb {
		appearance: none;
		inline-size: 0.95rem;
		block-size: 0.95rem;
		border-radius: 50%;
		background: var(--panel-fg, #0d110b);
		margin-block-start: -0.33rem;
	}

	.c-player__seek::-moz-range-thumb {
		inline-size: 0.95rem;
		block-size: 0.95rem;
		border: 0;
		border-radius: 50%;
		background: var(--panel-fg, #0d110b);
	}

	.c-player__seek:focus-visible {
		outline: 3px solid var(--panel-fg, #0d110b);
		outline-offset: 3px;
	}

	@media (max-width: 30rem) {
		.c-player[data-player-enhanced] {
			flex-wrap: wrap;
		}

		.c-player__seek {
			order: 3;
			flex-basis: 100%;
		}
	}
}

/* SOURCE: components/gallery.css */
/**
 * The shared large-image-plus-thumbnail-strip gallery component (D-06-09,
 * Phase 06 Plan 07) — template-parts/gallery-strip.php's own presentation.
 * Used identically by section-lektori.php's office-gallery pairing beside
 * Lektoři (CONT-23) and by programme-dock.php's own `galerie` field: one
 * implementation, one set of rules.
 *
 * Styled entirely through the `data-gallery-*` hooks gallery-strip.php
 * always renders — never through a caller's own chosen BEM prefix
 * (`.c-office-gallery` / `.c-gallery`), so a caller is free to choose
 * whatever prefix it wants (kept only for tools/tests/browser/
 * office-gallery.mjs's own pre-written selectors and for
 * tools/tests/test-a11y-structure.php's established `c-gallery` substring
 * check) without this file growing a second rule set per prefix.
 *
 * Retired from this file (Phase 06, Plan 07): the previous `.c-gallery`
 * flat scrolling row of full-size images (`.c-gallery__list`/`__item`/
 * `__img`/`__caption`) — programme-dock.php's own gallery no longer
 * renders that markup; both galleries now share the ONE component below.
 *
 * Keyboard operability and Enter/Space activation need NO CSS of their
 * own — every thumbnail is a real `<button>` (gallery-strip.php), and a
 * real button is natively focusable and natively fires a click on both
 * keys. `:focus-visible`'s own ring colour is already handled globally by
 * `.panel :focus-visible, .dock :focus-visible, .overlay :focus-visible`
 * (panels.css) — every ground this component can render inside already
 * resolves that ring to `--panel-fg`, proven >= 4.5:1 by
 * test-contrast-ratios.php, so no new focus-ring rule is declared here.
 */

@layer components {

	[data-gallery-strip] {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm, 0.75rem);
	}

	[data-gallery-stage] {
		margin: 0;
		aspect-ratio: 16 / 10;
		border-radius: 0.5rem;
		overflow: hidden;
		/* The overlay grounds are strongly coloured; a flat photo dropped
		   onto them reads as a hole. A slight inset edge in the ground's
		   own ink ties it back to the surface without a border — D-02
		   forbids hairlines, and this is a shadow, not a rule. Carried
		   over from this file's previous `.c-gallery__img` treatment. */
		box-shadow: 0 1px 0 color-mix(in srgb, var(--panel-fg, #0d110b) 18%, transparent) inset,
			0 8px 24px color-mix(in srgb, var(--panel-fg, #0d110b) 18%, transparent);
	}

	[data-gallery-image] {
		display: block;
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
	}

	[data-gallery-thumbs] {
		display: flex;
		gap: 0.5rem;
		margin: 0;
		padding: 0 0 0.375rem;
		list-style: none;
		overflow-x: auto;
		/*
		 * D-07-13 (Phase 07, Plan 03): the one declaration that stops a
		 * horizontal swipe inside this nested scroller from chaining out
		 * into the browser's own back-navigation gesture, once the strip
		 * moved into a third context (inline inside a programme's own
		 * dock, this same plan's Task 1/2). Already present — added by
		 * Phase 06, Plan 07 for this component's first two contexts —
		 * left as-is; no `touch-action` is declared alongside it, on
		 * purpose: restricting it would break the vertical page scroll a
		 * finger starting on a thumbnail should still get.
		 */
		overscroll-behavior-x: contain;
		scroll-snap-type: x proximity;
		scrollbar-width: thin;
		scrollbar-color: color-mix(in srgb, var(--panel-fg, #0d110b) 35%, transparent) transparent;
	}

	[data-gallery-thumb] {
		flex: 0 0 auto;
		inline-size: 4.5rem;
		block-size: 4.5rem;
		padding: 0;
		border: 0.125rem solid transparent;
		border-radius: 0.4rem;
		background: none;
		cursor: pointer;
		overflow: hidden;
		scroll-snap-align: start;
		/*
		 * Phase 07, Plan 03 (D-07-13): a keyboard user tabbing to an
		 * off-screen thumbnail gets it scrolled into view for free (the
		 * strip's own `overflow-x: auto`) — this keeps that focused
		 * thumbnail from landing flush against the strip's own edge,
		 * matching this component's third context (inline inside a
		 * programme's own dock) exactly as its first two already read.
		 */
		scroll-margin-inline: var(--space-sm);
	}

	[data-gallery-thumb] img {
		display: block;
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
	}

	/* The permanently-visible "this is the one showing" indicator — a
	   border, deliberately never `outline`, so it never fights the
	   transient :focus-visible ring's own outline on the same element. */
	[data-gallery-thumb].is-selected {
		border-color: var(--panel-fg, currentColor);
	}

	/* A visitor who has asked for less motion still gets the strip and
	   still gets the selected thumbnail scrolled into view (gallery-
	   strip.js checks the same media query before choosing `smooth` vs.
	   `auto`); they just do not get scroll-snap-assisted momentum fighting
	   their own scroll, matching this theme's established
	   prefers-reduced-motion idiom for a horizontally scrolling strip. */
	@media (prefers-reduced-motion: reduce) {
		[data-gallery-thumbs] {
			scroll-snap-type: none;
		}
	}
}

/* SOURCE: components/door.css */
/**
 * The pane's door — the one way through to about-us, references and prices
 * (Phase 02.6, owner 2026-08-23: "the about us should be an obviously visible
 * button (maybe scrolling pictures … with a fancy font button over it)").
 *
 * It previously had NO stylesheet at all — not a weak one, none — so it
 * rendered as a browser-default underlined link. That is why it read as body
 * copy rather than as the pane's single most important control.
 *
 * Constraints this has to respect, all from 02.4-SIGNOFF.md's hard negatives:
 *
 *   - NO GRADIENTS. The scrim over the photographs is a flat tonal layer, not
 *     a fade. That is a real constraint here, because a gradient scrim is the
 *     reflexive way to make type legible over imagery and it is forbidden.
 *   - NO 1px HAIRLINES. The door's edge is its own radius and shadow.
 *   - The label is Instrument Serif — the display face the approved design
 *     reserves for headline moments, which is exactly what this is.
 */

@layer components {

	.glass__door {
		position: relative;
		display: block;
		overflow: hidden;
		inline-size: 100%;
		min-block-size: 8.5rem;
		border-radius: 0.7rem;
		text-decoration: none;
		color: var(--color-ground, #f7f7f3);
		background: var(--color-ink, #0d110b);
		box-shadow: 0 2px 6px rgb(10 14 9 / 0.14), 0 14px 34px rgb(10 14 9 / 0.18);
		transition: transform 0.18s cubic-bezier(0.2, 0.7, 0.3, 1),
			box-shadow 0.18s ease;
	}

	.glass__door:hover {
		transform: translateY(-2px);
		box-shadow: 0 4px 10px rgb(10 14 9 / 0.18), 0 22px 46px rgb(10 14 9 / 0.24);
	}

	.glass__door:active {
		transform: translateY(0) scale(0.995);
	}

	.glass__door:focus-visible {
		outline: 3px solid var(--color-ink, #0d110b);
		outline-offset: 3px;
	}

	/* --- the scrolling strip ------------------------------------------- */

	.glass__door-strip {
		position: absolute;
		inset: 0;
		display: flex;
		gap: 0;
		inline-size: max-content;
		animation: cp-door-scroll 38s linear infinite;
	}

	.glass__door-img {
		block-size: 100%;
		inline-size: auto;
		max-inline-size: none;
		object-fit: cover;
		display: block;
		flex: none;
	}

	/* Flat scrim, never a gradient (D-02). Sized to the whole door so the
	   label's contrast does not depend on which photograph happens to be
	   under it at that moment — a scrim that varies with the imagery is a
	   contrast failure waiting for the wrong frame. */
	.glass__door::after {
		content: "";
		position: absolute;
		inset: 0;
		background: rgb(10 14 9 / 0.58);
		pointer-events: none;
	}

	/* --- the label ------------------------------------------------------ */

	.glass__door-label {
		position: relative;
		z-index: 1;
		display: grid;
		place-items: center;
		min-block-size: 8.5rem;
		padding: var(--space-md, 1.25rem);
		font-family: var(--font-display, "InstrumentSerif", Georgia, serif);
		font-size: clamp(1.5rem, 2.6vw, 2.1rem);
		font-weight: 400;
		line-height: 1.1;
		letter-spacing: -0.015em;
		text-align: center;
		color: #fff;
		text-wrap: balance;
	}

	/* The door with no imagery yet: the scrim would darken a ground that is
	   already ink, so it is dropped and the label sits on the flat ground. */
	.glass__door:not(:has(.glass__door-strip))::after {
		display: none;
	}

	/*
	 * Quick task 260831-v19 — the pane must not scroll, and at short
	 * viewport heights this 8.5rem photographic button is the largest
	 * piece of pure chrome in it. It compacts to a single text line
	 * rather than disappearing: it is the ONLY route into the pane
	 * overlay, and test-content-reachability.php treats losing that
	 * route as a failure, not a cosmetic regression.
	 *
	 * These rules live here, not in glass.css, because main.css imports
	 * door.css after glass.css — at equal specificity the later file
	 * wins, so the compaction has to sit after the min-block-size it is
	 * overriding. Found by measuring the door at 136px with the
	 * glass.css version supposedly active.
	 */
	@media (min-width: 1000px) {
		.glass__door {
			min-block-size: 2rem;
			overflow: visible;
			background: none;
			box-shadow: none;
			border-radius: 0;
			border-block-start: 1px solid rgb(13 17 11 / 0.12);
			color: var(--paper-ink, #0d110b);
		}

		.glass__door:hover,
		.glass__door:active {
			transform: none;
			box-shadow: none;
		}

		.glass__door-strip { display: none; }

		.glass__door::after { display: none; }

		.glass__door-label {
			min-block-size: 1.5rem;
			display: block;
			line-height: 1.5rem;
			padding: var(--space-sm, 0.5rem) 0 0;
			font-family: var(--font-grotesk, system-ui), sans-serif;
			font-size: 0.8125rem;
			font-weight: 700;
			letter-spacing: 0.02em;
			text-align: start;
			color: inherit;
			text-decoration: underline;
			text-underline-offset: 3px;
		}
	}

	@keyframes cp-door-scroll {
		from {
			transform: translateX(0);
		}

		to {
			/* Exactly half: the strip prints its images twice, so -50% lands
			   the second copy precisely where the first began. */
			transform: translateX(-50%);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.glass__door-strip {
			animation: none;
		}

		.glass__door:hover,
		.glass__door:active {
			transform: none;
		}
	}
}

/* SOURCE: components/footer.css */
/*
 * footer.css — the shared site footer (template-parts/site-footer.php)
 * and the three legal pages' reading typography (page.php's
 * `.legal-page`) — Phase 04, plan 04-01 (D-04-12/D-04-13/D-04-14).
 * Hand-authored, native nesting, no preprocessor, no bundler (D-16).
 * Wrapped entirely in the `components` layer declared by main.css.
 *
 * Pitfall 3 (04-RESEARCH.md): every colour declared below reuses the
 * already-verified `--paper`/`--paper-ink`/`--paper-ink-2`/`--paper-ink-3`
 * pair base.css declares (17.74:1 / 10.44:1 / 5.75:1) — no new hex
 * literal and no new ground/ink custom property anywhere in this file.
 * That makes the footer's and the legal pages' contrast compliance a
 * corollary of already-passing checks rather than a new thing to verify,
 * and per Pitfall 4 it means the global `--color-focus-ring` default
 * already clears 3:1 here with no new `:focus-visible` override needed.
 *
 * No @keyframes and no `transition`/`animation` declaration lives here —
 * per this plan's own instruction, the footer's left clearance (below)
 * snaps rather than eases when body.is-released toggles, unlike the
 * panel stack's own eased version of the same rule.
 */

@layer components {
	/*
	 * WordPress's own body_class() adds the literal `page` class to every
	 * singular Page view (is_page() === true) — and only there, never on
	 * front-page.php's home template (show_on_front is 'posts', so the
	 * homepage carries no Page object at all). This is what lets the
	 * three legal pages paint the verified paper ground for their WHOLE
	 * document — including the brand mark above `.legal-page` and the
	 * `.site-footer` below it — without a body_class() filter of this
	 * plan's own and without ever touching the one-pager's own
	 * `--color-ground`/`--color-ink` body default.
	 */
	body.page {
		background-color: var(--paper);
		color: var(--paper-ink);
		/*
		 * Quick task 260902-kv9. A layout custom property, not a colour or
		 * a ground/ink token — declared on `body.page` rather than on
		 * `.legal-page` because `.legal-page__brand` is a SIBLING of
		 * `.legal-page` in page.php, not a descendant, and both consumers
		 * (plus, per the owner's 2026-09-02 addendum below, `.site-footer`)
		 * must resolve the same value or they drift apart. The growth rate
		 * (8vw) and the ceiling (--space-4xl, 96px) are lifted literally
		 * from panels.css's own released full-width gutter
		 * (panels.css:182), so the legal pages inset by the same amount
		 * the released panel stack already does rather than by a second
		 * invented number. The floor stays --space-lg, which is what this
		 * file's own existing padding already resolved to on mobile, so
		 * mobile character is unchanged.
		 */
		--legal-gutter: clamp(var(--space-lg), 8vw, var(--space-4xl));
	}

	.legal-page__brand {
		display: inline-flex;
		width: 2rem;
		height: 2rem;
		flex-shrink: 0;
		color: var(--paper-ink);
		margin: clamp(var(--space-lg), 5vw, var(--space-3xl)) 0 0 var(--legal-gutter);
	}

	.legal-page__brand svg,
	.legal-page__brand img {
		width: 100%;
		height: 100%;
	}

	.legal-page {
		display: flex;
		flex-direction: column;
		gap: var(--space-lg);
		/*
		 * Quick task 260902-kv9. 80ch here is a DELIBERATELY WIDER measure
		 * than .u-measure's 65ch (utilities.css) — a legal document is
		 * read start to finish rather than scanned beside other content.
		 * The column is still left-aligned and never centred, matching
		 * utilities.css's own docblock rule that alignment belongs to the
		 * layout, not to the measure.
		 *
		 * reset.css:14 sets box-sizing: border-box globally, so max-width
		 * bounds the PADDING box — a bare 80ch here would leave the text
		 * itself at 80ch minus two gutters, about 60ch, narrower than
		 * today. Adding the two gutters back is what makes the reading
		 * text land at a true 80ch.
		 */
		max-width: calc(80ch + (2 * var(--legal-gutter)));
		margin-inline: 0;
		padding: clamp(var(--space-lg), 5vw, var(--space-3xl)) var(--legal-gutter);
	}

	.legal-page__title {
		font-family: var(--font-serif);
		font-weight: var(--weight-regular);
		font-size: clamp(2rem, 1.4rem + 2.4vw, 3.25rem);
		line-height: 1.1;
		letter-spacing: -0.02em;
	}

	.legal-page__body {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
	}

	.legal-page__body :is(h2, h3) {
		color: var(--paper-ink);
	}

	.legal-page__body :is(p, li) {
		color: var(--paper-ink-2);
		line-height: 1.6;
	}

	.legal-page__body :is(ul, ol) {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
		padding-inline-start: var(--space-lg);
	}

	.legal-page__body a {
		color: var(--paper-ink);
		text-decoration: underline;
	}

	/*
	 * Phase 04, plan 04-05, Task 0. Real Phase 04 integration defect, found
	 * by RENDERING — every structural/a11y test in this suite passed with
	 * this footer invisible, because none of them paints a pixel.
	 *
	 * `field.css`'s `.photo-field` is an opaque, full-viewport layer with
	 * `position: fixed` and `z-index: 0`. Per CSS 2.1 Appendix E's painting
	 * order, a POSITIONED element at z-index 0 paints in step 8 — after
	 * in-flow block backgrounds (step 4) and after in-flow inline content
	 * (step 7). `.site-footer` was `position: static` (the default) with
	 * no z-index, so its own paper background and its three legal links
	 * were painted in steps 4/7 and then immediately erased by the
	 * photo-field's opaque plate painting over them in step 8. `.panels`
	 * (panels.css:39), `.glass` (z-index 10), `.cookie-banner` (z-index 45)
	 * and `.secnav` (z-index 40) all escape this because they are
	 * positioned; `.site-footer` was the one body child with real content
	 * and no positioning at all.
	 *
	 * `pointer-events: none` on `.photo-field` is exactly why this shipped
	 * unnoticed: `document.elementFromPoint()` skips that layer for
	 * hit-testing, so the links stayed focusable and clickable — every
	 * structural check that asks "is this element present and reachable"
	 * passed. Painting is a separate pipeline from hit-testing, and nothing
	 * in this suite sampled actual pixels until this task's own
	 * measure-footer-paint.mjs did.
	 *
	 * The fix is the same counter `.panels` already uses against this same
	 * layer: put `.site-footer` in the SAME painting step (positioned) as
	 * `.photo-field`, so DOM order — footer after photo-field — decides the
	 * winner. No new z-index number: adding one would be a second value
	 * that can drift from `.photo-field`'s; bare `position: relative` is
	 * sufficient and matches `.panels`'s own precedent exactly. Declared on
	 * the BASE rule (not scoped to `.panels ~ .site-footer`) because it is
	 * harmless on `body.page` (the legal pages render no `.photo-field` at
	 * all) and it defends the footer against any future positioned sibling
	 * on any page.
	 *
	 * DO NOT remove this line to "simplify" the rule — doing so makes the
	 * footer invisible again on the front page, not merely differently
	 * stacked. Reverting it is exactly what this task's regression test
	 * (measure-footer-paint.mjs, run once against the reverted line) proves
	 * red.
	 */
	.site-footer {
		position: relative;
		background-color: var(--paper);
		color: var(--paper-ink-3);
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
		padding: clamp(var(--space-lg), 4vw, var(--space-2xl));
		font-size: 0.8125rem;
	}

	/*
	 * Owner decision, 2026-09-02 (quick task 260902-kv9 addendum): on the
	 * legal pages the brand mark, the reading column and the footer links
	 * now share ONE left edge, from the SAME --legal-gutter value rather
	 * than a second number that can drift. The misalignment was
	 * pre-existing (the brand sat at 64px against the footer's 48px
	 * before this task too), but widening the reading column's gutter to
	 * 96px made it newly conspicuous, so closing it is a direct
	 * consequence of that widening rather than unrelated scope.
	 *
	 * Scoped to `body.page` ONLY (2 classes beats .site-footer's 1, so
	 * this overrides `padding` above by specificity regardless of source
	 * order) — front-page.php's own `.panels ~ .site-footer` clearance
	 * rules below are untouched; `.panels` never renders on page.php's
	 * DOM at all, so those rules were already structurally unreachable
	 * here, but this rule does not rely on that: it only ever touches the
	 * inline padding, never the block padding the base rule set.
	 */
	body.page .site-footer {
		padding-inline: var(--legal-gutter);
	}

	.site-footer__legal {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-md) var(--space-lg);
	}

	.site-footer__legal a {
		color: var(--paper-ink-2);
	}

	.site-footer__disclosure {
		color: var(--paper-ink-3);
	}

	/*
	 * D-07.1-10 (Phase 07.1, plan 05). The pane footer's own address/phone
	 * pair, carried here on a phone — hidden above 1000px inside the
	 * `@media (min-width: 1000px)` block below, the exact complement of
	 * glass.css's own `.glass__footer` desktop restore (base rule
	 * `display: none`, restored to `display: flex` at the SAME 1000px
	 * boundary), so exactly one copy of the address/phone is ever in the
	 * accessibility tree. Tokens only — no new hex literal, no new
	 * ground/ink pair, per this file's own docblock (Pitfall 3).
	 */
	.site-footer__contact {
		display: flex;
		flex-direction: column;
		gap: var(--space-xs);
	}

	.site-footer__address {
		color: var(--paper-ink-2);
	}

	/*
	 * A real tap target: the anchor's OWN box carries at least
	 * var(--tap-target-min, 44px) of block size, laid out so the box (not
	 * just the visible text) is what a phone visitor actually taps —
	 * CONV-07's "one tap" names a target, not merely a link.
	 */
	.site-footer__phone {
		display: inline-flex;
		align-items: center;
		min-height: var(--tap-target-min, 44px);
		color: var(--paper-ink);
	}

	/*
	 * Owner decision, 2026-09-02 (quick task 260902-kv9 addendum,
	 * ADDITION A). Real Phase 04 integration defect, found by rendering:
	 * plan 04-01 added `.site-footer`, plan 04-03 added the fixed cookie
	 * banner, and each was verified on its own surface — nobody checked
	 * them together. On a FIRST visit, front-page.php's `.site-footer`
	 * (which carries the privacy-policy link) sits directly under the
	 * banner at the true bottom of the page, exactly when consent
	 * matters most.
	 *
	 * Keyed off `:has(.cookie-banner--visible)` — the SAME modifier class
	 * cookie-banner.js already toggles (cookie-banner.css:60) — rather
	 * than a new body class or new JS, so the clearance appears and
	 * disappears with the banner itself. `:has()` already has precedent
	 * in this theme (composed.css:550, door.css:104). Scoped to front-page
	 * only via `.panels ~`, this file's own established shape (below) —
	 * `.panels` never renders on page.php's DOM, and the legal pages have
	 * no banner-vs-footer collision to fix beyond the alignment already
	 * done above.
	 *
	 * Every term below reuses an EXISTING token or an EXISTING expression
	 * from cookie-banner.css/button.css — no invented pixel literal — but
	 * the banner's own rendered height is intrinsically content-based, so
	 * this is a deliberately GENEROUS floor, empirically verified against
	 * real renders at all four tested viewports (measure-layout.mjs group
	 * `panel`, check `F1`), not a byte-exact fit. If cookie-banner.css's
	 * own offset expression, padding, gap or button min-height ever
	 * change, this clearance must move with them — the same coupling
	 * warning that file's own docblock (cookie-banner.css:26-35) already
	 * states for the banner-vs-secnav relationship.
	 *
	 * MARGIN, not padding: `.site-footer` is the last element in normal
	 * flow before `</body>` (the cookie banner itself is `position: fixed`
	 * and the sitemap label span is `hidden`), so at "scrolled to the true
	 * bottom" the footer's OWN border box always touches the viewport's
	 * bottom edge regardless of how much extra space it contains — padding
	 * stays INSIDE `getBoundingClientRect()`, so it cannot move the
	 * footer's rect away from the banner; only extra space OUTSIDE the
	 * footer's box (margin) grows the document past it and lets the
	 * footer's own rect shift clear. Found by measuring: the first attempt
	 * at this rule used `padding-block-end` and F1 stayed red even with
	 * the correct calc() value, because the padding was invisibly
	 * swallowed inside the footer's own (still viewport-bottom-pinned)
	 * box.
	 *
	 * Mobile-first base value: below 640px `.cookie-banner__actions`
	 * stacks its two buttons into two rows (cookie-banner.css's own
	 * `@media (min-width: 640px)` switches it to one row — reused here
	 * verbatim, not a new breakpoint), and the banner's own text column
	 * is narrower, so both the actions block and the paragraph estimate
	 * are taller than the desktop override below.
	 */
	body:has(.cookie-banner--visible) .panels ~ .site-footer {
		margin-block-end: calc(
			/* the banner's own inset-block-end (cookie-banner.css:49) —
			   its distance from the viewport's true bottom to its own
			   bottom edge */
			clamp(14px, 2.2vh, 26px) + var(--tap-target-min, 44px) + var(--space-sm)
			/* the banner's own vertical padding, both edges
			   (cookie-banner.css:52) */
			+ (2 * var(--space-lg))
			/* the banner's own internal gap between its text and its
			   actions row (cookie-banner.css:51) */
			+ var(--space-md)
			/* two stacked buttons plus the one gap between them, matching
			   .cookie-banner__actions's own column layout below 640px —
			   each button's real min-height is button.css:22's own
			   expression, reused verbatim */
			+ (2 * (var(--tap-target-min, 44px) + var(--space-md))) + var(--space-sm)
			/* the banner's paragraph, generously estimated at
			   --space-4xl for the narrower stacked-layout column rather
			   than a raw pixel guess */
			+ var(--space-4xl)
		);
	}

	@media (min-width: 640px) {
		body:has(.cookie-banner--visible) .panels ~ .site-footer {
			margin-block-end: calc(
				clamp(14px, 2.2vh, 26px) + var(--tap-target-min, 44px) + var(--space-sm)
				+ (2 * var(--space-lg))
				+ var(--space-md)
				/* the actions row's real height in row layout, ≥640px
				   (button.css:22, .c-btn min-height) — one row now, not
				   two */
				+ (var(--tap-target-min, 44px) + var(--space-md))
				/* the 65ch cap on .cookie-banner__text still wraps the
				   Czech sentence to ~2 lines even on a wide banner —
				   --space-3xl generously covers that at this breakpoint */
				+ var(--space-3xl)
			);
		}
	}

	/*
	 * The fixed left pane still covers the viewport's left 46vw from
	 * 1000px up (glass.css) when this footer renders on front-page.php —
	 * the same clearance every full-bleed `.panel` already reads
	 * (panels.css), reused literally rather than a second number
	 * (Pitfall 3/P-04). Scoped on the panel stack's own presence via the
	 * general sibling combinator, `.panels ~`, instead of a body_class()
	 * flag, so page.php's legal pages — which never render `.panels` at
	 * all — are structurally unable to pick this up even if a future edit
	 * changes what body classes those pages carry.
	 */
	@media (min-width: 1000px) {
		/*
		 * D-07.1-10: the desktop complement of glass.css's own
		 * `.glass__footer` restore at this SAME 1000px boundary — the
		 * theme's one mobile/desktop boundary, quoted here rather than a
		 * second invented value (glass.css's own `@media (min-width:
		 * 1000px)` block, components/mobile-actions.css and
		 * components/sitemap.css all key off this identical value). Not
		 * scoped to `.panels ~` like the rules below: `site-footer__contact`
		 * only ever renders on front-page.php in practice (page.php's
		 * dispatch never passes `with_contact`), but the hide is written
		 * unconditionally so it holds even if that ever changes, and so it
		 * reads as a plain complement of the base rule rather than as
		 * coupled to this file's panel-clearance rules below.
		 */
		.site-footer__contact {
			display: none;
		}

		.panels ~ .site-footer {
			padding-left: calc(46vw + clamp(var(--space-lg), 4vw, var(--space-2xl)));
		}

		/* Lost once the pane has parked (field.js sets body.is-released) —
		   the exact reduced value panels.css already uses for every other
		   full-width section in the released state. */
		body.is-released .panels ~ .site-footer {
			padding-left: clamp(var(--space-2xl), 8vw, var(--space-4xl));
		}
	}
}

/* SOURCE: components/cookie-banner.css */
/*
 * cookie-banner.css — the cookie-consent banner (Phase 04, plan 04-03 —
 * D-04-08/09/10, LEGAL-05). Hand-authored, native nesting, no
 * preprocessor, no bundler (D-16). Wrapped in the `components` layer
 * declared by main.css, imported directly after components/footer.css —
 * see main.css's own docblock for why it sits there.
 *
 * Every colour below reuses the already-verified --paper/--paper-ink pair
 * base.css declares — no new hex literal and no new ground/ink pair, so
 * the global focus-ring default (`--color-focus-ring: var(--color-ink)`)
 * already clears 3:1 here with no override needed, matching footer.css's
 * own Pitfall 3/4 reasoning. The button component's two role properties
 * (--btn-bg/--btn-fg) are set to the paper pair with its roles swapped —
 * the same technique .overlay--pane and .glass .c-btn already use
 * (components/button.css) — so both controls read as paper chrome and
 * their own :focus-visible ring resolves to a real token.
 *
 * Reveal mechanism: `.cookie-banner` is not displayed at all by default;
 * it becomes displayed only when it also carries the modifier class
 * `cookie-banner--visible`, applied and removed exclusively by
 * cookie-banner.js. That pair is the whole reveal mechanism — the
 * container itself carries no `hidden` attribute (footer.php's own
 * comment explains why: test-nojs-content.php check 3(c) reserves every
 * runtime write of an element's `.hidden` property to panels.js alone).
 *
 * Stacking, derived from sitemap.css's own declared values rather than a
 * new number: .secnav (the floating section control) is z-index: 40,
 * with inset-block-end: clamp(14px, 2.2vh, 26px) and its own toggle's
 * min-height: var(--tap-target-min, 44px). This banner stacks clearly
 * above that control (z-index: 45) and its own bottom offset clears the
 * toggle's full footprint — secnav's own bottom clearance, plus its
 * toggle's minimum height, plus one gap step — so the section control
 * stays visible and reachable while the banner is open, never covered by
 * it. If either of sitemap.css's two values (the z-index or the bottom
 * clearance/toggle height) ever changes, this offset must move with it.
 *
 * No `transition` and no `animation` declaration anywhere in this file —
 * the banner appears and disappears without motion, so it needs no
 * reduced-motion branch and cannot fail plan 04-04's motion-coverage
 * check.
 */

@layer components {
	.cookie-banner {
		display: none;
		position: fixed;
		z-index: 45;
		inset-inline: 0;
		inset-block-end: calc(clamp(14px, 2.2vh, 26px) + var(--tap-target-min, 44px) + var(--space-sm));
		flex-direction: column;
		gap: var(--space-md);
		padding: var(--space-lg);
		background-color: var(--paper);
		color: var(--paper-ink);
		--btn-bg: var(--paper-ink);
		--btn-fg: var(--paper);
		box-shadow: 0 -10px 30px -8px rgb(10 14 9 / 0.42), 0 -2px 6px rgb(10 14 9 / 0.16);
	}

	.cookie-banner--visible {
		display: flex;
	}

	.cookie-banner__text {
		margin: 0;
		max-width: 65ch;
	}

	.cookie-banner__text a {
		color: var(--paper-ink);
	}

	.cookie-banner__actions {
		display: flex;
		flex-direction: column;
		gap: var(--space-sm);
	}

	@media (min-width: 640px) {
		.cookie-banner__actions {
			flex-direction: row;
		}

		.cookie-banner__actions .c-btn {
			flex: 1 1 0;
		}
	}
}

/* SOURCE: utilities.css */
/*
 * utilities.css — a small set of genuinely cross-cutting, single-purpose
 * helper classes, wrapped entirely in the `utilities` layer declared by
 * style.css:9. This is deliberately not a utility-class framework
 * (CLAUDE.md § Research Findings Area 1 rejects that scope) — just the
 * handful of helpers every section/component in this phase will reach
 * for repeatedly.
 */

@layer utilities {
	/* Visually hides content from sighted users while keeping it in the
	   accessibility tree — for screen-reader-only text. */
	.u-visually-hidden {
		position: absolute;
		width: 1px;
		height: 1px;
		padding: 0;
		margin: -1px;
		overflow: hidden;
		clip: rect(0, 0, 0, 0);
		white-space: nowrap;
		border: 0;
	}

	/* Caps body copy at a readable measure and centers it within its
	   container — for prose blocks (section descriptions, testimonials,
	   blog content). */
	/*
	 * `margin-inline: auto` CENTRES a prose block inside its container.
	 * That is right for a centred layout and wrong for every one of this
	 * utility's eight consumers, all of which sit in left-aligned columns:
	 * the block silently detaches from the left edge its own heading is
	 * aligned to. It was visible in every programme overlay — the title and
	 * the "CO JE SOUČÁSTÍ" label started at x=706 and the lede alone at
	 * x=744, a 43px indent nobody chose.
	 *
	 * The utility's job is the MEASURE (a readable line length). Alignment
	 * belongs to the layout that uses it.
	 */
	.u-measure {
		max-width: 65ch;
		margin-inline: 0;
	}

	/* Vertical rhythm helper: stacks direct children with a single
	   consistent gap, using the spacing tokens rather than ad-hoc
	   margins on each child. */
	.u-stack {
		display: flex;
		flex-direction: column;
		gap: var(--space-md);
	}

	.u-stack--sm {
		gap: var(--space-sm);
	}

	.u-stack--lg {
		gap: var(--space-lg);
	}

	/*
	 * Phase 04, plan 04-01 (A11Y-03, P-03). Off-screen until focused, then
	 * moves into the top-left corner of the viewport — never merely
	 * clip-hidden, which would remove it from the tab order along with the
	 * pixels. Painted on the already-verified paper/paper-ink pair
	 * (base.css) so the global `--color-focus-ring` token (~19.8:1 on
	 * paper) applies unchanged, per P-04 — no new ground/ink pair and no
	 * new :focus-visible override selector.
	 *
	 * z-index reads above .glass's own declared z-index: 10
	 * (components/glass.css:68) — the fixed pane is the only other
	 * positioned element competing for the viewport's top-left corner at
	 * ≥1000px, so the skip link must out-rank it, not a guessed number.
	 */
	.u-skip-link {
		position: fixed;
		top: 0;
		left: 0;
		z-index: 20;
		transform: translateY(-100%);
		background-color: var(--paper);
		color: var(--paper-ink);
		padding: var(--space-sm) var(--space-md);
		font-family: var(--font-grotesk);
		font-size: 0.9375rem;
		text-decoration: none;
	}

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

