/* #region resets, colors, font-sizing */
:root {
	/* From Ghesquiere's Spring-Summer 2015 and 2016 shows */ 
	--color-text: hsl(31, 5%, 8%); /*Leather Black*/
	--color-muted: hsl(202, 17%, 69%); /*Scalloped Ivory*/
	
	--color-bg: hsl(43, 28%, 90%); /**/
	--color-bg-console: rgba(232, 228, 220, 0.08);
	--color-surface: hsl(226, 25%, 19%); /*Navy Mesh*/
	
	--color-primary: hsl(333, 75%, 79%); /*Biker Pink*/
	--color-accent: hsl(188, 60%, 53%); /*Turquoise Knit*/
	--color-border: hsl(215, 11%, 22%); /*Gunmetal Weave*/
	
	--color-success: hsl(151, 82%, 30%); /*Emerald Stripe*/ 
	--color-warning: hsl(34, 76%, 49%); /*Eel-Skin Amber*/
	--color-error: hsl(350, 58%, 31%); /*Oxblood Tab*/
	--color-inkblot: rgba(21,94,108,0.22);
	/* Cutting-mat palette for architect annotations  */
	--color-mat:        #1b6b58;  
	--color-grid-minor: rgba(242,251,248,.10);
	--color-grid-major: rgba(242,251,248,.30);
	--color-ink:        #F2FBF8;
	--color-panel:      rgba(242,251,248,.07);
	--color-panel-edge: rgba(242,251,248,.55); 
	
	
	
	
	--content-basewidth: 680px;
	--content-doublewidth: calc(var(--content-basewidth) * 2.8);
	/* Viewport height on phones. Deliberately less than a full screen: a viewport
	that tall on a 20:9 phone frames a lot of sky and floor, and leaves the
	beat text nowhere to live. `svh` rather than `vh` so the collapsing
	browser chrome doesn't resize the viewport mid-scroll. */
	--viewport-h-mobile: 62svh;
	--viewport-aspect: 16 / 9;
	
	
	
	--font-decorative: "League Spartan", sans-serif;
	--font-body: "Inter", sans-serif; /* font-weight: 200 */
	--font-console: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	--font-idk: "Commissioner", sans-serif; /* font-weight: 200 */
	--weight-body: 200;
	--weight-buttons: 400;
	--weight-contrast: 600;
	--body-mult: 1.3;
	--h1-mult: 2.2;
	--h2-mult: 1.65;
	--h3-mult: 1.25;
}

p, ol { 
	margin: 0 0 1.6rem 0;
	font-size: calc(var(--type-scale) * var(--body-mult)); 
}
h1 { 
	font-size: calc(var(--type-scale) * var(--h1-mult)); 
	font-family: var(--font-idk);
	font-weight: var(--weight-body);
	margin: 0 0 .1rem 0;
}
h2{ 
	font-size: calc(var(--type-scale) * var(--h2-mult));
	margin: 12rem 0 0 0;
	
}
h3{ 
	font-size: calc(var(--type-scale) * var(--h3-mult)); 
	padding: 0;
	margin: 0;
}
h4 {
	padding: 0;
	margin: 1rem 0 0 0;
}

/* h1 {font-family: var(--font-decorative);} */



figure {
	margin: 0;
	width: 100%;
}

figure video {
	width: 100%;
	height: auto;
	display: block;
	/* preload="none" means no intrinsic size until the first frame arrives, so
	without this every clip collapses to the default 150px and then jumps.
	Reserving the box up front is what keeps CLS at zero. */
	aspect-ratio: var(--viewport-aspect);
}

/* Plain <figure> images (e.g. ex33-grapple-prompt.png) have no such rule and
render at native pixel width, which overflows a phone-width column. Unlike
the video above, these already have real intrinsic dimensions, so a
max-width cap (not a forced 100%) is enough — it shrinks oversized images
without stretching small ones, and leaves them as inline elements so
figure's text-align:center still centers them. */
figure img {
	max-width: 100%;
	height: auto;
}


