/* =========================================================================
 * Vida and Marmelo — the site's stylesheet
 *
 * Loaded AFTER the parent's main.css, on the frontend and in the editor
 * (inc/enqueue.php). The parent owns the framework: layout primitives, resets,
 * buttons, header behaviour, WooCommerce. This file owns the SITE'S IDENTITY:
 * the heading scale, the site's own components, and the effects below.
 *
 * Token VALUES live in theme.json, never here. This file re-exports the
 * site-only tokens under short names (§1) and uses only tokens after that —
 * see the parent's AGENTS.md, "Design system hierarchy".
 *
 * Sections:
 *   1. Site-only token aliases
 *   2. Heading scale
 *   3. Components (added as the site is built)
 *   E. Effects (special-effects.js) — the starter's built-in library
 * ========================================================================= */


/* -------------------------------------------------------------------------
 * 1. Site-only token aliases
 *
 * The parent's main.css §1 already re-exports the palette, the spacing scale,
 * the widths and the parent's custom colours (--color-text, --space-md,
 * --container-width, --color-muted …). Re-export here only what THIS site adds
 * in theme.json `settings.custom`, one line each, no literal values:
 *
 *   --color-dark: var(--wp--custom--color--dark);
 * ------------------------------------------------------------------------- */

:root {
	/* Gold: the ampersand in the logo, focus rings, small highlights. */
	--color-accent: var(--wp--custom--color--accent);
	--color-gold: var(--wp--custom--color--gold);
	--color-ink-deep: var(--wp--custom--color--ink-deep);
	/* A neutral near-black: the shade laid over photos so text reads on them. */
	--color-shade: var(--wp--custom--color--shade);

	/* The header's colours under difference blending (§3a): each is the paper
	   minus the colour it should read as on paper, so over the paper the
	   logo and menu come out exactly ink and gold, and over a photo or
	   a dark band they invert. Derived from the paper: retint the paper, ink or
	   accent and these must be worked out again (SYSTEM.md). */
	--color-diff-text: var(--wp--custom--color--diff-text);
	--color-diff-accent: var(--wp--custom--color--diff-accent);

	/* Two families: Lora for display, DM Sans for everything else. */
	--font-display: var(--wp--preset--font-family--display);
	--font-body: var(--wp--preset--font-family--body);

	/* Three radii. --radius (10px, the parent's token) is every image frame and
	   card; the small one is buttons, tags and inputs; the large one is the
	   hero's inset frame. */
	--radius-sm: var(--wp--custom--radius-sm);
	--radius-lg: var(--wp--custom--radius-lg);

	/* The header bar and the hero's inset frame. The header sits wider than the
	   sections (4.5vw against the 7vw gutter), as in the draft. */
	--header-height: var(--wp--custom--header-height);
	--header-gutter: var(--wp--custom--header-gutter);
	--hero-inset: var(--wp--custom--hero-inset);

	/* A hairline that fades out at both ends — the draft's divider everywhere
	   (facts, accordion rows, the footer). Used as a background-image so it never
	   adds height. */
	--fade-rule: linear-gradient(to right, transparent, var(--color-border) 15%, var(--color-border) 85%, transparent);

	/* The colour the image-reveal veil paints before the wipe (Effects). */
	--reveal-veil: var(--color-background);

	/* Buttons: the parent's component tokens (main.css §5), set to the draft's
	   look — thin ink border, small radius, medium weight, spaced capitals
	   (the capitals and tracking are in §2). */
	--button-border-width: 1px;
	--button-radius: var(--radius-sm);
	--button-padding: 1.15em 1.6em;
	--button-font-weight: 500;
}


/* -------------------------------------------------------------------------
 * 2. Base type and the heading scale
 *
 * Headings are a component, so their sizes live in CSS, not in theme.json's
 * font-size presets (the parent's AGENTS.md, "Typography: two systems, one
 * boundary"). They share one fluid scale with the presets (below);
 * SYSTEM.md, "Type scale", has the numbers.
 *
 *   h1  the hero's display line only — one per page
 *   h2  section titles
 *   h3  a home's name, a page cover's title
 *   h4  a row title: a value, a card
 *   h5, h6  small headings inside content (no pattern uses them yet)
 * ------------------------------------------------------------------------- */

/* The body's size (the "base" preset, 18px) and leading (1.8) come from
   theme.json's styles.typography (framework 1.9.0). */

/* The six steps, as variables so a component that is "an h4 in size" but not
   a heading (an accordion row's title) uses the step instead of retyping it.

   One fluid scale with the font-size presets (theme.json): every size grows
   in a straight line from a 390 px screen to a 1920 px one and holds there,
   so the steps keep their order and their spacing at every width. On a big
   screen each heading is about 1.25x the one below (27 / 33 / 41 / 51 /
   64 / 80 px, h6 to h1); on a phone the steps close up (19 / 22 / 25 / 28 /
   33 / 40 px). SYSTEM.md, "Type scale", has the table (2026-11-14). */
:root {
	--heading-1: clamp(2.5rem, 1.8627rem + 2.6144vw, 5rem);
	--heading-2: clamp(2.0625rem, 1.5686rem + 2.0261vw, 4rem);
	--heading-3: clamp(1.75rem, 1.3836rem + 1.5033vw, 3.1875rem);
	--heading-4: clamp(1.5625rem, 1.3076rem + 1.0458vw, 2.5625rem);
	--heading-5: clamp(1.375rem, 1.1998rem + 0.719vw, 2.0625rem);
	--heading-6: clamp(1.1875rem, 1.06rem + 0.5229vw, 1.6875rem);
}

/* Headings: each level's line-height is set here once and nowhere else; a
   pattern may change a heading's size, never its leading. */
h1,
h2,
h3,
h4,
h5,
h6 {
	font-family: var(--font-display);
	font-weight: 400;
	color: var(--color-text);
}

/* The draft's italic accent: "you can *just be*", "your next *happy place*". */
:is(h1, h2, h3, h4, h5, h6) em {
	font-style: italic;
	font-weight: 400;
}

h1 {
	font-size: var(--heading-1);
	line-height: 1.08;
	letter-spacing: -0.022em;
}

h2 {
	font-size: var(--heading-2);
	line-height: 1.2;
	letter-spacing: -0.032em;
}

h3 {
	font-size: var(--heading-3);
	line-height: 1.15;
	letter-spacing: -0.027em;
}

h4 {
	font-size: var(--heading-4);
	line-height: 1.25;
	letter-spacing: -0.022em;
}

h5 {
	font-size: var(--heading-5);
	line-height: 1.3;
	letter-spacing: -0.015em;
}

h6 {
	font-size: var(--heading-6);
	line-height: 1.3;
	letter-spacing: -0.01em;
}

/* Buttons: spaced capitals in the body face (the box is the parent's, §5). */
.wp-block-button__link.wp-block-button__link,
.wp-element-button.wp-element-button {
	font-family: var(--font-body);
	font-size: var(--wp--preset--font-size--sm);
	letter-spacing: 0.07em;
	text-transform: uppercase;
}

:focus-visible {
	outline: 2px solid var(--color-accent);
	outline-offset: 4px;
}


