/*
Theme Name: Ben Crum
Theme URI: https://ben-crum.com
Author: Ben Crum
Description: Block theme for ben-crum.com. No page builder — layout lives in
  templates, patterns and theme.json, so the whole design is reviewable as
  files in git. Content stays editable in wp-admin.
Version: 0.1.0
Requires at least: 6.6
Tested up to: 7.1
Requires PHP: 8.1
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: ben-crum
Tags: block-theme, portfolio, one-column, full-site-editing
*/

/* Everything expressible in theme.json lives there. This file is only for
   what theme.json cannot describe: pseudo-elements, transitions, and the
   handful of custom block styles registered in functions.php. */

/* --- Section label -------------------------------------------------------
   Was a mono uppercase accent-coloured line above every heading — the single
   most recognisable tell of a generated layout. It now sits in the margin
   beside the section number, quiet and in the body face. */
.is-style-eyebrow {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 500;
	letter-spacing: 0.01em;
	line-height: 1.35;
	color: var(--wp--preset--color--ink-soft);
	margin: 0;
	max-width: 12rem;
}

/* --- Stat: a big number with a caption under it ------------------------ */
.bc-stat__value {
	font-family: var(--wp--preset--font-family--display);
	/* Explicit clamp rather than the xx-large preset: a five-digit figure
	   like 45,456 overflows its grid track at the preset's 60px ceiling. */
	font-size: clamp(2.1rem, 1.2rem + 2.6vw, 3.4rem);
	font-weight: 500;
	line-height: 0.95;
	letter-spacing: -0.03em;
	color: var(--wp--preset--color--ink);
	font-variant-numeric: tabular-nums lining-nums;
}

.bc-stat__label {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--x-small);
	letter-spacing: 0.01em;
	line-height: 1.3;
	color: var(--wp--preset--color--ink-soft);
	margin-top: 0.6em;
	max-width: 14ch;
}

/* --- Card -----------------------------------------------------------------
   Was a bordered box that lifted on hover. Boxes-in-a-grid is the other big
   tell; these are now rows in an index, separated by rules and carried by
   type rather than by containers. */
.is-style-card {
	border: 0;
	background: none;
}

/* --- Header ------------------------------------------------------------ */
.bc-header {
	border-bottom: 1px solid var(--wp--preset--color--ink);
}

.bc-header .wp-block-navigation a {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	text-decoration: none;
	color: var(--wp--preset--color--ink);
}

.bc-header .wp-block-navigation a:hover,
.bc-header .wp-block-navigation .current-menu-item a {
	color: var(--wp--preset--color--accent);
}

.bc-site-title a {
	font-family: var(--wp--preset--font-family--display);
	font-weight: 500;
	letter-spacing: -0.02em;
	text-decoration: none;
	color: var(--wp--preset--color--ink);
}

/* --- Footer ------------------------------------------------------------ */
.bc-footer {
	border-top: 1px solid var(--wp--preset--color--line);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ink-soft);
}

.bc-footer a {
	color: var(--wp--preset--color--ink-soft);
	text-decoration: none;
}

.bc-footer a:hover {
	color: var(--wp--preset--color--accent);
	text-decoration: underline;
}

/* --- Accessibility ------------------------------------------------------ */
.skip-link:focus {
	position: fixed;
	top: 0.5rem;
	left: 0.5rem;
	z-index: 999;
	padding: 0.75rem 1rem;
	background: var(--wp--preset--color--ink);
	color: var(--wp--preset--color--paper);
	border-radius: 2px;
}

:where(a:focus-visible, button:focus-visible, .wp-block-button__link:focus-visible) {
	outline: 2px solid var(--wp--preset--color--accent);
	outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
	.is-style-card,
	.is-style-card:hover {
		transition: none;
		transform: none;
	}
}

/* --- Layout utilities -------------------------------------------------- */
/* These exist so page content can stay clean block markup with a class,
   rather than carrying dozens of inline styles that nobody can review. */