@media (min-width: 1200px) {
	:root { --content-basewidth: 780px; }
}
* { box-sizing: border-box; }
html, body { 
	margin:0; 
	padding:0; 
	overflow-x: clip; /* With a fixed full-viewport layer on the page, any sideways document overflow drags it off-screen and clips content at the edges on phones. */
}

body{
	font-family: var(--font-body);
	font-weight: var(--weight-body);
	line-height: 1.7;
	font-optical-sizing: auto;
	font-style: normal;
	font-size: clamp(1rem, 0.85rem + 0.5vw, 1.15rem); /* fall back for fancy scaling stuff below, but most browsers support fancy stuff since 2023 */
}

/* #region signature animation */
@keyframes drawStroke {
	from {
		stroke-dashoffset: var(--stroke-length, 1000);
	}
	to {
		stroke-dashoffset: 0;
	}
}

#Signature path {
	stroke-dasharray: var(--stroke-length, 1000);
	stroke-dashoffset: var(--stroke-length, 1000);
	animation: drawStroke 0.8s ease-in-out forwards;
}

#Signature path:nth-child(1) { --stroke-length: 500; animation-delay: 0s; }
#Signature path:nth-child(2) { --stroke-length: 450; animation-delay: 0.1s; }
#Signature path:nth-child(3) { --stroke-length: 520; animation-delay: 0.2s; }
#Signature path:nth-child(4) { --stroke-length: 480; animation-delay: 0.3s; }
#Signature path:nth-child(5) { --stroke-length: 380; animation-delay: 0.4s; }
#Signature path:nth-child(6) { --stroke-length: 320; animation-delay: 0.5s; }
#Signature path:nth-child(7) { --stroke-length: 360; animation-delay: 0.6s; }
#Signature path:nth-child(8) { --stroke-length: 400; animation-delay: 0.7s; }
#Signature path:nth-child(9) { --stroke-length: 590; animation-delay: 0.8s; }
#Signature path:nth-child(10) { --stroke-length: 460; animation-delay: 0.9s; }
#Signature path:nth-child(11) { --stroke-length: 420; animation-delay: 1s; }
#Signature path:nth-child(12) { --stroke-length: 510; animation-delay: 1.1s; }
/* #endregion */



/* #region sections, single- or two-columns-on-wider-screens */
main {
	display: flex;
	flex-direction: column;
	--type-scale: 1em;
	color: #333333;
}
@supports (container-type: inline-size) {
	main {
		container-type: inline-size;
		--type-scale: clamp(1rem, 0.85rem + 0.5cqw, 1.15rem);
	}
}

section {
	width: 100%;
	margin-inline: auto;
	padding-inline: clamp(1rem, 4vw, 2.5rem);
}

section.defaultWidth {
	max-width: var(--content-basewidth);
}

section.doubleWidth {
	max-width: min(var(--content-doublewidth), 88vw);
	
	container-type: inline-size;
	container-name: doublewidth;
}

/* Below the row-layout breakpoint (see @container/@media below), .columns is
still a single stacked column, so the 88vw cap above isn't framing a two-up
layout — it's just shaving ~12vw off the section for no reason, on top of
its own padding-inline. section.defaultWidth has no such cap and fills the
full column on a phone, which is why .threejs-viewport (living in a
defaultWidth section) reads wider than the video here without this. */
@media (max-width: 899px) {
	section.doubleWidth { max-width: none; }
}

section.doubleWidth .columns {
	display: flex;
	flex-direction: column;
	gap: clamp(1.5rem, 4vw, 3rem);
	/* No side padding while stacked single-column (below): it would inset the
	video narrower than .threejs-viewport, which has no such padding of its
	own. Re-added once the layout goes two-up, as breathing room between
	columns — see the row-layout breakpoints below. */
	padding: 0;
	background-color: black;
	color: white;
}
figure {
	text-align: center;
	margin-bottom: 1.6rem;
}
.columns a {
	color: var(--color-accent);
	background-color: rgba(255,255,255,.1);
	/* outline: 2px solid pink; */
}

