/* FT Viewer terrain section. The grid's second column is the detail panel:
   0 at rest, 420px when a marker is active. */
.terrain-viewer {
	position: relative;
	display: grid;
	grid-template-columns: 1fr 0px;
	width: 100%;
	aspect-ratio: 16 / 9;
	max-height: min(900px, calc(100vh - 120px));
	background: #fbfcfc;
	transition: grid-template-columns 700ms cubic-bezier(0.22, 1, 0.36, 1);
}

.terrain-viewer.is-open { grid-template-columns: 1fr 420px; }

.tv-stage { position: relative; height: 100%; overflow: hidden; }
.tv-stage canvas { display: block; width: 100%; height: 100%; }

/* The terrain settles in as the section arrives.
 *
 * A scale rather than a rise: the canvas fills the stage exactly, so nudging it
 * up would draw the background in along the bottom edge for the length of the
 * transition. Starting a little larger and easing down to fit keeps it covered
 * the whole way. The origin is below centre, which is where the landform is. */
.tv-stage canvas {
	opacity: 0;
	transform: scale(1.035);
	transform-origin: 50% 62%;
	transition: opacity 900ms ease, transform 1100ms cubic-bezier(0.22, 1, 0.36, 1);
}

.terrain-viewer.is-revealed .tv-stage canvas { opacity: 1; transform: none; }

/* Below 900 the section grows taller relative to its width, so the terrain has
   somewhere to be once the panel stops sitting beside it. It gets there by
   ramping, not by switching.
 *
 * A ratio of 390/420 here read as a sudden zoom. The camera's field of view is
 * vertical and fixed, so how large the terrain appears is proportional to this
 * box's height — and swapping 16/9 for 390/420 took the height from 488px to
 * 865px across two pixels of window width. The terrain did not zoom; it was
 * honestly reporting a box that had nearly doubled. An earlier attempt to
 * cancel that out in the camera is what made selecting a marker shrink the
 * terrain, so it is fixed here, where the jump actually is.
 *
 * The line is fixed by its ends. At 900px wide the gutter is 16 either side, so
 * the box is 868 across and 16/9 makes it 488.25 tall; at 390px the gutter is 8
 * and the intended 390/420 shape makes it 402.8. Through those two points:
 *
 *     h = 0.16755 · vw + 337.46
 *
 * which meets 16/9 exactly at the breakpoint and arrives at the phone shape by
 * 390. vw counts the scrollbar and the box does not, so this can be a couple of
 * pixels out in a desktop browser — far below anything visible, and the
 * narrow widths this serves do not have a persistent scrollbar. */
/* The ramp is the height of the terrain, so on a narrow screen it belongs to
   the stage's row rather than to the section — the panel sits underneath and
   adds its own height. max-height moves with it, for the same reason: left on
   the section it would take its toll out of the terrain to make room for the
   panel. */
@media (max-width: 900px) {
	.terrain-viewer {
		aspect-ratio: auto;
		height: auto;
		max-height: none;
		grid-template-columns: 1fr;
		grid-template-rows: min(calc(16.755vw + 337.46px), calc(100vh - 120px)) auto;
	}
	.terrain-viewer.is-open { grid-template-columns: 1fr; }
}

/* Markers are real buttons in a layer over the canvas, not sprites. The type
   stays crisp at any distance, Tab reaches them, Enter activates them, and a
   screen reader sees ordinary controls. Their position is the only thing the
   3D scene contributes, written each frame. */
.tv-markers { position: absolute; inset: 0; pointer-events: none; }

.tv-marker {
	position: absolute;
	transform: translate(-50%, -50%);
	pointer-events: auto;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 34px;
	height: 34px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: #24363f;
	color: #fff;
	font-family: 'JetBrains Mono', ui-monospace, monospace;
	font-size: 17px;
	line-height: 1;
	/* The glyph sits slightly high in the em box; nudge it back to the centre. */
	padding-bottom: 1px;
	cursor: pointer;
	opacity: 0;
	transition: opacity 400ms ease, transform 260ms ease, background 200ms ease;
}

