/*
	home.css
	Single-viewport hero page (index.html / bio.html / projects.html).
	Loaded after main.css — reuses its reset/typography/webfonts only;
	everything else here is independent of Story's SCSS palette system.
*/

:root {
	--sam-bg: #000000;
	--sam-fg: #ffffff;
	--box-w: 11rem;
	--box-h: 3rem;
	--frame-w: 14.28rem;
	/* Global so pages without .sam-Bio-home (e.g. the galleries) can
	   still derive sizing off the bio page's photo width. */
	--Bio-photo-w: 24rem;
	/* Figlet heading fill — a gradient, not a flat colour, so it reads
	   clearly against each theme's own background (see .sam-project-heading
	   pre): light-gray -> white on dark mode's black background. */
	--ascii-gradient: linear-gradient(135deg, #7a7a7a, #ffffff);
	/* Body-text outline, reusing --ascii-gradient's own dark-theme grey so
	   the white-on-black text and the title read as the same palette. */
	--sam-text-outline-color: #7a7a7a;
}

html[data-theme="light"] {
	--sam-bg: #ffffff;
	--sam-fg: #000000;
	/* Dark navy -> black on light mode's white background. Black itself
	   can't go any darker (#000000 is already the floor) — the lighter
	   end was the actual readability problem (a plain #858585 gray was
	   too light for good contrast against white; a dark-blue tint reads
	   just as dark as the #3a3a3a gray it replaced, with a bit more
	   character). */
	--ascii-gradient: linear-gradient(135deg, #161685, #000000);
	/* Same navy blue as --ascii-gradient's light-theme start colour, reused
	   here as the outline around black-on-white body text. */
	--sam-text-outline-color: #161685;
}

html, body {
	height: 100%;
	margin: 0;
	overflow: hidden;
}

body {
	background-color: var(--sam-bg);
	color: var(--sam-fg);
}

/* main.css hardcodes strong/b to #000000 (see its own strong, b { color:
   #000000 } rule) — loaded after it, so this wins the cascade at equal
   specificity and follows the theme instead. */
strong, b {
	color: var(--sam-fg);
}

/* Nav */

.sam-nav {
	position: fixed;
	top: 2.5rem;
	right: 2.5rem;
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 0.75rem;
	z-index: 100;
}

@media screen and (max-width: 736px) {

	.sam-nav {
		top: 1.5rem;
		right: 1.5rem;
		gap: 0.5rem;
	}

}

/* Box (Home / Projects / Bio / Mode) */

.sam-box {
	position: relative;
	width: var(--box-w);
	height: var(--box-h);
}

.sam-box-face {
	-webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	margin: 0;
	padding: 0;
	max-width: none;
	height: auto;
	line-height: normal;
	white-space: normal;
	font-family: inherit;
	font-size: 0.8rem;
	font-weight: 400;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	text-decoration: none;
	text-overflow: initial;
	color: var(--sam-fg) !important;
	background: var(--sam-bg);
	box-shadow: none;
	border: 1.5px solid var(--sam-fg);
	border-radius: 0;
	cursor: pointer;
	transition: inset 0.25s ease;
}

.sam-box-face:hover,
.sam-box-face:focus,
.sam-box-face:active {
	color: var(--sam-fg) !important;
	box-shadow: none;
}

.sam-box:hover .sam-box-face,
.sam-box:focus-within .sam-box-face {
	inset: -0.5rem 0 -0.5rem calc(-1 * var(--box-w));
}

/* Bio has no dropdown to reveal (unlike Projects, the rule above's actual
   target) — cancelled back to inset:0, same as .sam-box--home-icon's own
   cancellation below. Without this, the generic grow-leftward-by-a-full-
   box-width rule above still applies (nothing here overrode it before),
   silently doubling the face's width and shifting it left on hover —
   invisible while the label had no hover styling of its own, but a real
   bug surfaced by adding the underline/grow/fade affordance. */
.sam-box--Bio:hover .sam-box-face,
.sam-box--Bio:focus-within .sam-box-face {
	inset: 0;
}

.sam-box-face:focus-visible {
	outline: 1px dashed var(--sam-fg);
	outline-offset: 4px;
}

/* Home icon: back in the nav column (top slot, right-aligned with the
   boxes below, same as every other .sam-box) — square (width matches the
   shared --box-h instead of the wider --box-w). Plain static link — no
   dropdown to reveal, so the generic hover-grow above is cancelled back to
   inset:0 here.

   3D look built from the actual reference artwork (images/home_button.png)
   — a ::before layer shows a cropped, chroma-keyed version of it (images/
   home_icon_border.png — white background turned transparent, keeping the
   dark rounded-square/circle/house line art) as a background image
   filling the whole face, house glyph and all — the reference art's own
   house icon is the icon now, so the separate theme-swapped Home_icon_
   grey/black.png overlays are gone (see index.html/bio.html/projects.html).
   The source is dark line art on transparent — natively suited to *light*
   mode — so it's inverted (white line art) by default for dark mode, and
   the invert is cancelled in light mode. Same design on all 3 pages (this
   replaced the CSS-drawn coin/disc used briefly before it, and the
   bubble-SVG artwork before that). */
.sam-box--home-icon {
	/* width alone would size it correctly in a row-direction container
	   (.sam-nav-row, index.html) but not in a column-direction one
	   (plain .sam-nav, bio.html/projects.html) — there, flex-basis
	   controls the main (vertical) axis, leaving width, the cross axis,
	   to fall back to .sam-box's own 11rem unless set explicitly too. */
	width: var(--box-h);
	flex: 0 0 var(--box-h);
}

.sam-box--home-icon .sam-box-face {
	border: none;
	background: var(--sam-bg);
	border-radius: 22%;
	overflow: hidden;
}

.sam-box--home-icon:hover .sam-box-face,
.sam-box--home-icon:focus-within .sam-box-face {
	inset: 0;
}

.sam-box--home-icon .sam-box-face::before {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
	background-image: url("../../images/home_icon_border.png");
	background-size: cover;
	background-position: center;
	filter: invert(1);
}

html[data-theme="light"] .sam-box--home-icon .sam-box-face::before {
	filter: none;
}

.sam-mode-icon {
	width: 60%;
	height: 60%;
	object-fit: contain;
	position: relative; /* stacks above .sam-box--mode-toggle's own border-image */
}

/* Floating topic-jump buttons (projects.html only) — replaces the old
   vertical "topic carousel" widget with the same 3 buttons already used
   on the Projects-intro section (.sam-projects-topic-jump, defined
   further down), just docked to the right edge, roughly halfway down the
   viewport, and sized down to fit that narrower footprint. Hidden while
   on the intro section itself (index 0) since that section already shows
   the same 3 buttons inline; the button matching whichever topic is
   currently open gets .is-current (dimmed, unclickable) instead of
   duplicating the count — see renderTopicJumpsFloating in home.js. */
.sam-topic-jumps-floating {
	position: fixed;
	top: 50vh;
	right: 2.5rem;
	transform: translateY(-50%);
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	z-index: 100;
}

.sam-topic-jumps-floating.is-hidden {
	display: none;
}

@media screen and (max-width: 736px) {

	.sam-topic-jumps-floating {
		right: 1.5rem;
	}

}

.sam-topic-jumps-floating .sam-projects-topic-jump {
	width: 10rem;
	padding: 0.6rem 0.75rem;
	font-size: 0.65rem;
	text-align: center;
}

.sam-topic-jumps-floating .sam-projects-topic-jump:hover,
.sam-topic-jumps-floating .sam-projects-topic-jump:focus {
	font-size: 0.7rem;
}

.sam-topic-jumps-floating .sam-projects-topic-jump.is-current {
	opacity: 0.35;
	pointer-events: none;
	text-decoration: none;
}

/* Mode toggle: icon = the theme you'll switch *to* (opposite convention
   from Home's own icon, per request) — light_mode.png shown in dark mode
   (click to go light), dark_mode.png shown in light mode (click to go
   dark). */
.sam-mode-icon--to-dark {
	display: none;
}

html[data-theme="light"] .sam-mode-icon--to-light {
	display: none;
}

html[data-theme="light"] .sam-mode-icon--to-dark {
	display: block;
}

/* Projects: face splits into a (left) project list and a (right) title;
   at rest the face is only one box-width wide, so flex-end + clipping
   hides the list and leaves just the title, flush with the shared right edge. */

.sam-box--projects .sam-box-face {
	justify-content: flex-end;
	align-items: stretch;
	overflow: hidden;
	padding: 0;
	cursor: default;
}

.sam-box--projects:hover .sam-box-face,
.sam-box--projects:focus-within .sam-box-face,
.sam-box--projects.is-open .sam-box-face {
	/* z-index high enough to visually cover Bio too (later in the DOM, so
	   it paints on top of this panel by default stacking order otherwise)
	   — matches how this panel already covers Home/mode-toggle, which are
	   earlier in the DOM. */
	inset: -4rem 0 -4rem calc(-1 * var(--box-w));
	z-index: 60;
}

.sam-box-title {
	flex: 0 0 var(--box-w);
	display: flex;
	align-items: center;
	justify-content: center;
	/* Now an <a> (links to the Projects intro page), not a plain span —
	   reset the browser's own link styling so it still reads as part of
	   .sam-box-face's own text (color/text-transform/letter-spacing all
	   still inherit from there, since those aren't reset by the UA
	   stylesheet the way color/text-decoration are). */
	color: inherit;
	text-decoration: none;
	transition: font-size 0.2s ease, opacity 0.2s ease;
}

/* Grown/open affordance: underline + a larger size + a slight fade —
   distinct from the dropdown list items below it, so it's clear
   "Projects" itself is also directly clickable (not just each project in
   the list). Bio has no dropdown to open, but gets the identical
   underline/grow/fade treatment on hover/focus by reusing this same
   .sam-box-title class/rule (see bio.html) — the two buttons should read
   as behaving the same way, not just Projects. */
.sam-box--projects:hover .sam-box-title,
.sam-box--projects:focus-within .sam-box-title,
.sam-box--projects.is-open .sam-box-title,
.sam-box--Bio:hover .sam-box-title,
.sam-box--Bio:focus-within .sam-box-title {
	text-decoration: underline;
	font-size: 1rem;
	opacity: 0.7;
	/* main.css's global a:hover{color:#47D3E5} otherwise wins over
	   .sam-box-title's own plain color:inherit — same recurring
	   main.css-default-beats-custom-style fix used throughout this file. */
	color: var(--sam-fg) !important;
}

.sam-projects-list {
	flex: 0 0 var(--box-w);
	display: flex;
	flex-direction: column;
	justify-content: center;
	list-style: none;
	margin: 0;
	padding: 0.25rem 0;
}

.sam-projects-list a {
	display: flex;
	flex-direction: column;
	gap: 0.2rem;
	padding: 0.5rem 1rem;
	color: var(--sam-fg);
	text-decoration: none;
}

.sam-projects-list a:hover,
.sam-projects-list a:focus-visible {
	opacity: 0.6;
}

.sam-projects-list-title {
	font-size: 0.75rem;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	transition: font-size 0.2s ease;
}

/* Underline + grow on the individual link's own hover/focus — same
   affordance convention as .sam-box-title (Projects/Bio), scoped per
   topic so hovering "Masters" doesn't also grow "Undergraduate"/"CAD +
   Code". The fade is already covered by .sam-projects-list a:hover above. */
.sam-projects-list a:hover .sam-projects-list-title,
.sam-projects-list a:focus-visible .sam-projects-list-title {
	text-decoration: underline;
	font-size: 0.85rem;
	color: var(--sam-fg) !important;
}

.sam-projects-list-sub {
	font-size: 0.6rem;
	letter-spacing: 0.03em;
	line-height: 1.3;
	opacity: 0.65;
}

.sam-projects-list a.is-spacer {
	pointer-events: none;
	cursor: default;
}

.sam-projects-list-divider {
	margin: 0.5rem 1rem 0.65rem;
	border-top: 1px solid var(--sam-fg);
	opacity: 0.55;
}

/* Mode toggle: back in the nav's top row, sharing it with Home (home page
   only). The row is exactly --box-w wide (matching Projects/Bio below), so
   its left edge lines up with theirs; Home stays a fixed --box-h square on
   the row's right, and the toggle fills the remaining width on the left,
   with a --box-w-independent gap matching the nav's own vertical rhythm. */

.sam-nav-row {
	width: var(--box-w);
	height: var(--box-h);
	display: flex;
	gap: 0.75rem;
}

@media screen and (max-width: 736px) {

	.sam-nav-row {
		gap: 0.5rem;
	}

}

.sam-box--mode-toggle {
	width: auto;
	flex: 1 1 auto;
}

.sam-box--mode-toggle:hover .sam-box-face,
.sam-box--mode-toggle:focus-within .sam-box-face {
	inset: 0;
}

/* Decorative theme-aware border in place of the plain solid stroke: both
   themes reverted back to the original design — a tight, evenly-repeating
   tile (border-image-repeat:round on a small source, slice thirds): a soft
   wave in dark mode, a sharp zigzag in light mode. border-color is
   transparent since the image fully replaces the stroke. */

.sam-box--mode-toggle .sam-box-face {
	border-width: 0.5rem;
	border-style: solid;
	border-color: transparent;
	border-image-slice: 33.34%;
	border-image-repeat: round;
	border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cpath d='M20,10 C23,3 27,3 30,10 C33,17 37,17 40,10' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M20,50 C23,57 27,57 30,50 C33,43 37,43 40,50' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M10,20 C3,23 3,27 10,30 C17,33 17,37 10,40' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M50,20 C57,23 57,27 50,30 C43,33 43,37 50,40' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M10,20 Q10,10 20,10' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M40,10 Q50,10 50,20' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M50,40 Q50,50 40,50' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M20,50 Q10,50 10,40' stroke='white' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
}

html[data-theme="light"] .sam-box--mode-toggle .sam-box-face {
	border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 60'%3E%3Cpath d='M20,16 L30,2 L40,16' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M20,44 L30,58 L40,44' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M16,20 L4,30 L16,40' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M44,20 L56,30 L44,40' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M16,20 L20,16' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M40,16 L44,20' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M44,40 L40,44' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Cpath d='M20,44 L16,40' stroke='black' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* Hero */

.sam-hero {
	position: relative;
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
}

.sam-artifact {
	display: block;
	max-width: min(50vw, 60vh);
	max-height: min(50vw, 60vh);
	/* Decorative only: taps pass through to "Contact me!" and the menu
	   button whenever it drifts over them. */
	pointer-events: none;
	/* Drifts to a random spot across the whole viewport every 5s
	   (initDriftArtifact, home.js) — explicit stacking so it reliably
	   paints over .sam-contact-bar (z-index 3) wherever the two happen
	   to overlap, regardless of DOM order. */
	z-index: 5;
}

/* Bottom contact bar — sits behind the drifting artifact gif above (see
   its own z-index note). Icons follow the same two-file light/dark
   theme swap as .sam-mode-icon/.sam-project-escape-return-icon: each
   pair's "-dark" file is shown while dark theme is active (the default/
   --to-light slot), recolored so its background matches --sam-bg
   exactly (black) rather than using real alpha transparency, same as
   the mode-toggle icons already do. */
.sam-contact-bar {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 2.5rem;
	z-index: 3;
	margin: 0;
	display: flex;
	justify-content: center;
	gap: 3rem;
	color: var(--sam-fg);
	font-size: 1.2rem;
}

.sam-contact-item {
	display: flex;
	align-items: center;
	gap: 0.6rem;
}

/* "Contact me!" is a button (it opens the form below), stripped of
   main.css's global button styling so it reads as the same plain text
   link the email address used to be. */
.sam-contact-open {
	height: auto;
	max-width: none;
	margin: 0;
	padding: 0;
	border: none;
	border-radius: 0;
	background: none;
	box-shadow: none;
	color: inherit !important;
	font: inherit;
	letter-spacing: normal;
	line-height: 1.2;
	text-transform: none;
	white-space: nowrap;
	overflow: visible;
	cursor: pointer;
}

.sam-contact-open:hover,
.sam-contact-open:focus,
.sam-contact-open:active {
	background: none;
	box-shadow: none;
	color: inherit !important;
}

.sam-contact-open:hover span,
.sam-contact-open:focus-visible span {
	text-decoration: underline;
}

.sam-contact-icon {
	display: block;
	height: 1.1em;
	width: auto;
}

.sam-contact-icon--to-dark {
	display: none;
}

html[data-theme="light"] .sam-contact-icon--to-light {
	display: none;
}

html[data-theme="light"] .sam-contact-icon--to-dark {
	display: block;
}

/* The contact form: a plain email-compose window in the site's own look
   (theme colours, thin borders, small uppercase labels). The form fills
   the whole dialog, so a click landing on the <dialog> itself is a click
   on the dimmed backdrop (see the contact block in home.js). */
.sam-contact-dialog {
	box-sizing: border-box;
	width: min(34rem, calc(100vw - 2rem));
	max-height: calc(100vh - 2rem);
	padding: 0;
	border: 1.5px solid var(--sam-fg);
	border-radius: 0;
	background: var(--sam-bg);
	color: var(--sam-fg);
}

.sam-contact-dialog::backdrop {
	background: rgba(0, 0, 0, 0.6);
}

.sam-contact-form {
	display: flex;
	flex-direction: column;
	margin: 0;
}

.sam-contact-head,
.sam-contact-foot {
	display: flex;
	align-items: center;
	gap: 1rem;
	padding: 0.6rem 1rem;
}

.sam-contact-head {
	justify-content: space-between;
	border-bottom: 1px solid var(--sam-fg);
}

.sam-contact-head h2 {
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.8rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

/* Formspree's contact-us layout: each label sits above its field. */
.sam-contact-fields {
	display: flex;
	flex-direction: column;
	gap: 1rem;
	padding: 1.25rem 1rem 0.25rem;
}

.sam-contact-field {
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
}

.sam-contact-label {
	display: block;
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.75rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	line-height: 1.2;
	text-transform: uppercase;
}

/* main.css gives every input/textarea a rounded box, fixed height and a
   blue focus ring; here they're thin-bordered boxes in the theme colours. */
.sam-contact-form input,
.sam-contact-form textarea {
	box-sizing: border-box;
	width: 100%;
	height: auto;
	margin: 0;
	padding: 0.55rem 0.7rem;
	border: 1px solid rgba(128, 128, 128, 0.6);
	border-radius: 0;
	background: none;
	box-shadow: none;
	color: var(--sam-fg);
	font: inherit;
	font-size: 0.95rem;
	line-height: 1.4;
}

.sam-contact-form input:focus,
.sam-contact-form textarea:focus {
	border-color: var(--sam-fg);
	box-shadow: none;
	outline: none;
}

.sam-contact-message {
	min-height: 9rem;
	resize: vertical;
}

.sam-contact-help {
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.8rem;
	line-height: 1.4;
	opacity: 0.7;
}

/* Spam trap: invisible to people, filled in by bots, which Formspree
   then discards. */
.sam-contact-trap {
	position: absolute !important;
	left: -9999px;
	width: 1px !important;
	height: 1px !important;
	opacity: 0;
}

.sam-contact-foot {
	justify-content: space-between;
	border-bottom: none;
}

.sam-contact-status {
	margin: 0;
	font-size: 0.8rem;
}

.sam-contact-close,
.sam-contact-send {
	height: auto;
	max-width: none;
	margin: 0;
	border-radius: 0;
	box-shadow: none;
	color: var(--sam-fg) !important;
	font-family: inherit;
	line-height: 1;
	cursor: pointer;
}

.sam-contact-close {
	width: 2.5rem;
	padding: 0.4rem 0;
	border: none;
	background: none;
	font-size: 1.5rem;
	letter-spacing: 0;
}

.sam-contact-send {
	flex: 0 0 auto;
	padding: 0.75rem 1.5rem;
	border: 1.5px solid var(--sam-fg);
	background: none;
	font-size: 0.8rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

.sam-contact-close:hover,
.sam-contact-close:focus,
.sam-contact-close:active,
.sam-contact-send:hover,
.sam-contact-send:focus,
.sam-contact-send:active {
	background: none;
	box-shadow: none;
	color: var(--sam-fg) !important;
	opacity: 0.7;
}

.sam-contact-send:disabled {
	opacity: 0.4;
	cursor: default;
}

.sam-hero.is-animated .sam-artifact {
	position: absolute;
	transition: left 1.2s ease, top 1.2s ease;
}

/* Phones: the gif fills the screen width and drifts anywhere, partly
   off-screen at times (initDriftArtifact, home.js); the hero clips it so
   the page never scrolls. */
@media screen and (max-width: 736px) {

	.sam-hero {
		overflow: hidden;
	}

	.sam-artifact {
		width: 100vw;
		height: auto;
		max-width: none;
		max-height: none;
	}

}

/* Bio page */

.sam-Bio-home {
	position: relative;
	width: 100vw;
	height: 100vh;
	overflow-y: auto;
}

/* Binary snap-scroll between exactly two full-viewport sections — nav +
   heading + frame 1 + text, then frame 2 alone (for now). Plain CSS
   scroll-snap (no custom JS wheel-interception) — modern browsers snap
   natively and smoothly on wheel/trackpad/touch/keyboard with just this,
   and it sidesteps every class of bug a custom JS snap runs into.
   .sam-bio-snap-home cancels .sam-Bio-home's own flex/padding (each
   section reapplies its own instead, matching Masters' own section-snap
   pattern elsewhere in this file). */

.sam-bio-snap-home {
	padding: 0;
	scroll-snap-type: y mandatory;
}

.sam-bio-section {
	box-sizing: border-box;
	width: 100%;
	height: 100vh;
	scroll-snap-align: start;
	position: relative;
}

.sam-bio-section--main,
.sam-bio-section--secondary,
.sam-bio-section--tertiary {
	display: flex;
	flex-direction: column;
	padding: 11rem 2.5rem 2.5rem 2.5rem;
}

/* Nav/heading "locked to section 1" — position:absolute (relative to
   this section, which is now their nearest positioned ancestor) instead
   of the shared position:fixed rules below, at the same visual offsets,
   so they scroll away with section 1 rather than persisting into
   section 2 the way a viewport-fixed element would. */
.sam-bio-section--main .sam-nav {
	position: absolute;
	top: 2.5rem;
	right: 2.5rem;
}

/* Same three-track system the Projects detail page uses: the 32vw square
   sits in the centred middle track and its companion goes in a side track,
   so the square is always page-centred and the two can never overlap
   (the grid gives each its own column rather than letting them collide).
   Absolutely positioned and centred on the page, which also replaces the
   old hard `top: -150px` nudge — the heading and nav are already absolute
   and the scroll cue lives outside <main>, so this leaves each section with
   no in-flow children. minmax(0, 1fr) on the side tracks is what keeps them
   equal: a bare 1fr takes its content's min-content width as an automatic
   minimum, and the photo would otherwise widen its own side and push the
   square off centre. */
.sam-Bio-columns {
	position: absolute;
	top: 50%;
	left: 2.5rem;
	right: 2.5rem;
	transform: translateY(-50%);
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
	align-items: center;
}

/* The text square is the centred element in both sections. */
.sam-Bio-col--text {
	grid-column: 2;
	display: flex;
	flex-direction: column;
}

/* Photo keeps the side it's already on — left in section 1, right in
   section 2 — 20px from the square, matching the Projects page's own
   DETAIL_GAP. max-width keeps the frame inside its track on narrower
   viewports, so it shrinks rather than overlapping the square. */
.sam-Bio-col--photo {
	position: relative;
	max-width: 100%;
}

.sam-bio-section--main .sam-Bio-col--photo {
	grid-column: 1;
	justify-self: end;
	margin-right: 20px;
	/* Nudged 10px down from the row's vertical center, per request. On
	   the wrapping column rather than .sam-Bio-photo itself, which
	   already carries its own transform:scale(0.7). */
	transform: translateY(10px);
}

.sam-bio-section--secondary .sam-Bio-col--photo,
.sam-bio-section--tertiary .sam-Bio-col--photo {
	grid-column: 3;
	justify-self: start;
	margin-left: 20px;
}

/* Restores the reading tile's old vertical position from before the CV
   tile was split out into its own separately-positioned
   .sam-bio-cv-tilegroup (see the +5.5rem there for the matching other
   half of this): the two used to sit stacked in a 2x1 flex column (10rem
   each + 1rem gap = 21rem total), centered as a pair, which put the top
   item (this one) 5.5rem above the row's center line. Scoped to section
   2 specifically — section 3's dishes tilegroup was never part of that
   stack and is already correctly centered on its own. */
.sam-bio-section--secondary .sam-bio-tilegroup {
	transform: translateY(-5.5rem);
}

/* cursor:pointer since it's now click-to-activate too, like section 2's
   tiles — see the bio-active-stage block in home.js. */
.sam-Bio-photo {
	display: block;
	width: var(--Bio-photo-w);
	aspect-ratio: 3 / 4;
	object-fit: cover;
	transform: scale(0.7);
	cursor: pointer;
}

/* Section 2's resting state: two small SQUARE tiles in a 2x1 column, like
   the Projects page's 3x3 grid cells — .sam-frame's own base width
   (--frame-w), 3:4 aspect and scale(0.7) are all overridden here. Sized to
   fit both tiles within the footprint the section used to occupy when it
   held a single frame: that frame was --Bio-photo-w (24rem) at 3:4 then
   scale(0.7), i.e. a 16.8rem x 22.4rem box. Two squares of side S stacked
   with this 1rem gap need 2*S + 1rem <= 22.4rem (the binding constraint —
   S also needs to clear 16.8rem width, which a height-driven S clears
   easily) => S <= 10.7rem; 10rem leaves a comfortable margin on both axes. */
.sam-bio-tile-list {
	display: flex;
	flex-direction: column;
	gap: 1rem;
	/* margin-left is set inline by alignBioTileList in home.js, not here:
	   it has to line the tiles' left edge up with section 1's photo, which
	   sits in a different grid column (track 1) on the other side of the
	   text column (track 2, auto-sized to content) from this tile group
	   (track 3) — an offset CSS calc() can't express, since the grid's
	   auto column width isn't readable from CSS, and the site's root
	   font-size (so every rem-based measurement here) changes at several
	   breakpoints (see main.css), not smoothly. Reading both elements'
	   live rects and diffing them is the only way this stays correct
	   across viewport widths. */
}

/* .sam-frame.sam-bio-tile (compound, not just .sam-bio-tile): .sam-frame's
   own width/aspect-ratio rule ([home.css] further down) is equal
   specificity and later in the file, so it would otherwise win the
   cascade over a plain .sam-bio-tile selector. */
.sam-frame.sam-bio-tile {
	width: 10rem;
	aspect-ratio: 1 / 1;
	transform: none;
	cursor: pointer;
	overflow: hidden;
}

/* No border on any of the three resting tiles, in either theme —
   .sam-frame's border already uses the theme-reactive var(--sam-fg), so
   removing it outright covers both modes with one rule. Compound
   selectors to match .sam-frame.sam-bio-tile's own specificity rather
   than relying on source order to beat the bare .sam-frame rule above. */
.sam-frame.sam-bio-tile--reading,
.sam-frame.sam-bio-tile--dishes,
.sam-frame.sam-bio-tile--cv,
.sam-frame.sam-bio-tile--cert {
	border: none;
}

/* Phone-only page spacers and certificate text (see the phone bio block
   below); desktop shows the certificate as a tile only. */
.sam-bio-page,
.sam-bio-cert-text {
	display: none;
}

/* Positioned independently of .sam-Bio-columns' grid — needs to sit on
   the mirror-opposite side from wherever the reading tile's tile-list
   currently is (which itself is JS-realigned away from its own natural
   grid column, see alignBioTileList), so it can't just live in track 3
   alongside it. .sam-bio-section is already position:relative, making it
   a valid containing block. `right` is set inline by alignCvTileGroup in
   home.js, mirroring the reading tile's own live-measured left offset.

   The +5.5rem on top of the self-centering translateY(-50%) restores
   this tile's old vertical position from before it was split out of the
   reading tile's tile-list: the two used to sit stacked in a 2x1 flex
   column (10rem each + 1rem gap = 21rem total), centered as a pair, which
   put the bottom item (this one) 5.5rem below the row's center line —
   see the matching -5.5rem on .sam-bio-section--secondary
   .sam-bio-tilegroup below for the reading tile's own (opposite) half of
   this. */
.sam-bio-cv-tilegroup {
	position: absolute;
	top: 50%;
	/* +20px on top of the 5.5rem restore offset, per a later request. */
	transform: translateY(calc(-50% + 5.5rem + 20px));
}

/* CV tile's resting thumbnail: a single static image (no frame-sync
   animation — just a document icon), cropped to fill the square the same
   way the gif tiles are. Theme-swapped dark/light pair like the gif
   tiles' own (.sam-bio-tile-gif-img--dark/--light) — "dark" here means
   "shown in dark mode", not that the image itself is dark; it's actually
   the lighter (white bg/blue page) of the two. */
.sam-bio-tile-cv-img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.sam-bio-tile-cv-img--light {
	display: none;
}

html[data-theme="light"] .sam-bio-tile-cv-img--dark {
	display: none;
}

html[data-theme="light"] .sam-bio-tile-cv-img--light {
	display: block;
}

/* Tile 1's resting gif: fills its square frame, cropped like the Projects
   page's own resting-tile thumbnails (.sam-gallery-frame-img). Theme-swapped
   pair (black-padded frames for dark mode, white-padded for light) rather
   than a single asset — same reasoning and pattern as Undergraduate's own
   dark/light seagull pair (.sam-projects-bg-artifact--dark/--light): a
   plain animated <img src=.gif> per theme would each play on its own
   clock, so toggling theme could land on a different frame than the one
   you were just looking at. Driven from static per-frame PNGs instead
   (images/bio-reading-frames/dark_N.png / light_N.png) by one shared
   interval in home.js, so both variants always show the same instant. */
.sam-bio-tile-gif-img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.sam-bio-tile-gif-img--light {
	display: none;
}

html[data-theme="light"] .sam-bio-tile-gif-img--dark {
	display: none;
}

/* The dishes gif's canvas is an 800x800 square built from 4:3/3:4 source
   photos, so every frame letterboxes to exactly 25% padding on one axis
   — object-fit:cover alone can't crop that away, since the frame already
   matches the tile's own square aspect (nothing for "cover" to crop).
   Scaling up by 4/3 pushes that padding fully outside the tile; the
   overflow:hidden on .sam-frame.sam-bio-tile above clips it, so the
   resting tile reads as cropped-to-fill. The grown view
   (.sam-bio-active-gif-img) is untouched — full letterboxed frame,
   uncropped, as before. */
.sam-bio-tile--dishes .sam-bio-tile-gif-img {
	transform: scale(1.334);
}

html[data-theme="light"] .sam-bio-tile-gif-img--light {
	display: block;
}

/* Shared full-viewport activated view for all 3 bio images (section 1's
   photo, section 2's gif tile, section 2's second tile) — opened/closed by
   the bio-active-stage block in home.js, which fades .sam-bio-void-fade in
   then back out around the swap (see projJumpWithFade above for the same
   two-leg technique). Sized "large" rather than matched to any specific
   box — max-width/max-height keep it clear of the hint/stepper and the
   viewport edges on any screen. */
.sam-bio-void-fade {
	position: fixed;
	inset: 0;
	background: var(--sam-bg);
	opacity: 0;
	pointer-events: none;
	z-index: 200;
}

.sam-bio-active-stage {
	position: fixed;
	inset: 0;
	z-index: 150;
	background: var(--sam-bg);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
}

.sam-bio-active-stage[hidden] {
	display: none;
}

.sam-bio-active-img,
.sam-bio-active-placeholder {
	display: block;
	max-width: 85vw;
	max-height: 80vh;
}

.sam-bio-active-img {
	width: auto;
	height: auto;
	/* Wide images (the certificate) must clear the "Click off" hint on
	   the left (2.5rem in, ~20rem long, plus a 1.5rem gap = 24rem) once
	   centred and shifted 130px right: 50vw + 130px - width/2 >= 24rem.
	   The portrait photo is height-limited, so it's unaffected. */
	max-width: min(85vw, calc(100vw + 260px - 48rem));
	object-fit: contain;
	position: relative;
	left: 130px;
}

.sam-bio-active-img[hidden],
.sam-bio-active-placeholder[hidden] {
	display: none;
}

/* No real size of its own to derive from (there's no image loaded when a
   slide is empty) — a plain square, same idea as the site's other
   not-yet-populated placeholders. */
.sam-bio-active-placeholder {
	width: 60vh;
	height: 60vh;
	border: 1.5px solid var(--sam-fg);
}

/* Section 2's two gifs, activated — .sam-bio-active-gif--reading/--dishes
   are separate containers (one per gif, see the bio-reading/bio-dishes
   frame blocks in home.js) rather than one shared element whose src gets
   rewritten, since each has its own independent frame count/index; only
   one is ever un-hidden at a time. Each holds the same frame-synced
   dark/light pair as its resting tile (see .sam-bio-tile-gif-img above),
   just sized/fitted like .sam-bio-active-img instead of cropped to a
   square. Hiding the whole container (rather than relying on [hidden] on
   the individual images) sidesteps having to fight the dark/light display
   rules' specificity with an equally-specific [hidden] rule. */
.sam-bio-active-gif[hidden] {
	display: none;
}

.sam-bio-active-gif-img {
	display: block;
	max-width: 85vw;
	max-height: 80vh;
	width: auto;
	height: auto;
	object-fit: contain;
	position: relative;
	left: 130px;
}

.sam-bio-active-gif-img--light {
	display: none;
}

html[data-theme="light"] .sam-bio-active-gif-img--dark {
	display: none;
}

html[data-theme="light"] .sam-bio-active-gif-img--light {
	display: block;
}

.sam-bio-active-cv[hidden] {
	display: none;
}

/* Nudged right of dead-center: at typical desktop widths the flex-
   centered iframe's left edge otherwise overlaps .sam-bio-active-hint,
   pinned to the left margin (confirmed — the two visibly collided).
   A plain relative offset applied after normal flex centering, so it's
   an exact, unambiguous +130px regardless of viewport width — unlike
   adjusting the flex item's own margin, which centering would only pass
   half of through to the visible position. */
.sam-bio-active-cv {
	position: relative;
	left: 130px;
}

/* iframes have no intrinsic size the way images do, so explicit
   dimensions instead of the gif tiles' max-width/max-height+auto pattern
   — sized as a readable document-reading pane. Bordered since it's plain
   white document content floating in the dark/light stage background,
   unlike the gif views which have no border. */
.sam-bio-active-cv-iframe {
	display: block;
	width: min(60vw, 800px);
	height: 80vh;
	border: 1.5px solid var(--sam-fg);
}

/* Same position/style as Projects' own idle status text (.sam-topic-status
   + .sam-hover-timer-idle) — vertically centered, pinned to the page's
   left margin — reimplemented here rather than sharing that class, since
   its Projects-specific rules elsewhere (position:fixed once a project's
   active, visibility driven by Projects-only JS) don't apply to this page. */
.sam-bio-active-hint {
	position: absolute;
	top: 50%;
	left: 2.5rem;
	transform: translateY(-50%);
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.85rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	pointer-events: none;
}

/* Deliberately the 3x3 grid's own footprint (see
   body.sam-page--projects .sam-gallery-frames--projects-grid), so the Bio
   page and the Projects page read as the same system. */
.sam-Bio-text {
	width: 32vw;
	height: 32vw;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	justify-content: center;
}

.sam-Bio-text p {
	margin: 0 0 1em 0;
	line-height: 1.6;
	color: var(--sam-fg);
	opacity: 0.7;
	text-align: justify;
	font-weight: 700;
}

/* main.css (the template base, loaded before this file) hardcodes
   `a { color: #000000; }` with no theme awareness — override it here
   with the same theme-reactive color the rest of this text already uses,
   so links read white in dark mode instead of staying black. */
.sam-Bio-text a {
	color: var(--sam-fg);
	font-weight: 700;
}

.sam-Bio-text p:last-child {
	margin-bottom: 0;
}

/* Section 2 holds the most text (reading + CV), which overran the fixed
   32vw square on shorter screens and clipped "Download my CV (PDF)" —
   it grows to fit instead (still centred, as the columns are). */
.sam-bio-section--secondary .sam-Bio-text {
	height: auto;
	min-height: 32vw;
}

/* Section 2's text is split into a reading block and a CV block (so the
   phone layout can reorder them); the reading block's last paragraph
   keeps its usual gap, since it isn't the end of the text on desktop. */
.sam-bio-reading-text p:last-child {
	margin-bottom: 1em;
}

/* Generic empty bordered frame — shared by the bio page's placeholder
   frame and the project gallery pages, so they all stay the same size. */

.sam-frame {
	width: var(--frame-w);
	aspect-ratio: 3 / 4;
	border: 1.5px solid var(--sam-fg);
	transform: scale(0.7);
	transition: transform 0.25s ease;
}

@media screen and (max-width: 736px) {

	.sam-Bio-home {
		--Bio-photo-w: 18rem;
	}

	.sam-bio-section--main,
	.sam-bio-section--secondary,
	.sam-bio-section--tertiary {
		padding: 1.5rem;
		gap: 1.5rem;
	}

	.sam-bio-section--main .sam-nav {
		top: 1.5rem;
		right: 1.5rem;
	}

	/* Back to a single stacked column on mobile — the three-track centring
	   above has no room to work at this width, and the absolute centring
	   would clip a stacked column taller than the viewport. */
	.sam-Bio-columns {
		position: static;
		transform: none;
		display: flex;
		flex-direction: column;
		gap: 1.5rem;
	}

	.sam-bio-section--main .sam-Bio-col--photo,
	.sam-bio-section--secondary .sam-Bio-col--photo,
	.sam-bio-section--tertiary .sam-Bio-col--photo {
		grid-column: auto;
		justify-self: auto;
		margin-left: 0;
		margin-right: 0;
	}

	.sam-Bio-col--text {
		width: 100%;
		margin-top: 0;
	}

	.sam-Bio-text {
		width: 100%;
		height: auto;
		overflow: visible;
	}

	.sam-Bio-photo {
		width: 60vw;
	}

	/* Phone bio: sections size to their stacked content, and scrolling
	   is locked to whole pages: mandatory snapping always settles on a
	   page, and snap-stop makes a flick move exactly one page. */
	.sam-bio-snap-home {
		scroll-snap-type: y mandatory;
	}

	.sam-bio-section {
		height: auto;
		min-height: 100vh;
		min-height: 100svh;
		scroll-snap-stop: always;
	}

	/* Section 1: photo and text sit ~30px lower. */
	.sam-bio-section--main {
		padding-top: calc(1.5rem + 30px);
	}

	/* Phone pages, one screen each, contents centred vertically:
	   1 title, photo, intro (section 1, unchanged)
	   2 CV text + link, CV tile          (section 2)
	   3 reading gif, reading text        (section 2)
	   4 dishes gif, food text            (section 3)
	   5 certificate, its text            (section 3)
	   Sections 2 and 3 become a one-column grid with the desktop columns
	   flattened (display:contents). Each page is four auto rows — empty,
	   two content rows, empty — and an empty .sam-bio-page spacer spans
	   them at a full screen's height. Its leftover height is shared
	   equally by the four rows, and with the first item pushed to the
	   bottom of its row and the second to the top of its own, the pair
	   meets in the middle with equal space above and below. (1fr rows
	   would be equal across BOTH pages, so they couldn't each centre.)
	   The spacers are the snap points. */
	.sam-bio-section--secondary,
	.sam-bio-section--tertiary {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: repeat(8, auto);
		justify-items: center;
		row-gap: 0;
		min-height: 0;
		padding: 0 1.5rem;
		scroll-snap-align: none;
	}

	.sam-bio-section--secondary .sam-Bio-columns,
	.sam-bio-section--secondary .sam-Bio-col:not(.sam-bio-tilegroup),
	.sam-bio-section--secondary .sam-Bio-text,
	.sam-bio-section--tertiary .sam-Bio-columns,
	.sam-bio-section--tertiary .sam-Bio-col:not(.sam-bio-tilegroup),
	.sam-bio-section--tertiary .sam-Bio-text {
		display: contents;
	}

	.sam-bio-page {
		display: block;
		grid-column: 1;
		align-self: stretch;
		justify-self: stretch;
		min-height: 100vh;
		min-height: 100svh;
		scroll-snap-align: start;
		scroll-snap-stop: always;
		pointer-events: none;
	}

	.sam-bio-page--1 {
		grid-row: 1 / 5;
	}

	.sam-bio-page--2 {
		grid-row: 5 / 9;
	}

	/* The cues read wrongly once the content is split into pages. */
	.sam-bio-section--secondary .sam-scroll-cue,
	.sam-bio-section--tertiary .sam-scroll-cue {
		display: none;
	}

	/* Each page's leading blank paragraph would push its text off
	   centre. */
	.sam-bio-cv-text > p:first-child,
	.sam-bio-reading-text > p:first-child,
	.sam-bio-food-text > p:first-child {
		display: none;
	}

	.sam-bio-cv-text,
	.sam-bio-reading-text,
	.sam-bio-food-text,
	.sam-bio-cert-text {
		width: 100%;
		grid-column: 1;
	}

	/* Page 2: CV text, then the CV tile. */
	.sam-bio-cv-text {
		grid-row: 2;
	}

	/* !important beats the inline right/margin-left alignCvTileGroup and
	   alignBioTileList (home.js) set to place the tiles on desktop. */
	.sam-bio-cv-tilegroup {
		position: static;
		right: auto !important;
		transform: none;
		grid-column: 1;
		margin-top: 2rem;
	}

	.sam-bio-section--secondary .sam-bio-cv-tilegroup {
		grid-row: 3;
	}

	/* Page 3: reading gif, then its text. */
	.sam-bio-section--secondary .sam-bio-tilegroup {
		grid-column: 1;
		grid-row: 6;
		transform: none;
	}

	.sam-bio-reading-text {
		grid-row: 7;
		margin-top: 2rem;
	}

	/* Page 4: dishes gif, then the food text. */
	.sam-bio-section--tertiary .sam-bio-tilegroup {
		grid-column: 1;
		grid-row: 2;
	}

	.sam-bio-food-text {
		grid-row: 3;
		margin-top: 2rem;
	}

	/* Page 5: the certificate, then its text. */
	.sam-bio-section--tertiary .sam-bio-cv-tilegroup {
		grid-row: 6;
		margin-top: 0;
	}

	.sam-bio-cert-text {
		display: block;
		grid-row: 7;
		margin-top: 2rem;
	}

	/* The edX paragraph sits in the food text on desktop; phones show it
	   on the certificate's own page instead, so it's hidden here. */
	.sam-bio-desktop-only {
		display: none;
	}


	.sam-bio-cv-text,
	.sam-bio-section--secondary .sam-bio-tilegroup,
	.sam-bio-section--tertiary .sam-bio-tilegroup,
	.sam-bio-section--tertiary .sam-bio-cv-tilegroup {
		align-self: end;
	}

	.sam-bio-section--secondary .sam-bio-cv-tilegroup,
	.sam-bio-reading-text,
	.sam-bio-food-text,
	.sam-bio-cert-text {
		align-self: start;
	}

	.sam-bio-section--secondary .sam-bio-tile-list,
	.sam-bio-section--tertiary .sam-bio-tile-list {
		margin-left: 0 !important;
	}

	/* The bio's gif and CV tiles share one square size on phones. */
	.sam-frame.sam-bio-tile {
		width: min(60vw, 40vh);
		width: min(60vw, 40svh);
	}

}

/* Scroll cue: shown at the bottom of the initial screen on scrollable
   pages, fades out once the user scrolls away from the top and back in
   when they return to it (see home.js). */

.sam-scroll-cue {
	position: fixed;
	left: 50%;
	bottom: 2rem;
	transform: translateX(-50%);
	z-index: 60;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.5rem;
	color: var(--sam-fg);
	font-size: 0.7rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	opacity: 1;
	transition: opacity 0.3s ease;
	pointer-events: none;
}

.sam-scroll-cue.is-hidden {
	opacity: 0;
}

/* Per-section cues (bio sections 2/3's "scroll up"/"scroll down" hints):
   plain instances living inside their own section rather than the single
   page-level fixed one above — same pattern already used for the
   Projects page's own per-section .sam-scroll-cue (home.css, "SCROLL
   DOWN TO SEE MORE belongs to page 1..." below). Each bio section is
   exactly 100vh with snap-scroll between them, so an element placed
   inside one is visible for the entire time that section is current —
   no JS show/hide needed, unlike the fixed cue's scrollTop-driven
   fade. */
.sam-bio-section .sam-scroll-cue {
	position: absolute;
}

.sam-scroll-cue--top {
	top: 2.5rem;
	bottom: auto;
}

.sam-scroll-cue--bottom {
	bottom: 2rem;
}

.sam-scroll-cue-arrow {
	font-size: 1rem;
	line-height: 1;
	animation: sam-scroll-bounce 1.5s ease-in-out infinite;
}

.sam-scroll-cue span:not(.sam-scroll-cue-arrow) {
	font-weight: 700;
}

@keyframes sam-scroll-bounce {
	0%, 100% { transform: translateY(0); }
	50% { transform: translateY(6px); }
}

/* Project pages */

/* Standard visually-hidden pattern — keeps each figlet heading's real text
   available to screen readers/search without showing up visually (the
   ascii art in the sibling <pre> is aria-hidden instead). */
.sam-visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* Figlet ascii-art headings (see gen_figlet.py-equivalent output baked
   into each page, or injected by JS for the loading-gate instances) —
   replaces the old plain top-left text heading. Horizontally centered on
   every page. Vertically: the Projects intro page and the 3 topic
   loading-gate pages all use --loading (upper area) since each has other
   centered content below it (the topic-jump buttons; the centered gif +
   loading bar/dots) that a dead-center heading would collide with — Bio
   and the grid pages keep the original plain top offset for the same
   reason (a photo/text column or a 3x3 grid still needing room
   underneath). */
.sam-project-heading {
	position: absolute;
	left: 50%;
	top: 2.5rem;
	transform: translateX(-50%);
	/* Above the topic loading-overlay (z-index 90, see .sam-topic-loading-
	   overlay) so the heading stays visible both under and after it — the
	   overlay covers the grid/stage/stepper beneath, never the heading. */
	z-index: 95;
	margin: 0;
	text-align: center;
	color: var(--sam-fg);
	/* Purely decorative text — never intercept clicks meant for whatever
	   sits near/behind it (found via the Projects intro page's centered
	   heading blocking its own topic-jump buttons underneath). */
	pointer-events: none;
}

.sam-project-heading--loading {
	top: 12%;
}

/* Gradient-filled ascii art — a -webkit-text-stroke outline was tried
   first, but at the font sizes these dense figlet arts need to fit on
   screen each character's own stroke is only 1-2px wide, so a 1px
   outline consumed the entire glyph and left nothing for a fill to show
   through. background-clip:text lets the gradient (--ascii-gradient,
   theme-scoped) show through the glyph shapes directly, the same trick
   a "gradient text" effect always uses.

   Legibility fix (colossal's own density, not a font swap): colossal's
   glyphs are built from several overlapping block characters (8/d/b/P)
   per stroke — hinted/aliased at only a few pixels tall, adjacent
   characters blur into noise. Rendered here at a large, fixed, crisp
   font-size instead (2.2rem — plenty of pixels for every character's
   shape to stay distinct), then shrunk to the size the home actually
   needs via transform:scale() rather than by setting a tiny font-size
   directly — the browser composites the scale-down from an already-
   crisp layer, the same principle as supersampling, and reads far more
   clearly than native small-size text hinting does for this font.
   transform-origin:top center keeps the shrink centered/top-anchored,
   matching how the box was already positioned (h1's own translateX(-50%)
   centering is unaffected, since it centers the *unscaled* home box,
   and scaling this child around its own horizontal center keeps that
   same point fixed).

   --heading-scale (set inline per-heading, see projects.html/bio.html)
   is this transform factor now, not a font-size multiplier — computed
   per heading from that word's own colossal line count (some words
   render 8 lines, others — those with descenders, e.g. Projects/
   Undergraduate — render 11) so every heading's shrunk height fits the
   same vertical budget above the grid regardless of its own line count.
   Defaults to 1 (unchanged) where unset. */
.sam-project-heading pre {
	margin: 0;
	font-family: 'Courier New', Courier, monospace;
	font-size: 2.2rem;
	font-weight: bold;
	line-height: 1.15;
	white-space: pre;
	/* main.css's global h1{letter-spacing:-0.05em} rule otherwise
	   inherits down as a computed *pixel* value (resolved against the
	   h1's own, much larger, default font-size — not this pre's 2.2rem),
	   quietly eating a chunk off every character's advance width and
	   drifting long lines out of column alignment with shorter ones —
	   same recurring main.css-default-beats-custom-style bug this file
	   keeps hitting, just via inheritance instead of specificity this
	   time. */
	letter-spacing: normal;
	transform-origin: top center;
	transform: scale(var(--heading-scale, 1));
	color: transparent;
	background-image: var(--ascii-gradient);
	-webkit-background-clip: text;
	background-clip: text;
	-webkit-text-fill-color: transparent;
}

/* Light mode only: a thin stroke on the same glyphs, thickening each
   character's strokes slightly for a bit more perceived weight/contrast
   against the white background. Safe here (unlike an earlier attempt at
   this file's much smaller *native* font-sizes, where a stroke consumed
   the entire glyph) since this is the crisp 2.2rem render, well before
   the transform:scale() shrink — a sub-1px stroke is a small fraction of
   each character's own stroke width at that size. */
html[data-theme="light"] .sam-project-heading pre {
	-webkit-text-stroke: 0.6px #000000;
}

/* The active-project plain-text heading (State D — see showActive/
   activate in home.js) reuses the same gradient fill, just without the
   monospace/pre-formatting since it's ordinary prose text, not ascii art. */
.sam-project-heading-active {
	display: block;
	margin: 0;
	font-size: 2.25rem;
	font-weight: bold;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: transparent;
	background-image: var(--ascii-gradient);
	-webkit-background-clip: text;
	background-clip: text;
	-webkit-text-fill-color: transparent;
}

.sam-project-heading-active[hidden] {
	display: none;
}

@media screen and (max-width: 736px) {

	.sam-project-heading {
		top: 1.5rem;
	}

	.sam-project-heading pre {
		/* Same crisp-then-shrink technique, extra-scaled down for the
		   narrower viewport — font-size stays the fixed 2.2rem crisp
		   render size (unchanged from desktop). */
		transform: scale(calc(var(--heading-scale, 1) * 0.6));
	}

	.sam-project-heading-active {
		font-size: 1.4rem;
	}

}

.sam-gallery-frames {
	/* Same gap as bio's image↔text column, and (for the default, evenly-
	   sized case) the row's overall width matches bio's span from the
	   image's left edge to the text box's right edge. Rows using the
	   wide/large modifiers below size naturally instead. */
	--gallery-gap: calc(6rem - 8px);
	--gallery-span: calc(var(--Bio-photo-w) + var(--gallery-gap) + 28rem);
	--gallery-frame-w: calc((var(--gallery-span) - 2 * var(--gallery-gap)) / 3);
	display: flex;
	align-items: flex-start;
	gap: var(--gallery-gap);
}

.sam-gallery-frame {
	display: block;
	flex: none;
	overflow: hidden;
	cursor: pointer;
	transition: opacity 0.2s ease;
}

/* Clicking a gallery frame grows it (via growFrame in home.js) and hides
   every other frame in the row (is-hidden, below) instead of just dimming
   this one — the grown frame + its side detail box become the sole focus. */

.sam-gallery-frame.is-hidden {
	opacity: 0;
	pointer-events: none;
}

.sam-gallery-frame-img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* Masters' two pages (a static featured frame + text, then the "all
   projects" grid) each reapply the site's standard 11rem/2.5rem page
   margin themselves, matching every other topic's own two pages. */

.sam-masters-section {
	box-sizing: border-box;
	width: 100%;
	height: 100vh;
	/* Top padding reduced from 11rem — shifts the 3x3 grid (the only
	   normal-flow content in this padding box; the loading-gate sections'
	   own children are all position:absolute, so they're unaffected by
	   this) up, away from the section's bottom edge, per request. Still
	   clears the heading above it (~6rem tall at its current figlet
	   font-size, sitting at top:2.5rem) with a bit of headroom. */
	padding: 9rem 2.5rem 2.5rem 2.5rem;
	scroll-snap-align: start;
}

/* Projects page: single continuous snap-scroll page merging Undergraduate,
   Masters, and CAD + Code into 6 sections — each topic standardised to
   the same 2-page pattern (page 1: heading/nav/background artifact/
   "scroll down" cue; page 2: a 3x3 grid) — see projects.html. Same sudden
   "suction" snap as Bio's own page (home.js), with the scroll-snap-type
   below kept only as a fallback for input the JS doesn't intercept
   (keyboard, scrollbar drag, touch swipe). Masters' own .sam-masters-section
   rule above is reused as-is for all six sections (it's self-contained —
   own padding, own height:100vh) rather than duplicating it per topic. */

.sam-projects-snap-home {
	position: relative;
	width: 100vw;
	height: 100vh;
	overflow-y: auto;
	/* Without an explicit overflow-x, the CSS overflow spec computes it to
	   auto too (since overflow-y is scrolling) — any stray 1px of content
	   overflow anywhere in a section then silently grows a horizontal
	   scrollbar for the whole home. Locked to hidden since nothing in
	   these sections is meant to scroll sideways. */
	overflow-x: hidden;
	scroll-snap-type: y mandatory;
}

.sam-projects-section {
	box-sizing: border-box;
	width: 100%;
	height: 100vh;
	scroll-snap-align: start;
	position: relative;
}


/* Full-project view (State D) becomes its own two-page world: every other
   topic section drops out of the scroll flow, so the active one is the
   container's only child and its page 1 always sits at scrollTop 0 —
   whichever topic it belongs to. Previously the page stayed a plain
   4-section snap document underneath, which left the detail page
   unreachable and (on CAD + Code, the last section) meant the only
   available scroll direction was back up. See lockProjectScroll/
   unlockProjectScroll and the wheel handler's locked branch in home.js. */
.sam-projects-snap-home.is-project-locked .sam-projects-section {
	display: none;
}

.sam-projects-snap-home.is-project-locked .sam-projects-section.is-project-active {
	display: block;
}

/* Full-project mode is a single fixed view now (only .is-project-active
   stays in flow above) — nothing left to scroll to. Without this, any
   stray sub-pixel overflow (fractional vh/rem values are common in this
   stylesheet) can still tip scrollHeight a hair past clientHeight and
   make the browser draw a scrollbar here that does nothing useful.
   lockProjectScroll/unlockProjectScroll (home.js) don't rely on this
   container staying scrollable while locked. */
.sam-projects-snap-home.is-project-locked {
	overflow: hidden;
}

/* The 9 tiles are already invisible in State D; taking their wrapper out of
   flow entirely too avoids any stray layout influence from the (now
   fixed-positioned, out-of-flow) .sam-project-detail sitting alongside it. */
.sam-projects-section.is-project-active .sam-gallery-frames {
	display: none;
}

/* "Go back" (and the escape button beside it, which is positioned from this
   bubble's own rect) stays fixed on screen while full-project mode is open. */
.sam-projects-section.is-project-active .sam-topic-status {
	position: fixed;
}

/* Same "locked to own section" treatment for the hover-timer/idle-cue
   widget — otherwise position:fixed at one shared viewport spot, so
   Undergraduate's and Masters' own copies would render on top of each
   other (and both idle cues read "Click on a project..." simultaneously)
   regardless of which section is actually in view. (The escape button's
   own per-section positioning is handled entirely by its
   --cancel/--close state modifiers now — see below.) */
.sam-projects-section .sam-scroll-cue {
	position: absolute;
	left: 50%;
	bottom: 2rem;
}


/* Every topic's own page 1 (Undergraduate/Masters/CAD + Code) and page 2
   share the site's standard page margin — Masters' own .sam-masters-section
   rule already provides this; the other two topics' sections reuse it too
   (see projects.html, which puts .sam-masters-section on all six section
   elements now, not just Masters' own two) so this rule needs no topic-
   specific duplicates. */

/* Background placeholder — no longer drifting (initDriftArtifact used to
   reposition it every 5s; now fixed dead-center and static, per request).
   Dashed border matches the site's other not-yet-populated placeholders
   (e.g. .sam-bio-active-placeholder). Sits behind the heading/nav/cue
   (default stacking order, no z-index) and never intercepts clicks. */
.sam-projects-bg-artifact {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	width: min(50vw, 60vh);
	height: min(50vw, 60vh);
	border: 1.5px dashed var(--sam-fg);
	opacity: 0.5;
	pointer-events: none;
	z-index: 0;
}

/* Undergraduate's bg-artifact is a theme-swapped pair (white-outline
   seagulls gif for dark mode, black-outline for light mode) rather than
   a single asset like Masters' — both share the same centered/contain
   treatment, only one is ever visible at a time. */
.sam-projects-bg-artifact--dark,
.sam-projects-bg-artifact--light {
	border: none;
	opacity: 1;
	object-fit: contain;
}

/* Two-class selectors (not just the bare modifier) so this beats
   .sam-topic-gif-frame-img's own display:block at equal specificity when
   the same element carries both classes (the corner-frame's dark/light
   pair) — a bare single-class rule here would otherwise lose to whichever
   of the two same-specificity rules happens to come later in the file. */
.sam-projects-bg-artifact--light,
.sam-topic-gif-frame-img.sam-projects-bg-artifact--light {
	display: none;
}

html[data-theme="light"] .sam-projects-bg-artifact--dark,
html[data-theme="light"] .sam-topic-gif-frame-img.sam-projects-bg-artifact--dark {
	display: none;
}

html[data-theme="light"] .sam-projects-bg-artifact--light,
html[data-theme="light"] .sam-topic-gif-frame-img.sam-projects-bg-artifact--light {
	display: block;
}

/* Projects intro page: three jumps straight to each topic's own first
   page, centred in the section (which is already position:relative, per
   the shared .sam-projects-section rule). Same click-wiring as the
   cross-topic arrows above (home.js selects by [data-target-section]
   generically), just styled as plain bordered buttons instead of arrows. */
.sam-projects-topic-jumps {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	display: flex;
	flex-direction: row;
	gap: 1rem;
}

.sam-projects-topic-jump {
	appearance: none;
	-webkit-appearance: none;
	-moz-appearance: none;
	display: inline-block; /* also used on <a> (bio.html's cross-links), which default to inline otherwise */
	width: 14rem;
	padding: 1rem;
	background: var(--sam-bg);
	color: var(--sam-fg) !important; /* main.css's global button{color:#000 !important} rule otherwise wins — same fix as .sam-box-face */
	text-decoration: none;
	border: none;
	border-radius: 0;
	/* main.css's global button{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.2)}
	   (its own *resting*-state rule, not just the :hover one already
	   handled below) otherwise wins — near-invisible on dark mode's
	   black background, but shows as a visible ring in light mode. */
	box-shadow: none;
	font-family: inherit;
	font-size: 0.8rem;
	font-weight: 400;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	cursor: pointer;
	transition: opacity 0.2s ease, font-size 0.2s ease;
}

/* Underline + grow on hover/focus, on top of the fade it already had —
   same affordance convention as .sam-box-title/.sam-projects-list-title,
   making it clear each of these is a link. */
.sam-projects-topic-jump:hover,
.sam-projects-topic-jump:focus {
	opacity: 0.7;
	text-decoration: underline;
	font-size: 0.9rem;
	/* main.css's global button:hover{color:#47D3E5 !important; box-shadow:
	   inset 0 0 0 1px #47D3E5} otherwise wins — previously masked by this
	   button's own explicit border (removed for the row/no-border look),
	   which is what made it visible now. Same recurring main.css-default-
	   beats-custom-style fix used throughout this file. */
	color: var(--sam-fg) !important;
	box-shadow: none;
}

.sam-projects-topic-jump:focus-visible {
	outline: 1px dashed var(--sam-fg);
	outline-offset: 4px;
}

/* Cross-topic jump "void" transition: a full-viewport black cover, faded
   in then back out by home.js's projJumpWithFade (transition-duration
   set inline there, since the fade-in and fade-out legs use the same
   duration but need independent triggering) — reuses the exact spinning-
   dots loading indicator from the click-hold-to-activate mechanism
   (.sam-hover-timer-dots, already a free-running CSS @keyframes loop, no
   extra JS timing needed) so it reads as "loading" rather than a blank
   flash. Sits above everything (z-index) including the nav. */
.sam-projects-void-fade {
	position: fixed;
	inset: 0;
	background: #000;
	opacity: 0;
	pointer-events: none;
	z-index: 200;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Plain empty placeholder tile for CAD + Code's grid (no real projects
   yet) — same look as Masters' own resting tile, but static: no hover
   preview, no click behaviour, since there's nothing to show. */
.sam-projects-grid-tile--placeholder {
	width: 100%;
	height: 100%;
	border: none;
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
}

.sam-projects-grid-tile--placeholder-text {
	color: var(--sam-fg);
	font-weight: 700;
}

/* Undergraduate's own 5 real tiles, now living in the grid instead of the
   old collage — .sam-frame's base rule (width:var(--frame-w);
   aspect-ratio:3/4; transform:scale(0.7)) was sized/shrunk for the old
   freeform composition; here each tile should just fill its grid cell
   flush, unshrunk, at the same border weight as Masters' own tiles, for a
   consistent look between all three topics' grids. growFrame reads the
   resting transform live (decomposeTransform), so removing the base
   scale(0.7) here doesn't break its hover-grow math — it just starts
   growth from a resting scale of 1 instead of 0.7. */
.sam-gallery-frames--projects-grid .sam-frame {
	width: 100%;
	height: 100%;
	aspect-ratio: auto;
	border: none;
	transform: none;
}

/* Projects page only: all three grids sized so every cell is a perfect
   square (equal rows/columns/gaps means one value determines both
   dimensions — a grid of square cells is always square overall), scaled
   down 20% from the previous 40vw height to comfortably clear the
   section's top padding and its bottom "click on a project" cue without
   overlapping either (they used to be sized to match Bio's own content-
   column width instead, which made the cells wider than tall and, at
   that width, taller than the space actually available). Locked against
   browser zoom the same way Undergraduate's old collage was (expressed
   as vw so it holds its proportions rather than drifting at different
   zoom levels/breakpoints). Centred horizontally via margin auto, since
   it's now much narrower than its section. Scoped to body.sam-page--projects
   (see projects.html) so the old standalone MASTERS.html keeps its
   original, un-resized, un-locked grid. */
@media screen and (min-width: 737px) {

	body.sam-page--projects .sam-gallery-frames--masters-grid,
	body.sam-page--projects .sam-gallery-frames--projects-grid {
		width: 32vw;
		height: 32vw;
		margin-left: auto;
		margin-right: auto;
	}

}

.sam-masters-columns {
	display: flex;
	align-items: center;
	gap: calc(6rem - 8px);
}

/* Wrapper reserves the frame's actual *rotated+scaled* visual footprint
   (14.28rem x 19.04rem at rest, rotated 90deg and scaled 1.4x swaps and
   enlarges those to 26.656rem x 19.992rem) — transforms never change an
   element's own home box, only its paint, so without this the flex row
   would still reserve only the unrotated/unscaled width and the tile
   would visually overflow past the row (and the page's left margin). */
.sam-masters-frame-col {
	flex: none;
	position: relative;
	width: 26.656rem;
	height: 19.992rem;
}

/* Permanently rotated + doubled (no hover needed) — this tile has no
   .sam-gallery-frame class, and (unlike before) no .sam-frame class either,
   so it's structurally impossible for home.js's generic hover-grow
   (bound via document.querySelectorAll('.sam-frame, .sam-gallery-item-
   frame')) to ever match it. width/aspect-ratio/border below are the same
   values .sam-frame used to supply, just inlined here directly. Absolutely
   centered within its wrapper so the rotation/scale (which apply around
   the element's own center by default) land it centered regardless. */
.sam-masters-featured-frame {
	width: var(--frame-w);
	aspect-ratio: 3 / 4;
	border: 1.5px solid var(--sam-fg);
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%) rotate(90deg) scale(1.4);
}

.sam-masters-text {
	/* Not resizing the frame — this is picked so frame-col (26.656rem) +
	   this row's gap (6rem - 8px) + this width sums to the same total as
	   bio's own photo-width + gap + text-width row (58rem - 8px). */
	width: 25.344rem;
	flex: none;
}

.sam-masters-text p {
	margin: 0 0 1em 0;
	line-height: 1.6;
	color: var(--sam-fg);
	opacity: 0.7;
}

.sam-masters-text p:last-child {
	margin-bottom: 0;
}

/* "All projects" grid: uniform 3x3 grid of 9 equally-sized tiles at rest,
   sized as a whole to occupy exactly the same footprint as section 1's
   single tile at full growth (.sam-masters-frame-col's own 26.656rem x
   19.992rem) — tiles fill the remaining space evenly via 1fr tracks once
   the (deliberately small) gap is subtracted. Hovering any tile no
   longer grows it in place — instead it shows the shared
   .sam-masters-preview slot (see below), fixed-positioned/sized in
   home.js to match section 1's frame exactly, and hides this grid
   while that's showing. */

/* Undergraduate's and CAD + Code's new grid pages reuse this same rule
   under a topic-neutral second class name, rather than duplicating it
   three times — sharing every declaration below, including the width/
   height (only overridden for the three grids' Projects-page copies,
   scoped via body.sam-page--projects further down, so this rule's own
   defaults stay exactly as they were for the old standalone MASTERS.html). */
.sam-gallery-frames--masters-grid,
.sam-gallery-frames--projects-grid {
	display: grid;
	/* minmax(0, 1fr), not plain 1fr: a bare <flex> track keeps an implicit
	   automatic minimum equal to its content's own min-content size, which
	   lets a tall frame/image push its row taller than its nominal 1/3
	   share — silently overflowing the grid's own explicit height instead
	   of being constrained by it. minmax(0, ...) removes that automatic
	   minimum, forcing all 3 rows/columns to actually divide the declared
	   width/height evenly regardless of content (found via the Projects
	   page's wider grid, where 1fr alone let each frame's own resting size
	   force the grid ~600px past its declared 40vw height). */
	grid-template-columns: repeat(3, minmax(0, 1fr));
	grid-template-rows: repeat(3, minmax(0, 1fr));
	gap: 1rem;
	width: 26.656rem;
	height: 19.992rem;
	/* .sam-gallery-frames (shared base) sets align-items: flex-start for
	   its flex-based rows elsewhere (Undergraduate's collage) — that value
	   also applies to a grid container, overriding the default `stretch`
	   grid items would otherwise get, which is what makes each cell fill
	   its row's height. */
	align-items: stretch;
}

/* .sam-masters-grid-cell (the wrapper div around each tile) needs no
   rules of its own — grid items stretch to fill their track by default
   (CSS Grid's own align/justify-items: stretch). */

/* Grid caption stepper — a small, topic-generic widget shown only while a
   project tile is "in double view" (grown 2x / previewed), directly under
   it and slightly overlapping, like Masters' old toolbar used to be —
   additive, doesn't touch Undergraduate's existing click-to-grow tiles or
   Masters' existing image preview. Shows the current project's caption as
   plain text in a bordered box, no image — see initGridStepper,
   growFrame/shrinkFrame, and the Masters preview open/close in home.js.

   Uniform across all three topics now — hidden until JS adds .is-visible
   and positions it inline (see showActive in home.js), which also
   drives which tile's image/caption/detail the shared .sam-grid-stage
   displays (see below), rather than only cycling caption text. */
.sam-grid-stepper {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1.5rem;
}

.sam-grid-stepper--floating {
	display: none;
	position: fixed;
	z-index: 50;
}

.sam-grid-stepper--floating.is-visible {
	display: flex;
}

/* Fixed width (~15 characters at the default font-size below), not
   auto-sizing to the caption's own text — otherwise the prev/next arrows
   visibly shift position as captions of different lengths cycle through.
   Longer captions shrink their own font-size instead (see fitCaption in
   home.js); shorter ones are never enlarged past the default size set
   here. Deliberately `rem`, not `ch` — `ch` is relative to the element's
   *own* font-size, which fitCaption is actively adjusting, so the box
   would shrink in lockstep with the very text it's trying to fit,
   never actually converging. */
.sam-grid-stepper-caption {
	margin: 0;
	padding: 0.5rem 1.5rem;
	background: var(--sam-bg);
	color: var(--sam-fg);
	font-size: 0.8rem;
	font-weight: bold;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	text-align: center;
	width: 9rem;
	overflow: hidden;
	white-space: nowrap;
}

/* "The Floating Chapel" stacked caption (see CAPTION_STACKS in home.js)
   — an ascending staircase: "The" drops smaller and low (subscript
   against "Floating"), "Cha"/"pel" climb back up in two full-size
   superscript steps. `em`-relative so the whole stack keeps its
   proportions when fitCaption shrinks the container's own font-size to
   fit the fixed-width box above. */
.sam-grid-stepper-caption-sub {
	display: inline-block;
	font-size: 0.7em;
	vertical-align: -0.28em;
}

.sam-grid-stepper-caption-super-1 {
	display: inline-block;
	vertical-align: 0.28em;
}

.sam-grid-stepper-caption-super-2 {
	display: inline-block;
	vertical-align: 0.56em;
}

/* Chunky like .sam-masters-toolbar-arrow, plus a continuous flash to draw
   the eye — paused on hover/focus (full opacity, no motion) and replaced
   with a plain dim when exhausted at either end (.is-faded), same
   non-`disabled` rationale as .sam-masters-toolbar-arrow.is-faded above —
   a real disabled attribute kills hover the same way here. */
@keyframes sam-grid-stepper-flash {
	0%, 100% { opacity: 1; }
	50% { opacity: 0.35; }
}

.sam-grid-stepper-arrow {
	appearance: none;
	-webkit-appearance: none;
	-moz-appearance: none;
	background: none;
	border: none;
	padding: 0;
	display: inline-block;
	/* main.css's global button{height:3.75em} rule (em relative to this
	   button's own font-size, since nothing here overrides height) is what
	   actually sets these arrows' current height — matching that same
	   3.75em value here for width makes width == height exactly, adding
	   the extra room symmetrically around the glyph via text-align:center,
	   without touching height at all. */
	width: 3.75em;
	text-align: center;
	font-size: 1.5rem;
	font-weight: bold;
	line-height: 1;
	cursor: pointer;
	color: var(--sam-fg) !important;
	animation: sam-grid-stepper-flash 1.4s ease-in-out infinite;
	/* main.css's global button rules draw a faint inset ring (rounded by
	   its border-radius into a circle) at rest, and a blue one on
	   hover/focus/press — none of which belong on these bare arrows. */
	box-shadow: none;
}

.sam-grid-stepper-arrow:hover,
.sam-grid-stepper-arrow:focus,
.sam-grid-stepper-arrow:active {
	box-shadow: none;
	background: none;
}

.sam-grid-stepper-arrow:hover,
.sam-grid-stepper-arrow:focus-visible {
	animation: none;
	opacity: 1;
}

.sam-grid-stepper-arrow.is-faded {
	animation: none;
	opacity: 0.25;
	cursor: default;
}

/* Per-frame hover detail box: one shared, JS-positioned element (see
   home.js) placed beside the grown frame — to its right for frames
   sitting toward the left of the page, to its left for frames sitting
   toward the right, per each frame's data-detail-side attribute.
   Vertically centered on the grown frame. position:absolute (not fixed)
   and living inside .sam-gallery-home (its own containing block,
   already position:relative) so it scrolls naturally with the tile it's
   attached to once scrolling unlocks — growFrame converts its
   coordinates to be relative to that container. */

.sam-frame-detail {
	position: absolute;
	/* The "bio width rule" the project text always follows, sitting to the
	   stage's right (data-detail-side's old left/right branching is no
	   longer read). */
	width: 28rem;
	margin: 0;
	line-height: 1.6;
	color: var(--sam-fg);
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.2s ease;
	text-align: justify;
	/* Above .sam-grid-stage (z-index 20) — positionDetailBeside's image-
	   aware left edge can now legitimately sit inside the stage's own
	   bounding box (a portrait image's letterboxed empty space still
	   belongs to that box), and the stage's opaque background would
	   otherwise paint over/clip that portion of the text. */
	z-index: 21;
	font-weight: 700;
}

.sam-frame-detail.is-visible {
	opacity: 0.7;
}

/* Shared "grid stage" — the single enlarged-display slot every one of a
   topic's 9 tiles (or the stepper's prev/next arrows) shows into,
   regardless of which was clicked. Sized/positioned in home.js
   (positionStage) to exactly match the grid's own footprint (.sam-gallery-
   frames' rect), so it always grows dead-center within the 3x3 area and
   never in place from whichever tile happens to sit where. Flex-centered
   so an image inside keeps its own aspect ratio (max-width/max-height:100%,
   width/height:auto) instead of being force-cropped to the square footprint
   — the border sits directly on the image itself, so it hugs the image's
   own rendered size rather than the full square. */
.sam-grid-stage {
	display: none;
	position: fixed;
	align-items: center;
	justify-content: center;
	background: var(--sam-bg);
	z-index: 20;
}

.sam-grid-stage.is-visible {
	display: flex;
}

.sam-grid-stage-img {
	display: block;
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
	border: none;
	cursor: pointer;
}

.sam-grid-stage-img[hidden] {
	display: none;
}

/* Per-tile opt-in (data-stage-fit="cover" in the markup, toggled by
   showActive in home.js): fills the square stage completely instead of
   letterboxing to the image's own aspect ratio, cropping the excess
   instead of leaving empty space beside it. Only tiles whose source
   footage already fills its own frame edge-to-edge should use this —
   letterboxing baked into the source itself (see masters-tile3-workshop's
   re-crop) needs fixing at the source, not here. */
.sam-grid-stage-img.is-cover {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* No real image yet for most tiles — same square-bordered placeholder
   look the resting grid tiles already use. */
.sam-grid-stage-placeholder {
	width: 100%;
	height: 100%;
	border: none;
	cursor: pointer;
}

.sam-grid-stage-placeholder[hidden] {
	display: none;
}

/* Undergraduate gallery: hovering/focusing a tile for an uninterrupted 5s
   (see the hover-timer block in home.js) reveals this full detail
   section below it — the page becomes scrollable to reach it. The escape
   button (top-center, doesn't collide with the top-left heading) returns
   to the resting 5-tile view. */

.sam-project-escape[hidden] {
	/* main.css's button{display:inline-block} is an author rule, so it
	   otherwise beats the UA stylesheet's [hidden]{display:none} outright
	   regardless of specificity — same fix as .sam-box-face/.sam-mode-toggle's
	   button{color:#000!important} issue elsewhere in this file. */
	display: none;
}

.sam-project-escape {
	appearance: none;
	-webkit-appearance: none;
	-moz-appearance: none;
	z-index: 20;
	display: flex;
	align-items: center;
	justify-content: center;
	margin: 0;
	padding: 0;
	background: none;
	border: none;
	/* Invisible hit-box, same width mechanism as .sam-grid-stepper-arrow —
	   relies on main.css's global button{height:3.75em} for a matching
	   height, so this ends up exactly the stepper arrows' own hit-target
	   size. The actual visible circle is .sam-project-escape-circle
	   below, centered inside via the flex rules above, unchanged in size. */
	width: 3.75em;
	color: var(--sam-fg) !important;
	font-size: 1.5rem;
	line-height: 1;
	cursor: pointer;
}

.sam-project-escape-circle {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border: 1.5px solid var(--sam-fg);
	border-radius: 50%;
	background: var(--sam-bg);
}

/* State C (counting/holding a project's 3.5s activation) — cancels the
   hold and returns to State B (the grown-but-not-activated stage). State D
   (fully activated) closes back to State A (resting grid). Each sits just
   to the right of its own .sam-topic-status text bubble (the counting
   bar/dots, or "Go back" respectively) — top/left are set inline by
   positionEscapeNextTo() in home.js, independently for each, since each
   bubble's own position depends on live home (viewport height, its own
   rendered size) — the two buttons aren't expected to land in the same
   spot. */
.sam-project-escape--cancel,
.sam-project-escape--close {
	position: fixed;
}

.sam-project-escape-glyph {
	display: block;
}

.sam-project-escape-glyph[hidden] {
	display: none;
}

/* The x glyph's own visual center is nudged by the font's glyph metrics;
   the enter-key arrow (State D) doesn't need the same nudge. */
.sam-project-escape-glyph--cancel {
	transform: translate(2px, -2px);
}

/* State D's "Go back" icon — returnKey.png (recolored so its disc fill
   matches the Home nav icon's own grey, images/home_icon_border.png,
   rgb(46,46,46)), baked as two separate light/dark-theme files rather
   than one file + a live filter (the technique .sam-box--home-icon uses)
   — same two-file swap pattern as .sam-mode-icon--to-light/--to-dark
   above, just under its own class names since this icon is unrelated to
   the mode toggle. Sized to exactly replace the circle glyph it sits
   inside (2.75rem, matching .sam-project-escape-circle). */
.sam-project-escape-return-icon {
	display: block;
	width: 2.75rem;
	height: 2.75rem;
	object-fit: contain;
}

.sam-project-escape-return-icon--to-dark {
	display: none;
}

html[data-theme="light"] .sam-project-escape-return-icon--to-light {
	display: none;
}

html[data-theme="light"] .sam-project-escape-return-icon--to-dark {
	display: block;
}

/* The return-key image already has its own ring+disc baked in, so the
   plain CSS-drawn circle underneath is suppressed while it's showing —
   otherwise the two circles (image + .sam-project-escape-circle) would
   paint on top of each other. The --cancel state is untouched, so the
   plain × keeps its existing CSS circle exactly as before. */
.sam-project-escape--close .sam-project-escape-circle {
	border: none;
	background: none;
}

.sam-project-escape:hover,
.sam-project-escape:focus,
.sam-project-escape:active {
	color: var(--sam-fg) !important;
	opacity: 0.7;
}

.sam-project-escape:focus-visible {
	outline: 1px dashed var(--sam-fg);
	outline-offset: 4px;
}

.sam-project-detail[hidden] {
	/* main.css's HTML5-element reset sets section{display:block} as an
	   author rule, which otherwise beats the UA stylesheet's
	   [hidden]{display:none} regardless of specificity — same bug class
	   as .sam-project-escape below, this time on a <section>. Without
	   this, the section's own margin:100vh below still applies even while
	   "hidden", making the whole gallery page scroll at rest. */
	display: none;
}

.sam-project-detail {
	/* Full-project mode's single view — normal document flow, same as the
	   original "page 2" always used (no position:fixed, no JS-computed
	   positioning): the section's own padding-top (9rem) already clears
	   the heading, landing this right where the resting grid used to sit.
	   No extra margin-top/padding-top of its own — .sam-gallery-frames--
	   projects-grid (the resting grid) has none either, so matching that
	   exactly (rather than reusing the old page-2 block's own 2rem
	   padding-top, which was calibrated for sitting a further page down)
	   is what keeps the frame at the same height the grid always sat at.
	   No forced min-height either — the original page 2's
	   calc(100vh - 4.5rem) was sized to fill its own dedicated 100vh slot
	   within a 200vh section (after the first page's 9rem padding had
	   already been "spent"); sharing that same 9rem padding within one
	   ordinary 100vh section instead, forcing that height would guarantee
	   ~7rem of overflow regardless of content, so this sizes to its
	   content instead, exactly like the resting grid always did. */
	margin: 0 auto 2rem;
	width: 100%;
	line-height: 1.6;
	font-size: 0.95rem;
	color: var(--sam-fg);
	background: var(--sam-bg);
	display: flex;
	flex-direction: column;
	align-items: center;
}

/* Same arrangement as page 1's stage/frameDetail pairing: the FRAME is
   centered on the page and the text sits to its right, vertically centered
   against it — not the frame+text pair centered as a unit, which would push
   the frame left of center by half the text's width. The middle track holds
   the frame, so it lands dead center as long as the two side tracks stay
   equal; minmax(0, 1fr) is what keeps them equal, since a bare 1fr track
   takes its content's min-content width as an automatic minimum and the
   28rem text column would otherwise widen its own side. */
.sam-project-detail-home {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
	align-items: center;
	width: 100%;
}

/* Pulled up to overlap the frame's own bottom edge by the same 32px the
   grown-state floating stepper uses (stageRect.bottom - 32 in showActive,
   home.js) — this used to be masked by .sam-project-detail's now-removed
   gap:1rem, which instead sat the stepper *below* the frame with a gap,
   landing it much lower than the grown-state stepper does for the exact
   same frame. Matching that overlap keeps the two steppers' position
   consistent between states, and reclaims the vertical room full-project
   mode needs now that it shares one viewport with the heading instead of
   its own dedicated page. */
.sam-project-detail-stepper {
	margin-top: -32px;
}

.sam-project-detail-frame {
	grid-column: 2;
}

.sam-project-detail-text {
	grid-column: 3;
}

/* Full-project mode's own frame — deliberately the same footprint as the
   3x3 grid (see body.sam-page--projects .sam-gallery-frames--projects-grid),
   so the project reads at the same size it did at rest. */
.sam-project-detail-frame {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32vw;
	height: 32vw;
}

/* Border hugs the image's own aspect-fit size rather than the square frame,
   matching .sam-grid-stage-img's treatment. */
.sam-project-detail-img {
	display: block;
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
	border: none;
}

.sam-project-detail-img[hidden],
.sam-project-detail-placeholder[hidden] {
	display: none;
}

/* Same square-bordered stand-in .sam-grid-stage-placeholder uses, for
   frames that have no image yet — all three start this way, see the
   .sam-project-detail-slide markup in projects.html. */
.sam-project-detail-placeholder {
	width: 100%;
	height: 100%;
	border: none;
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
}

.sam-project-detail-placeholder-text {
	color: var(--sam-fg);
	font-weight: 700;
}

/* Same width and opacity as page 1's .sam-frame-detail (the "bio width"
   column) — max-width caps it to its own grid track, so a viewport too
   narrow to fit 28rem beside the frame shrinks the text rather than
   shoving the frame off center. */
.sam-project-detail-text {
	/* Left margin is the gap to the frame, matching home.js's DETAIL_GAP
	   (40px) as a default/fallback — alignDetailTextToImage in home.js
	   overrides it with an image-aware value via inline style once the
	   current slide's image has loaded (letterboxed portrait images don't
	   reach the frame's own right edge, so a fixed margin here would
	   measure from the empty box edge rather than the actual picture).
	   It also grows this box's width to compensate whenever it does, so
	   the right edge — anchored DETAIL_RIGHT_INSET from the track's own
	   boundary — never moves, only the left edge responds to the image.
	   max-width caps the text to its own grid track, so a viewport too
	   narrow to fit the text beside the frame shrinks it instead of
	   shoving the frame off center. width/max-width are both trimmed by
	   an extra 40px, matching .sam-frame-detail's own right-edge inset
	   (DETAIL_RIGHT_INSET in home.js), so the box's right edge sits
	   further in without moving its left edge. */
	margin: 0 0 0 40px;
	width: calc(28rem - 40px);
	max-width: calc(100% - 40px - 40px);
	opacity: 0.7;
	text-align: justify;
	font-weight: 700;
	/* .sam-project-detail (the ancestor) sets font-size: 0.95rem for the
	   whole full-project-mode subtree — overridden back to the page's
	   plain 1rem here so this text renders identically to .sam-frame-
	   detail's (state B) for the same project, instead of ~5% smaller. */
	font-size: 1rem;
}

.sam-project-detail-img {
	cursor: pointer;
}

/* Click-to-enlarge for the full-project stepper's current image — same
   two-leg opacity-fade technique as Bio's own click-to-enlarge
   (.sam-bio-void-fade/.sam-bio-active-stage above), reused here as one
   shared, page-wide overlay (only one topic's full-project mode can ever
   be open at a time, so a single instance covers all three). Driven by
   projectOpenActive/projectCloseActive in home.js. */
.sam-project-detail-void-fade {
	position: fixed;
	inset: 0;
	background: var(--sam-bg);
	opacity: 0;
	pointer-events: none;
	z-index: 200;
}

.sam-project-detail-active-stage {
	position: fixed;
	inset: 0;
	z-index: 150;
	background: var(--sam-bg);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
}

.sam-project-detail-active-stage[hidden] {
	display: none;
}

.sam-project-detail-active-img {
	display: block;
	max-width: 85vw;
	max-height: 80vh;
	width: auto;
	height: auto;
	object-fit: contain;
	position: relative;
	left: 130px;
}

.sam-project-detail-active-hint {
	position: absolute;
	top: 50%;
	left: 2.5rem;
	transform: translateY(-50%);
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.85rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	font-weight: 700;
	pointer-events: none;
}

/* Top-left status text: idle prompt / counting bar+dots / "Go back" are
   three mutually-exclusive states of the SAME top-left slot (States A/B,
   C, and D respectively — see showActive/startHold/activate in
   home.js), level with the grid's own top edge. */
.sam-topic-status {
	position: absolute;
	top: 50%;
	left: 2.5rem;
	transform: translateY(-50%);
	z-index: 60;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 0.5rem;
	color: var(--sam-fg);
	font-size: 0.85rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

.sam-hover-timer-idle[hidden],
.sam-hover-timer-counting[hidden],
.sam-hover-timer-goback[hidden] {
	/* Same [hidden]-vs-author-rule issue as .sam-project-escape below —
	   the display:flex two rules down would otherwise beat the UA
	   stylesheet's [hidden]{display:none} regardless of specificity. */
	display: none;
}

.sam-hover-timer-idle,
.sam-hover-timer-counting,
.sam-hover-timer-goback {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	margin: 0;
}

/* Faded in ~750ms after becoming un-hidden (see revealIdleText in
   home.js) rather than popping instantly, which read as a glitchy
   flicker whenever the idle/"click again" text swapped quickly. */
.sam-hover-timer-idle {
	opacity: 0;
	transition: opacity 0.2s ease;
	font-weight: 700;
}

.sam-hover-timer-goback {
	font-weight: 700;
}

.sam-hover-timer-idle.is-visible {
	opacity: 1;
}

/* The "click again" prompt's second line (see CLICK_AGAIN_TEXT in
   home.js). One block-level wrapper around both lines keeps this a
   single flex item inside .sam-hover-timer-idle's row home — the second
   line then breaks onto its own line because it's block-level within that
   wrapper's own (non-flex) normal flow. line-height governs the spacing
   between the two lines. */
.sam-hover-timer-idle-lines {
	display: block;
	line-height: 3.2;
}

.sam-hover-timer-idle-sub {
	display: block;
}

/* Stage D's "click to zoom" hint — bottom-left, matching .sam-topic-
   gif-frame's own left/bottom:2.5rem corner inset (that decorative gif
   is always hidden by the time Stage D is reachable, see gifFrame in
   home.js, so there's no visual clash — just a shared reference point).
   Same typography as .sam-topic-status/.sam-bio-active-hint for
   consistency with the rest of the site's hint text. Toggled alongside
   "Go back" in showGobackPrompt/hideAll/etc. (home.js), so it only ever
   shows in Stage D. No position:fixed toggle needed the way
   .sam-topic-status has one — the active section is always exactly
   100vh while Stage D is open, so bottom:2.5rem relative to its own box
   already lands at the viewport's own bottom-left corner. */
.sam-project-zoom-hint {
	position: absolute;
	left: 2.5rem;
	bottom: 2.5rem;
	z-index: 60;
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.85rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	font-weight: 700;
}

.sam-project-zoom-hint[hidden] {
	display: none;
}

/* CAD + Code has no projects yet — a single centered message in place of
   the 3x3 grid. With no .sam-gallery-frames/.sam-grid-stage in that
   section, initGalleryInteractions (home.js) skips it entirely. Same
   typography as the rest of the site's status text. */
.sam-projects-coming-soon {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	margin: 0;
	color: var(--sam-fg);
	font-size: 0.85rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

/* State D's own bottom-center "SCROLL DOWN TO SEE MORE" cue — unchanged position
   (inherits .sam-scroll-cue's fixed/absolute bottom-center rules
   elsewhere in this file), just no longer nested inside the top-left
   status container above. */
.sam-hover-timer-active[hidden] {
	display: none;
}

.sam-hover-timer-active {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.sam-hover-timer-bar {
	width: 140px;
	height: 4px;
	border: 1px solid var(--sam-fg);
}

.sam-hover-timer-bar-fill {
	height: 100%;
	width: 0%;
	background: var(--sam-fg);
	transition: width 0.15s linear;
}

.sam-hover-timer-dots {
	position: relative;
	width: 22px;
	height: 22px;
	animation: sam-hover-timer-spin 1s linear infinite;
}

.sam-hover-timer-dots span {
	position: absolute;
	top: 50%;
	left: 50%;
	width: 3px;
	height: 3px;
	margin: -1.5px;
	border-radius: 50%;
	background: var(--sam-fg);
}

.sam-hover-timer-dots span:nth-child(1) { transform: rotate(0deg) translateY(-9px); opacity: 1; }
.sam-hover-timer-dots span:nth-child(2) { transform: rotate(45deg) translateY(-9px); opacity: 0.87; }
.sam-hover-timer-dots span:nth-child(3) { transform: rotate(90deg) translateY(-9px); opacity: 0.74; }
.sam-hover-timer-dots span:nth-child(4) { transform: rotate(135deg) translateY(-9px); opacity: 0.61; }
.sam-hover-timer-dots span:nth-child(5) { transform: rotate(180deg) translateY(-9px); opacity: 0.48; }
.sam-hover-timer-dots span:nth-child(6) { transform: rotate(225deg) translateY(-9px); opacity: 0.35; }
.sam-hover-timer-dots span:nth-child(7) { transform: rotate(270deg) translateY(-9px); opacity: 0.22; }
.sam-hover-timer-dots span:nth-child(8) { transform: rotate(315deg) translateY(-9px); opacity: 0.15; }

@keyframes sam-hover-timer-spin {
	to { transform: rotate(360deg); }
}

/* Each topic's 4-second loading gate — now an overlay covering that
   topic's single (merged) section on fresh arrival, rather than a
   separate section of its own (see startTopicGate in home.js). Opaque
   backdrop so the grid/stage/stepper beneath are fully hidden while it's
   up; sits below the heading (z-index 95, .sam-project-heading) so the
   heading — outside this overlay in the DOM — stays visible throughout,
   but above everything else in the section (.sam-scroll-cue's 60,
   .sam-grid-stepper--floating's 50). */
.sam-topic-loading-overlay {
	position: absolute;
	inset: 0;
	background: var(--sam-bg);
	z-index: 90;
}

.sam-topic-loading-overlay[hidden] {
	display: none;
}

/* Reuses the exact same bar/dots visual components as the gallery's
   hold-to-activate timer above (.sam-hover-timer-bar/-dots), but in its
   own independent wrapper (not .sam-hover-timer itself) so it isn't
   cross-wired with that unrelated JS-driven instance. Sits below the
   centered gif/frame within the overlay above. */
.sam-topic-loading {
	position: absolute;
	left: 50%;
	bottom: 12%;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	gap: 0.75rem;
}

.sam-topic-loading[hidden] {
	display: none;
}

/* Permanent small gif frame, bottom-left of a topic's (now sole) section —
   replaces the old page-1 "loading page" gif with a small persistent
   accent instead, sized off the same --Bio-photo-w convention used
   elsewhere for cross-page width consistency, scaled down since the full
   Bio width would be nearly as large as the grid itself. */
.sam-topic-gif-frame {
	position: absolute;
	left: 2.5rem;
	bottom: 2.5rem;
	width: calc(var(--Bio-photo-w) * 0.5);
	z-index: 5;
	pointer-events: none;
}

/* Hidden while a project is shown on the stage (States B/C/D) — see
   showActive/hideStage in home.js. */
.sam-topic-gif-frame.is-hidden {
	display: none;
}

.sam-topic-gif-frame-img,
.sam-topic-gif-frame-placeholder {
	display: block;
	width: 100%;
}

.sam-topic-gif-frame-img {
	object-fit: contain;
}

/* No real gif asset yet for this topic — same dashed-border placeholder
   convention as .sam-projects-bg-artifact. */
.sam-topic-gif-frame-placeholder {
	aspect-ratio: 1 / 1;
	border: 1.5px dashed var(--sam-fg);
	opacity: 0.5;
}

@media screen and (max-width: 736px) {

	.sam-topic-gif-frame {
		left: 1.5rem;
		bottom: 1.5rem;
	}

}

@media screen and (max-width: 736px) {

	.sam-gallery-frames {
		flex-direction: column;
		align-items: center;
		width: auto;
		gap: 1.5rem;
	}

	.sam-gallery-frame {
		width: var(--frame-w);
	}

}

/* ------------------------------------------------------------------
   Phone layout (≤736px)

   Desktop never shows any of the elements below — they're hidden here
   by default and only switched on inside the media query that follows,
   which also hides the desktop-only pieces they replace.
   ------------------------------------------------------------------ */

.sam-menu-toggle,
.sam-menu,
.sam-phone-stepper,
.sam-phone-section-arrow,
.sam-phone-view-switch,
.sam-heading-phone {
	display: none;
}

@media screen and (max-width: 736px) {

	/* Menu: one button top-right opens a full-screen copy of the nav.
	   !important because deactivate() (home.js) resets the nav's inline
	   display when a project closes. */
	.sam-nav,
	.sam-topic-jumps-floating {
		display: none !important;
	}

	/* 80px square tap target (in px: the root font-size varies by
	   breakpoint), icon 2.75rem, sitting 4px from the corner so the icon
	   lands roughly where it always did. */
	.sam-menu-toggle {
		display: flex;
		position: fixed;
		top: 4px;
		right: 4px;
		z-index: 1001;
		align-items: center;
		justify-content: center;
		width: 80px;
		height: 80px;
		max-width: none;
		margin: 0;
		padding: 0;
		border: none;
		border-radius: 0;
		background: none;
		box-shadow: none;
		line-height: 1;
		cursor: pointer;
		touch-action: manipulation;
	}

	/* main.css's global button:hover/:focus/:active rules otherwise add a
	   blue ring and blue text the moment one of these is tapped. */
	.sam-menu-toggle:hover,
	.sam-menu-toggle:focus,
	.sam-menu-toggle:active,
	.sam-menu-projects-list button:hover,
	.sam-menu-projects-list button:focus,
	.sam-menu-projects-list button:active,
	.sam-phone-section-arrow:hover,
	.sam-phone-section-arrow:focus,
	.sam-phone-section-arrow:active {
		box-shadow: none;
		background: none;
		color: var(--sam-fg) !important;
	}

	.sam-menu-toggle:focus-visible,
	.sam-phone-section-arrow:focus-visible {
		outline: 1px dashed var(--sam-fg);
		outline-offset: 4px;
	}

	.sam-menu-icon {
		display: block;
		width: 2.75rem;
		height: 2.75rem;
	}

	.sam-menu-icon--to-dark {
		display: none;
	}

	html[data-theme="light"] .sam-menu-icon--to-light {
		display: none;
	}

	html[data-theme="light"] .sam-menu-icon--to-dark {
		display: block;
	}

	/* Anchored to the top rather than vertically centred: opening the
	   Projects list then grows the column downwards without moving the
	   Projects box, so a quick second tap still lands on it. */
	.sam-menu:not([hidden]) {
		display: flex;
		position: fixed;
		inset: 0;
		z-index: 1000;
		flex-direction: column;
		align-items: center;
		justify-content: flex-start;
		gap: 1rem;
		/* Centres the closed group (3 rows of 3rem + 2 gaps = 11rem);
		   opening the Projects list then only pushes Bio down. */
		padding: max(6rem, calc(50vh - 5.5rem)) 1.5rem 2rem;
		padding: max(6rem, calc(50svh - 5.5rem)) 1.5rem 2rem;
		overflow-y: auto;
		background: var(--sam-bg);
	}

	/* The menu reuses the desktop nav's own boxes (house icon, wavy/zigzag
	   theme toggle, bordered Projects/Bio), so they look identical. On
	   phones a tap counts as hover, which would trigger the desktop boxes'
	   sideways hover-grow, so it's cancelled here. */
	.sam-menu .sam-box {
		flex: none;
		width: var(--box-w);
		height: var(--box-h);
		min-height: 0;
		max-height: var(--box-h);
	}

	.sam-menu .sam-box--home-icon {
		width: var(--box-h);
	}

	/* Theme toggle and Home side by side, as in the desktop nav. */
	.sam-menu-row {
		display: flex;
		flex: none;
		gap: 0.75rem;
		width: var(--box-w);
	}

	/* The hidden in-page jump behind Projects' double tap (projects.html):
	   main.css's global button rule otherwise beats [hidden] and shows it
	   as an empty box between Projects and Bio. */
	.sam-menu-projects-home {
		display: none !important;
	}

	/* On desktop the toggle stretches to fill the nav row beside Home;
	   alone in the menu's column it has nothing to fill, so it gets the
	   width it ends up with there (a full box, less Home and the gap). */
	.sam-menu .sam-box--mode-toggle {
		width: calc(var(--box-w) - var(--box-h) - 0.75rem);
	}

	.sam-menu .sam-box:hover .sam-box-face,
	.sam-menu .sam-box:focus-within .sam-box-face {
		inset: 0;
	}

	.sam-menu-projects .sam-box-face {
		touch-action: manipulation;
	}

	/* Projects' list, opened by the first tap: the desktop dropdown's own
	   title/subtitle text styles, stacked beneath the box. */
	.sam-menu-projects-list {
		display: flex;
		flex-direction: column;
		align-items: center;
		width: var(--box-w);
		margin: -0.25rem 0 0;
		padding: 0;
		list-style: none;
	}

	.sam-menu-projects-list[hidden] {
		display: none;
	}

	.sam-menu-projects-list li {
		margin: 0;
		padding: 0;
	}

	.sam-menu-projects-list a,
	.sam-menu-projects-list button {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 0.2rem;
		width: 100%;
		height: auto;
		max-width: none;
		margin: 0;
		padding: 0.55rem 0.5rem;
		border: none;
		border-radius: 0;
		background: none;
		box-shadow: none;
		color: var(--sam-fg) !important;
		font-family: inherit;
		font-weight: 400;
		letter-spacing: normal;
		line-height: normal;
		text-align: center;
		text-decoration: none;
		text-transform: none;
		white-space: normal;
		overflow: visible;
		cursor: pointer;
	}

	html.sam-menu-open,
	html.sam-menu-open body,
	html.sam-menu-open .sam-projects-snap-home {
		overflow: hidden;
	}

	/* Headings: a plain-text title in place of the ASCII art, on its own
	   full-width row well below the bouncing up-arrow, so the arrow
	   doesn't read as part of the title. */
	.sam-project-heading {
		top: 6.25rem;
		width: calc(100vw - 2rem);
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.sam-project-heading pre {
		display: none;
	}

	/* Same look as the open-project title (.sam-project-heading-active):
	   the gradient-filled uppercase text, at its phone size. */
	.sam-heading-phone {
		display: block;
		margin: 0;
		font-size: 1.4rem;
		font-weight: bold;
		letter-spacing: 0.05em;
		text-align: center;
		text-transform: uppercase;
		color: transparent;
		background-image: var(--ascii-gradient);
		-webkit-background-clip: text;
		background-clip: text;
		-webkit-text-fill-color: transparent;
	}

	/* Bio's photo sits near the top, so its title sits level with the
	   menu button instead. */
	.sam-bio-section--main .sam-project-heading {
		top: 3.5rem;
		width: calc(100vw - 9rem);
	}

	/* An open project's plain-text title drops below the top-left Go
	   back button so long titles never run into it. */
	.sam-projects-section.is-project-active .sam-project-heading {
		top: 5.5rem;
	}

	/* Full-project mode shows its own plain-text title instead. */
	.sam-projects-section.is-project-active .sam-heading-phone {
		display: none;
	}

	/* Projects: scrolling is locked to whole sections, one per flick
	   (mandatory + snap-stop); each section is one screen tall. */
	.sam-projects-snap-home {
		scroll-snap-type: y mandatory;
	}

	.sam-projects-section {
		scroll-snap-stop: always;
	}

	.sam-projects-section,
	.sam-projects-section.sam-masters-section {
		height: auto;
		min-height: 100vh;
		min-height: 100svh;
		padding: 0;
	}

	/* Projects intro: the three topic buttons stack instead of running
	   off both screen edges. */
	.sam-projects-topic-jumps {
		flex-direction: column;
		align-items: center;
	}

	/* The desktop grid and everything that belongs to it. */
	.sam-gallery-frames,
	.sam-grid-stepper--floating,
	.sam-grid-stage,
	.sam-frame-detail,
	.sam-topic-status,
	.sam-topic-gif-frame {
		display: none !important;
	}

	/* Phone projects box: every image (stepper and opened project) sits in
	   this one fixed box, as big as the screen allows once the title,
	   stepper, gap, switch and bouncing arrows have their room. Fixed
	   width AND height, so a tall gif scales down instead of stretching
	   it. The text view swaps into the same box. */
	.sam-projects-snap-home {
		--phone-box-w: calc(100vw - 2rem);
		--phone-box-h: calc(100vh - 22.5rem);
		--phone-box-h: calc(100svh - 22.5rem);
	}

	/* Sideways flicks on the steppers belong to the page (addSwipe in
	   home.js): pan-y keeps vertical scrolling native, and turning off
	   sideways overscroll stops the browser treating a right-flick as
	   "go back a page". */
	html,
	.sam-projects-snap-home {
		overscroll-behavior-x: none;
	}

	.sam-phone-stepper,
	.sam-project-detail {
		touch-action: pan-y;
	}

	/* Phone stepper: gif, prompt, then one slide per project. Top to
	   bottom: (title) box, stepper, gap, switch. */
	.sam-phone-stepper {
		display: flex;
		flex-direction: column;
		align-items: center;
		padding: 8.75rem 1rem 3.5rem;
	}

	.sam-phone-frame {
		order: 1;
		display: flex;
		align-items: center;
		justify-content: center;
		flex: none;
		width: var(--phone-box-w);
		height: var(--phone-box-h);
		overflow: hidden;
	}

	.sam-phone-slide {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 100%;
		height: 100%;
	}

	.sam-phone-slide[hidden] {
		display: none;
	}

	.sam-phone-gif,
	.sam-phone-slide-img {
		display: block;
		max-width: 100%;
		max-height: 100%;
		object-fit: contain;
	}

	.sam-phone-gif.sam-projects-bg-artifact--light {
		display: none;
	}

	html[data-theme="light"] .sam-phone-gif.sam-projects-bg-artifact--dark {
		display: none;
	}

	html[data-theme="light"] .sam-phone-gif.sam-projects-bg-artifact--light {
		display: block;
	}

	.sam-phone-slide-img {
		cursor: pointer;
	}

	.sam-phone-slide--prompt p {
		margin: 0;
		color: var(--sam-fg);
		font-size: 0.85rem;
		font-weight: 700;
		letter-spacing: 0.1em;
		line-height: 2.2;
		text-align: center;
		text-transform: uppercase;
	}

	.sam-phone-stepper-nav {
		order: 2;
		gap: 0.25rem;
		margin-top: 0.25rem;
	}

	/* Text view: the description fills the image box's exact footprint
	   and scrolls inside it when long. The !important sizes also beat the
	   inline margin/width alignDetailTextToImage (home.js) sets to line
	   the opened project's text up beside its image on desktop. */
	.sam-phone-stepper-text,
	.sam-project-detail-text {
		order: 1;
		display: none;
		flex: none;
		box-sizing: border-box;
		width: var(--phone-box-w) !important;
		max-width: none !important;
		height: var(--phone-box-h);
		margin: 0 !important;
		padding: 0.25rem 0.25rem 0.5rem;
		overflow-y: auto;
		overscroll-behavior: contain;
		color: var(--sam-fg);
		font-size: 1rem;
		font-weight: 700;
		line-height: 1.6;
		opacity: 0.7;
		text-align: justify;
	}

	html.sam-view-text .sam-phone-stepper:not(.is-intro) .sam-phone-frame,
	html.sam-view-text .sam-project-detail-frame {
		display: none;
	}

	/* Flex column with growing spacers either side: short text sits
	   centred in the box; long text starts at the top and scrolls
	   (plain centring would push its first lines out of reach). */
	html.sam-view-text .sam-phone-stepper:not(.is-intro) .sam-phone-stepper-text,
	html.sam-view-text .sam-project-detail-text {
		display: flex;
		flex-direction: column;
	}

	.sam-phone-stepper-text::before,
	.sam-phone-stepper-text::after,
	.sam-project-detail-text::before,
	.sam-project-detail-text::after {
		content: "";
		flex: 1 1 0;
	}

	/* Image/text switch: below the stepper after a gap. Hidden (space
	   kept) on the stepper's intro slides, which have no text. */
	.sam-phone-view-switch {
		order: 3;
		display: flex;
		align-items: center;
		justify-content: center;
		flex: none;
		width: 6rem;
		height: 3rem;
		max-width: none;
		margin: 1rem 0 0;
		padding: 0;
		border: none;
		border-radius: 0;
		background: none;
		box-shadow: none;
		line-height: 1;
		cursor: pointer;
		touch-action: manipulation;
	}

	.sam-phone-view-switch:hover,
	.sam-phone-view-switch:focus,
	.sam-phone-view-switch:active {
		background: none;
		box-shadow: none;
	}

	.sam-phone-view-switch:focus-visible {
		outline: 1px dashed var(--sam-fg);
		outline-offset: 4px;
	}

	.sam-phone-stepper.is-intro .sam-phone-view-switch {
		visibility: hidden;
	}

	/* One of four images shows: images/text state x dark/light theme. */
	.sam-view-switch-img {
		display: none;
		width: auto;
		height: 2.4rem;
	}

	html:not([data-theme="light"]):not(.sam-view-text) .sam-view-switch-img--images.sam-view-switch-img--to-light,
	html[data-theme="light"]:not(.sam-view-text) .sam-view-switch-img--images.sam-view-switch-img--to-dark,
	html.sam-view-text:not([data-theme="light"]) .sam-view-switch-img--text.sam-view-switch-img--to-light,
	html.sam-view-text[data-theme="light"] .sam-view-switch-img--text.sam-view-switch-img--to-dark {
		display: block;
	}

	/* Bouncing arrows to the neighbouring sections — no text. */
	.sam-phone-section-arrow {
		display: flex;
		position: absolute;
		left: 50%;
		z-index: 60;
		align-items: center;
		justify-content: center;
		width: 3.5rem;
		height: 2.5rem;
		max-width: none;
		margin: 0;
		padding: 0;
		border: none;
		border-radius: 0;
		background: none;
		box-shadow: none;
		color: var(--sam-fg) !important;
		line-height: 1;
		transform: translateX(-50%);
		cursor: pointer;
	}

	.sam-phone-section-arrow .sam-scroll-cue-arrow {
		display: block;
		line-height: 0;
	}

	.sam-phone-arrow-svg {
		display: block;
		width: 1.1rem;
		height: auto;
	}

	.sam-phone-section-arrow--up {
		top: 0.75rem;
	}

	.sam-phone-section-arrow--down {
		bottom: 1rem;
	}

	.is-project-locked .sam-phone-section-arrow,
	.sam-projects-section.is-project-active .sam-phone-stepper {
		display: none;
	}

	/* Full-project mode: frame, then its stepper, then the text, and
	   the view scrolls if the text runs long (desktop locks scrolling). */
	.sam-projects-snap-home.is-project-locked {
		overflow-y: auto;
	}

	/* Opened project: (title) box, stepper, gap, switch, zoom hint. */
	.sam-project-detail {
		padding: 7.75rem 1rem 0;
		margin-bottom: 0;
		box-sizing: border-box;
	}

	.sam-project-detail-home {
		display: contents;
	}

	.sam-project-detail-frame {
		order: 1;
		flex: none;
		width: var(--phone-box-w);
		height: var(--phone-box-h);
		overflow: hidden;
	}

	.sam-project-detail-stepper {
		order: 2;
		margin-top: 0.25rem;
		gap: 0.25rem;
	}

	.sam-project-zoom-hint {
		position: static;
		margin: 0.5rem 1rem 1rem;
		text-align: center;
		font-size: 0.75rem;
	}

	html.sam-view-text .sam-project-zoom-hint {
		visibility: hidden;
	}

	/* Escape / Go back in the top-left corner (beats the inline left/top
	   positionEscapeNextTo sets beside the desktop label). */
	.sam-project-escape {
		left: 0.5rem !important;
		top: 0.5rem !important;
	}

	/* Zoom view: no desktop rightward nudge, hint along the bottom. */
	.sam-project-detail-active-img,
	.sam-bio-active-img,
	.sam-bio-active-gif-img,
	.sam-bio-active-cv {
		left: 0;
		max-width: 92vw;
	}

	.sam-bio-active-cv-iframe {
		width: 92vw;
	}

	.sam-project-detail-active-hint,
	.sam-bio-active-hint {
		top: auto;
		bottom: 1.5rem;
		left: 50%;
		width: max-content;
		max-width: 90vw;
		transform: translateX(-50%);
		text-align: center;
	}

}
