@property --range {
	syntax: "<length>";
	inherits: true;
	initial-value: 0px;
}

/* ---------------------------------------------------------------- tokens */

:root {
	--serif:		ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
	--sans:			ui-sans-serif, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;

	/* Benjamin Moore, off the Magnolia Bluff paint schedule of 10.30.17. */
	--pale-oak:		#ddd9ce;   /* OC-20  — halls, entry, stair, family, kitchen */
	--linen-white:	#f2ebda;   /* OC-146 — living room, sunroom, basement */
	--white-dove:	#efeee5;   /* I-06   — all trim, satin */
	--revere:		#cbc6b8;   /* HC-172 — living and sunroom ceilings */

	/* Not painted, so not on the schedule: the dining room is hung in a
	   silvered blue-grey fabric, Ken's office in sweetgrass. Both sampled
	   off the 2018 photographs. */
	--fabric:		#aab3b4;
	--sweetgrass:	#a89d8d;

	--wall:			var(--pale-oak);
	--lit:			color-mix(in srgb, var(--wall) 62%, #fffdf6);
	--sheen:		color-mix(in srgb, var(--trim) 88%, #fff);
	--shade:		color-mix(in srgb, var(--trim) 93%, #000);
	--trim:			var(--white-dove);

	/* Quarter-sawn white oak, sampled off the Galleria Hallway photograph. */
	--oak:			#b08a5a;
	--oak-lit:		#d8c4a0;
	--oak-dark:		#6b5436;

	--ink:			#1d1a16;
	--muted:		#7a7165;
	--rule:			#cdc6b8;
	--accent:		#7a3b12;

	/* The elevation, as four bands. They are grid rows, so the wall simply
	   takes what the others leave and nothing subtracts from a hundred. */
	--ceiling:		4.6rem;
	--baseboard:	2.75rem;
	--floor:		5.5rem;

	/* The tallest thing hung here is the six-foot Simonetta; --air is the
	   plaster above and below it, and the label needs a line or three. */
	--tallest:		72;
	--air:			4;
	--labels:		7em;

	/* The house has a paint schedule. Those colours are facts about a real
	   room, and a room does not have a night mode — so this does not invert
	   with the reader's system theme. */
	color-scheme: light;
}

/* Only what this document actually contains, and only where a UA default
   would fight the layout — which spaces everything with gap. :where() holds
   these at zero specificity so nothing below has to out-shout them. */
:where(body, h1, h2, h3, h4, p, dl, dd, figure, figcaption) { margin: 0; }
:where(.Pic, .Piece, .Panel, .Dossier) { box-sizing: border-box; }
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
[hidden] { display: none !important; }

/* The page is the elevation: ceiling, wall, baseboard, floor, top to bottom.
   Four rows in flow. The wall takes what the other three leave, which is why
   nothing here has to be measured against the viewport or subtracted from it.
   Only the wall scrolls, and only sideways, so the rest holds still without
   being pinned there. */
html, body { height: 100%; }
body {
	display: grid;
	grid-template-rows: var(--ceiling) 1fr var(--floor);
	overflow: hidden;
	timeline-scope: --hall;
	font: clamp(.94rem, .88rem + .25vw, 1.06rem)/1.6 var(--serif);
	color: var(--ink);
	background:
		radial-gradient(56% 44% at 18% 4%, var(--lit), transparent 70%),
		radial-gradient(56% 44% at 52% 0%, var(--lit), transparent 70%),
		radial-gradient(56% 44% at 86% 4%, var(--lit), transparent 70%),
		linear-gradient(var(--lit), var(--lit) 34%, var(--wall) 88%, color-mix(in srgb, var(--wall) 95%, #000));
}

.Voice {
	font: 600 .68rem/1.6 var(--sans);
	letter-spacing: .17em;
	text-transform: uppercase;
}
cite { font-style: italic; }

/* A measurement read off the photograph, not off the wall. */
.Est { font-style: italic; }
.Est::before { content: "≈\00a0"; font-style: normal; }

/* -------------------------------------------------------------- ceiling
   The mirror of the floor: a plane tipped away and upward, with the crown
   drawn along its lower edge where it meets the wall. The recessed cans are
   the only thing overhead with horizontal rhythm, so they carry the parallax
   — and they are what is actually up there in the Galleria Hallway. */

.Ceiling {
	grid-area: 1 / 1;
	z-index: -1;
	overflow: hidden;
	perspective: 20rem;
	perspective-origin: 50% 140%;
	background: var(--trim);
}

.Ceiling::before {
	content: "";
	position: absolute;
	inset: 0 auto -300% -60%;
	width: calc(220% + var(--range));
	will-change: transform;
	transform: rotateX(-72deg);
	transform-origin: 50% 0;
	background:
		/* Recessed cans, as fixtures: a bright lamp in a darker aperture ring.
		   Off for now — too busy along the top edge. The plane still pans, so
		   uncommenting these is all it takes to bring them back.
		radial-gradient(circle, rgb(255 251 236 / .9) 0 4%, rgb(255 247 224 / .3) 5.5%, rgb(122 108 88 / .3) 7%, transparent 8.5%) 4rem 20%/17rem 2.4rem repeat-x,
		radial-gradient(circle, rgb(255 251 236 / .55) 0 3%, rgb(122 108 88 / .2) 5%, transparent 6.5%) 11rem 44%/25rem 2.1rem repeat-x, */
		/* Overhead is nearest and catches the light; the far edge, where the
		   plane runs into the crown, is the corner and goes dark. It was the
		   other way round, which flattened the whole thing. */
		linear-gradient(
			color-mix(in srgb, var(--trim) 98%, #fff) 0 12%,
			var(--trim) 38%,
			color-mix(in srgb, var(--trim) 88%, #000) 72%,
			color-mix(in srgb, var(--trim) 72%, #000));
	animation: pan linear;
	animation-timeline: --hall;
}

/* The crown itself. A plain cove — which is what the house has — in hard
   gradient stops, so every arris stays a crisp pixel edge at any zoom. */
.Ceiling::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: .95rem;
	background: linear-gradient(
		var(--shade) 0 1px,									/* the reveal at the ceiling */
		color-mix(in srgb, var(--trim) 97%, #000) 1px,		/* and then nothing abrupt: */
		var(--sheen) 42%,									/* the cove rises to its crown */
		color-mix(in srgb, var(--trim) 96%, #000) 82%,		/* and falls away below it */
		var(--shade) 92% 93%,								/* the quirk, one line only */
		var(--trim) 96%);
	box-shadow: 0 3px 8px -2px rgb(40 30 15 / .34);
}

/* Ceiling and floor are part of the same room as the wall, so they pan by
   exactly the same distance it does — --range, the wall's scrollable width,
   measured once in JS. Nothing here fakes a rate: what you see as parallax is
   the perspective projection alone, near edges sweeping past faster than far
   ones because they are nearer. Static where scroll-driven animation is absent.

   It is translate, not background-position: the latter is a paint property,
   so animating it repainted fourteen gradient layers across a plane four
   times the height of its band, every frame, inside a perspective container.
   translate never leaves the compositor — the plane is rasterised once and
   the GPU slides it — at the cost of making the plane as wide as the wall is
   long, which is memory well spent. */
@keyframes pan { to { translate: calc(-1 * var(--range)) 0; } }

/* ----------------------------------------------------------------- floor
   Eight-inch boards tipped away from the eye. rotateX(74deg) squashes the
   plane to ~28% of its height, so it is drawn far overheight and every seam
   is specified thick enough to survive the compression. */

.Floor {
	grid-area: 3 / 1;
	z-index: -1;
	overflow: hidden;
	perspective: 13rem;
	perspective-origin: 50% -30%;
	background: var(--oak-dark);
}

.Floor::before {
	content: "";
	position: absolute;
	inset: -320% auto 0 -60%;
	width: calc(220% + var(--range));
	will-change: transform;
	transform: rotateX(74deg);
	transform-origin: 50% 100%;
	--board: 1.9rem;			/* eight inches, in the plane's own space */
	background:
		/* Boards end. Each of these lays the butt joints for one course,
		   phased differently from its neighbours so no two land together —
		   which is the whole reason a floor reads as boards and not stripes. */
		linear-gradient(90deg, transparent 0 18%, rgb(50 32 10 / .55) 18% calc(18% + 2.5px), transparent calc(18% + 2.5px)) 0 calc(100% - 0 * var(--board))/34rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 62%, rgb(50 32 10 / .55) 62% calc(62% + 2.5px), transparent calc(62% + 2.5px)) 0 calc(100% - 1 * var(--board))/41rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 37%, rgb(50 32 10 / .55) 37% calc(37% + 2.5px), transparent calc(37% + 2.5px)) 0 calc(100% - 2 * var(--board))/29rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 81%, rgb(50 32 10 / .55) 81% calc(81% + 2.5px), transparent calc(81% + 2.5px)) 0 calc(100% - 3 * var(--board))/46rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 9%, rgb(50 32 10 / .55) 9% calc(9% + 2.5px), transparent calc(9% + 2.5px)) 0 calc(100% - 4 * var(--board))/33rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 54%, rgb(50 32 10 / .55) 54% calc(54% + 2.5px), transparent calc(54% + 2.5px)) 0 calc(100% - 5 * var(--board))/38rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 74%, rgb(50 32 10 / .55) 74% calc(74% + 2.5px), transparent calc(74% + 2.5px)) 0 calc(100% - 6 * var(--board))/27rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 29%, rgb(50 32 10 / .55) 29% calc(29% + 2.5px), transparent calc(29% + 2.5px)) 0 calc(100% - 7 * var(--board))/43rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 46%, rgb(50 32 10 / .55) 46% calc(46% + 2.5px), transparent calc(46% + 2.5px)) 0 calc(100% - 8 * var(--board))/31rem var(--board) repeat-x,
		linear-gradient(90deg, transparent 0 88%, rgb(50 32 10 / .55) 88% calc(88% + 2.5px), transparent calc(88% + 2.5px)) 0 calc(100% - 9 * var(--board))/36rem var(--board) repeat-x,
		/* The seam between courses, tiled one board high and anchored to the
		   bottom edge — the same origin the joints are placed from, so a
		   joint runs from one seam to the next instead of drifting across
		   them. A repeating-gradient tiles from the top of the plane, whose
		   height is not a whole number of boards, which is what put the two
		   out of phase. */
		linear-gradient(
			rgb(44 26 6 / .4) 0 9%,
			rgb(255 244 218 / .22) 9% 15%,
			transparent 15%) 0 100%/100% var(--board) repeat,
		/* what the eye actually reads: polish, in broad soft bands */
		linear-gradient(105deg,
			transparent 12%, rgb(255 246 226 / .16) 26%, transparent 38%,
			transparent 58%, rgb(255 246 226 / .12) 71%, transparent 84%),
		/* quarter-sawn figure, running with the boards */
		repeating-linear-gradient(to bottom,
			transparent 0 .5rem,
			rgb(92 63 27 / .05) .5rem .8rem,
			transparent .8rem 1.7rem,
			rgb(255 236 202 / .045) 1.7rem 2rem),
		linear-gradient(var(--oak-dark), var(--oak) 42%, color-mix(in srgb, var(--oak-lit) 72%, var(--oak)));
	animation: pan linear;
	animation-timeline: --hall;
}

/* The three cans, pooling on the boards. Outside the transform, so they stay
   put while the floor runs past underneath. */
.Floor::after {
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(30% 120% at 18% 105%, rgb(255 244 220 / .26), transparent 72%),
		radial-gradient(30% 120% at 52% 105%, rgb(255 244 220 / .26), transparent 72%),
		radial-gradient(30% 120% at 86% 105%, rgb(255 244 220 / .26), transparent 72%),
		linear-gradient(rgb(38 24 6 / .4), transparent 55%);
}

/* Every glow in the room, and the only thing that does not move: it is fixed
   to the camera, so the ceiling, wall and their fixtures all travel under it.
   Above the art, below the controls. */
.Artlight {
	position: fixed;
	inset: 0 0 var(--floor) 0;
	z-index: 1;
	pointer-events: none;
	mix-blend-mode: soft-light;
	background:
		radial-gradient(26% 16% at 20% 0%, rgb(255 250 232 / .7), transparent 72%),
		radial-gradient(26% 16% at 55% 0%, rgb(255 250 232 / .7), transparent 72%),
		radial-gradient(26% 16% at 88% 0%, rgb(255 250 232 / .7), transparent 72%),
		radial-gradient(40% 62% at 12% 30%, rgb(255 250 234 / .62), transparent 70%),
		radial-gradient(40% 62% at 37% 26%, rgb(255 250 234 / .55), transparent 70%),
		radial-gradient(40% 62% at 63% 26%, rgb(255 250 234 / .55), transparent 70%),
		radial-gradient(40% 62% at 88% 30%, rgb(255 250 234 / .62), transparent 70%);
}

/* The baseboard runs along the wall, so it is drawn by the wall and travels
   with it — and a door casing crossing it cuts it, exactly as one does in the
   house. Absolutely positioned, so it takes no part in the room's layout. */
.Room::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: var(--baseboard);
	background: linear-gradient(
		rgb(74 60 38 / .16) 0 1px,							/* the wall's shadow on the cap */
		var(--sheen) 2px 4px,								/* the cap, and its small quirk */
		color-mix(in srgb, var(--trim) 96%, #000) 5px,
		var(--trim) 34%,									/* the board, brightening as it */
		var(--sheen) 88%,									/* falls toward the floor */
		color-mix(in srgb, var(--trim) 90%, #000) 95%,
		rgb(96 74 46 / .55) 100%);							/* and the line where it lands */
	box-shadow: 0 -7px 13px -8px rgb(40 30 15 / .35), 0 2px 5px rgb(38 24 6 / .5);
}

/* ---------------------------------------------------------------- header */

.Plate {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: .3rem 1.5rem;
	grid-area: 1 / 1;
	padding: 0 clamp(1.5rem, 4vw, 3.5rem) .95rem;
	align-content: end;
}
.Plate h1 { font-size: clamp(1.3rem, 1.05rem + 1.1vw, 2rem); font-weight: 400; }
.Plate p { max-width: 44ch; font-size: .85rem; color: var(--muted); }
.Plate .Ghost { margin-left: auto; }

/* ------------------------------------------------------------------ wall */

.Wall {
	grid-area: 2 / 1;
	container-type: size;
	display: flex;
	overflow-x: auto;
	overflow-y: hidden;
	overscroll-behavior-x: contain;
	scroll-timeline: --hall inline;
	/* Snapping is what lets the browser's own scroll buttons step work to
	   work: they scroll to the next snap position, so the snap positions are
	   the whole of the navigation logic. Nothing measures anything. */
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	scrollbar-width: thin;
	scrollbar-color: var(--rule) transparent;
	padding: 0 max(1.5rem, 9vw);
}

/* Each room carries its own paint. The wash is translucent so the ceiling
   light underneath still reads through it. */
.Room {
	/* One inch of real wall, in pixels, read off the wall's own height. */
	--in: min(14px, (100cqh - var(--baseboard) - var(--labels)) / (var(--tallest) + var(--air)));
	--gap: calc(11 * var(--in));   /* eleven inches of plaster between works */

	position: relative;
	display: grid;
	grid-auto-flow: column;
	grid-template-rows: 1fr auto;
	align-items: center;
	gap: 0 var(--gap);
	padding: 0 calc(var(--gap) * 1.6) var(--baseboard) 0;
	background: color-mix(in srgb, var(--wall) 38%, transparent);
}

/* Dining room: silvered fabric, cloudy and with a sheen to it. */
.Fabric {
	background:
		radial-gradient(58% 42% at 22% 22%, rgb(255 255 255 / .3), transparent 62%),
		radial-gradient(46% 34% at 68% 58%, rgb(255 255 255 / .22), transparent 64%),
		radial-gradient(42% 48% at 44% 88%, rgb(48 66 74 / .16), transparent 62%),
		repeating-linear-gradient(90deg, rgb(255 255 255 / .05) 0 1px, transparent 1px 4px),
		color-mix(in srgb, var(--wall) 62%, transparent);
}

/* Ken's office: sweetgrass. Strands run horizontally, bound vertically. */
.Grasscloth {
	background:
		repeating-linear-gradient(to bottom,
			rgb(255 250 238 / .12) 0 1px,
			transparent 1px 2px,
			rgb(74 62 46 / .08) 2px 3px,
			transparent 3px 5px),
		repeating-linear-gradient(90deg, rgb(70 58 42 / .07) 0 2px, transparent 2px 9px),
		color-mix(in srgb, var(--wall) 62%, transparent);
}

/* Between rooms, a cased opening — not a change of paint at a hard edge.

   It is a piece of joinery standing on the wall, so it casts onto the wall to
   either side rather than drawing its own shadow inside itself, and it runs
   the full height of the wall: past the label line, through the baseboard,
   which dies into it the way a baseboard does at a door.

   The profile runs across the casing, so it is a horizontal gradient in hard
   stops — every step in pixels, so the arrises stay crisp at any --in, while
   the broad face takes whatever is left. Two recessed panels are set into
   that face, their sizes in inches like everything else. */
.Threshold {
	--casing: 15;			/* inches of jamb */
	--stile: 2.4;			/* the flat either side of a panel */
	position: relative;
	z-index: 1;
	grid-row: 1 / span 2;
	align-self: stretch;
	width: calc(var(--casing) * var(--in));
	margin-bottom: calc(var(--baseboard) * -1);
	font-weight: 400;
	color: color-mix(in srgb, var(--muted) 85%, transparent);
	background:
		linear-gradient(rgb(255 255 255 / .3), transparent 26%, rgb(58 46 28 / .09)),
		linear-gradient(90deg,
			var(--sheen) 0 3px,										/* outer arris */
			color-mix(in srgb, var(--trim) 97%, #000) 3px 7px,		/* backband */
			color-mix(in srgb, var(--trim) 86%, #000) 7px 9px,		/* the step */
			var(--trim) 9px calc(100% - 9px),						/* the face */
			color-mix(in srgb, var(--trim) 86%, #000) calc(100% - 9px) calc(100% - 7px),
			color-mix(in srgb, var(--trim) 97%, #000) calc(100% - 7px) calc(100% - 3px),
			var(--sheen) calc(100% - 3px) 100%);					/* inner arris */
	box-shadow:
		-3px 0 5px -2px rgb(62 50 30 / .22),						/* cast on the wall, left */
		3px 0 5px -2px rgb(62 50 30 / .22);							/* and right */
}

/* The main entry is not a jamb with panels in it — it is the front door, and
   it should be. Six panels: two short over two tall over two short, in a
   casing. Sampled off the door: stile #3b3e42, field #414247, and the bevel
   catching light at #4c4e53 — so on a dark door the raise reads lighter than
   its field, the opposite way round from the white joinery elsewhere. Which
   is precisely `ridge` against `groove`. */
.Front {
	--casing: 42;			/* a 36in door and its casing */
	--door: #3b3e42;
	--field: #414247;
	background: none;		/* the casing belongs to the door, not the wall */
	box-shadow: none;
}
.Front::before,
.Front::after { content: none; }

.Door {
	/* Eighty inches of door and a couple of casing, standing on the floor —
	   so there is wall above the head, which is where a door stops. */
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: min(calc(84 * var(--in)), 80%);
	border: calc(2.6 * var(--in)) solid transparent;
	border-bottom: 0;		/* a door meets the floor; there is no casing under it */
	/* Two image layers, not a colour and a colour: in the background
	   shorthand a bare <color> is only legal on the final layer, so the
	   colour-plus-colour form is invalid and the whole declaration is
	   dropped — which left the door's stiles showing the casing's white. */
	background:
		linear-gradient(var(--door), var(--door)) padding-box,
		linear-gradient(var(--trim), var(--trim)) border-box;
	display: grid;
	grid-template-columns: 1fr 1fr;
	grid-template-rows: 1fr 2.7fr 1.15fr;
	gap: calc(2 * var(--in)) calc(2.2 * var(--in));
	padding: calc(2.4 * var(--in)) calc(2.6 * var(--in));
	box-shadow:
		inset 0 0 0 1px rgb(0 0 0 / .5),					/* the rebate the door sits in */
		-3px 0 5px -2px rgb(62 50 30 / .22),				/* casing, cast on the wall */
		3px 0 5px -2px rgb(62 50 30 / .22),
		0 -3px 6px -3px rgb(62 50 30 / .2);					/* and under its head */
}
.Door i {
	--drop: .55;
	--rise: 2.1;
	--deep: color-mix(in srgb, var(--field) 52%, #000);
	--edge: color-mix(in srgb, var(--field) 78%, #000);
	--lift: color-mix(in srgb, var(--field) 74%, #fff);
	--fall: color-mix(in srgb, var(--field) 70%, #000);
}

/* Nobody letters a doorway. The room's name goes on the plaster beside it,
   high enough to clear anything hung on the centre line. */
.Threshold b {
	position: absolute;
	top: .6rem;
	left: calc(100% + .8rem);
	writing-mode: vertical-rl;
	writing-mode: sideways-lr;
	white-space: nowrap;
}

/* The moulding in section, reading from the face inward: the surface is
   level, drops away suddenly over a very short run, then climbs back on a
   long shallow ramp until it returns to exactly the plane it started from.

   Two borders of different widths say that — a narrow one for the drop, a
   wide one for the ramp — which is the whole asymmetry, and is what
   border-style cannot do on its own, since groove and ridge split a single
   border in half and both faces come out equal.

   The shading is inset/outset semantics with the values named rather than
   derived: browsers darken border-color by a fixed ratio, which reads well on
   the dark door and comes out near-black on white joinery. Measured, the
   deepest the white ever goes is about four fifths of the trim. */
.Panels i,
.Door i {
	position: relative;
	background: var(--field);
	border: max(2px, calc(var(--drop) * var(--in))) solid;
	border-color: var(--deep) var(--edge) var(--edge) var(--deep);
}
.Panels i::before,
.Door i::before {
	content: "";
	position: absolute;
	inset: 0;
	border: max(3px, calc(var(--rise) * var(--in))) solid;
	border-color: var(--lift) var(--fall) var(--fall) var(--lift);
}

/* The jamb's pair. The lower is the shorter, as it is on the door. */
.Panels {
	--field: var(--trim);
	--drop: .55;			/* the door's own section, in white */
	--rise: 2.1;
	--deep: color-mix(in srgb, var(--trim) 80%, #000);		/* the drop, shaded */
	--edge: color-mix(in srgb, var(--trim) 95%, #000);		/* and its lit side */
	--lift: color-mix(in srgb, var(--trim) 96%, #fff);		/* the ramp, catching light */
	--fall: color-mix(in srgb, var(--trim) 91%, #000);
	position: absolute;
	inset: 0 calc(var(--stile) * var(--in)) calc(var(--baseboard) + var(--stile) * var(--in));
	display: grid;
	grid-template-rows: 1.75fr 1fr;
	gap: calc(var(--stile) * 1.5 * var(--in));
	padding: calc(var(--stile) * 1.4 * var(--in)) 0;
}


.Work {
	grid-row: 1 / span 2;
	display: grid;
	grid-template-rows: subgrid;
	justify-items: center;
	scroll-snap-align: center;
}

/* The box is the artwork's true footprint in inches; a frame is added outside
   it, so the painting stays at true scale and the moulding takes its own wall.
   The image is contained, so its pixel size never enters the layout and an
   imperfect crop can neither distort it nor throw off the scale. */
.Hang {
	/* Explicit because .Hang is a button, and browsers default form controls
	   to border-box. The work's footprint is the artwork itself: a frame's
	   moulding and a plinth's shaft are padding and border added outside it,
	   not carved out of it. Under border-box a deep plinth eats the whole
	   content box and the object vanishes. */
	box-sizing: content-box;
	align-self: center;
	width: calc(var(--w) * var(--in));
	height: calc(var(--h) * var(--in));
	cursor: zoom-in;
	transition: translate .35s cubic-bezier(.2, .8, .3, 1), filter .35s;
	filter: drop-shadow(0 1px 1px rgb(40 30 15 / .32)) drop-shadow(0 .85rem 1.1rem rgb(40 30 15 / .34));
}
.Hang img { width: 100%; height: 100%; object-fit: contain; }
.Hang:hover {
	translate: 0 -.35rem;
	filter: drop-shadow(0 2px 2px rgb(40 30 15 / .34)) drop-shadow(0 1.5rem 1.7rem rgb(40 30 15 / .42));
}

/* Frames, for works whose photograph came without one. An outer moulding as
   the border, an inset liner as the padding, and the artwork inside that.

   Not conic-gradient: its sectors only meet the corners on a square element,
   so on anything oblong the mitres cut diagonally across the flat faces. A
   raking linear-gradient reads as metal, and inset box-shadows give the
   bevel — those follow the rectangle whatever its proportions. */
.Framed {
	--moulding:	#0c0b0a;
	--liner:	#b8934c;
	--outer:	2;			/* inches */
	--inset:	1.5;

	border: calc(var(--outer) * var(--in)) solid var(--moulding);
	padding: calc(var(--inset) * var(--in));
	background: linear-gradient(128deg,
		color-mix(in srgb, var(--liner) 78%, #fff) 0%,
		var(--liner) 32%,
		color-mix(in srgb, var(--liner) 58%, #000) 68%,
		color-mix(in srgb, var(--liner) 72%, #fff) 100%);
	box-shadow:
		inset 0 0 0 1px rgb(0 0 0 / .6),					/* liner against the art */
		inset 1px 1px 0 1px var(--arris, rgb(255 245 220 / .32)),	/* liner's lit arris */
		inset -1px -1px 0 1px rgb(0 0 0 / .3),				/* and its shaded one */
		0 0 0 1px rgb(0 0 0 / .45);							/* outer edge of the moulding */
}

/* Objects are not hung; they stand. A case on a plinth, bottom-aligned to the
   wall rather than centred on the picture line — because that is where an
   object actually sits. The maker's studio ground becomes the case interior,
   which is why it is not knocked out. */
.Object .Hang {
	position: relative;
	align-self: end;
	cursor: zoom-in;
	padding-bottom: calc(var(--plinth) * var(--in));
	--shaft: calc(var(--plinth) * var(--in));
	background:
		/* cap */
		linear-gradient(
			color-mix(in srgb, var(--trim) 99%, #fff) 0 2px,
			color-mix(in srgb, var(--trim) 68%, #000)) 50% calc(100% - var(--shaft) + 5px)/94% 6px no-repeat,
		/* base */
		linear-gradient(
			color-mix(in srgb, var(--trim) 99%, #fff) 0 2px,
			color-mix(in srgb, var(--trim) 72%, #000)) 50% 100%/100% 7px no-repeat,
		/* shaft, lit from the left like everything else in the room */
		linear-gradient(90deg,
			color-mix(in srgb, var(--trim) 76%, #000) 0 3%,
			color-mix(in srgb, var(--trim) 99%, #fff) 22%,
			var(--trim) 55%,
			color-mix(in srgb, var(--trim) 66%, #000)) 50% 100%/62% var(--shaft) no-repeat;
}

/* Glass, raking across the front of the case. */
.Object .Hang::after {
	content: "";
	position: absolute;
	inset: 0 0 calc(var(--plinth) * var(--in)) 0;
	pointer-events: none;
	background: linear-gradient(108deg,
		rgb(255 255 255 / .13) 0 18%, transparent 30%,
		transparent 63%, rgb(255 255 255 / .08) 73%, transparent 86%);
	box-shadow: inset 0 0 0 1px rgb(255 255 255 / .22);
}

/* The basement photograph wall, at the same --in as every painting — so the
   frames come out the size they really are: small, and a very long way along.

   Every measurement below is knoxville.gallery's, converted from its
   centimetres and nothing more. Frame outsides 37.7 × 30 cm and 27.5 × 22.4
   cm; moulding 1.6 cm; prints 8 × 10 and 5 × 7; two inches between frames and
   three between the courses. Those were measured off the real frames, so they
   are not to be re-derived. */
/* Thirty-three feet wide: far too large to be one snap target, and its centre
   is half a room away from either end. The frames snap instead, so the
   basement is stepped through photograph by photograph. */
.Photowall { scroll-snap-align: none; }

.Photowall .Hang {
	--moulding:	.6299;		/* 1.6cm */
	--spacing:	2;			/* inches between frames */
	--shift:	3;			/* the lower course, offset so gaps never align */
	width: auto;
	height: auto;
	display: grid;
	gap: calc(var(--spacing) * var(--in));
	cursor: default;
	filter: none;
}
.Photowall .Hang:hover { translate: none; }
.Course { display: flex; gap: calc(var(--spacing) * var(--in)); }
/* The midline the whole layout hangs on: the upper course sits on it and the
   lower course hangs from it, so the gap between the two stays constant
   however the frame heights vary. */
.Upper { align-items: flex-end; }
.Lower { align-items: flex-start; margin-left: calc(var(--shift) * var(--in)); }
.Upper .Stack { align-content: end; }
.Stack { display: grid; gap: calc(var(--spacing) * var(--in)); }

/* The mat is whatever is left between the moulding and the print, which on
   these frames is wider at the sides than at top and bottom. */
.Pic {
	display: block;
	flex: none;
	scroll-snap-align: center;
	/* Most of the wall is off-screen; skip rendering what is. */
	content-visibility: auto;
	contain-intrinsic-size: auto calc(var(--fw) * var(--in)) auto calc(var(--fh) * var(--in));
	width: calc(var(--fw) * var(--in));
	height: calc(var(--fh) * var(--in));
	border: calc(var(--moulding) * var(--in)) solid #14120e;
	padding:
		calc((var(--fh) - 2 * var(--moulding) - var(--oh)) / 2 * var(--in))
		calc((var(--fw) - 2 * var(--moulding) - var(--ow)) / 2 * var(--in));
	background: #fbfaf7;
	box-shadow: 0 1px 2px rgb(40 30 15 / .34);
	transition: filter .25s;
}

/* Mat board is a couple of millimetres thick and its opening is bevel-cut, so
   the print sits a little behind it: the top of the opening drops a hairline
   of shadow onto the print and the bottom bevel catches the light. One pixel
   of each, taken out of the print rather than added around it — the frame is
   the size it is. Applies equally to the grey of an opening still waiting for
   a photograph. */
.Pic > img, .Pic::after {
	box-sizing: border-box;
	width: 100%;
	height: 100%;
	border-top: 1px solid rgb(38 28 12 / .3);
	border-left: 1px solid rgb(38 28 12 / .16);
	border-right: 1px solid rgb(255 252 244 / .5);
	border-bottom: 1px solid rgb(255 252 244 / .7);
}
.Pic > img { object-fit: cover; }
.Pic:empty::after {
	content: "";
	display: block;
	background: color-mix(in srgb, var(--wall) 55%, #8d8378);
}

/* The Galleria pair: two blocks of four flanking the Giacomo. Every dimension
   is read off the corner of the real frame — 0.46in of black moulding, a
   0.15in gilt fillet at the rebate, and the rest of the three inches in mat,
   which the .Pic padding works out on its own from 19 × 25 against 13 × 19.

   The fillet is an inset ring at the moulding's inner edge, with a hairline of
   shade just inside it where the gilt drops away to the mat. */
.Quad .Hang {
	--moulding: .46;
	--fillet: .15;
	--spacing: 3;
	width: auto;
	height: auto;
	display: grid;
	grid-template-columns: repeat(2, auto);
	gap: calc(var(--spacing) * var(--in));
	cursor: default;
	filter: none;
}
.Quad .Hang:hover { translate: none; }
.Quad .Pic {
	border-color: #16150f;
	background: #fdfdfa;
	box-shadow:
		inset 0 0 0 calc(var(--fillet) * var(--in)) #b18c46,
		inset 0 0 0 calc((var(--fillet) + .05) * var(--in)) rgb(40 28 10 / .45),
		0 1px 3px rgb(40 30 15 / .4),
		0 .35rem .7rem -.3rem rgb(40 30 15 / .3);
}

/* A hung frame opens like any other work. It should not look like a button,
   but it should own up to being one on approach. */
button.Pic { cursor: zoom-in; }
button.Pic:hover { filter: brightness(1.05) drop-shadow(0 2px 5px rgb(40 30 15 / .45)); }

.Photowall .Label .Ghost { justify-self: center; margin-top: .5rem; font-size: .6rem; }
.Dossier a { color: inherit; text-underline-offset: .22em; }

/* A work known to hang here but not yet photographed. Museums leave the label
   up when something is off the wall; so does this. */
.Absent .Hang {
	display: grid;
	place-items: center;
	cursor: help;
	filter: none;
	border: 1px dashed var(--rule);
	background: color-mix(in srgb, var(--wall) 22%, transparent);
	font: 600 .6rem/1.6 var(--sans);
	letter-spacing: .16em;
	text-transform: uppercase;
	text-align: center;
	color: color-mix(in srgb, var(--muted) 65%, transparent);
}
.Absent .Hang:hover { border-color: var(--muted); color: var(--muted); translate: none; }
.Ghostframe {
	flex: none;
	margin: auto;
	display: grid;
	place-items: center;
	width: min(50%, 20rem);
	aspect-ratio: 5 / 4;
	border: 1px dashed var(--rule);
	font: 600 .62rem/1.6 var(--sans);
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--muted);
}

/* Works whose room is not yet known. Held back behind a plain wall. */
.Unplaced { background: none; }
.Unplaced .Threshold { font-style: italic; }

/* A gallery-wrapped canvas has no moulding, but it still stands proud of the
   wall by its own depth. Straight on you cannot see the return — an eye at
   about 62in is below the top edge of anything centred on the 58in line, so
   no side of the stretcher is ever presented. What you actually see is the
   shadow it throws, so the shadow is what draws the thickness.

   Two casts: a hard one at the offset the depth and the light angle give,
   which is the canvas's own edge on the wall, and a soft one behind it for
   the room's ambient fill. Both scale with --depth, in inches, like
   everything else here. */
.Canvas .Hang {
	--depth: 1.5;
	--cast: calc(var(--depth) * var(--in));
	filter:
		drop-shadow(calc(var(--cast) * .12) calc(var(--cast) * .16) calc(var(--cast) * .22) rgb(44 32 14 / .55))
		drop-shadow(calc(var(--cast) * .38) calc(var(--cast) * .58) calc(var(--cast) * .95) rgb(44 32 14 / .3));
}
.Canvas .Hang:hover {
	filter:
		drop-shadow(calc(var(--cast) * .14) calc(var(--cast) * .2) calc(var(--cast) * .26) rgb(44 32 14 / .55))
		drop-shadow(calc(var(--cast) * .6) calc(var(--cast) * .95) calc(var(--cast) * 1.5) rgb(44 32 14 / .34));
}

/* You cannot see the return of a canvas hung square in front of you — but
   you are only square in front of one work at a time. Everything else on the
   wall is off to one side, and a canvas standing an inch and a half proud
   shows you the side facing you.

   Which side that is follows from where the work sits in the scrollport, and
   view() is exactly that: a timeline whose progress is the element's own
   passage across the viewport. A work over on the right is being seen from
   its left, so its left return is what shows; dead centre shows neither; over
   on the left it is the right return. The returns scale rather than resize,
   so this stays on the compositor with the rest of the scrolling.

   The painted edge is lit on the left and shaded on the right, like every
   other surface here. */
.Canvas .Hang { position: relative; }
.Canvas .Hang::before,
.Canvas .Hang::after {
	content: "";
	position: absolute;
	top: 1.5%;
	bottom: 1.5%;
	width: calc(var(--depth) * var(--in));
	animation-timeline: view(inline);
	animation-fill-mode: both;
	animation-timing-function: linear;
}
.Canvas .Hang::before {
	right: 100%;
	transform-origin: right;
	animation-name: return-left;
	background: linear-gradient(to right, rgb(126 108 78 / .5), rgb(58 44 22 / .5));
}
.Canvas .Hang::after {
	left: 100%;
	transform-origin: left;
	animation-name: return-right;
	background: linear-gradient(to left, rgb(46 34 16 / .62), rgb(30 22 10 / .55));
}
@keyframes return-left  { from { scale: 1 1; } 50%, to { scale: 0 1; } }
@keyframes return-right { from, 50% { scale: 0 1; } to { scale: 1 1; } }

/* The tombstone card, on one baseline the length of the wall. */
.Label {
	align-self: start;
	position: sticky;
	left: max(1.5rem, 9vw);
	right: max(1.5rem, 9vw);
	width: min(26ch, max(14ch, calc(var(--w) * var(--in))));
	margin-top: clamp(.7rem, 1.5vh, 1.3rem);
	font-size: .74rem;
	line-height: 1.45;
	text-align: center;
	text-wrap: balance;
	color: var(--muted);
}
.Label b, .Label cite, .Label .Meta { display: block; }
.Label b, .Label cite { color: var(--ink); }
.Label cite { font-size: .86rem; }

/* ---------------------------------------------------------------- detail */

.Detail, .Panel {
	color: var(--ink);
	background: color-mix(in srgb, var(--linen-white) 60%, #fffdf8);
}
.Detail::backdrop, .Panel::backdrop { background: rgb(14 10 6 / .58); backdrop-filter: blur(9px); }

/* The ground the picture hangs against, in the picture's own cell but behind
   it, so the picture's box stays the size of the picture. */
.Detail::before {
	content: "";
	grid-area: 1 / 1;
	z-index: -1;
	background: radial-gradient(80% 60% at 50% 38%, var(--lit), var(--wall));
}

.Detail {
	inset: 0;
	width: 100%;
	max-width: 100%;
	height: 100vh;
	height: 100dvh;
	max-height: 100vh;
	max-height: 100dvh;
	overflow: hidden;
}

/* Every track is bounded, so the popover cannot outgrow the viewport. An auto
   row here would take its height from the image and push the rest off screen. */
.Detail:popover-open {
	display: grid;
	grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
.Detail::before,
.Piece { grid-area: 1 / 1; }
.Dossier { grid-area: 2 / 1; }
@media (min-width: 58rem) {
	.Detail:popover-open {
		grid-template-rows: minmax(0, 1fr);
		grid-template-columns: minmax(0, 1fr) clamp(22rem, 32%, 30rem);
	}
	.Dossier {
		grid-area: 1 / 2;
		border-top: 0;
		border-left: 1px solid var(--rule);
	}
}

/* min-height:0 is the whole trick. A 1fr track's automatic minimum is
   min-content, so without it the image's own pixel height props the row open
   and the picture ends up sizing the box that was meant to size the picture.
   Waive that minimum and the track is free to be the height it was given;
   the percentages then resolve against a real number and object-fit does
   the rest. */
.Piece {
	min-width: 0;
	min-height: 0;
	max-width: calc(100% - 4em);
	max-height: calc(100% - 4em);
	place-self: center;
	object-fit: contain;
	filter: drop-shadow(0 1px 2px rgb(40 30 15 / .34)) drop-shadow(0 1.1rem 1.6rem rgb(40 30 15 / .38));
}

.Dossier {
	display: grid;
	align-content: start;
	gap: 1rem;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	padding: clamp(1.25rem, 4vh, 3rem) clamp(1.25rem, 3vw, 2.5rem) 3rem;
	border-top: 1px solid var(--rule);
}
/* The artist, under the object. Not behind another click. */
.Dossier h4 {
	margin-top: .3rem;
	padding-top: 1rem;
	border-top: 1px solid var(--rule);
	color: var(--muted);
}

.Dossier h3, .Panel h3 {
	font-size: clamp(1.2rem, 1rem + .7vw, 1.65rem);
	font-weight: 400;
	line-height: 1.25;
	text-wrap: balance;
}
.Dossier h3 { font-style: italic; }
.Dossier .Alt { font-style: normal; font-size: .8em; color: var(--muted); }
.Dossier .Artist, .Panel .Life { color: var(--muted); }
.Dossier p, .Panel p { text-wrap: pretty; }

/* Tombstone facts. An empty value keeps its row and says so. */
.Facts {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	gap: .32rem 1.2rem;
	padding: .85rem 0;
	border-top: 1px solid var(--rule);
	border-bottom: 1px solid var(--rule);
	font-size: .82rem;
}
.Facts dt {
	padding-top: .3em;
	font: 600 .66rem/1.6 var(--sans);
	letter-spacing: .13em;
	text-transform: uppercase;
	color: var(--muted);
}
.Facts dd:empty::after { content: "not yet recorded"; font-style: italic; color: var(--rule); }

/* Something the collection knows it does not know. */
.Wanting {
	padding-left: .9rem;
	border-left: 2px solid var(--rule);
	font-size: .82rem;
	font-style: italic;
	color: var(--muted);
}

/* Supporting evidence: process, certificates, the artists at work. */
.Exhibits { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr)); gap: .7rem; }
.Exhibits figure { display: grid; align-content: start; gap: .35rem; }
.Exhibits img { width: 100%; border-radius: 2px; box-shadow: 0 1px 4px rgb(0 0 0 / .32); }
.Exhibits figcaption { font-size: .68rem; line-height: 1.4; color: var(--muted); }

/* Opening a work morphs the painting out of the wall and into the panel, and
   the text slides in behind it. Only one detail is ever open, and a closed
   popover is display:none, so .Dossier can hold the name outright — the
   painting's has to be handed over at the moment of the swap, since the copy
   on the wall is still rendered underneath.

   The wall keeps working with the script off: the buttons are still
   popovertarget, and the morph is only layered on when the browser has it. */
::view-transition-group(work) { animation-duration: .42s; }

/* The snapshots must fill their group. Left alone they take the UA default —
   inline-size:100%, block-size:auto, pinned to the top of the group — so a
   picture that ends up centred in a tall panel animates to the top of it and
   then jumps to the middle when the real element takes over. Filling the
   group and letting object-fit hold the ratio makes the snapshot land exactly
   where the picture actually goes. The fade stays: it is the UA's, and it is
   what carries one image into the other. */
::view-transition-old(work),
::view-transition-new(work) {
	inline-size: 100%;
	block-size: 100%;
	object-fit: contain;
}

::view-transition-new(dossier) { animation: .42s cubic-bezier(.2, .8, .3, 1) both dossier-in; }
::view-transition-old(dossier) { animation: .24s ease-in both dossier-out; }
@keyframes dossier-in { from { translate: 100% 0; opacity: 0; } }
@keyframes dossier-out { to { translate: 100% 0; opacity: 0; } }

.Dossier { view-transition-name: dossier; }

/* Artist panels: a wall text, not an object label. `display` must appear only
   on :popover-open — set unconditionally it beats the UA rule that hides a
   closed popover, and pins every panel open. */
.Panel {
	position: fixed;
	width: min(42rem, calc(100vw - 2rem));
	max-height: min(34rem, calc(100vh - 3rem));
	max-height: min(34rem, calc(100dvh - 3rem));
	margin: auto;
	overflow-y: auto;
	padding: clamp(1.75rem, 4vw, 3rem);
	box-shadow: 0 2rem 5rem -1rem rgb(0 0 0 / .55);
}
.Panel:popover-open { display: grid; align-content: start; gap: .9rem; }

.Close {
	position: absolute;
	top: .6rem;
	right: .7rem;
	z-index: 3;
	display: grid;
	place-items: center;
	width: 2.4rem;
	height: 2.4rem;
	border-radius: 50%;
	color: var(--muted);
	cursor: pointer;
}
.Close:hover { color: var(--ink); background: color-mix(in srgb, var(--rule) 45%, transparent); }

.Ghost {
	justify-self: start;
	padding: .45rem 0;
	border-bottom: 1px solid var(--rule);
	color: var(--muted);
	cursor: pointer;
	transition: color .2s, border-color .2s;
}
.Ghost:hover { color: var(--ink); border-color: currentColor; }

/* The wall draws its own controls. ::scroll-button() is a real button the
   browser owns: it knows the scroll position, disables itself at either end,
   is reachable by keyboard and announced to assistive tech, and it scrolls to
   the next snap position — which, with the works snapping on their centres,
   is the next work. That is the whole of what the old JS did. */
.Wall::scroll-button(*) {
	position: fixed;
	top: calc((100% - var(--floor)) / 2);
	z-index: 5;
	display: grid;
	place-items: center;
	width: 2.75rem;
	height: 2.75rem;
	border: 0;
	border-radius: 50%;
	font: 1rem/1 var(--sans);
	color: var(--muted);
	background: color-mix(in srgb, var(--lit) 92%, transparent);
	box-shadow: 0 1px 4px rgb(40 30 15 / .2);
	cursor: pointer;
	transition: color .2s, scale .2s, opacity .25s;
}
.Wall::scroll-button(*):hover { color: var(--ink); scale: 1.09; }
.Wall::scroll-button(*):disabled { opacity: 0; pointer-events: none; }
.Wall::scroll-button(left) { content: "\2039"; left: clamp(.35rem, 1.5vw, 1.1rem); }
.Wall::scroll-button(right) { content: "\203a"; right: clamp(.35rem, 1.5vw, 1.1rem); }

@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after { transition-duration: .01ms !important; animation: none !important; }
	.Wall { scroll-behavior: auto; }
}
