/* ==========================================================
   Static pages and the 404 screen. Both lean on the tokens and
   prose rules declared in style.css; everything here is scoped to
   .static-page / .error-404 so no global selector is redefined.
   ========================================================== */

.static-page .page-body,
.error-404 .error-body {
	max-width: 72ch;
	margin-inline: auto;
	padding-bottom: clamp(56px, 8vw, 104px);
}

.static-page .page-hero h1 {
	max-width: 22ch;
}

/* ---------- 404 ---------- */

.error-404 .page-hero h1 {
	font-family: var(--font-mono);
	letter-spacing: -0.04em;
}

.error-lede {
	max-width: 46ch;
	margin: 18px auto 0;
	text-align: center;
}

.error-actions {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 12px;
	margin-top: 28px;
}

/* ---------- about ---------- */

/* The about page is a profile, not an article, so it opts out of the shared
   72ch column and lays itself out per block. */
.about-page {
	position: relative;
	isolation: isolate;
	background:
		radial-gradient(120% 60% at 50% 0%, var(--teal-050) 0%, transparent 62%),
		linear-gradient(var(--paper) 0%, var(--paper-2) 100%);
}

.about-page .page-hero {
	padding-bottom: 0;
}

/* ---- hero ---- */

.about-hero {
	position: relative;
	overflow: hidden;
	padding-bottom: clamp(40px, 6vw, 76px);
}

/* A single soft light source behind the opening statement. It sits under the
   text (z-index) and never intercepts a click (pointer-events). */
.about-aurora {
	position: absolute;
	top: -34%;
	left: 50%;
	z-index: -1;
	width: min(1180px, 128vw);
	aspect-ratio: 1;
	pointer-events: none;
	transform: translateX(-50%);
	background:
		radial-gradient(closest-side, rgb(15 179 168 / 22%), transparent 70%),
		radial-gradient(closest-side at 68% 42%, rgb(124 240 221 / 26%), transparent 66%);
	filter: blur(18px);
	animation: ysj-aurora-drift 26s var(--ease-inout) infinite alternate;
}

@keyframes ysj-aurora-drift {
	from {
		transform: translateX(-50%) translateY(0) scale(1);
	}

	to {
		transform: translateX(-46%) translateY(26px) scale(1.08);
	}
}

/* The hero is above the fold, so it plays once on load rather than waiting for
   an IntersectionObserver. Each element starts hidden and is animated in with
   `both`, so the end state sticks even if the animation is interrupted. */
@keyframes ysj-hero-in {
	from {
		opacity: 0;
		transform: translateY(22px);
	}

	to {
		opacity: 1;
		transform: none;
	}
}

.about-hero .eyebrow,
.about-hero .page-hero h1,
.about-lede,
.about-intro {
	animation: ysj-hero-in var(--dur-slow) var(--ease-out) both;
}

.about-hero .eyebrow {
	animation-delay: 60ms;
}

.about-hero .page-hero h1 {
	animation-delay: 150ms;
}

.about-lede {
	animation-delay: 260ms;
}

.about-intro {
	animation-delay: 420ms;
}

.about-opening {
	padding-top: clamp(24px, 4vw, 44px);
}

/* Two lines at every width. The statement is ~25 CJK character-widths, so the
   line box has to hold at least 13 of them: the font size is capped against the
   viewport (6vw) rather than only against the type scale, which is what keeps
   it from spilling onto a third line on a narrow phone. */
.about-lede {
	max-width: 100%;
	margin: 0;
	font-size: min(6vw, 4.4rem);
	font-weight: 600;
	line-height: 1.18;
	letter-spacing: -0.03em;
	color: var(--ink-900);
	text-wrap: balance;
}

@media (min-width: 1200px) {
	/* The requested closing phrase is short enough to fit as one desktop line.
	   Keep mobile fluid so the same wording never forces horizontal overflow. */
	.about-lede-keep {
		white-space: nowrap;
	}
}

/* .shell sets margin-inline: auto, so a narrower max-width on the same element
   would centre the paragraph while every other block stays left-aligned. The
   intro is its own child now, and is pinned left.

   No measure cap: the intro has to end exactly where the section hairline ends,
   so it fills its column and the column edge is the measure. */