@container doublewidth (min-width: 700px) {
	section.doubleWidth .columns {
		flex-direction: row;
		align-items: flex-start;
		padding: 0 1rem;
	}
	section.doubleWidth .columns > * {
		flex: 1;
		min-width: 0;
	}
	section.doubleWidth .columns.wide-first > :first-child {
		flex: 0 0 66%;
	}
	section.doubleWidth .columns.wide-first > :last-child {
		flex: 1;
	}
}

/* Fallback */
@supports not (container-type: inline-size) {
	@media (min-width: 900px) {
		section.doubleWidth .columns {
			flex-direction: row;
			align-items: flex-start;
			padding: 0 1rem;
		}
		section.doubleWidth .columns > * {
			flex: 1;
			min-width: 0;
		}
		section.doubleWidth .columns.wide-first > :first-child {
			flex: 0 0 66%;
		}
		section.doubleWidth .columns.wide-first > :last-child {
			flex: 1;
		}
	}
	
}
/* #endregion */



/* #region ink wash effect */
.ink-wash-background {
	position: relative;
	margin-bottom: 3rem;
	overflow: visible;
	color:
}

.ink-wash-background::before {
	content: '';
	position: absolute;
	inset: 0;
	background: radial-gradient(120% 120% at 30% 25%, var(--color-primary), transparent 70%);
	filter: url(#inkwash);
	border-radius: 40% 60% 55% 45%;
	z-index: -1; /* sits behind the text */
}

.ink-wash-background.accent::before {
	background: radial-gradient(120% 120% at 30% 25%, var(--color-accent), transparent 50%);
	border-radius: 65% 35% 42% 58%;
}

.ink-wash-background.accent {
	background:radial-gradient(120% 120% at 30% 25%, var(--color-accent), transparent 50%);
	border-radius: 65% 35% 42% 58%;
}

.ink-wash-background.success {
	background:radial-gradient(120% 120% at 30% 25%, var(--color-success), transparent 50%);
	border-radius: 48% 52% 38% 62%;
}

.ink-wash-background.warning {
	background:radial-gradient(120% 120% at 30% 25%, var(--color-warning), transparent 50%);
	border-radius: 58% 42% 65% 35%;
}

.ink-wash-background.error {
	background:radial-gradient(120% 120% at 30% 25%, var(--color-error), transparent 50%);
	border-radius: 35% 65% 52% 48%;
}
/* #endregion */



/* #region grid like on ttae.art  */
.scatterGrid {
	display: grid;
	grid-template-columns: repeat(12, 1fr);
	grid-auto-rows: 4vw;
	gap: 12px;  /* ← Increased from 3px to accommodate lines + whitespace */
	margin-bottom: 4rem;
}
.scatterGrid .cell {
	grid-column: var(--colDesktop);
	grid-row: var(--rowDesktop);
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	min-height: 0; /* prevent grid blowout */
	background: #222; /* shows while the placeholder SVG is being generated */
	overflow: visible; /* let absolutely positioned children (like .take-control) overflow */
}
@media (max-width: 768px) {
	.scatterGrid {
		display: grid;
		grid-template-columns: repeat(12, 1fr);
		grid-auto-rows: 4vw;
		gap: 12px;  /* ← Increased from 3px to accommodate lines + whitespace */ 
	}
	.scatterGrid .cell {
		/* falls back to the desktop value when no mobile override is set */
		grid-column: var(--colMobile, var(--colDesktop));
		grid-row: var(--rowMobile, var(--rowDesktop));
	}
}
/* #endregion */



/* #region sticky header navigation */
header {
	position: sticky;
	top: 0;
	z-index: 100;
	display: flex;
	align-items: center;
	gap: clamp(1rem, 4vw, 2rem);
	/* padding: clamp(0.75rem, 2vw, 1.25rem); */
	background: rgba(255, 255, 255, 0.98);
	backdrop-filter: blur(8px);
	border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}

header nav {
	display: flex;
	gap: clamp(1rem, 3vw, 2rem);
	align-items: center;
}

header nav a {
	font-size: clamp(0.85rem, 2vw, 1rem);
	text-decoration: none;
	color: #333;
	font-weight: 400;
	transition: color 0.2s ease;
}

header nav a:hover {
	color: #000;
}

#nameplate {
	flex-shrink: 0;
	height: clamp(2.5rem, 6vw, 4rem);
	display: flex;
	align-items: center;
}