.bc-lede {
	font-size: var(--wp--preset--font-size--large);
	line-height: 1.5;
	color: var(--wp--preset--color--ink-soft);
	max-width: 38ch;
}

/* A constrained layout centres every child with `margin-left: auto !important`
   (see the `.is-layout-constrained > :where(...)` rule in core's global
   styles). Because the lede sets its own max-width that centring is visible —
   it sits indented from the body copy below it. Overriding core's !important
   is the only way to win here. */
.is-layout-constrained > .bc-lede {
	margin-inline-start: 0 !important;
}

.bc-stats,
.bc-cards {
	display: grid;
	gap: var(--wp--preset--spacing--30);
}

/* Zero the flow-layout margins core adds; the grid gap handles spacing. */
.bc-stats > *,
.bc-cards > * {
	margin-block: 0;
}

.bc-stats {
	grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
	gap: var(--wp--preset--spacing--40) var(--wp--preset--spacing--30);
}

/* An index, not a grid of boxes: title hangs on the left, everything else
   sits in a second column, each row divided by a rule. */
.bc-cards {
	grid-template-columns: 1fr;
	gap: 0;
}

.bc-cards > * {
	display: grid;
	grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
	gap: var(--wp--preset--spacing--20) var(--wp--preset--spacing--40);
	align-items: start;
	padding-block: var(--wp--preset--spacing--30);
	border-top: 1px solid var(--wp--preset--color--line);
}

.bc-cards > *:last-child {
	border-bottom: 1px solid var(--wp--preset--color--line);
}

.bc-cards h3 {
	grid-column: 1;
	margin: 0;
	font-size: var(--wp--preset--font-size--large);
}

.bc-cards > * > *:not(h3) {
	grid-column: 2;
	margin: 0;
}

.bc-cards p {
	font-size: var(--wp--preset--font-size--medium);
	color: var(--wp--preset--color--ink-soft);
	max-width: 46ch;
}

.bc-cards h3 a {
	text-decoration: none;
}

.bc-cards > *:hover h3 a {
	color: var(--wp--preset--color--accent);
}

@media (max-width: 44rem) {
	.bc-cards > * {
		grid-template-columns: 1fr;
	}

	.bc-cards > * > *:not(h3) {
		grid-column: 1;
	}
}

/* Tag list: "API integration · Python · Headless".
   Was mono uppercase with wide tracking. Mono is now reserved for section
   numbers and actual code — using it for decoration everywhere was part of
   what made the page read as generated. */
.bc-tags {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 500;
	letter-spacing: 0;
	color: var(--wp--preset--color--ink-soft);
}

/* Sections are numbered and asymmetric: an index number (and label, if the
   content carries one) hangs in a narrow left margin, the content sits in a
   wider column beside it. The number comes from a CSS counter, so nothing
   has to be maintained by hand in wp-admin. */
main {
	counter-reset: bc-section;
}

.bc-section {
	display: grid;
	grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr);
	column-gap: var(--wp--preset--spacing--40);
	row-gap: var(--wp--preset--spacing--20);
	border-top: 1px solid var(--wp--preset--color--ink);
	padding-top: var(--wp--preset--spacing--30);
	margin-top: var(--wp--preset--spacing--60);
}

.bc-section::before {
	counter-increment: bc-section;
	content: counter(bc-section, decimal-leading-zero) " \2044";
	grid-column: 1;
	grid-row: 1;
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 500;
	color: var(--wp--preset--color--ink-soft);
	line-height: 1.6;
}

/* The label, where a section has one, tucks under the number. */
.bc-section > .is-style-eyebrow {
	grid-column: 1;
	grid-row: 2;
	align-self: start;
}

.bc-section > *:not(.is-style-eyebrow) {
	grid-column: 2;
}

/* No explicit rows for the content: `:first-of-type` matches the first of
   EVERY element type, so the heading, the first paragraph and the first div
   all claimed row 1 and stacked on top of one another. Auto-placement fills
   column 2 top to bottom on its own. */

