/* ============================================================================
   LONG VIEW  —  /landscape-architect-websites/
   A theme page that is also a landing page. Loads NO css/style.css and NO
   js/main.js; this file is the whole design.

   THE MATERIAL: a landscape architect's drawing sheet. One warm vellum, one
   ink, hairline rules, uppercase lettering, registration marks, a north point.
   Nothing is raised, nothing casts a shadow, and there is not a card on the
   page: the surfaces on a drawing are ruled, not lit. (That is also what keeps
   it out of Paper's territory, which is all soft shadow, and out of Daylight
   Study's, which is sheets pinned to a lit wall.)

   THE COLOUR RULE, and it is one line so that it can be checked:

       The drawing is ink on vellum. Every colour on this page arrives
       inside a photograph, and nothing else on the page is coloured.

   There is no accent variable in this file on purpose. The primary button is
   ink filled with vellum type; a link is ink with a ruled underline; the
   runway's "before" state is the photograph drained to a ghost and its "after"
   is the same photograph at full saturation. So the one moment of colour on
   the page is the one the reader scrubs into existence, which is the argument
   the section is making. ADDING A COLOURED TOKEN COSTS THE RULE ITS MEANING.

   THE ONE SCROLL-LOCKED SECTION is #long-view. Its justification is narrow and
   is stated on the page: a practice sells a thing that will not exist for two
   decades, and scroll is the only control a browser has that maps onto elapsed
   time. It is not a licence for a second one here.
   ============================================================================ */


/* ============================================================
   FONTS

   ONE FACE, AND ONE ONLY: Cormorant Garamond, self-hosted and
   subset by tools/fonts.py, preloaded in the <head> because it
   renders the banner h1, which sits on top of the LCP element and
   would otherwise repaint in a fallback. Body copy and every
   .anno stay on the system sans, so the serif is the VOICE and
   the sans is the INFORMATION. NO cross-origin request anywhere
   on this page.

   NO LATIN-EXT TWIN, deliberately: nothing on this page renders
   a character outside the LATIN subset. One accented letter in
   the copy means adding the twin to tools/fonts.py AND a second
   @font-face here, TOGETHER — a face declared over a range whose
   file does not exist renders tofu, not a fallback.

   NO ITALIC, also deliberately, and it saved 38K. The one place
   this page wants one is botanical names, and a planting plan
   letters those in spaced capitals anyway (see .lv_text), which
   is both more authentic and free.
   ============================================================ */
@font-face {
	font-family: "Cormorant Garamond";
	src: url(/assets/fonts/cormorant-latin-v1.woff2) format("woff2");
	font-weight: 400 600;
	font-style: normal;
	font-display: swap;
	unicode-range: U+0020-007E, U+00A0, U+00A9, U+00AE, U+00B0, U+00B7, U+2010-2027, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2190-2193, U+2212;
}

/* ---- Poster faces, for the collection band at the foot of the page.
   style.css's own subsets, declared verbatim: .poster_daylight and
   .poster_silver ask for them by name and this page loads no style.css.
   (.poster_paper wants plain Arial and needs nothing.) The unicode-ranges
   must match tools/fonts.py's LATIN / LATIN_EXT exactly and must not overlap.

   NOT PRELOADED, deliberately — small faces for three previews at the very
   bottom of the page, below several screens of content. The page preloads
   exactly ONE face, declared above. */
@font-face {
	font-family: "JetBrains Mono";
	src: url(/assets/fonts/jetbrains-mono-latin-v1.woff2) format("woff2");
	font-weight: 500;
	font-style: normal;
	font-display: swap;
	unicode-range: U+0020-007E, U+00A0, U+00A9, U+00AE, U+00B0, U+00B7, U+2010-2027, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2190-2193, U+2212;
}

@font-face {
	font-family: "JetBrains Mono";
	src: url(/assets/fonts/jetbrains-mono-latinext-v1.woff2) format("woff2");
	font-weight: 500;
	font-style: normal;
	font-display: swap;
	unicode-range: U+00A1-00A8, U+00AA-00AD, U+00AF, U+00B1-00B6, U+00B8-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC;
}

@font-face {
	font-family: "Cinzel Decorative";
	src: url(/assets/fonts/cinzel-decorative-poster-v1.woff2) format("woff2");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}


/* ============================================================
   TOKENS

   Every ratio below is measured against --sheet #EBE6DA and is
   stated because this page argues for accessibility in its own
   copy. MOVE --sheet AND THEY ALL MOVE.
   ============================================================ */
:root {
	--sheet: #EBE6DA;
	/* the vellum */
	--sheet-lift: #F3EFE6;
	/* a cleaner patch of it: figure frames, the form */
	--sheet-press: #E1DBCC;
	/* a duller patch: pressed rows, the plate's ground */

	--ink: #1F2A21;
	/* 11.95:1 — body, headings, the drawing */
	--ink-soft: #46523F;
	/* 6.64:1 — .anno, captions, secondary copy */
	--ink-ghost: #9A9384;
	/* 2.45:1 — DECORATIVE ONLY. Never set copy in it. */

	--rule: rgb(31 42 33 / .26);
	--rule-soft: rgb(31 42 33 / .13);

	/* THE DRAWN RULES' INK, and the two values are NOT free choices: they
	   are the OPAQUE equivalents of the two above, composited over the
	   sheet. THE RULING in the motion layer draws a line on top of a
	   hairline that is already there, so the two have to be
	   indistinguishable once it has finished, or every rule on the page
	   ends the scroll a shade off itself. color-mix states the derivation
	   instead of hard-coding it, so moving --ink or --sheet moves these.
	   A browser without color-mix resolves them to nothing, the
	   declaration that uses them is dropped, and the reader simply gets
	   the static hairline — which is the right failure. */
	--rule-drawn: color-mix(in srgb, var(--ink) 26%, var(--sheet));
	--rule-soft-drawn: color-mix(in srgb, var(--ink) 13%, var(--sheet));

	--gutter: clamp(1.25rem, 5vw, 4rem);
	--sheet-max: 1180px;
	--measure: 66ch;
	--mast-h: 3.75rem;

	--display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
	--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

	--ease: cubic-bezier(.22, .61, .36, 1);
}


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

html {
	/* WHITE-FLASH RULE: the page's real ground colour lives here, not
	   only on body, so a view-transition snapshot taken against the
	   root never flashes a colour this page never shows. */
	background: var(--sheet);
	scroll-behavior: smooth;
	-webkit-text-size-adjust: 100%;
}

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

body {
	margin: 0;
	background: var(--sheet);
	color: var(--ink);
	font-family: var(--sans);
	font-size: clamp(1rem, .96rem + .2vw, 1.09rem);
	line-height: 1.62;
	-webkit-font-smoothing: antialiased;
	overflow-x: clip;
}

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

img {
	display: block;
	height: auto;
}

.skip {
	position: absolute;
	left: -9999px;
	top: 0;
	z-index: 100;
	padding: .75rem 1.25rem;
	background: var(--ink);
	color: var(--sheet);
	text-decoration: none;
}

.skip:focus {
	left: 0;
}

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


/* ============================================================
   TYPE

   Cormorant is a light, high-contrast Garamond with a small
   x-height, so every size here is a step larger than the same
   role would take on a grotesque, and NOTHING sets body copy in
   it. Its hairlines need the size.
   ============================================================ */
h1,
h2,
h3 {
	font-family: var(--display);
	font-weight: 500;
	line-height: 1.08;
	letter-spacing: .004em;
	margin: 0;
	text-wrap: balance;
}

h1 {
	font-size: clamp(2.9rem, 1.6rem + 5.6vw, 5.6rem);
	font-weight: 600;
	line-height: 1.02;
	letter-spacing: -.006em;
}

h2 {
	font-size: clamp(2rem, 1.3rem + 2.8vw, 3.4rem);
	max-width: 24ch;
}

h3 {
	font-size: clamp(1.3rem, 1.1rem + .8vw, 1.65rem);
}

p {
	margin: 0 0 1.15em;
	max-width: var(--measure);
}

p:last-child {
	margin-bottom: 0;
}

strong {
	font-weight: 600;
}

em {
	font-style: italic;
	/* the system sans supplies this; the display face ships no italic */
}

/* A LINK IS INK PLUS A RULE, never a colour — THE COLOUR RULE. The
   underline is offset and thin so it reads as a drawn rule rather than
   as a browser default. */
a {
	color: var(--ink);
	text-decoration: underline;
	text-decoration-color: var(--rule);
	text-decoration-thickness: 1px;
	text-underline-offset: .22em;
	transition: text-decoration-color .2s var(--ease);
}

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

/* THE ANNOTATION VOICE — uppercase, letterspaced, small, on the sans.
   It is the drawing's lettering, and it is what carries hierarchy on a
   page with no colour and no shadow to spend on it. */
.anno {
	margin: 0;
	font-family: var(--sans);
	font-size: .7rem;
	font-weight: 500;
	letter-spacing: .17em;
	text-transform: uppercase;
	line-height: 1.5;
	color: var(--ink-soft);
	max-width: none;
}

.anno--note {
	margin-top: 2rem;
	margin-bottom: .6rem;
}

/* The titleblock strip: two annotations at opposite ends of a hairline,
   exactly as a drawing sheet signs itself off. */
.titleblock {
	display: flex;
	flex-wrap: wrap;
	gap: .5rem 1.5rem;
	justify-content: space-between;
	align-items: baseline;
	margin-top: 2.5rem;
	padding-top: .7rem;
	border-top: 1px solid var(--rule);
}


/* ============================================================
   BUTTONS — a ruled box, or an ink-filled one. No gradient, no
   shadow, no colour.
   ============================================================ */
.mybutton {
	display: inline-block;
	padding: .72em 1.5em;
	border: 1px solid var(--ink);
	background: transparent;
	color: var(--ink);
	font-family: var(--sans);
	font-size: .78rem;
	font-weight: 500;
	letter-spacing: .14em;
	text-transform: uppercase;
	text-decoration: none;
	cursor: pointer;
	transition: background-color .22s var(--ease), color .22s var(--ease);
}

.mybutton--primary {
	background: var(--ink);
	color: var(--sheet);
	/* vellum on ink measures the same 11.95:1 read the other way round */
}

@media (hover: hover) and (pointer: fine) {
	.mybutton:hover {
		background: var(--ink);
		color: var(--sheet);
	}

	.mybutton--primary:hover {
		background: var(--ink-soft);
		border-color: var(--ink-soft);
	}
}

.mybutton:disabled {
	opacity: .5;
	cursor: default;
}

/* One action per section, with a line of annotation under it saying what
   the action is for. */
.card_actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .85rem 1.5rem;
	margin-top: 2.75rem;
}


/* ============================================================
   THE MASTHEAD + THE INDEX PANEL

   Above 900px a plain row of the site's seven real pages. Below
   it the same seven move into a drawn panel behind the toggle,
   because seven links wrapping to three lines makes a fixed
   masthead a third of a phone screen tall.

   CRAWL PARITY: nothing is display:none and no link is dropped.
   The closed panel is a transformed, visibility:hidden sheet
   holding the same seven real <a href>, made `inert` by the
   script at the foot of the page.
   ============================================================ */
.masthead {
	position: fixed;
	inset: 0 0 auto;
	z-index: 40;
	min-height: var(--mast-h);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	padding: .5rem var(--gutter);
	background: var(--sheet);
	border-bottom: 1px solid var(--rule);
}

.wordmark {
	font-family: var(--display);
	font-size: 1.32rem;
	font-weight: 600;
	letter-spacing: .01em;
	text-decoration: none;
	white-space: nowrap;
}

.masthead nav ul {
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	gap: .35rem 1.35rem;
	margin: 0;
	padding: 0;
	list-style: none;
}

.masthead nav a {
	font-size: .72rem;
	font-weight: 500;
	letter-spacing: .13em;
	text-transform: uppercase;
	text-decoration: none;
	color: var(--ink-soft);
	transition: color .2s var(--ease);
}

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

.masthead .index_eyebrow,
.masthead .index_block,
.nav_toggle {
	display: none;
}

@media (max-width: 900px) {

	/* THE PANEL. A drawn sheet, ruled like everything else, that slides
	   down from under the masthead. `visibility` is what takes it out of
	   the a11y tree without taking it out of the crawl. */
	.nav_toggle {
		display: block;
		width: 2.4rem;
		height: 2.4rem;
		padding: 0;
		border: 1px solid var(--rule);
		background: transparent;
		cursor: pointer;
	}

	.nav_rules {
		display: block;
		width: 1.1rem;
		margin-inline: auto;
	}

	.nav_rule {
		display: block;
		height: 1px;
		background: var(--ink);
		transition: transform .28s var(--ease), opacity .2s var(--ease);
	}

	.nav_rule--2 {
		margin: .3rem 0;
	}

	html.nav-open .nav_rule--1 {
		transform: translateY(.31rem) rotate(45deg);
	}

	html.nav-open .nav_rule--2 {
		opacity: 0;
	}

	html.nav-open .nav_rule--3 {
		transform: translateY(-.31rem) rotate(-45deg);
	}

	#index_panel {
		position: fixed;
		inset: var(--mast-h) 0 auto;
		padding: 1.75rem var(--gutter) 2rem;
		background: var(--sheet);
		border-bottom: 1px solid var(--rule);
		transform: translateY(-102%);
		visibility: hidden;
		transition: transform .38s var(--ease), visibility .38s;
	}

	html.nav-open #index_panel {
		transform: none;
		visibility: visible;
	}

	html.nav-open,
	html.nav-open body {
		overflow: hidden;
	}

	.masthead .index_eyebrow,
	.masthead .index_block {
		display: flex;
	}

	.masthead .index_eyebrow {
		display: block;
		margin-bottom: 1rem;
	}

	.masthead nav ul {
		display: block;
	}

	.masthead nav li+li {
		border-top: 1px solid var(--rule-soft);
	}

	.masthead nav a {
		display: block;
		padding: .78rem 0;
		font-size: .8rem;
		color: var(--ink);
	}
}


/* ============================================================
   THE SCALEBAR — reading progress, drawn as a scale bar is: a
   hairline with a heavier one advancing along it. Decorative,
   and only the motion layer draws it.
   ============================================================ */
.scalebar {
	display: none;
	position: fixed;
	top: var(--mast-h);
	left: 0;
	right: 0;
	z-index: 39;
	height: 2px;
	background: var(--rule-soft);
	pointer-events: none;
}

.scalebar span {
	display: block;
	height: 100%;
	background: var(--ink);
	transform-origin: 0 50%;
	transform: scaleX(0);
}