.about-intro {
	max-width: none;
	margin: clamp(28px, 4vw, 44px) 0 0;
	color: var(--ink-700);
}

/* Both paragraphs are ~85 CJK characters. Holding them to three lines means
   ~29 characters per line, so the size tracks the column width rather than the
   type scale — the same trick the lede uses, one step down. */
.about-intro p {
	font-size: clamp(0.95rem, 0.42rem + 0.92vw, 1.45rem);
	line-height: 1.75;
}

.about-sections {
	display: grid;
	gap: clamp(56px, 8vw, 104px);
	margin-top: clamp(64px, 9vw, 120px);
	padding-bottom: clamp(64px, 9vw, 120px);
}

/* A hairline per section is what turns a run of prose into a numbered index. */
.about-section {
	position: relative;
	padding-top: clamp(28px, 3.5vw, 44px);
	border-top: 1px solid var(--ink-100);
}

.about-section-rail {
	display: flex;
	align-items: baseline;
	gap: 14px;
}

.about-section-index {
	font-family: var(--font-mono);
	font-size: var(--step--1);
	font-weight: 500;
	color: var(--teal-700);
	letter-spacing: 0.08em;
}

/* The rule above each section draws itself from left to right as the section
   scrolls into view. Where scroll-driven animation is unavailable the border
   declared above is already the finished state, so nothing is lost. */
@supports (animation-timeline: view()) {
	@media not (prefers-reduced-motion: reduce) {
		.about-section {
			border-top-color: transparent;
		}

		.about-section::before {
			content: "";
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 1px;
			background: linear-gradient(90deg, var(--teal-500), var(--ink-100));
			transform-origin: left;
			animation: ysj-rule-draw linear both;
			animation-timeline: view();
			animation-range: entry 10% entry 65%;
		}
	}
}

@keyframes ysj-rule-draw {
	from {
		transform: scaleX(0);
	}

	to {
		transform: scaleX(1);
	}
}

.about-section-title {
	margin: 0;
	font-size: var(--step-1);
	font-weight: 600;
	line-height: 1.3;
	letter-spacing: -0.01em;
	color: var(--ink-900);
	text-wrap: balance;
}

/* Gradient type, guarded: background-clip: text makes the colour transparent,
   so it can only be applied where the clip itself is supported — otherwise the
   heading would disappear entirely. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
	.about-section-title {
		background: linear-gradient(96deg, var(--ink-900) 0%, var(--teal-700) 58%, var(--teal-500) 100%);
		background-clip: text;
		-webkit-background-clip: text;
		color: transparent;
		-webkit-text-fill-color: transparent;
	}
}

.about-section-body {
	margin-top: clamp(24px, 3vw, 36px);
}

/* Same rule as the opening paragraph: no measure cap, so the text ends exactly
   where the section hairline ends at every width. */
.about-section-intro {
	max-width: none;
	color: var(--ink-700);
}

.about-cards {
	display: grid;
	/* Cards size to their own text: one very long card would otherwise stretch
	   its whole row and leave the short one half empty. */
	align-items: start;
	gap: clamp(14px, 1.8vw, 22px);
	margin: clamp(24px, 3vw, 36px) 0 0;
	padding: 0;
	list-style: none;
}

.about-card {
	position: relative;
	padding: clamp(22px, 2.6vw, 32px);
	background: linear-gradient(168deg, var(--paper) 0%, var(--paper-2) 100%);
	border: 1px solid var(--ink-100);
	border-radius: var(--r-card);
	transition: border-color var(--dur-base) var(--ease-out),
		box-shadow var(--dur-base) var(--ease-out),
		transform var(--dur-base) var(--ease-out);
}

/* A teal wash that fades in behind the card on hover. It lives on a pseudo
   element so the card's own gradient does not have to be re-declared, and it is
   pinned below the text with z-index. */
.about-card::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	border-radius: inherit;
	background: radial-gradient(120% 100% at 0% 0%, var(--teal-050), transparent 62%);
	opacity: 0;
	transition: opacity var(--dur-base) var(--ease-out);
}

.about-card > * {
	position: relative;
	z-index: 1;
}

.about-card:hover {
	border-color: var(--teal-300);
	box-shadow: var(--shadow-2);
	transform: translateY(-4px);
}