@media (max-width: 48rem) {
	.bc-section {
		grid-template-columns: 1fr;
	}

	/* The `:not()` has to be repeated here. `.bc-section > *` alone loses to
	   `.bc-section > *:not(.is-style-eyebrow)` above, because :not() adds its
	   argument's specificity — so the content stayed in column two and grid
	   created an implicit second track at phone widths. */
	.bc-section > *,
	.bc-section > *:not(.is-style-eyebrow),
	.bc-section > .is-style-eyebrow {
		grid-column: 1;
		grid-row: auto;
	}

	.bc-section::before {
		grid-row: auto;
	}
}

/* Back-link to the parent page, used at the top of case studies. */
.bc-backlink a {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);
	letter-spacing: 0;
	text-decoration: none;
	color: var(--wp--preset--color--ink-soft);
}

.bc-backlink a:hover {
	color: var(--wp--preset--color--accent);
}

/* --- Motion ------------------------------------------------------------ */
/* motion.js adds `.bc-reveal` only to elements that are already below the
   fold, so nothing visible is ever hidden by script and a failure cannot
   blank the page — the trap Elementor's `elementor-invisible` falls into.
   That is also why no `.js` guard class is needed here. */

.bc-reveal {
	opacity: 0;
	transform: translateY(18px);
}

.bc-reveal.is-revealed {
	opacity: 1;
	transform: none;
	transition:
		opacity 600ms cubic-bezier(0.2, 0.6, 0.2, 1),
		transform 600ms cubic-bezier(0.2, 0.6, 0.2, 1);
}

/* --- Media band: text over a looping background video ------------------ */
.bc-media-band {
	position: relative;
	isolation: isolate;
	overflow: hidden;
	border-radius: 3px;
	margin-block: var(--wp--preset--spacing--60);
	/* Painted before any media loads, so the band is never a bright flash
	   and text is legible from the first frame. */
	background-color: var(--wp--preset--color--ink);
}

.bc-media-band__bg {
	position: absolute;
	inset: 0;
	z-index: -2;
	background-size: cover;
	background-position: center;
}

