/*
Theme Name: Gabe Woods
Author: JKC Consulting
Version: 1.1.0
*/

/* ==========================================================================
   1. TOKENS
   Five rules borrowed from republique.studio:
   1. Two spacing tokens, used everywhere. Not a scale.
   2. One easing curve, site-wide.
   3. One body font size. Plus a display size.
   4. All colour via variables — never hardcoded in a rule.
   5. body[class*=tpl-] + .is-loading are the only state hooks.
   ========================================================================== */
:root {
	font-size: 16px;

	/* Colour — every colour in this file resolves back to one of these. */
	--color-text: #0a0a0a;
	--color-link: #0a0a0a;
	--color-bg: #ffffff;
	--color-gray-500: #999999;
	--color-hover: #999999;
	--color-rule: #0a0a0a;

	/* Spacing — these two values, and nothing between them. */
	--sm-base-space: 1rem;
	--md-base-space: 1.5rem;

	/* Type — mono is the display face. Brutalist, monolithic, one voice.
	   Fraunces stays loaded but is not used structurally. */
	--font-display: "IBM Plex Mono", ui-monospace, "Courier New", monospace;
	--font-body: "Hanken Grotesk", Helvetica, Arial, sans-serif;
	--font-mono: "IBM Plex Mono", ui-monospace, monospace;
	/* Bundle values verbatim: 12px at every breakpoint — one body size, no
	   responsive step — with 1.22em leading and zero tracking. */
	--fs-main: 12px;
	--lh-main: 1.22em;
	--tracking-ui: 0;

	/* Motion — one curve, three durations. */
	--ease: cubic-bezier(0.7, 0, 0.3, 1);
	--dur-fast: 0.2s;
	--dur-base: 0.3s;
	--dur-slow: 0.6s;

	/* Set by JS in functions.php. Fallbacks keep layout sane pre-hydration.
	   --fh is the measured footer height: the v05 first screen is
	   calc(100vh - var(--hh) - var(--fh)), so a hardcoded footer height would
	   drift the moment the footer's contents change. */
	--hh: 64px;
	--fh: 46px;
	--vh: 1vh;
}

/* No responsive --fs-main step: the bundle ships 12px at both breakpoints. */

/* ==========================================================================
   2. FONTS
   Variable fonts — one file covers weights 100–900.
   ========================================================================== */
@font-face {
	font-family: "Fraunces";
	src: url("assets/fonts/fraunces-v.ttf") format("truetype-variations");
	font-weight: 100 900;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Fraunces";
	src: url("assets/fonts/fraunces-i.ttf") format("truetype-variations");
	font-weight: 100 900;
	font-style: italic;
	font-display: swap;
}

@font-face {
	font-family: "Hanken Grotesk";
	src: url("assets/fonts/hanken-v.ttf") format("truetype-variations");
	font-weight: 100 900;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Hanken Grotesk";
	src: url("assets/fonts/hanken-i.ttf") format("truetype-variations");
	font-weight: 100 900;
	font-style: italic;
	font-display: swap;
}

/* Real weights — never let the browser synthesize a bold. Faux-bolding a
   mono smears the stems and destroys the letterforms at display size.
   All four weights advance 0.6em, so the hero math is weight-independent. */
@font-face {
	font-family: "IBM Plex Mono";
	src: url("assets/fonts/ibm-light.ttf") format("truetype");
	font-weight: 300;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("assets/fonts/ibm.ttf") format("truetype");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("assets/fonts/ibm-medium.ttf") format("truetype");
	font-weight: 500;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("assets/fonts/ibm-semibold.ttf") format("truetype");
	font-weight: 600;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "IBM Plex Mono";
	src: url("assets/fonts/ibm-bold.ttf") format("truetype");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}

/* ==========================================================================
   3. RESET
   ========================================================================== */
html {
	box-sizing: border-box;
	background-color: var(--color-bg);
	-webkit-text-size-adjust: 100%; /* Safari on iOS, older Android */
	text-size-adjust: 100%;
}

*,
*::before,
*::after {
	box-sizing: inherit;
	margin: 0;
}

body {
	min-height: 100%;
	font-family: var(--font-body);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-text);
	background-color: var(--color-bg);
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-kerning: normal;
}

a {
	color: var(--color-link);
	text-decoration: none;
	cursor: pointer;
	transition: color var(--dur-fast) var(--ease);
}

/* Mono design: citations read as regular text, never italic. */
cite {
	font-style: normal;
}

@media (hover: hover) and (pointer: fine) {
	a:hover {
		color: var(--color-hover);
	}
}

a.active {
	color: var(--color-hover);
}

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

img,
video {
	display: block;
	max-width: 100%;
	height: auto;
}

/* The bundle never uses bold — every font-weight in it is normal, and it
   explicitly resets strong/b to normal too. Weight is not how this design
   gets its force; scale and tight tracking are. */
h1,
h2,
h3,
h4 {
	font-family: var(--font-display);
	font-weight: normal;
	line-height: 0.85em;
	letter-spacing: -0.06em;
	text-transform: uppercase;
}

strong,
b {
	font-weight: normal;
}

i,
em {
	font-style: normal;
}

/* Visible focus. Never remove an outline without replacing it — keyboard
   users lose all sense of position otherwise. */
:focus-visible {
	outline: 2px solid var(--color-text);
	outline-offset: 3px;
}

.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* ==========================================================================
   4. PAGE TRANSITION
   body starts at opacity 0; functions.php adds .loaded on DOMContentLoaded.
   Prevents the flash of unstyled/unpositioned content on first paint.
   ========================================================================== */
/* The fade-in is opt-in via .js — set by the inline script in wp_head. If JS
   is disabled or errors, .js is never added and the page renders visible.
   A cosmetic transition must never be able to hide the entire site. */
html.js body {
	opacity: 0;
}

html.js body.loaded {
	opacity: 1;
	transition: opacity var(--dur-fast) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
	body,
	body.loaded {
		opacity: 1;
		transition: none;
	}

	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		transition-duration: 0.01ms !important;
	}
}

/* ==========================================================================
   5. HEADER
   Fixed frame. Height is measured into --hh and every template pads by it.
   ========================================================================== */
.site-header {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	padding: var(--sm-base-space);
	background-color: var(--color-bg);
	border-bottom: 1px solid var(--color-rule);
	z-index: 999;
	transition: background-color var(--dur-fast) var(--ease);
}

@media screen and (min-width: 1025px) {
	.site-header {
		padding: var(--md-base-space);
	}
}

.header {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	width: 100%;
	gap: var(--sm-base-space);
}

.site-title {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	white-space: nowrap;
}

.site-menu ul {
	display: flex;
	align-items: baseline;
	gap: var(--sm-base-space);
}

@media screen and (min-width: 1025px) {
	.site-menu ul {
		gap: var(--md-base-space);
	}
}

.site-menu li {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
}

/* ==========================================================================
   5A. NAV DROPDOWN — reusable animated disclosure

   One accessible dropdown for any nav parent (li.menu-item-has-children).
   Works with no JS: on desktop the panel opens on :hover / :focus-within.
   nav.js layers a real <button.submenu-toggle> on top for touch + keyboard,
   toggling .is-open on the parent li.

   The animation is compositor-only (opacity + transform) so it never triggers
   layout; the panel is taken out of flow (absolute on desktop) so opening it
   causes zero layout shift. prefers-reduced-motion makes it instant.
   ========================================================================== */

/* The parent's link + toggle sit on one baseline. Positioning context is set
   per-breakpoint below: on mobile the panel anchors to .site-header (the fixed
   ancestor) so it drops full-width-safe under the whole nav; on desktop the
   parent becomes the anchor so the box sits directly under its label. */
.site-menu .menu-item-has-children {
	display: inline-flex;
	align-items: baseline;
}

/* The fused unit: real <a> + real <button> wrapped so they read as ONE box
   (Adrian Roselli's "link + disclosure widget" pattern — two controls, one
   visual). box-sizing: border-box + a border that only ever changes COLOUR
   (never width) means the colour inversion below causes zero layout shift.
   Only .menu-item-has-children gets this box — Work/About (plain <li>, no
   children) are untouched. */
.site-menu .nav-fused {
	display: inline-flex;
	align-items: center;
	box-sizing: border-box;
	border: 1px solid var(--color-text);
	color: var(--color-text);
	background-color: var(--color-bg);
	transition:
		color var(--dur-fast) var(--ease),
		background-color var(--dur-fast) var(--ease),
		border-color var(--dur-fast) var(--ease);
}

.site-menu .nav-fused > a {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0 0.6em;
	color: inherit;
}

/* The injected disclosure toggle: a hamburger built from three currentColor
   bars, sized in em so it scales with font-size and inherits the fused box's
   colour for free — one rule on .nav-fused (hover/focus-within/.is-open)
   recolours the link text AND the bars atomically. 44px min tap target. */
.site-menu .submenu-toggle {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 0.25em;
	min-width: 44px;
	min-height: 44px;
	padding: 0 0.6em 0 0.2em;
	border: 0;
	background: none;
	color: inherit;
	font: inherit;
	line-height: 1;
	cursor: pointer;
}

.site-menu .submenu-toggle-bar {
	display: block;
	width: 1em;
	height: 0.09em;
	background-color: currentColor;
}

.site-menu .submenu-toggle:focus-visible {
	outline: 2px solid currentColor;
	outline-offset: 2px;
}

/* Colour inversion: default black-on-white, hover/open white-on-black. Fires
   on hover of the whole fused box, :focus-within (keyboard tabbing into the
   link or button), and .is-open (touch/keyboard toggle) so every input mode
   gets the same inverted affordance. */
@media (hover: hover) and (pointer: fine) {
	.site-menu .nav-fused:hover {
		color: var(--color-bg);
		background-color: var(--color-text);
		border-color: var(--color-text);
	}
}

.site-menu .nav-fused:focus-within,
.site-menu .menu-item-has-children.is-open > .nav-fused {
	color: var(--color-bg);
	background-color: var(--color-text);
	border-color: var(--color-text);
}

/* The submenu panel. Hidden by default via opacity + transform (NOT display,
   so it can animate and stays measurable). visibility + pointer-events gate
   interaction so a closed panel is inert and out of the tab order visually. */
.site-menu .sub-menu {
	list-style: none;
	margin: 0;
	padding: 0;
	opacity: 0;
	transform: translateY(-4px);
	visibility: hidden;
	pointer-events: none;
	transition:
		opacity var(--dur-base) var(--ease),
		transform var(--dur-base) var(--ease),
		visibility 0s linear var(--dur-base);
}

.site-menu .sub-menu li {
	display: block;
}

.site-menu .sub-menu a {
	display: block;
}

/* OPEN STATE — driven by JS (.is-open) on every input mode. */
.site-menu .menu-item-has-children.is-open > .sub-menu {
	opacity: 1;
	transform: translateY(0);
	visibility: visible;
	pointer-events: auto;
	transition:
		opacity var(--dur-base) var(--ease),
		transform var(--dur-base) var(--ease),
		visibility 0s;
}

/* MOBILE / TABLET (default, < 1025px): a solid box anchored to the fixed
   .site-header, dropped below the whole nav row and RIGHT-ALIGNED to the header
   so it can never overflow the viewport (the parent, Bookings, sits at the
   right edge; a li-anchored panel there ran off-screen). Width-driven, NOT
   pointer-driven — an emulator that reports hover:fine at 375px still gets this
   branch. JS-only: .is-open (from the toggle) reveals it; no hover path, since
   a stuck-open panel on touch has no pointer-leave to close it.

   Closed it is opacity 0 / visibility hidden and pointer-inert, so it occupies
   no interactive space; being absolute it also causes zero layout shift. */
.site-menu .sub-menu {
	/* Reset the flex row that .site-menu ul sets on the top-level menu — the
	   submenu must stack its items vertically, not lay them out in a row.
	   (Sibling-selector leak: .site-menu ul {display:flex} matches this ul too.) */
	display: block;
	position: absolute;
	top: 100%;
	right: 0;
	left: auto;
	min-width: 60vw;
	max-width: calc(100vw - 2 * var(--sm-base-space));
	margin-top: var(--sm-base-space);
	background: var(--color-bg);
	border: 1px solid var(--color-rule);
	z-index: 100;
	transition:
		opacity var(--dur-base) var(--ease),
		transform var(--dur-base) var(--ease),
		visibility 0s linear var(--dur-base);
}

.site-menu .sub-menu li + li {
	border-top: 1px solid var(--color-rule);
}