.about-card:hover::before {
	opacity: 1;
}

/* Not just bigger and bolder: a short teal rule sits above the title and grows
   when the card is hovered, which is what gives the four cards a shared mark
   instead of four blocks of heavy text. */
.about-card-title {
	position: relative;
	margin: 0 0 12px;
	padding-top: 18px;
	font-size: var(--step-0);
	font-weight: 650;
	line-height: 1.4;
	letter-spacing: -0.015em;
	color: var(--ink-900);
}

.about-card-title::before {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	width: 26px;
	height: 3px;
	border-radius: 3px;
	background: linear-gradient(90deg, var(--teal-500), var(--teal-300));
	transition: width var(--dur-base) var(--ease-out);
}

.about-card:hover .about-card-title::before {
	width: 52px;
}

@supports (background-clip: text) or (-webkit-background-clip: text) {
	.about-card-title {
		background: linear-gradient(94deg, var(--ink-900) 0%, var(--teal-700) 100%);
		background-clip: text;
		-webkit-background-clip: text;
		color: transparent;
		-webkit-text-fill-color: transparent;
	}
}

.about-card-body > :first-child {
	margin-top: 0;
}

.about-card-body > :last-child {
	margin-bottom: 0;
}

.about-card-body p,
.about-card-body li {
	font-size: var(--step-0);
	color: var(--ink-700);
}

/* ---------- contact ---------- */

.contact-page .page-hero {
	padding-bottom: clamp(30px, 5vw, 58px);
}

.contact-hero h1 {
	max-width: 12ch;
}