/* -------------------------------------------------------------------------
 * 3. Components
 *
 * A component two patterns share lives here, never in either pattern's
 * folder (AGENTS.md, "the middle case"). Pattern-specific styling stays in
 * patterns/<id>/pattern.css.
 *
 *   3a. Header        3b. Footer       3c. Labels and text
 *   3d. Buttons and the arrow          3e. Accordion rows
 *   3f. House facts and rating       3g. Rise scene (a panel over a held photo)
 *   3h. Pool ceiling (a section's background, WebGL)
 *   3i. Enlarge cursor (a glass ring over a photo that enlarges)
 * ------------------------------------------------------------------------- */

/* 3a. Header ------------------------------------------------------------- */

/* The bar has no ground and no rule of its own: transparent at all times,
   pinned or not (inc/header.php), so the page shows through it. The parent
   draws paper and a bottom border. */
.site-header,
.wp-block-template-part:has(> .site-header__inner) {
	border-bottom: 0;
	background: transparent;
}

.site-header__inner.site-header__inner {
	min-height: var(--header-height);
	padding-block: 0;
	padding-inline: var(--header-gutter);
}
.brand p {
	margin: 0;
	line-height: 1.2;
}

/* The wordmark is a logo lockup, not type: sized as one, bespoke. 31.8 px on a
   desktop and 26.5 px on a phone in the draft. */
.brand__name {
	font-family: var(--font-display);
	font-size: clamp(1.65rem, 1.5rem + 0.45vw, 2rem);
	letter-spacing: -0.044em;
	white-space: nowrap;
}

.brand__name a {
	color: var(--header-ink, var(--color-text));
	text-decoration: none;
}

.brand__name em {
	font-size: 0.9em;
	color: var(--header-accent, var(--color-accent));
}

/* The lockup's small line: bespoke like the wordmark (9.5 px in the draft,
   raised to 11 px to read comfortably). */
.brand__tagline {
	margin-top: var(--space-xs) !important;
	font-size: 0.6875rem;
	letter-spacing: 0.25em;
	text-transform: uppercase;
}

.site-header__nav .wp-block-navigation__container {
	gap: calc(var(--space-md) + var(--space-xs));
	align-items: center;
}

.site-header__nav .wp-block-navigation-item__content {
	font-size: var(--wp--preset--font-size--base);
	font-weight: 400;
	color: var(--header-ink, var(--color-text));
}

.site-header__nav .nav-inquire > .wp-block-navigation-item__content {
	display: inline-flex;
	align-items: center;
	/* The side padding less the tracking after the last capital and its side
	   bearing (about 0.12em): the arrow's ink as far from the label's ink as
	   from the edge. */
	gap: calc(1.4em - 0.12em);
	padding: 1em 1.4em;
	border: 1px solid var(--color-primary);
	border-radius: var(--radius-sm);
	/* Smaller than the menu's words, as in the draft: capitals read large. */
	font-size: var(--wp--preset--font-size--sm);
	line-height: 1.3;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	transition: background-color 0.35s ease, color 0.35s ease;
}

.site-header__nav .nav-inquire > .wp-block-navigation-item__content:hover {
	background: var(--color-primary);
	color: var(--color-on-dark);
}

/* Glass (`is-glass` on a button or a navigation link): a pane of glass. What
   is behind shows through, frosted and a little richer in colour, under a
   faint white tint, inside one plain ink border: no rim, no shadow.
   assets/js/glass.js adds the refraction, in Chromium browsers: the edges
   bend what is behind them inward, like a thick pane (--glass-lens, an SVG
   filter it builds per pane). Elsewhere the pane is frosted, unbent. Hover
   keeps the button's own fill: the ink, as a darker glass. */
a.is-glass,
.is-glass > a,
.site-header__nav .is-glass > .wp-block-navigation-item__content {
	border: 1px solid var(--color-primary);
	background-color: color-mix(in srgb, var(--color-on-dark) 35%, transparent);
	box-shadow: none;
	-webkit-backdrop-filter: blur(14px) saturate(1.5);
	backdrop-filter: blur(14px) saturate(1.5);
}

:is(a.is-glass, .is-glass > a).has-glass-lens.has-glass-lens {
	backdrop-filter: blur(6px) var(--glass-lens) saturate(1.5);
}

.site-header__nav .is-glass > .wp-block-navigation-item__content:hover {
	background-color: color-mix(in srgb, var(--color-primary) 82%, transparent);
}

/* The phone menu: core's overlay, on paper, links set large. */
.site-header__nav .wp-block-navigation__responsive-container.is-menu-open {
	background: var(--color-background);
	color: var(--color-text);
	padding: var(--space-lg) var(--header-gutter);
}

.site-header__nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
	gap: var(--space-md);
}

.site-header__nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
	font-family: var(--font-display);
	font-size: var(--wp--preset--font-size--xl);
}

.site-header__nav .wp-block-navigation__responsive-container.is-menu-open .nav-inquire > .wp-block-navigation-item__content {
	font-family: var(--font-body);
	font-size: var(--wp--preset--font-size--sm);
	margin-top: var(--space-sm);
}

/* Tablets: the menu tightens so it stays on one line beside the lockup. */
@media (max-width: 64rem) {
	.site-header__nav .wp-block-navigation__container {
		gap: var(--space-sm);
	}

	.site-header__nav .wp-block-navigation-item__content {
		font-size: var(--wp--preset--font-size--sm);
	}

	.site-header__nav .nav-inquire > .wp-block-navigation-item__content {
		gap: calc(1em - 0.12em);
		padding: 0.8em 1em;
	}
}

@media (max-width: 40rem) {
	.site-header__inner.site-header__inner {
		min-height: var(--wp--custom--header-height-sm);
		padding-inline: var(--gutter);
	}
}

/* The header in two layers (front end, with the pinned header). The logo
   and the menu are drawn in DIFFERENCE against the page: ink over the paper,
   light over photos and dark bands, letter by letter where they cross an
   edge. "Plan your stay" is glass, and glass must see the page behind it,
   which nothing inside a blended layer can; so the button lives in a second
   layer of its own (`site-header__cta`, parts/header.html), above, in the
   same place.

   Blending only reaches the page if the blended box is itself a layer of the
   page, not something inside the pinned bar (a pinned bar is a layer of its
   own, and its children would blend only with it). So the bar's wrapper stops
   being one: it keeps the bar's height in the flow and nothing else, and the
   two rows are each fixed to the top of the screen, which pins them the same
   way. */
body.header-sticky .site-header.site-header,
body.header-sticky .wp-block-template-part:has(> .site-header__inner) {
	position: relative;
	z-index: auto;
	height: var(--wp--custom--header-height);
}

body.header-sticky .site-header__inner.site-header__inner,
body.header-sticky .site-header__cta.site-header__cta {
	position: fixed;
	top: 0;
	inset-inline: 0;
	z-index: var(--z-header);
	min-height: var(--wp--custom--header-height);
	margin: 0;
	padding-inline: var(--header-gutter);
}

body.header-sticky .site-header__inner.site-header__inner {
	--header-ink: var(--color-diff-text);
	--header-accent: var(--color-diff-accent);
	/* The tagline and the phone's menu button take this too. */
	color: var(--header-ink);
	mix-blend-mode: difference;
}

/* The button's layer: above the blended one, and only the button takes the
   pointer, so the menu under the rest of the row stays clickable. */
body.header-sticky .site-header__cta.site-header__cta {
	z-index: calc(var(--z-header) + 1);
	align-items: center;
	pointer-events: none;
}

.site-header__cta .wp-block-navigation-item {
	pointer-events: auto;
}