.site-menu .sub-menu a {
	padding: 0.7rem var(--sm-base-space);
}

/* DESKTOP (>= 1025px, matches the theme breakpoint everywhere else): the parent
   becomes the positioning anchor so the box sits directly under its own label
   rather than under the whole header. hover + focus-within open it (so it works
   even if nav.js never runs); .is-open covers touch/keyboard via the toggle.

   Still RIGHT-ALIGNED: Bookings is the right-most nav item, so right:0 opens the
   box leftward into the page. If a left-edge parent ever gains a submenu, give
   it a .submenu-left modifier that sets left:0; right:auto. */
@media screen and (min-width: 1025px) {
	.site-menu .menu-item-has-children {
		position: relative;
	}

	.site-menu .sub-menu {
		min-width: max-content;
		max-width: none;
		/* Zero-gap: no margin between trigger and panel. This IS the hover
		   bridge — a gap here would break the continuous hover region and the
		   panel would close before the pointer reaches it. */
		margin-top: 0;
	}

	.site-menu .sub-menu a {
		padding: 0.55rem var(--sm-base-space);
		white-space: nowrap;
	}

	.site-menu .menu-item-has-children:hover > .sub-menu,
	.site-menu .menu-item-has-children:focus-within > .sub-menu,
	.site-menu .menu-item-has-children.is-open > .sub-menu {
		opacity: 1;
		transform: translateY(0);
		visibility: visible;
		pointer-events: auto;
		transition:
			opacity var(--dur-base) var(--ease),
			transform var(--dur-base) var(--ease),
			visibility 0s;
	}

	@media (hover: hover) and (pointer: fine) {
		.site-menu .sub-menu a:hover {
			opacity: 0.5;
		}
	}
}

/* Reduced motion: instant show/hide, no transition on any property. The
   300ms close-grace timer in nav.js is untouched — it's pointer-forgiveness,
   not motion. */
@media (prefers-reduced-motion: reduce) {
	.site-menu .sub-menu,
	.site-menu .menu-item-has-children.is-open > .sub-menu,
	.site-menu .nav-fused {
		transition: none;
	}
}

/* ==========================================================================
   6. LAYOUT
   ========================================================================== */
.site-content {
	padding-top: var(--hh);
}

.wrap {
	padding-left: var(--sm-base-space);
	padding-right: var(--sm-base-space);
}

@media screen and (min-width: 1025px) {
	.wrap {
		padding-left: var(--md-base-space);
		padding-right: var(--md-base-space);
	}
}

/* ==========================================================================
   7. HERO
   Display type set in Fraunces — high-contrast serif against the mono UI.
   clamp() keeps it enormous without overflowing narrow screens.
   ========================================================================== */
/* The hero row. Mobile/tablet: a plain block — the title is the only visible
   child (the canvas section is display:none below 1024px), so it reads exactly
   as the old stacked hero did. Desktop (>=1024px, matching the canvas JS gate):
   a two-column grid, title left, ASCII canvas right, vertically centred, with a
   single rule under the whole row. */
.hero-row {
	/* Mobile rhythm: 12vh/8vh left a dead band between the identity line
	   and the INDEX strip at 375px (2026-07-25). Desktop home overrides
	   this in 9. TEMPLATE VARIATIONS (3vh/2vh inside the flex lock). */
	padding: 6vh 0 4vh;
	border-bottom: 1px solid var(--color-rule);
}

@media screen and (min-width: 1024px) {
	.hero-row {
		display: grid;
		/* Title column collapses to the exact rendered width of its widest line
		   ("WOODS"); the canvas column (1fr) absorbs ALL remaining width. This
		   replaces the old fixed 42%, which wasted ~170px the type never needed
		   and starved the canvas. */
		grid-template-columns: max-content 1fr;
		column-gap: var(--md-base-space);
		/* Sit the canvas's bottom edge on the WOODS baseline so the artwork and
		   the type read as one deliberate block. */
		align-items: end;
	}
}

/* The row now owns the bottom rule and vertical padding; the title just carries
   the display type (see .hero-title h1 below). */

/* Mono is fixed-pitch: IBM Plex Mono advances 0.6em per glyph. The longest
   line is "WOODS" (5 chars), so 5 × 0.6em = 3em of width per 1em of size.
   Sizing at (100vw - gutters) / 3 makes the type span the measure exactly
   and never overflow — no magic vw number to re-tune per breakpoint. */
/* Sized in raw viewport units like the bundle (12.2vw / 10vw), not a fitted
   calc. word-wrap guards the overflow case the bundle also guards against. */
.hero-title h1 {
	font-size: 12.2vw;
	word-wrap: break-word;
	max-width: 100%;
}

@media screen and (min-width: 1025px) {
	.hero-title h1 {
		font-size: 10vw;
	}
}

.hero-tagline {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	margin-top: var(--md-base-space);
	max-width: 34ch;
}

/* The hero identity line. A factual comp-card descriptor under the h1, not a
   voice line: this is the h1+descriptor pair an answer engine quotes. Sits at
   full contrast, not the grey of .hero-tagline, because it is load-bearing
   copy rather than chrome. Explicit mono family per MEMORY lesson 9: a new
   bare element inherits --font-body otherwise, and this has bitten twice. */
.hero-role {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	text-transform: uppercase;
	color: var(--color-text);
	margin-top: var(--sm-base-space);
	margin-bottom: var(--sm-base-space);
	max-width: 40ch;
}

/* ==========================================================================
   7A. ASCII HERO (front page, desktop only)
   A single <canvas> paints an interactive ASCII portrait of Gabe Woods. The
   engine (assets/js/ascii-hero.js) is enqueued on the front page only and
   hard-gates to a fine-pointer, hover, >=1024px screen — it does NOTHING on
   mobile. This CSS is the other half of that gate: below 1024px the whole block
   is hidden, so mobile never renders an empty canvas and the server-rendered
   .hero-title stays the visible hero. Every colour here resolves to a theme
   token; the glyph ramp colours themselves are baked into the canvas atlas in
   the JS (the approved v19 values) because a canvas paints its own pixels.
   ========================================================================== */
.gw-hero-ascii {
	display: none; /* hidden by default; revealed only at the desktop breakpoint */
	width: 100%;
	/* Reserve the canvas's true 130:51 proportion so the grid allots height
	   before/without JS. The engine sizes the <canvas> from stage.clientWidth
	   and sets its own inline height at the same 130:51 ratio, so this only
	   governs pre-hydration layout — the two never disagree. */
	aspect-ratio: 130 / 51;
	background-color: var(--color-bg);
	/* No border/padding here: inside .hero-row the row owns the bottom rule and
	   the vertical rhythm. A border here would draw a second rule mid-row. */
}

.gw-hero-stage {
	position: relative;
	display: flex;
	justify-content: center;
	width: 100%;
	cursor: none; /* the decaying trail IS the cursor (matches the v19 prototype) */
	-webkit-user-select: none;
	-moz-user-select: none;
	user-select: none;
	touch-action: none;
}

#gw-hero-canvas {
	display: block;
	max-width: 100%;
	height: auto;
	/* Cap the band height so a tall DPR/aspect never pushes past the fold. The JS
	   sets an explicit inline width/height; this max-height wins as a ceiling,
	   keeping the hero at a hero's scale. */
	max-height: 78vh;
}

/* Debug HUD — only in the DOM when ?asciidebug=1. Green-on-black, mono 12px,
   matching the prototype readout. Debug chrome, deliberately not themed. */
.gw-hero-perf {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	background: #0a0a0a;
	color: #0f0;
	padding: 8px 12px;
	margin-bottom: 12px;
	display: inline-block;
	white-space: nowrap;
}

/* Desktop gate (CSS half): reveal the canvas hero only on a wide screen. The JS
   gate additionally requires hover + fine pointer, so a 1024px touch device sees
   the block but the canvas stays empty — rare and acceptable; the common mobile
   case is hidden here entirely. */
@media screen and (min-width: 1024px) {
	.gw-hero-ascii {
		display: block;
	}
}

@media (prefers-reduced-motion: reduce) {
	.gw-hero-stage {
		cursor: auto; /* no interaction, so don't hide the real cursor */
	}
}

/* ==========================================================================
   8. INDEX ROWS
   Three columns on desktop, two on mobile (category drops — it is the least
   load-bearing of the three).
   ========================================================================== */
.project-index {
	padding-top: 4vh;
	padding-bottom: 8vh;
}

.index-head {
	color: var(--color-gray-500);
}

.index-row {
	display: grid;
	grid-template-columns: 1fr 4.5em;
	gap: var(--sm-base-space);
	align-items: baseline;
	padding: 0.85em 0;
	border-bottom: 1px solid var(--color-rule);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
}

@media screen and (min-width: 800px) {
	.index-row {
		grid-template-columns: 1fr 1fr 4.5em;
		gap: var(--md-base-space);
	}
}

.index-row .col-type {
	display: none;
	color: var(--color-gray-500);
}

@media screen and (min-width: 800px) {
	.index-row .col-type {
		display: block;
	}
}

.index-row .col-year {
	text-align: right;
	font-variant-numeric: tabular-nums;
	color: var(--color-gray-500);
}

.index-row .col-title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Row hover: the hovered row stays black, its siblings grey out. Draws the
   eye down the list rather than lighting up one item in isolation. */
@media (hover: hover) and (pointer: fine) {
	.project-index:hover .index-row.is-item {
		color: var(--color-gray-500);
	}

	.project-index .index-row.is-item:hover {
		color: var(--color-text);
	}

	.index-row.is-item a {
		display: block;
	}
}

.index-empty {
	color: var(--color-gray-500);
}

/* Pre-launch index state. Deliberately understated — it sits in the same
   row grammar as a real entry rather than announcing itself. */
.index-soon .col-title {
	color: var(--color-gray-500);
}

/* ==========================================================================
   8A. PINNED INDEX GALLERY (front page)
   The homepage index is a full-bleed horizontal band of catalogue-post
   slides. Geometry is v05 (approved 2026-07-25): screen 1 is the locked
   composition at calc(100vh - var(--hh) - var(--fh)) with the footer fully
   visible on load; act 2 is a sticky stage whose track translates
   horizontally as the page scrolls vertically.

   Every value here resolves to an existing token. No new colour, no new
   spacing, no invented tracking. The lerp itself (assets/js/pin-index.js)
   is a Gabe-requested deviation from the republique reference, flagged and
   approved under working-agreement rule 3.

   Scoped body.tpl-home throughout: this must never leak onto a Page.
   ========================================================================== */

/* The head strip sits at the bottom of screen 1, directly under the hero,
   reusing .index-head's grey. It is the only part of the gallery visible on
   load. */
body.tpl-home .pin-index__head {
	display: flex;
	justify-content: space-between;
	gap: var(--md-base-space);
	padding-top: 0.85em;
	padding-bottom: 0.85em;
	border-top: 1px solid var(--color-rule);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
}

/* Mobile-first: the gallery is a plain vertical column in normal flow. The
   desktop pin is added at >=1025px only, matching the theme's desktop
   breakpoint convention (MEMORY lesson 17: gate layout on viewport width,
   never on pointer type). */
body.tpl-home .pin-index__stage {
	overflow: hidden;
}

body.tpl-home .pin-index__track {
	display: flex;
	flex-direction: column;
	gap: var(--md-base-space);
	padding: var(--md-base-space) var(--sm-base-space);
}

body.tpl-home .pin-index__slide {
	display: block;
	flex: 0 0 auto;
	text-decoration: none;
	color: var(--color-text);
}

body.tpl-home .pin-index__slide img {
	display: block;
	width: 100%;
	height: auto;
	object-fit: cover;
}

/* Legend: the .index-row hairline treatment, reused. */
body.tpl-home .pin-index__legend {
	display: flex;
	justify-content: space-between;
	gap: var(--sm-base-space);
	align-items: baseline;
	padding-top: 0.85em;
	padding-bottom: 0.85em;
	border-bottom: 1px solid var(--color-rule);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
}

/* Single-line legend, ellipsis on the title span only. FLAGGED as a taste
   call for Gabe (plan open question 1): removing these three declarations
   gives a wrapping two-line legend instead. */
body.tpl-home .pin-index__title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

body.tpl-home .pin-index__meta {
	flex: 0 0 auto;
	color: var(--color-gray-500);
	font-variant-numeric: tabular-nums;
}

/* The counter and hint are desktop pin chrome. They describe a mechanic that
   does not exist on mobile, so they are hidden there. */