.contact-stage {
	position: relative;
	display: grid;
	grid-template-columns: minmax(0, 1.08fr) minmax(300px, 0.92fr);
	min-height: clamp(400px, 44vw, 540px);
	margin-bottom: clamp(64px, 9vw, 120px);
	isolation: isolate;
	overflow: hidden;
	background: linear-gradient(152deg, #04413e 0%, var(--teal-900) 46%, #01201f 100%);
	border: 1px solid rgb(124 240 221 / 18%);
	border-radius: clamp(22px, 3vw, 34px);
	box-shadow: 0 24px 70px rgb(1 47 45 / 18%);
}

/* Three teal light sources drifting behind the panel. Absolutely positioned, so
   it leaves the grid flow and never becomes a third column. */
.contact-stage::before {
	content: "";
	position: absolute;
	inset: -30%;
	z-index: 0;
	pointer-events: none;
	background: radial-gradient(closest-side at 24% 30%, rgb(15 179 168 / 32%), transparent 72%),
		radial-gradient(closest-side at 76% 74%, rgb(124 240 221 / 20%), transparent 70%),
		radial-gradient(closest-side at 64% 16%, rgb(2 109 106 / 45%), transparent 74%);
	animation: ysj-mesh-drift 24s var(--ease-inout) infinite alternate;
}

@keyframes ysj-mesh-drift {
	from {
		transform: translate3d(-3%, -2%, 0) scale(1);
	}

	to {
		transform: translate3d(4%, 3%, 0) scale(1.12);
	}
}

.contact-panel,
.contact-visual {
	position: relative;
	z-index: 1;
}

.contact-panel {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	justify-content: center;
	padding: clamp(34px, 6vw, 84px);
}

.contact-copy {
	padding-bottom: clamp(34px, 5vw, 56px);
}

/* The measure belongs on the paragraph, not the wrapper: `ch` resolves against
   the element's own font size, and the wrapper inherits .article-body's 18px,
   which made 26ch about nine display-sized characters instead of twenty-six. */
.contact-copy p {
	max-width: 15.5em;
	font-size: clamp(1.3rem, 1.05rem + 0.95vw, 1.9rem);
	font-weight: 500;
	line-height: 1.62;
	letter-spacing: -0.015em;
	color: var(--paper);
	/* Chinese wraps per character by default, which is the correct behaviour
	   here: forcing whole phrases to stay together left every line ending early
	   at the nearest 、 and made the paragraph look ragged. */
	overflow-wrap: break-word;
}

.contact-copy .contact-inline-email {
	color: var(--teal-300);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.18em;
	transition: color var(--dur-fast) var(--ease-out);
}

.contact-copy .contact-inline-email:hover,
.contact-copy .contact-inline-email:focus-visible {
	color: var(--paper);
}

/* The pill stays — it is the button shape used across the whole site — but it
   gets a gradient face and a sheen that sweeps across on hover. */
.contact-email {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: space-between;
	gap: 34px;
	min-width: min(100%, 240px);
	padding: 15px 18px 15px 22px;
	overflow: hidden;
	border: 1px solid rgb(124 240 221 / 38%);
	border-radius: var(--r-control);
	background: linear-gradient(118deg, var(--teal-300) 0%, #a8f7e8 52%, var(--teal-300) 100%);
	color: var(--teal-900);
	font-weight: 650;
	box-shadow: 0 10px 28px rgb(15 179 168 / 28%);
	transition: box-shadow var(--dur-base) var(--ease-out),
		border-color var(--dur-fast) var(--ease-out),
		transform var(--dur-fast) var(--ease-out);
}

.contact-email::after {
	content: "";
	position: absolute;
	top: 0;
	left: -140%;
	width: 60%;
	height: 100%;
	background: linear-gradient(100deg, transparent, rgb(255 255 255 / 72%), transparent);
	transform: skewX(-18deg);
	transition: left var(--dur-slow) var(--ease-out);
}

.contact-email:hover,
.contact-email:focus-visible {
	border-color: var(--paper);
	box-shadow: 0 16px 38px rgb(15 179 168 / 42%);
	transform: translateY(-2px);
}

.contact-email:hover::after,
.contact-email:focus-visible::after {
	left: 160%;
}

.contact-email span,
.contact-email svg {
	position: relative;
	z-index: 1;
}

.contact-email svg {
	flex: 0 0 auto;
	transition: transform var(--dur-base) var(--ease-out);
}

.contact-email:hover svg,
.contact-email:focus-visible svg {
	transform: translateX(4px);
}

.contact-visual {
	position: relative;
	display: grid;
	place-items: center;
	min-height: 100%;
	padding: clamp(30px, 4vw, 56px);
}

.contact-glow {
	position: absolute;
	inset: -10% -20% -10% -30%;
	pointer-events: none;
	background: radial-gradient(closest-side at 58% 50%, rgb(15 179 168 / 34%), transparent 72%);
	filter: blur(10px);
}

/* The logo is the graphic. It is sized against the panel rather than given a
   fixed width so it stays the same optical weight at every breakpoint. */
.contact-mark {
	position: relative;
	width: min(100%, 460px);
	height: auto;
	overflow: visible;
	/* White logo, teal light around it: the accent bar is the only coloured
	   stroke, so it has to be the only thing carrying the brand colour. */
	color: #fff;
	filter: drop-shadow(0 0 14px rgb(15 179 168 / 45%));
	animation: ysj-mark-power 1.6s var(--ease-out) 4.6s both;
	}

.contact-mark-accent {
	color: var(--teal-300);
	}

/* Every path carries pathLength="1", so one dash pattern hides any stroke no
   matter how long it really is — no magic number to keep above the longest
   path, and the whole logo draws at one honest speed.

   The delay is read from --draw-delay, set inline on each path. Writing it as
   :nth-child rules instead would lose to this rule's `animation` shorthand,
   which resets animation-delay and has the higher specificity. */
.contact-mark path {
	stroke-dasharray: 1;
	stroke-dashoffset: 1;
	animation: ysj-mark-draw 1.5s linear both;
	animation-delay: var(--draw-delay, 0s);
	}

/* Outlined type: the letterform is traced first and its fill arrives once the
   outline has closed, so the wordmark reads as drawn rather than pasted. */
.contact-mark-word path {
	fill-opacity: 0;
	stroke-width: 1.6;
	animation: ysj-mark-draw 1.2s linear both,
		ysj-mark-fill 0.8s var(--ease-out) both;
	animation-delay: var(--draw-delay, 0s), calc(var(--draw-delay, 0s) + 0.85s);
	}

@keyframes ysj-mark-draw {
	to {
		stroke-dashoffset: 0;
	}
	}

@keyframes ysj-mark-fill {
	to {
		fill-opacity: 1;
	}
	}

/* One soft bloom after the last stroke lands, so the sequence has an ending. */
@keyframes ysj-mark-power {
	0% {
		filter: drop-shadow(0 0 14px rgb(15 179 168 / 45%));
	}

	45% {
		filter: drop-shadow(0 0 30px rgb(124 240 221 / 75%));
	}

	100% {
		filter: drop-shadow(0 0 18px rgb(15 179 168 / 52%));
	}
	}

@media (max-width: 760px) {
	.contact-stage {
		grid-template-columns: 1fr;
		min-height: 0;
	}

	.contact-visual {
		min-height: 190px;
		border-top: 1px solid rgb(124 240 221 / 14%);
	}

	.contact-mark {
		width: min(100%, 280px);
	}
}

@media (min-width: 900px) {
	/* One spine down the page: the rail column holds the numbers, everything
	   else — intro included — starts where the section bodies start. */
	.about-opening,
	.about-section {
		display: grid;
		grid-template-columns: minmax(200px, 260px) 1fr;
		gap: 0 clamp(32px, 4vw, 64px);
		align-items: start;
	}

	.about-lede {
		grid-column: 1 / -1;
	}

	.about-intro {
		grid-column: 2;
	}

	/* The heading tracks the reader down its own section, which is what keeps
	   the long skill list from feeling like an undifferentiated column. */
	.about-section-rail {
		position: sticky;
		top: 104px;
		display: block;
	}

	.about-section-index {
		display: block;
		margin-bottom: 10px;
	}

	.about-section-body {
		margin-top: 0;
	}

	.about-cards {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	/* The fourth block runs several times longer than the other three, so it
	   takes a column of its own instead of being wrapped into the flow. Guarded
	   by :has() so it only applies to a section that has exactly four blocks —
	   any other count falls back to the plain two-up grid. */
	.about-cards:has(> :nth-child(4):last-child) > :nth-child(-n + 3) {
		grid-column: 1;
	}

	.about-cards:has(> :nth-child(4):last-child) > :nth-child(4) {
		grid-column: 2;
		grid-row: 1 / span 3;
		align-self: start;
	}
}

/* ---------- 404 ---------- */

.error-groups {
	display: grid;
	gap: clamp(28px, 4vw, 44px);
}

.error-group h2 {
	margin: 0 0 14px;
	font-size: var(--step-0);
	font-weight: 600;
	color: var(--ink-900);
}

.error-group p {
	margin: 0 0 14px;
	color: var(--ink-500);
}

.error-chips {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin: 0;
	padding: 0;
	list-style: none;
}

/* A chip that is a link needs its own affordance: .chip alone is a static
   label, so the hover and focus states live here rather than in style.css. */
.error-chips a {
	border: 1px solid var(--ink-100);
	text-decoration: none;
	transition: color var(--dur-fast) var(--ease-out),
		background-color var(--dur-fast) var(--ease-out),
		border-color var(--dur-fast) var(--ease-out);
}

.error-chips a:hover,
.error-chips a:focus-visible {
	color: var(--paper);
	background: var(--teal-700);
	border-color: var(--teal-700);
}

.error-chips .chip-count {
	margin-left: 7px;
	font-family: var(--font-mono);
	font-size: 0.75rem;
	opacity: 0.7;
}

@media (prefers-reduced-motion: reduce) {
	.error-chips a,
	.about-card,
	.contact-email,
	.contact-inline-email {
		transition: none;
	}

	/* Nothing may move, but everything must still end up visible: the hero
	   elements are animated with `both`, so they are simply given their final
	   state instead of the animation. */
	.about-aurora,
	.about-hero .eyebrow,
	.about-hero .page-hero h1,
	.about-lede,
	.about-intro,
	.contact-stage::before,
	.contact-mark,
	.contact-mark path,
	.contact-mark-word path {
		animation: none;
	}

	.about-hero .eyebrow,
	.about-hero .page-hero h1,
	.about-lede,
	.about-intro {
		opacity: 1;
		transform: none;
	}

	/* The logo is drawn with `both`, so without its animation it would stay a
	   blank panel. Both the stroke and the wordmark fill need their end state. */
	.contact-mark path {
		stroke-dashoffset: 0;
	}

	.contact-mark-word path {
		fill-opacity: 1;
	}

	.contact-email::after {
		display: none;
	}
}