/* ============================================================
   01 · THE BANNER, AND IT IS A CONTACT SHEET

   A full-screen photograph with the copy centred over it, which
   is the shape of the source template's own home banner.
   REPLACES the line-only cover (a decorative contour survey
   behind an eyebrow, headline, lede, two buttons and a
   titleblock) — that version opened exactly like the site's
   other drawing-sheet theme, which is the one thing this page
   could not afford to do. Do not rebuild it; the contour survey
   went with it.

   STRUCTURE ONLY. The track height, the sticky stage and every
   timeline live in THE MOTION LAYER at the foot of this file, so
   what is written here is the SETTLED, READABLE state: one
   full-bleed photograph with the copy on it, which is exactly
   what this banner was before 2026-08-07 and exactly what a
   reduced-motion reader still gets.

   THE EIGHT OUTER CELLS ARE `display: none` AT REST AND THAT IS
   LOAD-BEARING, not tidiness. They are lazy, so a display:none
   cell is never fetched — which is what makes the contact sheet
   cost a reduced-motion reader nothing at all rather than eight
   pictures they will never be shown.

   THE TYPE IS THE SHEET COLOUR OVER A DARKENED PHOTOGRAPH, not
   white: this page's paper is #EBE6DA and white type beside it
   reads as a second, colder palette.
   ============================================================ */
.banner {
	position: relative;
}

.banner_stage {
	position: relative;
	min-height: 100svh;
	display: flex;
	align-items: center;
	overflow: hidden;
	padding: calc(var(--mast-h) + clamp(3rem, 9vh, 6rem)) var(--gutter) clamp(4.5rem, 11vh, 7rem);
	background: var(--ink);
	/* the ground under the photograph is ink, so the section never
	   flashes vellum behind a picture that has not decoded yet */
}

.banner_grid {
	position: absolute;
	inset: 0;
}

.bg_cell {
	display: none;
	position: relative;
	margin: 0;
	overflow: hidden;
	/* `position` IS NOT OPTIONAL AND IT IS NOT COSMETIC. The pictures are
	   `position: absolute; inset: 0`, so without it every one of the nine
	   resolves against .banner_grid instead of its own cell: nine images
	   stacked on top of each other across all 300% of the grid, of which
	   you see the last in DOM order, stretched over three viewports and
	   upscaled to about 4200px wide. Caught live by the owner on 2026-08-07.

	   The clip is not optional either, even at rest: the centre picture
	   runs the 1.05 drift, and in a real grid cell that overshoot would
	   paint over its neighbours. */
}

.bg_cell--center {
	display: block;
	position: absolute;
	inset: 0;
}

.bg_cell img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center;
}

/* THE HAIRLINES BETWEEN THE CELLS. Four background layers, each one
   pixel on its own axis, positioned at the thirds — so they are 1px
   at every moment of the zoom, which a border on .bg_cell could not
   be (it would be scaled with the grid and open the banner at 3px).
   Hidden at rest, because at rest there is only one picture and a
   line across it would be ruling nothing. */
.banner_rules {
	display: none;
	position: absolute;
	inset: 0;
	pointer-events: none;
	--bl: rgb(235 230 218 / .32);
	background-image:
		linear-gradient(var(--bl) 0 100%),
		linear-gradient(var(--bl) 0 100%),
		linear-gradient(var(--bl) 0 100%),
		linear-gradient(var(--bl) 0 100%);
	background-size: 1px 100%, 1px 100%, 100% 1px, 100% 1px;
	background-position: 33.3333% 0, 66.6667% 0, 0 33.3333%, 0 66.6667%;
	background-repeat: no-repeat;
}

/* THE SCRIM IS A GRADIENT AND MUST STAY ONE. A hard-edged panel over a
   photograph is a straight boundary with no visible cause sitting on
   live content, which is what NO INVISIBLE WALLS forbids.

   TWO LAYERS, AND THE RADIAL IS THE LOAD-BEARING ONE: it sits under the
   copy column and is sized so the combined alpha never drops below
   about .8 anywhere type lands. That number is not taste. This
   photograph has a pale sky in it and the next one will have something
   else; #EBE6DA over unscrimmed sky measures 1.8:1, and over sky at
   .8 it measures 6:1. THE BANNER PHOTOGRAPH IS GOING TO BE SWAPPED, so
   the scrim has to hold for a picture nobody has seen yet — re-check
   this against any new one, at the eyebrow, which is the smallest type
   on the screen. The linear underneath it carries the scroll hint. */
.banner_scrim {
	position: absolute;
	inset: 0;
	background:
		radial-gradient(ellipse 72% 62% at 50% 46%, rgb(18 25 19 / .82) 0%, rgb(18 25 19 / .6) 58%, rgb(18 25 19 / .34) 100%),
		linear-gradient(to top, rgb(18 25 19 / .72) 0%, rgb(18 25 19 / .12) 32%);
	pointer-events: none;
}

/* AND IT NEEDED THE NARROW TWIN THE PARAGRAPH ABOVE PREDICTED (2026-08-06).
   The photograph was recropped for the diagram — the two share one file —
   and a narrow `cover` on a 2.05:1 source keeps a sliver down the middle,
   which on this picture is the pale sky above the axis. Measured at the
   eyebrow, the smallest type on the screen: 5.00:1 before the recrop,
   4.04:1 after, under the 4.5 floor for text that size.
   THE ELLIPSE GOES WIDER THAN THE VIEWPORT, and that is the actual repair:
   under 900px the copy column is effectively the full width and the
   eyebrow WRAPS, so the desktop ellipse's 72% does not reach the ends of
   the lines however dark its centre is made. Widening it first is what
   let the alpha stops stay near the desktop values — this twin measures
   5.6-6.3:1 across 360-900px against the desktop rule's 5.6-6.0, so it
   is the same picture, not a murkier one.
   900px, NOT the 860 the diagram uses: this is about the copy column
   going full width, which is a different thing at a different size.
   RE-MEASURE, do not eyeball, after any change to the photograph, the
   crop, or the eyebrow's copy. The owner's own banner photographs are still
   coming. */
@media (max-width: 900px) {
	.banner_scrim {
		background:
			radial-gradient(ellipse 125% 60% at 50% 42%, rgb(18 25 19 / .8) 0%, rgb(18 25 19 / .62) 60%, rgb(18 25 19 / .36) 100%),
			linear-gradient(to top, rgb(18 25 19 / .72) 0%, rgb(18 25 19 / .14) 32%);
	}
}

.banner_inner {
	position: relative;
	width: min(100%, 46rem);
	margin-inline: auto;
	text-align: center;
	color: var(--sheet);
}

.banner_eyebrow {
	margin-bottom: 1.5rem;
	color: var(--sheet);
	opacity: .88;
	text-wrap: balance;
	text-shadow: 0 1px 10px rgb(18 25 19 / .7);
	/* the smallest type on the first screen, so it is the one the scrim
	   is checked against, and it carries its own shadow as well */
}

.banner h1 {
	max-width: 15ch;
	margin-inline: auto;
	text-shadow: 0 1px 24px rgb(18 25 19 / .45);
	/* the photograph behind the headline is not a flat field, and a
	   Cormorant hairline needs the one thing a scrim cannot give it:
	   contrast against whatever it lands on locally */
}

/* THE HEADLINE AND THE LINE SHARE ONE BOX ON THE SHEET BUILD, and this
   is the settled, no-sheet state of that box: no box at all. `display:
   contents` means the wrapper generates nothing, so the two children
   are laid out by .banner_inner exactly as they were before the
   wrapper existed — which is what a reduced-motion reader, a browser
   with no scroll timelines and a page whose JavaScript never ran all
   get, and all three should read both sentences.
   The stacking, and the fact that the line starts invisible, are the
   MOTION LAYER's business and are declared there under `html.sheet`. */
.banner_swap {
	display: contents;
}

/* The banner line is display type and the lede is not, on purpose: two
   consecutive lines on the same face read as two competing titles. The
   lede itself now lives in #what. */
.banner_line {
	margin: 1.4rem auto 2.25rem;
	font-family: var(--display);
	font-size: clamp(1.35rem, 1.05rem + 1.15vw, 2.05rem);
	line-height: 1.24;
	max-width: 30ch;
	text-shadow: 0 1px 18px rgb(18 25 19 / .5);
}

.lede {
	max-width: 60ch;
	color: var(--ink-soft);
}

/* the one button on the first screen wears the sheet, not the ink: over
   a darkened photograph the primary's ink fill would disappear */
.banner .mybutton--primary {
	background: var(--sheet);
	border-color: var(--sheet);
	color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
	.banner .mybutton--primary:hover {
		background: transparent;
		border-color: var(--sheet);
		color: var(--sheet);
	}
}

/* The one thing on the page that has to move before the reader has done
   anything, so it is a time loop rather than a scrub. It sits on the
   banner's bottom edge, where the scrim is heaviest and a hairline in
   the sheet colour is legible over any photograph put behind it. */
.scroll_hint {
	position: absolute;
	left: 50%;
	bottom: clamp(1.5rem, 4vh, 2.5rem);
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	gap: .75rem;
	margin: 0;
	color: var(--sheet);
	opacity: .78;
}

.scroll_track {
	display: block;
	position: relative;
	width: 4.5rem;
	height: 1px;
	background: rgb(235 230 218 / .35);
	overflow: hidden;
}

.scroll_line {
	position: absolute;
	inset: 0;
	background: var(--sheet);
	transform: translateX(-110%);
}


/* ============================================================
   02 · THE LONG VIEW — the pinned runway

   Structure only. EVERY track height, sticky and timeline for
   this section lives in THE MOTION LAYER at the foot of this
   file, so the state written here IS the collapsed, readable
   one: an ordinary section, the photograph at full colour, the
   plan sitting over it as a quiet overlay, both statements in
   normal flow. A browser with no scroll timelines gets that and
   needs no undo.
   ============================================================ */
.lv_track {
	position: relative;
	border-top: 1px solid var(--rule);
}

.lv_stage {
	display: flex;
	flex-direction: column;
	gap: clamp(1rem, 2.6vh, 1.9rem);
	padding: clamp(2.5rem, 7vh, 4.5rem) var(--gutter);
	/* pinned, this stage's top IS the top of the viewport, and the
	   masthead is fixed over it — so the top padding has a floor that
	   clears it, or "Diagram 01" reads through the bar */
	padding-top: max(clamp(2.5rem, 7vh, 4.5rem), calc(var(--mast-h) + 1rem));
	width: min(100%, var(--sheet-max));
	margin-inline: auto;
}

.lv_head h2 {
	margin-top: .5rem;
	max-width: 26ch;
}

/* THE FRAME. Unpinned it holds the photograph's own aspect ratio; pinned
   it takes the leftover height of the stage instead (see the motion
   layer), which is why nothing here hard-codes a height.
   PINNED IT IS A WINDOW, NOT THE PICTURE. The stage's leftover height
   swings from about 1.5:1 on a tall desktop to 2.8:1 on a 720px laptop,
   so whatever ratio the photograph is cut to, a frame that FILLED would
   throw away a different third of it on every machine. It contains
   instead (see .lv_photo), which means the mat below shows as a margin
   of pressed paper on one axis — a photographic plate mounted on a
   sheet, which is what this page's material already is. */
.lv_frame {
	position: relative;
	aspect-ratio: 1200 / 585;
	overflow: hidden;
	border: 1px solid var(--rule);
	background: var(--sheet-press);
}

.lv_photo,
.lv_plan {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

/* TWO PAIRS THAT MUST AGREE, and this is the load-bearing line of the
   whole section. `object-fit: contain` is `meet`; `object-position:
   center` is `xMidYMid`, which is the SVG default and so is written
   nowhere — that is why the <svg> carries no preserveAspectRatio at all.
   The moment either pair disagrees the drawing slides off the ground it
   is drawn on. There is a SECOND pair of them in the motion layer, for
   the narrow-screen crop.
   NOTHING IS SLICED HERE, AND THAT IS THE FIX OF 2026-08-06. It was
   `cover` anchored `center bottom`, which threw away the top of the
   photograph — on a 1280x720 laptop the frame runs 2.78:1 and only the
   bottom 54% of the picture survived, so the background conifers, which
   are the twenty years this section is about, were not on screen at all
   on the machine most people read it on. Contain cannot do that to any
   viewport: the whole photograph and the whole drawing are always in
   the frame, at the same size, registered. */
.lv_photo {
	object-fit: contain;
	object-position: center;
	/* THE SETTLED GROUND IS HELD DRAINED, and it is a legibility rule,
	   not a mood. The ink is #1F2A21 and this photograph is a dark green
	   garden: at full colour the drawing over it is invisible, and the
	   still frame is exactly what a reader with no scroll timelines, no
	   JavaScript or reduced motion is left with. Half-drained and lifted,
	   every line reads and the drawing is the subject, which here it is.
	   The SCRUBBED version never has the problem — by the time the ground
	   reaches full colour the ink has already retired off it, which is
	   why `ground-arrive` ends on `filter: saturate(1) brightness(1)
	   contrast(1)` and overrides this. IT LIVES IN THE BASE STATE ON
	   PURPOSE: a twin under html.no-runway alone would miss a reduced-
	   motion reader with JavaScript off, who never gets the stamp. */
	filter: saturate(.28) brightness(1.22) contrast(.82);
}

.lv_plan {
	color: var(--ink);
	pointer-events: none;
}

/* --- the drawing ---------------------------------------------------- */
.lv_ink {
	fill: none;
	stroke: currentColor;
	vector-effect: non-scaling-stroke;
	/* the drawing is on the sheet, not on the photograph: a hairline that
	   thickened with the viewport would read as paint */
}

/* the spot level, which is the mark a perspective can carry. A north
   point cannot go on one, so it REPLACED the north point when the
   photograph stopped being plan view. */
.lv_level {
	stroke-width: 1;
}

.lv_level_mark {
	fill: currentColor;
	stroke: none;
}

/* the centre line, dash-dot as it is in every drawing office there has
   ever been, run all the way into the urn */
.lv_axis {
	stroke-width: 1.5;
	stroke-dasharray: 20 7 4 7;
}

.lv_edge {
	stroke-width: 2.2;
}

.lv_hatch {
	stroke-width: 1;
	opacity: .5;
}

/* THE MATURITY RING — the drawing convention this whole section is
   built on: a dashed outline at the size a plant will reach when it is
   grown, drawn at full size on day one around something the size of a
   dinner plate. It is the promise, in the profession's own notation.
   IT IS AN ELLIPSE, NOT A CIRCLE, and that is the registration: a
   circle drawn on the ground and seen from eye level is an ellipse, so
   the foreshortening is what lands the drawing on the photograph. Each
   ring is paired with a straight dashed cap at the crown, which is the
   HEIGHT half of the same promise and the half plan view could never
   show. */
.lv_ring {
	stroke-width: 1.5;
	stroke-dasharray: 9 7;
}

/* and the canopy that eventually fills it */
.lv_canopy {
	stroke-width: 1.5;
	fill: currentColor;
	fill-opacity: .1;
	transform-box: view-box;
	/* view-box, so the transform-origin written inline on each circle is
	   in the drawing's own coordinates. IT SITS AT THE PLANT'S BASE, not
	   its middle, so the canopy grows up out of the ground. */
}

.lv_leader {
	stroke-width: 1;
}

/* Spaced capitals, which is how a planting plan letters, and why this
   page ships no italic for its botanical names.
   THE PAPER HALO: each glyph is stroked in the sheet colour UNDER its
   own fill (`paint-order: stroke`), so a note reads on bark, on foliage
   or on paving without a callout box behind it — and a box is exactly
   what NO INVISIBLE WALLS would not have. It is what freed the lettering
   to sit beside the plants it names rather than all of it on the one
   pale surface in the picture.
   `vector-effect` is reset because .lv_ink sets `non-scaling-stroke` for
   the linework: inherited here it would hold the halo at 3.2 CSS pixels
   while the glyphs themselves scaled down with the frame, and on a small
   frame the halo would eat the letter. In user units it scales with the
   type, which is the only version that is a halo at every size. */
.lv_text {
	fill: currentColor;
	stroke: var(--sheet);
	stroke-width: 3.2;
	stroke-linejoin: round;
	paint-order: stroke;
	vector-effect: none;
	font-family: var(--sans);
	font-size: 15px;
	font-weight: 600;
	letter-spacing: .17em;
	text-transform: uppercase;
}

.lv_text--sm {
	font-size: 12.5px;
	font-weight: 500;
	stroke-width: 2.8;
	opacity: .8;
}

.lv_text--end {
	text-anchor: end;
}

/* THE LETTERING COMES OFF THE DRAWING ON A NARROW SCREEN, at exactly
   the width the frame starts letterboxing (see the motion layer — MOVE
   BOTH TOGETHER). Below it the frame is around 350px wide for a 1200-
   unit drawing, so 15px of lettering renders at four, and the fix is
   not to enlarge it: at a size that would read, "BUXUS SEMPERVIRENS" is
   two and a half times the width of the paving it is written on and
   lands across the plants it names.
   Nothing is lost. The rings, the caps and the canopies are the
   argument; the years are on the datum bar under them; the words are
   the two statements in the section's own copy; and the SVG's <title>
   is unchanged, so the accessible description still says all of it. */
@media (max-width: 860px) {

	.lv_letters,
	.lv_level {
		display: none;
	}

	/* AND SO DOES THE OUTERMOST PAIR OF CONIFERS, for the same reason one
	   step further out: below this width the frame crops the flanks of the
	   photograph (see the motion layer), and those two stand at x 124 and
	   x 1112 of 1200. Left in, their rings and height caps would be cut in
	   half by the frame edge — a dashed promise running off the sheet,
	   which reads as an error rather than as a drawing. The two big cones
	   are inside the kept band and carry the same argument. */
	.lv_tree--edge {
		display: none;
	}

	/* and the line weight goes up with the lettering gone. The strokes
	   are `non-scaling`, so they are 1.5 CSS pixels whatever size the
	   frame is — which is a hairline on a desktop drawing and nearly
	   nothing on a phone, where the whole diagram is a third of the
	   size and the reader is holding it at arm's length. */
	.lv_ring,
	.lv_canopy,
	.lv_axis {
		stroke-width: 2.1;
	}

	.lv_edge {
		stroke-width: 2.8;
	}
}

/* --- the datum: scroll position, read as years ---------------------- */
.lv_datum {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	padding: .7rem clamp(.75rem, 2.5vw, 1.5rem) .8rem;
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	align-items: center;
	gap: .35rem;
	font-family: var(--sans);
	font-size: .62rem;
	font-weight: 600;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--ink);
	background: linear-gradient(to top, rgb(235 230 218 / .92), rgb(235 230 218 / 0));
}