/* Over a band marked `header-white` (Hero 007's photo): the bar is plain
   white, not blended. assets/js/header-white.js adds `is-header-white` to the
   body while such a band is under the bar (inc/header.php sets it on load);
   the open phone menu keeps its own colours. The button takes a white border and
   white letters on a fainter pane. */
body.header-sticky.is-header-white .site-header__inner.site-header__inner:not(:has(.is-menu-open)) {
	--header-ink: var(--color-on-dark);
	--header-accent: var(--color-on-dark);
	mix-blend-mode: normal;
}

body.header-sticky.is-header-white .site-header__nav .nav-inquire > .wp-block-navigation-item__content {
	border-color: var(--color-on-dark);
	background-color: color-mix(in srgb, var(--color-on-dark) 12%, transparent);
	color: var(--color-on-dark);
}

body.header-sticky.is-header-white .site-header__inner,
body.header-sticky.is-header-white .site-header__inner .brand__name a,
body.header-sticky.is-header-white .site-header__inner .brand__name em,
body.header-sticky.is-header-white .site-header__nav .nav-inquire > .wp-block-navigation-item__content {
	transition: color 0.35s ease, border-color 0.35s ease, background-color 0.35s ease;
}

/* The menu keeps a copy of the button (`nav-inquire--menu`): unseen in the
   bar, where it only holds the button's room so the links sit where they
   always did, and the button itself in the phone menu. */
.site-header__nav .nav-inquire--menu {
	visibility: hidden;
}

.site-header__nav .is-menu-open .nav-inquire--menu {
	visibility: visible;
}

/* Without the pinned header (the editor, a page without it) the copy in the
   menu is the button and the second row is not drawn. */
body:not(.header-sticky) .site-header__cta {
	display: none;
}

body:not(.header-sticky) .site-header__nav .nav-inquire--menu {
	visibility: visible;
}

/* The phone menu is open: the menu is a paper panel over the page, read as
   it is, not blended, in its own colours; the second row's button gives way
   to the one in the panel. */
body.header-sticky .site-header__inner.site-header__inner:has(.is-menu-open) {
	--header-ink: var(--color-text);
	--header-accent: var(--color-accent);
	mix-blend-mode: normal;
}

/* Where core folds the menu behind a button (below 600px, its own
   breakpoint, not a design one), the second row's button is not shown: the
   phone has the copy in the menu. */
@media (max-width: 599.98px) {
	body.header-sticky .site-header__cta.site-header__cta {
		display: none;
	}
}

@media (max-width: 40rem) {
	body.header-sticky .site-header.site-header,
	body.header-sticky .wp-block-template-part:has(> .site-header__inner) {
		height: var(--wp--custom--header-height-sm);
	}

	body.header-sticky .site-header__inner.site-header__inner,
	body.header-sticky .site-header__cta.site-header__cta {
		min-height: var(--wp--custom--header-height-sm);
		padding-inline: var(--gutter);
	}
}

/* 3b. Footer ------------------------------------------------------------- */

.site-footer {
	border-top: 0;
}

.site-footer__inner.site-footer__inner {
	padding-block: var(--space-lg) var(--space-md);
}

.site-footer__top {
	gap: var(--space-md);
	padding-bottom: var(--space-lg);
	align-items: center;
}

.site-footer__top > .brand {
	margin-right: auto;
}

.site-footer__nav .wp-block-navigation__container {
	gap: var(--space-md);
}

.site-footer__nav .wp-block-navigation-item__content {
	display: inline-flex;
	align-items: center;
	gap: var(--space-xs);
	font-size: var(--wp--preset--font-size--base);
	color: var(--color-text);
}

.site-footer__bottom {
	padding-top: var(--space-md);
	background: var(--fade-rule) top / 100% 1px no-repeat;
	font-size: var(--wp--preset--font-size--sm);
	color: var(--color-muted);
}

.site-footer__bottom p {
	margin: 0;
}

.site-footer__bottom a {
	color: inherit;
	text-decoration: none;
}

@media (max-width: 40rem) {
	.site-footer__top > .brand {
		width: 100%;
		margin-right: 0;
	}

	.site-footer__bottom {
		font-size: var(--wp--preset--font-size--xs);
	}

	.site-footer__tagline {
		display: none;
	}
}

/* 3c. Labels and text ---------------------------------------------------- */

/* Small spaced capitals above a heading: "WELCOME TO ISTRIA". */
.eyebrow {
	margin: 0;
	font-size: var(--wp--preset--font-size--base);
	font-weight: 500;
	letter-spacing: 0.14em;
	line-height: 1.6;
	text-transform: uppercase;
}

/* `.section-label` (an eyebrow that heads a section) draws nothing of its
   own: eyebrows carry no dot (decided 2026-11-14). The class stays in the
   markup as a hook. */

/* Body copy in a section: the draft's quieter grey. */
.is-muted {
	color: var(--color-muted);
}

@media (max-width: 40rem) {
	.eyebrow {
		font-size: var(--wp--preset--font-size--sm);
	}
}

/* 3d. Buttons and the arrow ---------------------------------------------- */

/* Colour changes ease in and out, so a hover never flashes. */
.wp-block-button__link.wp-block-button__link,
.wp-element-button.wp-element-button {
	transition: background-color 0.35s ease, border-color 0.35s ease, color 0.35s ease;
}

/* Hover lifts the ink a shade instead of darkening it, as in the draft. */
.wp-block-button__link:hover,
.wp-block-button__link:focus-visible {
	--button-bg: color-mix(in srgb, var(--color-primary) 82%, var(--color-on-dark));
	--button-border-color: color-mix(in srgb, var(--color-primary) 82%, var(--color-on-dark));
}

.is-style-secondary .wp-block-button__link:hover,
.is-style-secondary .wp-block-button__link:focus-visible {
	--button-bg: var(--color-primary);
	--button-border-color: var(--color-primary);
}

/* `button-sm` on a Buttons block: the compact pair under a home card. */
.button-sm .wp-block-button__link.wp-block-button__link {
	padding: 0.65em 1.2em;
	white-space: nowrap;
}

/* The arrow's gap from the label is the button's side padding, so it sits as
   far from the words as from the edge. */
.button-sm .wp-block-button.has-arrow .wp-block-button__link {
	gap: calc(1.2em - 0.13em);
}

/* `has-arrow` on a Button or a Navigation link (and every Fluent Forms submit):
   a thin diagonal arrow after the label, as a slot reel.

   The markup is inc/button-arrow.php: a window (`.button-arrow`) that clips,
   and a reel of three identical arrows on a diagonal inside it. On hover the
   reel slides two steps up and to the right: the arrow goes out, one passes,
   the third stops in place. On leaving, it slides back.

   The arrows never move as masks; the reel moves by `transform`, which the
   browser composites. (Animating `mask-position` re-rasterised the SVG at
   every step and stuttered.) The step is shorter than the window, so the next
   arrow is already in view as the last one leaves: no blink in between. */
.wp-block-button.has-arrow .wp-block-button__link,
.has-arrow > .wp-block-navigation-item__content {
	display: inline-flex;
	align-items: center;
	gap: var(--space-md);
}

/* In a button the gap is its side padding (--button-padding, §1: 1.6em),
   less the tracking after the last capital and its side bearing (about
   0.13em), so the arrow's ink sits as far from the label's as from the edge. */