.bc-media-band__bg video {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* The scrim is what makes this safe. A video frame can be any colour,
   including pure white, so contrast cannot depend on the footage. At 0.78
   minimum opacity the body text clears WCAG AAA (7:1) even against a fully
   white frame — see scripts/check-contrast.py, which derives the floor. */
.bc-media-band__scrim {
	position: absolute;
	inset: 0;
	z-index: -1;
	background: linear-gradient(
		180deg,
		rgba(21, 24, 28, 0.88) 0%,
		rgba(21, 24, 28, 0.78) 45%,
		rgba(21, 24, 28, 0.9) 100%
	);
}

.bc-media-band__inner {
	padding: var(--wp--preset--spacing--60) var(--wp--preset--spacing--40);
	color: var(--wp--preset--color--paper);
}

/* Flow layout inside the band, with prose capped for readability and the
   stat grid allowed the full width. Doing this here rather than with nested
   constrained layouts keeps every child on the same left edge. */
.bc-media-band__inner > * {
	max-width: 46rem;
}

.bc-media-band__inner > .bc-stats {
	max-width: none;
}

.bc-media-band__inner :where(h2, h3) {
	color: var(--wp--preset--color--paper);
}

.bc-media-band__inner .is-style-eyebrow {
	color: var(--wp--preset--color--accent-soft);
}

.bc-media-band__inner .bc-stat__value {
	color: var(--wp--preset--color--paper);
}

.bc-media-band__inner .bc-stat__label,
.bc-media-band__inner .bc-tags {
	color: var(--wp--preset--color--accent-soft);
}

.bc-media-band__inner .bc-lede {
	color: var(--wp--preset--color--accent-soft);
}

/* --- Reduced motion ----------------------------------------------------- */
/* One place that switches every effect off: reveals show immediately,
   counters print their final value, and motion.js declines to insert the
   video at all (it checks the same media query). */
@media (prefers-reduced-motion: reduce) {
	.bc-reveal,
	.bc-reveal.is-revealed {
		opacity: 1;
		transform: none;
		transition: none;
	}
}

/* --- Visually hidden ---------------------------------------------------- */
/* The counters put the real figure in one of these and animate a separate
   aria-hidden span, so assistive tech always gets the final number. */
.screen-reader-text:not(.skip-link) {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* --- Scroll-scrubbed frame sequence ------------------------------------ */
/* Apple/Sonos-style: scroll position drives which frame of an image sequence
   is drawn, so the animation advances while scrolling and holds still the
   moment scrolling stops. Nothing is time-based.

   The static <img> is what ships in the HTML and what everyone sees by
   default. JS only swaps in the canvas once it has decided to animate AND
   the first frame has decoded, so no-JS, reduced-motion, narrow screens and
   failed loads all land on a perfectly good still image.

   Two pinned variants:
     --behind  full-bleed artwork, text scrolling over it
     --split   artwork pinned in the right half, text column on the left  */

.bc-scrollseq {
	position: relative;
	margin-block: var(--wp--preset--spacing--50);
}

.bc-scrollseq__media {
	position: relative;
	max-width: 46rem;
	margin-inline: auto;
}

.bc-scrollseq__canvas {
	display: none;
	width: 100%;
	height: auto;
}

.bc-scrollseq__static {
	display: block;
	width: 100%;
	height: auto;
}

.bc-scrollseq__veil,
.bc-scrollseq__hint,
.bc-scrollseq__progress {
	display: none;
}

.bc-scrollseq__content {
	position: relative;
	z-index: 1;
	max-width: 75rem;
	margin-inline: auto;
	padding-inline: var(--wp--preset--spacing--30);
}

/* Everything below applies only once JS has taken over. Keeping the tall
   track and the sticky pin behind this class means the section occupies
   normal document height for everyone else. */

.bc-scrollseq.is-animating .bc-scrollseq__canvas {
	display: block;
	width: auto;
	height: auto;
}

.bc-scrollseq.is-animating .bc-scrollseq__static {
	display: none;
}

.bc-scrollseq.is-animating .bc-scrollseq__media {
	display: flex;
	align-items: center;
	justify-content: center;
	max-width: none;
	position: sticky;
	top: 0;
	height: 100vh;
}

/* --- Variant: full-bleed artwork behind the text ----------------------- */

.bc-scrollseq--behind.is-animating {
	/* Enough scroll distance for 30 frames even if the copy is short. */
	min-height: 240vh;
}

.bc-scrollseq--behind.is-animating .bc-scrollseq__media {
	/* Pull the content back up over the pinned artwork. */
	margin-bottom: -100vh;
	z-index: 0;
	/* Anchored right and allowed to run off the edge, so the drawing reads as
	   a full-bleed backdrop rather than a centred picture. */
	justify-content: flex-end;
	overflow: hidden;
}

.bc-scrollseq--behind.is-animating .bc-scrollseq__canvas {
	max-height: 96vh;
	margin-right: -5vw;
	/* Verified against the real frames in scripts/check-artwork-contrast.py:
	   at this opacity, under the veil below, body text reads 14.8:1 and
	   secondary text 5.19:1. */
	opacity: 0.65;
}

/* The veil is what makes text over artwork safe. Paper-coloured, near-opaque
   across the text column, clearing entirely on the right so the drawing is
   seen at full strength where nothing is written over it. */
.bc-scrollseq--behind.is-animating .bc-scrollseq__veil {
	display: block;
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: linear-gradient(
		90deg,
		rgba(251, 250, 247, 0.94) 0%,
		rgba(251, 250, 247, 0.94) 44%,
		rgba(251, 250, 247, 0.82) 56%,
		rgba(251, 250, 247, 0.5) 68%,
		rgba(251, 250, 247, 0.18) 80%,
		rgba(251, 250, 247, 0) 92%
	);
}

.bc-scrollseq--behind.is-animating .bc-scrollseq__content {
	padding-block: 30vh 40vh;
}

.bc-scrollseq--behind.is-animating .bc-scrollseq__content > * {
	/* Keep the copy inside the veiled column. */
	max-width: 32rem;
}
/* Space the copy down the track so it accompanies the whole assembly rather
   than finishing in the first screen and leaving bare artwork behind. */
.bc-scrollseq--behind.is-animating .bc-scrollseq__content > * + * {
	margin-block-start: 34vh;
}

/* --- Variant: artwork pinned right, text column left ------------------- */

.bc-scrollseq--split.is-animating {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--wp--preset--spacing--40);
	align-items: start;
	max-width: 75rem;
	margin-inline: auto;
	padding-inline: var(--wp--preset--spacing--30);
}

.bc-scrollseq--split.is-animating .bc-scrollseq__media {
	grid-column: 2;
	grid-row: 1;
}

.bc-scrollseq--split.is-animating .bc-scrollseq__canvas {
	max-height: 74vh;
	max-width: 100%;
}

.bc-scrollseq--split.is-animating .bc-scrollseq__hint,
.bc-scrollseq--split.is-animating .bc-scrollseq__progress {
	right: auto;
	left: 50%;
	transform: translateX(-50%);
}

.bc-scrollseq--split.is-animating .bc-scrollseq__content {
	grid-column: 1;
	grid-row: 1;
	max-width: none;
	padding-inline: 0;
	/* Scroll distance for the scrub, independent of how long the copy is. */
	min-height: 240vh;
	padding-block: 30vh 40vh;
}

/* Spread the copy down the track in this variant too, so it accompanies the
   assembly rather than finishing in the first screen. */
.bc-scrollseq--split.is-animating .bc-scrollseq__content > * + * {
	margin-block-start: 30vh;
}

/* Not enough room for two columns: fall back to the unpinned still. */
@media (max-width: 64rem) {
	.bc-scrollseq--split.is-animating {
		display: block;
		min-height: 0;
	}

	.bc-scrollseq--split.is-animating .bc-scrollseq__media {
		position: static;
		height: auto;
		max-width: 46rem;
	}

	.bc-scrollseq--split.is-animating .bc-scrollseq__content {
		min-height: 0;
	}
}

/* --- Shared chrome ------------------------------------------------------ */

.bc-scrollseq.is-animating .bc-scrollseq__hint {
	display: block;
	position: absolute;
	bottom: 9vh;
	right: 8vw;
	left: auto;
	margin: 0;
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--x-small);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--wp--preset--color--ink-soft);
	/* Fades out as the sequence completes; driven by scroll, not a timer. */
	transition: opacity 300ms ease;
}