.lv_datum_rule {
	grid-column: 1 / -1;
	position: relative;
	height: 1px;
	margin-bottom: .5rem;
	background: var(--rule);
}

/* the only thing in the frame that travels horizontally, and what tells
   the reader this is a timeline rather than a slideshow */
.lv_datum_mark {
	position: absolute;
	top: -3px;
	left: 0;
	width: 1px;
	height: 7px;
	background: var(--ink);
}

.lv_datum_tick--b,
.lv_datum_tick--c {
	text-align: center;
}

.lv_datum_tick--d {
	text-align: right;
}

/* --- the two statements --------------------------------------------- */
.lv_says {
	display: flex;
	flex-wrap: wrap;
	gap: .6rem clamp(1.5rem, 5vw, 4rem);
	justify-content: space-between;
}

.lv_say {
	margin: 0;
	max-width: 26ch;
	font-family: var(--display);
	font-size: clamp(1.3rem, 1.05rem + 1vw, 1.9rem);
	line-height: 1.2;
}

.lv_say--two {
	text-align: right;
	margin-left: auto;
}

@media (max-width: 700px) {
	.lv_say--two {
		text-align: left;
		margin-left: 0;
	}
}


/* ============================================================
   THE BANDS — ordinary sections, ruled off from each other.

   NO CARDS. A drawing does not stack panels; it rules areas
   apart and letters them. That is the layout argument and it is
   what keeps this page out of Paper's material.
   ============================================================ */
.band {
	padding: clamp(3.5rem, 10vh, 7rem) var(--gutter);
	border-top: 1px solid var(--rule);
}

.band_inner {
	width: min(100%, var(--sheet-max));
	margin-inline: auto;
}

.band_inner>.anno:first-child {
	margin-bottom: .9rem;
}

.band h2 {
	margin-bottom: 1.6rem;
}

.split {
	display: grid;
	grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
	gap: clamp(1.75rem, 4vw, 3.5rem);
	align-items: start;
}

.split--wide {
	grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
}

@media (max-width: 900px) {

	.split,
	.split--wide {
		grid-template-columns: 1fr;
	}
}

.notes {
	display: grid;
	gap: clamp(1.25rem, 3vh, 2rem);
}

/* THE DEFINITION — the AEO block, set a step up so an answer engine and
   a skimming reader both find it first. */
.define {
	font-family: var(--display);
	font-size: clamp(1.2rem, 1.05rem + .7vw, 1.55rem);
	line-height: 1.4;
	max-width: 46ch;
}

/* THE PLATE — a ruled note, pressed rather than raised. The corner ticks
   are the registration marks off the drawing, and they are what make it
   read as part of the sheet instead of as a UI card. */
.plate {
	position: relative;
	padding: clamp(1.25rem, 3vw, 1.9rem);
	background: var(--sheet-press);
}

.plate::before,
.plate::after {
	content: "";
	position: absolute;
	width: 9px;
	height: 9px;
	border: 1px solid var(--ink);
	pointer-events: none;
}

.plate::before {
	top: 6px;
	left: 6px;
	border-right: 0;
	border-bottom: 0;
}

.plate::after {
	right: 6px;
	bottom: 6px;
	border-left: 0;
	border-top: 0;
}

.plate h3 {
	margin-bottom: .6rem;
}

.plate .anno {
	margin-bottom: .5rem;
}

.plate p {
	font-size: .95rem;
	color: var(--ink-soft);
	max-width: 46ch;
}

.plate--quiet {
	background: transparent;
	border: 1px solid var(--rule-soft);
}

.cards {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: clamp(1.25rem, 2.5vw, 2rem);
}

@media (max-width: 900px) {
	.cards {
		grid-template-columns: 1fr;
	}
}

/* --- the specification list: a drawing's schedule ------------------- */
.specs {
	list-style: none;
	margin: 0;
	padding: 0;
	border-top: 1px solid var(--rule);
}

.specs li {
	display: grid;
	grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
	gap: .25rem 1.25rem;
	padding: .7rem 0;
	border-bottom: 1px solid var(--rule-soft);
	font-size: .92rem;
}

@media (max-width: 560px) {
	.specs li {
		grid-template-columns: 1fr;
	}
}

.spec_k {
	font-family: var(--sans);
	font-size: .68rem;
	font-weight: 600;
	letter-spacing: .15em;
	text-transform: uppercase;
	color: var(--ink-soft);
	padding-top: .2em;
}

.spec_v {
	color: var(--ink);
}

/* --- the questions -------------------------------------------------- */
.qas {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: clamp(1.5rem, 3.5vw, 2.75rem);
}

@media (max-width: 900px) {
	.qas {
		grid-template-columns: 1fr;
	}
}

.qa h3 {
	margin-bottom: .55rem;
	padding-top: .8rem;
	border-top: 1px solid var(--rule);
	max-width: 30ch;
}

.qa p {
	color: var(--ink-soft);
	font-size: .95rem;
	max-width: 52ch;
}

/* --- photographs: a figure on a drawing ----------------------------- */
.shot {
	margin: 0 0 clamp(1.75rem, 4vh, 2.75rem);
	padding: clamp(.5rem, 1.2vw, .85rem);
	background: var(--sheet-lift);
	border: 1px solid var(--rule);
}

/* THE FRAME INSIDE THE FIGURE, and it is structure, not decoration.
   `overflow: hidden` and nothing else, so with no motion layer it changes
   nothing at all: the picture is still 100% wide and the figure is still
   the picture's own height.

   What it buys is a box that HOLDS STILL. THE DEVELOP in the motion layer
   scales every photograph a few per cent and drifts it vertically as the
   section passes, and both of those have to be clipped by something that
   is not moving, or the picture's own edge travels across the sheet, which
   is the one thing NO INVISIBLE WALLS forbids. The overflow lives here and
   never on .shot, because the caption is inside .shot and a drifting
   picture would ride over it. */
.shot_frame {
	overflow: hidden;
}

.shot img {
	width: 100%;
}

/* a stacked column of figures in a split's sidebar, ruled apart by the
   same gap the notes column uses. Two figures, not one: see the pair in
   #portfolio. */
.shots {
	display: grid;
	gap: clamp(1.25rem, 3vh, 2rem);
}

.shot figcaption {
	margin-top: .7rem;
	max-width: 62ch;
}

.shot--wide {
	margin-bottom: clamp(2rem, 5vh, 3.25rem);
}

.split .shot,
.split--wide .shot {
	margin-bottom: 0;
}

.band--figure .shot--wide {
	margin-bottom: clamp(2.5rem, 6vh, 4rem);
}


/* ============================================================
   06 · BY THE NUMBERS — the counter band

   The source template's "Our Proof / By The Numbers" section:
   a 2x2 of counting figures on one side, the claim on the other,
   over a dark ground with a photograph behind it at low opacity.

   IT IS THE ONE DARK SURFACE IN THE THEME, and it is deliberate
   rather than borrowed. Everything else on this page is drawn on
   vellum; the measured claim gets a plate, so it reads as the
   page stopping to show its working. A second dark band would
   make it a colour scheme instead of a moment.

   NO CARDS still holds: the four figures are ruled apart by
   hairlines, the way a drawing rules a schedule, not stacked as
   raised panels.
   ============================================================ */
.numbers {
	position: relative;
	overflow: hidden;
	padding: clamp(3.5rem, 10vh, 7rem) var(--gutter);
	background: var(--ink);
	color: var(--sheet);
	border-top: 1px solid var(--rule);
}

/* decorative, and the only reason it is here is that the source band
   has one: dark foliage under a dark plate, at the opacity where it is
   texture rather than a picture */
.numbers_photo {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: .16;
	filter: saturate(.3) brightness(.72) contrast(.9);
}

.numbers_inner {
	position: relative;
	width: min(100%, var(--sheet-max));
	margin-inline: auto;
	display: grid;
	gap: clamp(2.5rem, 6vw, 4.5rem);
	align-items: center;
}

@media (min-width: 900px) {
	.numbers_inner {
		grid-template-columns: 1fr 1fr;
	}
}

/* stacked, the claim goes FIRST. Side by side the eye takes the heading
   and the figures in one look; stacked, leading with four bare numbers
   asks the reader what they are looking at. */
@media (max-width: 899px) {
	.numbers_say {
		order: -1;
	}
}

.numbers_grid {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	/* the hairlines are the ruling, so the cells own the gap and the
	   borders sit on the two inner edges only — a full box around each
	   figure would be four cards */
	border-top: 1px solid rgb(235 230 218 / .28);
	border-left: 1px solid rgb(235 230 218 / .28);
}

.stat {
	margin: 0;
	padding: clamp(1.25rem, 3vw, 2.1rem);
	border-right: 1px solid rgb(235 230 218 / .28);
	border-bottom: 1px solid rgb(235 230 218 / .28);
}

/* the figure itself: display type, because it is the only place on the
   page where a number is the argument */
.stat_n,
.stat_u {
	font-family: var(--display);
	font-size: clamp(2.6rem, 1.6rem + 3.4vw, 4rem);
	font-weight: 600;
	line-height: 1;
	letter-spacing: -.01em;
	/* TABULAR FIGURES ARE NOT OPTIONAL: a proportional 1 is narrower
	   than a 7, so a counter running to 447 jitters its own width on
	   every frame and shoves the label about with it. */
	font-variant-numeric: tabular-nums;
}

.stat_u {
	font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2rem);
	margin-left: .12em;
}

.stat_k {
	display: block;
	margin-top: .7rem;
	font-family: var(--sans);
	font-size: .82rem;
	line-height: 1.45;
	color: rgb(235 230 218 / .82);
}

.numbers_say h2 {
	margin-bottom: 1.1rem;
}

.numbers_say .anno {
	color: rgb(235 230 218 / .8);
}

.numbers_say p {
	max-width: 44ch;
	color: rgb(235 230 218 / .9);
}

/* on ink, the primary button inverts for the same reason the banner's
   does: an ink fill on an ink ground is not a button */
.numbers .mybutton--primary {
	margin-top: .6rem;
	background: var(--sheet);
	border-color: var(--sheet);
	color: var(--ink);
}

@media (hover: hover) and (pointer: fine) {
	.numbers .mybutton--primary:hover {
		background: transparent;
		border-color: var(--sheet);
		color: var(--sheet);
	}
}

/* below 520px two columns of figures leaves the labels three words
   wide; one column, still ruled */
@media (max-width: 520px) {
	.numbers_grid {
		grid-template-columns: 1fr;
	}
}


/* ============================================================
   THE CONTACT FORM  → POST /api/contact
   Fields are pressed into the sheet; the honeypot is genuinely
   off-screen rather than display:none, because a bot reads the
   computed style too.
   ============================================================ */
.contact_form {
	display: grid;
	gap: 1rem;
	align-content: start;
	padding: clamp(1.25rem, 3vw, 2rem);
	background: var(--sheet-press);
}

.field {
	display: grid;
	gap: .35rem;
}

.field label {
	font-family: var(--sans);
	font-size: .68rem;
	font-weight: 600;
	letter-spacing: .15em;
	text-transform: uppercase;
	color: var(--ink-soft);
}

.field input,
.field textarea {
	width: 100%;
	padding: .65rem .75rem;
	border: 1px solid var(--rule);
	background: var(--sheet-lift);
	color: var(--ink);
	font-family: var(--sans);
	font-size: 1rem;
	line-height: 1.5;
}

.field textarea {
	resize: vertical;
}

.field input:focus-visible,
.field textarea:focus-visible {
	outline: 2px solid var(--ink);
	outline-offset: 1px;
}