body.tpl-home .pin-index__count,
body.tpl-home .pin-index__hint {
	display: none;
}

@media screen and (min-width: 1025px) {

	/* ---- Screen 1: the locked composition, measured not hardcoded ----
	   min-height, not height: if the hero ever needs more room it grows
	   rather than clipping. --hh already sits INSIDE this border-box as
	   padding-top (see 9. TEMPLATE VARIATIONS), so only --fh is
	   subtracted: main's bottom edge must land exactly on the fixed
	   footer's top edge, or the journey (next in flow) shows through the
	   gap on load. */
	body.tpl-home .site-content {
		display: flex;
		flex-direction: column;
		min-height: calc(100vh - var(--fh));
	}

	/* The hero absorbs the leftover height of screen 1. min-height:0 lets the
	   canvas shrink below its intrinsic size. */
	body.tpl-home .hero-row {
		flex: 1 1 auto;
		min-height: 0;
		padding-top: 3vh;
		padding-bottom: 2vh;
	}

	/* The head strip is the last thing on screen 1 and does not flex. */
	body.tpl-home .pin-index__head {
		flex: 0 0 auto;
	}

	/* ---- Act 2: the pin ----
	   .pin-index gets its height from JS (100vh plus the track's horizontal
	   overflow). The stage sticks for exactly that distance, then releases
	   into the appendix. */
	body.tpl-home .pin-index__stage {
		position: sticky;
		top: 0;
		height: 100vh;
		display: flex;
		flex-direction: column;
		justify-content: center;
		background-color: var(--color-bg);
	}

	body.tpl-home .pin-index__track {
		flex-direction: row;
		align-items: center;
		gap: var(--md-base-space);
		padding: 0 0 0 var(--md-base-space);
		will-change: transform;
		/* The track IS the band: capped at one viewport so no stretched
		   segment can inflate the cross-axis and push its siblings' tops
		   above the stage (the clipped About/calendar text, 2026-07-25). */
		height: 100vh;
	}

	body.tpl-home .pin-index__slide img {
		height: 66vh;
		width: auto;
		max-width: 86vw;
	}

	/* Bundle counter grammar (pswp counter): naked full-contrast text on
	   a guaranteed ground - no chip, no gray. The ground is guaranteed
	   here because slides are 56/66vh centred in the band, so the
	   bottom-left corner is always page background. */
	body.tpl-home .pin-index__count {
		display: block;
		position: absolute;
		left: var(--md-base-space);
		bottom: var(--md-base-space);
		font-family: var(--font-mono);
		font-size: var(--fs-main);
		letter-spacing: var(--tracking-ui);
		font-variant-numeric: tabular-nums;
		color: var(--color-text);
	}

	body.tpl-home .pin-index__count:empty {
		display: none;
	}

	body.tpl-home .pin-index__hint {
		display: inline;
	}
}

/* Reduced motion: no pin, no lerp, no sticky. A static vertical list, which
   is exactly the mobile layout. The JS gate returns early under the same
   query, so nothing ever sets a transform here. */
@media (prefers-reduced-motion: reduce) {
	body.tpl-home .pin-index__stage {
		position: static;
		height: auto;
	}

	body.tpl-home .pin-index__track {
		flex-direction: column;
		transform: none !important;
	}

	body.tpl-home .pin-index__slide img {
		height: auto;
		width: 100%;
		max-width: none;
	}

	body.tpl-home .pin-index__count {
		display: none;
	}
}

/* ==========================================================================
   8B. HOME APPENDIX (front page, below the footer)
   The credibility layer: factual anchor, press rows, services routes and the
   Q&A. It renders after the footer in DOM order, which is what keeps the
   first screen untouched. Components reuse existing patterns: the spec rows
   are .index-row's hairline grammar, the routes are a hover-invert, and the
   <details> reuse the legal page's hairline box.
   ========================================================================== */
.home-appendix {
	border-top: 1px solid var(--color-rule);
	background-color: var(--color-bg);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
}

.home-appendix__inner {
	max-width: 1200px;
	padding-top: 8vh;
	padding-bottom: 8vh;
}

.home-appendix__section-title {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	font-weight: 400;
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	margin-bottom: var(--sm-base-space);
}

/* Mono needs a shorter measure than the proportional face (MEMORY lesson 10:
   .entry-content uses 68ch, About prose uses 58ch). These are the only
   long-form paragraphs on the page, so they take the About measure. */
.home-appendix__anchor,
.home-appendix__lede {
	font-family: var(--font-mono);
	max-width: 58ch;
	line-height: 1.5em;
	margin-bottom: var(--md-base-space);
}

.home-appendix__anchor {
	margin-bottom: 8vh;
}

/* Press rows: the .index-row hairline treatment, reused. */
.home-appendix__spec {
	border-top: 1px solid var(--color-rule);
	max-width: 760px;
	margin-bottom: 8vh;
}

.home-appendix__row {
	display: grid;
	grid-template-columns: 1fr;
	gap: 0.35em;
	padding-top: 0.85em;
	padding-bottom: 0.85em;
	border-bottom: 1px solid var(--color-rule);
	text-transform: uppercase;
}

@media screen and (min-width: 800px) {
	.home-appendix__row {
		grid-template-columns: 14em 1fr;
		gap: var(--md-base-space);
		align-items: baseline;
	}
}

.home-appendix__row dt {
	color: var(--color-gray-500);
}

.home-appendix__row dd {
	margin: 0;
}

/* Routes: hover-invert, matching the theme's existing button treatment. The
   hairline grid is the background showing through 1px gaps. */
.home-appendix__routes {
	display: grid;
	grid-template-columns: 1fr;
	gap: 1px;
	background-color: var(--color-rule);
	border: 1px solid var(--color-rule);
	max-width: 1000px;
	margin-bottom: 8vh;
}

@media screen and (min-width: 1025px) {
	.home-appendix__routes {
		grid-template-columns: repeat(4, 1fr);
	}
}

.home-appendix__routes a {
	display: flex;
	justify-content: space-between;
	gap: var(--sm-base-space);
	align-items: center;
	min-height: 44px;
	padding: var(--sm-base-space);
	background-color: var(--color-bg);
	color: var(--color-text);
	text-decoration: none;
	text-transform: uppercase;
	transition: background-color var(--dur-base) var(--ease),
		color var(--dur-base) var(--ease);
}

.home-appendix__routes a:hover,
.home-appendix__routes a:focus-visible {
	background-color: var(--color-text);
	color: var(--color-bg);
}

/* Q&A: hairline boxes with collapsing borders. */
.home-appendix__qa {
	max-width: 760px;
	margin-bottom: var(--md-base-space);
}

.home-appendix__qa details {
	border: 1px solid var(--color-rule);
	margin-bottom: -1px;
}

.home-appendix__qa summary {
	display: flex;
	justify-content: space-between;
	gap: var(--sm-base-space);
	align-items: center;
	min-height: 44px;
	padding: 0.85em var(--sm-base-space);
	cursor: pointer;
	list-style: none;
	text-transform: uppercase;
}

.home-appendix__qa summary::-webkit-details-marker {
	display: none;
}

.home-appendix__qa summary::after {
	content: "+";
	color: var(--color-gray-500);
}

.home-appendix__qa details[open] summary::after {
	content: "\2212";
}

/* The h3 exists so the scorer sees a question-shaped heading (a bare
   <summary> is invisible to it). It must not look like a heading. */
.home-appendix__qa summary h3 {
	display: inline;
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	font-weight: 400;
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
}

.home-appendix__qa details p {
	font-family: var(--font-mono);
	max-width: 58ch;
	line-height: 1.5em;
	padding-left: var(--sm-base-space);
	padding-right: var(--sm-base-space);
	padding-bottom: var(--sm-base-space);
}

.home-appendix__updated {
	font-family: var(--font-mono);
	color: var(--color-gray-500);
	text-transform: uppercase;
	margin-top: var(--md-base-space);
}

/* ==========================================================================
   9. TEMPLATE VARIATIONS
   Everything page-specific hangs off the body class set in functions.php.
   ========================================================================== */
body.tpl-home .site-content {
	padding-top: var(--hh);
}

/* The one-viewport lock that used to live here is RETIRED (2026-07-25).
   It set body.tpl-home to height:100dvh plus overflow:hidden, which makes
   vertical scrolling impossible, and the v05 pinned index gallery is driven
   by vertical scroll. Amendment A1 retires the literal no-scroll lock; the
   surviving ruling is "first-screen composition intact on load, footer
   visible on load".

   That ruling is now enforced in section 8A by measured heights:
   body.tpl-home .site-content { min-height: calc(100vh - var(--fh)) }
   (--hh is NOT subtracted: it already sits inside the border-box as this
   section's padding-top - subtracting it twice left an --hh-tall gap the
   journey bled through, fixed 2026-07-25). The hero flex-fills the
   remainder; --hh and --fh are measured in gabe_woods_runtime_vars().
   Do not reintroduce overflow:hidden on body. */

/* The ASCII canvas keeps LIVE-site sizing on the front page. The generic
   .gw-hero-ascii rule reserves a 130:51 box with a 78vh cap; on the home
   page the hero is a flex cell, so the canvas fits the cell's height
   instead. ascii-hero.js clamps its width-derived height to
   stage.clientHeight, so CSS only needs to give it a bounded box. */
@media screen and (min-width: 1025px) {
	body.tpl-home .gw-hero-ascii {
		height: 100%;
		min-height: 0;
		aspect-ratio: auto;
	}

	body.tpl-home .gw-hero-stage {
		height: 100%;
		min-height: 0;
		align-items: center;
	}

	body.tpl-home #gw-hero-canvas {
		max-height: 100%;
	}

	/* The flex column in section 8A already seats the footer at the bottom of
	   screen 1; the site-wide 6vh top margin would push it below the fold. */
	body.tpl-home .site-footer {
		margin-top: 0;
	}
}

body.tpl-project .entry-content,
body.tpl-page .entry-content {
	max-width: 68ch;
	padding-top: 6vh;
	padding-bottom: 8vh;
	font-size: 1.05rem;
}

body.tpl-project .entry-content p,
body.tpl-page .entry-content p {
	margin-bottom: 1.2em;
}

/* Bundle's secondary display step: --custom-font-size-sm/md (2.3em / 3.32em)
   with --custom-letter-spacing -0.04em and --custom-line-height 1em. */
body.tpl-project .entry-title,
body.tpl-page .entry-title,
body.tpl-about .entry-title {
	font-size: 2.3em;
	line-height: 1em;
	letter-spacing: -0.04em;
	padding-top: 8vh;
	word-wrap: break-word;
	max-width: 100%;
}

@media screen and (min-width: 1025px) {
	body.tpl-project .entry-title,
	body.tpl-page .entry-title,
	body.tpl-about .entry-title {
		font-size: 3.32em;
	}
}

body.tpl-project .entry-meta {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	padding-top: var(--md-base-space);
	padding-bottom: 2rem;
	border-bottom: 1px solid var(--color-rule);
}

/* ==========================================================================
   10. FOOTER
   ========================================================================== */
.site-footer {
	border-top: 1px solid var(--color-rule);
	padding-top: var(--sm-base-space);
	padding-bottom: var(--sm-base-space);
	margin-top: 6vh;
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
}

@media screen and (min-width: 1025px) {
	.site-footer {
		padding-top: var(--md-base-space);
		padding-bottom: var(--md-base-space);
	}
}