.bc-scrollseq.is-animating .bc-scrollseq__progress {
	display: block;
	position: absolute;
	bottom: 7vh;
	right: 8vw;
	left: auto;
	width: min(14rem, 30%);
	height: 2px;
	background: var(--wp--preset--color--line);
	border-radius: 2px;
	overflow: hidden;
}

.bc-scrollseq__progress span {
	display: block;
	height: 100%;
	width: 0;
	background: var(--wp--preset--color--accent);
}

@media (prefers-reduced-motion: reduce) {
	.bc-scrollseq.is-animating,
	.bc-scrollseq--behind.is-animating,
	.bc-scrollseq--split.is-animating {
		display: block;
		min-height: 0;
	}

	.bc-scrollseq.is-animating .bc-scrollseq__media {
		position: static;
		height: auto;
		margin-bottom: 0;
		max-width: 46rem;
	}

	.bc-scrollseq.is-animating .bc-scrollseq__canvas {
		opacity: 1;
	}

	.bc-scrollseq.is-animating .bc-scrollseq__veil {
		display: none;
	}
}

/* --- Page transitions --------------------------------------------------- */
/* Leaving: content blurs, slides down, fades out, then the browser navigates.
   Arriving: content rises, unblurs, fades in.

   The arrival is a plain CSS animation with `both` fill, so it runs on load
   and finishes by itself. Nothing waits on JavaScript to become visible — if
   page-transition.js never loads, the page simply animates in and ordinary
   links work as normal. Only the departure needs script, because it has to
   hold the navigation open long enough to play.

   Tune everything here; the JS reads no timings. Keep --bc-pt-out in step
   with the `fallback` in page-transition.js. */