.hp_field {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

.contact_form .mybutton {
	justify-self: start;
}

.form_status {
	margin: 0;
	min-height: 1.5em;
	font-size: .88rem;
	color: var(--ink-soft);
}

.form_status.err {
	color: var(--ink);
	font-weight: 600;
}


/* ============================================================
   THE PS CLOSE — the last thing read on every landing page.
   Built only out of parts the sheet already has: the annotation
   voice, the body face, and one rule above it.
   ============================================================ */
.ps_note {
	margin-top: clamp(2.5rem, 6vh, 4rem);
	padding-top: 1.1rem;
	border-top: 1px solid var(--rule);
}

.ps_note .anno {
	margin-bottom: .7rem;
}

.ps_note p {
	max-width: 58ch;
	color: var(--ink-soft);
}


/* ============================================================
   THE COLLECTION BAND  (.alsolike)

   "Like this theme? You might also like:", written to TRAVEL:
   nothing here is named for this page and nothing is an id.

   THE CARDS ARE HOME'S POSTERS, PORTED VERBATIM (the owner: "the
   exact same code we use for the home page slider") from
   css/style.css. NOT ported: the 3-D hover pops and the fan
   poses, both of which need the fan's preserve-3d rig. A change
   to style.css's POSTERS block is a change to every copy.

   The band takes a DIFFERENT GROUND from the rest of the page
   — the pressed sheet — so the previews, which are the only
   other colour on the page besides the runway's photograph,
   read as pinned-up samples rather than as part of the drawing.
   ============================================================ */
.alsolike {
	padding: clamp(3.5rem, 9vh, 7rem) var(--gutter);
	background: var(--sheet-press);
	border-top: 1px solid var(--ink);
}

.alsolike_inner {
	width: min(100%, var(--sheet-max));
	margin-inline: auto;
}

.alsolike h2 {
	margin: .7rem 0 1rem;
	max-width: 20ch;
}

.alsolike_lead {
	margin-bottom: clamp(2rem, 5vh, 3.25rem);
	max-width: 62ch;
	color: var(--ink-soft);
}

.alsolike_grid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: clamp(1.25rem, 2.5vw, 2.25rem);
}

@media (max-width: 860px) {
	.alsolike_grid {
		grid-template-columns: 1fr;
	}
}

.alsolike_more {
	margin-top: clamp(1.75rem, 4vh, 2.75rem);
}

.tt_card {
	display: block;
	text-decoration: none;
	container-type: inline-size;
	transition: translate .3s var(--ease);
}

@media (hover: hover) and (pointer: fine) {
	.tt_card:hover {
		translate: 0 -4px;
	}
}

/* `container-type: inline-size` is what gives the poster its em scale,
   exactly as the home fan's column does; with no padding, 100cqw IS the
   poster's width. */
.tt_art {
	position: relative;
	overflow: hidden;
	aspect-ratio: 700 / 450;
	--u: calc(100cqw / 700);
	font-size: calc(16 * var(--u));
	border: 1px solid var(--rule);
}

.tt_note {
	margin-top: .9rem;
	font-size: .92rem;
	color: var(--ink-soft);
}

.tt_card .titleblock {
	margin-top: .9rem;
}

/* =========================== POSTERS ====================================
   Ported from css/style.css. Everything is em-based so it rides .tt_art's
   --u-derived font-size. */
.slide_poster {
	position: relative;
	width: 100%;
	height: 100%;
	border-radius: inherit;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: .4em;
	text-align: center;
	transform-style: preserve-3d;
}

.poster_bg {
	position: absolute;
	inset: 0;
	overflow: hidden;
	border-radius: inherit;
	transform: translateZ(0);
}

/* Every poster element rests 2px off the card face — never coplanar with
   the opaque .poster_bg at Z 0, so preserve-3d's depth sort never has a tie
   to resolve. Give any NEW poster element the same rest lift.

   The resets are this page's tax, not home's: home has no element rules on
   h3/p at all, so a poster title there renders in the poster's own face.
   Here h1,h2,h3 carry the display face, a weight and a tracking, all of
   which would leak in under .poster_title and restyle other studios' work. */
.poster_title {
	margin: 0;
	position: relative;
	font-size: 3.4em;
	line-height: 1.05;
	letter-spacing: 2px;
	font-weight: 700;
	text-wrap: wrap;
	transform: translateZ(2px);
}

.poster_sub {
	margin: 0;
	position: relative;
	max-width: none;
	font-size: 1.3em;
	line-height: 1.2;
	transform: translateZ(2px);
}

.poster_kicker {
	margin: 0 0 .4em;
	position: relative;
	max-width: none;
	font-size: .8em;
	letter-spacing: .28em;
	text-transform: uppercase;
	font-family: 'Courier New', monospace;
	transform: translateZ(2px);
}