.footer-container {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

@media screen and (min-width: 800px) {
	.footer-container {
		flex-direction: row;
		justify-content: space-between;
		align-items: center;
		gap: var(--md-base-space);
	}
}

.footer-right a {
	transition: opacity var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.footer-right a:hover {
		opacity: 0.5;
	}
}

.footer-right a + a{margin-left:var(--md-base-space);}
.footer-right .fx-cursor::after{content:"\2588";margin-left:.18em;animation:gw-blink 1.1s steps(1,end) infinite;}
@keyframes gw-blink{0%,50%{opacity:1}50.01%,100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.fx-cursor::after{animation:none;opacity:1;}}

/* ==========================================================================
   11. ABOUT

   Scoped to body.tpl-about so these components never leak into other Pages.
   The republique bundle has no prose, table, quote or scroll-reveal
   treatment at all - it is a gallery site whose only motion vocabulary is
   "fade opacity 0.2-0.5s on one bezier, triggered by asset load". So this
   section is authored from the theme's own vocabulary - rule lines,
   uppercase mono labels in grey, --fs-main, two spacing tokens, one ease
   curve, no bold - and any motion here is a deliberate extension of the
   brief, not a port from it.
   ========================================================================== */

body.tpl-about .about {
	padding-top: 6vh;
	padding-bottom: 10vh;
}

body.tpl-about .entry-title {
	margin-bottom: var(--md-base-space);
}

/* --- Typewriter h1 -------------------------------------------------------
   The text is server-rendered and stays at opacity:1; only a width clip
   animates. Chrome excludes opacity:0 elements from LCP candidacy entirely,
   so an opacity fade here would cost the metric - a clip does not, because
   the element is painted, sized and coloured from first paint.

   IBM Plex Mono means 1ch === one glyph, so steps() maps exactly to the
   character count with no JS measurement. "Gabe Woods" is 10 characters.
   The final width is declared on the element itself, so the clip opening
   cannot cause layout shift. */
body.tpl-about .type-reveal__text {
	display: inline-block;
	overflow: hidden;
	white-space: nowrap;
	vertical-align: top;
	width: 10.1ch;
	border-right: 0.06em solid currentColor;
	/* jump-start, not the default `end`: with `end` the first interval holds
	   at width 0, so the h1 renders collapsed for the first 120ms. jump-start
	   advances immediately, so character one is present on the first frame. */
	animation:
		gw-type 1.2s steps(10, jump-start) forwards,
		gw-caret 1.2s steps(10, jump-start) forwards;
}

@keyframes gw-type {
	from { width: 0ch; }
	to   { width: 10.1ch; }
}

@keyframes gw-caret {
	to { border-right-color: transparent; }
}

/* --- Scroll reveal -------------------------------------------------------
   Opacity plus a small rise, on the house curve. The hiding rule is gated
   behind html.js AND .reveal, and .in-view is added by IntersectionObserver
   in functions.php. Without JS nothing is ever hidden, so a script failure
   cannot blank the content. Same failsafe principle as the page fade. */
html.js body.tpl-about .reveal {
	opacity: 0;
	transform: translateY(20px);
	transition:
		opacity 0.4s var(--ease),
		transform 0.4s var(--ease);
}

html.js body.tpl-about .reveal.in-view {
	opacity: 1;
	transform: none;
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-about .type-reveal__text {
		animation: none !important;
		width: auto !important;
		border-right: none !important;
	}

	html.js body.tpl-about .reveal,
	html.js body.tpl-about .reveal.in-view {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}
}

/* --- Two-column band -----------------------------------------------------
   Text left, portrait right and sticky: the image pins while the prose
   scrolls past, then releases at the band's end. Pure position:sticky, no
   scroll listener, native momentum preserved. Single column below 1025px,
   where sticky is switched off - a pinned image on a phone eats the
   viewport. */
@media screen and (min-width: 1025px) {
	body.tpl-about .about-band {
		display: grid;
		grid-template-columns: 1fr 38%;
		grid-column-gap: calc(var(--md-base-space) * 3);
		align-items: start;
	}

	body.tpl-about .about-band__media {
		position: sticky;
		top: calc(var(--hh) + var(--md-base-space));
	}
}

/* Section headings. Uppercase mono label with a rule under - the boundary
   marker between the factual block and the mythology. */
body.tpl-about .about h2 {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-bottom: var(--sm-base-space);
	margin-top: 10vh;
	margin-bottom: var(--md-base-space);
	border-bottom: 1px solid var(--color-rule);
}

body.tpl-about .about-band__text .about-myth h2 {
	margin-top: 8vh;
}

/* --- Prose ---------------------------------------------------------------
   Mono glyphs are wider than the proportional face .entry-content was
   measured for, so 68ch reads long. 58ch is right in a single column; in the
   two-column band the column itself is already the constraint, so the cap
   relaxes to 64ch rather than leaving the text column half empty. */
body.tpl-about .about-anchor,
body.tpl-about .about-myth {
	max-width: 58ch;
}

@media screen and (min-width: 1025px) {
	body.tpl-about .about-band__text .about-anchor,
	body.tpl-about .about-band__text .about-myth {
		max-width: 64ch;
	}
}

body.tpl-about .about-anchor p,
body.tpl-about .about-myth p,
body.tpl-about .route-note {
	/* Mono is the working face site-wide. Without this the prose inherits
	   --font-body (Hanken), which is loaded but deliberately unused. */
	font-family: var(--font-mono);
	font-size: 1.05rem;
	line-height: 1.5em;
	margin-bottom: 1.2em;
}

body.tpl-about .about-anchor {
	padding-bottom: var(--md-base-space);
}

/* Inline prose links must read as links. The global `a` has no
   text-decoration, which is right for nav and wrong inside a paragraph. */
body.tpl-about .about-anchor a,
body.tpl-about .about-myth a {
	text-decoration: underline;
	text-underline-offset: 0.2em;
	text-decoration-thickness: 1px;
}

/* --- Images --------------------------------------------------------------
   Photography is the only colour on this site. */
body.tpl-about .about-portrait,
body.tpl-about .about-wide {
	margin: 0;
}

body.tpl-about .about-portrait {
	margin-top: 8vh;
}

@media screen and (min-width: 1025px) {
	body.tpl-about .about-portrait {
		margin-top: 0;
	}
}

body.tpl-about .about-wide {
	margin-top: 12vh;
}

body.tpl-about .about-portrait img,
body.tpl-about .about-wide img {
	display: block;
	width: 100%;
	height: auto;
}

body.tpl-about .about-portrait figcaption,
body.tpl-about .about-wide figcaption {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-top: var(--sm-base-space);
}

/* Ownership notice sits with the credit but reads as secondary to it. */
body.tpl-about .copyright-notice {
	white-space: nowrap;
}

/* --- Specs ---------------------------------------------------------------
   Label/value rows with a rule - the .index-row pattern, reused. */
body.tpl-about .spec-list {
	max-width: 68ch;
	border-top: 1px solid var(--color-rule);
}

body.tpl-about .spec-row {
	display: grid;
	grid-template-columns: 8em 1fr;
	grid-column-gap: var(--sm-base-space);
	padding-top: 0.75rem;
	padding-bottom: 0.75rem;
	border-bottom: 1px solid var(--color-rule);
}

@media screen and (min-width: 1025px) {
	body.tpl-about .spec-row {
		grid-template-columns: 14em 1fr;
		grid-column-gap: var(--md-base-space);
	}
}

body.tpl-about .spec-row dt {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
}

body.tpl-about .spec-row dd {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	margin: 0;
}

/* --- Jump to booking -----------------------------------------------------
   Rides with the sticky portrait on desktop; becomes a fixed bottom-right
   button below 1025px where the media column collapses. Points at #booking
   on this page - the four route destinations do not exist yet.

   Smooth scrolling is set on html, not via JS, so it costs nothing and is
   automatically disabled under prefers-reduced-motion below. scroll-margin
   on the target stops the fixed header covering the heading on arrival. */
html {
	scroll-behavior: smooth;
}

body.tpl-about #booking {
	scroll-margin-top: calc(var(--hh) + var(--md-base-space));
}

/* Inverted by default: solid black, white text. This is the only inverted
   element on the site, which is the point - it has to read as a button
   rather than as another rule-lined row. Hover flips it back to white so
   the interaction is unmistakable either side. */
body.tpl-about .jump-book {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sm-base-space);
	margin-bottom: var(--md-base-space);
	padding: 0.85rem var(--sm-base-space);
	border: 1px solid var(--color-text);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
	/* 44px minimum tap target. */
	min-height: 44px;
	color: var(--color-bg);
	background-color: var(--color-text);
	transition:
		background-color var(--dur-fast) var(--ease),
		color var(--dur-fast) var(--ease);
}

/* The label types itself ONCE, then holds. Same clip technique as the h1:
   the text is in the DOM at full opacity and only a width mask animates, so
   it is always readable to a crawler or screen reader.

   Deliberately not a loop - permanent motion in the corner of the eye reads
   as broken rather than lively, and it would fight the page's deadpan. The
   arrow pulse below carries the ongoing signal instead.

   "Jump to bookings" is 16 characters; mono means 1ch === 1 glyph. */
body.tpl-about .jump-book__label {
	display: inline-block;
	overflow: hidden;
	white-space: nowrap;
	vertical-align: bottom;
	width: 16ch;
}

/* Type when the button ENTERS VIEW, not on page load - it sits well down the
   page and would otherwise finish before anyone saw it. .in-view is added by
   the same IntersectionObserver the reveal blocks use (functions.php). If
   that never fires, the 2.5s failsafe adds it anyway and the label is fully
   visible regardless, since the width above is its resting state. */
html.js body.tpl-about .jump-book.in-view .jump-book__label {
	animation: gw-type-btn 0.9s steps(16, jump-start) both;
}

@keyframes gw-type-btn {
	from { width: 0ch; }
	to   { width: 16ch; }
}

/* The arrow nudges down on a slow loop - enough to catch the eye on a long
   scroll without becoming a distraction. Starts only once the label has
   finished typing, so the two do not compete. Stops on hover, where the
   cursor has already done the job. */
body.tpl-about .jump-book__arrow {
	display: inline-block;
}

body.tpl-about .jump-book.in-view .jump-book__arrow {
	animation: gw-nudge 2s var(--ease) 0.9s infinite;
}

/* No JS: no .in-view is ever added, so the label is simply visible at its
   resting width and the arrow is still. Nothing is hidden. */

@keyframes gw-nudge {
	0%, 60%, 100% { transform: translateY(0); }
	30%           { transform: translateY(0.3em); }
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-about .jump-book:hover {
		color: var(--color-text);
		background-color: var(--color-bg);
	}

	body.tpl-about .jump-book:hover .jump-book__arrow {
		animation: none;
		transform: translateY(0.25em);
		transition: transform var(--dur-fast) var(--ease);
	}
}

/* Below the two-column breakpoint the media column collapses and the button
   would scroll away with it. Pin it instead - mobile is most of the traffic
   and this is the only route to the booking block from mid-page. */
@media screen and (max-width: 1024px) {
	body.tpl-about .jump-book {
		position: fixed;
		right: var(--sm-base-space);
		bottom: var(--sm-base-space);
		z-index: 998;
		margin-bottom: 0;
		width: auto;
		gap: 0.5rem;
		/* White halo so the black button stays legible over photography. */
		box-shadow: 0 0 0 2px var(--color-bg);
		transition:
			opacity var(--dur-fast) var(--ease),
			background-color var(--dur-fast) var(--ease),
			color var(--dur-fast) var(--ease);
	}

	/* Once the booking block is on screen the button has done its job and
	   would otherwise sit on top of the thing it scrolled you to. */
	body.tpl-about.at-booking .jump-book {
		opacity: 0;
		pointer-events: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}

	body.tpl-about .jump-book,
	body.tpl-about .jump-book__arrow,
	body.tpl-about .jump-book__label,
	html.js body.tpl-about .jump-book.in-view .jump-book__label {
		transition: none !important;
		animation: none !important;
		width: auto !important;
	}

	body.tpl-about .jump-book:hover .jump-book__arrow {
		transform: none !important;
	}
}

/* --- Selected performances -----------------------------------------------
   Event and venue, no years (Gabe's call: these are recurring parties, and
   a year would date the list). Same rule-line vocabulary as the specs table
   so the two read as one system. Venue stacks under the event on mobile and
   moves to its own column on desktop. */
body.tpl-about .show-list {
	max-width: 68ch;
	border-top: 1px solid var(--color-rule);
	list-style: none;
	margin: 0;
	padding: 0;
}

body.tpl-about .show-row {
	display: grid;
	grid-template-columns: 1fr;
	padding-top: 0.75rem;
	padding-bottom: 0.75rem;
	border-bottom: 1px solid var(--color-rule);
}

@media screen and (min-width: 1025px) {
	body.tpl-about .show-row {
		grid-template-columns: 1fr 1fr;
		grid-column-gap: var(--md-base-space);
		align-items: baseline;
	}
}

body.tpl-about .show-name,
body.tpl-about .show-venue {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	text-transform: uppercase;
}

/* Venue is supporting detail: grey, like every other label on the page. */
body.tpl-about .show-venue {
	color: var(--color-gray-500);
}

/* --- Quote wall ----------------------------------------------------------
   No pattern existed to port; built to match the spec rows so the two read
   as one system. Nothing here is Review schema - see the template header. */
body.tpl-about .quote-wall {
	max-width: 68ch;
	border-top: 1px solid var(--color-rule);
}

