@font-face {
	font-family: 'Clash Display Variable';
	src: url('/redesign/fonts/ClashDisplay-Variable.woff2') format('woff2');
	font-weight: 200 700;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: 'Clash Grotesk Variable';
	src: url('/redesign/fonts/ClashGrotesk-Variable.woff2') format('woff2');
	font-weight: 200 700;
	font-style: normal;
	font-display: swap;
}
/* --- Theme tokens ---------------------------------------------------------
   Every surface, rule and glyph below takes its color from these, so a locale
   only has to swap the block rather than hunt through the sheet. The default
   is the white-on-black theme; adding `theme-light` to <html> (see
   eng-vnm/index.html) repaints the same layout black-on-cream. The per-topic
   accents (--accent / --next-accent, set inline on each .topic-content) are
   mid-tone and shared by both themes. */
:root {
	--bg: #000;
	--fg: #fff;
	--fg-muted: rgba(255, 255, 255, 0.38);
	--fg-nav: rgba(255, 255, 255, 0.65);
	--dot: rgba(255, 255, 255, 0.45);
	--rule: rgba(255, 255, 255, 0.2);
	/* Text sitting on an accent-filled surface -- the completed-topic bar and
	   the next-topic button. The accents are mid-tone, so black reads on both
	   themes and this stays put when --fg flips. */
	--on-accent: #000;
	/* Optional per-topic override for accent-coloured *type*, set inline beside
	   --accent. An accent bright enough to fill a button can be too light to
	   read as type on the cream ground, so a topic can darken its ink without
	   touching the fill that matches its artwork. Unset means type just uses
	   --accent, which is how every topic but vnm's yellow one behaves. */

	/* --- Type ---
	   Three roles, so a locale can repoint the whole page at one face rather
	   than chase every rule that names one: --font-body for running text,
	   --font-grotesk for the intro arc and the word ring, --font-display for
	   the arced topic title over the carousel. The English locales get the
	   Clash pair the design was drawn in; see html.lang-vie below for why
	   Vietnamese cannot. */
	--font-body: 'DM Sans', sans-serif;
	--font-display: 'Clash Display Variable', 'DM Sans', sans-serif;
	--font-grotesk: 'Clash Grotesk Variable', 'DM Sans', sans-serif;
}
html.theme-light {
	--bg: #f3ecdc;
	--fg: #000;
	--fg-muted: rgba(0, 0, 0, 0.4);
	--fg-nav: rgba(0, 0, 0, 0.6);
	--dot: rgba(0, 0, 0, 0.45);
	--rule: rgba(0, 0, 0, 0.2);
}
/* Vietnamese repoints all three roles at one face, because none of the three
   the design ships in can set the language. Clash Display and Clash Grotesk
   carry no Vietnamese at all -- no dot below, no hook above, no horn, and none
   of the precomposed vowels built from them -- and DM Sans, which every locale
   falls back to, is served by Google Fonts in latin and latin-ext only, which
   stops at U+024F and so covers o-horn and d-stroke but nothing in U+1EA0.
   Every one of the four topics on this page names a glyph one of them is
   missing. A missing glyph is not a blank: the browser fetches it from a system
   font per character, so the marked vowels in a word arrive in a different
   typeface, at a different weight, off the baseline the rest of the word sits
   on. Be Vietnam Pro is drawn for the language, covers every character on the
   page (checked against the full copy, uppercased, since the display type is),
   and reads close enough to DM Sans that the body copy keeps its voice. Set on
   <html> beside the theme so the whole page moves together, including the
   contact template cloned in from script. */
html.lang-vie {
	--font-body: 'Be Vietnam Pro', sans-serif;
	--font-display: 'Be Vietnam Pro', sans-serif;
	--font-grotesk: 'Be Vietnam Pro', sans-serif;
}
/* Be Vietnam Pro's bold sits lighter than the Clash pair's, so beside eng-vnm
   the intro arc and the word ring read thin at the same 700. The three places
   that were set in Clash step up a notch to carry the weight the design has
   there; body copy stays where it is, which is where DM Sans and Be Vietnam Pro
   already agree. 800 costs about half a percent of width, which the arcs have. */