.wp-block-button.has-arrow .wp-block-button__link {
	gap: calc(1.6em - 0.13em);
}

.site-footer__nav .has-arrow > .wp-block-navigation-item__content {
	gap: var(--space-xs);
}

/* The window. Whole pixels, so the thin stroke never sits between pixels and
   shimmers as it moves. */
.button-arrow {
	--arrow: 20px;
	--arrow-step: calc(var(--arrow) * 0.75);

	position: relative;
	flex: none;
	width: var(--arrow);
	height: var(--arrow);
	overflow: hidden;
}

.site-footer__nav .button-arrow {
	--arrow: 16px;
}

.button-arrow__reel {
	position: absolute;
	inset: 0;
	transition: transform 0.9s cubic-bezier(0.45, 0, 0.2, 1);
	/* Its own layer from the start: created on hover instead, the layer switch
	   repainted the button and its fill flickered on enter and on leave. */
	will-change: transform;
}

/* The rolling letters in a button (the framework's is-text-roll) move by
   transform too: the same, so starting and ending a roll never repaints the
   button's fill. Buttons only; a menu's letters stay as the framework sets
   them. */
.wp-block-button__link .text-roll__glyph,
.nav-inquire .text-roll__glyph {
	will-change: transform;
}

.button-arrow__glyph {
	position: absolute;
	inset: 0;
	background-color: currentColor;
	-webkit-mask: url("../masks/arrow-up-right.svg") center / contain no-repeat;
	mask: url("../masks/arrow-up-right.svg") center / contain no-repeat;
}

/* The reel: each next arrow sits one step down and to the left. */
.button-arrow__glyph:nth-child(2) {
	transform: translate(calc(var(--arrow-step) * -1), var(--arrow-step));
}

.button-arrow__glyph:nth-child(3) {
	transform: translate(calc(var(--arrow-step) * -2), calc(var(--arrow-step) * 2));
}

/* The footer's "Back to the sunshine": up arrows, a vertical reel. */
.button-arrow--up .button-arrow__glyph {
	-webkit-mask-image: url("../masks/arrow-up.svg");
	mask-image: url("../masks/arrow-up.svg");
}

.button-arrow--up .button-arrow__glyph:nth-child(2) {
	transform: translateY(var(--arrow-step));
}

.button-arrow--up .button-arrow__glyph:nth-child(3) {
	transform: translateY(calc(var(--arrow-step) * 2));
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
	.wp-block-button__link:hover .button-arrow__reel,
	.wp-block-navigation-item__content:hover .button-arrow__reel,
	.ff-btn-submit:hover .button-arrow__reel {
		transform: translate(calc(var(--arrow-step) * 2), calc(var(--arrow-step) * -2));
	}

	.wp-block-navigation-item__content:hover .button-arrow--up .button-arrow__reel {
		transform: translateY(calc(var(--arrow-step) * -2));
	}
}

/* 3e. Accordion rows ----------------------------------------------------- */

/* `accordion-row` on a core Details block: the values and the FAQ. A hairline
   below each row, a thin plus that turns into a minus, and no marker. The
   one-open-at-a-time behaviour and the line reveal are site.js (see there). */
.accordion-row {
	margin: 0;
	background: var(--fade-rule) bottom / 100% 1px no-repeat;
	overflow-anchor: none;
}

.accordion-row:last-child {
	background: none;
}

.accordion-row > summary {
	position: relative;
	display: flex;
	align-items: center;
	gap: var(--space-md);
	padding: var(--space-md) 2.5rem var(--space-md) 0;
	border-radius: var(--radius-sm);
	cursor: pointer;
	list-style: none;
	transition: color 0.25s;
}

.accordion-row > summary::-webkit-details-marker {
	display: none;
}

/* The plus/minus: two 16 px lines; the upright one turns flat when open. */
.accordion-row > summary::before,
.accordion-row > summary::after {
	content: "";
	position: absolute;
	right: 5px;
	top: 50%;
	width: 16px;
	height: 1px;
	background: var(--color-text);
	transition: transform 0.3s;
}

.accordion-row > summary::after {
	transform: rotate(90deg);
}

.accordion-row[open] > summary::after {
	transform: rotate(0);
}

.accordion-row > summary:hover {
	color: var(--color-secondary);
}

/* Scroll-driven rows (site.js): the scroll alone opens and closes them, so
   the summary is no control: no pointer, no hover colour. The plus/minus
   stays, showing whether the row is open. */
.accordion-group--scroll .accordion-row > summary {
	cursor: default;
}

.accordion-group--scroll .accordion-row > summary:hover {
	color: inherit;
}

.accordion-row > :not(summary) {
	margin-block: 0;
}

.accordion-row > :not(summary):last-child {
	padding-bottom: var(--space-md);
}

.accordion-row .accordion-line {
	display: block;
	overflow: hidden;
}

.accordion-row .accordion-line__text {
	display: block;
	backface-visibility: hidden;
}

/* 3f. House facts and rating --------------------------------------------- */

/* `house-facts` on a List block, one item per fact, each item carrying its
   kind as a class (`is-area`, `is-guests`, `is-bedrooms`, `is-bathrooms`,
   `is-pets`): the icon comes from that class, the words stay editable. */
.house-facts {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: var(--space-sm) var(--space-md);
	margin: var(--space-md) 0;
	padding: var(--space-md) 0;
	list-style: none;
	background:
		var(--fade-rule) top / 100% 1px no-repeat,
		var(--fade-rule) bottom / 100% 1px no-repeat;
}

.house-facts li {
	display: flex;
	align-items: center;
	gap: var(--space-xs);
	margin: 0;
	font-size: var(--wp--preset--font-size--lg);
	font-weight: 500;
	line-height: 1.3;
	white-space: nowrap;
}

.house-facts li::before {
	content: "";
	flex-shrink: 0;
	width: 22px;
	height: 22px;
	background-color: currentColor;
	-webkit-mask: var(--fact-icon) center / contain no-repeat;
	mask: var(--fact-icon) center / contain no-repeat;
}

.house-facts .is-area      { --fact-icon: url("../masks/area.svg"); }
.house-facts .is-guests    { --fact-icon: url("../masks/guests.svg"); }
.house-facts .is-bedrooms  { --fact-icon: url("../masks/bedrooms.svg"); }
.house-facts .is-bathrooms { --fact-icon: url("../masks/bathrooms.svg"); }
.house-facts .is-pets      { --fact-icon: url("../masks/pets.svg"); }

.house-facts .is-pets {
	font-size: var(--wp--preset--font-size--sm);
	font-weight: 400;
}

/* The label words, written as emphasis in the item so the editor keeps them,
   are for screen readers: hidden on screen. */
.house-facts em {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
}

/* `rating` on a paragraph: "★ 5.0 / 26 Airbnb reviews". The star and the
   count are spans the editor keeps (`<strong>` for the star, `<em>` for the
   count, restyled here). */
.rating {
	display: flex;
	align-items: center;
	gap: var(--space-xs);
	margin: 0;
	font-size: var(--wp--preset--font-size--base);
}

.rating strong {
	font-weight: 400;
	font-size: var(--wp--preset--font-size--lg);
	color: var(--color-accent);
}

.rating em {
	font-style: normal;
	font-size: var(--wp--preset--font-size--sm);
	color: var(--color-muted);
}