body.tpl-about .quote {
	padding-top: var(--md-base-space);
	padding-bottom: var(--md-base-space);
	border-bottom: 1px solid var(--color-rule);
	margin: 0;
}

/* Stars: SOLID by default. An outline-only star at 1rem reads as empty
   rather than as a rating - the hairline stroke simply disappears. They are
   filled and larger, and hover pops each one up in a staggered sweep so the
   interaction is unmistakable. Every review gets five, including the
   lorikeet. It is never remarked on. */
body.tpl-about .quote-stars {
	display: block;
	margin-bottom: var(--sm-base-space);
	font-size: 1.4rem;
	line-height: 1;
	letter-spacing: 0.12em;
	color: var(--color-text);
}

body.tpl-about .quote-stars .star {
	display: inline-block;
	transform-origin: 50% 55%;
	transition:
		transform var(--dur-fast) var(--ease),
		opacity var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	/* Dim the row, then pop each star back up in sequence: the sweep reads
	   as deliberate rather than as a single on/off switch. */
	body.tpl-about .quote:hover .quote-stars .star {
		transform: scale(1.35);
	}

	body.tpl-about .quote:hover .star:nth-child(1) { transition-delay: 0ms; }
	body.tpl-about .quote:hover .star:nth-child(2) { transition-delay: 60ms; }
	body.tpl-about .quote:hover .star:nth-child(3) { transition-delay: 120ms; }
	body.tpl-about .quote:hover .star:nth-child(4) { transition-delay: 180ms; }
	body.tpl-about .quote:hover .star:nth-child(5) { transition-delay: 240ms; }
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-about .quote-stars .star {
		transition: none !important;
		transition-delay: 0ms !important;
		transform: none !important;
	}
}

/* Year sits opposite the attribution, in the grey label register. */
body.tpl-about .quote-meta {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--sm-base-space);
}

body.tpl-about .quote-year {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	white-space: nowrap;
}

body.tpl-about .quote p {
	font-family: var(--font-mono);
	font-size: 2.3em;
	line-height: 1em;
	letter-spacing: -0.04em;
	text-transform: uppercase;
	margin: 0 0 var(--sm-base-space);
	overflow-wrap: anywhere;
}

@media screen and (min-width: 1025px) {
	body.tpl-about .quote p {
		font-size: 3.32em;
	}
}

body.tpl-about .quote cite {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	font-style: normal;
	color: var(--color-gray-500);
	text-transform: uppercase;
}

/* --- Routes --------------------------------------------------------------
   Segmentation block. Deliberately plain - no email, no phone; every path
   ends in a capture form. */
body.tpl-about .about-routes {
	max-width: 68ch;
}

body.tpl-about .route-list {
	border-top: 1px solid var(--color-rule);
}

body.tpl-about .route {
	display: grid;
	grid-template-columns: 1fr auto;
	grid-column-gap: var(--sm-base-space);
	align-items: baseline;
	padding-top: var(--md-base-space);
	padding-bottom: var(--md-base-space);
	border-bottom: 1px solid var(--color-rule);
	text-decoration: none;
	min-height: 44px;
}

body.tpl-about .route-label {
	grid-column: 1;
	grid-row: 1;
	font-family: var(--font-mono);
	font-size: 2.3em;
	line-height: 1em;
	letter-spacing: -0.04em;
	text-transform: uppercase;
	transition: color var(--dur-fast) var(--ease);
}

@media screen and (min-width: 1025px) {
	body.tpl-about .route-label {
		font-size: 3.32em;
	}
}

body.tpl-about .route-desc {
	grid-column: 1;
	grid-row: 2;
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	padding-top: var(--sm-base-space);
}

body.tpl-about .route-arrow {
	grid-column: 2;
	grid-row: 1;
	font-family: var(--font-mono);
	font-size: 1.4em;
	line-height: 1;
	transition: transform var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-about .route:hover .route-arrow {
		transform: translateX(0.4em);
	}

	body.tpl-about .route:hover .route-label {
		color: var(--color-hover);
	}
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-about .route-arrow {
		transition: none !important;
	}

	body.tpl-about .route:hover .route-arrow {
		transform: none !important;
	}
}

body.tpl-about .route-note {
	color: var(--color-gray-500);
	padding-top: var(--md-base-space);
	max-width: 58ch;
}

/* Currency line, last thing on the page. Same register as .legal-updated:
   mono, grey, a timestamp rather than prose. */
body.tpl-about .about-updated {
	font-family: var(--font-mono);
	font-weight: 300;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	padding: var(--md-base-space) 0 0;
}

/* ==========================================================================
   12. CAPTURE PAGES

   Scoped to body.tpl-capture (bookings, collaborate, guest, media). Reuses
   the About vocabulary: rule lines, uppercase mono grey labels, --fs-main,
   the two spacing tokens, one ease curve. .spec-row / .spec-list are the same
   pattern the About specs use; the rules below re-declare them under the
   capture scope (the About rules are body.tpl-about only) and add what is new
   here: intro prose, the rate row, the comp-card image grid, and the form.
   ========================================================================== */

body.tpl-capture .capture {
	padding-top: 6vh;
	padding-bottom: 10vh;
}

body.tpl-capture .entry-title {
	margin-bottom: var(--md-base-space);
}

/* Section headings: uppercase mono grey label with a rule under, matching
   About. New components do NOT inherit the mono family; set it explicitly. */
body.tpl-capture .capture h2 {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-bottom: var(--sm-base-space);
	margin-top: 10vh;
	margin-bottom: var(--md-base-space);
	border-bottom: 1px solid var(--color-rule);
}

/* Intro prose. Mono working face, same measure as About prose. */
body.tpl-capture .capture-intro {
	max-width: 58ch;
}

body.tpl-capture .capture-intro p,
body.tpl-capture .rates-note,
body.tpl-capture .capture-rates p,
body.tpl-capture .form-success p {
	font-family: var(--font-mono);
	font-size: 1.05rem;
	line-height: 1.5em;
	margin-bottom: 1.2em;
	max-width: 58ch;
}

/* Rates and specs reuse About's .spec-list / .spec-row pattern, re-declared
   under the capture scope. */
body.tpl-capture .spec-list {
	max-width: 68ch;
	border-top: 1px solid var(--color-rule);
}

body.tpl-capture .spec-row {
	display: grid;
	grid-template-columns: 10em 1fr;
	grid-column-gap: var(--sm-base-space);
	padding-top: 0.75rem;
	padding-bottom: 0.75rem;
	border-bottom: 1px solid var(--color-rule);
}

@media screen and (min-width: 1025px) {
	body.tpl-capture .spec-row {
		grid-template-columns: 18em 1fr;
		grid-column-gap: var(--md-base-space);
	}
}

body.tpl-capture .spec-row dt {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
}

body.tpl-capture .spec-row dd {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	margin: 0;
}

/* Rate row: the figure sits above its supporting detail in the value cell.
   The figure reads at body-plus so a price is the first thing scanned; the
   detail stays in the grey label register. */
body.tpl-capture .rate-row .rate-figure {
	display: block;
	font-size: 1.25em;
	line-height: 1.2em;
	letter-spacing: -0.02em;
	margin-bottom: 0.35rem;
}

body.tpl-capture .rate-row .rate-detail {
	display: block;
	color: var(--color-gray-500);
}

body.tpl-capture .rates-note {
	color: var(--color-gray-500);
	padding-top: var(--md-base-space);
}

/* Citation line, same de-emphasis register as .legal-updated / .about-updated:
   mono, grey, a credit rather than prose. */
body.tpl-capture .capture-note {
	color: var(--color-gray-500);
	padding-top: var(--md-base-space);
}

/* Comp card. Two images side by side on desktop, stacked on mobile. */
body.tpl-capture .comp-grid {
	display: grid;
	grid-template-columns: 1fr;
	grid-gap: var(--md-base-space);
}

@media screen and (min-width: 1025px) {
	body.tpl-capture .comp-grid {
		grid-template-columns: 1fr 1fr;
	}
}

body.tpl-capture .comp-shot {
	margin: 0;
}

body.tpl-capture .comp-shot img {
	display: block;
	width: 100%;
	height: auto;
}

body.tpl-capture .comp-shot figcaption {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-top: var(--sm-base-space);
}

body.tpl-capture .comp-specs {
	margin-top: var(--md-base-space);
}

/* --- Inquiry form --------------------------------------------------------
   Rule-lined fields, mono labels in grey, one inverted submit button (the
   only inverted control, matching the About jump button). Honeypot is pushed
   off-screen but stays in the DOM. */
body.tpl-capture .capture-form {
	max-width: 44ch;
	margin-top: 4vh;
}

body.tpl-capture #form {
	scroll-margin-top: calc(var(--hh) + var(--md-base-space));
}

body.tpl-capture .inquiry .field {
	margin-bottom: var(--md-base-space);
}

body.tpl-capture .inquiry label {
	display: block;
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
	margin-bottom: 0.5rem;
}

body.tpl-capture .inquiry .opt {
	text-transform: none;
}

body.tpl-capture .inquiry input,
body.tpl-capture .inquiry textarea {
	width: 100%;
	font-family: var(--font-mono);
	font-size: 1rem;
	line-height: 1.4em;
	color: var(--color-text);
	background-color: var(--color-bg);
	border: 1px solid var(--color-rule);
	border-radius: 0;
	padding: 0.6rem 0.7rem;
	/* 44px minimum tap target. */
	min-height: 44px;
	-webkit-appearance: none;
	appearance: none;
}

body.tpl-capture .inquiry textarea {
	min-height: 8em;
	resize: vertical;
}

body.tpl-capture .inquiry input:focus-visible,
body.tpl-capture .inquiry textarea:focus-visible {
	outline: 2px solid var(--color-text);
	outline-offset: 2px;
}

/* Inverted submit, mirroring the About jump button so the site has one button
   style. */
body.tpl-capture .inquiry .submit {
	display: inline-block;
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--color-bg);
	background-color: var(--color-text);
	border: 1px solid var(--color-text);
	border-radius: 0;
	padding: 0.85rem var(--md-base-space);
	min-height: 44px;
	cursor: pointer;
	transition:
		background-color var(--dur-fast) var(--ease),
		color var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-capture .inquiry .submit:hover {
		color: var(--color-text);
		background-color: var(--color-bg);
	}
}

/* Honeypot: in the DOM for bots, out of sight and out of the tab order for
   humans. Not display:none (some bots skip those). */
body.tpl-capture .gw-hp {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/* Success and error states. */
body.tpl-capture .form-success__lead {
	font-family: var(--font-mono);
	font-size: 1.4em;
	line-height: 1.2em;
	letter-spacing: -0.04em;
	margin-bottom: var(--sm-base-space);
}

body.tpl-capture .form-error {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-text);
	border: 1px solid var(--color-rule);
	padding: 0.7rem;
	margin-bottom: var(--md-base-space);
}

/* --- Two-column booking layout (round 3) ---------------------------------
   Desktop (>=1024px): left column scrolls (intro, rates, retainer, comp
   gallery), right column is the enquiry form, position:sticky so it stays in
   view while the left scrolls. Wider page use than the single narrow column:
   the grid container relaxes the per-section ch caps that otherwise keep the
   page narrow. Below 1024px .bookings-layout is a plain block, so the current
   stacked flow is unchanged. Pure CSS grid + sticky, no JS, native momentum
   scroll preserved. Mirrors the About .about-band mechanism (top offset =
   header height + one spacing token). */
@media screen and (min-width: 1024px) {
	body.tpl-bookings .bookings-layout {
		display: grid;
		/* Left column keeps its ~64ch measure; the form column is 40ch and is
		   pushed to the wrap's RIGHT content edge. space-between puts the empty
		   space BETWEEN the two columns, not to the right of them, so the form
		   sits flush right like the homepage hero canvas. Full wrap width — no
		   108ch cap. */
		grid-template-columns: minmax(0, 64ch) 40ch;
		justify-content: space-between;
		align-items: start;
	}

	/* Left column scrolls. Its children keep their own ch caps; the wider
	   grid track just gives them room to breathe rather than sit in a narrow
	   ribbon. */
	body.tpl-bookings .bookings-main {
		min-width: 0;
	}

	/* Right column pins. top offset matches About's sticky portrait. The form
	   inside keeps its 44ch cap but now fills the aside track. */
	body.tpl-bookings .bookings-aside {
		position: sticky;
		top: calc(var(--hh) + var(--md-base-space));
	}

	body.tpl-bookings .bookings-aside .capture-form {
		margin-top: 0;
		max-width: none;
	}

	/* Form visible at load makes the inline jump redundant: hide it on
	   desktop. The mobile fixed button (max-width:1024px block below) stays. */
	body.tpl-bookings .bookings-main .jump-book {
		display: none;
	}
}