/* The markers arrive one after another, once the terrain has had a moment.
 *
 * The delay is given per property, in the order the transition above lists them
 * — opacity, transform, background — so only the fade waits. Written as a
 * single value it delayed all three, which left the later markers lagging by up
 * to a quarter of a second before answering a hover, for the whole life of the
 * page rather than just during the reveal.
 *
 * One line per marker, so a fifth one needs a fifth line. */
.terrain-viewer.is-revealed .tv-marker { opacity: 1; transition-delay: 320ms, 0ms, 0ms; }
.terrain-viewer.is-revealed .tv-marker:nth-child(2) { transition-delay: 440ms, 0ms, 0ms; }
.terrain-viewer.is-revealed .tv-marker:nth-child(3) { transition-delay: 560ms, 0ms, 0ms; }
.terrain-viewer.is-revealed .tv-marker:nth-child(4) { transition-delay: 680ms, 0ms, 0ms; }

.tv-marker:hover { transform: translate(-50%, -50%) scale(1.12); }
.tv-marker:focus-visible { outline: 2px solid #24363f; outline-offset: 3px; }
.tv-marker[aria-expanded="true"] { background: #0f1d24; }

/* Behind a ridge. Dimmed rather than hidden: a marker that vanishes is harder
   to follow than one that recedes. */
.tv-marker.is-occluded { opacity: 0.3; }
.terrain-viewer.is-revealed .tv-marker.is-occluded { opacity: 0.3; }

/* Hidden across, for the column that opens from 0px; scrolls down, because a
   point with a description, advantages and applications can be taller than
   the terrain box on a laptop-height screen. */
.tv-panel {
	overflow-x: hidden;
	overflow-y: auto;
	background: #fff;
	border-left: 1px solid rgba(36, 54, 63, 0.16);
	opacity: 0;
	transition: opacity 360ms ease;
}

.terrain-viewer.is-open .tv-panel { opacity: 1; }

.tv-panel-inner { width: 420px; padding: 28px; }
.tv-panel-media {
	display: block;
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	background: #f1f4f5;
}
.tv-panel h3 { margin: 20px 0 16px; font-size: 20px; font-weight: 500; }
.tv-panel p { margin: 0; color: rgba(36, 54, 63, 0.72); line-height: 1.6; }
/* Type and colour come from .label; only the spacing is the panel's. */
.tv-panel .tv-panel-uses { margin-top: 32px; padding-top: 0; }
.tv-panel-adv { margin: 24px 0 0; padding: 0; list-style: none; }
.tv-panel-adv li { padding: 12px 0; border-top: 1px solid rgba(36, 54, 63, 0.12); }
.tv-panel-adv li:last-child { border-bottom: 1px solid rgba(36, 54, 63, 0.12); }
.tv-panel-adv strong { display: block; font-size: 15px; font-weight: 500; }
.tv-panel-adv span { display: block; margin-top: 2px; color: rgba(36, 54, 63, 0.72); font-size: 14px; line-height: 1.5; }

/* No side column to open on a narrow screen, so the panel sits under the
   terrain and stays there.
 *
 * It used to be a sheet: absolutely positioned against the bottom of the
 * section and slid up over the terrain when a marker was chosen. At this width
 * the panel is as tall as the stage, so the sheet covered the whole of it —
 * tapping a marker hid the thing the marker was pointing at. As a row it takes
 * its own space and the terrain keeps all of its own. It is always visible,
 * which is why select() always has something to put in it. */
@media (max-width: 900px) {
	.tv-panel {
		position: static;
		border-left: 0;
		border-top: 1px solid rgba(36, 54, 63, 0.16);
		transform: none;
		opacity: 1;
	}
	.tv-panel-inner { width: auto; padding: 20px; }
}

@media (prefers-reduced-motion: reduce) {
	.terrain-viewer,
	.terrain-viewer .tv-marker,
	.terrain-viewer .tv-stage canvas,
	.terrain-viewer .tv-panel,
	.terrain-viewer .tv-modes { transition: none; }
	.terrain-viewer .tv-marker { opacity: 1; transition-delay: 0ms; }
	/* No reveal to watch, so the terrain is simply there. */
	.terrain-viewer .tv-stage canvas { opacity: 1; transform: none; }
	.terrain-viewer .tv-modes { opacity: 1; }
}

/* The layer toggle, over the terrain at the bottom left. Inside the stage, so
   it keeps its place when the panel opens and takes a 420px column out of the
   width — the toggle moves with the picture rather than with the section. */
.tv-modes {
	position: absolute;
	left: 16px;
	bottom: 16px;
	z-index: 2;
	display: flex;
	gap: 4px;
	padding: 4px;
	max-width: calc(100% - 32px);
	overflow-x: auto;
	scrollbar-width: none;
	background: rgba(253, 253, 253, 0.85);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid #cacfd1;
	border-radius: 999px;
	opacity: 0;
	transition: opacity 400ms ease;
	transition-delay: 320ms;
}

.tv-modes::-webkit-scrollbar { display: none; }
.terrain-viewer.is-revealed .tv-modes { opacity: 1; }

.tv-mode {
	flex: 0 0 auto;
	padding: 6px 14px;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: #24363f;
	/* The variable, not a literal: the brief named Switzer, which this site
	   never loads — no @font-face, no font link — so a literal here would
	   quietly fall to system-ui while every other control on the page renders
	   in Inter Tight. var(--sans) is what "matches the design system" means. */
	font-family: var(--sans);
	font-size: 14px;
	font-weight: 400;
	line-height: 1.2;
	white-space: nowrap;
	cursor: pointer;
	transition: background 150ms ease, color 150ms ease, opacity 150ms ease;
}

.tv-mode:hover { opacity: 0.6; }
.tv-mode:focus-visible { outline: 2px solid #24363f; outline-offset: 2px; }
/* opacity: 1 here deliberately outranks .tv-mode:hover's 0.6 by source order:
   the selected pill is not meant to dim on hover. Dimming the mode that is
   already active would read as the control breaking, not responding — a
   literal departure from the brief's "hover: text opacity 0.6", kept on
   purpose. */
.tv-mode[aria-checked="true"] { background: #24363f; color: #fdfdfd; opacity: 1; }

/* One label or the other, chosen by CSS so nothing has to listen to the width. */
.tv-mode-narrow { display: none; }

@media (max-width: 640px) {
	.tv-mode-wide { display: none; }
	.tv-mode-narrow { display: inline; }
	.tv-modes { left: 12px; bottom: 12px; max-width: calc(100% - 24px); }
}

/* Which layer the panel is describing. Above the title, small, monospace —
   the same voice as the section's other labels. */
.tv-panel-mode {
	margin: 0 0 14px;
	font-size: 11px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: rgba(36, 54, 63, 0.5);
}

.tv-panel-mode:empty { display: none; }

/* The DSM ramp, and only while DSM is showing. Bottom right, opposite the
   toggle, so the two never collide when the panel narrows the stage. */
.tv-legend {
	position: absolute;
	right: 16px;
	bottom: 16px;
	z-index: 2;
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 12px;
	background: rgba(253, 253, 253, 0.85);
	backdrop-filter: blur(12px);
	-webkit-backdrop-filter: blur(12px);
	border: 1px solid #cacfd1;
	border-radius: 999px;
	font-family: var(--mono);
	font-size: 11px;
	color: rgba(36, 54, 63, 0.7);
	white-space: nowrap;
}

/* main.css's global `[hidden] { display: none !important }` is what
   actually hides this outside DSM today — `.tv-legend`'s own `display: flex`
   above only loses to a plain `[hidden]` because of that rule's
   `!important`. But that rule lives in a separate stylesheet, versioned on
   its own `?v=` marker, so the two can drift apart. Stated here too, so the
   legend still hides itself if main.css's `!important` is ever weakened or
   dropped. */
.tv-legend[hidden] { display: none; }

.tv-legend-bar {
	display: block;
	width: 96px;
	height: 8px;
	border-radius: 4px;
	background: linear-gradient(to right,
		#1f3a4d 0%, #3c6e71 20%, #a9c5a0 45%, #e8d9a8 65%, #c98b5a 85%, #fdfdfd 100%);
}

@media (max-width: 640px) {
	.tv-legend { right: 12px; bottom: 12px; padding: 4px 10px; }
	.tv-legend-bar { width: 64px; }
}