#nameplate svg {
	height: 100%;
	width: auto;
	max-width: clamp(180px, 30vw, 300px);
}

/* Pages without a nav (e.g. article pages) have only #nameplate in the
   header, so let it fill the bar and center the signature within it. */
header:not(:has(nav)) #nameplate {
	width: 100%;
	justify-content: center;
}
@media (max-width: 768px) {
	header {
		flex-direction: column-reverse;
		gap: 0.75rem;
		padding: 0.75rem 1rem;
	}
	
	header nav {
		width: 100%;
		gap: 0.75rem;
		justify-content: center;
	}
	
	header nav a {
		font-size: 0.8rem;
	}
	
	#nameplate {
		width: 100%;
		justify-content: center;
		height: clamp(2rem, 5vw, 3rem);
	}
	
	#load-bar { height: 1.25rem; }
	#load-bar-status { font-size: 0.625rem; }
}

footer p {
	font-size: clamp(0.75rem, 1rem + 0.5vw, 1.2rem);
	/* color: pink; */
}
/* #endregion */


/* #region overall load progress bar
Driven by 01distance/src/loadProgress.js. Lives inside <header>, which is the
only position:sticky element on the page — and which is also the containing
block here, since both position:sticky and backdrop-filter establish one. So
top:100% hangs the bar flush under the header's border-bottom, and it rides
the header's z-index:100, which puts it over #overlay-canvas-global (z-index:5).

It overlays the article rather than displacing it: a bar that occupied layout
would shove the whole page upward the moment it dismissed. */
#load-bar {
	position: absolute;
	top: 100%;
	left: 0;
	right: 0;
	height: 1.5rem;
	overflow: hidden;
	background: var(--color-bg-console);
	border-bottom: 1px solid rgba(0, 0, 0, 0.08);
	transition: opacity 300ms ease, height 300ms ease;
}

/* scaleX on a plain div is compositor-only: no layout, no repaint of the header,
and deliberately nothing animated on the signature SVG, which is already doing
its own scroll-driven stroke-dashoffset work. */
#load-bar-fill {
	position: absolute;
	inset: 0;
	transform-origin: left center;
	transform: scaleX(0);
	background: var(--color-accent);
	transition: transform 180ms linear;
}

#load-bar-status {
	position: relative; /* reads over the fill */
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1rem;
	height: 100%;
	padding: 0 clamp(0.5rem, 2vw, 1rem);
	font-family: var(--font-console);
	font-size: 0.6875rem;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	/* Not .ascii-bar's var(--ink-dim) — that custom property is never defined. */
	color: var(--color-text);
	white-space: nowrap;
}

#load-bar-task {
	overflow: hidden;
	text-overflow: ellipsis;
}

#load-bar-pct {
	flex-shrink: 0;
	font-variant-numeric: tabular-nums; /* so the percentage doesn't jitter as digits change */
}

/* Nothing credited yet: the module graph is in flight and there are no byte
counts to show. A sweep says "working" without claiming a number. */
#load-bar.is-indeterminate::after {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	width: 30%;
	background: linear-gradient(90deg, transparent, var(--color-accent), transparent);
	opacity: 0.45;
	animation: loadBarSweep 1.2s linear infinite;
}

@keyframes loadBarSweep {
	from { transform: translateX(-100%); }
	to   { transform: translateX(333%); }
}

#load-bar.is-done {
	opacity: 0;
	height: 0;
}

@media (prefers-reduced-motion: reduce) {
	/* The fill stays: progress is information, not decoration. The sweep and the
	dismissal are decoration, so they go. */
	#load-bar,
	#load-bar-fill { transition: none; }
	#load-bar.is-indeterminate::after { animation: none; opacity: 0; }
}
/* #endregion */