/* --- Jump to enquiry (bookings) ------------------------------------------
   Adapted from the About .jump-book control, re-scoped to body.tpl-bookings
   so the two pages share one button vocabulary without leaking into each
   other. On desktop it sits at the top of the content; below 1025px it
   becomes a fixed bottom-right button. Points at #form on this page. The
   label types itself once via the shared runtime script (typeJumpButton(),
   page-agnostic, already targets .jump-book). "Jump to enquiry" is 15 chars;
   mono means 1ch === 1 glyph, so width:15ch is the resting state.

   scroll-behavior:smooth and its reduced-motion reset already live globally
   (About's jump-to-booking block), so they are not repeated here. #form
   already carries scroll-margin-top in the capture block above. */
body.tpl-bookings .jump-book {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sm-base-space);
	max-width: 44ch;
	margin-top: var(--md-base-space);
	margin-bottom: var(--md-base-space);
	padding: 0.85rem var(--sm-base-space);
	border: 1px solid var(--color-text);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
	min-height: 44px;
	color: var(--color-bg);
	background-color: var(--color-text);
	transition:
		background-color var(--dur-fast) var(--ease),
		color var(--dur-fast) var(--ease);
}

body.tpl-bookings .jump-book__label {
	display: inline-block;
	overflow: hidden;
	white-space: nowrap;
	vertical-align: bottom;
	/* 15 glyphs, but 0.04em tracking adds ~7px total, so 15ch clips the last
	   character. 16ch gives one glyph of slack to hold the full "y" without
	   affecting the space-between layout (the arrow still sits hard right). */
	width: 16ch;
}

html.js body.tpl-bookings .jump-book.in-view .jump-book__label {
	animation: gw-type-btn-bk 0.9s steps(15, jump-start) both;
}

@keyframes gw-type-btn-bk {
	from { width: 0ch; }
	to   { width: 16ch; }
}

body.tpl-bookings .jump-book__arrow {
	display: inline-block;
}

body.tpl-bookings .jump-book.in-view .jump-book__arrow {
	animation: gw-nudge 2s var(--ease) 0.9s infinite;
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-bookings .jump-book:hover {
		color: var(--color-text);
		background-color: var(--color-bg);
	}

	body.tpl-bookings .jump-book:hover .jump-book__arrow {
		animation: none;
		transform: translateY(0.25em);
		transition: transform var(--dur-fast) var(--ease);
	}
}

/* Below the two-column breakpoint the control pins bottom-right, matching
   About. Mobile is most of the traffic and this is the only mid-page route
   to the form. */
@media screen and (max-width: 1024px) {
	body.tpl-bookings .jump-book {
		position: fixed;
		right: var(--sm-base-space);
		bottom: var(--sm-base-space);
		z-index: 998;
		max-width: none;
		width: auto;
		margin-top: 0;
		margin-bottom: 0;
		gap: 0.5rem;
		box-shadow: 0 0 0 2px var(--color-bg);
	}
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-bookings .jump-book,
	body.tpl-bookings .jump-book__arrow,
	body.tpl-bookings .jump-book__label,
	html.js body.tpl-bookings .jump-book.in-view .jump-book__label {
		transition: none !important;
		animation: none !important;
		width: auto !important;
	}

	body.tpl-bookings .jump-book:hover .jump-book__arrow {
		transform: none !important;
	}
}

/* --- Rates & comp-card accordions (capture) ------------------------------
   The rate categories and the comp card in the /legal/ hairline-box
   language: 1px boxes flush into a single ruled list, squared corners,
   summary weight 400, a CSS-only +/- marker swapped on [open] with the
   site's one easing curve. Zero JS. The rate summary carries the category
   name and the range so prices scan without expanding; the body holds the
   detail sentence. This is a parallel, capture-scoped copy of the
   body.tpl-legal details styling — the legal block is left untouched. */
body.tpl-capture .rate-accordion,
body.tpl-capture .comp-accordion {
	max-width: 68ch;
	margin-top: var(--md-base-space);
}

body.tpl-capture .rate-item,
body.tpl-capture .comp-item {
	border: 1px solid var(--color-rule);
	margin-bottom: -1px;
}

body.tpl-capture .rate-item > summary,
body.tpl-capture .comp-item > summary {
	font-family: var(--font-mono);
	font-weight: 400;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	color: var(--color-text);
	list-style: none;
	cursor: pointer;
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--sm-base-space);
	padding: var(--sm-base-space);
	min-height: 44px;
	transition: color var(--dur-fast) var(--ease);
}

body.tpl-capture .rate-item > summary::-webkit-details-marker,
body.tpl-capture .comp-item > summary::-webkit-details-marker {
	display: none;
}

body.tpl-capture .rate-item > summary::after,
body.tpl-capture .comp-item > summary::after {
	content: "+";
	font-family: var(--font-mono);
	font-weight: 400;
	font-size: 1.2em;
	line-height: 1;
	flex: 0 0 auto;
	transition: transform var(--dur-fast) var(--ease);
}

body.tpl-capture .rate-item[open] > summary::after,
body.tpl-capture .comp-item[open] > summary::after {
	content: "\2212"; /* minus sign */
	transform: rotate(180deg);
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-capture .rate-item > summary:hover,
	body.tpl-capture .comp-item > summary:hover {
		color: var(--color-hover);
	}
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-capture .rate-item > summary::after,
	body.tpl-capture .comp-item > summary::after {
		transition: none !important;
	}
}

/* Summary inner layout: name left, range right. The range sits in the value
   register (larger, tight tracking) so a price is the first thing scanned,
   matching the old .rate-figure. Wraps on narrow screens. */
body.tpl-capture .rate-summary {
	display: flex;
	flex: 1 1 auto;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: baseline;
	gap: 0.35rem var(--sm-base-space);
}

body.tpl-capture .rate-summary__name {
	text-transform: uppercase;
	color: var(--color-gray-500);
}

body.tpl-capture .rate-summary__figure {
	font-size: 1.25em;
	line-height: 1.2em;
	letter-spacing: -0.02em;
	color: var(--color-text);
}

body.tpl-capture .comp-summary__label {
	text-transform: uppercase;
	color: var(--color-gray-500);
}

/* Round 3: price moves out of the summary into the rate body. The summary
   now carries ONLY the category name (+ the CSS +/- marker). The old
   .rate-summary/.rate-summary__figure flex split is replaced by a single
   uppercase grey name, matching .comp-summary__label. */
body.tpl-capture .rate-item > summary .rate-name {
	text-transform: uppercase;
	color: var(--color-gray-500);
}

/* Price, now first line of the accordion body, in the value register (larger,
   tight tracking) so it is the first thing scanned on expand. The detail
   sentence follows in the grey label register (existing .rate-body p). */
body.tpl-capture .rate-body__figure {
	display: block;
	font-family: var(--font-mono);
	font-size: 1.25em;
	line-height: 1.2em;
	letter-spacing: -0.02em;
	color: var(--color-text);
	margin-bottom: 0.35rem;
}

/* Round 3: comp-card gallery. A curated 8-image set inside the (collapsed)
   comp accordion, left column, normal scroll flow (float, not parallax).
   Lazy-loaded, width/height set on every img so opening the panel reserves
   the box from the intrinsic ratio and adds no CLS beyond the natural
   expansion. Two columns on mobile, three on desktop; spacing tokens set the
   gutters. */
body.tpl-capture .comp-gallery {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	grid-gap: var(--md-base-space);
	margin-top: var(--sm-base-space);
}

@media screen and (min-width: 1024px) {
	body.tpl-capture .comp-gallery {
		grid-template-columns: repeat(3, 1fr);
	}
}

body.tpl-capture .comp-gallery__shot {
	margin: 0;
}

body.tpl-capture .comp-gallery__shot img {
	display: block;
	width: 100%;
	height: auto;
}

/* Every gallery image carries a visible caption credit. Same grey mono
   register as the existing .comp-shot figcaption. */
body.tpl-capture .comp-gallery__shot figcaption {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-top: 0.5rem;
}

/* Round 3 form additions ---------------------------------------------------
   Event time start + end sit side by side under the event-date field; they
   wrap on narrow screens. Each keeps the 44px tap target from the shared
   input rule. */
body.tpl-capture .inquiry .time-row {
	display: flex;
	flex-wrap: wrap;
	gap: var(--sm-base-space);
}

body.tpl-capture .inquiry .time-field {
	flex: 1 1 12ch;
	min-width: 12ch;
	margin-bottom: 0;
}

/* Event-type checkbox group. Each option is a label + native checkbox, at
   least 44px tall for the tap target. Mono, sentence case (not the uppercase
   grey label register — these are readable options, not field labels). */
body.tpl-capture .inquiry .checkbox-group {
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
	border: 1px solid var(--color-rule);
	padding: var(--sm-base-space);
}

body.tpl-capture .inquiry .checkbox-option {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	min-height: 44px;
	font-family: var(--font-mono);
	font-size: 1rem;
	line-height: 1.4em;
	color: var(--color-text);
	text-transform: none;
	margin-bottom: 0;
	cursor: pointer;
}

body.tpl-capture .inquiry .checkbox-option input[type="checkbox"] {
	width: 0.9rem;
	height: 0.9rem;
	min-height: 0;
	margin: 0;
	padding: 0;
	flex: 0 0 auto;
	accent-color: var(--color-text);
	cursor: pointer;
}

/* The general input reset above strips appearance, so the native tick
   never paints - the checked state MUST have its own visual or the box
   looks dead. Ticked = solid fill, matching the square-and-ink system. */
body.tpl-capture .inquiry .checkbox-option input[type="checkbox"]:checked {
	background-color: var(--color-text);
}

/* Thank-you block on ?sent=1 — replaces the form. Straight register, no
   jokes. Reuses the success lead scale. */
body.tpl-capture .form-thankyou {
	max-width: 58ch;
}

body.tpl-capture .form-thankyou__lead {
	font-family: var(--font-mono);
	font-size: 1.4em;
	line-height: 1.2em;
	letter-spacing: -0.04em;
	margin-bottom: var(--sm-base-space);
}

body.tpl-capture .form-thankyou p {
	font-family: var(--font-mono);
	font-size: 1.05rem;
	line-height: 1.5em;
	margin-bottom: 1.2em;
	max-width: 58ch;
}

/* Accordion bodies: same indented padding as the legal .legal-body. */
body.tpl-capture .rate-body,
body.tpl-capture .comp-body {
	padding: 0 var(--sm-base-space) var(--sm-base-space);
}

body.tpl-capture .rate-body p {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	margin: 0;
}

/* The comp-card grid keeps its existing rules (.comp-grid / .comp-shot /
   .comp-specs above); inside the details panel it just gets top spacing so
   it does not crowd the summary rule. */
body.tpl-capture .comp-body .comp-grid {
	margin-top: var(--sm-base-space);
}

/* Contact page: the enquiry-type <select> (styled like the text inputs, with a
   CSS-drawn caret) and the consent line under the submit button. */
body.tpl-capture .inquiry select{
  width:100%;font-family:var(--font-mono);font-size:1rem;line-height:1.4em;
  color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-rule);
  border-radius:0;padding:.6rem .7rem;min-height:44px;appearance:none;-webkit-appearance:none;
  cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--color-text) 50%),
                   linear-gradient(135deg,var(--color-text) 50%,transparent 50%);
  background-position:calc(100% - 18px) center,calc(100% - 13px) center;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat;
}
body.tpl-capture .inquiry select:focus-visible{outline:2px solid var(--color-text);outline-offset:2px;}
body.tpl-capture .inquiry .form-consent{
  margin-top:var(--md-base-space);font-family:var(--font-mono);font-size:var(--fs-main);
  line-height:var(--lh-main);color:var(--color-gray-500);max-width:44ch;
}
body.tpl-capture .inquiry .form-consent a{color:var(--color-text);text-decoration:underline;text-underline-offset:2px;}

/* ==========================================================================
   13. LEGAL

   Scoped to body.tpl-legal (the /legal/ page). One long page holding the
   Privacy Policy and Terms of Use as native <details> accordions. Each clause
   is a 1px hairline box (var(--color-rule)), squared corners, stacked with the
   spacing tokens. Body copy is IBM Plex Mono Light (300); the <summary> header
   stays weight 400 for scanability. A CSS-only +/- marker on the summary,
   swapped on [open] using the site's single easing curve. Zero JS.
   ========================================================================== */