@media (max-width: 40rem) {
	.house-facts {
		column-gap: var(--space-sm);
	}

	.house-facts li {
		font-size: var(--wp--preset--font-size--base);
		gap: var(--space-xs);
	}

	.house-facts li::before {
		width: 20px;
		height: 20px;
	}

	.house-facts .is-pets {
		font-size: var(--wp--preset--font-size--xs);
	}
}


/* 3g. Rise scene ---------------------------------------------------------- */

/* `rise-scene` on a full-width section whose first child is a full-screen
   photo (`rise-scene__photo`) and whose next is a panel (`rise-scene__panel`)
   (or a panel alone, rising over the previous section's last photo, below):
   the photo holds still (sticky) while the panel rises over it as the page
   scrolls. The holding is CSS alone. site.js draws the panel's top as the
   arc of a circle (clip-path): it comes up as a circle narrower than the
   screen and grows as it rises, its top flattening until, as it reaches the
   top of the screen, it fills the width; then the clip is dropped. Without
   the script the panel simply has a straight top. The manifesto (Content
   Band 001) and the amenities (Feature Grid 004) are built on it.
   `rise-scene--dark` gives the panel the deepest ink. */
.rise-scene {
	position: relative;
	isolation: isolate;
}

.rise-scene.rise-scene > * {
	margin-block: 0;
}

/* A rise scene with no photo of its own, right after a section that holds a
   full-screen photo at its end (the sideways rooms, Content Slider 001,
   `is-horizontal` from tablets up; or Content Band 003 while it is telling its reviews), rises over THAT
   photo. For the rooms: the rooms hold their full-screen final photo for a
   screen at the end (pattern.js, HOLD), and this scene moves up by a screen
   to overlap that stretch, its panel coming in from the bottom with its
   curve. Above the rooms' pinned stage. On phones the rooms stack and there
   is no hold, so nothing overlaps. Front end only: in the editor (body
   `editor-styles-wrapper`) nothing is pinned, so the sections simply stack. */
body:not(.editor-styles-wrapper) .wp-block-post-content > :is(.content-slider-001.is-horizontal, .content-band-003.is-telling) + .rise-scene.rise-scene:not(:has(> .rise-scene__photo)) {
	z-index: 1;
	margin-block-start: -100svh;
}

/* The lift-off: a rise scene followed by the aerial map (Map Band 001) does
   not scroll away over it; it lifts off it. The map moves up by a screen,
   under the sheet, so its pinned photo is already waiting there, and the
   sheet's bottom edge, with a soft shadow, uncovers it from below as it rises.
   Only with the motion (pattern.js); without it the two simply follow. */
@media (prefers-reduced-motion: no-preference) {
	html.js .wp-block-post-content > .rise-scene:has(+ .map-band-001) {
		z-index: 2;
	}

	html.js .wp-block-post-content > .rise-scene + .map-band-001.map-band-001 {
		margin-block-start: -100svh;
	}

	html.js .rise-scene:has(+ .map-band-001) > .rise-scene__panel {
		box-shadow: 0 2.5rem 4rem -1rem color-mix(in srgb, var(--color-ink-deep) 40%, transparent);
	}
}

/* A chapter sits flush against its neighbours: no block gap above a rise
   scene or above the full-width section that follows one. The gap is the
   page's paper showing between sections, invisible between paper sections
   but a strip between a dark chapter and a white one. */
.wp-block-post-content > .rise-scene.rise-scene,
.wp-block-post-content > .rise-scene + .alignfull.alignfull {
	margin-block-start: 0;
}

/* Doubled class: the editor makes every block position:relative. */
.rise-scene .rise-scene__photo.rise-scene__photo {
	position: sticky;
	top: 0;
	z-index: 0;
	height: 100svh;
	overflow: hidden;
}

.rise-scene__photo img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.rise-scene .rise-scene__panel.rise-scene__panel {
	--rise-fill: var(--color-background);

	position: relative;
	z-index: 1;
	min-height: 100svh;
	background: var(--rise-fill);
}

.rise-scene--dark .rise-scene__panel.rise-scene__panel {
	--rise-fill: var(--color-ink-deep);

	color: var(--color-on-dark);
}

/* With site.js (`is-curving`) the panel's fill moves onto two layers behind
   its content, and only the top one, one screen tall, carries the circle
   (--rise-clip). Clipping the whole panel, several screens tall with all its
   content, was heavy enough to repaint that Chrome now and then showed a
   frame without the curve: a flicker. The content itself is never clipped. */
.rise-scene.is-curving .rise-scene__panel.rise-scene__panel {
	background: transparent;
}

.rise-scene.is-curving .rise-scene__panel::before,
.rise-scene.is-curving .rise-scene__panel::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	z-index: -1;
	background: var(--rise-fill);
	pointer-events: none;
}

.rise-scene.is-curving .rise-scene__panel::before {
	top: 0;
	height: 100svh;
	clip-path: var(--rise-clip, none);
	will-change: clip-path;
}

/* The rest, from one screen down: never on screen while the top is curved.
   It starts a pixel early so no seam shows where the two meet. */
.rise-scene.is-curving .rise-scene__panel::after {
	top: calc(100svh - 1px);
	bottom: 0;
}

.rise-scene--dark :is(h1, h2, h3, h4, h5, h6) {
	color: inherit;
}

/* 3h. Pool ceiling --------------------------------------------------------
   `is-pool-ceiling` on a full-width block: its background is the light a pool
   throws on a ceiling, drawn by assets/js/pool-ceiling.js on a canvas behind
   the content. Two colours, from the palette: the plaster, which is the page
   colour itself (only the light lines show), and the colour the lines reach.
   Without WebGL the section is simply the page colour. */

.is-pool-ceiling {
	--pool-ceiling-base: var(--color-background);
	--pool-ceiling-light: var(--color-on-dark);

	position: relative;
	/* The canvas sits behind the content, inside the section's own layer. */
	isolation: isolate;
	background: var(--pool-ceiling-base);
}

/* Doubled class: the editor makes every block position: relative (the canvas
   is not a block, but the same weight keeps the two rules from tying). */
.is-pool-ceiling .pool-ceiling__canvas.pool-ceiling__canvas {
	position: absolute;
	inset: 0;
	z-index: -1;
	display: block;
	width: 100%;
	height: 100%;
	pointer-events: none;
}

/* As the page's first section, the ceiling runs up under the header: the
   canvas reaches up by the header's height, and the header (transparent
   until the first section is passed: inc/header.php) shows it through, so
   there is no edge where the bar ends. The section itself does not move. */
.wp-block-post-content > .is-pool-ceiling:first-child .pool-ceiling__canvas.pool-ceiling__canvas {
	top: calc(var(--header-height) * -1);
	height: calc(100% + var(--header-height));
}

/* 3i. Enlarge cursor -----------------------------------------------------
   Over a photo with "Enlarge on click" (core's lightbox), the mouse becomes a
   frosted glass ring with a thin plus, the photo showing through it
   (site.js §5). While the photo is open, the ring is a minus over it (a
   click anywhere closes it); over the overlay's buttons the mouse is itself. Only on a device with a mouse; the script adds
   `has-enlarge-cursor` to <html> when it takes over, so without it core's
   zoom-in cursor stays. Core's own enlarge button in the photo's corner is
   hidden while the mouse is over the photo (the ring says the same) but
   still appears on keyboard focus. A faint shade around the ring keeps it
   visible on a bright photo. */