/* ---- Daylight Study: a drawing sheet, ruled ---- */
.poster_daylight .poster_bg {
	background:
		linear-gradient(118deg, rgba(240, 215, 164, .58) 0 20%, rgba(240, 215, 164, 0) 62%),
		linear-gradient(168deg, #eceded 0%, #dfe1dd 100%);
}

.poster_daylight .poster_bg::after {
	content: "";
	position: absolute;
	inset: .65em;
	border: 1px solid rgba(21, 24, 26, .28);
}

.poster_daylight .poster_kicker {
	font-family: "JetBrains Mono", ui-monospace, monospace;
	color: #676c6e;
}

.poster_daylight .poster_title {
	font-family: "JetBrains Mono", ui-monospace, monospace;
	font-size: 2.3em;
	letter-spacing: .01em;
	text-transform: uppercase;
	color: #15181a;
}

.poster_daylight .poster_sub {
	color: #676c6e;
}

/* ---- Paper: neumorphic — embossed ghost headline + soft pill ---- */
.poster_paper .poster_bg {
	background: #e4e5e9;
}

.poster_paper .poster_title {
	font-family: arial, sans-serif;
	font-size: 4.2em;
	color: #e4e5e9;
	text-shadow:
		-0.02em -0.02em 0.04em #ffffff,
		0.02em 0.03em 0.05em #b4b5bd;
}

.poster_paper .poster_pill {
	position: relative;
	width: 38%;
	height: 1.9em;
	border-radius: 2em;
	background: radial-gradient(circle at 50% 60%, #eef0f4, #e0e1e6 70%);
	box-shadow: .35em .35em .8em #c2c3ca, -.35em -.35em .8em #ffffff;
	transform: translateZ(2px);
}

.poster_paper .poster_sub {
	color: #74757d;
}

/* ---- Silver Plate: silver 3D type over the real theme's blue-steel room
   light (--steel #38475B fading to --coal #242C38), gold sold ribbon ---- */
.poster_silver .poster_bg {
	background:
		radial-gradient(circle 16em at 30% 20%, #38475b, transparent 70%),
		#242c38;
}

/* Centre-anchored on the corner, so the label lands on the middle of the
   VISIBLE chord whatever the text measures — this page's ribbon inherits
   its own sans, not the site's Orbitron, so a tuned top/right pair cannot
   be right in both places. Full note: css/style.css, same selector. */
.poster_silver .poster_ribbon {
	position: absolute;
	top: 0;
	right: 0;
	transform: translate(50%, -50%) translate(-4em, 4em) rotate(45deg);
	padding: .3em 3em;
	font-size: .8em;
	letter-spacing: .25em;
	text-transform: uppercase;
	font-weight: 700;
	background: linear-gradient(180deg, #f0c96c, #c89a34);
	color: #241a05;
}

.poster_silver .poster_teapot {
	position: absolute;
	left: -.5em;
	bottom: -.5em;
	width: 4.6em;
	height: auto;
	opacity: .6;
	filter: drop-shadow(0 .1em .2em rgba(0, 0, 0, .5));
}

.poster_silver .poster_teapot--mirror {
	left: auto;
	right: -.5em;
	transform: scaleX(-1);
}

.poster_silver .poster_title {
	font-family: "Cinzel Decorative", Georgia, 'Times New Roman', serif;
	letter-spacing: .06em;
	background: linear-gradient(180deg, #fdfdfd 15%, #8d8d96 55%, #ececf4 90%);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	filter: drop-shadow(0 .04em .05em rgba(0, 0, 0, .85));
}

.poster_silver .poster_sub {
	color: #9d9da6;
}


/* ============================================================
   THE COLOPHON — the sheet's own titleblock strip. It takes the
   BAND'S ground, not the page's, which is the pairing that
   ports with the collection band: a footer with no ground of
   its own lands on whatever the last section left behind.
   ============================================================ */
.colophon {
	display: flex;
	flex-wrap: wrap;
	gap: .5rem 1.5rem;
	justify-content: space-between;
	padding: 1.4rem var(--gutter);
	background: var(--sheet-press);
	border-top: 1px solid var(--rule);
}


/* ============================================================
   THE DRAWN RULES — the static half of THE RULING

   Every hairline on this page that separates one thing from the
   next gets a second line laid exactly on top of it, and the
   motion layer is what draws that second line across. Nothing
   here moves; with no motion layer at all this block is
   invisible, which is the requirement: a hairline at
   --rule over the sheet and an opaque --rule-drawn over the same
   sheet are the same colour (see the tokens), so the overlay is
   the border until something scales it.

   WHY THE OVERLAY EXISTS AT ALL, rather than animating the
   border itself: a border cannot be scaled without scaling its
   box, and the alternative build — sliding a sheet-coloured
   cover off the line — is a hard edge travelling across the page
   with no visible cause, which is the one thing NO INVISIBLE
   WALLS forbids and which this theme forbids twice over. A line
   that grows from its own left end is a line being ruled.

   EACH HOST ALSO DECLARES A NAMED TIMELINE, and that is not
   decoration either: a 1px-tall pseudo reading its own view()
   has an entry phase about a thousandth of the scroll long, so
   it would snap rather than draw. Same trap the schedule rows
   and the registration ticks are written around.
   ============================================================ */
.band,
.alsolike,
.shot,
.specs li,
.qa h3,
.titleblock,
.ps_note {
	position: relative;
}

/* THE GROUND THE RULE IS COMPOSITED OVER IS PART OF THE COLOUR, and it
   is the one thing that can make this block visible when it must not
   be. A hairline at --rule is 26% ink over WHATEVER IS BEHIND IT, so
   its opaque twin is only the same colour on the vellum. Two surfaces
   on this page are not the vellum: a figure sits on the lifted sheet
   and the collection band on the pressed one, and both were ~7 levels
   off before this. Re-declaring the token rather than writing a second
   one is what makes it travel — .tt_card's titleblock is three levels
   inside .alsolike and inherits the right value without knowing it. */
.shot {
	--rule-drawn: color-mix(in srgb, var(--ink) 26%, var(--sheet-lift));
}

.alsolike {
	--rule-drawn: color-mix(in srgb, var(--ink) 26%, var(--sheet-press));
}

.band::before,
.alsolike::before,
.shot::before,
.shot::after,
.specs li::after,
.qa h3::before,
.titleblock::before,
.ps_note::before {
	content: "";
	position: absolute;
	background: var(--rule-drawn);
	pointer-events: none;
}

/* the section rules: full-bleed, sitting on the band's own top border */
.band::before,
.alsolike::before {
	inset: -1px 0 auto;
	height: 1px;
	transform-origin: 0 50%;
}

/* the collection band's rule is the page's one full-strength one, and it
   is the only place the sheet is ruled off in solid ink */
.alsolike::before {
	background: var(--ink);
}

/* a figure is SQUARED UP before the picture arrives: the top edge and
   the left edge, the two a draughtsman sets out first. The other two
   stay as the figure's own border. */
.shot::before {
	inset: -1px -1px auto;
	height: 1px;
	transform-origin: 0 50%;
}

.shot::after {
	inset: -1px auto -1px -1px;
	width: 1px;
	transform-origin: 50% 0;
}

/* the schedule's row rules, on the lighter weight the rows already use */
.specs li::after {
	inset: auto 0 -1px;
	height: 1px;
	background: var(--rule-soft-drawn);
	transform-origin: 0 50%;
}

.qa h3::before,
.titleblock::before,
.ps_note::before {
	inset: -1px 0 auto;
	height: 1px;
	transform-origin: 0 50%;
}


/* ============================================================================
   ============        T H E   M O T I O N   L A Y E R        =================
   ============================================================================

   EVERYTHING scroll-driven on this page is inside this one block, and nothing
   outside it moves except hover transitions and the cover's scroll hint.

   The guard is doubled on purpose: @supports so a browser without scroll
   timelines simply gets the settled page, and prefers-reduced-motion so a
   reader who asked for stillness gets it from the OS without needing the
   site's own stamp. The site's stamp (html.no-runway) is handled AFTER the
   block, because a class cannot be tested inside @supports.

   THE PINS ARE DECLARED HERE AND NOWHERE ELSE. A browser that cannot resolve
   these timelines must never get a 400svh section whose contents do not move.
   ============================================================================ */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {

		/* ---- reading progress ---- */
		.scalebar {
			display: block;
		}

		.scalebar span {
			animation: bar-fill linear both;
			animation-timeline: scroll(root block);
		}

		@keyframes bar-fill {
			to {
				transform: scaleX(1);
			}
		}

		/* ---- the banner's own entrance: a page-LOAD stagger, not a scrub.
		   Nothing has scrolled when this paints, so a scrubbed hero would
		   sit at opacity 0 on arrival. ---- */
		.banner_eyebrow,
		#h1,
		.banner_line,
		.banner .mybutton {
			animation: sheet-rise .9s var(--ease) both;
		}

		#h1 {
			animation-delay: .1s;
		}

		.banner_line {
			animation-delay: .22s;
		}

		.banner .mybutton {
			animation-delay: .34s;
		}

		/* ---- AND ON THE SHEET BUILD THE RISE MOVES UP ONE LEVEL, onto the
		   swap box, because the h1 cannot carry both clocks. Two animations
		   on one element resolve LAST-WINS per property, and the scroll-driven
		   fade declared below is `both`-filled — so before its range opens it
		   holds its own from-state, opacity 1, and overwrites the load rise's
		   opacity 0 at the first paint. The headline would simply be there,
		   un-risen, while everything around it rose.
		   So the wrapper owns the arrival and each child owns its own scrub.
		   The line's .22s delay goes with it; there is nothing to stagger when
		   the second sentence is not on screen yet. ---- */
		html.sheet .banner_swap {
			animation: sheet-rise .9s var(--ease) .1s both;
		}

		/* ============ 01 · THE BANNER ZOOMS OUT INTO A CONTACT SHEET ============
		   200svh track over a 100dvh pin: ONE SCREEN OF SCRUB, which is
		   The owner's number (2026-08-07). The banner reads as one ordinary
		   photograph; the instant you scroll you are held and the picture
		   pulls back to reveal it was the centre cell of a 3x3 sheet of the
		   page's own work. REPLACES THE PLANNED CAROUSEL.

		   THE TRACK IS svh AND THE STAGE IS dvh, as in THE LONG VIEW below
		   and for the same reason: the pin must not move when a mobile URL
		   bar collapses.

		   THIS IS THE PAGE'S SECOND PINNED SECTION AND ITS LAST. The Long
		   View is the locked RUNWAY — 400svh, scroll position is twenty
		   years, and it is argued for. This is a COVER that pins for one
		   screen and then hands over. Not a licence for a third.

		   ---- THE GRID IS LAID OUT AT 300% AND ONLY EVER SCALES DOWN, and
		   that is the whole trick. The obvious build is a stage-sized grid
		   scrubbed from scale 3 to 1, and it is wrong: at rest the centre
		   picture would be rastered at a third of its layout size and blown
		   up to fill the screen, so this page's LCP element — on the one
		   page that publishes what it costs to open — would open soft.
		   Inverted, the centre cell is exactly the stage at scale 1, the
		   picture is laid out at viewport size and rasters crisp, and every
		   frame of the zoom is a DOWNSCALE, which never blurs.

		   `inset: -100%` rather than `translate: -50% -50%` for the
		   centring: it leaves `translate` free, which is what lets the tail
		   parallax below own that property without fighting the zoom for
		   it. Same discipline as the two clocks that used to be here.

		   ZERO GAP, because the gap is what would break the arithmetic: a
		   gutter inside the thirds means the centre cell is no longer the
		   visible frame and the opening photograph no longer fills the
		   screen exactly. The rules between the cells are drawn by
		   .banner_rules, outside the scaled grid, at a true 1px.

		   NO INVISIBLE WALLS IS SATISFIED, and it is worth saying because
		   this is a clip: the stage's edge IS the screen edge, and the
		   motion REVEALS rather than swallows. Nothing on this page is ever
		   seen disappearing behind a boundary with no visible cause. ---- */

		/* ---- EVERY RULE THAT MAKES THE SHEET A SHEET IS GATED ON
		   `html.sheet`, WHICH IS A LOADING DECISION WEARING A LAYOUT
		   DECISION'S CLOTHES. The eight outer pictures are `data-src`,
		   because `loading="lazy"` measurably does NOT defer them — at rest
		   they sit one viewport out, which is inside Chrome's lazy distance
		   threshold, so all seven photographs were on the wire before the
		   reader had scrolled. So the deferral is ours, and the price is
		   that a page whose JavaScript never runs would zoom out onto eight
		   empty cells. The gate is the answer: no class, no grid, and the
		   base state at the top of this file — one full-bleed photograph —
		   is what is left, which is what that reader should have anyway.
		   Stamped in <head>, not from the foot script: this rule sets the
		   document's height, and growing the document after first paint is
		   a layout shift. ---- */
		html.sheet .banner {
			height: 200svh;
			view-timeline: --banner block;
		}

		html.sheet .banner_stage {
			position: sticky;
			top: 0;
			height: 100dvh;
			min-height: 0;
		}

		html.sheet .banner_grid {
			inset: -100%;
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			grid-template-rows: repeat(3, 1fr);
			gap: 0;
			animation: banner-zoom var(--ease) both;
			animation-timeline: --banner;
			animation-range: contain 0% contain 100%;
		}

		html.sheet .bg_cell {
			display: block;
		}

		html.sheet .bg_cell--center {
			position: relative;
			inset: auto;
		}

		html.sheet .banner_rules {
			display: block;
			animation: rules-in linear both;
			animation-timeline: --banner;
			animation-range: contain 55% contain 82%;
		}

		/* 1 → 1/3, arriving at 78% so the last fifth of the pin is STILL:
		   the assembled sheet gets a beat to be looked at before the page
		   moves on, which is the same hold A-101.1 and the runway both
		   take. The ease is on the animation, not the keyframe, so the
		   pull-back decelerates into the sheet instead of stopping dead. */
		@keyframes banner-zoom {

			0% {
				scale: 1;
			}

			78%,
			100% {
				scale: .3333;
			}
		}

		/* THERE IS NO TAIL ON THE GRID ANY MORE (2026-08-07, the owner). The
		   sheet used to sink 4svh and fade to .06 over the last twelfth of
		   the pin, so the section handed over to an almost-empty ink field.
		   The banner now holds the assembled sheet, lit, until the pin
		   releases and it scrolls away like anything else — which is what
		   the copy staying legible on it requires. `banner-parallax` went
		   with it; do not rebuild it. `translate` is free on the grid
		   again, and the zoom still deliberately owns only `scale`. */

		@keyframes rules-in {
			from {
				opacity: 0;
			}

			to {
				opacity: 1;
			}
		}

		/* THE PICTURE STILL DRIFTS, and it is what sells the first second:
		   for as long as the reader has not scrolled, this has to look like
		   an ordinary banner. It survives the zoom because it is on a
		   DIFFERENT ELEMENT — the grid owns `scale` on the wrapper, the
		   drift owns `scale` on the centre image — so the two never contend
		   for the property.

		   THE ALTERNATION IS PERMANENT NOW (2026-08-07). It used to be a
		   placeholder: the source template runs a strict 1.05 → 1 per slide
		   and hides the jump back under the crossfade into the next
		   picture, and this was to take that strict form when the carousel
		   landed. THE CAROUSEL IS CANCELLED — there is one picture here and
		   there will be one picture — so a strict loop would visibly snap
		   and alternating, which reads as one continuous drift, is simply
		   the right answer rather than a stand-in for a better one. */
		.banner_photo {
			animation: banner-drift 12s var(--ease) infinite alternate;
		}

		@keyframes banner-drift {
			from {
				scale: 1.05;
			}

			to {
				scale: 1;
			}
		}

		/* ---- THE COPY STAYS, AND THE SCRIM STAYS WITH IT (2026-08-07,
		   The owner). REPLACES `banner-recede` on .banner_inner + .scroll_hint
		   and `rules-out` on .banner_scrim, which cleared the whole first
		   screen over the opening quarter of the pin so the contact sheet
		   assembled behind bare ink. Do not rebuild either.

		   The two go together and cannot be separated: the eyebrow, the
		   button and the scroll hint are only readable over nine
		   photographs BECAUSE the wash is still under them, and the wash is
		   only worth keeping because there is type on it. The scrim was
		   built as a gradient for exactly this — NO INVISIBLE WALLS — so
		   holding it costs the sheet a veil, never an edge.

		   What moves instead is the SWAP: one sentence takes the other's
		   place in the box below the eyebrow, and it is the only thing on
		   the first screen that changes while the picture pulls back. ---- */

		/* The box the two sentences share. It is a one-cell grid rather than
		   two absolutely-positioned children so the ROW STILL MEASURES
		   ITSELF — the h1 is the taller of the two and sets the height, and
		   nothing below it moves when the shorter line takes over.
		   `align-items: center` is what makes the exchange read as a
		   replacement: the line lands on the headline's optical centre
		   instead of at the top of a box built for two more display lines.
		   The children's own margins go, since the gap between them is now
		   the thing that does not exist; the wrapper carries the space down
		   to the button. */
		html.sheet .banner_swap {
			display: grid;
			align-items: center;
			justify-items: center;
			margin: 0 auto 2.25rem;
		}

		html.sheet .banner_swap>* {
			grid-area: 1 / 1;
			margin: 0;
		}

		/* OUT FIRST, IN SECOND, WITH DAYLIGHT BETWEEN THEM. Cross-fading the
		   two through each other would put two different display faces at
		   half opacity over the same photographs for a quarter of the pin,
		   which is the one moment on this banner where nothing is legible.
		   The gap (30% → 34%) is small enough to read as one gesture and
		   long enough that the box is briefly, deliberately empty.
		   Both are done by 58%, so the sheet finishes assembling under a
		   settled sentence and the hold from 78% is a still frame of the
		   finished thing. Percentages of the pin, like everything else here:
		   change the track length and these do not move. */
		html.sheet #h1 {
			animation: swap-out linear both;
			animation-timeline: --banner;
			animation-range: contain 8% contain 30%;
		}

		html.sheet .banner_line {
			animation: swap-in linear both;
			animation-timeline: --banner;
			animation-range: contain 34% contain 58%;
		}

		/* They travel the same 1rem in the same direction — the headline
		   leaves upward and the line arrives from below it — so the two
		   read as one thing sliding through the frame rather than as two
		   unrelated fades. `translate`, not `transform`: the load rise on
		   the wrapper owns `transform` and these must not contend for it. */
		@keyframes swap-out {
			from {
				opacity: 1;
				translate: 0 0;
			}

			to {
				opacity: 0;
				translate: 0 -1rem;
			}
		}

		@keyframes swap-in {
			from {
				opacity: 0;
				translate: 0 1rem;
			}

			to {
				opacity: 1;
				translate: 0 0;
			}
		}

		@keyframes sheet-rise {
			from {
				opacity: 0;
				transform: translateY(1rem);
			}

			to {
				opacity: 1;
				transform: none;
			}
		}

		.scroll_line {
			animation: hint-sweep 2.6s var(--ease) infinite;
		}

		@keyframes hint-sweep {

			0%,
			10% {
				transform: translateX(-110%);
			}

			90%,
			100% {
				transform: translateX(260%);
			}
		}

		/* ============ THE LONG VIEW ============
		   400svh track, 100dvh stage: 300svh of scrub. Four phases, all in
		   `contain` percentages, so they are relative to the pin and never
		   need retuning for where the section sits on the page.

		     P1  0–14%   YEAR 00. The drawing draws itself over a ghost of
		                 the site photograph.
		     P2 14–38%   YEAR 01. The ground comes up out of the paper and
		                 the two shrubs are planted, tiny, inside the
		                 circles the plan drew for them.
		     P3 38–74%   THE GROWTH. The canopies scale out to meet those
		                 circles; each dashed ring retires as its own
		                 canopy reaches it.
		     P4 74–100%  YEAR 20. The photograph stands alone and the
		                 drawing has gone.

		   THE THREE TIMINGS ARE ONE SYSTEM: statement one at 14% (the first
		   frame the ground starts arriving), the growth finishing at 74%,
		   statement two at 74% (the frame it finishes). Move one and move
		   the other two, and read the h2 with them — the heading and the two
		   statements are one sentence across the scroll.

		   NOTHING IS REVEALED BY A MOVING EDGE. The photograph fades and
		   saturates in place and the ink fades out over it. A hard boundary
		   travelling across live content is exactly what NO INVISIBLE WALLS
		   forbids, and the coincidence of the drawn regions with the
		   photographed ones is what reads as the plan filling itself in. */
		.lv_track {
			height: 400svh;
			view-timeline: --lv block;
		}

		.lv_stage {
			position: sticky;
			top: 0;
			height: 100dvh;
			justify-content: center;
		}

		/* PINNED, THE FRAME TAKES THE LEFTOVER HEIGHT OF THE STAGE and
		   works its own width back out of it. `flex: 1 1 auto` sets the
		   height (the main axis); `align-self: center` stops the cross
		   axis being stretched, so `width: auto` against the kept
		   aspect-ratio resolves to height x 2.051 and the rule hugs the
		   photograph instead of ruling a box round it with paper in the
		   corners. On a short viewport that is the whole difference
		   between a 771px picture in a 1052px frame and a 771px picture,
		   full stop.
		   `max-width` is what a TALL viewport hits: past about 513px of
		   leftover height the picture would be wider than the column, so
		   it is capped there and the slack shows as mat above and below.
		   That slack is real spare screen, not lost width. */
		.lv_frame {
			flex: 1 1 auto;
			min-height: 0;
			align-self: center;
			width: auto;
			max-width: 100%;
		}

		/* ---- EXCEPT ON A NARROW SCREEN, WHERE IT TAKES A RATIO IT CHOOSES
		   RATHER THAN THE STAGE'S, AND WHERE THE PHOTOGRAPH GOES BACK TO
		   COVERING IT. A pinned frame on a phone is TALLER than it is
		   wide; contain inside it would letterbox a 2.05:1 picture into a
		   band under 200px tall with half the screen as mat, and a ring
		   that small is not a drawing any more. So here the frame takes a
		   ratio of its own and the picture is cropped at the flanks to
		   fill it — the one place on this page anything is cropped.

		   1.35 IS DERIVED, NOT CHOSEN. The band the drawing cannot lose
		   is the two big cones and everything between them, x 236–990 of
		   1200, which is 62.8% of the width; a slice keeps
		   frameAspect / 2.051 of it, so the frame may go as square as
		   1.288 before that band starts getting cut. 1.35 keeps 65.8%,
		   the same band with a little air either side. The pair of
		   conifers OUTSIDE it is dropped rather than clipped — see
		   .lv_tree--edge. IT IS NOT THE PHOTOGRAPH'S OWN RATIO ON
		   PURPOSE: 2.05 letterboxes to nothing on a phone. Recrop the
		   photograph or widen the drawing and re-derive BOTH numbers. ---- */
		@media (max-width: 860px) {
			.lv_frame {
				flex: 0 0 auto;
				/* stretched again: here the frame is the full width and the
				   PICTURE is what gets cropped, which is the inverse of the
				   desktop arrangement above */
				align-self: stretch;
				width: auto;
				/* and the desktop rule's max-width comes OFF, or it caps the
				   frame at the column and the negative margins below buy
				   nothing */
				max-width: none;
				/* TWO TOKENS, AND THE SECOND IS DERIVED FROM THE FIRST:
				   --lv-crop is the frame's ratio; --lv-crop-w is the
				   photograph's ratio over it, (1200 / 585) / 1.35 = 1.5195,
				   which is how much wider than the frame the plan has to be
				   drawn for the two to crop identically. THEY MOVE
				   TOGETHER. (It is not one token and a division because
				   calc() will not divide by a var().) */
				--lv-crop: 1.35;
				--lv-crop-w: 1.5195;
				aspect-ratio: var(--lv-crop);
				/* and it takes the gutter back, because at this size every
				   pixel of frame is a pixel of diagram */
				margin-inline: calc(var(--gutter) * -1);
				border-inline: 0;
			}

			/* THE SECOND PAIR THAT MUST AGREE. `cover` and this box are the
			   same instruction written the only two ways each element takes
			   it: the picture fills the frame's height and overflows its
			   width equally either side; the SVG is given a box of the
			   photograph's own ratio, full height, centred — which its
			   default `meet` then fills exactly. Same rectangle, same crop.
			   The frame's `overflow: hidden` does the cutting for both.
			   THE WIDTH IS COMPUTED, NOT AUTO: an <svg> with `width: auto`
			   falls back to SVG's own default sizing, which is 100% of its
			   container, and no aspect-ratio talks it out of that. It has
			   to be told in so many words. */
			.lv_photo {
				object-fit: cover;
			}

			.lv_plan {
				left: 50%;
				right: auto;
				width: calc(100% * var(--lv-crop-w));
				/* the page's `img, svg { max-width: 100% }` reset would clamp
				   that straight back to the frame's width, which is the whole
				   point of overflowing it */
				max-width: none;
				translate: -50%;
			}
		}

		.lv_head,
		.lv_photo,
		.lv_edge,
		.lv_axis,
		.lv_hatch,
		.lv_level,
		.lv_ring,
		.lv_canopy,
		.lv_letters,
		.lv_ink,
		.lv_datum_mark,
		.lv_say {
			animation-timeline: --lv;
			animation-fill-mode: both;
			animation-timing-function: linear;
		}

		.lv_head {
			animation-name: sheet-rise-scrub;
			animation-range: contain 0% contain 7%;
			animation-timing-function: var(--ease);
		}

		@keyframes sheet-rise-scrub {
			from {
				opacity: 0;
				transform: translateY(1rem);
			}

			to {
				opacity: 1;
				transform: none;
			}
		}

		/* P1→P4 · THE GROUND. It starts as a survey photograph seen through
		   tracing paper (drained, lifted, nearly gone) and ends as the
		   place. Both filters list the same three functions in the same
		   order, which is what lets them interpolate at all. */
		/* 14% IS THE SAME NUMBER AS STATEMENT ONE, and it has to be: P1 is
		   the drawing alone, so the ground may not start arriving until
		   the reader is told that year one is what they get. It started
		   at 6% in the first pass and the phase boundary was a fiction. */
		.lv_photo {
			animation-name: ground-arrive;
			animation-range: contain 14% contain 74%;
		}

		@keyframes ground-arrive {
			from {
				opacity: .16;
				filter: saturate(0) brightness(1.3) contrast(.72);
			}

			to {
				opacity: 1;
				filter: saturate(1) brightness(1) contrast(1);
			}
		}

		/* P1 · the hardscape draws itself, then the rest of the furniture
		   letters in behind it */
		.lv_edge {
			animation-name: draw-edge;
			animation-range: contain 0% contain 12%;
		}

		@keyframes draw-edge {
			from {
				stroke-dasharray: 1;
				stroke-dashoffset: 1;
			}

			to {
				stroke-dasharray: 1;
				stroke-dashoffset: 0;
			}
		}

		.lv_axis,
		.lv_level {
			animation-name: ink-in;
			animation-range: contain 2% contain 11%;
		}

		@keyframes ink-in {
			from {
				opacity: 0;
			}
		}

		/* the hatch never comes back to full: it is a fill pattern, not a
		   line, and at 1 it competes with the planting under it */
		.lv_hatch {
			animation-name: hatch-in;
			animation-range: contain 2% contain 11%;
		}

		@keyframes hatch-in {
			from {
				opacity: 0;
			}

			to {
				opacity: .5;
			}
		}

		/* P2 · the shrubs are planted: a dinner-plate-sized thing inside a
		   circle drawn for what it will become. P3 · it grows out to meet
		   it. This is the whole diagram, and it is two keyframes. */
		.lv_canopy {
			animation-name: canopy-grow;
			animation-range: contain 22% contain 74%;
			animation-timing-function: var(--ease);
		}

		@keyframes canopy-grow {
			from {
				opacity: 0;
				transform: scale(.06);
			}

			12% {
				opacity: 1;
			}

			to {
				opacity: 1;
				transform: scale(1);
			}
		}

		/* P3 · the promise retires as it is met. A dashed line is only a
		   promise until the thing arrives at it. */
		/* The ring is drawn EARLY, with the rest of the plan, because it is
		   the promise: at year 00 it has to be on the sheet at full size
		   around nothing at all. Then it holds while the canopy travels
		   out to it, and retires as it is met. */
		.lv_ring {
			animation-name: ring-retire;
			animation-range: contain 3% contain 74%;
		}

		@keyframes ring-retire {
			from {
				opacity: 0;
			}

			11%,
			62% {
				opacity: 1;
			}

			to {
				opacity: 0;
			}
		}

		/* P4 · the drawing has done its job and leaves the place alone. It
		   fades; it is never wiped or slid off. */
		.lv_ink {
			animation-name: plan-retire;
			animation-range: contain 76% contain 92%;
		}

		@keyframes plan-retire {
			from {
				opacity: 1;
			}

			to {
				opacity: 0;
			}
		}

		/* the lettering goes first, because it is the thing that would be
		   read over the photograph rather than seen on it */
		.lv_letters {
			animation-name: ink-in, ink-out;
			animation-range: contain 9% contain 17%, contain 64% contain 76%;
		}

		@keyframes ink-out {
			to {
				opacity: 0;
			}
		}

		/* THE DATUM travels the whole runway: it is the clock, and it is
		   the one thing in the frame that moves horizontally.

		   `left`, not a transform, and that is forced rather than lazy: a
		   percentage translateX resolves against the ELEMENT's own width,
		   which here is one pixel, so a transform cannot express "travel
		   the length of the rule". It is a 1px hairline, so the layout
		   cost of animating `left` is nil. */
		.lv_datum_mark {
			animation-name: datum-travel;
			animation-range: contain 0% contain 100%;
		}

		@keyframes datum-travel {
			from {
				left: 0;
			}

			to {
				left: 100%;
			}
		}

		/* TWO STATEMENTS, both of which arrive and STAY (never a cycle).
		   The first is keyed to the exact frame the ground starts coming
		   up; the second to the frame the canopies finish. */
		.lv_say {
			animation-name: say-in;
			animation-timing-function: var(--ease);
		}

		.lv_say--one {
			animation-range: contain 14% contain 22%;
		}

		.lv_say--two {
			animation-range: contain 74% contain 82%;
		}

		@keyframes say-in {
			from {
				opacity: 0;
				transform: translateY(.6rem);
			}

			to {
				opacity: 1;
				transform: none;
			}
		}

		/* ---- the ordinary bands: ONE ARRIVAL EACH, on the section, because
		   a section arrives as one thing. Four separate ENTRANCES inside a
		   band is the page contradicting its own argument in motion.

		   That rule stands, and everything below it is the other half of it
		   (2026-08-06, the owner asked for more motion on this page). The band
		   arrives as one sheet; what happens INSIDE it afterwards is not a
		   second entrance, it is the drawing being worked on: a photograph
		   develops, a schedule is lettered in line by line, registration
		   ticks are struck into the corners of a note. Each of those is an
		   INSTRUMENT acting on its own range, well after the sheet itself
		   has landed. Nothing below re-announces a section.

		   The range is `entry`, which for a view() timeline spans exactly
		   the element's OWN height — so a short band and a tall one both
		   finish arriving a fifth of the way in, and neither needs a
		   per-section number. No duration: a timeline-driven animation is
		   progress-driven and ignores one. ---- */
		.band_inner,
		.alsolike_inner {
			animation-name: sheet-lay;
			animation-fill-mode: both;
			animation-timing-function: var(--ease);
			animation-timeline: view();
			animation-range: entry 0% entry 24%;
		}

		/* NOT sheet-rise-scrub, which this used to borrow. That keyframe
		   belongs to the pinned diagram, where 1rem of travel is a
		   caption settling inside a frame that is already on screen and
		   an inch is plenty. A whole section arriving off the bottom of
		   the window has a screen and a half of travel to be seen in,
		   and at 1rem it read as nothing at all — the section was simply
		   there. Sharing one keyframe between the two meant either the
		   diagram's caption lurched or the sections did not move; they
		   are two gestures and they get two keyframes.

		   4.5rem, up from the 2.75 of the first re-scale (2026-08-07,
		   The owner asked for the whole set below the diagram to read more
		   prominently, on the model of /architecture-firm-websites/).
		   That page throws whole blocks 200px and 15vw; this one cannot
		   — it is one flat sheet with no depth to move things through —
		   so the prominence is bought in two ways instead: every
		   amplitude in this block goes up by roughly half, and the
		   things that had no instrument at all get one. See THE HEAD IS
		   LETTERED at the foot of the block for the other half. */
		@keyframes sheet-lay {
			from {
				opacity: 0;
				transform: translateY(4.5rem);
			}

			to {
				opacity: 1;
				transform: none;
			}
		}

		/* ============ 02b · THE RULING ============
		   The one instrument that plays on every band, and the page's
		   own signature: a drawing is a set of areas ruled apart, so
		   every rule that separates them is DRAWN rather than found
		   already there. Section rules, figure edges, schedule rows,
		   the line over each question, every titleblock and the PS.

		   It is /architecture-firm-websites/'s `datum-strike` recycled,
		   which is the owner's own move — but on that page it is one line
		   under one heading, and here it is the page's grammar: nothing
		   on this sheet is bounded by anything except a ruled line, so
		   ruling them in is the sheet drawing itself.

		   THE STATIC HALF IS OUTSIDE THIS BLOCK (see THE DRAWN RULES).
		   All that happens here is that the line is scaled from its own
		   left end, or its own top end, and released. Killing the
		   animation leaves the line at full length, which is the border
		   the page has always had. */
		.band,
		.alsolike {
			view-timeline: --sheet block;
		}

		.band::before,
		.alsolike::before {
			animation: rule-draw-x linear both;
			animation-timeline: --sheet;
			animation-timing-function: var(--ease);
			/* the band's own entry phase is as long as the band, so a
			   fifth of it is a comfortable screenful of drawing with the
			   ruled edge still near the bottom of the window */
			animation-range: entry 1% entry 19%;
		}

		@keyframes rule-draw-x {
			from {
				scale: 0 1;
			}

			to {
				scale: 1 1;
			}
		}

		@keyframes rule-draw-y {
			from {
				scale: 1 0;
			}

			to {
				scale: 1 1;
			}
		}

		/* THE LATE VARIANT — the same stroke, held back inside its own
		   range so a rule that shares a range with the thing it belongs
		   to is drawn AFTER it rather than under it. It is what lets the
		   schedule rows and the questions keep one stated range each
		   instead of two parallel ladders of nth-child stops. */
		@keyframes rule-draw-x-late {

			0%,
			34% {
				scale: 0 1;
			}

			100% {
				scale: 1 1;
			}
		}

		/* the titleblock and the PS close: a sheet signs itself off, so
		   the signing line is ruled in as it arrives. Stated in `cover`
		   because both are short strips and neither has an entry phase
		   worth reading percentages off. */
		.titleblock {
			view-timeline: --tb block;
		}

		.titleblock::before {
			animation: rule-draw-x linear both;
			animation-timeline: --tb;
			animation-timing-function: var(--ease);
			animation-range: cover 14% cover 38%;
		}

		.ps_note {
			view-timeline: --ps block;
		}

		.ps_note::before {
			animation: rule-draw-x linear both;
			animation-timeline: --ps;
			animation-timing-function: var(--ease);
			animation-range: cover 6% cover 28%;
		}

		/* ============ 03 · THE DEVELOP ============
		   EVERY PHOTOGRAPH ON THIS PAGE ARRIVES THE WAY THE RUNWAY'S GROUND
		   DOES, and that is the point of it: #long-view spends three screens
		   turning a drained survey photograph into the place, so a picture
		   further down the page that simply faded in would be arriving in a
		   language the page had already taught the reader to read as
		   something else. `develop` is `ground-arrive` written for a figure
		   rather than for a runway — the same three filter functions in the
		   same order, which is what lets them interpolate at all.

		   IT STARTS WHERE THE RUNWAY'S GROUND STARTS NOW (2026-08-07): .32
		   against the .45 it shipped with, on the same drained filter the
		   diagram opens on. The gentler version was pitched at a picture
		   "only a few hundred pixels of the screen", but these figures are
		   the full width of the sheet and the range is a screen and a half —
		   at .45 the picture was most of the way developed before it was
		   properly on screen, so the page's own signature gesture was the
		   one nobody saw. It does NOT go all the way to the runway's .16:
		   that one has three screens of pin to resolve in, this has a
		   section passing the window.

		   TWO CLOCKS AGAIN, on two properties: the develop takes opacity and
		   filter, the drift takes `translate`. Same trick as the banner.

		   THE DRIFT IS THE BANNER'S PARALLAX AT FIGURE SCALE. The picture is
		   scaled 1.12 so it hangs over .shot_frame top and bottom, then
		   slides ±4% against that box as the section passes: it never runs
		   out of picture, and .shot_frame's `overflow: hidden` is a fixed
		   frame rather than a travelling edge.

		   THE TWO NUMBERS ARE A PAIR AND THE PERCENTAGES ARE NOT IN THE
		   SAME UNITS, which is the trap in checking them by eye. The scale
		   buys (S - 1) / 2 of the FRAME's height at each edge; the drift
		   spends D of the IMAGE's height, and the image is S times the
		   frame. So the condition is D x S <= (S - 1) / 2 — here
		   4% x 1.12 = 4.5% against 6%, a third in hand. Raising either
		   number means re-running that line (2026-08-07: 1.06 / ±2.5%
		   together, then 1.12 / ±4%). Break it and the picture's own edge
		   crosses the frame, which is NO INVISIBLE WALLS broken by the one
		   element that exists to prevent it. */
		/* BOTH CLOCKS READ --shot, AND AN ANONYMOUS view() HERE IS A DEAD
		   ANIMATION (2026-08-07, measured: the develop had never run on a
		   single photograph on this page). `view()` resolves against the
		   nearest ANCESTOR SCROLL CONTAINER, and .shot_frame is one — its
		   `overflow: hidden` is what makes it a frame, and it is also what
		   makes it a scrollport. That port never scrolls, so the picture's
		   progress was pinned at whatever the frame's fixed geometry
		   resolved to and every photograph simply painted finished.
		   Sampled at six scroll positions from a screen below the fold to
		   three screens above it: opacity 1 and `saturate(1)` at all six.

		   The file used to say the anonymous timeline was "very nearly the
		   same box and was already correct". It was neither. .shot's named
		   timeline is declared on the figure, whose own nearest scroll
		   container is the document, which is the box the ranges below
		   were always written against — the same one the figure's two
		   drawn edges and its caption already read. */
		.shot img {
			scale: 1.12;
			animation: develop linear both, shot-drift linear both;
			animation-timeline: --shot, --shot;
			animation-range: entry 12% cover 46%, cover 0% cover 100%;
		}

		/* AND THE FIGURE IS SET OUT BEFORE THE PICTURE IS IN IT. Two
		   edges, top and left, in the order a drawing gets squared up,
		   both finished before the develop is halfway — so the sequence
		   the reader sees is frame, then photograph, then the note under
		   it, which is the order the thing was made in.

		   The three read the FIGURE's named timeline. The picture keeps
		   its own anonymous view(), which is very nearly the same box
		   and was already correct; the caption cannot use one, because a
		   two-line note's entry phase is 40px of scroll. */
		.shot {
			view-timeline: --shot block;
		}

		.shot::before {
			animation: rule-draw-x linear both;
			animation-timeline: --shot;
			animation-timing-function: var(--ease);
			animation-range: cover 1% cover 16%;
		}

		.shot::after {
			animation: rule-draw-y linear both;
			animation-timeline: --shot;
			animation-timing-function: var(--ease);
			animation-range: cover 5% cover 20%;
		}

		/* the caption is the drawing's note, so it is lettered in from
		   the left, and only once the picture it is about has finished
		   developing (`develop` ends at cover 46%) */
		.shot figcaption {
			animation: schedule-in linear both;
			animation-timeline: --shot;
			animation-timing-function: var(--ease);
			animation-range: cover 46% cover 64%;
		}

		@keyframes develop {
			from {
				opacity: .32;
				filter: saturate(.04) brightness(1.3) contrast(.72);
			}

			to {
				opacity: 1;
				filter: saturate(1) brightness(1) contrast(1);
			}
		}

		@keyframes shot-drift {
			from {
				translate: 0 -4%;
			}

			to {
				translate: 0 4%;
			}
		}

		/* ============ 04 · THE SCHEDULE ============
		   #fast's spec list is a drawing's schedule, so it is FILLED IN, one
		   line at a time, top to bottom.

		   ONE NAMED TIMELINE ON THE LIST, eight explicit stops off it, and
		   that is deliberate rather than eight anonymous view() timelines:
		   each row would then read its own box, and a row is 40px tall, so
		   its own entry phase is a few per cent of the scroll and every line
		   would snap rather than arrive. Read off the LIST's timeline they
		   are one gesture with a stated order, which is what a schedule
		   being written out looks like. Same reasoning as the collection
		   band's head below. */
		.specs {
			view-timeline: --schedule block;
		}

		.specs li {
			animation: schedule-in linear both;
			animation-timeline: --schedule;
			animation-timing-function: var(--ease);
		}

		/* AND THE ROW IS RULED OFF AS IT IS WRITTEN. The pseudo shares
		   its row's range exactly and gets its delay from the LATE
		   keyframe instead, which is why there is one ladder of stops
		   here and not two: pair the selectors and the rule can never
		   drift out of step with the line it belongs to. */
		.specs li::after {
			animation: rule-draw-x-late linear both;
			animation-timeline: --schedule;
			animation-timing-function: var(--ease);
		}

		/* STATED IN `cover`, AND `entry` IS THE BUG IT REPLACES.
		   `entry` is exactly as long as the subject, and the schedule is
		   a ~380px list in a 900px window — so the whole eight-row
		   ladder ran out inside the first 310px of the list entering,
		   which is to say the last row finished writing itself while it
		   was still below the fold. Nobody ever saw rows five to eight
		   arrive. `cover` is the subject PLUS the window, so the same
		   ladder is spread over the whole passage the list is readable
		   in. Measured at 1440x900: row 1 starts with the list's top at
		   y=746 and row 8 finishes with it at y=106, both on screen.
		   The same reasoning applies to any subject shorter than the
		   viewport — see the form at the foot of this block. */
		.specs li,
		.specs li::after {
			animation-range: cover 10% cover 32%;
		}

		.specs li:nth-child(2),
		.specs li:nth-child(2)::after {
			animation-range: cover 14% cover 36%;
		}

		.specs li:nth-child(3),
		.specs li:nth-child(3)::after {
			animation-range: cover 18% cover 40%;
		}

		.specs li:nth-child(4),
		.specs li:nth-child(4)::after {
			animation-range: cover 22% cover 44%;
		}

		.specs li:nth-child(5),
		.specs li:nth-child(5)::after {
			animation-range: cover 26% cover 48%;
		}

		.specs li:nth-child(6),
		.specs li:nth-child(6)::after {
			animation-range: cover 30% cover 52%;
		}

		.specs li:nth-child(7),
		.specs li:nth-child(7)::after {
			animation-range: cover 34% cover 56%;
		}

		.specs li:nth-child(8),
		.specs li:nth-child(8)::after {
			animation-range: cover 38% cover 60%;
		}

		/* 1.8rem, up from the 1.1 of the first re-scale and the .7 this
		   shipped with. It is the page's general-purpose LETTERING
		   gesture — schedule rows, figure captions, the counter band's
		   figures, and since 2026-08-07 the notes and the band heads
		   too — so it is the amplitude that sets the page's overall
		   volume, and it is the one worth being generous with.

		   IT IS BOUNDED BY --gutter, like every lateral on this page, and
		   the clamp is what does the bounding — a flat 1.8rem is 28.8px
		   against a 20px gutter at 390px wide, which is the questions'
		   own 2px-overflow bug written eight times larger. The page's
		   only horizontal bound is the window, so a lateral that beats
		   the gutter is a scrollbar. Checked against gutter
		   `clamp(1.25rem, 5vw, 4rem)`: 16 vs 20 at 390px, 21.8 vs 32 at
		   640px, 28.8 vs 45 at 900px, 28.8 vs 64 from 1280px up. */
		@keyframes schedule-in {
			from {
				opacity: 0;
				translate: calc(-1 * clamp(1rem, 3.4vw, 1.8rem)) 0;
			}

			to {
				opacity: 1;
				translate: none;
			}
		}

		/* ============ 05 · THE REGISTRATION ============
		   A plate is a note pressed into the sheet, so it presses in, and
		   then a beat later its two registration ticks are STRUCK into the
		   corners. The ticks are the page's own mark and they are the reason
		   a plate reads as part of a drawing instead of as a UI card, so
		   they are the thing worth the flourish.

		   The pseudos read the plate's NAMED timeline, not their own view():
		   a 9px corner mark's own entry phase is about a thousandth of the
		   scroll, which is the same trap the schedule rows are in above. */
		.plate {
			/* the NAME is declared for the pseudos below; the plate's own
			   animation reads the anonymous view() of the same box, which is
			   the identical timeline and avoids an element referencing a
			   name it declares on itself */
			view-timeline: --plate block;
			animation: plate-press linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: entry 8% cover 38%;
		}

		.cards .plate:nth-child(2),
		.notes .plate:nth-child(2) {
			animation-range: entry 14% cover 44%;
		}

		.cards .plate:nth-child(3) {
			animation-range: entry 20% cover 50%;
		}

		/* 3rem, up from 1.9 (2026-08-07). A plate is a note pressed into
		   the sheet and it is one of the biggest boxes on the page, so
		   it can carry the travel; the counter band's head and the one
		   action per band borrow this keyframe and go up with it. */
		@keyframes plate-press {
			from {
				opacity: 0;
				translate: 0 3rem;
			}

			to {
				opacity: 1;
				translate: none;
			}
		}

		.plate::before,
		.plate::after {
			animation: tick-strike linear both;
			animation-timeline: --plate;
			animation-timing-function: var(--ease);
			animation-range: entry 26% cover 46%;
		}

		/* each tick grows out of its own corner of the sheet, which is where
		   a draughtsman's hand would have started it */
		.plate::before {
			transform-origin: 0 0;
		}

		.plate::after {
			transform-origin: 100% 100%;
		}

		@keyframes tick-strike {
			from {
				opacity: 0;
				scale: .15;
			}

			to {
				opacity: 1;
				scale: 1;
			}
		}

		/* AND THE NOTE IS LETTERED ONTO THE PRESSED GROUND (2026-08-07).
		   The third beat, between the press and the ticks, and the reason
		   a plate now reads as being MADE rather than as arriving: the
		   box presses in, its heading is written across it, and the
		   registration marks are struck last. Nine plates on the page
		   take it.

		   Only the heading and its label, never the paragraph. The body
		   of a note is what the reader is here to read, and copy that
		   slides under its own heading is the page charging for the
		   gesture twice. Off the plate's NAMED timeline for the same
		   reason the ticks are: a two-line h3 has no entry phase. */
		.plate>.anno,
		.plate h3 {
			animation: schedule-in linear both;
			animation-timeline: --plate;
			animation-timing-function: var(--ease);
			animation-range: entry 16% cover 42%;
		}

		/* ============ 06 · THE QUESTIONS ============
		   The two columns arrive from opposite sides, toward each other.
		   Borrowed from /architecture-firm-websites/, where the same section
		   slides in from ±15vw; here it is 1.4rem, because that page is a
		   wall of pinned sheets and this one is a single sheet with a rule
		   ruled across it. The gesture travels, the amplitude does not. */
		.qa {
			--side: -1;
			/* the NAME is for the rule below; the question itself reads
			   the anonymous view() of the same box, exactly as .plate
			   does, so nothing references a name it declares on itself */
			view-timeline: --ask block;
			animation: ask-in linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: entry 10% cover 38%;
		}

		.qa:nth-child(even) {
			--side: 1;
		}

		/* AND EACH QUESTION IS RULED OFF FIRST. The line over an h3 is
		   what makes the six read as a schedule of questions rather than
		   as six paragraphs, so it is the part worth drawing. Same range
		   as the question, held back by the LATE keyframe. */
		.qa h3::before {
			animation: rule-draw-x-late linear both;
			animation-timeline: --ask;
			animation-timing-function: var(--ease);
			animation-range: entry 10% cover 38%;
		}

		/* 3.25rem at the top (2026-08-07), against the 2.25 of the first
		   re-scale, the 1.4 this shipped with, and the 15vw the same
		   section uses on /architecture-firm-websites/. That page is a
		   wall of pinned sheets and can throw a block most of a screen
		   sideways; this one is a single sheet, and the ceiling here is
		   set by the gutter — nothing may ever be seen sliding out from
		   under the window edge.

		   THE CEILING IS 3.4vw AND THAT IS WHAT MAKES IT SAFE, not the
		   3.25rem cap: the gutter is 5vw through the whole middle of the
		   range, so a lateral stated at a smaller vw multiple is under it
		   by construction at every width in between. The cap is only what
		   stops it growing past the 4rem gutter on a wide screen (52px
		   against 64px), and the 1rem floor only bites below 659px, where
		   the lateral is switched off for one column anyway. */
		@keyframes ask-in {
			from {
				opacity: 0;
				translate: calc(var(--side) * clamp(1rem, 3.4vw, 3.25rem)) 0;
			}

			to {
				opacity: 1;
				translate: none;
			}
		}

		/* AND BELOW 900px THERE ARE NO OPPOSING SIDES TO ARRIVE FROM: .qas
		   collapses to one column there, so both halves of the gesture would
		   be the same half. It also measured 2px of horizontal overflow at
		   390px wide, which is the whole point of the rule below the fix: a
		   1.4rem lateral on a 350px column is 22px of a page whose only
		   bound is `overflow-x: clip`. The same arrival, played vertically. */
		@media (max-width: 900px) {
			.qa {
				animation-name: plate-press;
			}
		}

		/* ============ 07 · THE COUNTER BAND ============
		   The page's one dark surface had no motion at all, which made it
		   the one section that simply appeared. It gets the quietest set on
		   the page, because the figures inside it are already counting.

		   THE STATS' RANGE ENDS EARLY ON PURPOSE. The count-up is fired by
		   an IntersectionObserver at 60% visibility (see index.html), so an
		   arrival that finished any later would run the counter behind an
		   element still at opacity 0 and the reader would meet the settled
		   number. Everything here is done inside the band's first half
		   screen of travel. */
		.numbers {
			view-timeline: --proof block;
		}

		/* the same 0.9x drift the banner and the figures run, at the depth a
		   texture at .16 opacity can carry */
		.numbers_photo {
			scale: 1.16;
			animation: numbers-drift linear both, numbers-develop linear both;
			animation-timeline: --proof, --proof;
			animation-range: cover 0% cover 100%, cover 2% cover 52%;
		}

		@keyframes numbers-drift {
			from {
				translate: 0 -5.5%;
			}

			to {
				translate: 0 5.5%;
			}
		}

		/* THE DEVELOP, PLAYED ON THE DARK BAND'S TEXTURE. Every other
		   photograph on the page resolves out of a drained state as you
		   reach it, and this one is still a photograph even at .16 —
		   arriving already finished made the one section that inverts
		   also the one that ignored the page's own language.

		   TWO CLOCKS, TWO PROPERTIES, the same as the banner and the
		   figures: the drift owns `translate`, the develop owns opacity
		   and filter. It lands on .numbers_photo's BASE values, so
		   killing the animation leaves the texture exactly as the
		   unanimated page has always drawn it. */
		@keyframes numbers-develop {
			from {
				opacity: .05;
				filter: saturate(0) brightness(.5) contrast(.8);
			}

			to {
				opacity: .16;
				filter: saturate(.3) brightness(.72) contrast(.9);
			}
		}

		.numbers_say>* {
			animation: plate-press linear both;
			animation-timeline: --proof;
			animation-timing-function: var(--ease);
			animation-range: entry 6% entry 34%;
		}

		.stat {
			animation: schedule-in linear both;
			animation-timeline: --proof;
			animation-timing-function: var(--ease);
			animation-range: entry 12% entry 40%;
		}

		.stat:nth-child(2) {
			animation-range: entry 16% entry 44%;
		}

		.stat:nth-child(3) {
			animation-range: entry 20% entry 48%;
		}

		.stat:nth-child(4) {
			animation-range: entry 24% entry 52%;
		}

		/* ============ 08 · THE COLLECTION BAND ============
		   The three theme cards are pinned up in order after the head has
		   landed. /architecture-firm-websites/ opens them like a triptych in
		   3-D, raked and turning flat; THIS PAGE HAS NO DEPTH IN IT
		   ANYWHERE — nothing is raised and nothing casts a shadow — so the
		   same beat is played flat, as three sheets laid onto the vellum in
		   sequence.

		   `transform`, NOT `translate`: .tt_card owns `translate` for its
		   hover lift, and a running animation beats a transition, so a
		   translate keyframe here would silently kill the hover. The two
		   properties compose, so written this way both work. */
		.tt_card {
			animation: card-layon linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 6% cover 34%;
		}

		.tt_card:nth-child(2) {
			animation-range: cover 10% cover 38%;
		}

		.tt_card:nth-child(3) {
			animation-range: cover 14% cover 42%;
		}

		@keyframes card-layon {
			from {
				opacity: 0;
				transform: translateY(5rem);
			}

			to {
				opacity: 1;
				transform: none;
			}
		}

		/* ============ 09 · THE ACTION ============
		   One action per band, and it was the only thing in a band that
		   never moved — so on a long section the reader arrived at a
		   button that was simply sitting there while everything above it
		   had been drawn. It presses in like a plate, because that is
		   what it is: a ruled box on the sheet.

		   Its own view() is safe here and nowhere else in this file: the
		   strip is a couple of lines tall, so the range is stated in
		   `cover`, where a short box and a tall one mean the same
		   thing. */
		.card_actions {
			animation: plate-press linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 12% cover 34%;
		}

		/* ============ 10 · THE FORM ============
		   The last band on the page, and the fields are pressed into the
		   sheet, so they are lettered in the way the schedule is: top to
		   bottom, one stated order, off the FORM's timeline rather than
		   each field's own — a 70px field has no entry phase to read. */
		.contact_form {
			view-timeline: --form block;
		}

		.contact_form .field,
		.contact_form .mybutton {
			animation: schedule-in linear both;
			animation-timeline: --form;
			animation-timing-function: var(--ease);
			/* `cover`, for the schedule's reason exactly: the form is a
			   ~420px box in a 900px window, and an `entry` ladder on it
			   would have the submit button settled while it was still
			   under the fold. nth-of-TYPE, not nth-child: the honeypot
			   is a fourth div in here and must never be counted. */
			animation-range: cover 14% cover 38%;
		}

		.contact_form .field:nth-of-type(2) {
			animation-range: cover 20% cover 44%;
		}

		.contact_form .field:nth-of-type(3) {
			animation-range: cover 26% cover 50%;
		}

		.contact_form .mybutton {
			animation-range: cover 32% cover 56%;
		}

		/* ============ 11 · THE HEAD IS LETTERED ============
		   2026-08-07, the owner: the set below the diagram is to read more
		   prominently and on more elements, on the model of
		   /architecture-firm-websites/. Half of that is the amplitudes
		   above; this is the other half, and it is the biggest type on
		   the page finally moving.

		   ONE ARRIVAL PER BAND STILL HOLDS, and this is not a second
		   one. The band lays itself down as one sheet (`sheet-lay`) and
		   then its head is WRITTEN ONTO IT — sheet number, headline,
		   opening statement, in that order, the same lettering gesture
		   every note and every schedule row on this page already uses.
		   It is the arrival articulated, not repeated, and it is exactly
		   what /architecture-firm-websites/ does to its own collection
		   head: one stated order rather than four things each fading up
		   on its own account.

		   THE SHEET LAYS VERTICALLY AND THE HEAD LETTERS LATERALLY, and
		   that is the whole reason the two beats are legible on top of
		   each other. Two vertical rises composing on a parent and its
		   child is one rise of unpredictable size; a rise crossed with a
		   lateral is a sheet being put down and then written on. They
		   also drive different properties — `sheet-lay` owns `transform`,
		   these own `translate` — so neither can overwrite the other.

		   EACH READS ITS OWN view() IN `cover`, not the band's timeline.
		   The band's `entry` phase is as long as the band, and the bands
		   here run from one screen to nearly four, so a head stop stated
		   against it would mean something different in every section —
		   the per-section-number trap the schedule is written around. In
		   `cover` a short box and a tall one mean the same thing, which
		   is what makes one set of stops correct in all seven. */
		.band_inner>.anno:first-child,
		.alsolike_eyebrow {
			animation: head-letter linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 5% cover 26%;
		}

		.band h2,
		.alsolike h2 {
			animation: head-letter linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 8% cover 30%;
		}

		/* the opening statement under the headline. `.band_inner > p` and
		   not `.band p`: the direct child is the band's LEDE, and every
		   other paragraph on the page is body copy inside a split, which
		   stays still on purpose — a page where every paragraph slides is
		   a page nobody can read while it is moving. #contact states its
		   head inside the split (the form is the other column), so its
		   copy column is named explicitly. */
		.lede,
		.band_inner>p:not(.anno),
		.band--contact .split>div>p,
		.alsolike_lead {
			animation: head-letter linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 12% cover 34%;
		}

		/* THE HEADLINE'S OWN AMPLITUDE, and it is the page's largest
		   lateral because it is the page's largest type. Bounded exactly
		   the way `ask-in` is and for the same reason: 3.4vw is under the
		   gutter's 5vw by construction everywhere in the middle of the
		   range, the 3.25rem cap keeps it under the 4rem gutter on a wide
		   screen, and the 1rem floor keeps it under the 1.25rem gutter on
		   a phone. Nothing is ever seen sliding out from under the window
		   edge — this page has no horizontal bound but the window. */
		@keyframes head-letter {
			from {
				opacity: 0;
				translate: calc(-1 * clamp(1rem, 3.4vw, 3.25rem)) 0;
			}

			to {
				opacity: 1;
				translate: none;
			}
		}

		/* ============ 12 · THE NOTES ============
		   The two blocks the typography already sets apart from the prose
		   around them: the AEO definition, set a step up in the display
		   face, and the `.anno--note` asides that introduce a turn in the
		   argument. They are notes written onto the sheet, so they are
		   lettered on like every other note here.

		   This is where the "more elements" stops, deliberately. Every
		   named block on the page now has an instrument and every plain
		   paragraph still holds still, which is the line worth keeping:
		   the page argues that the work should be the loudest thing on
		   it, and prose that moves while it is being read is the page
		   competing with its own copy.

		   BOTH ARE SCOPED TO `.band` AND THAT IS NOT TIDINESS. The
		   counter band is a `.numbers` section, not a `.band`, and its
		   own `.anno--note` already rides `.numbers_say > *` at the same
		   specificity — an unscoped selector here would sit later in the
		   cascade and quietly restage the one section the file calls the
		   quietest on the page. */
		.band .define,
		.band .anno--note {
			animation: schedule-in linear both;
			animation-timeline: view();
			animation-timing-function: var(--ease);
			animation-range: cover 10% cover 32%;
		}

		/* ============ 13 · THE SHEET SIGNS ITSELF OFF ============
		   THE RULING already draws the line over the titleblock and over
		   the PS close; what sat under those lines was the one thing on
		   the page that arrived finished. Now the rule is ruled and then
		   the sheet is signed under it, off each host's own named
		   timeline so the two halves cannot drift apart.

		   `.band_inner >` scopes the titleblock to the page's own two,
		   and away from the three inside the collection band's poster
		   cards — those ride `.tt_card`'s card-layon as part of the card
		   and must not also letter themselves in — and away from the
		   masthead's index block, which lives in a fixed panel where a
		   view timeline never resolves at all. */
		.band_inner>.titleblock>.anno {
			animation: schedule-in linear both;
			animation-timeline: --tb;
			animation-timing-function: var(--ease);
			animation-range: cover 22% cover 44%;
		}

		.ps_note>.anno {
			animation: schedule-in linear both;
			animation-timeline: --ps;
			animation-timing-function: var(--ease);
			animation-range: cover 14% cover 36%;
		}

		.ps_note>p:not(.anno) {
			animation: schedule-in linear both;
			animation-timeline: --ps;
			animation-timing-function: var(--ease);
			animation-range: cover 20% cover 42%;
		}
	}
}


/* ============================================================
   THE html.no-runway TWIN

   A class cannot be tested inside @supports, so the site's own
   stamp is handled here instead. It is stamped pre-paint for
   reduced motion AND for the 'standard' / 'deck' experiences,
   and it is a strict superset of html.reduce-motion.

   The experience selector is not on this page, but a choice
   stored anywhere else on the site still stamps the flag, so
   this block is what stops the runway rendering as four screens
   of nothing.

   A COLLAPSED RUNWAY MUST HAVE ITS SCRUB KILLED IN THE SAME
   RULE: animation-fill-mode: both with no resolvable timeline
   holds the FROM state, which here is opacity 0 on almost
   everything in the frame.
   ============================================================ */
html.no-runway .lv_track {
	height: auto;
}

html.no-runway .lv_stage {
	position: static;
	height: auto;
}

html.no-runway .lv_frame {
	flex: none;
	aspect-ratio: 1200 / 585;
	max-width: 100%;
	/* unpinned there is no stage to fit, so the frame goes back to the
	   photograph's own ratio and nothing is cropped at all.
	   THESE TWO ARE NOT OPTIONAL. The pinned rule works its width back out
	   of the height flex gave it (align-self: center + width: auto), and
	   with `flex: none` there is no such height — the frame resolved to
	   4x2 pixels. It has to be stretched to the column again and take its
	   height from the ratio, the way it does with no motion layer at all. */
	align-self: stretch;
	width: auto;
}

/* AND THE NARROW-SCREEN CROP COMES OFF WITH IT, both halves of it. The
   motion layer's 860px block is guarded on @supports and
   prefers-reduced-motion, NOT on this class, so a no-runway reader on a
   phone was getting the frame at the photograph's own ratio (above) with
   the picture still set to cover and the plan still drawn 1.52x wider
   than it — the two pairs disagreeing, which is the one thing that slides
   the drawing off its ground. Measured 3.15:1 against a 2.05:1 frame
   before this existed. */
html.no-runway .lv_photo {
	object-fit: contain;
}

html.no-runway .lv_plan {
	inset: 0;
	width: 100%;
	max-width: 100%;
	translate: none;
}

html.no-runway .scalebar {
	display: none;
}

html.no-runway .banner_eyebrow,
html.no-runway #h1,
html.no-runway .banner_line,
html.no-runway .banner .mybutton,
html.no-runway .banner_photo,
html.no-runway .scroll_line,
html.no-runway .lv_head,
html.no-runway .lv_photo,
html.no-runway .lv_edge,
html.no-runway .lv_axis,
html.no-runway .lv_hatch,
html.no-runway .lv_level,
html.no-runway .lv_ring,
html.no-runway .lv_canopy,
html.no-runway .lv_letters,
html.no-runway .lv_ink,
html.no-runway .lv_datum_mark,
html.no-runway .lv_say,
html.no-runway .band_inner,
html.no-runway .alsolike_inner {
	animation: none !important;
	opacity: 1;
	transform: none;
}

/* THE SETTLED STATE OF THE DIAGRAM. A reader here has scrubbed nothing,
   so nothing has been earned: they get the drawing and the photograph
   at the same time, both statements in flow, and the maturity rings
   still on the sheet — which is, correctly, the promise and the payoff
   in one frame.
   The ground's drain and the ink's full weight are BASE state now (see
   .lv_photo), so there is nothing to undo here: a reduced-motion reader
   with JavaScript off never gets this class and must still be able to
   read the drawing. */
html.no-runway .lv_hatch {
	opacity: .5;
}

html.no-runway .lv_edge {
	stroke-dasharray: none;
	stroke-dashoffset: 0;
}

html.no-runway .lv_datum_mark {
	left: 100%;
}

/* ---- AND THE 2026-08-06 MOTION SET (the banner's two clocks, the
   develop, the schedule, the registration ticks, the questions, the
   counter band, the collection). Stated apart from the list above
   because that rule resets `transform`, and NONE of these animate it:
   they drive the INDIVIDUAL transform properties, which `transform:
   none` says nothing about. A `translate` left mid-scrub is a picture
   parked off its frame.

   THE OPACITY RESET IS PER ELEMENT AND CANNOT BE BLANKET. Three of the
   things below are not opaque at rest: .scroll_hint sits at .78 and
   .numbers_photo at .16, and both would be restyled rather than
   un-animated by a shared `opacity: 1`. ---- */
html.no-runway .banner_photo,
html.no-runway .banner_inner,
html.no-runway .shot img,
html.no-runway .specs li,
html.no-runway .plate,
html.no-runway .plate::before,
html.no-runway .plate::after,
html.no-runway .qa,
html.no-runway .numbers_say>*,
html.no-runway .stat,
html.no-runway .tt_card {
	animation: none !important;
	opacity: 1;
	translate: none;
}

html.no-runway .scroll_hint,
html.no-runway .numbers_photo {
	animation: none !important;
	translate: none;
}

/* ---- AND THE 2026-08-07 BANNER: THE CONTACT SHEET, UNDONE BACK TO ONE
   PHOTOGRAPH. This reader gets the banner exactly as it was before the
   zoom existed — which is the point of the whole structure: the base
   state in the block at the top of this file IS this state, and the
   motion layer is the only thing that ever makes nine cells out of one.

   THE EIGHT OUTER CELLS GO BACK TO display:none AND THAT IS A LOADING
   DECISION, not a visual one. They are lazy, so a display:none cell is
   never fetched: a reduced-motion reader pays nothing at all for a
   contact sheet they are never shown. Undoing this with `opacity: 0` or
   `visibility: hidden` instead would cost them eight requests.

   `scale: none` ON THE GRID IS NOT OPTIONAL AND IS THE TRAP THE
   PARAGRAPH ABOVE DESCRIBES: the zoom drives the INDIVIDUAL transform
   properties, so the `transform: none` list further up says nothing
   about it, and a grid left mid-scrub is a sheet of nine pictures
   parked at a third of the screen. ---- */
html.no-runway .banner {
	height: auto;
}

html.no-runway .banner_stage {
	position: static;
	height: auto;
	min-height: 100svh;
}

html.no-runway .banner_grid {
	animation: none !important;
	inset: 0;
	display: block;
	opacity: 1;
	translate: none;
	scale: none;
}

html.no-runway .bg_cell {
	display: none;
}

html.no-runway .bg_cell--center {
	display: block;
	position: absolute;
	inset: 0;
}

html.no-runway .banner_rules {
	display: none;
}

/* THE SCRIM NEEDS NO UNDO ANY MORE and that is worth stating rather than
   leaving a dead reset behind: it no longer animates for anybody, since
   holding the wash is what keeps the copy legible over the assembled
   sheet. Same for .banner_inner and .scroll_hint, whose `banner-recede`
   is gone — their entries in the reset lists above are now belt only.

   THE SWAP BOX GOES BACK TO NO BOX. This reader was never shown the zoom,
   so nothing ever replaces the headline; both sentences must simply be in
   flow, which is what the base rule at the top of this file already says.
   Restated here for the same reason .banner_grid's `display: block` is,
   even though `html.sheet` is never stamped alongside this class. */
html.no-runway .banner_swap {
	display: contents;
	animation: none !important;
	opacity: 1;
	transform: none;
}

/* ---- AND THE 2026-08-07 SET: THE RULING, plus the three things below
   the diagram that had no instrument at all (the figure captions, the
   one action per band, the form's fields).

   THE DRAWN RULES NEED NOTHING BUT THE KILL. Their whole from-state is
   `scale`, which is declared in the keyframe and nowhere else, so
   dropping the animation drops it with them and every line is back at
   full length — which is the border the static page has always had.
   `scale: none` is stated anyway, because a rule at scale 0 is an
   invisible hairline and the failure would be silent. ---- */
html.no-runway .band::before,
html.no-runway .alsolike::before,
html.no-runway .shot::before,
html.no-runway .shot::after,
html.no-runway .specs li::after,
html.no-runway .qa h3::before,
html.no-runway .titleblock::before,
html.no-runway .ps_note::before {
	animation: none !important;
	scale: none;
}

html.no-runway .shot figcaption,
html.no-runway .card_actions,
html.no-runway .contact_form .field,
html.no-runway .contact_form .mybutton {
	animation: none !important;
	opacity: 1;
	translate: none;
}

/* ---- AND THE 2026-08-07 SET, PART TWO: the band heads, the notes, the
   plate's lettering and the sheet's sign-off (THE HEAD IS LETTERED and
   the two blocks under it).

   EVERY ONE OF THESE DRIVES `translate`, never `transform`, so none of
   them is covered by the `transform: none` list at the top of this
   block — the two properties are independent and resetting one says
   nothing about the other. A head left mid-scrub is a headline parked
   half a gutter off the sheet at opacity 0, which on these selectors
   is most of the visible copy on the page. ---- */
html.no-runway .band_inner>.anno:first-child,
html.no-runway .alsolike_eyebrow,
html.no-runway .band h2,
html.no-runway .alsolike h2,
html.no-runway .lede,
html.no-runway .band_inner>p:not(.anno),
html.no-runway .band--contact .split>div>p,
html.no-runway .alsolike_lead,
html.no-runway .band .define,
html.no-runway .band .anno--note,
html.no-runway .plate>.anno,
html.no-runway .plate h3,
html.no-runway .band_inner>.titleblock>.anno,
html.no-runway .ps_note>.anno,
html.no-runway .ps_note>p:not(.anno) {
	animation: none !important;
	opacity: 1;
	translate: none;
}

/* the two pictures the motion layer scales with a STATIC declaration
   rather than a keyframe: killing the animation cannot undo those, and
   the develop's filter is on the same element. .numbers_photo keeps its
   own base filter, which is what makes it texture instead of a picture. */
html.no-runway .shot img {
	scale: none;
	filter: none;
}

html.no-runway .numbers_photo {
	scale: none;
}