body.tpl-legal .legal {
	padding-top: 6vh;
	padding-bottom: 10vh;
	max-width: 76ch;
}

body.tpl-legal .entry-title {
	margin-bottom: var(--md-base-space);
}

/* Document band: heading + updated line, then the accordion stack. The two
   docs are separated by extra top margin on the second section. */
body.tpl-legal .legal-doc {
	scroll-margin-top: calc(var(--hh) + var(--md-base-space));
}

body.tpl-legal #terms {
	margin-top: 10vh;
}

/* Uppercase mono grey label with a rule under, matching About/capture. */
body.tpl-legal .legal-doc-title {
	font-family: var(--font-mono);
	font-weight: 400;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	text-transform: uppercase;
	padding-bottom: var(--sm-base-space);
	margin-bottom: var(--sm-base-space);
	border-bottom: 1px solid var(--color-rule);
}

body.tpl-legal .legal-updated {
	font-family: var(--font-mono);
	font-weight: 300;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-gray-500);
	margin-bottom: var(--md-base-space);
}

/* The OAIC complaints citation. Same register as .legal-updated but full
   text contrast (--color-text) since it is a substantive clause, not a
   timestamp, and needs its link to read clearly. */
body.tpl-legal .legal-note {
	font-family: var(--font-mono);
	font-weight: 300;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-text);
	margin-bottom: var(--md-base-space);
}

body.tpl-legal .legal-note a {
	color: var(--color-link);
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-legal .legal-note a:hover {
		color: var(--color-hover);
	}
}

/* The accordion box. 1px hairline, squared corners, stacked flush so the
   borders read as a single ruled list; each box is a discrete tap target.
   The -1px margin collapses doubled borders into single hairlines. */
body.tpl-legal details {
	border: 1px solid var(--color-rule);
	margin-bottom: -1px;
}

/* Summary = the header row. Weight 400 for scanability, mono, 12px. The
   marker is drawn via ::after so we can swap it; hide the native one. */
body.tpl-legal summary {
	font-family: var(--font-mono);
	font-weight: 400;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	letter-spacing: var(--tracking-ui);
	color: var(--color-text);
	list-style: none;
	cursor: pointer;
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--sm-base-space);
	padding: var(--sm-base-space);
	min-height: 44px;
	transition: color var(--dur-fast) var(--ease);
}

/* Remove the default disclosure triangle across engines. */
body.tpl-legal summary::-webkit-details-marker {
	display: none;
}

/* The +/- marker. "+" closed, "-" open. Rotated with the site's easing curve
   so the swap has motion without new JS. */
body.tpl-legal summary::after {
	content: "+";
	font-family: var(--font-mono);
	font-weight: 400;
	font-size: 1.2em;
	line-height: 1;
	flex: 0 0 auto;
	transition: transform var(--dur-fast) var(--ease);
}

body.tpl-legal details[open] summary::after {
	content: "\2212"; /* minus sign */
	transform: rotate(180deg);
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-legal summary:hover {
		color: var(--color-hover);
	}
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-legal summary::after {
		transition: none !important;
	}
}

/* The clause body. IBM Plex Mono Light (300). Indented to align under the
   summary text. */
body.tpl-legal .legal-body {
	font-family: var(--font-mono);
	font-weight: 300;
	font-size: var(--fs-main);
	line-height: var(--lh-main);
	color: var(--color-text);
	padding: 0 var(--sm-base-space) var(--sm-base-space);
}

body.tpl-legal .legal-body p {
	margin-bottom: var(--sm-base-space);
}

body.tpl-legal .legal-body p:last-child {
	margin-bottom: 0;
}

body.tpl-legal .legal-body ul {
	list-style: none;
	margin: 0 0 var(--sm-base-space) 0;
	padding: 0;
}

body.tpl-legal .legal-body li {
	position: relative;
	padding-left: 1.5em;
	margin-bottom: 0.4em;
}

body.tpl-legal .legal-body li::before {
	content: "\2014"; /* em dash bullet, matches mono register */
	position: absolute;
	left: 0;
	color: var(--color-gray-500);
}

body.tpl-legal .legal-body a {
	color: var(--color-link);
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-legal .legal-body a:hover {
		color: var(--color-hover);
	}
}

/* ==========================================================================
   8C. HOME JOURNEY SEGMENTS + PROJECT MEDIA (2026-07-25)
   Extends 8A/8B for the sideways-scroll overhaul: the four credibility
   sections travel INSIDE the pinned track as text segments between
   category runs of slides (inc/home-appendix.php on wp_footer, DOM order
   main -> footer -> journey). All 8A stage/track/slide/legend rules apply
   unchanged; everything new lives here. Tokens only, no new colours or
   spacing. Appended as its own section so 8A/8B stay byte-identical to
   their verified state.
   ========================================================================== */

/* The journey section: hairline off the footer above it (desktop DOM) or
   off main (mobile visual order). Background solid so the sticky stage
   never shows page content through it. */
body.tpl-home .gw-journey {
	border-top: 1px solid var(--color-rule);
	background-color: var(--color-bg);
}

/* Text segments: vertical columns inside the horizontal track. Mobile
   first - in the stacked column they read as the original appendix
   sections, padded like the old vertical block. */
body.tpl-home .pin-index__seg {
	display: flex;
	flex-direction: column;
	padding-top: var(--md-base-space);
	padding-bottom: var(--md-base-space);
}

/* Segment internals reuse the 8B home-appendix component grammar. Inside
   a panel the wide-screen grids and page-scale margins come back down to
   panel scale. */
body.tpl-home .pin-index__seg .home-appendix__anchor,
body.tpl-home .pin-index__seg .home-appendix__lede {
	margin-bottom: var(--md-base-space);
}

body.tpl-home .pin-index__seg .home-appendix__spec {
	margin-bottom: 0;
	max-width: none;
}

body.tpl-home .pin-index__seg .home-appendix__row {
	grid-template-columns: 1fr;
	gap: 0.35em;
}

body.tpl-home .pin-index__seg .home-appendix__routes {
	max-width: none;
	margin-bottom: 0;
}

body.tpl-home .pin-index__seg .home-appendix__qa {
	max-width: none;
	margin-bottom: var(--sm-base-space);
}

body.tpl-home .pin-index__seg .home-appendix__updated {
	margin-top: var(--sm-base-space);
}

/* Mobile visual order: the journey renders after the footer in DOM (that
   is what protects screen 1 on desktop), but in the stacked mobile flow
   the footer belongs at the true page bottom (Amendment A). Flex order at
   body level, scoped to the front page. The fallback vertical appendix
   gets the same treatment. */
@media screen and (max-width: 1024px) {
	body.tpl-home {
		display: flex;
		flex-direction: column;
	}

	body.tpl-home .gw-journey,
	body.tpl-home .home-appendix {
		order: 1;
	}

	body.tpl-home .site-footer {
		order: 2;
	}
}

@media screen and (min-width: 1025px) {

	/* Segments are full-height columns against the 66vh centred slides:
	   the about and services text hangs high, press hangs low, questions
	   centres - the asymmetric rhythm of the journey. Widths in ch so the
	   mono measure stays honest. */
	body.tpl-home .pin-index__seg {
		flex: 0 0 auto;
		align-self: stretch;
		justify-content: flex-start;
		padding-top: 12vh;
		padding-bottom: 12vh;
		margin-left: var(--md-base-space);
	}

	body.tpl-home .pin-index__seg--about {
		width: 46ch;
	}

	body.tpl-home .pin-index__seg--press {
		width: 56ch;
		justify-content: flex-end;
	}

	body.tpl-home .pin-index__seg--services {
		width: 58ch;
	}

	body.tpl-home .pin-index__seg--questions {
		width: 54ch;
		justify-content: center;
		margin-right: var(--md-base-space);
	}
}

/* Panel reveal: each panel eases in as the track carries it into the
   viewport (pin-index.js latches .is-in, one way). Hidden state exists
   ONLY when JS runs, on the desktop pin, with motion allowed - every
   other reader sees everything, always. */
@media screen and (min-width: 1025px) and (prefers-reduced-motion: no-preference) {
	html.js body.tpl-home .pin-index__panel {
		opacity: 0;
		transform: translateX(2rem);
		transition: opacity var(--dur-base) var(--ease),
			transform var(--dur-base) var(--ease);
	}

	html.js body.tpl-home .pin-index__panel.is-in {
		opacity: 1;
		transform: none;
	}
}

/* Project pages: the featured image under the meta line, then the body.
   The back link returns to the index. */
body.tpl-project .entry-media {
	max-width: 900px;
	padding-top: var(--md-base-space);
}

body.tpl-project .entry-media img {
	display: block;
	width: 100%;
	height: auto;
}

body.tpl-project .entry-back {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	padding-bottom: 8vh;
}

body.tpl-project .entry-back a {
	transition: opacity var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	body.tpl-project .entry-back a:hover {
		opacity: 0.5;
	}
}

/* ==========================================================================
   8D. JOURNEY v2: PERSISTENT FOOTER, FIXED HERO, LOOP SEAM, MOBILE STRIP
   (2026-07-25, Gabe's corrected spec.) Extends 8C. Sequence: hero photo ->
   About + CTA -> press rows -> Get more Gabe gallery -> Services ->
   Questions -> wordmark seam -> loop. Desktop footer is fixed (always
   visible through the journey); mobile is a vertical editorial column
   with the gallery as a side-scrolling strip.
   ========================================================================== */

/* Mobile first: the hero photo is a full-width editorial opener and the
   gallery is a horizontal swipe strip inside the vertical column. */
body.tpl-home .pin-index__hero img {
	display: block;
	width: 100%;
	height: auto;
}

/* Mobile gallery strip ONLY. Unscoped, these out-specific the desktop
   height-driven slide sizing in 8D and turn every even slide into a
   fixed cover-crop box - the beheaded portraits of 2026-07-25. Keep
   them behind the mobile media query permanently. */
@media screen and (max-width: 1024px) {
	body.tpl-home .pin-index__gallery {
		display: flex;
		overflow-x: auto;
		gap: var(--sm-base-space);
		-webkit-overflow-scrolling: touch;
	}

	body.tpl-home .pin-index__gallery .pin-index__slide {
		flex: 0 0 78vw;
	}

	/* Uniform strip height: mixed-aspect sources previously set the strip
	   to the TALLEST slide and left a dead white band under the short
	   ones. Faces bias to the upper third, so the crop looks up. */
	body.tpl-home .pin-index__gallery .pin-index__slide img {
		width: 100%;
		height: 78vw;
		object-fit: cover;
		object-position: 50% 30%;
	}
}

/* The seam and the loop are desktop pin chrome only. */
body.tpl-home .pin-index__seam {
	display: none;
}

body.tpl-home .pin-index__cta {
	margin-top: var(--md-base-space);
}

@media screen and (min-width: 1025px) {

	/* Persistent footer: fixed to the viewport bottom for the whole
	   journey. Screen 1 already reserves its height (--fh), so load
	   geometry is unchanged; the journey stage runs underneath it. */
	body.tpl-home .site-footer {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 10;
		background-color: var(--color-bg);
	}

	/* The counter sits above the fixed footer, not under it. */
	body.tpl-home .pin-index__count {
		bottom: calc(var(--fh) + var(--sm-base-space));
	}

	/* ---- Chrome collapse (Gabe, 2026-07-25) ----
	   pin-index.js latches body.gw-journeying past the first travelled
	   pixel of the journey and releases it back at screen 1: header
	   slides up, footer slides down, the horizontal room owns the full
	   viewport. Transform ONLY - functions.php measures offsetHeight
	   into --hh/--fh and transforms never change those. 0.6s on the
	   signature curve: the bundle's only full-viewport translateY move
	   (body.slide-down) runs .6s cubic-bezier(0.7,0,0.3,1). */
	body.tpl-home .site-header,
	body.tpl-home .site-footer {
		transition: background-color var(--dur-fast) var(--ease),
			transform var(--dur-slow) var(--ease);
	}

	body.tpl-home.gw-journeying .site-header {
		transform: translateY(-100%);
	}

	body.tpl-home.gw-journeying .site-footer {
		transform: translateY(100%);
	}

	/* With the footer away, the counter drops to the viewport edge. */
	body.tpl-home.gw-journeying .pin-index__count {
		bottom: var(--md-base-space);
	}
}