.enlarge-cursor {
	position: fixed;
	left: 0;
	top: 0;
	z-index: 100001; /* above core's lightbox overlay (100000) */
	width: 4rem;
	height: 4rem;
	margin: -2rem 0 0 -2rem;
	border: 1px solid color-mix(in srgb, var(--color-on-dark) 85%, transparent);
	border-radius: 50%;
	background: color-mix(in srgb, var(--color-on-dark) 18%, transparent);
	box-shadow: 0 0 1.5rem color-mix(in srgb, var(--color-shade) 18%, transparent);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	pointer-events: none;
	transform: scale(0);
	opacity: 0;
}

/* The plus: two thin lines, like the accordion's. */
.enlarge-cursor::before,
.enlarge-cursor::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 1.125rem;
	height: 1px;
	margin: -0.5px 0 0 -0.5625rem;
	background: var(--color-on-dark);
}

.enlarge-cursor::after {
	transform: rotate(90deg);
	transition: transform 0.3s;
}

/* Over the open photo: the upright line turns flat, a minus. */
.enlarge-cursor.is-minus::after {
	transform: rotate(0);
}

.has-enlarge-cursor .wp-lightbox-container img,
.has-enlarge-cursor .wp-lightbox-overlay {
	cursor: none;
}

.has-enlarge-cursor .wp-lightbox-overlay button {
	cursor: pointer;
}

.has-enlarge-cursor .wp-lightbox-container img:hover + button:not(:focus-visible) {
	opacity: 0;
}

/* The open photo sits on deep ink, not the page's paper (decided
   2026-11-14), with its close and arrow buttons light. !important: core
   writes both colours inline, from the site's background and text. */
.wp-lightbox-overlay .scrim {
	background-color: var(--color-ink-deep) !important;
}

.wp-lightbox-overlay .wp-lightbox-close-button,
.wp-lightbox-overlay .wp-lightbox-navigation-button {
	color: var(--color-on-dark);
	fill: var(--color-on-dark) !important;
}

/* Enlarge on click shows the whole photo (inc/lightbox-whole-photo.php).
   Core places the enlarge button from the shape it is told the photo has,
   which is now the whole photo's, not the crop on the page: the button
   floated inside a cropped photo. Pinned to the corner of the photo as shown.
   !important: core writes the position inline. */
.wp-lightbox-container .lightbox-trigger.lightbox-trigger {
	top: 16px !important;
	right: 16px !important;
}

/* The zoom opens out of the crop on the page and closes back into it: core's
   transform, with a clip from the crop to the whole photo. The start values
   are set on the box by assets/js/lightbox-whole-photo.js; without them the
   clip is none and this is core's zoom. Weighted above core's own rule. */
@media (prefers-reduced-motion: no-preference) {
	body .wp-lightbox-overlay.zoom.active .lightbox-image-container {
		animation-name: msl-zoom-in;
	}

	body .wp-lightbox-overlay.zoom.show-closing-animation:not(.active) .lightbox-image-container {
		animation-name: msl-zoom-out;
	}
}

@keyframes msl-zoom-in {
	0% {
		transform: translate(calc((-100vw + var(--wp--lightbox-scrollbar-width)) / 2 + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale));
		clip-path: var(--msl-clip, inset(0%));
	}

	100% {
		transform: translate(-50%, -50%) scale(1, 1);
		clip-path: inset(0%);
	}
}

@keyframes msl-zoom-out {
	0% {
		visibility: visible;
		transform: translate(-50%, -50%) scale(1, 1);
		clip-path: inset(0%);
	}

	99% {
		visibility: visible;
	}

	100% {
		visibility: hidden;
		transform: translate(calc((-100vw + var(--wp--lightbox-scrollbar-width)) / 2 + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale));
		clip-path: var(--msl-clip, inset(0%));
	}
}

/* -------------------------------------------------------------------------
 * E. Effects — assets/js/special-effects.js
 *
 * Switched on by a class on a block (Advanced → Additional CSS class(es)):
 *
 *   .image-reveal, -top, -right, -left   a photo wipes in, scrubbed to scroll
 *   .image-reveal-once (+ -top)          a photo wipes in once, on entry
 *   .image-reveal-veil                   a veil over the photo is wiped away
 *   .staggered-blur-reveal               a heading's letters blur in (SplitText)
 *   .stagger-group > .stagger-item       items enter one after another
 *   .reveal-lines                        a paragraph's lines rise in
 *   .is-scroll-recede                    a section scrolls slower than the page
 *   .settle-in                           CSS only: a first-screen image settles
 *
 * Every hold that hides content from the first paint sits under `html.js`
 * (set by inc/enqueue.php before anything paints) and carries a 3s failsafe,
 * so no script, a script error or reduced motion always ends on visible
 * content. documentation/special-effects.md describes each one.
 * ------------------------------------------------------------------------- */

/* Image reveal — the image wipes in, and fades up with it, as it enters the
 * viewport. Both properties ride one tween on one ease, so they stay locked
 * together for the whole run rather than drifting apart. Timing lives in the
 * script, not here — don't restate the duration in this comment.
 *
 * Four directions, one mechanism: the variant classes below set ONLY which edge
 * the hidden state clips from. The script animates every one of them to
 * `inset(0% 0% 0% 0%)` and never learns the direction, so adding a fifth is a
 * one-line CSS change with no JS to touch.
 *
 * The resting state here is the VISIBLE one, and the clipped/transparent start
 * state is armed by assets/js/special-effects.js adding `.is-reveal-armed`. That
 * split is the whole progressive-enhancement story: a clip-path reveal is the
 * inverse of the parent's parallax, whose CSS is harmlessly inert without its JS
 * (main.css §8b — the oversize needs a definite height the JS supplies). Here
 * the hidden state is active by construction, so declaring `clip-path` and
 * `opacity` unconditionally means no GSAP, a JS error, or
 * `prefers-reduced-motion` leaves the photo PERMANENTLY invisible — with nothing
 * on screen to say why.
 *
 * The script arms only when it is actually going to animate, so every bail-out
 * path lands on a plainly visible image.
 *
 * No flash from arming late: these figures sit well below the fold, and the
 * script is a normal blocking footer script, so it runs before they could paint.
 */
.image-reveal,
.image-reveal-top,
.image-reveal-right,
.image-reveal-left {
  overflow: hidden;
}

.image-reveal img,
.image-reveal-top img,
.image-reveal-right img,
.image-reveal-left img {
  display: block;
  width: 100%;
  height: auto;
}

/* Direction — the only thing that differs between the four.
 *
 * `inset()` clips AWAY from each edge, in the order top/right/bottom/left. So
 * the hidden state clips 100% from the edge the wipe travels TOWARD, which
 * leaves a zero-size strip pinned against the OPPOSITE edge for the image to
 * grow out of. Handy consequence: the class suffix and the inset edge are the
 * same word, which is the only reason four inset() quadruples stay readable.
 *
 * `.image-reveal` is the "-bottom" case under a shorter name, per the brief.
 *
 * The variants sit AFTER the default deliberately. All four weigh (0,1,0), so a
 * figure carrying both `image-reveal` and `image-reveal-left` is decided by
 * source order — this way the specific direction wins instead of it being a
 * coin toss nobody can see in the markup.
 *
 * Declared on the FIGURE and read on the <img>: custom properties inherit, and
 * keeping the switch on the figure is what lets one class change a direction. */