:root {
	--bc-pt-distance: 28px;
	--bc-pt-blur: 8px;
	--bc-pt-out: 320ms;
	--bc-pt-in: 480ms;
	/* Leaving accelerates away: slow to start, faster as it goes. */
	--bc-pt-ease-out: cubic-bezier(0.4, 0, 1, 1);
	/* Arriving decelerates into place rather than slamming to a stop. */
	--bc-pt-ease-in: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: no-preference) {
	main {
		/* `backwards`, not `both`: the first keyframe applies before the
		   animation starts (so there is no flash of un-animated content), but
		   nothing lingers afterwards. `both` would leave filter: blur(0) and a
		   transform permanently on <main>, which creates a stacking context
		   and pins a compositing layer around the whole page — including the
		   scroll-sequence canvas — for no visual gain. */
		animation: bc-page-enter var(--bc-pt-in) var(--bc-pt-ease-in) backwards;
	}

	.bc-leaving main {
		/* Cancel the arrival if it is somehow still running. */
		animation: none;
		opacity: 0;
		transform: translateY(var(--bc-pt-distance));
		filter: blur(var(--bc-pt-blur));
		transition:
			opacity var(--bc-pt-out) var(--bc-pt-ease-out),
			transform var(--bc-pt-out) var(--bc-pt-ease-out),
			filter var(--bc-pt-out) var(--bc-pt-ease-out);
	}
}

@keyframes bc-page-enter {
	from {
		opacity: 0;
		/* Rises into place from below. */
		transform: translateY(var(--bc-pt-distance));
		filter: blur(var(--bc-pt-blur));
	}

	to {
		opacity: 1;
		transform: none;
		filter: blur(0);
	}
}


/* --- Hero -----------------------------------------------------------------
   Asymmetric rather than centred: the name and positioning run wide on the
   left, the portrait holds a narrower column on the right, aligned to the
   baseline of the copy rather than floated in a box. */
.bc-hero {
	display: grid;
	grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
	column-gap: var(--wp--preset--spacing--50);
	align-items: end;
	padding-block: var(--wp--preset--spacing--60) var(--wp--preset--spacing--40);
	border-bottom: 1px solid var(--wp--preset--color--ink);
}

.bc-hero__text > * {
	margin-block: 0;
}

.bc-hero__text .is-style-eyebrow {
	max-width: 26ch;
	font-size: var(--wp--preset--font-size--small);
}

.bc-hero__text h1 {
	margin-block: 0.15em 0.35em;
}

.bc-hero__portrait {
	margin: 0;
	align-self: end;
}

.bc-hero__portrait img {
	display: block;
	width: 100%;
	height: auto;
}

.bc-hero__portrait figcaption {
	margin-top: 0.7em;
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--ink-soft);
	max-width: 22ch;
}

@media (max-width: 52rem) {
	.bc-hero {
		grid-template-columns: 1fr;
		row-gap: var(--wp--preset--spacing--40);
	}

	.bc-hero__portrait {
		max-width: 18rem;
		order: -1;
	}
}

/* --- Lede ------------------------------------------------------------------
   Set in the display serif so the opening statement carries weight without
   needing a larger size or an accent colour. */
.bc-lede {
	font-family: var(--wp--preset--font-family--display);
	font-weight: 400;
}