/* #region core article elements */
.difficultyIndicator {
	margin: 0;
}
.articleIntro {
	font-style: italic;
	margin-top: 4rem;
}
.articleDisclaimer, .articleTakeawayBox {
	margin: 2vw;
	padding: 2vh 1.4vw 2vh 2vw;
}

.articleDisclaimer {
	background-color: var(--color-accent);
	font-weight: var(--weight-contrast);
}

.articleTakeawayBox {
	background-color: var(--color-primary)
}

ol * {
	margin-bottom: 1.6rem;
}

.spectrum {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 0.35em 1rem;
	font-family: var(--font-console);
	font-size: calc(var(--type-scale) * 1.45);
	line-height: 1.35;
	overflow-wrap: anywhere;
}
.spectrum__arrow { grid-column: 1 / -1; width: 100%; height: auto; }
.spectrum__head { font-size: 1.25em; }
.spectrum__head--end,
.spectrum__sub--end { text-align: right; }

/* #endregion */


/* #region debug/console look for explainer variables */
.ascii-bar-label, .ascii-bar {
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

.ascii-bar-label {
	margin: .5rem 0 1.5rem 0;
}
.ascii-bar {
	font-size: 0.875em;
	letter-spacing: 0;
	white-space: pre;
	color: var(--ink-dim);
	background: rgba(232, 228, 220, 0.08);
	padding: 0.5em 0.75em;
	border-radius: 4px;
	display: inline-block;
}

.triggerTime, .responseTime {
	font-family: var(--font-console);
	font-variant-numeric: tabular-nums;
	font-weight: 600;
	transition: color 200ms ease;
	opacity: 0.6;
}

.triggerTime.locked {
	color: var(--color-accent);
	opacity: 1;
}

.responseTime.locked {
	color: var(--color-primary);
	opacity: 1;
}

.triggerTime.is-ticking, .responseTime.is-ticking {
	animation: counterTick 220ms ease-out;
}

/* Table cells with time styling */
#timesCompared {
	margin: 3rem auto;
	width: fit-content;
}

#timesCompared th {
	font-weight: 600;
	text-align: left;
	padding: 0.75em 1em 0.75em 0;
}

#timesCompared td {
	padding: 0.75em 1em 0.75em 0;
}

#timesCompared td.triggerTime,
#timesCompared td.responseTime {
	background: rgba(232, 228, 220, 0.08);
	padding: 0.5em 0.75em;
	border-radius: 4px;
	opacity: 1;
	color: var(--color-muted);
}

#timesCompared td.triggerTime {
	color: var(--color-accent);
}

#timesCompared td.responseTime {
	color: var(--color-primary);
}

@keyframes counterTick {
	from {
		opacity: 0.35;
		transform: translateY(-2px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

@media (prefers-reduced-motion: reduce) {
	.triggerTime.is-ticking, .responseTime.is-ticking,
	.triggerTime:not(.locked), .responseTime:not(.locked) {
		animation: none;
	}
}
/* #endregion */


/* #region grid like on ttae.art  */
.scatterGrid {
	display: grid;
	grid-template-columns: repeat(12, 1fr);
	grid-auto-rows: 4vw;
	gap: 12px;  /* ← Increased from 3px to accommodate lines + whitespace */ 
}
.scatterGrid .cell {
	grid-column: var(--colDesktop);
	grid-row: var(--rowDesktop);
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	min-height: 0; /* prevent grid blowout */
	background: #222; /* shows while the placeholder SVG is being generated */
	overflow: visible; /* let absolutely positioned children (like .take-control) overflow */
}

@media (max-width: 768px) {
	.scatterGrid {
		display: grid;
		grid-template-columns: repeat(12, 1fr);
		grid-auto-rows: 4vw;
		gap: 12px;  /* ← Increased from 3px to accommodate lines + whitespace */ 
	}
	.scatterGrid .cell {
		/* falls back to the desktop value when no mobile override is set */
		grid-column: var(--colMobile, var(--colDesktop));
		grid-row: var(--rowMobile, var(--rowDesktop));
	}
}
/* #endregion */



/* #region multiple instances of a threejs canvas on the page */
#overlay-canvas-global { /*// global canvas. we use scissor test to render inside specific divs based on scroll. these divs are virtual canvases */
	position: fixed;
	inset: 0;
	width: 100vw;
	height: 100vh;
	z-index: 5;
	display: block;
	pointer-events: none; /* toggled to 'auto' while hovering a viewport */
	touch-action: none;
}

/* the virtual canvases */
.threejs-viewport{
	position: relative;
	-webkit-user-select: none;
	user-select: none;
	width: 100%;
	aspect-ratio: var(--viewport-aspect);
	margin: 3rem 0;
	cursor: pointer; /* focusing one takes control of it — see PlayMode */
}

/* PlayMode gives every viewport tabindex="0": focus is what starts play, so it
has to be visible when it arrives by keyboard. */
.threejs-viewport:focus { outline: none; }
.threejs-viewport:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }

/* #endregion */


/* #region PlayMode */
#play-surface {
	position: fixed;
	z-index: 5;
	display: none;
	cursor: crosshair;
	touch-action: none;
}
body.is-playing #play-surface { display: block; }