.image-reveal       { --reveal-from: inset(0% 0% 100% 0%); }  /* top → bottom */
.image-reveal-top   { --reveal-from: inset(100% 0% 0% 0%); }  /* bottom → top */
.image-reveal-right { --reveal-from: inset(0% 100% 0% 0%); }  /* left → right */
.image-reveal-left  { --reveal-from: inset(0% 0% 0% 100%); }  /* right → left */

/* Armed by the script. Two classes = (0,2,1), which is deliberate: it must not
   collide with the parallax rules on the same <img> — those set height/top/
   position, this sets only clip-path and opacity, so they compose rather than
   compete.

   NOTE: the fade is currently OFF at both ends — opacity is 1 here, and the
   tween's `opacity` line is commented out in special-effects.js. Re-enabling it
   means changing both. */
.image-reveal.is-reveal-armed img,
.image-reveal-top.is-reveal-armed img,
.image-reveal-right.is-reveal-armed img,
.image-reveal-left.is-reveal-armed img {
  clip-path: var(--reveal-from);
  opacity: 1;
}

/* One-shot image reveal — `.image-reveal-once` on an Image block. The photo
 * wipes open top to bottom, once, when it comes into view (special-effects.js).
 * For a photograph near the top of a page, where the scrubbed reveal above has
 * no scroll distance to ride.
 *
 * Held closed from the FIRST PAINT, unlike the scrubbed reveal: this one is
 * meant for images that are on screen at load, where arming late would show
 * the photo, hide it, then wipe it in. The same guards as the staggered groups
 * below give it back on every path where the effect will not run — no
 * JavaScript, reduced motion, the script bailing (`is-reveal-armed`), or the
 * script never arriving (the 3s failsafe). */
/* The edge it opens from, read by the script too: top down by default;
   `image-reveal-once-top` beside it opens bottom up, like `image-reveal-top`. */
.image-reveal-once     { --reveal-once-from: inset(0% 0% 100% 0%); }
.image-reveal-once-top { --reveal-once-from: inset(100% 0% 0% 0%); }

/* Not in the lightbox: "Enlarge on click" copies the figure, classes and all,
   into its overlay, where nothing arms it; held, the enlarged photo was blank
   (Feature Grid 005). The veil's hold below is excluded the same way. */
@media (prefers-reduced-motion: no-preference) {
  html.js .image-reveal-once:not(.is-reveal-armed):not(.wp-lightbox-overlay *) {
    clip-path: var(--reveal-once-from);
    animation: image-reveal-once-failsafe 0s 3s forwards;
  }
}

/* The script has it and is waiting for it to come into view: still closed,
   by this stylesheet rather than an inline clip (core's lightbox script can
   wipe that, see special-effects.js), and the failsafe stood down. */
@media (prefers-reduced-motion: no-preference) {
  html.js .image-reveal-once.is-reveal-waiting:not(.is-reveal-armed):not(.wp-lightbox-overlay *) {
    animation: none;
  }
}

@keyframes image-reveal-once-failsafe {
  to { clip-path: inset(0% 0% 0% 0%); }
}

/* Veil reveal — `.image-reveal-veil` on an Image block. The photo is there
 * from the first paint under a veil (white, or the section's own background —
 * and the veil is wiped away top to bottom (special-effects.js):
 * the same gesture as the one-shot wipe above, but drawn on the veil, not on
 * the photo.
 *
 * That difference is the point of it. Chrome times the Largest Contentful
 * Paint by when an image is painted and does not look at what lies on top. The
 * one-shot wipe clips the photo itself, so on a cover photo it was counted only
 * once the script had opened it — 1.9s of "render delay" in PageSpeed for a
 * photo that had long arrived. Covered instead of clipped, it counts at once.
 *
 * Before the script: a plain veil, this ::after, from the first paint. The
 * script lays its own veil over it and adds `is-reveal-armed` in the same step,
 * which takes the plain one away. The same exits as the reveals above give the
 * photo back — no JavaScript, reduced motion, the script bailing, or the 3s
 * failsafe if it never arrives. */
.image-reveal-veil {
  position: relative;
}

@media (prefers-reduced-motion: no-preference) {
  html.js .image-reveal-veil:not(.is-reveal-armed):not(.wp-lightbox-overlay *)::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    background: var(--reveal-veil, var(--color-background));
    animation: image-reveal-veil-failsafe 0s 3s forwards;
  }
}

@keyframes image-reveal-veil-failsafe {
  to { visibility: hidden; }
}

/* The script's veil: removed once the wipe is done. */
.image-reveal-veil__veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: var(--reveal-veil, var(--color-background));
}

/* Staggered blur reveal — assets/js/special-effects.js splits a
 * `.staggered-blur-reveal` heading into characters and animates them in.
 *
 * All the motion is GSAP's; this only warns the browser that each character
 * is about to animate filter and opacity, so the blur is composited rather
 * than repainted per frame. Scoped under the effect class because SplitText's
 * `.char` is a generic name that would otherwise catch anything on the site.
 *
 * The split is reverted once the animation finishes, so these wrappers exist
 * only for the duration of the reveal. */
.staggered-blur-reveal .char {
  will-change: filter, opacity;
}

/* Hidden from the first paint, so the heading does not flash as plain text,
 * vanish when the script splits it, then animate back in.
 *
 * Every way this could strand the heading invisible is closed off, because a
 * CSS-hidden element that waits on a script is the same trap the image reveal
 * is built to avoid:
 *   - no JS at all        -> `html.js` is never set (inc/enqueue.php), so this
 *                            never applies and the heading is simply visible;
 *   - reduced motion      -> the media query below shows it; the script bails;
 *   - GSAP or SplitText missing, or the script bails -> the script un-hides it;
 *   - the script errors or never arrives -> the failsafe animation shows it
 *     after 3s regardless. The script, if it turns up later than that, leaves
 *     the heading plain rather than hide a visible heading to animate it.
 *
 * `visibility`, not `opacity`: the script hides each character itself, and
 * a parent's opacity would multiply into theirs; visibility is simply switched
 * back on for the element the moment the characters are in place. */
html.js .staggered-blur-reveal {
  visibility: hidden;
  animation: staggered-blur-reveal-failsafe 0s 3s forwards;
}

@keyframes staggered-blur-reveal-failsafe {
  to { visibility: visible; }
}

/* Staggered groups (`.stagger-group` > `.stagger-item`, special-effects.js):
 * held invisible from the FIRST PAINT, for the same reason as the heading
 * above. The script runs after the page has been drawn, so without this the
 * items showed, were hidden by the script, and then animated in — a flash on
 * every load.
 *
 * Opacity, not `display` or `visibility`: the content stays in the document,
 * in the layout and in the accessibility tree, so nothing moves when it
 * appears and a crawler reads the page exactly as before.
 *
 * Every way the effect might NOT run gives the items back:
 *   - no JavaScript            `html.js` is never set, so this never applies
 *   - reduced motion            excluded by the media query
 *   - the script bails or runs  it adds `is-stagger-armed` to the group
 *   - the script never arrives  the failsafe shows them after 3s
 * An item outside a group is never animated, so it is never hidden. */
@media (prefers-reduced-motion: no-preference) {
  html.js .stagger-group:not(.is-stagger-armed) .stagger-item {
    opacity: 0;
    animation: stagger-item-failsafe 0s 3s forwards;
  }
}