html.lang-vie .intro-question text,
html.lang-vie .word-arc text,
html.lang-vie .carousel-word text {
	font-weight: 800;
}
* {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
input, textarea, select, button {
	font: inherit;
	color: inherit;
}
body {
	background: var(--bg);
	color: var(--fg);
	font-family: var(--font-body);
	min-height: 100vh;
	/* Pinch/double-tap zoom is disabled site-wide (see the viewport meta and the
	   gesture handlers in app.js); `manipulation` kills the double-tap zoom
	   gesture and its 300ms tap delay. */
	touch-action: manipulation;
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
}
.rotate-overlay {
	display: none;
	position: fixed;
	inset: 0;
	z-index: 9999;
	background: var(--bg);
	color: var(--fg);
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 24px;
	padding: 24px;
	text-align: center;
}
.rotate-icon {
	animation: rotateHint 2.4s ease-in-out infinite;
	transform-origin: center;
}
.rotate-text {
	font-size: 16px;
	font-weight: 700;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	max-width: 280px;
}
@keyframes rotateHint {
	0%, 100% { transform: rotate(-90deg); }
	50% { transform: rotate(0deg); }
}
@media (orientation: landscape) and (max-height: 500px) {
	.rotate-overlay {
		display: flex;
	}
	body {
		overflow: hidden;
	}
}
.container {
	max-width: 425px;
	margin: 0 auto;
	padding: 20px 20px 0;
	height: 50vh;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 48px;
}
@keyframes fadeIn {
	to {
		opacity: 1;
	}
}
@keyframes fadeToHalf {
	from {
		opacity: 1;
	}
	to {
		opacity: 0.5;
	}
}
@keyframes fadeToQuarter {
	from {
		opacity: 1;
	}
	to {
		opacity: 0.25;
	}
}
.fade-in {
	opacity: 0;
	animation: fadeIn 0.8s ease forwards;
}
.brand-logo {
	display: block;
	margin: 0 auto;
	width: 60px;
	height: auto;
	opacity: 0;
	animation: fadeIn 0.8s ease 0.2s forwards, fadeToQuarter 0.9s ease 1s forwards;
}
.heading {
	position: relative;
	width: 100%;
	max-width: 360px;
	height: 200px;
	margin: 0 auto;
	font-weight: 400;
	font-size: 20px;
}
.heading .word {
	position: absolute;
	top: 50%;
	left: 50%;
	font-size: 26px;
	font-weight: 700;
	white-space: nowrap;
	opacity: 1;
	transform: translate(-50%, -50%) translate(var(--x, 0), var(--y, 0)) rotate(var(--r, 0deg));
}
.heading .qmark {
	position: absolute;
	top: 50%;
	left: 50%;
	display: grid;
	font-size: 64px;
	font-weight: 700;
	line-height: 1;
	transform: translate(-50%, -50%);
}
.heading .qmark-layer {
	grid-area: 1 / 1;
	color: var(--c, var(--fg));
	opacity: 0;
	transform: rotate(-360deg);
	animation: qmarkSpin 0.9s cubic-bezier(0.22, 0.61, 0.36, 1) var(--d, 0s) forwards;
}
@keyframes qmarkSpin {
	to {
		opacity: 1;
		transform: rotate(var(--r, 0deg));
	}
}
.prompt {
	font-size: 24px;
	font-weight: 700;
	text-align: center;
}
.prompt span {
	opacity: 0;
	animation: fadeIn 0.6s ease forwards;
}
.prompt span:nth-child(1) { animation-delay: 2.8s; }
.prompt span:nth-child(2) { animation-delay: 3.05s; }
.prompt span:nth-child(3) { animation-delay: 3.3s; }
.prompt span:nth-child(4) { animation-delay: 3.55s; }
.prompt span:nth-child(5) { animation-delay: 3.8s; }
section {
	display: flex;
	flex-direction: column;
	height: 100vh;
}
/* --- Intro sequence ------------------------------------------------------
   While the intro plays the page is one locked screen: the question fades in,
   then the carousel, which then spins itself once through all four topics
   before the scroll arrow appears. `intro` is set on <html> before the first
   paint (inline script in index.html) and removed by carousel.js when the
   sequence ends -- or as soon as the user swipes the carousel and takes over.
   Collapsing the content below is what actually removes the scroll; the
   overflow is a guard for viewports where the section overflows on its own. */
html.intro,
html.intro body {
	overflow: hidden;
}
html.intro .content-container {
	display: none;
}
/* The indicator is the cue to scroll, so it stays inert until it is shown. */
html.intro .scroll-indicator-inner {
	pointer-events: none;
}
/* `intro-hold` covers only the opening stretch, up to the end of the
   carousel's fade-in: until then the carousel takes no swipes or drags.
   carousel.js drops it when the auto-spin starts, so a swipe during the spin
   is free to interrupt it. */
html.intro-hold .carousel-stack {
	pointer-events: none;
}
.intro-question {
	position: relative;
	z-index: 5;
	display: flex;
	align-items: flex-end;
	justify-content: center;
	min-height: 124px;
	margin: 0 auto;
	padding: 0 20px;
	text-align: center;
}
.intro-question svg {
	display: block;
	width: 300px;
	max-width: 100%;
	height: auto;
	margin: 0 auto;
	overflow: visible;
}
.intro-question text {
	font-family: var(--font-grotesk);
	font-size: 18px;
	font-weight: 700;
	letter-spacing: 0.03em;
	fill: var(--fg);
}
/* Intro load sequence: the arc question fades in word by word (staggered via
   the per-word --d delay), then the carousel, then the scroll indicator. */
.intro-question tspan {
	opacity: 0;
	animation: fadeIn 0.7s ease var(--d, 0s) forwards;
}
.carousel-stack {
	position: relative;
	width: 100%;
	max-width: 425px;
	margin: 0 auto;
	opacity: 0;
	animation: fadeIn 0.9s ease 1.7s forwards;
}
.word-arc {
	position: absolute;
	top: 30px;
	left: 0;
	right: 0;
	z-index: 4;
	height: 110px;
	/* Overlay that sits on the carousel image's colorful top edge and moves
	   with the carousel as a single unit. */
	overflow: hidden;
	/* Drag surface for the ring: claim horizontal gestures, leave vertical ones
	   to the page so the section still scrolls from here. */
	touch-action: pan-y;
	cursor: grab;
	-webkit-user-select: none;
	user-select: none;
}
.word-arc:active {
	cursor: grabbing;
}
/* Same left/right fade as the carousel image, so the words dissolve into the
   page background at the edges as they spin past. Implemented as a mask on the words
   themselves (rather than a black overlay) so nothing is painted over the
   carousel image below — the previous overlay bled onto the image's top edge
   and created a visible seam. The nav arrows live outside this container so
   they stay crisp and clickable. */
.word-arc-words {
	position: absolute;
	inset: 0;
	overflow: hidden;
	-webkit-mask-image: linear-gradient(to right, transparent 0%, #000 50%, transparent 100%);
	mask-image: linear-gradient(to right, transparent 0%, #000 50%, transparent 100%);
}
.word-arc-svg {
	position: absolute;
	top: 0;
	left: 50%;
	width: 800px;
	height: 150px;
	overflow: visible;
	transform: translateX(-50%);
}
/* Swipe hint. On a locale that skips the intro's auto-spin (`intro-no-spin`,
   see carousel.js) nothing has shown that the ring moves, so it rocks the words
   a little each way once the fades are over -- and stops early on the first
   touch. Same rotation a drag applies, about the same center, so the words
   swing along the arc rather than sliding straight across it: this svg renders
   its view box at one unit to the pixel (see .word-arc-svg above), so the
   circle's center at 400,1000 in view-box units is 400px 1000px here too, and
   0.65deg of it carries the middle word about eleven pixels. The rock goes on
   the <svg> and not on the ring <g> inside it so that it composes with the
   ring's own rotation instead of overwriting it, and so the edge fade -- masked
   onto the parent -- holds still while the words move past it. The base
   translate is repeated in every keyframe because an animated transform
   replaces the property rather than adding to it; it is a translation, so the
   rotation still turns about the origin set below. */
@keyframes ringHint {
	0%, 100% { transform: translateX(-50%) rotate(0deg); }
	25%      { transform: translateX(-50%) rotate(-0.65deg); }
	75%      { transform: translateX(-50%) rotate(0.65deg); }
}
.word-arc-svg.is-hinting {
	transform-box: view-box;
	transform-origin: 400px 1000px;
	animation: ringHint 1.5s ease-in-out 2;
}
.word-arc-ring {
	transform-box: view-box;
	transform-origin: 400px 1000px;
	transform: rotate(var(--ring-rot, 0deg));
	transition: transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* While a finger is down the ring follows it 1:1; the eased transition is only
   for the snap on release. */
.word-arc-ring.is-dragging {
	transition: none;
}
.word-arc text {
	font-family: var(--font-grotesk);
	font-size: 20px;
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	fill: var(--fg-muted);
	/* Scaled, not re-sized: font-size is what layoutArc() measures to space the
	   words and to work out the rotation that centers each one, so growing the
	   selected word that way would drag every other word along with it. A
	   transform leaves the arc layout untouched. The origin is the word's own
	   baseline point on the arc, set per word by layoutArc() -- same user-unit
	   view-box origin the ring's rotation uses, so the word grows out of the arc
	   line it sits on. */
	transform-box: view-box;
	transform: scale(1);
	transition: fill 0.4s ease, transform 0.4s ease;
}
.word-arc text.is-active {
	fill: var(--fg);
	/* Ceiling, not a preference: layoutArc() leaves a constant 50-unit gap either
	   side of a word, and the growth eats half the scale-up on each side, so the
	   longest topic on any locale (ACCOMPLISHMENTS, 145 units, in eng-vnm) starts
	   touching its neighbours past ~1.6. Going bigger means a wider ARC_GAP, which
	   in turn needs #wordArcPath lengthened -- twelve words at the current gap
	   already run to ~1943 of its ~2000 units, and text past the end of a path is
	   simply not drawn. */
	transform: scale(1.6);
}
/* Locales whose topics are long size the ring down so the active word -- which
   is scaled up, and so widest of all -- still lands inside the strip's edge
   fade rather than dissolving at both ends. eng-vnm runs to ACCOMPLISHMENTS,
   fifteen characters; vie-vnm's topics are whole phrases and its longest,
   NHỮNG THÀNH TỰU, is a little wider still at the same size. layoutArc()
   measures the rendered text, so the spacing and the centering rotations follow
   the smaller size on their own -- but the words it lays out have to fit along
   #wordArcPath, and vie-vnm's twelve reach ~2270 units where eng-vnm's stop at
   ~1943 -- past the end of eng-vnm's ~2200 path -- so that page carries a
   longer one. */
.word-arc-long text {
	font-size: 14px;
}
.word-arc-long text.is-active {
	transform: scale(1.35);
}
.word-arc-nav {
	/* Temporarily hidden — restore `display: flex` to bring the arrows back.
	   Markup and carousel.js handlers are left in place so this is a one-word
	   revert; swipe/drag and the arc words still change slides meanwhile. */
	display: none;
	position: absolute;
	top: 32px;
	z-index: 5;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 44px;
	padding: 0;
	border: 0;
	background: none;
	color: var(--fg-nav);
	cursor: pointer;
}
.word-arc-nav:hover {
	color: var(--fg);
}
.word-arc-nav svg {
	width: 20px;
	height: 20px;
	display: block;
}
.word-arc-nav.prev {
	left: 6px;
}
.word-arc-nav.prev svg {
	transform: rotate(-12deg);
}
.word-arc-nav.next {
	right: 6px;
}
.word-arc-nav.next svg {
	transform: rotate(12deg);
}
.carousel {
	position: relative;
	max-width: 425px;
	margin: 0 auto;
	overflow: hidden;
	touch-action: pan-y;
	/* Taller than the picture on purpose. The mask fits to width and centres, so
	   a band of bare page is left above and below -- and both bands are in use:
	   the word ring sits in the top one (.word-arc, top: 30px, meeting the
	   picture's edge at 117px) and .scroll-indicator's negative margin pulls the
	   arrow back up through the bottom one.
	   Pinned here rather than inherited from whatever the first slide carries,
	   because a slide can lead with a video -- cut to the mask's own 402:354 --
	   which would otherwise collapse the box by the height of both bands and
	   drop the ring onto the picture. 402:605 is the stills' ratio, which is the
	   height the box had before it was written down. */
	aspect-ratio: 402 / 605;
}
.carousel-slide {
	position: absolute;
	inset: 0;
	opacity: 0;
	transition: opacity 0.5s ease;
	pointer-events: none;
	z-index: 0;
	/* Centre the mask box in the slide. For a still this changes nothing -- the
	   image is taller than the mask, so its box already fills the slide -- but a
	   video slide's box is the mask's own 402:354 and would otherwise hang from
	   the top while the stills' masks render centred, jumping the picture on
	   every change. See .carousel-mask video. */
	display: flex;
	flex-direction: column;
	justify-content: center;
}
.carousel-slide.active {
	opacity: 1;
	pointer-events: auto;
	z-index: 1;
}
.carousel-slide.active .carousel-mask :is(img, video) {
	animation: carouselIn 0.6s ease-out;
}
@keyframes carouselIn {
	from {
		transform: scale(1.05);
	}
}
.carousel-mask {
	position: relative;
	/* Its own height, never squeezed to fit the slide it is centred in. */
	flex: none;
	-webkit-mask-image: url('/redesign/assets/carousel-mask.svg');
	mask-image: url('/redesign/assets/carousel-mask.svg');
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	overflow: hidden;
}
.carousel-mask::after {
	content: '';
	position: absolute;
	inset: 0;
	background: linear-gradient(to right, var(--bg) 0%, transparent 50%, var(--bg) 100%);
	pointer-events: none;
}
.carousel-mask img {
	width: 100%;
	height: auto;
	display: block;
	object-fit: cover;
	user-select: none;
	-webkit-user-drag: none;
	pointer-events: none;
}
/* A topic can lead with a video instead of a still (on the Vietnam page every
   topic but Accomplishments does). These clips are cut to the mask's
   shape already, so the box is pinned to the mask's exact ratio rather than the
   file's -- a hair off it (1006x886 against 402:354) would leave a seam of
   background along the curve. object-fit absorbs that difference. */
.carousel-mask video {
	width: 100%;
	height: auto;
	aspect-ratio: 402 / 354;
	display: block;
	object-fit: cover;
	user-select: none;
	pointer-events: none;
	/* Above the edge fade (.carousel-mask::after) rather than under it. The fade
	   is tuned for the flat, high-contrast stills, which carry it; over
	   photographic footage it reads far heavier and washes the frame out well
	   past the halfway point where it is meant to have died away. The word ring
	   keeps its own fade either way -- that one is a mask on the words
	   themselves (.word-arc-words), not this overlay. */
	position: relative;
	z-index: 1;
}
.carousel-overlay {
	position: absolute;
	/* Match the mask's *rendered* area, not the slide box. carousel-mask.svg has
	   no preserveAspectRatio, so it defaults to xMidYMid meet: it fits to width
	   and centres vertically, letterboxing inside the taller slide box. Giving
	   the overlay the mask's own 402:354 ratio and centring it reproduces that
	   area exactly, so flex-end lands on the visible bottom of the image — and
	   it stays correct whatever aspect the slide images are.
	   Padding-% resolves against width, so the mask's 9.33%-of-height bottom
	   curve (y=321 of 354) becomes 9.33% x 354/402 = 8.21% of width, plus a
	   fixed lift to hold the word clear of the curve. */
	left: 0;
	right: 0;
	top: 50%;
	transform: translateY(-50%);
	aspect-ratio: 402 / 354;
	z-index: 2;
	pointer-events: none;
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	align-items: center;
	gap: 12px;
	padding-bottom: calc(8.21% + 20px);
}
.carousel-word {
	/* Temporarily hidden — restore `display: block` to bring the arced topic
	   title back over the image. Markup and the carousel.js word/fade updates are
	   left in place so this is a one-word revert; the arc ring above the carousel
	   names the topic meanwhile.
	   Arced via <textPath>; the viewBox is 300 wide like the intro question's,
	   so font-size is in user units and scales with the carousel width. 26 units
	   renders at ~36px at the 425px max-width. */
	display: none;
	width: 100%;
	height: auto;
	overflow: visible;
	animation: wordFade 0.5s ease;
}
.carousel-word text {
	font-family: var(--font-display);
	font-size: 26px;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	fill: var(--fg);
}
@keyframes wordFade {
	from {
		opacity: 0;
	}
}
.carousel-dots {
	/* Temporarily hidden — restore `display: flex` to bring the dots back.
	   Markup and the carousel.js active-state toggling are left in place. */
	display: none;
	justify-content: center;
	gap: 8px;
}
.carousel-dots .dot {
	width: 8px;
	height: 8px;
	border-radius: 4px;
	background: var(--dot);
	transition: width 0.3s ease, background 0.3s ease;
}
.carousel-dots .dot.active {
	width: 22px;
	background: var(--fg);
}
.scroll-indicator {
	position: relative;
	flex: 1;
	/* Pull up into the carousel mask's curved bottom inset so the indicator
	   sits close to the visible image rather than its box edge -- the mask
	   letterboxes, so the image ends well above the box bottom. z-index keeps
	   it above the active slide (z-index: 1) so the image's dark lower edge
	   can't paint over the arrow where they now overlap. */
	margin-top: -120px;
	z-index: 2;
	display: flex;
	align-items: flex-start;
	justify-content: center;
	/* The container now overlaps the carousel's bottom; let swipes pass through
	   it to the carousel and only make the SVG circle itself interactive. */
	pointer-events: none;
	opacity: 1;
	/* Triggered fade (see carousel.js) -- runs on its own timing rather than
	   being scrubbed by scroll position. */
	transition: opacity 0.45s ease;
}
/* Locales whose intro sits taller pull the indicator up further still: the
   vnm page's lower arc is widened for "IDENTITY IN MY...", which leaves the
   carousel image ending higher in its box than on eng-afr. Scoped to a
   modifier rather than raised on the shared rule so eng-afr keeps its own
   spacing. */
.scroll-indicator-high {
	margin-top: -150px;
}
.scroll-indicator.is-hidden {
	opacity: 0;
}
/* Faded out it's effectively gone, so stop it catching taps. */
.scroll-indicator.is-hidden .scroll-indicator-inner {
	pointer-events: none;
}
.scroll-indicator-inner {
	position: relative;
	width: 120px;
	height: 120px;
	cursor: pointer;
	pointer-events: auto;
}
/* Last step of the intro: the arrow holds at opacity 0 until carousel.js
   drops `intro` from <html> (the carousel has finished its auto-spin, or the
   user interrupted it), then fades in and settles into a soft pulse to draw
   the eye toward the scroll cue. Keyed off the class rather than a fixed delay
   so it can't appear while the carousel is still spinning. */
.scroll-arrow {
	position: absolute;
	inset: 0;
	display: block;
	width: 100%;
	height: 100%;
	opacity: 0;
}
/* The arrow's <path> carries a fill="white" presentation attribute, which any
   CSS rule outranks -- so the theme token wins without touching the markup. */
.scroll-arrow path {
	fill: var(--fg);
}
html:not(.intro) .scroll-arrow {
	animation: fadeIn 0.8s ease forwards,
		arrowPulse 1.8s ease-in-out 0.8s infinite;
}
@keyframes arrowPulse {
	0%, 100% { transform: scale(1); }
	50%      { transform: scale(1.1); }
}
/* Respect reduced-motion: show the intro fully, skip the staged fade-in. */
@media (prefers-reduced-motion: reduce) {
	.intro-question tspan,
	.carousel-stack,
	.scroll-arrow,
	html:not(.intro) .scroll-arrow {
		opacity: 1;
		animation: none;
	}
	.scroll-indicator {
		transition: none;
	}
	/* carousel.js does not start the hint under reduced motion either. */
	.word-arc-svg.is-hinting {
		animation: none;
	}
}
.reveal {
	opacity: 0;
	transform: translateY(20px);
	transition: opacity 1s ease 0.2s, transform 1s ease 0.2s;
}
.reveal.visible {
	opacity: 1;
	transform: translateY(0);
}
.content-container {
	max-width: 425px;
	margin: 0 auto;
	padding: 24px 20px 40px;
}
.topic-content {
	display: none;
}
.topic-content.active {
	display: block;
}
.topic-content h2 {
	font-weight: 700;
	font-size: 24px;
	text-transform: uppercase;
	margin-top: 24px;
	margin-bottom: 28px;
	line-height: 1.25;
}
.topic-content h2 .word {
	transition: color 0.4s ease;
}
.topic-content h2 .word.colored {
	color: var(--accent-text, var(--accent, #e85d3c));
}
.topic-content p {
	font-weight: 400;
	font-size: 16px;
	line-height: 1.6;
	margin-bottom: 20px;
}
.topic-content h3 {
	font-weight: 700;
	font-size: 20px;
	text-transform: uppercase;
	margin-top: 48px;
	margin-bottom: 20px;
}
.scatter-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 16px;
}
.scatter-row.reveal {
	opacity: 1;
	transform: none;
}
.scatter-word {
	display: inline-block;
	opacity: 0;
	transform: translateY(20px) rotate(var(--r, 0deg));
	transition: opacity 0.6s ease, transform 0.6s ease;
}
.scatter-row.visible .scatter-word {
	opacity: 1;
	transform: translateY(var(--y, 0)) rotate(var(--r, 0deg));
}
.scatter-row.visible .scatter-word:nth-child(1) { transition-delay: 0s; }
.scatter-row.visible .scatter-word:nth-child(2) { transition-delay: 0.2s; }
.scatter-row.visible .scatter-word:nth-child(3) { transition-delay: 0.4s; }
.scatter-row.visible .scatter-word:nth-child(4) { transition-delay: 0.6s; }
.scatter-row.visible .scatter-word:nth-child(5) { transition-delay: 0.8s; }
.scatter-qmark {
	color: var(--accent-text, var(--accent, var(--fg)));
}
.topic-content .full-bleed {
	display: block;
	width: calc(100% + 40px);
	max-width: none;
	height: auto;
	margin: 40px -20px;
}
.topic-content blockquote {
	margin: 32px 0 36px;
	padding-left: 16px;
	border-left: 2px solid color-mix(in srgb, var(--accent-text, var(--accent, var(--fg))) 35%, var(--fg));
	color: color-mix(in srgb, var(--accent-text, var(--accent, var(--fg))) 35%, var(--fg));
}
.topic-content blockquote p {
	font-style: italic;
	margin-bottom: 12px;
}
.topic-content blockquote cite {
	font-style: normal;
	font-weight: 700;
	font-size: 14px;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--accent-text, var(--accent, var(--fg)));
}
.topic-complete {
	margin: 48px -20px 0;
	width: calc(100% + 40px);
	border-bottom: 1px solid var(--rule);
	position: relative;
}
.topic-complete-top-line {
	height: 1px;
	background: var(--rule);
	opacity: 0;
}
.topic-complete-inner {
	height: 0;
	overflow: hidden;
	background: var(--accent, var(--fg));
	display: flex;
	align-items: center;
	justify-content: center;
}
.topic-complete-content {
	display: flex;
	align-items: center;
	gap: 10px;
	opacity: 0;
	color: var(--on-accent);
	font-weight: 700;
	font-size: 13px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	white-space: nowrap;
}
.topic-complete-content strong {
	font-weight: 900;
}
.topic-complete-badge {
	flex-shrink: 0;
}
.topic-complete.run .topic-complete-top-line {
	animation: topicCompleteLine 3.6s ease forwards;
}
.topic-complete.run .topic-complete-inner {
	animation: topicCompleteExpand 3.6s ease forwards;
}
.topic-complete.run .topic-complete-content {
	animation: topicCompleteContent 3.6s ease forwards;
}
@keyframes topicCompleteLine {
	0%, 100% { opacity: 0; }
	15%, 85% { opacity: 1; }
}
@keyframes topicCompleteExpand {
	0%, 100% { height: 0; }
	18%, 82% { height: 60px; }
}
@keyframes topicCompleteContent {
	0%, 28%, 72%, 100% { opacity: 0; }
	40%, 60% { opacity: 1; }
}
.next-topic-divider {
	border: 0;
	border-top: 1px solid var(--rule);
	margin: 48px -20px 24px;
	width: calc(100% + 40px);
}
.next-topic-button {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 12px;
	width: 100%;
	padding: 18px 24px;
	background: var(--next-accent, var(--fg));
	color: var(--on-accent);
	border: none;
	border-radius: 999px;
	font-size: 16px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	cursor: pointer;
	margin-bottom: 16px;
	transition: filter 0.2s ease, transform 0.2s ease;
}
@media (hover: hover) {
	.next-topic-button:hover {
		filter: brightness(1.08);
	}
}
.next-topic-button:active {
	transform: scale(0.98);
}
.next-topic-button strong {
	font-weight: 900;
}
.contact-list {
	list-style: none;
	padding: 0;
	margin: 32px 0 24px;
	display: flex;
	flex-direction: column;
	gap: 12px;
}
/* The pill styling hangs off .contact-item rather than off the li, so a row can
   be either a bare li (the Email toggle) or an <a> wrapped in one (Messenger)
   without the padding and border landing on the wrong box. The colour and
   underline resets and the tap highlight only matter in the anchor case, where
   iOS would otherwise flash a grey box over the pill that the Email row, not
   being a link, never shows. */
.contact-item {
	font-size: 16px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	padding: 14px 20px;
	border: 1px solid var(--fg);
	border-radius: 999px;
	cursor: pointer;
	display: flex;
	align-items: center;
	gap: 12px;
	color: inherit;
	text-decoration: none;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.2s ease, color 0.2s ease;
}
.contact-icon {
	flex-shrink: 0;
}
.contact-list .contact-arrow {
	margin-left: auto;
}
.contact-item:hover {
	background: var(--fg);
	color: var(--bg);
}
.contact-arrow {
	flex-shrink: 0;
	transition: transform 0.3s ease;
}
.contact-tags {
	list-style: none;
	padding: 0;
	margin: 16px 0 24px;
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}
.contact-tags li {
	font-size: 12px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	padding: 6px 10px;
	border: 1px solid var(--fg);
	border-radius: 999px;
	display: inline-flex;
	align-items: center;
	gap: 6px;
}
.contact-tags li svg {
	width: 1em;
	height: 1em;
	flex-shrink: 0;
}