/* The runway scrollbar advertises a five-storey page for what is
   experienced as a horizontal room - hidden STATICALLY (never toggled:
   toggling changes innerWidth and re-runs measure() mid-journey).
   Scoped to no-preference so the reduced-motion static column keeps
   its scrollbar. */
@media screen and (min-width: 1025px) and (prefers-reduced-motion: no-preference) {
	html:has(> body.tpl-home) {
		scrollbar-width: none;
	}

	html:has(> body.tpl-home)::-webkit-scrollbar {
		display: none;
	}
}

@media screen and (min-width: 1025px) {

	/* Hero photo panel: the journey's opener. pin-index.js counter-
	   translates the image so it reads as fixed while its own panel
	   departs with the track: the panel edge wipes across the still
	   image. overflow:hidden is load-bearing - without it the pinned
	   image paints through the track's flex gaps into later panels. */
	body.tpl-home .pin-index__hero {
		flex: 0 0 auto;
		width: 44vw;
		position: relative;
		z-index: 0;
		overflow: hidden;
	}

	body.tpl-home .pin-index__hero img {
		height: 74vh;
		width: auto;
		max-width: none;
		object-fit: cover;
		will-change: transform;
	}

	/* Panels that travel over the fixed hero need their own paint. */
	body.tpl-home .pin-index__seg {
		position: relative;
		z-index: 1;
		background-color: var(--color-bg);
	}

	body.tpl-home .pin-index__slide {
		position: relative;
		z-index: 1;
	}

	/* Desktop: the gallery wrapper dissolves so every slide is a direct
	   flex item of the track - one continuous row, identical geometry. */
	body.tpl-home .pin-index__gallery {
		display: contents;
	}

	/* The gallery label is a narrow chapter marker. */
	body.tpl-home .pin-index__seg--gallery {
		width: 22ch;
		justify-content: center;
	}

	/* The wordmark seam: the loop's return point. Hero-scale mono. */
	body.tpl-home .pin-index__seam {
		display: flex;
		flex: 0 0 auto;
		align-items: center;
		width: 56vw;
		padding-left: var(--md-base-space);
	}

	/* Must render identical to the hero h1 (family, weight, tracking,
	   leading) or the loop-back reads as a different page. */
	body.tpl-home .pin-index__seam-mark {
		font-family: var(--font-display);
		font-weight: normal;
		font-size: 10vw;
		line-height: 0.85em;
		letter-spacing: -0.06em;
		text-transform: uppercase;
	}
}

/* Reduced motion: no pin, no loop, no fixed footer overlay - the static
   column flows normally and everything stays in the document flow. */
@media screen and (min-width: 1025px) and (prefers-reduced-motion: reduce) {
	body.tpl-home .site-footer {
		position: static;
	}

	body.tpl-home .pin-index__seam {
		display: none;
	}

	body.tpl-home .pin-index__hero img {
		height: auto;
		width: 100%;
	}
}

/* Loop clones (pin-index.js cloneHead) are desktop pin furniture only:
   in the mobile column or the reduced-motion static list they would
   read as duplicated content. */
@media screen and (max-width: 1024px) {
	body.tpl-home .pin-index__clone {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	body.tpl-home .pin-index__clone {
		display: none;
	}
}

/* 8D fix (Gabe screenshot, 2026-07-25): inside a journey segment the
   routes grid is 2x2, never 4-across - the 58ch Services panel was
   cutting off the fourth button. Spec buttons styled as buttons (the
   naked URLs are gone; <cite> carries the citation signal). Slides get
   scale rhythm and a smaller footprint. Section titles take the
   established secondary display step. */
@media screen and (min-width: 1025px) {
	body.tpl-home .pin-index__seg .home-appendix__routes {
		grid-template-columns: 1fr 1fr;
	}

	/* The About CTA reuses the routes grammar but holds ONE link: on a
	   two-column grid the empty second cell paints the hairline
	   background - the stray black box of 2026-07-25. */
	body.tpl-home .pin-index__seg .pin-index__cta {
		grid-template-columns: 1fr;
	}

	body.tpl-home .pin-index__slide img {
		height: 56vh;
		max-width: 60vw;
	}

	body.tpl-home .pin-index__gallery .pin-index__slide:nth-child(even) img {
		height: 66vh;
	}

	body.tpl-home .pin-index__seg .home-appendix__section-title {
		font-size: 2.3em;
		line-height: 1em;
		letter-spacing: -0.04em;
		color: var(--color-text);
		margin-bottom: var(--md-base-space);
	}
}

body.tpl-home .home-appendix__spec-btn,
body.tpl-project .home-appendix__spec-btn {
	display: inline-flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sm-base-space);
	min-height: 44px;
	min-width: 44px;
	padding: 0.5em var(--sm-base-space);
	margin-top: 0.5em;
	border: 1px solid var(--color-rule);
	justify-self: start;
	background-color: var(--color-bg);
	color: var(--color-text);
	text-decoration: none;
	text-transform: uppercase;
	transition: background-color var(--dur-base) var(--ease),
		color var(--dur-base) var(--ease);
}

body.tpl-home .home-appendix__spec-btn:hover,
body.tpl-home .home-appendix__spec-btn:focus-visible,
body.tpl-project .home-appendix__spec-btn:hover,
body.tpl-project .home-appendix__spec-btn:focus-visible {
	background-color: var(--color-text);
	color: var(--color-bg);
}

/* ==========================================================================
   8E. EVENTS CALENDAR (2026-07-25)
   Journey panel between the gallery and Services. The server-rendered
   upcoming list is the content (AEO); the month grid is JS-built chrome
   (gw-calendar.js) - hairline cells on the routes 1px-gap grammar,
   click a day to list its events. Tokens only.
   ========================================================================== */

body.tpl-home .gw-events__list {
	list-style: none;
	margin: 0 0 var(--md-base-space);
	padding: 0;
	border-top: 1px solid var(--color-rule);
}

body.tpl-home .gw-events__list a {
	display: flex;
	justify-content: space-between;
	gap: var(--sm-base-space);
	align-items: baseline;
	min-height: 44px;
	padding: 0.85em 0;
	border-bottom: 1px solid var(--color-rule);
	text-decoration: none;
	color: var(--color-text);
	text-transform: uppercase;
	transition: opacity var(--dur-fast) var(--ease);
}

body.tpl-home .gw-events__list a:hover {
	opacity: 0.5;
}

body.tpl-home .gw-events__list time {
	color: var(--color-gray-500);
	font-variant-numeric: tabular-nums;
	flex: 0 0 auto;
}

body.tpl-home .gw-events__none {
	color: var(--color-gray-500);
	text-transform: uppercase;
	margin-bottom: var(--md-base-space);
}

body.tpl-home .gw-cal__head {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: var(--sm-base-space);
	margin-bottom: var(--sm-base-space);
	text-transform: uppercase;
}

body.tpl-home .gw-cal__nav-btn {
	min-width: 44px;
	min-height: 44px;
	border: 1px solid var(--color-rule);
	background-color: var(--color-bg);
	color: var(--color-text);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	cursor: pointer;
	transition: background-color var(--dur-base) var(--ease),
		color var(--dur-base) var(--ease);
}

body.tpl-home .gw-cal__nav-btn + .gw-cal__nav-btn {
	margin-left: -1px;
}

body.tpl-home .gw-cal__nav-btn:hover,
body.tpl-home .gw-cal__nav-btn:focus-visible {
	background-color: var(--color-text);
	color: var(--color-bg);
}

body.tpl-home .gw-cal__grid {
	display: grid;
	grid-template-columns: repeat(7, 1fr);
	gap: 1px;
	background-color: var(--color-rule);
	border: 1px solid var(--color-rule);
}

body.tpl-home .gw-cal__dow {
	background-color: var(--color-bg);
	color: var(--color-gray-500);
	padding: 0.5em 0;
	text-align: center;
	text-transform: uppercase;
}

body.tpl-home .gw-cal__cell {
	position: relative;
	min-height: 44px;
	border: 0;
	background-color: var(--color-bg);
	color: var(--color-text);
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	font-variant-numeric: tabular-nums;
	cursor: pointer;
	transition: background-color var(--dur-base) var(--ease),
		color var(--dur-base) var(--ease);
}

body.tpl-home .gw-cal__cell--pad {
	cursor: default;
}

body.tpl-home .gw-cal__cell.is-today {
	outline: 1px solid var(--color-text);
	outline-offset: -1px;
}

body.tpl-home .gw-cal__cell.has-event::after {
	content: "\2022";
	position: absolute;
	left: 50%;
	transform: translateX(-50%);
	bottom: 0.2em;
	line-height: 1;
}

body.tpl-home .gw-cal__cell.has-event:hover,
body.tpl-home .gw-cal__cell.is-selected {
	background-color: var(--color-text);
	color: var(--color-bg);
}

body.tpl-home .gw-cal__day {
	margin-top: var(--sm-base-space);
}

body.tpl-home .gw-cal__day-row {
	display: flex;
	justify-content: space-between;
	gap: var(--sm-base-space);
	align-items: baseline;
	min-height: 44px;
	padding: 0.85em 0;
	border-bottom: 1px solid var(--color-rule);
	text-decoration: none;
	color: var(--color-text);
	text-transform: uppercase;
}

body.tpl-home .gw-cal__day-row time {
	color: var(--color-gray-500);
	font-variant-numeric: tabular-nums;
}

@media screen and (min-width: 1025px) {
	body.tpl-home .pin-index__seg--events {
		width: 64ch;
	}
}

/* ==========================================================================
   8F. ARCHIVES + 404 (2026-07-25)
   index.php stopped looping when the journey moved to wp_footer, so
   archives get their own template on the index-row grammar.
   ========================================================================== */
body.archive .entry-title,
body.error404 .entry-title {
	font-size: 2.3em;
	line-height: 1em;
	letter-spacing: -0.04em;
	padding-top: 8vh;
	padding-bottom: var(--md-base-space);
	word-wrap: break-word;
	max-width: 100%;
}

@media screen and (min-width: 1025px) {
	body.archive .entry-title,
	body.error404 .entry-title {
		font-size: 3.32em;
	}
}

.archive-index a.index-row {
	text-decoration: none;
	color: var(--color-text);
	transition: opacity var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.archive-index a.index-row:hover {
		opacity: 0.5;
	}
}

.archive-empty,
.error-note {
	font-family: var(--font-mono);
	color: var(--color-gray-500);
	text-transform: uppercase;
	margin-bottom: var(--md-base-space);
}

body.archive .entry-back,
body.error404 .entry-back {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	padding-top: var(--md-base-space);
	padding-bottom: 8vh;
}

.pagination,
.nav-links {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	padding-top: var(--md-base-space);
}

.nav-links .page-numbers {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 44px;
	min-height: 44px;
	border: 1px solid var(--color-rule);
	margin-right: -1px;
	padding: 0 0.85em;
	text-decoration: none;
	color: var(--color-text);
	transition: background-color var(--dur-base) var(--ease),
		color var(--dur-base) var(--ease);
}

.nav-links .page-numbers.current,
.nav-links .page-numbers:hover {
	background-color: var(--color-text);
	color: var(--color-bg);
}

/* ==========================================================================
   8F. PROJECT PAGES: MIXED TEMPLATE (Session B, 2026-07-25)
   Header always; gw_layout meta picks the arrangement. Image-led keeps the
   photo under the meta line (8C rules unchanged). Type-led steps the title
   up to the display step (12.2vw / 10vw, the wordmark grammar - no third
   scale invented) and the photo, when one exists, lands after the body as
   a closing plate. Credit and reference rows are new components: mono set
   explicitly (lesson 9).
   ========================================================================== */

body.tpl-project .entry--type-led .entry-title {
	font-size: 12.2vw;
	letter-spacing: -0.06em;
	text-transform: uppercase;
}

@media screen and (min-width: 1025px) {
	body.tpl-project .entry--type-led .entry-title {
		font-size: 10vw;
	}
}

body.tpl-project .entry-media--plate {
	padding-bottom: var(--md-base-space);
}

body.tpl-project .entry-credit {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	text-transform: uppercase;
	letter-spacing: var(--tracking-ui);
	color: var(--color-gray-500);
	padding-top: calc(var(--sm-base-space) / 2);
}

body.tpl-project .entry-ref {
	font-family: var(--font-mono);
	font-size: var(--fs-main);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--sm-base-space);
}

body.tpl-project .entry-ref cite {
	color: var(--color-gray-500);
	overflow-wrap: anywhere;
}