.threejs-viewport.is-playing { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.threejs-viewport.is-playing .viewport-hud:not(.viewport-hud--play-only) { display: none; }
/* #endregion */


/* #region HUD overlays inside a threejs viewport (health bars, prompts, etc.)
These must be CHILDREN of .threejs-viewport, not siblings placed over the same
grid area: ViewportRenderer._measure() scissors to viewport.el.getBoundingClientRect(),
so only a descendant of that exact box tracks the rendered pixels on scroll/resize.
.threejs-viewport is position:relative with no z-index of its own, so it does NOT
open a new stacking context — these overlays are compared directly against
#overlay-canvas-global's z-index:5 in the document's root stacking context.
Anything here needs z-index > 5 or the canvas paints over it. */
.viewport-hud {
	position: absolute;
	z-index: 10;
	max-width: 45%;
	padding: 0.4em 0.6em;
	font-size: 0.75rem;
	line-height: 1.3;
	box-sizing: border-box;
	/* Real HUD chips (health bars, prompts) never wrap. Wrapping also broke
	mobile specifically: .scatterGrid's row height is 4vw (viewport WIDTH),
	so a narrow phone gives the viewport box very little height — a wrapped
	2-line label then grows tall enough that the top and bottom corners
	collide in the middle. */
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	
	pointer-events: none;
}
.viewport-hud--top-left { top: 0; left: 0; }
.viewport-hud--top-right { top: 0; right: 0; text-align: right; }
.viewport-hud--CTA {
	bottom: 10%; left: 25%; width: 50%; min-height: 20%;
	font-size: 1rem;
	white-space: normal;
	overflow: visible;
	text-overflow: clip;
}
.viewport-hud--bottom-left { bottom: 0; left: 0; }
.viewport-hud--bottom-right { bottom: 0; right: 0; text-align: right; }

/* Inverse of the base .viewport-hud rule above: hidden in cinematic mode,
shown only once play starts (device-control-hint chips — see
wireViewportHudContent in main.js). Excluded from the blanket .is-playing
hide rule above via :not(), so this pair is what actually controls them. */
.viewport-hud--play-only {
	display: none;
	background: rgba(0, 0, 0, 0.5);
	color: #fff;
	white-space: normal;
	overflow: visible;
	text-overflow: clip;
	font-size: clamp(0.75rem, 1.25rem + 0.5vw, 2.1rem);
}
.threejs-viewport.is-playing .viewport-hud--play-only { display: block; }

/* One [data-device="keyboard"|"gamepad"] variant block per play-only chip;
wireViewportHudContent (main.js) toggles `hidden` to match InputManager's
activeDevice. Each row inside is one action: a label plus its icon(s). */
.hud-controls__row {
	display: flex;
	align-items: center;
	gap: 0.4em;
}
.hud-controls__row + .hud-controls__row { margin-top: 0.3em; }
.viewport-hud--bottom-right .hud-controls__row { justify-content: flex-end; }

.hud-icon { width: 1.3em; height: 1.3em; flex: none; transition: color 80ms ease-out; }

/* Pressed state (see SVG-INPUT-ICONS_README.md), toggled by
wireControlHintPressedStates in main.js. Set on the <svg> root rather than
scoped to .press-stroke: every currentColor-based stroke/text in these
icons (outline, label, arrows) inherits color with no local override, so
one declaration recolors the whole icon. .press-fill needs its own rule
because its shape always carries an explicit fill="none" attribute, which
inheritance can't reach past — only a same-or-higher-specificity rule
targeting the shape itself can. */
.hud-icon.is-pressed { color: var(--color-surface); }
.hud-icon.is-pressed .press-fill { fill: var(--color-accent); transition: fill 80ms ease-out; }

/* WASD arranged like the physical keyboard cluster: W centered above A S D. */
.hud-controls__icons--wasd {
	display: inline-grid;
	grid-template-columns: repeat(3, 1.3em);
	grid-template-rows: repeat(2, 1.3em);
	gap: 2px;
}
.hud-icon--w { grid-column: 2; grid-row: 1; }
.hud-icon--a { grid-column: 1; grid-row: 2; }
.hud-icon--s { grid-column: 2; grid-row: 2; }
.hud-icon--d { grid-column: 3; grid-row: 2; }
/* #endregion */


/* #region Notification queue (see NotificationQueue.js)
Deliberately not a .viewport-hud: those are hidden outright by
.threejs-viewport.is-playing above, but a notification can only ever be
triggered *during* play (cylinder proximity only updates then), so it needs
the opposite rule. Lives inside #play-surface, which PlayMode already keeps
sized and positioned over the active viewport, so top:0;left:0 here is all
it takes to sit in that viewport's top-left corner.
transition-duration must match NOTIFICATION_FADE_SECONDS in
NotificationQueue.js, or the queue advances to the next notification before
this one has visibly finished fading out. */
.notification-tray {
	position: absolute;
	top: 0;
	left: 0;
	/* A firm width, not a cap: unlike max-width, this scales the icon up as
	well as down, tracking #play-surface's JS-computed size (PlayMode
	keeps it matched to the active .threejs-viewport) in both directions. */
	width: 22%;
	pointer-events: none;
	opacity: 0;
	transition: opacity 0.4s ease;
}
.notification-tray.is-visible { opacity: 1; }

.notification-tray__image {
	display: block;
	width: 100%;
	height: auto;
}
/* #endregion */


/* ─────────────────────────────────────────────────────────────────────────
Architect annotations
- .annotatedNode hosts the SVG overlay drawn by src/article/architect-annotations.js
- position:relative is the one thing the module cannot supply for itself
- lives at the top level: nested inside :root{} it resolves to ":root .annotatedNode",
which happens to work but reads like a mistake
───────────────────────────────────────────────────────────────────────── */
.annotatedNode{
	position: relative;
	margin: 3rem 0;
	padding: 2.5rem 6rem 2.5rem 2.5rem;   /* right padding leaves room for the vertical dimension */ 
	
	background-color: var(--mat);
	background-image:
	linear-gradient(var(--grid-major) 1px, transparent 1px),
	linear-gradient(90deg, var(--grid-major) 1px, transparent 1px),
	linear-gradient(var(--grid-minor) 1px, transparent 1px),
	linear-gradient(90deg, var(--grid-minor) 1px, transparent 1px);
	background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px;
}


.measuredPanel{
	height: 120px;
	margin-bottom: 3.5rem;   /* clearance for the dimension line drawn below */
	background: var(--panel);
	border: 1px solid var(--panel-edge);
	border-radius: 2px;
	color: var(--ink);
} 

.annotatedNode figcaption{
	color: var(--ink); 
	font-size: .8rem;
	opacity: .85;
}

/* #endregion */