@keyframes stagger-item-failsafe {
  to { opacity: 1; }
}

/* Settle — `.settle-in` on a page's largest first-screen element when that is
 * an image. It is
 * painted in full at the first frame and only eases down into place: a touch
 * larger and lower, settling over 1.1s.
 *
 * The opposite of the holds above, on purpose. Those keep an element invisible
 * until special-effects.js arrives, and PageSpeed times its largest paint by
 * the moment that element is shown — so a hidden first-screen element cost one page
 * about 1.5s of "render delay" and most of its Performance score. This needs
 * no script and hides nothing: the motion is transform only, from the first
 * paint, so the element counts as painted at once. Not a stagger item for the
 * same reason — leave `stagger-item` off it. */
@media (prefers-reduced-motion: no-preference) {
  .settle-in {
    animation: settle-in 1.1s cubic-bezier(0.16, 1, 0.3, 1) both;
  }
}

@keyframes settle-in {
  from { transform: translateY(28px) scale(1.035); }
}

@media (prefers-reduced-motion: reduce) {
  html.js .staggered-blur-reveal {
    visibility: visible;
    animation: none;
  }
}

/* -------------------------------------------------------------------------
 * Line reveal — `.reveal-lines`
 *
 * A heading or paragraph rises into place line by line from behind a mask
 * (assets/js/special-effects.js). Shared by the editorial patterns, so it
 * lives here rather than in any one of them.
 *
 * Hidden from first paint under `html.js`, with the same three exits as the
 * blur reveal above: no JS never hides it, reduced motion shows it, and the
 * failsafe shows it after 3s if the script never arrives.
 * ---------------------------------------------------------------------- */
html.js .reveal-lines {
	visibility: hidden;
	animation: staggered-blur-reveal-failsafe 0s 3s forwards;
}

/* SplitText wraps each line in a clipping mask exactly one line-box tall, and
   Hedvig's descenders hang below a tight line box — at line-height 1.1 the tail
   of a "j" or "g" was cut off for the whole reveal. The padding gives the mask
   room for the descender and the equal negative margin takes that room back, so
   the line spacing does not change. */
.reveal-lines .line-mask {
	padding-block-end: 0.18em;
	margin-block-end: -0.18em;
}

@media (prefers-reduced-motion: reduce) {
	html.js .reveal-lines {
		visibility: visible;
		animation: none;
	}
}

/* -------------------------------------------------------------------------
 * Recede — `is-scroll-recede`
 *
 * Add the class to a full-width section and, as it leaves, it scrolls at a
 * fraction of the page's speed while what FOLLOWS slides up over it at full
 * speed (assets/js/special-effects.js). The motion is the script's; the
 * stacking that makes the rest of the page cover it is here.
 *
 *   --scroll-recede   how much it is held back, 0 to 0.9 — 0.5 is half speed.
 *                     Set it on the section to tune one instance.
 *
 * EVERYTHING after the section is stacked above it, not just the next block.
 * The receding section is still on screen for half a viewport after its slot
 * has scrolled away, and a short block right after it (a one-line row under a
 * cover, say) is not tall enough to hide that on its own — the section would
 * reappear below it, on top of whatever came next.
 *
 * Whatever covers has to be opaque, or the receding section shows through. A
 * following block with no background of its own is therefore given the page's
 * colour — what was showing behind it anyway.
 *
 * Both of those rules weigh NOTHING (`:where`), so a block's own position or
 * background, set in the editor or by its pattern, always wins. That is also
 * what lets the class sit on consecutive sections: a receding section keeps
 * its own z-index below, and among equals the later one in the page is on top.
 *
 * Without the script (no JS, reduced motion) none of this changes anything you
 * can see: sections, one after the other.
 * ---------------------------------------------------------------------- */
.is-scroll-recede {
	--scroll-recede: 0.5;

	position: relative;
	z-index: 0;
}

:where(.is-scroll-recede ~ *) {
	position: relative;
	z-index: 1;
}

/* A receding section that FOLLOWS another receding section. The rule above
   is zero weight, so `.is-scroll-recede`'s own z-index: 0 won over it and the
   later section sat BELOW everything between the two — the sections before
   it are at 1. Harmless while every section stays inside its box; the moment
   one of them let content run past its edge (a section whose
   items recede on phones, its last one reaching into the next section) that
   content painted over the following section instead of under it. At 1, in
   source order, it paints above what precedes it and still below what
   follows, which is all a receding section needs. */
.is-scroll-recede ~ .is-scroll-recede {
	z-index: 1;
}

:where(.is-scroll-recede ~ :not(.has-background)) {
	background-color: var(--color-background);
}

/* The block right after may sit inside the page gutter (`has-gutter`) rather
   than run edge to edge, and its background stops where it does — leaving a
   strip either side through which the receding section shows. A spread shadow
   in the same colour carries the fill out to the screen edges; the clip keeps
   that shadow from reaching up or down over its neighbours. Only the adjacent
   block needs it: it is the one that meets the receding section edge to edge. */
:where(.is-scroll-recede + :not(.has-background)) {
	box-shadow: 0 0 0 100vmax var(--color-background);
	clip-path: inset(0 -100vmax);
}


/* -------------------------------------------------------------------------
 * Dark scene — `dark-scene`
 *
 * Add the class to a section and, from when its top reaches the middle of
 * the screen, the whole page turns dark: the paper becomes the deep ink of the
 * enlarged photo's overlay (§3i) and the ink becomes white. It turns back as
 * the next section's content comes into view. site.js (6) sets `is-dark-scene` on <html>; this fades
 * the colours.
 *
 * It swaps the colour TOKENS, not each component's colours, so everything
 * drawn with them follows: text, headings, the accordion's plus/minus and
 * hairlines, the tags, the page's own paper. Registered as colours
 * (@property) so the browser can fade between their values; on <html> they
 * fade once, and every element inherits the colour of the moment. The
 * header needs nothing: it is blended by difference (§3a), so it already
 * reads light on a dark page.
 *
 * Without the script, or with reduced motion, nothing changes (reduced
 * motion: the page still turns dark, at once, without the fade).
 * ------------------------------------------------------------------------- */

@property --color-text {
	syntax: "<color>";
	inherits: true;
	initial-value: #253f4b;
}

@property --color-background {
	syntax: "<color>";
	inherits: true;
	initial-value: #f7f5ee;
}

@property --color-muted {
	syntax: "<color>";
	inherits: true;
	initial-value: #65757a;
}

@property --color-border {
	syntax: "<color>";
	inherits: true;
	initial-value: #b9cbd0;
}

@property --color-surface {
	syntax: "<color>";
	inherits: true;
	initial-value: #ffffff;
}

html {
	transition:
		--color-text 1s ease-in-out,
		--color-background 1s ease-in-out,
		--color-muted 1s ease-in-out,
		--color-border 1s ease-in-out,
		--color-surface 1s ease-in-out;
}

html.is-dark-scene {
	--color-text: var(--color-on-dark);
	--color-background: var(--color-ink-deep);
	--color-muted: color-mix(in srgb, var(--color-on-dark) 65%, var(--color-ink-deep));
	--color-border: color-mix(in srgb, var(--color-on-dark) 22%, var(--color-ink-deep));
	--color-surface: color-mix(in srgb, var(--color-on-dark) 6%, var(--color-ink-deep));
}

@media (prefers-reduced-motion: reduce) {
	html {
		transition: none;
	}
}
