/* ============================================================
   ANJALI CREATIVE — Design System
   Brand Perspective Shifters · ©2026
   ------------------------------------------------------------
   01 Tokens        02 Base          03 Utilities
   04 Motion        05 Nav           06 Hero
   07 Trifecta      08 Cases         09 Guarantee
   10 Clients       11 Services      12 Results
   13 Footer        14 Responsive    …
   23 Page shell    24 Page lede, chapter index & crosslink
   25 Motion v2
   ============================================================ */

/* ---------- 01 · Tokens ---------- */
:root {
	--gold: #D1B180;
	--gold-soft: #E6D3AC;
	--gold-deep: #A98B57;
	--paper: #FAF7F1;
	--cream: #F1EADD;
	--cocoa: #6B4F3A;
	--ink: #2A211B;
	--espresso: #171210;
	--light: #F3EDE3;

	--ease: cubic-bezier(0.22, 1, 0.36, 1);

	--font-display: "Poppins", "Segoe UI", system-ui, -apple-system, sans-serif;
	--font-mono: "Space Mono", "Cascadia Code", Consolas, monospace;

	/* Grows with the viewport instead of parking at a fixed width — the wide
	   screens this site is viewed on were leaving 300–660px unused on each
	   side. Paragraphs stay readable because they carry their own ch caps. */
	--container: min(1800px, 100% - 8vw);
	--gutter: clamp(1.25rem, 4vw, 2.5rem);
	--section: clamp(5.5rem, 11vw, 9.5rem);
	/* Raised 30% at the client's request, with every nav token moved by the same
	   factor so the bar grows as one object rather than as a taller box holding
	   the old type. Nothing else needs touching: scroll-margin, the pinned
	   chapter index, the hero stage and the sticky brand bar all measure
	   themselves against this. */
	--nav-h: 94px;
	--radius: 22px;

	/* ----- The floating WhatsApp disc, as a measurement -----
	   The button is fixed and the footer is the one thing it lands on: the bar at
	   the very bottom of every page ends exactly where the disc sits. It was
	   covering the tagline on wide screens and standing on the copyright line on
	   phones. Declared here rather than inside section 30 because the footer has
	   to reserve the room, and a reserve computed from a literal 60px would drift
	   the moment the disc is resized. Section 30 reads these too — the disc's own
	   offsets are --fab-inset now. */
	--fab-size: 60px;
	--fab-inset: clamp(1.1rem, 2.6vw, 1.9rem);
	--fab-clear: calc(var(--fab-size) + var(--fab-inset) * 2);

	--grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}

/* ---------- 02 · Base ---------- */
html {
	scroll-behavior: smooth;
	/* Inherited, so every scroller on the page picks it up — the branding rails and the
	   social rails included. A transparent track is the only choice that works over both
	   grounds: this page alternates paper and espresso, and a scrollbar cannot re-theme
	   itself per section. --gold-deep clears 3:1 against both. */
	scrollbar-width: thin;
	scrollbar-color: var(--gold-deep) transparent;
}

/* Safari has no scrollbar-color, so WebKit needs its own set. Scoped to html: the
   internal rails already style their own bars, and a global rule would fight them. */
html::-webkit-scrollbar {
	width: 12px;
}

html::-webkit-scrollbar-track {
	background: rgba(42, 33, 27, .05);
}

/* The transparent border plus content-box clip is what makes the thumb read as a
   floating pill rather than a bar wedged into the gutter. */
html::-webkit-scrollbar-thumb {
	background: linear-gradient(180deg, var(--gold), var(--gold-deep));
	background-clip: content-box;
	border: 3px solid transparent;
	border-radius: 999px;
}

html::-webkit-scrollbar-thumb:hover {
	background: linear-gradient(180deg, var(--gold-soft), var(--gold));
	background-clip: content-box;
}

/* QA mode (?qa=1) has no .js class: jump instantly so tooling lands on anchors,
   and pin the hero to a fixed height so tall full-page captures stay proportional. */
html:not(.js) {
	scroll-behavior: auto;
}

html:not(.js) .aj-hero {
	min-height: 780px;
}

body {
	margin: 0;
	font-family: var(--font-display);
	font-size: 1.0625rem;
	line-height: 1.65;
	color: var(--ink);
	background: var(--paper);
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
	overflow-x: clip;
}

img {
	max-width: 100%;
	height: auto;
	display: block;
}

a {
	color: inherit;
}

h1, h2, h3, h4 {
	font-family: var(--font-display);
}

::selection {
	background: var(--gold);
	color: var(--espresso);
}

:focus-visible {
	outline: 2px solid var(--gold-deep);
	outline-offset: 3px;
	border-radius: 4px;
}

[id] {
	scroll-margin-top: calc(var(--nav-h) + 16px);
}

.aj-skip {
	position: fixed;
	top: -100px;
	left: 1rem;
	z-index: 100;
	background: var(--espresso);
	color: var(--light);
	padding: .8em 1.4em;
	border-radius: 999px;
	text-decoration: none;
	font-size: .9rem;
	transition: top .3s var(--ease);
}

.aj-skip:focus-visible {
	top: 1rem;
}

/* WordPress' conventional visually-hidden utility. */
.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---------- 03 · Utilities ---------- */
.aj-container {
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

.aj-section {
	position: relative;
	padding-block: var(--section);
}

.aj-section--dark {
	background:
		radial-gradient(52% 40% at 82% 0%, rgba(209, 177, 128, .10), transparent 62%),
		radial-gradient(42% 34% at 6% 100%, rgba(209, 177, 128, .06), transparent 60%),
		var(--espresso);
	color: var(--light);
}

.aj-section--dark::after {
	content: "";
	position: absolute;
	inset: 0;
	background-image: var(--grain);
	opacity: .05;
	mix-blend-mode: soft-light;
	pointer-events: none;
}

.aj-section--dark > * {
	position: relative;
	z-index: 1;
}

.aj-eyebrow {
	display: inline-flex;
	align-items: center;
	gap: .7rem;
	font-family: var(--font-mono);
	font-size: .78rem;
	letter-spacing: .22em;
	text-transform: uppercase;
	color: var(--cocoa);
	margin: 0 0 1.4rem;
}

.aj-eyebrow::before {
	content: "";
	width: 2.2rem;
	height: 1px;
	background: var(--gold-deep);
	flex: none;
}

.aj-section--dark .aj-eyebrow,
.aj-footer .aj-eyebrow {
	color: var(--gold);
}

.aj-section--dark .aj-eyebrow::before,
.aj-footer .aj-eyebrow::before {
	background: var(--gold);
}

.aj-display {
	font-size: clamp(2.1rem, 4.6vw, 3.7rem);
	line-height: 1.06;
	letter-spacing: -.02em;
	font-weight: 800;
	margin: 0 0 1.4rem;
}

.aj-lead {
	font-size: clamp(1.08rem, 1.5vw, 1.3rem);
	line-height: 1.6;
	color: color-mix(in srgb, var(--ink) 76%, transparent);
	max-width: 56ch;
	margin: 0;
}

.aj-section--dark .aj-lead {
	color: rgba(243, 237, 227, .72);
}

.aj-section-head {
	margin-bottom: clamp(3rem, 6vw, 5rem);
}

/* ----- Chapter opener -----
   Used only where a section contains its own sub-groups. The long rule is what
   tells the eye this title outranks the group titles beneath it.

   It used to carry a ghosted numeral too — 02 on Results, 04 on Branding, 05 on
   Influencer. That set could never be completed: one discipline in the Services
   list owns two sections (Digital Marketing → Results + Web), another owns two
   more (Influencer & Performance → Influencer + Media Buying), and a fifth owns
   none at all. Whatever numbers went up there, 01 and 03 would still be missing
   and the reader would still be looking for them. The rule now is that a number
   only appears where its set is complete and visible in one place — the Trifecta
   01–03, the five disciplines, the ad steps, the branding chapters, the three
   briefs. Scope that used to hide in a decorative numeral is stated plainly in
   .aj-sechead__meta instead. */
.aj-section-head--chapter {
	position: relative;
	margin-bottom: clamp(4rem, 8vw, 7rem);
}

.aj-section-head--chapter .aj-eyebrow::before {
	width: clamp(3rem, 8vw, 7rem);
}

/* ----- Section head with a meta column -----
   Copy on the left, scope on the right. The container grows to 1800px and a
   62ch column leaves most of that empty, which is the hole the ghosted numeral
   was decorating rather than filling. */
.aj-sechead {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: end;
	gap: clamp(1.2rem, 4vw, 3.5rem);
}

.aj-sechead__text {
	min-width: 0;
}

.aj-sechead__meta {
	display: grid;
	gap: .5rem;
	list-style: none;
	margin: 0;
	padding: .2rem 0 .2rem clamp(1rem, 2vw, 1.6rem);
	border-left: 1px solid rgba(169, 139, 87, .45);
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--cocoa);
	white-space: nowrap;
}

.aj-section--dark .aj-sechead__meta {
	border-left-color: rgba(209, 177, 128, .4);
	color: var(--gold);
}

@media (max-width: 1180px) {
	.aj-sechead {
		grid-template-columns: minmax(0, 1fr);
	}

	.aj-sechead__meta {
		justify-self: start;
	}
}

/* ----- Light band: the section head as a full-bleed paper panel -----
   The twin of .aj-imgroup with the ground inverted — cream band opens a chapter,
   paper band opens a section. Used where a head has to introduce a run of dark
   panels: Influencer opens five of them and sits at the end of the longest
   unbroken stretch of dark ground on the page, and a dark head there gives the
   reader no way in.

   Rule: dark grounds carry CONTENT — panels, proof, work. Heads stand on paper.
   Results is the exception and stays dark, because its head hands straight over
   to the gold overture that lights its own ground.

   The band sits inside .aj-section--dark, which forces light type, so every
   colour has to be stated back — the same tax .aj-imgroup pays. */
.aj-lightband {
	position: relative;
	z-index: 1;
	padding-block: clamp(3.5rem, 7vw, 6rem);
	background: var(--paper);
	color: var(--ink);
}

/* Where a chapter band follows, the two hand over directly. A margin here showed
   a strip of the section's espresso carrying nothing — 65px of ground doing no
   work between two bands that are both openings.
   The hairline goes with it. It used to stand down because the chapter band drew
   its own rule immediately below; the bands carry no rules at all now, and a
   single gold line left at this one join would be the only one on the page. */
.aj-lightband:has(+ .aj-imgroup) {
	margin-bottom: 0;
}

.aj-lightband:has(+ .aj-imgroup)::after {
	display: none;
}

.aj-lightband:not(:has(+ .aj-imgroup)) {
	margin-bottom: clamp(2.5rem, 5vw, 4rem);
}

/* One hairline, at the bottom only: it marks the hand-over to whatever ground
   comes next, and the top edge already has the section boundary doing that job. */
.aj-lightband::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 1px;
	background: var(--gold-deep);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 1.2s var(--ease);
	pointer-events: none;
}

html.js .aj-lightband.is-in::after,
html:not(.js) .aj-lightband::after {
	transform: scaleX(1);
}

.aj-lightband .aj-eyebrow {
	color: var(--cocoa);
}

.aj-lightband .aj-eyebrow::before {
	background: var(--gold-deep);
}

.aj-lightband .aj-display {
	color: var(--ink);
}

.aj-lightband .aj-lead {
	color: color-mix(in srgb, var(--ink) 76%, transparent);
}

.aj-lightband .aj-sechead__meta {
	border-left-color: rgba(169, 139, 87, .45);
	color: var(--cocoa);
}

/* The band supplies the clearance now, so the head inside it gives up its own. */
.aj-lightband .aj-section-head {
	margin-bottom: 0;
}

.aj-mono-label {
	display: flex;
	align-items: center;
	gap: 1rem;
	font-family: var(--font-mono);
	font-size: .8rem;
	letter-spacing: .2em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .55);
	margin: 0 0 1.6rem;
}

.aj-mono-label::after {
	content: "";
	flex: 1;
	height: 1px;
	background: linear-gradient(90deg, rgba(209, 177, 128, .4), transparent);
}

.aj-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: .5rem;
	/* This theme is content-box throughout, which is invisible on a pill that
	   shrinks to fit — and a trap on the two that do not. The stacked footer
	   pair and the drawer's CTA are the only buttons on the site with a declared
	   width, and both were rendering 36px wider than the column they were given:
	   1.85em of padding on each side, added on top of a 100% that had already
	   used up the measure. Every other .aj-btn is auto-width, so this changes
	   nothing anywhere else. */
	box-sizing: border-box;
	font-family: var(--font-display);
	font-weight: 600;
	font-size: .98rem;
	line-height: 1;
	padding: 1.02em 1.85em;
	border-radius: 999px;
	text-decoration: none;
	border: 0;
	cursor: pointer;
	transition:
		transform .5s var(--ease),
		box-shadow .5s var(--ease),
		background .3s,
		color .3s,
		border-color .3s;
}

.aj-btn--gold {
	background: linear-gradient(135deg, var(--gold-soft), var(--gold) 55%, var(--gold-deep));
	color: var(--espresso);
	box-shadow: 0 10px 28px -12px rgba(169, 139, 87, .55);
}

.aj-btn--gold:hover {
	transform: translateY(-2px);
	box-shadow: 0 16px 34px -12px rgba(169, 139, 87, .7);
}

.aj-btn--ghost {
	border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
	background: transparent;
	color: inherit;
}

.aj-btn--ghost:hover {
	border-color: currentColor;
	transform: translateY(-2px);
}

/* Nav only — the padding is in em, so raising the size scales the pill with the
   type instead of leaving a small button with big words in it. */
.aj-btn--sm {
	padding: .78em 1.35em;
	font-size: 1.17rem;
}

.aj-chip {
	display: inline-flex;
	align-items: center;
	font-family: var(--font-mono);
	font-size: .68rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	padding: .5em .9em;
	border-radius: 999px;
	border: 1px solid rgba(169, 139, 87, .5);
	color: var(--gold-deep);
	white-space: nowrap;
}

.aj-section--dark .aj-chip {
	border-color: rgba(209, 177, 128, .4);
	color: var(--gold);
}

mark {
	background: linear-gradient(120deg, var(--gold-soft), var(--gold));
	color: var(--espresso);
	padding: .08em .22em;
	border-radius: .3em;
	-webkit-box-decoration-break: clone;
	box-decoration-break: clone;
}

/* ---------- 04 · Motion ---------- */
html.js :where([data-reveal]) {
	opacity: 0;
	transform: translateY(30px);
	transition:
		opacity .9s var(--ease),
		transform .9s var(--ease);
	transition-delay: var(--d, 0s);
}

html.js :where([data-reveal].is-in) {
	opacity: 1;
	transform: none;
	transition-delay: 0s;
}

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}

	html.js [data-reveal] {
		opacity: 1;
		transform: none;
		transition: none;
	}

	.aj-marquee__track {
		animation: none !important;
		flex-wrap: wrap;
		width: auto !important;
	}

	/* Wrapped and still, the duplicate sets are just the same logos listed twice. They
	   only exist to hide the loop seam, and there is no loop here. */
	.aj-marquee__half[aria-hidden="true"] {
		display: none;
	}

	.aj-hero__scroll::after {
		animation: none !important;
	}
}

/* ---------- 05 · Nav ---------- */
.aj-nav {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 60;
	transition: background .4s, box-shadow .4s;
}

.admin-bar .aj-nav {
	top: var(--wp-admin--admin-bar--height, 32px);
}

/* The second selector is not a scroll state — it is a page that has no pale
   opening to be transparent over. See the body_class filter in functions.php:
   /our-family/ starts on the dark cutting mat, so the bar wears its scrolled
   ground from the first frame rather than putting ink on green. */
.aj-nav.is-scrolled,
body.aj-opens-dark .aj-nav {
	background: rgba(250, 247, 241, .82);
	-webkit-backdrop-filter: blur(16px) saturate(1.4);
	backdrop-filter: blur(16px) saturate(1.4);
	box-shadow: 0 1px 0 rgba(42, 33, 27, .08);
}

.aj-nav__inner {
	max-width: var(--container);
	height: var(--nav-h);
	margin-inline: auto;
	padding-inline: var(--gutter);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1.5rem;
}

/* No gap, no type. The brand was a flex row holding a mark and a typed wordmark;
   it holds one image now, and the font declarations that styled the text came
   off with it. Kept as a flex box rather than collapsed to a bare link so the
   logo stays vertically centred in the 94px bar without a line-height trick. */
.aj-nav__brand {
	display: flex;
	align-items: center;
	text-decoration: none;
	position: relative;
	z-index: 70;
}

/* ----- The lockup -----
   Sized by height, with an explicit width beside it. Both matter, for different
   reasons: height is the only dimension the artwork's own proportion needs, and
   the width reserves the box before the file lands — the nav is the first thing
   painted on every page and a brand that shifts sideways on load is the worst
   place on the site to spend layout.

   163 × 44 is 3.7117:1, the artwork's trimmed aspect ratio. 44px is not an
   arbitrary bump from the 39px the old mark stood at: the mark inside this
   lockup carries the same 1.355:1 proportion as the standalone file, so at 44px
   it renders 60×44 against the old 53×39 — a shade larger, which it has earned
   now that it is carrying the whole identity rather than half of it. The block
   still comes in at 163px wide against the ~222px the old mark-plus-text row
   occupied, because the wordmark is two stacked lines instead of one long one.
   The nav gets width back, not spends it. */
.aj-nav__logo {
	height: 44px;
	width: 163px;
	flex: none;
}

.aj-nav__links {
	display: flex;
	align-items: center;
	gap: 2.6rem;
}

.aj-nav__links a:not(.aj-btn) {
	text-decoration: none;
	font-size: 1.24rem;
	font-weight: 500;
	color: color-mix(in srgb, var(--ink) 82%, transparent);
	position: relative;
	transition: color .3s;
}

.aj-nav__links a:not(.aj-btn)::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: -8px;
	height: 2px;
	border-radius: 2px;
	background: var(--gold-deep);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform .45s var(--ease);
}

.aj-nav__links a:not(.aj-btn):hover {
	color: var(--ink);
}

/* The current page keeps the rule the others only borrow on hover. Same mark,
   permanent — so hovering another item reads as "this one too", not as a
   different kind of state. */
.aj-nav__links a:not(.aj-btn):hover::after,
.aj-nav__links a.is-current::after {
	transform: scaleX(1);
}

.aj-nav__links a.is-current {
	color: var(--ink);
	font-weight: 600;
}

.aj-nav__toggle {
	display: none;
	flex-direction: column;
	gap: 6.5px;
	background: none;
	border: 0;
	padding: 10px;
	cursor: pointer;
	position: relative;
	z-index: 70;
}

.aj-nav__toggle span {
	width: 29px;
	height: 2px;
	background: var(--ink);
	border-radius: 2px;
	transition: transform .4s var(--ease), opacity .3s;
}

/* ---------- 05b · Mobile drawer ----------
   A full-screen index, not a dropdown. Three destinations is too few to need a
   list and too many to leave unranked, so they are numbered — the same 01/02/03
   voice the case studies and the services list already speak in, at the scale a
   thumb reaches for.

   Every rule is inside the breakpoint. Above 880 the element is display:none and
   costs the desktop nothing: no paint, no stacking context, no layout.

   Positioning contract: this is a sibling of .aj-nav (see patterns/nav.php for
   why that is load-bearing) and it must stay one. It is fixed to the viewport,
   so any ancestor that grows a transform, a filter or `contain` steals the
   containing block back and puts the bug straight back in. */

/* Not "hidden until opened" — absent. The markup is printed on every page, so
   above the breakpoint it has to leave the flow entirely: it is a static div
   sitting immediately after the bar, and without this it renders as a stray
   list of links and a gold button under the header on every desktop page. */
.aj-menu {
	display: none;
}

@media (max-width: 880px) {
	.aj-menu {
		display: block;
		position: fixed;
		inset: 0;
		/* Under the bar (60) so the lockup and the hamburger sit on top of the
		   panel — that is what lets one button be both open and close. Over the
		   WhatsApp disc (55), which hides itself here anyway. */
		z-index: 58;
		visibility: hidden;
		/* The wipe. Bottom inset 100% → 0 pulls the panel down from under the bar
		   rather than fading it in, which is the same gesture the page curtain
		   makes and the reason this reads as part of the site.
		   visibility is delayed by the close duration so the panel is still
		   painted while it wipes back up, then leaves the tab order for good. */
		clip-path: inset(0 0 100% 0);
		transition:
			clip-path var(--curtain-close, .42s) var(--curtain-ease-close, ease),
			visibility 0s linear var(--curtain-close, .42s);
	}

	.admin-bar .aj-menu {
		top: var(--wp-admin--admin-bar--height, 32px);
	}

	html.menu-open .aj-menu {
		visibility: visible;
		clip-path: inset(0 0 0 0);
		transition:
			clip-path var(--curtain-open, .55s) var(--ease),
			visibility 0s;
	}

	/* ----- The ground -----
	   Paper, but lit: the same warm falloff the hero pours in from the corners,
	   turned to come from behind the bar so the top of the panel is the brightest
	   thing on it and the eye lands on 01 first. The grain is the site's own, at
	   the weight it runs at on light ground — enough to stop a flat fill, not
	   enough to be a texture. */
	.aj-menu__ground {
		position: absolute;
		inset: 0;
		background:
			radial-gradient(74% 44% at 84% 0%, rgba(209, 177, 128, .22), transparent 68%),
			radial-gradient(64% 40% at 4% 100%, rgba(169, 139, 87, .14), transparent 72%),
			linear-gradient(180deg, #FCFAF5 0%, var(--paper) 44%, var(--cream) 100%);
	}

	.aj-menu__ground::after {
		content: "";
		position: absolute;
		inset: 0;
		background-image: var(--grain);
		opacity: .04;
		mix-blend-mode: multiply;
		pointer-events: none;
	}

	/* Scrollable in its own right, and contained: a landscape phone has less
	   height than this list wants, and overscroll-behavior stops a flick at the
	   end of the menu from handing the gesture to the document underneath. */
	.aj-menu__inner {
		position: relative;
		/* border-box, explicitly. This theme is content-box throughout — see the
		   note on .aj-container in patterns/family.php — so height:100% plus the
		   nav-height top padding made this box 156px taller than the screen, and
		   the foot that margin-top:auto is supposed to park at the bottom landed
		   just under the fold instead. The one place on the site where a box is
		   sized to the viewport is the one place that has to opt out. */
		box-sizing: border-box;
		height: 100%;
		max-width: var(--container);
		margin-inline: auto;
		/* The index starts below the bar, not against it — vh-driven, so a short
		   landscape phone gives the offset back to the list and a tall one
		   spends it settling the whole composition lower on the screen. */
		padding:
			calc(var(--nav-h) + clamp(.6rem, 5vh, 3rem))
			var(--gutter)
			clamp(1.4rem, 4vh, 2.4rem);
		display: flex;
		flex-direction: column;
		overflow-y: auto;
		overscroll-behavior: contain;
		-webkit-overflow-scrolling: touch;
	}

	/* ----- The index ----- */
	.aj-menu__link {
		position: relative;
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto;
		align-items: center;
		column-gap: 1rem;
		row-gap: .15rem;
		/* 60px floor, so every row clears the tap target minimum even when the
		   type is at the small end of its clamp. */
		min-height: 60px;
		padding: clamp(.85rem, 2.4vh, 1.15rem) 0;
		text-decoration: none;
		color: color-mix(in srgb, var(--ink) 82%, transparent);
		transition: color .3s;
		-webkit-tap-highlight-color: transparent;
	}

	.aj-menu__num {
		grid-column: 1;
		font-family: var(--font-mono);
		font-size: .7rem;
		letter-spacing: .22em;
		line-height: 1;
		color: var(--gold-deep);
	}

	.aj-menu__label {
		grid-column: 1;
		font-weight: 700;
		font-size: clamp(2rem, 8vw, 2.6rem);
		line-height: 1.05;
		letter-spacing: -.02em;
	}

	.aj-menu__arrow {
		grid-column: 2;
		grid-row: 1 / span 2;
		font-size: 1.45rem;
		line-height: 1;
		color: var(--gold-deep);
		opacity: .5;
		transition: transform .45s var(--ease), opacity .3s;
	}

	/* The rule under each row, drawn from the left. Same stroke, same origin and
	   same ease as the underline the bar puts under a nav item — one gesture at
	   two scales, rather than a border that was simply already there. */
	.aj-menu__link::after {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		bottom: 0;
		height: 1px;
		background: rgba(42, 33, 27, .10);
		transform: scaleX(0);
		transform-origin: left;
		transition: transform .5s var(--ease);
	}

	html.menu-open .aj-menu__link::after {
		transform: scaleX(1);
	}

	.aj-menu__link:hover,
	.aj-menu__link:active {
		color: var(--ink);
	}

	.aj-menu__link:hover .aj-menu__arrow,
	.aj-menu__link:active .aj-menu__arrow {
		transform: translateX(8px);
		opacity: 1;
	}

	/* The page you are on keeps, permanently, what the others borrow: full ink
	   and a gold rule. Identical logic to .is-current in the bar. */
	.aj-menu__link.is-current {
		color: var(--ink);
	}

	.aj-menu__link.is-current::after {
		background: var(--gold-deep);
	}

	/* ----- The foot -----
	   margin-top:auto, so on a tall phone it sits at the bottom of the screen and
	   on a short one it simply follows the list and scrolls with it. */
	.aj-menu__foot {
		margin-top: auto;
		padding-top: clamp(1.5rem, 5vh, 2.4rem);
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 1.15rem;
	}

	.aj-menu__cta {
		width: min(100%, 22rem);
	}

	.aj-menu__meta {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: .35rem .8rem;
		margin: 0;
		font-family: var(--font-mono);
		font-size: .68rem;
		letter-spacing: .16em;
		text-transform: uppercase;
	}

	.aj-menu__meta a {
		color: var(--cocoa);
		text-decoration: none;
		padding-bottom: 3px;
		border-bottom: 1px solid rgba(169, 139, 87, .35);
		transition: color .3s, border-color .3s;
	}

	.aj-menu__meta a:hover {
		color: var(--ink);
		border-color: var(--gold-deep);
	}

	.aj-menu__meta > span {
		color: rgba(107, 79, 58, .35);
	}
}

/* ---------- 06 · Hero ----------
   Tall section, sticky stage. The extra height is the scroll runway the scrub needs;
   the stage stays pinned while --hp climbs 0→1 and the scene, the copy and the veil each
   answer at a different rate. Progress comes from motion.js — CSS owns every transform,
   the same split as --plx. */
/* One screen by default. The runway only appears once motion.js has confirmed it will
   actually drive the scrub — that class is what keeps no-JS and reduced-motion from
   inheriting 220svh of empty space they have no use for. */
.aj-hero {
	position: relative;
	min-height: 100svh;
	background:
		radial-gradient(58% 42% at 78% 92%, rgba(107, 79, 58, .30), transparent 70%),
		radial-gradient(70% 46% at 14% 100%, rgba(169, 139, 87, .24), transparent 72%),
		linear-gradient(#FCFAF5 0%, #F7F1E6 55%, #EFE4D0 100%);
}

.aj-hero.is-scrub {
	min-height: 220svh;
}

/* border-box is load-bearing. The theme is content-box by default, so 100svh plus the
   nav's 72px of padding measured 972px on a 900px screen — which put the tagline and the
   Scroll cue 46px below the fold. Harmless while the hero scrolled away; permanent once
   the stage is pinned. */
.aj-hero__stage {
	position: relative;
	box-sizing: border-box;
	min-height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding-top: var(--nav-h);
	overflow: clip;
	isolation: isolate;
}

.aj-hero.is-scrub .aj-hero__stage {
	position: sticky;
	top: 0;
	height: 100svh;
	min-height: 0;
}

.aj-hero__stage::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 3;
	background-image: var(--grain);
	opacity: .04;
	mix-blend-mode: multiply;
	pointer-events: none;
}

.aj-hero__scene {
	position: absolute;
	inset: 0;
	z-index: 0;
	/* Grows toward the statue, so the move reads as stepping closer to it rather than
	   the frame swelling. */
	transform-origin: 72% 100%;
	will-change: transform;
}

/* ----- The deck -----
   Four frames on one another, one lit. It sits inside the scrubbed plane rather than
   beside it, which is the whole reason the cross-fade and the scroll zoom do not fight:
   --hp scales .aj-hero__scene, the deck dissolves underneath that scale, and neither
   effect ever reads the other's transform.

   The ground under the stack is paper, not transparent and certainly not black. Frame 01
   is a video and a video has nothing to show for its first few hundred milliseconds —
   landing on the theme's own paper is invisible, landing on black is a flash. */
.aj-hero__deck {
	position: absolute;
	inset: 0;
	z-index: 0;
	background:
		radial-gradient(72% 58% at 74% 62%, rgba(107, 79, 58, .20), transparent 72%),
		linear-gradient(#FCFAF5 0%, #F1EADD 62%, #E4D7C0 100%);
}

/* No transition here on purpose. Section 25 owns the push-dissolve and only hands it to
   the two frames actually taking part, so a frame being reset to its off-stage position
   snaps there instead of sliding back across the screen while invisible. */
/* visibility, not just opacity. Four full-bleed frames at opacity 0 are still four
   full-screen layers the compositor keeps around; hidden ones are not painted at all.
   .is-leaving keeps its visibility for the length of the dissolve and loses it when
   motion.js drops the class, which is a beat after the fade has finished. */
.aj-hero__slide {
	position: absolute;
	inset: 0;
	opacity: 0;
	visibility: hidden;
}

.aj-hero__slide.is-active,
.aj-hero__slide.is-leaving {
	visibility: visible;
}

.aj-hero__slide.is-active {
	opacity: 1;
}

/* Contains the screen blend below. Without it the lift would reach past the frame and
   bleach the deck's own ground, which is the one thing under the stack. */
.aj-hero__slide {
	isolation: isolate;
}

.aj-hero__media {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* Cover-cropping a widescreen frame into a 100svh stage throws most of it away, so
	   every frame declares which part of itself it cannot afford to lose. Default is the
	   statue plate's, which is the one this hero was drawn around. */
	object-position: var(--focal, 72% 100%);
	/* The grade. Declared per frame in inc/hero-data.php, because this site is cream and
	   gold and footage is whatever it happens to be — the first pass at this deck put an
	   electric blue sky and a neon street directly under a gold wordmark. Cheap on the
	   GPU: saturate, sepia, brightness and contrast are all per-pixel shader work with no
	   sampling, unlike the blur the dissolve uses for a second at a time. */
	filter: var(--grade, none);
}

/* ----- Lift -----
   Paper screened over the frame, so crushed blacks come up to something the page can
   hold rather than reading as a hole cut in it. Two jobs at once: it is the difference
   between a night frame that looks like faded film and one that looks broken, and it is
   what keeps the top of the stage light enough for the transparent navbar's ink. */
.aj-hero__slide::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
	background: var(--paper);
	opacity: var(--lift, 0);
	mix-blend-mode: screen;
	pointer-events: none;
}

/* ----- Tone -----
   The opposite dial: a warm veil for frames that arrive too bright or too busy on the
   left, where the gold headline has to sit. The statue plate sets both to 0, so nothing
   about the shot that shipped has changed. */
.aj-hero__slide::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 2;
	background: linear-gradient(105deg,
			rgba(107, 79, 58, .58),
			rgba(23, 18, 16, .34) 58%,
			rgba(23, 18, 16, .14));
	opacity: var(--tone, 0);
	pointer-events: none;
}

/* The scene's white ground is 2% lighter than paper, so instead of recolouring it the
   left side is washed back to paper — which also buys the headline a clean field to sit
   on. The wash is what makes the seam invisible.

   z-index is load-bearing now that the deck is a real element rather than a lone <img>:
   without it the wash and the stack are both auto-positioned and the last one in paint
   order wins by accident. The wash has to stay on top of every frame — it is the constant
   the copy is legible against, whichever frame is playing. */
.aj-hero__scene::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
	background:
		/* The navbar band. The bar is transparent until the page scrolls, so its ink is
		   sitting on whatever the hero happens to be showing — which was fine while the
		   hero was one pale plate and is not fine now that any frame, including the
		   client's own footage later, can appear under it. Ends before the headline
		   starts, so it costs the composition nothing. */
		linear-gradient(180deg,
			rgba(250, 247, 241, .78) 0,
			rgba(250, 247, 241, .34) calc(var(--nav-h) * 1.15),
			transparent calc(var(--nav-h) * 2)),
		linear-gradient(100deg,
			var(--paper) 0%,
			rgba(250, 247, 241, .88) 32%,
			rgba(250, 247, 241, .12) 62%,
			transparent 78%),
		linear-gradient(0deg, rgba(250, 247, 241, .55), transparent 34%);
	pointer-events: none;
}

/* Paper takes the screen back before the stage releases into Manifesto, so the handover
   is a fade rather than a cut. */
.aj-hero__veil {
	position: absolute;
	inset: 0;
	z-index: 1;
	background: linear-gradient(180deg, rgba(250, 247, 241, .2), var(--paper) 78%);
	opacity: 0;
	pointer-events: none;
}

.aj-hero__inner {
	position: relative;
	z-index: 2;
	width: 100%;
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
	will-change: transform;
}

.aj-hero__title {
	margin: 0;
	font-weight: 800;
	letter-spacing: -.035em;
	line-height: .92;
	font-size: clamp(3.4rem, 11.5vw, 9.2rem);
}

.aj-hero__line {
	display: block;
	background: linear-gradient(115deg, #E8D6AF 5%, #D1B180 45%, #B3925F 90%);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	padding-bottom: .04em;
}

.aj-hero__line--offset {
	margin-left: clamp(2.5rem, 12vw, 11rem);
	margin-top: -.05em;
}

.aj-hero__sub {
	max-width: 44ch;
	font-size: clamp(1.1rem, 1.6vw, 1.35rem);
	line-height: 1.6;
	color: color-mix(in srgb, var(--ink) 75%, transparent);
	margin: 1.8rem 0 2.4rem;
}

.aj-hero__cta {
	display: flex;
	gap: 1rem;
	flex-wrap: wrap;
}

.aj-hero__foot {
	position: absolute;
	left: 0;
	right: 0;
	margin-inline: auto;
	bottom: 1.6rem;
	z-index: 2;
	will-change: opacity;
	width: 100%;
	max-width: var(--container);
	padding-inline: var(--gutter);
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1.5rem;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: color-mix(in srgb, var(--ink) 55%, transparent);
}

.aj-hero__scroll {
	display: inline-flex;
	align-items: center;
	gap: .55rem;
	flex: none;
}

.aj-hero__scroll::after {
	content: "↓";
	display: inline-block;
	animation: aj-down 2.2s var(--ease) infinite;
}

@keyframes aj-down {
	0%, 100% { transform: translateY(0); opacity: .55; }
	50% { transform: translateY(5px); opacity: 1; }
}

/* ----- Deck controls -----
   Clustered at the bottom right rather than floated over the left and right edges of the
   stage, which is where a boxed slider would put them. This stage is not boxed: the left
   third is the headline and the CTAs, and the right third is the subject of whatever
   frame is playing. Arrows there would sit on both. The foot row is already the stage's
   instrument panel — mono, uppercase, quiet — so the deck's readout belongs in it. */
.aj-hero__ctl {
	display: flex;
	align-items: center;
	gap: clamp(.7rem, 1.6vw, 1.3rem);
	/* Groups the cluster with the Scroll cue at the right instead of letting
	   space-between strand it in the middle of the row. */
	margin-left: auto;
	flex: none;
}

.aj-hero__ticks {
	display: flex;
	align-items: center;
	gap: .45rem;
}

/* A 2px bar is the readout; the button around it is 24px tall so it clears the minimum
   target size without the mark growing to match. */
.aj-hero__tick {
	position: relative;
	display: grid;
	align-items: center;
	width: clamp(26px, 3.4vw, 40px);
	height: 24px;
	padding: 0;
	border: 0;
	background: none;
	cursor: pointer;
	-webkit-appearance: none;
	appearance: none;
}

.aj-hero__tick::before {
	content: "";
	height: 2px;
	border-radius: 999px;
	background: rgba(42, 33, 27, .22);
	transition: background .3s;
}

.aj-hero__tick:hover::before {
	background: rgba(42, 33, 27, .4);
}

.aj-hero__tickfill {
	position: absolute;
	left: 0;
	right: 0;
	top: 50%;
	height: 2px;
	margin-top: -1px;
	border-radius: 999px;
	background: linear-gradient(90deg, var(--gold-soft), var(--gold-deep));
	transform: scaleX(0);
	transform-origin: left center;
}

/* Frames already played stay filled, so the row reads as a progress strip rather than a
   single travelling dot with no memory of where it has been. */
.aj-hero__tick.is-done .aj-hero__tickfill {
	transform: scaleX(1);
}

.aj-hero__ctlbtns {
	display: flex;
	gap: .4rem;
}

/* Inherits .aj-rail__btn — the deck is a different component but it is the same gesture,
   and the site should not own two circular arrow buttons that disagree by 2px. */
.aj-hero__btn {
	width: 34px;
	height: 34px;
	font-size: 1.05rem;
}

.aj-hero__btn--toggle {
	color: var(--cocoa);
}

/* Two glyphs from one box: a pair of bars becomes a triangle by trading its side borders
   for a left border. Cheaper than two SVGs and it takes currentColor for free. */
.aj-hero__glyph {
	display: block;
	width: 9px;
	height: 11px;
	border-left: 3px solid currentColor;
	border-right: 3px solid currentColor;
}

.aj-hero__btn--toggle[aria-pressed="true"] .aj-hero__glyph {
	width: 0;
	height: 0;
	margin-left: 2px;
	border-left: 10px solid currentColor;
	border-right: 0;
	border-top: 6px solid transparent;
	border-bottom: 6px solid transparent;
}

/* Once the scrub has carried the foot most of the way out it is still in the layout at
   opacity 0, and an invisible row of buttons that still takes clicks is a trap. */
.aj-hero.is-past .aj-hero__foot {
	pointer-events: none;
}

/* ---------- 07 · Trifecta ----------
   Was the tail of a Manifesto section that also carried a display headline. The
   headline came off in the three-page split; what is left is a mono label and
   three cards, so the section has to hold a full dark band on its own. It does
   it with air: the label is the only type above the cards and the grid is given
   the section's whole measure. */
.aj-trifecta {
	margin-top: clamp(3.5rem, 7vw, 5.5rem);
}

/* Standing alone there is nothing above it to be spaced from. */
.aj-trifecta-sec .aj-trifecta {
	margin-top: 0;
}

/* The default ghost is sized for a section two screens tall and would run off
   the top of this one. Dropped to the lower right, behind the cards, where it
   reads as texture on the ground rather than as a word competing with them. */
.aj-trifecta-sec {
	overflow: clip;
}

/* The other half of the Trifecta → Jarakan join. .aj-cpanel bleeds espresso down
   into the amber panel; this bleeds the amber back up into the espresso, so that
   seam dovetails from both sides like the three below it rather than being the
   one join on the page that only blends one way.

   Deliberately a copy of .aj-cpanel::before's bottom half rather than something
   of its own — same .5 alpha, same bloom alpha, same band height. The cancelling
   only works if both faces of a join are built to the same recipe, and this is
   the one face that belongs to a different component. Its top end is fully
   transparent, so the band overlapping the section's bottom padding costs the
   cards nothing. */
.aj-trifecta-sec::before {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 0;
	height: clamp(90px, 14vh, 190px);
	background:
		radial-gradient(120% 100% at 50% 100%, rgba(209, 177, 128, .14), transparent 70%),
		linear-gradient(0deg, rgba(139, 93, 49, .5), transparent);
	background-repeat: no-repeat;
	background-size: 100% 50%, 100% 100%;
	background-position: 0 100%, 0 100%;
	pointer-events: none;
}

/* Quieter than it was. At .10 it was tuned to hold its own against a .8rem mono
   label; against a 5rem gold title it became the second gold thing in the
   section and the two read as competing, not as figure and ground. */
.aj-trifecta-sec .aj-ghost {
	top: auto;
	bottom: -2.5rem;
	right: 2%;
	font-size: clamp(5rem, 12vw, 9rem);
	-webkit-text-stroke-color: rgba(209, 177, 128, .07);
}

/* ----- Section head -----
   Centred, gold, and the size of a real display title — the client read the
   section as unmarked while its head was a .8rem mono label ranged left.

   Built from parts the page already owns: .aj-shine is the hero's gold gradient
   and its one-shot sweep, data-anim="mask" is the hero's rise-out-of-a-clip. The
   only new thing here is the centred rule, and that exists because the shared
   .aj-mono-label rule runs off to the right, which under a centred title reads
   as a mistake rather than as a flourish. */
.aj-trifecta__head {
	text-align: center;
	margin-bottom: clamp(3rem, 6vw, 4.5rem);
}

.aj-trifecta__title {
	margin: 0;
	font-size: clamp(2.6rem, 6.2vw, 5.4rem);
	font-weight: 800;
	line-height: 1.02;
	letter-spacing: -.03em;
	/* Sits above .aj-display (3.7rem cap) and well under the hero (9.2rem): this
	   is the largest type on the page that is not the brand's own name. */
	color: transparent;
	/* .aj-shine supplies the gradient and the sweep; without a transparent fill
	   the paint has nothing to show through. Same two-line contract as
	   .aj-hero__line. */
	padding-bottom: .06em;
	max-width: 24ch;
	margin-inline: auto;
}

/* Drawn from the middle outwards — a left-origin draw under centred type looks
   like the line missed its mark. The diamond is the only ornament on the page
   and it is here because a bare rule under a title this size reads as unfinished. */
.aj-trifecta__rule {
	display: block;
	position: relative;
	width: clamp(5rem, 12vw, 9rem);
	height: 1px;
	margin: clamp(1.4rem, 2.6vw, 2rem) auto 0;
	background: linear-gradient(90deg, transparent, var(--gold) 28%, var(--gold) 72%, transparent);
}

.aj-trifecta__rule::after {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	width: 6px;
	height: 6px;
	margin: -3px 0 0 -3px;
	background: var(--gold);
	transform: rotate(45deg);
	box-shadow: 0 0 14px rgba(209, 177, 128, .75);
}

/* ----- The grid, and the light under it -----
   The gap grew with the cards: at 1.2rem three larger plates read as one banded
   object rather than as three briefs.

   ::before is the aurora — a soft gold bed that lights as the section arrives.
   It is the single biggest reason the set now reads as highlighted: the cards
   used to sit flat on espresso, and a lit ground is what makes them look like
   they are standing on something rather than painted onto it. Negative insets
   because the glow has to spill past the plates to work as light. */
.aj-trifecta__grid {
	position: relative;
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 1.6rem;
}

.aj-trifecta__grid::before {
	content: "";
	position: absolute;
	z-index: -1;
	inset: -20% -7%;
	background:
		radial-gradient(46% 58% at 50% 46%, rgba(209, 177, 128, .17), transparent 68%),
		radial-gradient(32% 46% at 13% 60%, rgba(209, 177, 128, .10), transparent 70%),
		radial-gradient(32% 46% at 87% 60%, rgba(209, 177, 128, .10), transparent 70%);
	filter: blur(16px);
	pointer-events: none;
}

/* ----- Gold-lit card -----
   The wash used to sit at .13 with a .20 border, which on espresso put the three
   cards within a few percent of the ground behind them: readable if you stopped,
   invisible if you were scrolling. Lit properly now.

   The four numbers live here as custom properties because .aj-step (the three
   Media Buying steps) is declared in the stylesheet as the same object, and a
   claim like that only stays true if both read from one source. Overriding
   --tc-* on .aj-step is the way to split them again if that is ever wanted. */
:root {
	--tc-wash-a: rgba(209, 177, 128, .28);
	--tc-wash-b: rgba(209, 177, 128, .07);
	--tc-edge: rgba(209, 177, 128, .45);
	--tc-edge-hover: rgba(209, 177, 128, .62);
	--tc-copy: rgba(243, 237, 227, .82);
}

/* Padding, lift and shadow all moved together. The card was a flat plate the
   same distance from the ground as everything else in the section; it now sits
   on a real drop shadow, which is what lets the aurora behind it read as light
   falling on something rather than as a stain on the background. */
.aj-tcard {
	position: relative;
	padding: 2.6rem 2.2rem 2.8rem;
	border-radius: var(--radius);
	background: linear-gradient(165deg, var(--tc-wash-a), var(--tc-wash-b) 60%);
	border: 1px solid var(--tc-edge);
	box-shadow: 0 30px 60px -22px rgba(0, 0, 0, .55);
	transition:
		transform .6s var(--ease),
		border-color .4s,
		box-shadow .6s var(--ease);
	overflow: hidden;
}

/* The hover shadow is gold rather than black: the card is not moving further
   from a light source, it is switching one on. */
.aj-tcard:hover {
	transform: translateY(-10px);
	border-color: var(--tc-edge-hover);
	box-shadow: 0 26px 64px -18px rgba(209, 177, 128, .34);
}

/* The lit edge. Drawn left to right as the card arrives — the last beat of the
   card's own cascade, so it reads as the card switching on rather than as a
   decoration that was always there.
   ---
   This used to be dead on every desktop. .aj-spot (added by JS to exactly these
   cards on fine pointers) also claimed ::before, was declared later in the file,
   and overwrote the gradient with its own inset:0 radial at opacity 0 — so on a
   pointer device the edge was an invisible 2px strip that only appeared under
   the cursor. The spotlight moved to ::after; this element is the edge again,
   which is also why the section looked unlit on the machines it was reviewed on. */
.aj-tcard::before {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 2;
	height: 3px;
	background: linear-gradient(90deg, var(--gold-soft), var(--gold) 45%, transparent);
	box-shadow: 0 0 16px rgba(209, 177, 128, .5);
	transform-origin: left;
}

/* ----- Animated gold rim -----
   A border cannot be a gradient, so the rim is a filled box masked down to its
   own 1px frame: paint the whole area, then punch the content box out of the
   mask and only the edge survives. Still at rest — it turns and brightens under
   the pointer, which is the one place a moving ornament is answering something
   the reader did rather than demanding attention on its own.

   --rim is registered below so the conic angle is animatable. Browsers without
   @property just hold it at 0deg and get a static gold frame, which is the same
   ornament minus the turn. */
.aj-tcard__rim {
	position: absolute;
	inset: 0;
	z-index: 3;
	border-radius: inherit;
	padding: 1px;
	background: conic-gradient(
		from var(--rim, 0deg),
		transparent 0deg,
		var(--gold) 55deg,
		var(--gold-soft) 90deg,
		transparent 145deg,
		transparent 215deg,
		var(--gold-deep) 275deg,
		transparent 330deg
	);
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
	opacity: 0;
	transition: opacity .5s var(--ease);
	pointer-events: none;
}

.aj-tcard:hover .aj-tcard__rim {
	opacity: 1;
	animation: aj-rim 8s linear infinite;
}

@property --rim {
	syntax: "<angle>";
	inherits: false;
	initial-value: 0deg;
}

@keyframes aj-rim {
	to {
		--rim: 360deg;
	}
}

/* Filled chip, not loose type: at .75rem in gold on a now-brighter wash the bare
   numeral had nothing to hold it, and the set 01–03 is the one thing that says
   these three are a set. */
.aj-tcard__num {
	display: inline-block;
	font-family: var(--font-mono);
	font-size: .82rem;
	font-weight: 700;
	letter-spacing: .16em;
	color: var(--espresso);
	background: linear-gradient(135deg, var(--gold-soft), var(--gold) 60%, var(--gold-deep));
	padding: .5em .95em;
	border-radius: 999px;
	line-height: 1;
	/* The chip is the only solid gold object inside the plate, so it is what the
	   eye lands on first. The halo is what stops it reading as a flat sticker on
	   a lit card — it is the same light the aurora and the edge are made of. */
	box-shadow: 0 0 0 1px rgba(232, 205, 160, .3), 0 6px 20px -6px rgba(209, 177, 128, .55);
}

/* h2 and h3 both: the card title dropped a level when the section gained a real
   display head, and the pair is written as one selector so the plate does not
   care which rank the page it sits on happens to need. */
.aj-tcard :is(h2, h3) {
	font-size: 1.55rem;
	font-weight: 700;
	letter-spacing: -.015em;
	line-height: 1.22;
	margin: 1.15rem 0 .7rem;
}

.aj-tcard p {
	margin: 0;
	font-size: 1.02rem;
	line-height: 1.62;
	color: var(--tc-copy);
}

/* ---------- 08 · Cases ---------- */
/* The section itself carries almost nothing now: three panels bring their own
   grounds and their own padding, and the only thing left on paper is the
   crosslink that closes it. */
.aj-cases {
	background: var(--paper);
	overflow-x: clip;
	/* Panel 01 starts on the section's top edge, so espresso meets amber with no
	   band of paper in between. A ground that carries nothing is the thing this
	   page keeps removing. */
	padding-top: 0;
	/* Zero at both ends now, for the same reason. This padding used to be the room
	   the Crosslink stood in; once that row moved out to the disciplines deck it was
	   a band of bare paper between the espresso guarantee panel and the deck below —
	   a white seam carrying nothing. The last panel hands straight over. */
	padding-bottom: 0;
}

/* ----- Case panel -----
   The full-bleed replacement for .aj-cplate. The plate was a brand-coloured box
   floating on paper that did two jobs at once: give alpha cut-outs something to
   stand on, and carry the client's colour. Full-bleed does both better — the
   cut-outs get the whole measure to stand in, and the colour stops being a
   swatch inside the page and becomes the page.

   Modelled on .aj-vspot, the Jasa Raharja panel: one saturated field, its own
   band of the page, light type on top. Deliberately NOT .aj-section--dark —
   that is espresso with a hint of colour thrown at it, and these grounds are
   the colour, at full strength.

   isolation: isolate is load-bearing. The grain layer sits at z-index -1 so it
   does not have to out-paint the cut-outs; without an isolated stacking context
   it would fall behind the panel's own background and disappear. Same rule
   .aj-ads already follows. */
.aj-cpanel {
	position: relative;
	isolation: isolate;
	overflow: clip;
	padding-block: clamp(4rem, 8vw, 7rem);
	color: var(--light);

	/* ----- Seam contract -----
	   Every panel declares the colour of whatever is directly above and below it
	   and bleeds a little of each into its own field. See the seam block further
	   down for why. Default transparent: a panel with no declared neighbour just
	   draws nothing, so adding a fourth brief cannot break the chain, it can only
	   leave one join unblended until its two variables are filled in. */
	--seam-h: clamp(90px, 14vh, 190px);
	--seam-up: transparent;
	--seam-dn: transparent;
	/* Equal on both faces, and it has to be: the two blooms that meet at a join
	   come from different panels, and any difference between them puts a step of
	   gold exactly where the seam is meant to disappear. */
	--seam-glow-t: .14;
	--seam-glow-b: .14;
}

.aj-cpanel::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image: var(--grain);
	opacity: .06;
	mix-blend-mode: soft-light;
	pointer-events: none;
}

/* ----- The seam -----
   This used to be a gold hairline, drawn left to right as each panel arrived —
   a real edge, and the client read it exactly as one: three separate boxes
   stacked up rather than one run of colour. What they asked for is the join to
   stop being visible.

   So the border is gone and the colours dovetail instead. Each panel bleeds its
   upstairs neighbour's bottom edge down into its own top, and its downstairs
   neighbour's top edge up into its own bottom. Both sides of every join carry
   the same two colours falling away from each other, and the result is a
   crossfade with no point in it the eye can land on and call the boundary.

   Gold survives as a bloom rather than a line: a wide, low ellipse of light
   sitting on the join. The colour is the same --gold the hairline was made of,
   so the seam still says "Anjali" — it just says it as light instead of as a
   rule. --seam-glow-b: 0 turns it off where a join should stay quiet.

   STATIC ON PURPOSE. The hairline had an entrance and this does not, which is
   the point: the reveal observer fires at 15% visibility, so on a panel a screen
   tall the join is already well inside the viewport before .is-in lands. Anything
   that animates here would show the reader an unblended edge first and then blend
   it — advertising the seam in the act of hiding it. The neighbouring articles no
   longer carry data-reveal for that reason.

   z-index 0 keeps it above the panel's own background and below .aj-cpanel__inner
   at z-index 1, so it blends grounds without fogging the cut-outs standing on them. */
.aj-cpanel::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background:
		radial-gradient(120% 100% at 50% 0%, rgba(209, 177, 128, var(--seam-glow-t)), transparent 70%),
		linear-gradient(180deg, var(--seam-up), transparent),
		radial-gradient(120% 100% at 50% 100%, rgba(209, 177, 128, var(--seam-glow-b)), transparent 70%),
		linear-gradient(0deg, var(--seam-dn), transparent);
	background-repeat: no-repeat;
	/* The bloom is shorter than the bleed so the light sits on the join rather
	   than washing the whole transition. */
	background-size:
		100% calc(var(--seam-h) * .5),
		100% var(--seam-h),
		100% calc(var(--seam-h) * .5),
		100% var(--seam-h);
	background-position: 0 0, 0 0, 0 100%, 0 100%;
}

/* Lifted from .aj-cplate's gradients, then opened up. The old values were mixed
   for a small box sitting on bright paper, where a dark field was what stopped
   the box from shouting. As a full ground the same mix read as "dark with a
   brown tint" rather than as brown, so the linear leg starts two stops lighter
   and the warm radial carries more of the field. The reference is the Jasa
   Raharja panel, which is unmistakably blue rather than dark-with-blue. */
/* ----- Why every seam colour is written at exactly .5 alpha -----
   This is the whole trick, and it was wrong the first time round. Bleeding the
   neighbour's colour in at full strength puts THEIR colour on OUR side of the
   join and ours on theirs — the two grounds swap across the line, which draws a
   harder edge than the hairline it replaced.

   At half alpha each side renders the same thing:

       above the line   = .5 × own_bottom + .5 × neighbour_top
       below the line   = .5 × neighbour_bottom + .5 × own_top

   which are the same two colours in the same proportion. The join lands on the
   midpoint from both directions and cancels out exactly, whatever the colours
   are — it does not need tuning per panel, and it cannot drift when a ground is
   recoloured.

   The colours themselves are each panel's *rendered* edge sampled off the page
   with the seam layer switched off, not the raw stop from its linear leg. It
   makes a real difference: every ground here is a linear gradient with two
   radials over it, so Jarakan's bottom renders rgb(96,60,33) where its linear
   stop says #4E301B = rgb(78,48,27), and quoting the stop left a measurable step
   at the join. Sampled across eight columns and averaged, since the radials also
   vary the edge left to right.

   Re-measure these if a ground is ever recoloured — that is the one maintenance
   cost of this approach, and it is cheaper than the hairline it replaced. */
.aj-cpanel--jarakan {
	background:
		radial-gradient(58% 70% at 24% 18%, rgba(240, 183, 101, .55), transparent 66%),
		radial-gradient(46% 58% at 88% 88%, rgba(122, 78, 43, .5), transparent 62%),
		linear-gradient(155deg, #8A5A2E, #6B4224 52%, #4E301B 100%);
	/* Espresso above (the Trifecta band), Changcuters red below. */
	--seam-up: rgba(25, 20, 17, .5);
	--seam-dn: rgba(154, 41, 33, .5);
	/* ----- The light behind the brief's mark -----
	   Read by .aj-smsigil__aura, which composes them with its own alphas — the same
	   contract .aj-smpanel declares these under in the Results chapter, which is why
	   they are bare RGB triplets rather than colours.

	   Each panel gives its own lit accent, one step brighter than the field it sits
	   on: the light already in the room, gathered behind the mark. One gold for all
	   three would have been the safe answer and the wrong one — the same lamp behind
	   three different brands on three different grounds. */
	--glow-a: 240, 183, 101;
	--glow-b: 154, 100, 52;
}

.aj-cpanel--changcuters {
	background:
		radial-gradient(56% 66% at 78% 16%, rgba(226, 78, 58, .5), transparent 64%),
		radial-gradient(44% 56% at 12% 90%, rgba(140, 30, 26, .5), transparent 62%),
		linear-gradient(155deg, #A02A22, #7A1C18 52%, #4A100E 100%);
	/* The hardest join on the page: #503222 brown meeting #9E2E24 red with
	   nothing in common but their warmth. It gets the deepest bleed of the three. */
	--seam-up: rgba(96, 60, 33, .5);
	--seam-dn: rgba(104, 44, 26, .5);
	--seam-h: clamp(110px, 17vh, 230px);
	--glow-a: 232, 96, 74;
	--glow-b: 150, 36, 30;
}

.aj-cpanel--lyma {
	background:
		radial-gradient(50% 64% at 20% 14%, rgba(214, 108, 54, .42), transparent 64%),
		radial-gradient(44% 54% at 86% 86%, rgba(120, 44, 24, .4), transparent 62%),
		linear-gradient(155deg, #6B2E1C, #4A1C11 55%, #2A1109 100%);
	--seam-up: rgba(100, 22, 19, .5);
	/* Nothing to blend into: below this the section returns to paper for the
	   crosslink. That edge should stay decisive — it is the reader leaving the
	   dark chapter — so the bottom carries no bleed and no bloom, only the panel's
	   own field going quiet. A gold line there would be the one hairline left on
	   the page, which is the thing this whole block exists to remove. */
	--seam-dn: transparent;
	--seam-glow-b: 0;
	--glow-a: 224, 122, 66;
	--glow-b: 132, 52, 28;
}

/* Same measure and gutter as every other full-bleed panel on the site, so the
   type in here lines up with the type in .aj-vspot and .aj-impanel. */
.aj-cpanel__inner {
	position: relative;
	z-index: 1;
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
	display: grid;
	/* Art gets the larger share: the point of the rebuild is that the cut-outs
	   are finally big enough to read. */
	grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
	gap: clamp(2rem, 5vw, 5rem);
	align-items: center;
}

.aj-cpanel--flip .aj-cpanel__inner {
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
}

.aj-cpanel--flip .aj-cstage {
	order: 2;
}

/* LYMA's shot is capped at its supplied resolution (410px), so a 1.15fr column
   would leave it stranded in the middle of empty ground. Narrower column, same
   picture, and it reads as filling its half.
   ---
   Narrower again, .8 → .7, now that the copy column carries the deck's five
   blocks instead of one paragraph. The picture does not shrink — it is capped in
   pixels and its column still clears 560px at full width — the measure just
   stops being split evenly between a fixed-size photograph and a column that
   grew fivefold. */
/* ----- Brief 03's tracks, re-cut for three pictures -----
   Was .7fr / 1.3fr, sized when the media column held one board capped at its own
   supplied resolution. It now holds a cluster — board plus the two photographs the
   client supplied — and 574px at review width cannot carry that.

   The copy track is capped rather than proportional, and the cap is what makes the
   change free. Every block in this column already carries its own ch measure: the body
   and the handover stop at 60ch (576px), and the quote stops at 34ch. 54rem is wider
   than all of them, so narrowing the track from 1066 to 860 re-wraps nothing and the
   panel's height does not move. 50% keeps it from eating the media column on the
   narrow end, where the ch caps are the things still governing. */
.aj-cpanel--lyma .aj-cpanel__inner {
	grid-template-columns: minmax(0, 1fr) minmax(0, min(54rem, 50%));
}

/* ----- The stage -----
   What is left of the plate once the box is gone: a positioned box with a ratio,
   holding absolutely-placed cut-outs. No background, no rounding, no clipping —
   the ground behind it is the plate now.

   Placed with insets, never with a centring transform. The variant reset
   html.js :where([data-reveal][data-anim]) writes transform:none at (0,1,1) and
   beats a plain class — a translateX(-50%) here gets wiped. */
.aj-cstage {
	position: relative;
	aspect-ratio: 5 / 4;
}

.aj-cstage__base {
	position: absolute;
	bottom: 0;
	width: auto;
	height: auto;
	max-height: 94%;
	object-fit: contain;
	object-position: bottom center;
	filter: drop-shadow(0 26px 34px rgba(0, 0, 0, .34));
}

/* Brief 01's lamb, and only brief 01's now: brief 02's second cut-out became a ground
   (.aj-cproof) and does not use this pair any more. The division of labour is the one
   the whole file runs on — wrapper owns the parallax channel, image owns its arrival. */
.aj-cstage__plx {
	position: absolute;
	z-index: 2;
	pointer-events: none;
}

.aj-cstage__pop {
	display: block;
	width: 100%;
	height: auto;
	filter: drop-shadow(0 30px 38px rgba(0, 0, 0, .4));
}

/* ----- Brief 01's art: inside the measure, at the size the stage can carry -----
   This pair spent one revision anchored to the viewport instead of to the stage — a
   panel-level .aj-cscene at left:0, sized in vw. It answered the complaint it was
   built for (the art was small, the panel read as empty) and broke a different thing
   badly enough to be worth a note.

   Anchored to the panel, art tracks the VIEWPORT. Copy tracks a container that stops
   growing at 1800px. At 2545 those two land next to each other and it looks right. At
   5090 — a 2K screen at 50% zoom — the container is still 1800 in the middle of the
   screen while the art has walked out to 2596 wide against the left edge, and 920px of
   nothing opens between the family and the lamb.

   The real difference from brief 02 is what the pictures ARE, not how they are placed.
   A phone cut off by the edge of the screen reads as an object carrying on past it;
   edge-anchoring is the correct reading of it. A family and a lamb are whole objects on
   a stage, and they have to go wherever the measure goes or they stop being a
   composition. So: percentages of .aj-cstage, which is a grid item in the centred
   container. At 5090 this renders identically to 2545 with more ground either side —
   the same thing every other section on the page does.

   Size comes out of the stage instead. 100% of the column with a 6% overhang is 877px
   at review width, 1.22x the 82% it used to be, and the overhang is safe at every
   width the two-column layout runs at: it still leaves 51–60px between the group and
   the viewport edge at 941–1440, so no face is ever cut. */
.aj-cpanel--jarakan .aj-cstage__base {
	left: -6%;
	max-width: 100%;
	/* Two passes rather than one, the same lesson the Changcuters band learned: at
	   this size a single diffuse shadow reads as a sticker, and the tight pass is the
	   contact edge that puts the feet on the ground. */
	filter:
		drop-shadow(0 12px 18px rgba(0, 0, 0, .3))
		drop-shadow(0 34px 44px rgba(0, 0, 0, .34));
}

/* The lamb went back to the size it started at, and the fix for "it covers the family"
   is 9% of travel rather than any change in scale. Measured off the render: the man's
   head sits at 64–76% of the family's width, so with the group at -6%..94% of the
   stage his head ends at 614. Pushing the lamb's right edge 9% past the stage puts its
   left edge at 658 — 44px of clear air, and the only thing behind it is his arm.

   The overhang lands in the gutter, never in the copy: 9% of the stage is 79px at 2545
   against an 80px gap, 36px at 941 against 47. Top stays at 0, which is the panel's own
   padding line, so the ears are clear of the espresso bleed at the join and the feet
   float ~200px above the family's floor. That gap is the leap. */
.aj-cpanel--jarakan .aj-cstage__plx {
	top: 0;
	right: -9%;
	width: 34%;
}

/* Same size and place as before — that half of the panel was signed off and the
   grid, the stage ratio and this cap are all untouched. Only the shadow changed.
   The band used to stand on red ground, where one wide diffuse pass was enough;
   it now stands in front of a lit phone screen, and against white a single soft
   shadow reads as a sticker laid on top of the picture. The second, tighter pass
   is the contact edge, and it is the whole reason the band still reads as being
   in the room rather than pasted over it. */
.aj-cpanel--changcuters .aj-cstage__base {
	left: 0;
	max-width: 78%;
	z-index: 3;
	filter:
		drop-shadow(0 10px 16px rgba(0, 0, 0, .34))
		drop-shadow(0 30px 44px rgba(0, 0, 0, .42));
}

/* ----- Brief 02's proof: a ground, not a card -----
   This was a small tilted card inside .aj-cstage. The client asked for it flush
   right and filling the whole right side of the section, and no inset inside the
   stage can do that: .aj-cpanel__inner is capped at --container and padded by
   --gutter, so its subtree stops about 100px short of the viewport whatever it is
   told. Parented to .aj-cpanel instead, right:0 IS the viewport edge — no 50vw
   arithmetic, and nothing to re-derive if the container ever changes.

   z-index -1 puts it exactly where a ground belongs: above the panel's own red
   field, below the grain (::after, later in the DOM at the same level), below the
   seam wash (::before at 0) — so the crossfades into Jarakan and LYMA run over the
   phone as well as over the field — and below .aj-cpanel__inner at 1, which is why
   the band and the copy need no z-index of their own to stay in front of it.

   Height drives, width follows from aspect-ratio, so the asset can never be
   stretched by a layout it did not expect. Three caps, and each one earns its
   place: the section (108%, i.e. a little bleed for the drift to happen inside),
   the window (a short viewport must not be handed a phone taller than the screen),
   and the measure.

   52vw is measured against the asset's ink rather than against its box, which is
   what makes it as large as it is. The crop opens with 6% of transparent margin
   before the phone's bezel reaches half coverage, so the gap the reader sees is
   wider than the boxes suggest: at 941px, the narrowest width this layout runs at,
   the box overlaps nothing and the bezel still stands 39px clear of the copy; at
   1440 it is 63px, and above that the section's own height takes over as the cap
   long before the measure does.

   One consequence worth knowing before touching these numbers: between about 1200
   and 1500px the phone cannot be BOTH full height and clear of the copy. A 1:1
   asset filling an 820px-tall panel wants 858px of width, the copy column and its
   gutter want 680, and the viewport has 1440. Something has to give, and what gives
   is the height — the phone stays flush and top-aligned and its falloff simply
   starts earlier down the panel. The lever, if that is ever judged the wrong
   trade, is the copy column: narrowing brief 02's grid to .92fr / 1.23fr below
   1500px buys back the ~50px it is short by. That was left alone deliberately;
   the client signed off on the copy side as it stands. */
.aj-cproof {
	position: absolute;
	z-index: -1;
	right: 0;
	/* One definition, used twice. top has to be a fraction of THIS box, not of the
	   panel, or the two stop agreeing the moment the caps bite: at 1440 the box is
	   691px inside an 820px panel, and a panel-relative -4% would hang the box 6px
	   lower than the mask below expects, which is exactly how much of a straight
	   edge shows up at the seam. Percentages inside resolve against the panel height
	   in both properties, so the two stay welded whatever the caps do. */
	--proof-h: min(108%, 94vh, 52vw);
	height: var(--proof-h);
	top: calc(var(--proof-h) * -.045);
	width: auto;
	aspect-ratio: 1130 / 1080;
	pointer-events: none;

	/* ----- The feather, and why it is on this box -----
	   The asset is cut through the screen content at its top and bottom edges, and
	   at full height those cuts land exactly on the joins into Jarakan and LYMA. A
	   straight edge there would put back the boundary the seam block exists to
	   remove — the client read three hairlines as three stacked boxes once already.
	   So the ends dissolve instead. On a photograph this defocused the falloff
	   reads as depth of field rather than as a fade.

	   Vertical only. The left edge is already alpha in the asset, and the right
	   edge is the viewport, where a hard cut is the correct reading: the phone
	   carries on off screen.

	   The mask sits on this box, not on the drifting child, because this box does
	   not move — the falloff stays welded to the section's own edges whatever the
	   parallax is doing underneath it. On the child, the feather would slide off
	   the cut edges and show them again at the seam, which is the one thing it is
	   here to prevent.

	   no-repeat is load-bearing, not tidiness: mask images tile by default, so
	   whatever the child drifts outside this box would sail back into view through
	   the next tile.

	   ----- Why these four numbers and not rounder ones -----
	   Every stop is a percentage of the box, because the one thing that has to stay
	   clear is a feature of the ASSET, not of the layout: the Karunia Semesta row
	   sits at 13% of the image height whatever size it renders at, with nothing but
	   blurred tab bar above it. So the top has 13% to work in and no more.

	   4.5% is the bleed, and it is spent hiding the image's own cut edge: the box
	   hangs that far above the section, so box-local 4.5% IS the section's top edge,
	   which is where the mask reaches zero. Nothing is visible at the join at all —
	   the phone fades up out of it. It also has to out-reach the parallax, which at
	   0.025 lands between 23px and 36px of travel on windows 900–1400 tall; 4.5% is
	   38px at full size, so the cut stays outside the clip while it drifts.

	   10.5% is where it is fully lit — 2.5% of clear air under it before the Karunia
	   row starts.

	   76% → 96% is the bottom, and it stops short of 100% on purpose. The section's
	   bottom edge falls at box-local 97%, so ending the falloff at 96% puts zero
	   alpha there rather than the 13% a fade running to 100% would leave — thirteen
	   percent of a light photograph is still a straight line drawn across the join
	   into LYMA. */
	--proof-mask: linear-gradient(180deg,
		transparent 0,
		transparent 4.5%,
		#000 10.5%,
		#000 76%,
		transparent 96%);
	-webkit-mask-image: var(--proof-mask);
	mask-image: var(--proof-mask);
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
}

/* Drift only. Same division of labour as .aj-cstage__plx / __pop: the wrapper owns
   the parallax channel, the image owns its arrival, and neither overwrites the
   other. Positive factor, unlike the cut-outs — this is a ground, so it has to lag
   the scroll rather than outrun it, which is the convention .aj-ghost already
   follows at 0.1.

   The factor itself is capped by the mask, not by taste: the drift moves the image
   down inside a stationary feather, so anything past the 4.5% bleed walks the
   image's cut edge into the lit part of the panel. 0.025 keeps the travel under
   that on any window height a desktop layout sees, and it is the same order as the
   cut-outs above, which is what keeps the three panels feeling like one scroll. */
.aj-cproof__plx {
	position: absolute;
	inset: 0;
}

/* contain rather than cover: the height caps above can hand this box a height
   shorter than the asset's own ratio, and cover would answer that by slicing into
   the phone's body. contain keeps the whole device and object-position holds it
   against the edge it is flush with. */
.aj-cproof__img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	object-position: right center;
}

/* .55 was the alpha this caption carried on espresso. Amber is a far brighter
   ground and the same value measured 3.46:1 there — a fail for body-sized type.
   Raised until it clears on the lightest of the three grounds rather than tuned
   per panel: one caption, one value. */
.aj-cstage__tag {
	position: absolute;
	bottom: -2.4rem;
	left: 0;
	margin: 0;
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .12em;
	text-transform: uppercase;
	color: rgba(255, 251, 243, .82);
}

/* .aj-case__on — the platform chip row — was removed with the placeholder copy.
   TikTok / Instagram / "TikTok campaign" were written against the pictures and
   appear nowhere in the client's deck, so there is nothing left for the row to
   hold. .aj-chip itself stays: the deck's own "labelled as Popular on TikTok"
   is still a chip on brief 02. */

.aj-media__badge {
	position: absolute;
	top: 1.1rem;
	left: 1.1rem;
	font-family: var(--font-mono);
	font-size: .68rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--light);
	background: rgba(23, 18, 16, .4);
	-webkit-backdrop-filter: blur(8px);
	backdrop-filter: blur(8px);
	padding: .55em .95em;
	border-radius: 999px;
	border: 1px solid rgba(243, 237, 227, .18);
}

.aj-media__play {
	position: absolute;
	inset: 0;
	margin: auto;
	width: 74px;
	height: 74px;
	border-radius: 50%;
	border: 1px solid rgba(243, 237, 227, .5);
	background: rgba(23, 18, 16, .25);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	display: grid;
	place-items: center;
	transition: transform .5s var(--ease);
}

.aj-media__play::after {
	content: "";
	width: 0;
	height: 0;
	margin-left: 4px;
	border-style: solid;
	border-width: 9px 0 9px 15px;
	border-color: transparent transparent transparent var(--light);
}

/* ----- The still's affordance -----
   Its own object rather than a second .aj-media__play. Every frame on Work &
   Services opens on click now, but a play triangle over a photograph promises
   something that will not happen, and the two kinds sit side by side in the same
   rail — so the difference has to be visible before the click, not after it.

   Smaller and cornered rather than centred and 74px, which is the honest
   hierarchy: a clip is a thing you start, a picture is a thing you enlarge. */
.aj-media__zoom {
	position: absolute;
	right: .9rem;
	bottom: .9rem;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	border: 1px solid rgba(243, 237, 227, .45);
	background: rgba(23, 18, 16, .38);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	display: grid;
	place-items: center;
	opacity: 0;
	transform: scale(.86);
	transition:
		opacity .4s var(--ease),
		transform .4s var(--ease),
		background-color .4s var(--ease),
		border-color .4s var(--ease);
	pointer-events: none;
}

/* Two strokes crossing: a plus, which is the one glyph that reads as "bigger"
   without a magnifier's handle to draw at 14px. */
.aj-media__zoom::before,
.aj-media__zoom::after {
	content: "";
	position: absolute;
	background: var(--light);
	border-radius: 1px;
}

.aj-media__zoom::before {
	width: 14px;
	height: 2px;
}

.aj-media__zoom::after {
	width: 2px;
	height: 14px;
}

.aj-vframe:hover .aj-media__zoom,
.aj-vframe:focus-visible .aj-media__zoom {
	opacity: 1;
	transform: none;
	background: rgba(209, 177, 128, .34);
	border-color: rgba(209, 177, 128, .85);
}

/* Touch has no hover to reveal it, and a still with no affordance at all reads
   as a picture rather than as something you can open. */
@media (hover: none) {
	.aj-media__zoom {
		opacity: .8;
		transform: none;
	}
}

/* ----- Anything that opens in the lightbox -----
   One rule for both element kinds. The Branding images are <img role="button">
   and get no pointer or focus treatment for free, so both are stated here. */
[data-photo] {
	cursor: zoom-in;
}

img[data-photo]:focus-visible {
	outline: 2px solid var(--gold);
	outline-offset: 3px;
}

/* ----- The client's mark, opening each brief -----
   .aj-smsigil wholesale — see anjali_sigil() in inc/social-data.php for why that
   component rather than a new one. All this adds is a box to size it in: the
   sigil is aspect-ratio 1 and fills whatever it is given, and in the Results
   chapter what it is given is a whole grid column.

   The cap is the width the mark can take before it stops being a credential and
   starts being the second picture in the panel. Measured against what is beside
   it: at 7.25rem the plate is a little over half the height of .aj-case__title's
   first line pair, which reads as a stamp on the copy rather than as art. */
.aj-case__sigil {
	width: clamp(5.5rem, 8vw, 7.25rem);
	margin-bottom: clamp(1.1rem, 2vw, 1.5rem);
}

.aj-case__label {
	font-family: var(--font-mono);
	font-size: .78rem;
	letter-spacing: .2em;
	text-transform: uppercase;
	color: var(--cocoa);
	margin: 0;
}

.aj-section--dark .aj-case__label {
	color: var(--gold);
}

/* The three briefs are numbered because all three are visible in one section —
   the same condition every other numbered set on this page has to meet. The
   rule draws itself to the right as the label arrives, the gesture the service
   rows and the stat underlines already use. */
.aj-case__num {
	color: var(--gold-deep);
}

.aj-section--dark .aj-case__num {
	color: var(--gold);
}

.aj-case__num::after {
	content: "";
	display: inline-block;
	width: 1.7rem;
	height: 1px;
	margin: 0 .7rem .25em;
	vertical-align: middle;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: left;
	transition: transform .7s var(--ease) .12s;
}

html.js .aj-case__label[data-reveal].is-in .aj-case__num::after,
html:not(.js) .aj-case__num::after {
	transform: scaleX(1);
}

/* The headline is a sentence out of the deck now rather than a written-to-fit
   line, so it runs two or three lines instead of one. The ch cap is what stops
   it running to five on a 1800px measure — a heading that wide stops being read
   as a heading and starts being read as the first paragraph. */
.aj-case__title {
	font-size: clamp(1.7rem, 3vw, 2.5rem);
	font-weight: 800;
	letter-spacing: -.02em;
	line-height: 1.12;
	max-width: 22ch;
	margin: .5rem 0 1rem;
}

/* The deck sets one phrase per brief in bold italic — the turn the whole story
   makes. Italic alone would be lost at 800 weight, so the emphasis is carried in
   gold: the site's own way of saying "this is the part that matters", and the
   same colour the chip, the rule and the <mark> in brief 03 are made of. */
.aj-case__title em {
	font-style: italic;
	color: var(--gold-soft);
}

.aj-case__copy {
	color: color-mix(in srgb, var(--ink) 76%, transparent);
	max-width: 52ch;
	margin: 0 0 1.8rem;
}

.aj-case__stats {
	display: flex;
	flex-direction: column;
	gap: .9rem;
	align-items: flex-start;
}

.aj-stat__num {
	display: block;
	font-size: clamp(2.6rem, 5vw, 4rem);
	font-weight: 800;
	letter-spacing: -.03em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
}

.aj-stat__label {
	display: block;
	margin-top: .45rem;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--cocoa);
}

/* Same mono voice as the label, on the other side of the number. The deck asks
   "The result?" before it gives the figure, and a question that arrives after
   its answer is not a question. Shares the label's type so the number sits
   between two lines of one voice rather than three competing ones. */
.aj-stat__lead {
	display: block;
	margin-bottom: .5rem;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--cocoa);
}

/* ----- The outcome line -----
   Replaces .aj-case__note, which was a footnote: .95rem at 65% ink, sized to sit
   under a figure and add a detail. This carries what each brief actually proved,
   and on brief 01 it is the outcome — the deck gives that brief no figure at
   all, so a footnote is the wrong rank for it.

   The gold left rule is not new. It is the divider .aj-clients__copy already
   uses to separate a figure from the sentence that explains it, which is the
   same job here, one panel down. */
.aj-case__result {
	margin: 0;
	padding-left: clamp(1rem, 2vw, 1.4rem);
	border-left: 2px solid var(--gold);
	max-width: 52ch;
	font-size: 1.02rem;
	line-height: 1.6;
	color: color-mix(in srgb, var(--ink) 78%, transparent);
}

/* Brief 02 already closed on a figure and a chip; its outcome line follows them
   rather than standing in for them, so it needs the air a new block wants. */
.aj-case__stats + .aj-case__result {
	margin-top: 1.6rem;
}

/* ----- Case type on a colour ground -----
   The rules above were written for paper and lean on --ink and --cocoa. The
   briefs stand on amber, red and ember now, so every one of them has to be
   restated — the same tax .aj-vspot and .aj-imgroup already pay for living on a
   ground their base styles were not written for.

   Alphas are set from measured contrast against the lightest point of each of
   the three grounds, not from the espresso figures: an amber field is far
   brighter than espresso and the .7-ish body alphas that clear AA there do not
   clear it here. Chips get a solid rather than translucent edge for the same
   reason. */
.aj-cpanel .aj-case__label,
.aj-cpanel .aj-case__num {
	color: var(--gold-soft);
}

.aj-cpanel .aj-case__title,
.aj-cpanel .aj-stat__num {
	color: #FFFDF8;
}

.aj-cpanel .aj-case__copy,
.aj-cpanel .aj-guarantee__copy {
	color: rgba(255, 251, 243, .9);
}

/* Not the body alpha above it: this is the sentence each brief is actually
   making, and .9 on a lit amber ground is where it stops competing with the
   headline and starts losing to it. Full strength, and the gold rule beside it
   is what marks it out rather than a difference in tone. */
.aj-cpanel .aj-case__result {
	color: #FFFDF8;
}

/* The hinge into the promise on brief 03 — a step above the body it follows,
   a step under the quote it introduces. */
.aj-cpanel .aj-guarantee__handover {
	color: var(--gold-soft);
}

.aj-cpanel .aj-stat__label,
.aj-cpanel .aj-stat__lead {
	color: var(--gold-soft);
}

.aj-cpanel .aj-chip {
	color: rgba(255, 251, 243, .94);
	border-color: rgba(255, 251, 243, .42);
	background: rgba(23, 18, 16, .22);
}

/* The stat's drawn underline is currentColor on paper; on these grounds the
   number is near-white and a white rule under it reads as an underline rather
   than as the gold accent it is everywhere else. */
.aj-cpanel .aj-stat__num::after {
	background: var(--gold);
}

/* ---------- 09 · Guarantee ----------
   Brief 03. The brisket floats free on the ground rather than sitting in a card,
   the same treatment .aj-imcase__shot gives the influencer product shots.

   Picture left, quote right — mirrored from the old arrangement so the three
   briefs alternate art/copy/art down the page instead of putting two pictures
   on the same side in a row. The grid itself is .aj-cpanel__inner; the only
   thing declared per-panel is the narrower art column. */
.aj-guarantee__lead {
	min-width: 0;
}

.aj-guarantee__quote {
	margin: 0 0 1.6rem;
	padding: 0;
}

/* Down a step from 3.4rem. It used to be the only thing in this column and could
   carry the panel; the deck's copy puts an argument above it and its consequence
   below, and at the old size it stopped reading as a quote inside that argument
   and started reading as a second headline cutting through it. Still the largest
   thing in the column, which is the rank it should have. */
.aj-guarantee__quote p {
	margin: 0;
	font-size: clamp(1.7rem, 3.2vw, 2.7rem);
	font-weight: 800;
	line-height: 1.18;
	letter-spacing: -.02em;
	max-width: 21ch;
}

/* Stacks now — the deck gives this brief two body paragraphs before the quote
   and one after it, where the panel used to hold a single block and could take
   margin: 0. */
.aj-guarantee__copy {
	max-width: 60ch;
	color: rgba(243, 237, 227, .7);
	margin: 0 0 1.1rem;
}

.aj-guarantee__copy:last-child {
	margin-bottom: 0;
}

/* The sentence that hands over to the promise. Sized with the body so it stays
   part of the paragraph flow, but it carries the colon that opens the quote, so
   it is lit and weighted enough to read as a turn rather than as more copy. */
.aj-guarantee__handover {
	max-width: 60ch;
	margin: 1.6rem 0 1.4rem;
	font-weight: 600;
}

/* Briefs 01 and 02 get this clearance from .aj-case__title's own .5rem top
   margin. This column opens on a paragraph instead of a heading, and a paragraph
   brings no top margin, so the label was sitting almost on top of the copy — the
   only place in the set where the number did not have air under it. */
.aj-guarantee__lead .aj-case__label {
	margin-bottom: 1.3rem;
}

/* Base is the stacked shape — a block in the flow above the copy, which is what it
   goes back to being at 940px. The desktop override below lifts it to the panel. */
.aj-guarantee__media {
	position: relative;
	margin: 0;
	display: grid;
	justify-items: center;
	min-width: 0;
}

/* ----- Brief 03's cluster: board, then the pair beside it -----
   This spent one revision anchored to the panel at left:0 and came back inside the
   grid for the same reason brief 01's art did — see the note on .aj-cpanel--jarakan
   .aj-cstage__base. Short version: a board of brisket is a whole object on a stage,
   and it has to travel with the measure or the panel comes apart at 50% zoom.

   Two tracks, 2.2fr to 1fr, which puts the board at 519px at review width — within
   3px of the size it already was — and gives the pair 236. The board keeps the larger
   share because it is the subject and the other two are evidence; an even split would
   make the panel read as three photographs of equal rank, which is not what the copy
   beside it is claiming.

   justify-items has to be reset. The base rule centres its items, which for a stretched
   track means shrink-to-fit — and the only intrinsic width in here is the board's own
   682px, so the cluster would blow past its column. */
.aj-cpanel--lyma .aj-guarantee__media {
	grid-template-columns: minmax(0, 2.2fr) minmax(0, 1fr);
	gap: clamp(.8rem, 1.6vw, 1.6rem);
	align-items: center;
	justify-items: stretch;
}

/* Warm light thrown off the board, sampled from the asset itself, mean rgb(138,85,57).
   Negative inset on the block axis only. A negative inline inset is wider than the page
   gutter and pushes past the viewport — it measured +72px of document width once this
   column went full-bleed at tablet size. The blur does the sideways spreading, and blur
   does not grow the box. Same rule the web-case glow already follows.

   Opened up when the panel took an ember ground of its own. The shot is capped at
   the resolution that was supplied and cannot grow to match the column the way the
   other two cut-outs did, so the glow is what fills the rest of that half — the
   picture reads as the source of the light rather than as a small object in a
   large space. */
.aj-guarantee__glow {
	position: absolute;
	inset: -34% 0;
	z-index: 0;
	pointer-events: none;
	background: radial-gradient(56% 52% at 50% 46%, rgba(232, 146, 84, .42), transparent 72%);
	filter: blur(34px);
	opacity: 0;
	transition: opacity 1.2s var(--ease);
}

/* Centred on the BOARD rather than on the cluster, and weighted left to reach into the
   ground the measure leaves on that side. That is the hierarchy the whole panel turns
   on: the board is the source of the light and the two stills are lit by it, so the
   glow belongs to the board's track and not to the figure as a whole.

   Left reach is the generous one because there is nothing there to protect. Right stops
   short, and the little it does reach is the radial's transparent tail — the stills sit
   in that zone and a warm wash creeping over a photograph would read as a colour cast.

   The old warning about negative inline insets growing the document by 72px still
   applies in principle; it does not bite here because .aj-cpanel clips. */
.aj-cpanel--lyma .aj-guarantee__glow {
	inset: -14% -6% -14% -46%;
	background: radial-gradient(52% 48% at 46% 50%, rgba(232, 146, 84, .44), transparent 74%);
	filter: blur(42px);
}

html:not(.js) .aj-guarantee__glow,
.aj-guarantee__glow.is-in {
	opacity: 1;
}

.aj-guarantee__media .aj-vplx {
	position: relative;
	z-index: 1;
}

/* A grid item again, stretched to its track, so the board's size is decided by the
   track and nothing else — no height fractions, no viewport caps, nothing that can
   disagree with where the copy is. The track is what travels with the measure. */
.aj-cpanel--lyma .aj-guarantee__media .aj-vplx {
	width: 100%;
}

/* THE ONE ASSET WITH NO HEADROOM. The supplied source is 413x604, but 20px of
   that is transparent margin on every side — the content bounding box is exactly
   341x496, which is what the shipped WebP already holds. Re-encoding recovers
   nothing; it was measured rather than assumed.
   (scratchpad/reencode-lyma.js: "No gain — the shipped file already holds all
   the content pixels.")

   ROUND TWO. The client has asked twice for this board to be bigger. The first pass
   answered with a 1.23x browser upscale of the 341px file. This one answers
   differently: the FILE now ships at 682x992, resampled 2x offline with a mild
   unsharp, and the alpha had to be split off to do it — convolving a cut-out
   directly puts a bright halo round the silhouette, because the kernel's negative
   weights read the transparent pixels' black as if it were neighbouring picture.
   (scratchpad/export-briefs.php holds the pipeline: dilate, upscale, sharpen,
   recombine.)

   None of that invents detail. What it buys is control of the interpolation and a
   render that is a DOWNSCALE — 516 CSS px out of a 682px file, where before it was
   420 out of 341. On a 2x display that is 1.5x of upscaling instead of 3x.

   So this board grew by a quarter where the other two panels' art grew by half, and
   the rest of its half of the panel is carried by the light: the glow is opened up
   and weighted left precisely because the picture cannot fill that ground itself.
   Supply the original brisket and two numbers change — this file goes back to
   native, and --shot-h can stop being polite.

   Sized entirely by the wrapper, and it has to be: the wrapper is stretched to its grid
   track, so 100% here resolves against the track. Asking for the size on the image
   itself is what does not work — its parent's width would then come from this same
   image, and that circular reference silently collapses to the intrinsic 682px. */
.aj-guarantee__shot {
	display: block;
	width: 100%;
	height: auto;
	/* Deeper than it was, for the same reason the family's is: half again the area,
	   and this one is suspended rather than resting, so the shadow is the only thing
	   saying how far off the ground it floats. */
	filter: drop-shadow(0 46px 56px rgba(0, 0, 0, .58));
}

/* ----- The pair beside the board -----
   Two photographs the client supplied of LYMA's food, cropped square. Square because
   two identical frames read as a deliberate pair where two different aspects read as
   two loose photos, and because it is the one crop that keeps all of both subjects —
   both sources are 1440x2160 with the food in the middle third.

   One wrapper, one parallax factor, and that is the point of the wrapper: separate
   factors would pull a stacked pair apart as the reader scrolls. The Jasa Raharja wall
   made exactly that mistake once, with each frame carrying its own drift, and the row
   never lined up. -0.016 against the board's -0.03 puts the pair a layer behind it.

   align-content centres the stack against the board rather than filling the track: the
   pair is shorter than the board and should read as nested inside its span, not as a
   second column of equal weight. */
.aj-gstills {
	/* Positioned for the same reason .aj-vplx is, and it is not decoration: the glow is
	   absolute at z-index 0, which paints after in-flow content. A static wrapper here
	   would put warm light over the top of both photographs. */
	position: relative;
	z-index: 1;
	display: grid;
	gap: clamp(.6rem, 1.2vw, 1.2rem);
	align-content: center;
	min-width: 0;
}

/* Rectangular, cornered and edged — the language the whole site uses for a
   photograph, and the thing that keeps these from being mistaken for cut-outs like the
   board they stand next to. Three quarters of --radius rather than all of it: 22px is
   the site's card radius, drawn for cards several times this size, and at 236px square
   it rounds a photograph into a badge.

   The border is what holds the edge on a dark ground. Without it the darker corners of
   both crops dissolve into the ember field and the frames stop being frames. */
.aj-gstill {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 1;
	object-fit: cover;
	border-radius: calc(var(--radius) * .75);
	border: 1px solid rgba(255, 251, 243, .16);
	box-shadow: 0 22px 38px -14px rgba(0, 0, 0, .55);
}

/* .aj-guarantee__caption is gone with "Fifteen hours, slow smoked" — a caption
   written here to describe the photograph, not a line from the client's deck.
   The <figure> keeps its <figcaption>-less form: the brisket is the ground's
   subject, and the copy beside it already says what the brief is about. */

/* ---------- 09b · Brief 03 ----------
   LYMA used to be its own section, and the hairline treatment that framed it was
   .aj-cdark. All three briefs are full-bleed panels now, so that job moved up to
   .aj-cpanel — one component draws the seams for all of them, which is what makes
   amber → red → ember read as three chapters instead of one dark stretch.

   What is left here is the quote's own type. Stepped down from 3.4rem. At that size it measured within a hair of
   .aj-display and read as a second section heading — which is the exact
   problem this restructure exists to remove. It loses nothing: it is still the
   only large type on its own ground, still carries the gold highlighter, and
   now has a numeral and a rule introducing it.

   The measure goes up as the size comes down. At 24ch the smaller type broke
   after "get" and left "achieved," alone on a line, which puts a hard stop in
   the middle of the sentence the whole panel is built around. Wide enough for
   the clause to hold, and balance keeps the two lines even. */
.aj-guarantee__quote p {
	font-size: clamp(1.7rem, 3.4vw, 2.8rem);
	max-width: 34ch;
	text-wrap: balance;
}
/* ---------- 10 · Clients ----------
   Tall section, sticky stage — the same construction as the hero, for the same
   reason: the extra height is scroll runway, and motion.js turns the reader's
   scroll into one full pass of the roster before releasing the page.

   The runway only exists once motion.js has confirmed it will actually drive the
   scrub. That class gate is what keeps no-JS, reduced motion, touch and short
   viewports from inheriting two screens of empty cream they have no use for. */
.aj-clients {
	position: relative;
	background: var(--cream);
	padding-block: calc(var(--section) * .8);
	overflow: clip;
}

.aj-clients.is-pinned {
	/* height comes from motion.js — it is measured from the real width of one
	   roster pass, so a longer client list buys a longer pin rather than a
	   faster one. */
	padding-block: 0;
}

.aj-clients.is-pinned .aj-clients__stage {
	position: sticky;
	top: 0;
	height: 100svh;
	display: flex;
	flex-direction: column;
	justify-content: center;
	/* The nav floats over the top of it while pinned. */
	padding-top: var(--nav-h);
	box-sizing: border-box;
}

/* width: 100%, and it is load-bearing. .aj-container centres itself with
   margin-inline: auto, and an auto inline margin on a FLEX ITEM makes it
   shrink-to-fit — the head collapsed to the width of its own two paragraphs and
   floated into the middle of the screen, 340px off the measure every other
   section uses. A definite width turns the auto margins back into ordinary
   centring. Same trap .aj-mat hit as a grid item in Our Family. */
.aj-clients.is-pinned .aj-clients__stage > * {
	width: 100%;
}

/* The number and the sentence that finishes it are one statement, so they sit as
   one block. space-between threw them to opposite ends of a 1800px measure — about
   820px of nothing between a figure and the line that explains it. */
.aj-clients__head {
	display: flex;
	align-items: stretch;
	justify-content: flex-start;
	gap: 0;
	flex-wrap: wrap;
	margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
}

.aj-clients__num {
	margin: 0;
	font-size: clamp(4rem, 9vw, 7.5rem);
	font-weight: 800;
	line-height: .9;
	letter-spacing: -.04em;
}

.aj-clients__num span {
	color: var(--gold-deep);
}

/* The gold rule is the divider: the same border-left the meta columns use, so the
   page has one way of separating a figure from its caption rather than two. */
.aj-clients__copy {
	display: flex;
	align-items: flex-end;
	margin: 0;
	padding-left: clamp(1.3rem, 3vw, 2.6rem);
	margin-left: clamp(1.3rem, 3vw, 2.6rem);
	border-left: 1px solid rgba(169, 139, 87, .45);
	max-width: 34ch;
	font-size: 1.05rem;
	color: color-mix(in srgb, var(--ink) 72%, transparent);
}

@media (max-width: 700px) {
	/* Stacked, a left rule would hang off nothing. */
	.aj-clients__copy {
		margin: 1.2rem 0 0;
		padding-left: 0;
		border-left: 0;
	}
}

/* Padding grew with the marks. The row's hover state lifts a logo 4px and the
   mask crops anything that leaves the box, so the clearance has to hold the
   tallest mark plus its travel. */
.aj-marquee {
	overflow: hidden;
	padding-block: 1.2rem;
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}

.aj-marquee__track {
	display: flex;
	width: max-content;
	animation: aj-marquee 38s linear infinite;
}

.aj-marquee--reverse .aj-marquee__track {
	animation-direction: reverse;
	animation-duration: 46s;
}

.aj-marquee:hover .aj-marquee__track {
	animation-play-state: paused;
}

/* ----- Scrubbed roster -----
   Once the pin is armed the keyframe hands over to motion.js, which writes --rp
   in pixels: scroll progress plus a slow idle drift, taken modulo one pass. The
   modulo is what makes it loop as seamlessly as the keyframe did; the drift is
   what stops the row freezing dead if the reader stops mid-pin.

   Hover no longer pauses it — while pinned the reader is the one driving, and a
   row that stops because the pointer happens to be resting on it reads as a
   fault. */
.aj-clients.is-pinned .aj-marquee__track {
	animation: none;
	transform: translate3d(calc(var(--rp, 0) * -1px), 0, 0);
	will-change: transform;
}

.aj-clients.is-pinned .aj-marquee:hover .aj-marquee__track {
	animation-play-state: running;
}

/* Only shown while the pin is armed: it is an instruction, and an instruction
   for behaviour the reader is not getting is worse than no instruction. Fades
   out as the pass completes — motion.js writes --rprog 0→1. */
.aj-clients__cue {
	display: none;
}

.aj-clients.is-pinned .aj-clients__cue {
	display: block;
	margin: clamp(1.6rem, 3vw, 2.4rem) 0 0;
	text-align: center;
	font-family: var(--font-mono);
	font-size: .68rem;
	letter-spacing: .22em;
	text-transform: uppercase;
	color: var(--cocoa);
	opacity: calc(1 - var(--rprog, 0) * 2.2);
}

.aj-clients.is-pinned .aj-clients__cue::after {
	content: "";
	display: block;
	width: clamp(4rem, 12vw, 9rem);
	height: 1px;
	margin: .8rem auto 0;
	background: rgba(169, 139, 87, .35);
	background-image: linear-gradient(90deg, var(--gold-deep), var(--gold-deep));
	background-repeat: no-repeat;
	background-size: calc(var(--rprog, 0) * 100%) 100%;
}

.aj-marquee__half {
	display: flex;
	align-items: center;
	gap: 2.6rem;
	list-style: none;
	margin: 0;
	padding: 0 2.6rem 0 0;
}

/* Sized up with the marks beside them — a typed name at the old 1.6rem next to a
   76px logo read as a caption rather than as another client. Moved again by the
   same factor as the marks when those grew 50%: the six clients who arrived
   without a logo file are not lesser clients, and the row only keeps saying so
   while type and art stay in step. */
.aj-marquee__item {
	font-weight: 700;
	font-size: clamp(2rem, 3.6vw, 2.9rem);
	letter-spacing: -.01em;
	color: rgba(42, 33, 27, .48);
	white-space: nowrap;
	transition: color .3s;
}

/* ----- Logo roster -----
   One row now, so the height the second row was spending goes into the marks.
   The art is monochrome, so opacity alone does the resting/hover states.

   Gap moved with the marks. Held at the old value the row stopped reading as a
   list of clients and started reading as a solid band of art — at 114px tall the
   marks need proportionally the same air between them that they had at 76px. */
.aj-marquee--logos .aj-marquee__half {
	gap: clamp(2.8rem, 5.6vw, 5.2rem);
	padding-right: clamp(2.8rem, 5.6vw, 5.2rem);
}

.aj-marquee__logo {
	display: flex;
	align-items: center;
}

/* .58 was tuned for a 48px mark. At 76px the same alpha reads washed out rather
   than restrained, because there is more of it to see.
   ---
   Raised 50% at the client's request: the marks were too small to be legible as
   brands at a glance, which is the whole job of this row. Every stop of the clamp
   moved by the same factor so the growth is uniform rather than a bigger cap with
   the same floor — 54→82px on a phone, 76→114px on a wide screen. Alpha went up
   with it: past ~100px the mark is a shape being read rather than a texture being
   scanned, and .66 left the finer wordmarks (Cakap, Republikorp) looking faded. */
.aj-marquee__logo img {
	width: auto;
	height: clamp(5.1rem, 7.8vw, 7.13rem);
	opacity: .72;
	transition: opacity .4s var(--ease), transform .4s var(--ease);
}

.aj-marquee__logo:hover img {
	opacity: 1;
	transform: translateY(-4px);
}

.aj-marquee__item:hover {
	color: var(--ink);
}

.aj-marquee__dot {
	color: var(--gold-deep);
	font-size: 1.2em;
	line-height: 1;
}

/* Travels exactly one set, however many sets the track ends up holding. The old
   -50% assumed there would always be two; motion.js clones more when one set is
   narrower than the viewport, and writes the count here. */
@keyframes aj-marquee {
	to { transform: translateX(calc(-100% / var(--sets, 2))); }
}

/* ---------- 11 · Services ---------- */
.aj-services {
	background: var(--paper);
}

.aj-services__list {
	list-style: none;
	margin: 0;
	padding: 0;
	border-top: 1px solid rgba(42, 33, 27, .12);
}

.aj-service {
	display: grid;
	grid-template-columns: 5rem 1fr;
	gap: 1rem 2.5rem;
	padding: 2.2rem 0 2.4rem;
	border-bottom: 1px solid rgba(42, 33, 27, .12);
}

.aj-service__num {
	font-family: var(--font-mono);
	font-size: .85rem;
	letter-spacing: .1em;
	color: var(--gold-deep);
	padding-top: .55rem;
}

.aj-service h3 {
	margin: 0 0 .5rem;
	font-size: clamp(1.35rem, 2.2vw, 1.8rem);
	font-weight: 700;
	letter-spacing: -.015em;
}

.aj-service p {
	margin: 0 0 1.1rem;
	color: color-mix(in srgb, var(--ink) 72%, transparent);
	max-width: 56ch;
}

.aj-service__tags {
	display: flex;
	flex-wrap: wrap;
	gap: .5rem;
	list-style: none;
	margin: 0;
	padding: 0;
}

.aj-service__tags li {
	font-family: var(--font-mono);
	font-size: .68rem;
	letter-spacing: .1em;
	text-transform: uppercase;
	padding: .55em .9em;
	border: 1px solid rgba(42, 33, 27, .18);
	border-radius: 999px;
	color: rgba(42, 33, 27, .62);
	transition: border-color .3s, color .3s;
}

.aj-service:hover .aj-service__tags li {
	border-color: rgba(169, 139, 87, .55);
	color: var(--cocoa);
}

/* ---------- 12 · Results ----------
   The scoreboard that used to live here was replaced by the account panels in
   section 22: the overture states the aggregate, and each brand's numbers now sit
   in its own panel rather than being quoted twice. Only the section footnote is
   still shared. */
.aj-results__note {
	margin: 2rem 0 0;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .45);
}

/* ---------- 13 · Footer ---------- */
/* The hairline is for /work-services/, which ends on Web Development — espresso
   meeting espresso, with nothing to say where the page stopped and the footer
   started. It costs nothing on the two pages that arrive here off cream, where
   it reads as a rule drawn under the site rather than as a repair. */
.aj-footer {
	position: relative;
	background: var(--espresso);
	color: var(--light);
	padding-top: var(--section);
	border-top: 1px solid rgba(209, 177, 128, .12);
	overflow: clip;
}

.aj-footer__glow {
	position: absolute;
	inset: 0;
	background:
		radial-gradient(46% 38% at 50% 0%, rgba(209, 177, 128, .13), transparent 65%);
	pointer-events: none;
}

.aj-footer__cta {
	position: relative;
	text-align: center;
	display: flex;
	flex-direction: column;
	align-items: center;
}

.aj-footer__title {
	margin: .2rem 0 1.2rem;
	font-size: clamp(3rem, 9vw, 7rem);
	font-weight: 800;
	letter-spacing: -.035em;
	line-height: .95;
	background: linear-gradient(115deg, #E8D6AF 5%, #D1B180 45%, #B3925F 90%);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
	padding-bottom: .06em;
}

.aj-footer__sub {
	margin: 0 0 2.2rem;
	font-size: 1.15rem;
	max-width: 40ch;
	color: rgba(243, 237, 227, .7);
}

.aj-footer__actions {
	display: flex;
	gap: 1rem;
	flex-wrap: wrap;
	justify-content: center;
	margin-bottom: 1.6rem;
}

.aj-footer__site {
	margin: 0;
}

.aj-footer__site a {
	color: var(--gold);
	font-family: var(--font-mono);
	font-size: .92rem;
	letter-spacing: .06em;
	text-decoration: none;
	border-bottom: 1px solid rgba(209, 177, 128, .4);
	transition: border-color .3s;
}

.aj-footer__site a:hover {
	border-color: var(--gold);
}

.aj-footer__bar {
	position: relative;
	margin-top: clamp(3.5rem, 7vw, 5.5rem);
	border-top: 1px solid rgba(243, 237, 227, .12);
	padding: 1.4rem 0 1.6rem;
}

.aj-footer__bar-inner {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1rem;
	flex-wrap: wrap;
	font-size: .8rem;
	color: rgba(243, 237, 227, .5);
}

.aj-footer__bar-inner p {
	margin: 0;
}

.aj-footer__tag {
	font-family: var(--font-mono);
	letter-spacing: .08em;
}

/* The tagline is the right-hand item of a space-between row, and the WhatsApp
   disc is fixed to that same corner — so the last words of the line were sitting
   under it on every wide screen. The tagline steps aside by the disc's own
   width.

   On the tagline, not as padding on the row: .aj-container is content-box, so
   padding widens the box rather than pulling its contents in — the row simply
   grew past the viewport and took the tagline most of the way back out with it.
   A margin on the flex item itself moves the item, which is what was wanted.

   The disc only, not its hover label: the label is 130-odd pixels more, it is
   only on screen while the pointer is on the button, and nobody reads a footer
   line they are currently hovering away from. Reserving for it would cost the
   tagline a quarter of the bar to solve a case that cannot be looked at. */
/* 1160px is measured, not chosen. The row holds two fixed strings — the copyright
   renders at 325px, the mono tagline at 567px — plus the disc's ~120px reserve.
   Against .aj-container's 92% of the viewport, the three stop fitting at about
   1115px, and flex-wrap drops the tagline onto a second, left-aligned line. That
   is the same broken axis the phone layout had: two lines ranged left under a
   block that is centred. It was simply happening 475px higher up than the width
   it was reported at.

   The switch is set 45px above the crossover so the row is never merely fitting:
   at 1160 there are ~55px of clear space between the two lines, which is a gap
   rather than a near miss.

   Both numbers are properties of these two strings. Rewrite either line and this
   threshold has to be re-measured, not guessed. */
@media (min-width: 1160px) {
	/* Qualified by the row, because .aj-footer__bar-inner p above zeroes the
	   margin on both lines and a bare .aj-footer__tag loses to it on
	   specificity — silently, which is the expensive kind. */
	.aj-footer__bar-inner .aj-footer__tag {
		margin-right: var(--fab-clear);
	}
}

@media (max-width: 1159px) {
	/* column-reverse puts the brand line above the copyright, which is the order
	   they rank in. Both are static text with no focus or interaction, so the
	   visual/DOM mismatch costs nothing to read out. */
	.aj-footer__bar-inner {
		flex-direction: column-reverse;
		align-items: center;
		justify-content: center;
		text-align: center;
		gap: .75rem;
	}

	/* .08em of tracking on a line that has just started wrapping is what made it
	   ragged. Pulled in, given a measure, and given the eyebrow's own 2.2rem gold
	   rule — which is what turns two orphaned sentences into a signed-off block
	   rather than two leftovers. */
	.aj-footer__tag {
		font-size: .68rem;
		letter-spacing: .06em;
		line-height: 1.7;
		/* Two lines at 640, one at 1100, and narrower screens are capped by the
		   container before this ever binds. A tighter measure broke a
		   63-character sentence into three, which is one line more than a
		   sign-off can carry without turning back into a paragraph. */
		max-width: 42ch;
	}

	.aj-footer__tag::before {
		content: "";
		display: block;
		width: 2.2rem;
		height: 1px;
		margin: 0 auto .85rem;
		background: var(--gold-deep);
	}

	.aj-footer__bar-inner p:not(.aj-footer__tag) {
		font-size: .72rem;
		color: rgba(243, 237, 227, .42);
	}
}

/* The disc sits about 90px in from the right edge. Centred footer lines grow with
   the viewport, so they only reach under it on small screens — measured, they
   cross at roughly 600px. Above that the bar is already clear and this reserve
   would be 90px of empty dark for nothing. */
@media (max-width: 600px) {
	.aj-footer__bar {
		padding-bottom: calc(var(--fab-size) + var(--fab-inset) + .9rem);
	}
}

/* ---------- 14 · Responsive ---------- */
@media (max-width: 1080px) {
	/* The quote needs the full measure before the brisket squeezes it. */
	.aj-guarantee__inner {
		grid-template-columns: minmax(0, 1fr);
		gap: clamp(2.4rem, 5vw, 3.4rem);
	}

	.aj-guarantee__media {
		justify-items: start;
	}
}

@media (max-width: 940px) {
	.aj-cpanel__inner,
	.aj-cpanel--flip .aj-cpanel__inner,
	.aj-cpanel--lyma .aj-cpanel__inner {
		grid-template-columns: minmax(0, 1fr);
		gap: clamp(2.6rem, 6vw, 3.4rem);
	}

	/* Art always leads when stacked, whichever side it took on desktop —
	   alternating the order down a single column reads as a mistake, not as
	   rhythm. */
	.aj-cpanel--flip .aj-cstage {
		order: 0;
	}

	/* justify-self, not margin-inline:auto. An auto inline margin on a grid item makes it
	   shrink-to-fit, and the only thing here with an intrinsic width is the caption — the
	   stage collapsed to 230px, the width of its own tag text. */
	.aj-cstage {
		width: 100%;
		max-width: 560px;
		justify-self: center;
		/* The tag hangs below the stage; stacked, it needs the room the text
		   column used to give it. */
		margin-bottom: 2.6rem;
	}

	/* Stacked, the proof cannot be as tall as the section any more: the section is
	   art on top of four blocks of copy here, and a full-height phone would stand
	   behind the paragraphs. It keeps both the things the client asked for — flush
	   to the edge, the dominant object on its side — and gets driven by width
	   instead of by height.

	   The top feather comes off with it, and that is the point rather than an
	   omission. Feathering the top exists to stop a straight cut landing on the
	   crossfade into Jarakan; stacked, the phone starts at the panel's own top
	   padding with red above it, so its top edge is not at a join at all. It reads
	   as the edge of a card, which is what it read as here before any of this. A
	   fade there would cost the Karunia Semesta row instead: at 300px tall the row
	   is only 39px down, and any falloff deep enough to be worth having eats it.

	   The bottom keeps its falloff. That edge is behind the band, and letting it
	   dissolve there is what stops the two reading as two flat cut-outs. */
	.aj-cproof {
		top: clamp(4rem, 8vw, 7rem);
		height: auto;
		width: min(74%, 460px);
		--proof-mask: linear-gradient(180deg, #000 0, #000 82%, transparent 100%);
	}

	/* The stage goes square only on this panel, and only to move the band. The band
	   is bottom-anchored inside it, so a taller box drops its heads clear of the
	   phone's Karunia Semesta row instead of standing them in front of it — the one
	   part of the proof that has to stay readable. The band itself keeps its size:
	   at 78% of a 560px stage it is width-bound either way, so the cap below only
	   decides which dimension governs, not how big it comes out. */
	.aj-cpanel--changcuters .aj-cstage {
		aspect-ratio: 1 / 1;
	}

	.aj-cpanel--changcuters .aj-cstage__base {
		max-height: 76%;
	}

	/* Brief 01's overhang comes off when the art is centred in a single column: at
	   -6% of a 560px stage the group hangs 34px past the left edge of a box that is
	   already only as wide as the screen, which reads as a crop rather than as reach.
	   The lamb's does too, for the same reason — there is no gutter to hang into. */
	.aj-cpanel--jarakan .aj-cstage__base {
		left: 0;
	}

	.aj-cpanel--jarakan .aj-cstage__plx {
		right: 0;
	}

	/* ----- Brief 03's cluster, stacked -----
	   Board above, pair below it side by side. Beside-and-nested is a wide-layout
	   arrangement: in one column the board would be a third of the width of its own
	   figure and the two stills would be thumbnails. Turned through ninety degrees the
	   board keeps most of the width and the pair reads as a caption strip under it. */
	.aj-cpanel--lyma .aj-guarantee__media {
		grid-template-columns: minmax(0, 1fr);
		justify-items: center;
		gap: clamp(1rem, 3vw, 1.6rem);
	}

	.aj-cpanel--lyma .aj-guarantee__media .aj-vplx {
		width: min(78%, 420px);
	}

	.aj-gstills {
		grid-template-columns: 1fr 1fr;
		width: min(78%, 420px);
	}

	/* Pulled in with it. The wide layout spends the glow on ground the measure leaves
	   over; stacked there is no such ground — the copy is directly underneath, and a
	   -46% reach would put warm light behind the first paragraph. */
	.aj-cpanel--lyma .aj-guarantee__glow {
		inset: -14% -20%;
		filter: blur(34px);
	}

	.aj-guarantee__media {
		justify-self: center;
	}
}

@media (max-width: 880px) {
	.aj-nav__toggle {
		display: inline-flex;
	}

	html.menu-open {
		overflow: hidden;
	}

	/* Half the distance between bar centres: 6.5px gap + 2px bar. Hardcoded
	   because it has to follow the gap above — if that changes, this does too,
	   or the two strokes cross off-centre. */
	html.menu-open .aj-nav__toggle span:nth-child(1) {
		transform: translateY(8.5px) rotate(45deg);
	}

	html.menu-open .aj-nav__toggle span:nth-child(2) {
		opacity: 0;
	}

	html.menu-open .aj-nav__toggle span:nth-child(3) {
		transform: translateY(-8.5px) rotate(-45deg);
	}

	/* The bar's own list is a desktop object now and nothing more. It used to
	   double as the mobile panel — fixed, inset:0, nested inside a bar that
	   takes a backdrop-filter on scroll, which is exactly the arrangement that
	   made the menu collapse into the header. The panel lives in section 05b
	   now, outside this element, and this simply steps aside. */
	.aj-nav__links {
		display: none;
	}

	/* No glass over the drawer. The bar keeps its z-index so the lockup and the
	   hamburger stay above the panel, but a blurred, tinted strip across the top
	   of a surface that has its own ground reads as a seam. Transitioned on the
	   same .4s the bar already uses for .is-scrolled, so closing the menu brings
	   the glass back at the speed it left rather than snapping. */
	html.menu-open .aj-nav {
		background: transparent;
		-webkit-backdrop-filter: none;
		backdrop-filter: none;
		box-shadow: none;
	}
}

@media (max-width: 820px) {
	.aj-trifecta__grid {
		grid-template-columns: 1fr;
	}

	/* Stacked, the grid is three plates tall and a -20% vertical inset would push
	   the aurora most of a screen past each end — a wash over the section rather
	   than a pool of light under the set. It follows the column in instead. */
	.aj-trifecta__grid::before {
		inset: -5% -10%;
		background:
			radial-gradient(60% 42% at 50% 50%, rgba(209, 177, 128, .16), transparent 70%),
			radial-gradient(52% 26% at 50% 12%, rgba(209, 177, 128, .10), transparent 72%),
			radial-gradient(52% 26% at 50% 88%, rgba(209, 177, 128, .10), transparent 72%);
	}

	.aj-tcard {
		padding: 2.2rem 1.9rem 2.4rem;
	}

	/* The stacked entrance is not declared here. It has to override the card's
	   hidden state, which lives in section 22 further down the file at the same
	   specificity — so it is written there, in its own 820px query, where the
	   cascade puts it after what it is overriding. */
}

@media (max-width: 640px) {
	.aj-service {
		grid-template-columns: 1fr;
		gap: .4rem;
	}

	.aj-service__num {
		padding-top: 0;
	}

	.aj-hero__foot .aj-hero__tagline {
		display: none;
	}

	/* The tagline was the thing holding the left end of the foot row, so with it gone the
	   auto margin would shove the cluster against the Scroll cue and leave half the row
	   empty. Deck on the left, Scroll on the right — space-between does the rest. */
	.aj-hero__ctl {
		margin-left: 0;
		gap: .55rem;
	}

	.aj-hero__ticks {
		gap: .35rem;
	}

	/* Prev and next go: a phone already has the gesture, and three 34px discs plus four
	   ticks plus the Scroll cue do not fit a 360px row without one of them wrapping. The
	   pause control stays — it is the accessibility floor, not a convenience. */
	.aj-hero__btn:not(.aj-hero__btn--toggle) {
		display: none;
	}

	/* ----- The footer, on a phone -----
	   Everything below fixes one fault: the block was built for a wide row and
	   then let go at narrow widths rather than rebuilt for them. The CTA stayed
	   centred while the bar under it fell to the left, the two pills went
	   edge-to-edge and lost their ranking, and the type kept sizes that were
	   chosen against 1440px of measure. */

	/* --section is 5.5rem at its floor, which is a desktop's idea of generous.
	   The eyebrow already carries the entrance. */
	.aj-footer {
		padding-top: clamp(4rem, 13vw, 5.5rem);
	}

	/* The desktop clamp bottoms out at 3rem and stops moving below 533px, so a
	   360px phone gets the same 48px display a 530px one does. Re-clamped against
	   the widths that actually occur here: 2.4rem at 320, 3.4rem by 640. */
	.aj-footer__title {
		font-size: clamp(2.4rem, 11vw, 3.4rem);
		margin-bottom: 1rem;
	}

	.aj-footer__sub {
		font-size: 1.02rem;
		max-width: 30ch;
		margin-bottom: 1.8rem;
	}

	/* Capped, not full-bleed. width:100% put a 26-character address in the middle
	   of a 537px pill with 180px of dead space either side — and overflowed it
	   outright below 360px, where the label plus its em padding is wider than the
	   container. 22rem is the measure the pair reads as a pair at. */
	.aj-footer__actions {
		flex-direction: column;
		width: min(100%, 22rem);
		gap: .7rem;
		margin-bottom: 1.3rem;
	}

	.aj-footer__actions .aj-btn {
		width: 100%;
		font-size: .92rem;
		padding: 1.05em 1.2em;
	}

	/* Warm rather than neutral. At full width the outlined pill read as an empty
	   input; at 22rem it reads as a button again, and a gold-tinted edge ranks it
	   as the second of two rather than as an unstyled one. */
	.aj-footer__actions .aj-btn--ghost {
		border-color: rgba(209, 177, 128, .30);
	}

	.aj-footer__site a {
		font-size: .86rem;
	}

	/* The bar's own layout is handled at 1100 — see section 13, where the row
	   stops fitting. All this width has to do is tighten the join. */
	.aj-footer__bar {
		margin-top: clamp(2.6rem, 9vw, 5.5rem);
		padding-top: 1.3rem;
	}
}

/* The last width where the address still has to fit. 320px leaves 254px of
   container, and .92rem of Poppins plus 1.2em of padding either side is just
   over it. */
@media (max-width: 380px) {
	.aj-footer__actions .aj-btn {
		font-size: .84rem;
		padding: 1.05em .85em;
	}
}

/* ---------- 15 · Video Showcase ---------- */
/* ----- The brown field -----
   Rebuilt in CSS from the deck's marbled artwork rather than shipping the
   1.25 MB source: the palette is sampled from it (#5D3327 base through to the
   #905F48 highlight), and building it from gradients means it can drift.
   Two highlight layers travel on different periods so the loop never lands on
   the same frame twice. It is a backdrop — the movement stays barely there. */
/* Highlight alphas are held down deliberately: at full strength the lightest
   point of the field only gave body copy 3.2:1. Kept here, the field still
   reads as marbled but never brightens past what light type can sit on. */

/* ----- The run owns the ground, not the chapters -----
   This block used to sit on .aj-video--field, which every chapter section
   carries — so the four chapters rendered four copies of it, stacked. Each copy
   restarted the linear leg, and every join put one chapter's tail (#5D3327,
   dark) directly against the next one's head (#673B2D, light). That step is the
   line the client pointed at.

   Nothing here is a blend or a patch: the ground simply moved up to the element
   that actually spans the run, and the chapters went transparent. There is no
   join between 01 and 02 to hide, because there is no longer a boundary there —
   it is one field with four sections of content standing on it.

   FIVE HIGHLIGHTS, NOT TWO. Two radials were mixed for a ~1,000px box. Spread
   over the ~4,100px run they would leave the middle flat and the ends lit, so
   the same texture is carried by five, seeded down the length at 12/30/52/71/88%
   and alternating side to side. Deliberately NOT a repeating background: a tile
   that repeats every N pixels would draw a new set of lines exactly where this
   change removes the old ones.

   The drift keeps its two periods. Total animated area is unchanged — the same
   pixels move, they just belong to one element now instead of four. */
.aj-vrun {
	position: relative;
	isolation: isolate;
	background:
		radial-gradient(38% 12% at var(--f1x, 22%) calc(12% + var(--fdy, 0%)), rgba(144, 95, 72, .5), transparent 68%),
		radial-gradient(34% 11% at var(--f2x, 82%) calc(30% - var(--fdy, 0%)), rgba(122, 75, 56, .45), transparent 66%),
		radial-gradient(40% 13% at var(--f1x, 22%) calc(52% + var(--fdy, 0%)), rgba(150, 100, 76, .42), transparent 68%),
		radial-gradient(34% 11% at var(--f2x, 82%) calc(71% - var(--fdy, 0%)), rgba(122, 75, 56, .45), transparent 66%),
		radial-gradient(38% 12% at var(--f1x, 22%) calc(88% + var(--fdy, 0%)), rgba(144, 95, 72, .46), transparent 68%),
		linear-gradient(168deg, #673B2D, #5D3327 78%);
}

/* One grain layer for the whole run — the chapters' own copies are switched off
   below, so the texture no longer restarts at each section either. */
.aj-vrun::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image: var(--grain);
	opacity: .05;
	mix-blend-mode: soft-light;
	pointer-events: none;
}

html.js .aj-vrun {
	animation: aj-field-a 38s var(--ease) infinite alternate;
}

html.js .aj-vrun::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background:
		radial-gradient(34% 12% at var(--f3x, 62%) 22%, rgba(160, 108, 82, .28), transparent 64%),
		radial-gradient(30% 11% at var(--f3x, 62%) 62%, rgba(160, 108, 82, .24), transparent 64%);
	animation: aj-field-b 52s var(--ease) infinite alternate;
	pointer-events: none;
}

/* The chapters stand on the run now. .aj-section--dark would otherwise paint
   espresso over the field, and its grain pseudo would stack a second copy of the
   texture on top of the run's own. */
.aj-vrun .aj-vchapter,
.aj-vrun .aj-vchapter.aj-section--dark {
	background: none;
}

.aj-vrun .aj-vchapter.aj-section--dark::after {
	display: none;
}

.aj-video--field {
	position: relative;
}

.aj-video--field .aj-lead,
.aj-video--field .aj-vframe__note {
	color: rgba(243, 237, 227, .85);
}

/* --fdy is a single shared vertical offset rather than a y per highlight: five
   seeds each drifting on their own value would slide past each other and the
   spacing that keeps the run evenly lit would come apart. One offset moves the
   whole set together, in opposite directions on alternating seeds. */
@keyframes aj-field-a {
	to {
		--f1x: 34%;
		--f2x: 70%;
		--fdy: 2.4%;
	}
}

@keyframes aj-field-b {
	to {
		--f3x: 44%;
	}
}

/* Gradient positions only animate when the browser knows they are lengths. */
@property --f1x { syntax: "<percentage>"; initial-value: 22%; inherits: false; }
@property --f2x { syntax: "<percentage>"; initial-value: 82%; inherits: false; }
@property --f3x { syntax: "<percentage>"; initial-value: 62%; inherits: false; }
@property --fdy { syntax: "<percentage>"; initial-value: 0%; inherits: false; }

/* The overture's index list was built for paper — restate it for the field. */
.aj-video--field .aj-vindex {
	border-top-color: rgba(243, 237, 227, .18);
}

.aj-video--field .aj-vindex li {
	border-bottom-color: rgba(243, 237, 227, .18);
}

.aj-video--field .aj-vindex__name {
	color: var(--light);
}

.aj-video--field .aj-vindex a:hover .aj-vindex__name {
	color: var(--gold);
}

.aj-video--field .aj-vindex__num {
	color: var(--gold);
}

.aj-video--field .aj-vindex__meta,
.aj-video--field .aj-vchapter__clients {
	color: rgba(243, 237, 227, .55);
}

.aj-video--field .aj-vindex a::after {
	background: var(--gold);
}

/* One overture (chapter index) followed by four chapter sections, each
   composed like its slide in the company profile deck. All of them sit on the
   brown field above — the old paper/cream alternation is gone. */

/* The overture is paper now, not the brown field: it is the showcase's head, and
   heads stand on light ground here. It no longer hands over to chapter 01 on the
   same colour, so it takes a full close rather than the clipped one it used when
   the two grounds were continuous. .aj-vindex needs no overrides on paper — the
   base style was written for exactly this ground. */
.aj-video--overture {
	background: var(--paper);
	padding-bottom: clamp(3.5rem, 7vw, 6rem);
}

.aj-video--overture .aj-section-head {
	margin-bottom: 0;
}

.aj-vchapter {
	padding-block: clamp(3.2rem, 6.5vw, 5.6rem);
}

/* ----- Chapter index (overture) ----- */
.aj-vindex {
	list-style: none;
	margin: clamp(2.4rem, 5vw, 3.6rem) 0 0;
	padding: 0;
	border-top: 1px solid rgba(42, 33, 27, .14);
}

.aj-vindex li {
	border-bottom: 1px solid rgba(42, 33, 27, .14);
}

.aj-vindex a {
	position: relative;
	display: grid;
	grid-template-columns: 3.5rem 1fr auto;
	align-items: baseline;
	gap: 1rem;
	padding: 1.15rem .2rem;
	text-decoration: none;
}

.aj-vindex__num {
	font-family: var(--font-mono);
	font-size: .78rem;
	letter-spacing: .2em;
	color: var(--gold-deep);
}

.aj-vindex__name {
	font-size: clamp(1.15rem, 2.2vw, 1.55rem);
	font-weight: 700;
	letter-spacing: -.015em;
	transition: transform .55s var(--ease), color .4s var(--ease);
}

.aj-vindex__meta {
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: color-mix(in srgb, var(--ink) 48%, transparent);
}

.aj-vindex a::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: -1px;
	height: 1px;
	background: var(--gold-deep);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform .6s var(--ease);
}

.aj-vindex a:hover::after,
.aj-vindex a:focus-visible::after {
	transform: scaleX(1);
}

.aj-vindex a:hover .aj-vindex__name {
	transform: translateX(10px);
	color: var(--cocoa);
}

/* ----- Chapter head ----- */
.aj-vchapter__head {
	margin-bottom: clamp(2.2rem, 4.5vw, 3.4rem);
	max-width: 60ch;
}

.aj-vchapter__head .aj-display {
	font-size: clamp(1.9rem, 3.9vw, 3.1rem);
	margin-bottom: 1.1rem;
}

.aj-vchapter__clients {
	margin: 1.1rem 0 0;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--cocoa);
}

/* ----- Walls: one row, one line -----
   Every wall used to be its own composition — a staircase here, a centre frame
   stepping forward there, a stacked pair of landscape cuts in the fourth column
   of the first — built out of per-child margins and uneven column tracks. The
   client asked for the frames to be level, and level means all four of the
   things that were pushing them apart had to go, not just the obvious one:

     1. the per-child margin-top offsets, removed below and in .aj-smrail
        and .aj-imcase__wall;
     2. uneven tracks — 1.16fr and 2.3fr made a wider column a taller frame
        even at a fixed ratio, so every wall is repeat(n, 1fr) now;
     3. .aj-vstack, which put two frames in the height of one column. It is
        gone from patterns/video.php: the landscape cuts stand in the row as
        ordinary frames, letterboxed inside the same 4:5 window;
     4. per-frame parallax factors, which pulled a level row apart again the
        moment the page scrolled — see anjali_video_frame().

   What the walls lose in composition they get back in timing: the frames still
   arrive one after another (--d, 0.08s apart), so the rhythm moved from where
   a frame sits to when it lands. */
/* align-items is left at its stretch default on purpose — that is what lets the
   cards fill their track and end on one line. See .aj-vplx. */
.aj-vwall {
	display: grid;
	gap: clamp(.9rem, 1.6vw, 1.25rem);
}

/* 01 — three portraits and the two landscape cuts, all in one row. */
.aj-vwall--commercial {
	grid-template-columns: repeat(5, 1fr);
}

/* 02 */
.aj-vwall--product {
	grid-template-columns: repeat(3, 1fr);
}

/* 03 */
.aj-vwall--monolog {
	grid-template-columns: repeat(3, 1fr);
}

/* 04 */
.aj-vwall--production {
	grid-template-columns: repeat(3, 1fr);
}

/* ----- Frame (browser chrome card) ----- */
/* The wrapper owns the parallax transform, the card owns reveal + hover — one
   transform channel per element. Reveal choreography lives in section 22.

   height:100% is what levels the bottom edge. Levelling the offsets got the
   tops onto one line and 4:5 got the screens to one height, but a row still
   broke at the hem: "GEN" is one line and "The camera that stays awake" is two.
   The walls stretch their tracks (the grid default), so letting the card fill
   its track makes every card in a row exactly as tall as the tallest caption in
   it, and the spare height falls as quiet space under the type rather than as a
   gap between the title and its label. */
.aj-vplx {
	display: block;
	min-width: 0;
	height: 100%;
}

.aj-vframe {
	position: relative;
	display: block;
	height: 100%;
	border-radius: var(--radius);
	overflow: hidden;
	border: 1px solid rgba(42, 33, 27, .12);
	background: #fff;
	box-shadow: 0 24px 48px -28px rgba(42, 33, 27, .28);
	color: inherit;
	text-decoration: none;
}

.aj-vframe:hover,
.aj-vframe:focus-visible {
	box-shadow: 0 34px 62px -26px rgba(42, 33, 27, .44);
	border-color: rgba(169, 139, 87, .55);
}

.aj-vframe__bar {
	position: relative;
	display: flex;
	align-items: center;
	gap: 5px;
	padding: .6rem .9rem;
	overflow: hidden;
	background: linear-gradient(90deg, var(--gold-soft), var(--gold) 45%, var(--gold-deep));
}

.aj-vframe__bar span {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	background: rgba(23, 18, 16, .5);
	flex: none;
}

.aj-vframe__bar::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .55) 50%, transparent 60%);
	transform: translateX(-130%);
	transition: transform .85s var(--ease);
}

.aj-vframe:hover .aj-vframe__bar::after {
	transform: translateX(130%);
}

/* ----- One ratio, every frame -----
   4:5 across the board, at the client's request. Three different shapes used to
   live here — 9:16 for clips, 16:9 for the landscape cuts, and the still's own
   proportions, whatever they happened to be — and that is what made a row of
   frames sit at three different heights however carefully the columns were set.
   The ratio is on .aj-vframe__screen itself now rather than on three variants,
   so there is one number to change and no way for a fourth shape to appear. */
.aj-vframe__screen {
	position: relative;
	display: block;
	width: 100%;
	aspect-ratio: 4 / 5;
	overflow: hidden;
}

/* ----- Every frame now fills its window -----
   .aj-vframe--fit used to live here. Anything wider than 4:5 was letterboxed
   instead of cropped, on the argument that cropping a wide frame costs width and
   width on this site is type: the landscape cuts lose 55% of the shot, the square
   Driva posters lose the end of "PREMIO STANDART LUXURY", the Nivea grid loses a
   column of thumbnails. All of that is still true.

   The client asked for it anyway, and what makes it the right call now is the
   photo lightbox (section 25). When the argument was written, a cropped thumbnail
   was the only version of the picture the site had — losing half a frame meant
   losing it. Every still on this page opens full-size on click now, so the
   thumbnail is a window onto the work rather than the work itself, and a window
   should be filled. Six posters are affected; each carries a --focus-x / --focus
   pair chosen against its own subject rather than defaulting to centre.

   The frames that need the most care are still the three 16:9 cuts. If the client
   ever supplies 4:5-native artwork for those, it replaces the crop outright. */
.aj-vframe--still {
	margin: 0;
}

/* Opacity tracks the image decode; the transform channel belongs to the
   reveal settle and the hover zoom (section 22), so the two never collide.

   --focus decides which part of the poster survives the crop. 4:5 keeps 70% of
   a 9:16 clip, so where the subject sits high or low in frame the default
   centre crop takes a head or a pair of feet off; the data sets a percentage
   per item for those.

   --focus-x is the other axis, and it only became necessary when the letterbox
   came off. It stays unset on every tall poster — those are vertical clips
   cropped vertically and nothing needs moving sideways — and is set only on the
   six that are WIDER than 4:5, where width is what the crop spends and the
   subject is rarely in the middle of it.

   Deliberately two properties rather than one shorthand: 'focus' already means
   the vertical axis throughout inc/*.php, and renaming it would have silently
   re-pointed every value already in the data. */
.aj-vframe__poster {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--focus-x, 50%) var(--focus, 50%);
	opacity: 0;
	transition: opacity .7s var(--ease);
}

.aj-vframe__poster.is-loaded {
	opacity: 1;
}

/* No-JS / QA mode never sets .is-loaded — keep the stills visible. */
html:not(.js) .aj-vframe__poster {
	opacity: 1;
}

/* Gradient beds sit behind the stills while they lazy-load. */
.aj-vscreen--commercial {
	background:
		radial-gradient(70% 55% at 70% 18%, rgba(226, 168, 80, .4), transparent 62%),
		linear-gradient(160deg, #6B4A26, #3E2A16 70%);
}

.aj-vscreen--product {
	background:
		radial-gradient(65% 50% at 30% 15%, rgba(120, 168, 156, .32), transparent 60%),
		linear-gradient(160deg, #54584C, #2A2B22 70%);
}

.aj-vscreen--monolog {
	background:
		radial-gradient(65% 50% at 70% 80%, rgba(209, 177, 128, .28), transparent 60%),
		linear-gradient(160deg, #5C4433, #2A211B 70%);
}

.aj-vscreen--production {
	background:
		radial-gradient(65% 50% at 25% 20%, rgba(186, 84, 58, .3), transparent 60%),
		linear-gradient(160deg, #4E2A20, #241310 70%);
}

.aj-vscreen--spotlight {
	background:
		radial-gradient(65% 50% at 30% 12%, rgba(66, 122, 205, .38), transparent 62%),
		linear-gradient(160deg, #16305E, #0E1524 70%);
}

/* display:block is load-bearing — these are <span>s wrapping block children,
   and an inline box splits around them, so its padding never wraps the text. */
.aj-vframe__info {
	display: block;
	padding: 1rem 1.15rem 1.2rem;
}

.aj-vframe__title {
	display: block;
	margin-bottom: .3rem;
	font-size: 1.05rem;
	line-height: 1.3;
	font-weight: 700;
	letter-spacing: -.01em;
}

.aj-vframe__note {
	display: block;
	font-family: var(--font-mono);
	font-size: .68rem;
	line-height: 1.5;
	letter-spacing: .12em;
	text-transform: uppercase;
	color: color-mix(in srgb, var(--ink) 55%, transparent);
}

/* ----- Client spotlight (Jasa Raharja) ----- */
/* Full-bleed, not a floating card: as a rounded panel inside the chapter it
   read like an unfinished insert. Now it owns its own band of the page. */
.aj-vspot {
	position: relative;
	/* Top clearance carries the crossfade as well as the usual breathing room:
	   without it the eyebrow would land halfway through the dissolve, on ground
	   that is still more brown than blue. */
	padding-top: calc(clamp(3rem, 6vw, 5rem) + var(--seam-h) * .55);
	padding-bottom: clamp(3rem, 6vw, 5rem);
	overflow: hidden;
	isolation: isolate;
	color: var(--light);
	/* How far the brown field carries into the blue. Matches --seam-h on
	   .aj-cpanel, which solves the same problem on Home. */
	--seam-h: clamp(110px, 17vh, 230px);
}

/* ----- The one join in this run that IS a colour change -----
   Chapters 01–04 have no joins left to hide: they stand on .aj-vrun, one field.
   This panel is the exception — it carries its own blue, so brown genuinely has
   to become blue somewhere.

   The ground moved off the element and onto this pseudo so it can be masked. The
   run's field sits directly behind it, so fading the blue out at the top lets the
   brown through and the two dissolve into each other over ~190px. Nothing is
   sampled and nothing is copied: recolour either ground and the crossfade still
   lands, which is the one maintenance cost the --seam-up/--seam-dn contract on
   .aj-cpanel could not avoid.

   z-index -2 puts it under the grain at -1 and under the content, inside the
   isolate this element already declares. */
.aj-vspot::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -2;
	background:
		radial-gradient(46% 62% at 14% 12%, rgba(56, 112, 200, .48), transparent 64%),
		radial-gradient(40% 54% at 88% 84%, rgba(38, 78, 150, .34), transparent 62%),
		linear-gradient(150deg, #12244A, #101A2E 58%, #171210 96%);
	-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 var(--seam-h));
	mask-image: linear-gradient(180deg, transparent 0, #000 var(--seam-h));
	pointer-events: none;
}

.aj-vspot::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image: var(--grain);
	opacity: .06;
	mix-blend-mode: soft-light;
	pointer-events: none;
}

/* ----- Identity above the work, not beside it -----
   This was two columns: the client's mark pinned in a 360px track on the left,
   the three films taking the rest. It put the one thing that says whose panel
   this is into the narrowest slot on it, and on a 1800px container the mark and
   its films were nearly a metre apart on screen.

   Stacked and centred, the panel reads the way a title card does — label, mark,
   membership, brief, then the work it is all about. The client asked for it and
   the composition was already arguing for it. */
.aj-vspot__inner {
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
	display: grid;
	justify-items: center;
	gap: clamp(2rem, 4vw, 3.4rem);
	text-align: center;
}

/* Held to a reading measure. Centred text that runs the full container turns
   every line into a hunt for where the next one starts. */
.aj-vspot__brand {
	max-width: 62ch;
}

/* ----- The mark, at the size the panel is about it -----
   1.7x what it was, and the artwork is exactly why it stops there. The crop is
   431x351 native (assets/img/clients/jasa-raharja-lg.png, re-cut from the client's
   own 2048x940 logo sheet), so the honest ceiling is 175px tall: at 2x that asks
   for 350 device pixels against the 351 available. Past it the browser starts
   inventing edges on a trademark.

   The middle stop is what does the work, not the cap. 9vw looked like it read
   "1.7x" but resolves to 130px on a 1440 screen — only 1.29x the 101px the old
   clamp gave there. 12vw lands on 173px, which is the ratio the client asked for
   on the width this gets reviewed at, and the 11rem cap still catches it before
   the pixels run out on anything wider. */
.aj-vspot__logo {
	display: block;
	width: auto;
	height: clamp(7rem, 12vw, 11rem);
	margin: 0 auto 1.1rem;
	filter: brightness(0) invert(1);
	opacity: .94;
}

/* Centred, the eyebrow's single left-hand rule reads as a component that lost
   half of itself, so it gets its twin. Same 2.2rem the ::before draws. */
.aj-vspot .aj-eyebrow {
	color: var(--gold);
	justify-content: center;
	margin-bottom: 1rem;
}

.aj-vspot .aj-eyebrow::before,
.aj-vspot .aj-eyebrow::after {
	background: var(--gold);
}

.aj-vspot .aj-eyebrow::after {
	content: "";
	width: 2.2rem;
	height: 1px;
	flex: none;
}

.aj-vspot__meta {
	margin: 0 0 1.1rem;
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--gold);
}

.aj-vspot__note {
	margin: 0;
	font-size: .97rem;
	line-height: 1.6;
	color: rgba(243, 237, 227, .72);
}

/* Capped, now that it has the whole measure instead of the right two thirds.
   Three 640x1138 portraits across an 1800px container would stand 1,044px tall —
   taller than the screen they are shown on, and taller than the identity block
   they belong to. 1080px keeps them at roughly the height they already were. */
.aj-vspot__wall {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: clamp(.8rem, 1.5vw, 1.1rem);
	width: 100%;
	max-width: min(100%, 1080px);
	margin-inline: auto;
}

/* Frames on a dark ground — the Jasa Raharja panel and the whole influencer
   section share one treatment: glass instead of paper, light caption. */
.aj-vspot .aj-vframe,
.aj-section--dark .aj-vframe {
	background: rgba(243, 237, 227, .05);
	border-color: rgba(243, 237, 227, .16);
	box-shadow: 0 26px 50px -30px rgba(0, 0, 0, .7);
}

.aj-vspot .aj-vframe:hover,
.aj-section--dark .aj-vframe:hover {
	border-color: rgba(209, 177, 128, .5);
	box-shadow: 0 34px 62px -26px rgba(0, 0, 0, .8);
}

.aj-vspot .aj-vframe__title,
.aj-section--dark .aj-vframe__title {
	color: var(--light);
}

.aj-vspot .aj-vframe__note,
.aj-section--dark .aj-vframe__note {
	color: rgba(243, 237, 227, .58);
}

/* De Koeta Radja clips share the video showcase's gradient bed language. */
.aj-vscreen--dekoeta {
	background:
		radial-gradient(65% 50% at 40% 15%, rgba(226, 168, 80, .3), transparent 62%),
		linear-gradient(160deg, #4A3626, #21170F 70%);
}

/* ---------- 16 · Influencer ---------- */
/* The light band is the first thing in the section and runs full-bleed, so the
   section's own top padding would leave a strip of espresso above it. Same move
   .aj-ads makes with its cream band. */
/* Zero at both ends. The top was already open because the light band is the first
   thing in here and runs full-bleed. The bottom closed the same way once the last
   client panel started handing straight over to Media Buying's band: the section's
   own 152px left a strip of bare espresso carrying nothing between two grounds
   that both fade to espresso at the edge they meet on. Same reason .aj-cases and
   .aj-ads already run to their own edges. */
.aj-influencer {
	padding-top: 0;
	padding-bottom: 0;
}

/* ----- Chapter: the band and the first client, on one photograph -----
   The band opens a client's ground rather than sitting on a surface of its own,
   and that ground is a photograph. Painting it twice — once on the band, once on
   the panel — laid a bar of flat espresso through the middle of a single picture,
   because .aj-impanel__bg fades to espresso at both of its own edges.

   So the picture belongs here, to the box that spans both, and the first panel
   renders without a ground. The same move .aj-vrun makes for the video chapters
   and .aj-brchapter for Branding: when two elements share a surface, the surface
   belongs to their parent.

   isolation is load-bearing — .aj-impanel__bg sits at z-index -1 and would fall
   behind the section's own ground without a stacking context to sit in. */
.aj-imchapter {
	position: relative;
	isolation: isolate;
	overflow: hidden;
}

.aj-influencer__note {
	margin: 2rem 0 0;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .45);
}

/* Panels and group bands run edge to edge, so each re-applies the container
   measure on an inner element to keep every left edge on the same line. */
.aj-imgroup__inner {
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

/* ----- Chapter band: an opening, not an object -----
   This was a cream slab with a gold wash sweeping across it and a gold hairline
   drawn top and bottom — seven of them down the page, in Influencer, Media
   Buying and Branding. The client read every one as a strip pasted onto the
   page rather than as part of the chapter it opens, and the hairlines are why:
   two hard rules around a colour that appears nowhere else in the chapter is
   the definition of an inserted object.

   So the band stops carrying a ground of its own. It takes whatever ground the
   chapter below it stands on — a scrimmed photograph in Influencer, the section
   photograph in Media Buying, paper and its construction grid in Branding — and
   becomes the top of that surface instead of a lid on it. Three modifiers below
   do the three cases; everything they share lives here.

   The wash and both hairlines are gone entirely. What is left of the entrance
   is the content's own stagger, which was always doing the real work.

   THE TYPE STAYS LEFT. Centring it was tried at the client's request and taken
   back out: the bands are openings to chapters whose content is left-aligned all
   the way down, and a centred head over left-aligned work reads as a different
   page. It also broke the section head above it — .aj-lightband borrows
   .aj-imgroup__inner for its measure, so the rule reached a component it was
   never meant to touch. */
.aj-imgroup {
	position: relative;
	padding-block: clamp(3rem, 6vw, 4.8rem);
}

.aj-imgroup__inner {
	position: relative;
	z-index: 1;
}

/* The section around this is .aj-section--dark, which forces light type —
   every colour here has to be stated back. */
.aj-imgroup .aj-eyebrow {
	color: var(--cocoa);
}

.aj-imgroup .aj-eyebrow::before {
	background: var(--gold-deep);
}

/* Deliberately a step below .aj-display: when both sat at the same scale the
   section head and the group head read as the same level. */
.aj-imgroup__title {
	margin: 0 0 1rem;
	max-width: 62ch;
	font-size: clamp(1.55rem, 2.8vw, 2.4rem);
	font-weight: 800;
	letter-spacing: -.02em;
	line-height: 1.08;
	color: var(--ink);
}

.aj-imgroup__copy {
	margin: 0;
	max-width: 62ch;
	font-size: clamp(1.02rem, 1.4vw, 1.18rem);
	line-height: 1.62;
	color: color-mix(in srgb, var(--ink) 76%, transparent);
}

/* ----- Bands that open a dark chapter -----
   Influencer and Media Buying. The ground underneath is a photograph under an
   espresso scrim, so the band's type has to be restated for it — the same three
   values .aj-impanel already uses, so a band and the panel it opens are one
   voice rather than two. */
.aj-imgroup--dark .aj-imgroup__title {
	color: var(--light);
}

.aj-imgroup--dark .aj-imgroup__copy {
	color: rgba(243, 237, 227, .72);
}

.aj-imgroup--dark .aj-eyebrow {
	color: var(--gold);
}

.aj-imgroup--dark .aj-eyebrow::before {
	background: var(--gold);
}

/* ----- Campaign entries ----- */
/* Two shapes, both taken from the client deck: 'split' reads narrative →
   social proof → product; 'wall' reads narrative → a row of framed media. */
.aj-imcase {
	position: relative;
	gap: clamp(1.6rem, 3.5vw, 3rem);
	align-items: center;
}

/* Only between siblings — a panel's own edge already separates it from the
   next client, so a rule at the top of every entry would just add noise. */
.aj-imcase + .aj-imcase {
	margin-top: clamp(2.4rem, 5vw, 4rem);
	padding-top: clamp(2.4rem, 5vw, 4rem);
}

.aj-imcase + .aj-imcase::before {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 1px;
	background: linear-gradient(90deg, var(--gold-deep), rgba(209, 177, 128, .06) 72%, transparent);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 1.1s var(--ease);
}

html.js .aj-impanel.is-in .aj-imcase + .aj-imcase::before,
html:not(.js) .aj-imcase + .aj-imcase::before {
	transform: scaleX(1);
}

.aj-imcase--split {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr) minmax(0, .82fr);
}

/* Mirrored so the two NIVEA cases never read as one repeated block. The track
   widths mirror too — `order` alone would drop the product into the widest
   column and the narrative into the narrowest. */
.aj-imcase--split.aj-imcase--reverse {
	grid-template-columns: minmax(0, .82fr) minmax(0, 1.1fr) minmax(0, 1fr);
}

.aj-imcase--reverse .aj-imcase__lead {
	order: 3;
}

.aj-imcase--reverse .aj-imcase__proof {
	order: 2;
}

.aj-imcase--reverse .aj-imcase__product {
	order: 1;
}

/* The narrative is pinned rather than proportional — as a fraction it grew
   with the panel and quietly ate the width the media needed. */
.aj-imcase--wall {
	display: grid;
	grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
}

/* Below this the two columns leave the media too narrow to read, so the
   narrative moves above it and the wall takes the panel's full width. */
@media (max-width: 1599px) {
	.aj-imcase--wall {
		grid-template-columns: minmax(0, 1fr);
		align-items: start;
	}

	.aj-imcase--wall .aj-imcase__lead {
		max-width: 62ch;
		margin-bottom: clamp(1.4rem, 2.6vw, 2.2rem);
	}
}

.aj-imcase__wall {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: clamp(.7rem, 1.4vw, 1.05rem);
}

.aj-imcase__wall--de-koeta-radja {
	grid-template-columns: repeat(5, minmax(0, 1fr));
}

.aj-imcase__wall--changcuters {
	grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* ----- Client panel: one background, one wordmark, one or more campaigns ----- */
/* Panels sit outside .aj-container so they are already full width — no 50vw
   trick needed, and no scrollbar rounding to fight. The inner element re-applies
   the container so panel content lands on the same grid as the group heads. */
.aj-impanel {
	position: relative;
	padding-block: clamp(2.8rem, 5.5vw, 4.6rem);
	overflow: hidden;
	isolation: isolate;
}

.aj-impanel__bg {
	position: absolute;
	inset: 0;
	z-index: -1;
	overflow: hidden;
}

/* Desaturating is what lets these photos live on a gold-and-espresso page:
   the electric blues and greens drop back, the warm scrim tints what is left,
   and the picture reads as atmosphere rather than as a competing subject. */
.aj-impanel__bg img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	filter: saturate(.55) brightness(.82);
	transform: none;
	transition: transform 1.6s var(--ease), opacity 1s var(--ease);
}

/* Scrim: heaviest where the copy sits, opening up over the media side.
   --scrim is set per panel because the supplied photos differ a lot in
   brightness — the mountain road needs far more cover than the restaurant.

   The vertical fade is stated in PIXELS, not percent. It used to be 16%/84%,
   written when every panel was about 700px tall. Panels grew — Changcuters
   carries three phone mockups, NIVEA carries two campaigns — and the band grew
   with them: 187px of solid espresso top and bottom on Changcuters, 274px on
   NIVEA, which is what read as a black box sitting under the photograph. The
   fade exists to blend a panel into its neighbours, and that job needs a fixed
   distance, not a share of however tall the panel happens to be. */
.aj-impanel__bg::after {
	/* Capped twice: never more than 130px, and never more than 14% of the panel.
	   The pixel cap is what stops tall panels from being eaten; the percentage cap
	   is what stops a short panel (TRAC is 728px) from getting a bigger fade than
	   it had before. Whichever is smaller wins. */
	--fade: min(clamp(60px, 8vw, 130px), 14%);
	content: "";
	position: absolute;
	inset: 0;
	background:
		linear-gradient(90deg,
			rgb(23 18 16 / var(--scrim, .92)) 0%,
			rgb(23 18 16 / calc(var(--scrim, .92) * .94)) 38%,
			rgb(23 18 16 / calc(var(--scrim, .92) * .84)) 100%),
		linear-gradient(180deg, var(--espresso), transparent var(--fade), transparent calc(100% - var(--fade)), var(--espresso)),
		var(--grain);
	background-size: auto, auto, 160px 160px;
}

.aj-impanel__inner {
	position: relative;
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
}

/* Nameplate: the client is stated once, however many campaigns follow. */
.aj-impanel__head {
	margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
}

/* ----- Case narrative column ----- */
.aj-imcase__logo {
	display: block;
	width: auto;
	height: clamp(1.9rem, 3vw, 2.5rem);
	margin-bottom: clamp(1.1rem, 2.2vw, 1.6rem);
}

/* Client marks supplied as monochrome art — flipped to white, which is the
   correct treatment for these brands on a dark ground. Colour marks are
   excluded, inverting those would wreck them.
   These logos reveal with data-anim="fade" on purpose: the "blur" variant
   writes its own filter and would blow this one away. */
.aj-imcase__logo--mono {
	filter: brightness(0) invert(1);
	opacity: .92;
}

/* Sized by shape, not by colour: a stacked badge needs far more height than a
   wide wordmark to read at the same optical weight. */
.aj-imcase__logo--badge {
	height: clamp(4rem, 6.75vw, 5.75rem);
}

.aj-imcase__stat {
	margin: 0;
	font-size: clamp(2.6rem, 5.2vw, 4.2rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -.03em;
	color: var(--gold);
}

/* A word can carry the anchor when there is no honest number to show. */
.aj-imcase__stat--word {
	font-size: clamp(1.5rem, 2.9vw, 2.3rem);
	letter-spacing: -.02em;
	word-break: break-word;
}

.aj-imcase__statlabel {
	margin: .55rem 0 0;
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .5);
}

.aj-imcase__title {
	margin: clamp(1.1rem, 2.2vw, 1.6rem) 0 .6rem;
	font-size: clamp(1.2rem, 1.9vw, 1.5rem);
	font-weight: 700;
	letter-spacing: -.015em;
	line-height: 1.25;
	color: var(--light);
}

.aj-imcase__copy {
	margin: 0;
	font-size: .97rem;
	line-height: 1.65;
	color: rgba(243, 237, 227, .68);
}

/* ----- Product shot: floats free on the dark ground ----- */
.aj-imcase__product {
	display: grid;
	place-items: center;
}

/* Capped by height as well as width: these shots range from near-square to
   very tall, and width alone would let the tall ones tower over the case. */
.aj-imcase__shot {
	width: auto;
	height: auto;
	max-width: 100%;
	max-height: clamp(250px, 31vw, 440px);
	filter: drop-shadow(0 34px 46px rgba(0, 0, 0, .55));
}

/* ---------- 17 · Media Buying ---------- */
/* Paper rather than cream so the cream band on top of it reads. Keeping this
   section light — after four dark client panels the page needs the air, and
   the shared band is what ties it back to the influencer chapter. */
/* Ground comes from .aj-section--dark now; only the top padding is dropped, so the
   cream band lands flush against the section's top edge. isolate keeps the photo
   layer's negative z-index from escaping behind the section's own background. */
.aj-ads {
	padding-top: 0;
	position: relative;
	isolation: isolate;
	overflow: hidden;
}

/* ----- The photograph as ground -----
   Same anatomy as .aj-impanel__bg, retuned for a panel whose content runs the
   full measure instead of sitting in one column: the client panels scrim heavily
   on the left because the copy lives there, this one scrims evenly and lets the
   corners keep a little of the picture. */
.aj-ads__bg {
	position: absolute;
	inset: 0;
	z-index: -1;
	overflow: hidden;
}

.aj-ads__bg img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center 62%;
	filter: saturate(.5) brightness(.72);
	transform: none;
	transition: transform 1.6s var(--ease), opacity 1s var(--ease);
}

html.js .aj-ads:not(.is-in) .aj-ads__bg img {
	transform: scale(1.12);
	opacity: .5;
}

.aj-ads__bg::after {
	--fade: clamp(60px, 8vw, 130px);
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(130% 100% at 50% 45%, rgb(23 18 16 / .78), rgb(23 18 16 / .93) 72%),
		linear-gradient(180deg, var(--espresso), transparent var(--fade), transparent calc(100% - var(--fade)), var(--espresso)),
		var(--grain);
	background-size: auto, auto, 160px 160px;
}

.aj-imgroup--ads {
	margin-bottom: clamp(3rem, 6vw, 5rem);
}

.aj-ads .aj-section-head {
	margin-bottom: 2.6rem;
}

.aj-steps {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 1.1rem;
}

/* Same object as .aj-tcard in the Trifecta: gold-tinted glass on espresso. Three
   numbered cards stating a method is the same job in both places, so they should
   not be two different components — which is why the values are the --tc-* set
   the Trifecta declares, not a second copy that can drift away from it. */
.aj-step {
	padding: 1.7rem 1.6rem 1.9rem;
	border: 1px solid var(--tc-edge);
	border-radius: var(--radius);
	background: linear-gradient(165deg, var(--tc-wash-a), var(--tc-wash-b) 60%);
	transition: border-color .4s, transform .6s var(--ease);
}

.aj-step:hover {
	border-color: var(--tc-edge-hover);
	transform: translateY(-6px);
}

.aj-step__num {
	display: inline-block;
	font-family: var(--font-mono);
	font-size: .74rem;
	font-weight: 700;
	letter-spacing: .16em;
	color: var(--espresso);
	background: linear-gradient(135deg, var(--gold-soft), var(--gold) 60%, var(--gold-deep));
	padding: .42em .85em;
	border-radius: 999px;
	line-height: 1;
}

.aj-step h3 {
	margin: .85rem 0 .45rem;
	font-size: 1.2rem;
	font-weight: 700;
	letter-spacing: -.01em;
}

.aj-step p {
	margin: 0;
	font-size: .95rem;
	color: var(--tc-copy);
}

.aj-platforms {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .6rem;
	margin-top: 2.2rem;
}

.aj-platforms__label {
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--cocoa);
	margin-right: .6rem;
}

/* ----- Digital advertising: the second half of the service ----- */
/* start, not center: the right column now carries two labelled groups, and
   centring one against the other left both of them floating. */
.aj-adsplit {
	display: grid;
	grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
	gap: clamp(1.8rem, 4vw, 3.6rem);
	align-items: start;
	margin-top: clamp(3rem, 6vw, 4.6rem);
	padding-top: clamp(2.4rem, 5vw, 3.6rem);
	border-top: 1px solid rgba(209, 177, 128, .2);
}

/* The spec column: what we buy, then where we buy it. Two groups on one rule so
   the pair reads as a single panel rather than as two strays. */
.aj-adspec {
	display: grid;
	gap: clamp(1.8rem, 3.5vw, 2.8rem);
	min-width: 0;
	padding-left: clamp(1.2rem, 2.5vw, 2.2rem);
	border-left: 1px solid rgba(209, 177, 128, .2);
}

.aj-adspec__group {
	min-width: 0;
}

.aj-adsplit__title {
	margin: 0 0 .9rem;
	font-size: clamp(1.7rem, 3.2vw, 2.4rem);
	font-weight: 800;
	letter-spacing: -.02em;
	line-height: 1.12;
}

.aj-adsplit__copy {
	margin: 0;
	font-size: 1.02rem;
	line-height: 1.65;
	color: rgba(243, 237, 227, .74);
	max-width: 46ch;
}

.aj-adsplit__products {
	display: flex;
	flex-wrap: wrap;
	gap: .6rem;
}

.aj-chip--lg {
	font-size: .84rem;
	padding: .7em 1.3em;
}

/* ----- Platform rail: inline glyphs, tinted by currentColor -----
   No top margin any more: the rail lives inside the spec column and its group
   already owns the spacing above it. */
.aj-plat {
	margin-top: 0;
}

.aj-plat__label {
	margin: 0 0 1.1rem;
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--gold);
}

.aj-plat__rail {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: clamp(1rem, 2.4vw, 2rem);
}

.aj-plat__item {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: .55rem;
}

/* Glass on espresso — the same treatment .aj-vframe takes on a dark ground. */
.aj-plat__pill {
	display: grid;
	place-items: center;
	width: clamp(52px, 6vw, 64px);
	height: clamp(52px, 6vw, 64px);
	border-radius: 50%;
	border: 1px solid rgba(243, 237, 227, .16);
	background: rgba(243, 237, 227, .05);
	color: var(--light);
	transition: transform .45s var(--ease), border-color .4s var(--ease), color .4s var(--ease), background .4s var(--ease);
}

.aj-plat__icon {
	width: 46%;
	height: 46%;
}

/* Inverted with the ground: the pill used to go dark on hover against paper, so
   on espresso it lights up instead. */
.aj-plat__item:hover .aj-plat__pill {
	transform: translateY(-5px);
	border-color: rgba(209, 177, 128, .55);
	background: rgba(209, 177, 128, .14);
	color: var(--gold);
}

/* .62, not .55: the ground under this row is a photograph now, and at .55 the
   caption measured 4.76:1 against the brightest patch of it — passing, but with
   almost nothing left. */
.aj-plat__name {
	font-family: var(--font-mono);
	font-size: .66rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .62);
}

/* Staggered pop as the rail arrives. */
html.js .aj-plat[data-reveal]:not(.is-in) .aj-plat__item {
	opacity: 0;
	transform: translateY(14px) scale(.92);
}

html.js .aj-plat__item {
	transition:
		opacity .55s var(--ease) var(--d, 0s),
		transform .55s cubic-bezier(.34, 1.56, .64, 1) var(--d, 0s);
}

@media (max-width: 820px) {
	.aj-adsplit {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* ---------- 18 · Branding ----------
   Four chapters, each shaped to the work it holds rather than to one card
   template: the asset weight runs from two files to eighteen. Chapter heads
   reuse the cream .aj-imgroup band, so the rhythm comes from the band and not
   from another block of colour.

   Three things drive every layout below:

   · Ratio, not crop. The key visuals arrived as 1.79, 0.80 and 0.80 and the old
     grid forced all three into a square, which is what exposed the transparent
     padding baked into those exports. Frames declare --ar and the rows justify
     against it, so nothing is cropped and nothing shows ground through.
   · Density over width. The container grows to 1800px, and two small assets
     stretched across that is what read as "far apart" in brand architecture.
     Compositions that hold few assets cap themselves well under the container.
   · One rail, three uses. The adaptation strip and both photo sets are the same
     problem — a set too long to grid, made of frames whose ratios disagree — so
     they are one component with real controls rather than three bare scrollers. */
.aj-branding {
	background: var(--paper);
	overflow: clip;
}

/* The chapter index sits directly under the head, so the head gives up most of
   its own clearance to it. */
.aj-branding .aj-section-head--chapter {
	margin-bottom: clamp(1.8rem, 3.5vw, 2.8rem);
}

/* The longest chapter title on the page. Every other section head clears the
   ghosted numeral on its own; this one ran under it at mid-width, so it is held
   back by exactly the width the numeral occupies. */
.aj-branding .aj-section-head--chapter .aj-display {
	max-width: calc(100% - clamp(6rem, 15vw, 13rem) - 2rem);
}

/* ----- Chapter index ----- */
/* The first chapter band starts immediately after this, full-bleed, so the index
   needs real clearance under it — at 1rem the pills sat almost on the band's top
   hairline. */
.aj-brnav {
	display: flex;
	flex-wrap: wrap;
	gap: .55rem;
	padding-bottom: clamp(2.6rem, 5vw, 4.2rem);
}

.aj-brnav__link {
	display: inline-flex;
	align-items: baseline;
	gap: .6rem;
	padding: .62em 1.2em;
	border-radius: 999px;
	border: 1px solid rgba(42, 33, 27, .14);
	background: rgba(255, 255, 255, .55);
	text-decoration: none;
	color: color-mix(in srgb, var(--ink) 66%, transparent);
	transition:
		transform .45s var(--ease),
		border-color .35s,
		background .35s,
		color .35s;
}

.aj-brnav__i {
	font-family: var(--font-mono);
	font-size: .66rem;
	letter-spacing: .16em;
	color: var(--gold-deep);
	transition: color .35s;
}

.aj-brnav__t {
	font-size: .92rem;
	font-weight: 600;
	letter-spacing: -.01em;
}

.aj-brnav__link:hover {
	transform: translateY(-2px);
	border-color: rgba(169, 139, 87, .55);
	background: #fff;
	color: var(--ink);
}

.aj-brnav__link.is-active {
	border-color: transparent;
	background: linear-gradient(135deg, var(--gold-soft), var(--gold) 60%, var(--gold-deep));
	color: var(--espresso);
	box-shadow: 0 10px 24px -14px rgba(169, 139, 87, .8);
}

.aj-brnav__link.is-active .aj-brnav__i {
	color: color-mix(in srgb, var(--espresso) 70%, transparent);
}

/* ----- Chapter: band and body on one field -----
   The construction grid used to be drawn inside each body, with the cream band
   standing clear above it. Now that the band has no ground of its own, the two
   are one surface and the grid belongs to the pair — drawn once, from one
   origin, so its rows do not step at the join. See the note in patterns/branding.php.

   The grid is absolutely positioned and everything else has to sit on top of it:
   an absolute box with z-index 0 paints above ordinary in-flow content, so the
   content is lifted rather than the field pushed behind (z-index:-1 would drop it
   behind the section's own background and disappear). */
.aj-brchapter {
	position: relative;
}

.aj-brchapter > *:not(.aj-gridfield) {
	position: relative;
	z-index: 1;
}

.aj-brbody {
	position: relative;
}

/* Chapter 02 is the one body with a ground of its own — the blueprint is cream,
   because that chapter is literally about the grid it stands on. The ground moves
   up to the wrapper so the band shares it and the two read as one panel; .aj-arch
   gives up its own below. */
.aj-brchapter--architecture {
	background: var(--cream);
}

.aj-brchapter + .aj-brchapter {
	margin-top: clamp(3.5rem, 7vw, 6rem);
}

/* Cream against cream needs no gap; the two chapters either side of the blueprint
   close straight onto it. A strip of bare paper between them was what the old
   hairline was covering up. */
.aj-brchapter--architecture,
.aj-brchapter--architecture + .aj-brchapter {
	margin-top: 0;
}

.aj-imgroup--brand {
	margin-bottom: clamp(2.2rem, 4vw, 3.4rem);
}

/* The band runs full-bleed but only ever carried a 62ch column, so on a wide
   screen most of it was empty. The index numeral and the spec list are what
   hold the right half. */
.aj-brband {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, .52fr);
	align-items: center;
	gap: clamp(1.6rem, 4vw, 4rem);
}

.aj-brband__text {
	min-width: 0;
}

.aj-brband__meta {
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: clamp(1.1rem, 2.4vw, 2.2rem);
	min-width: 0;
}

/* Outline, never fill: it is a landmark, not a heading, and a solid numeral this
   size would outrank the title. This one stays where the section-level numerals
   were removed because its set is complete and visible — four chapters, four
   numerals, each sitting on the chapter it names. */
.aj-brband__num {
	flex: none;
	font-size: clamp(3.6rem, 8vw, 7.5rem);
	font-weight: 800;
	line-height: .82;
	letter-spacing: -.05em;
	color: transparent;
	-webkit-text-stroke: 1px rgba(169, 139, 87, .5);
	user-select: none;
}

/* ----- Shared image behaviour -----
   The images carry no reveal of their own; their wrapper owns it. That leaves
   the transform channel free for the hover zoom. */
.aj-brimg {
	display: block;
	max-width: 100%;
}

.aj-brimg--fill {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform .7s var(--ease);
}

.aj-kv__shot,
.aj-jrow__shot,
.aj-arch__shot {
	display: block;
	position: relative;
	overflow: hidden;
	border-radius: var(--radius);
	background: rgba(42, 33, 27, .05);
	box-shadow: 0 22px 46px -32px rgba(42, 33, 27, .55);
}

.aj-kv__shot:hover .aj-brimg--fill,
.aj-jrow__shot:hover .aj-brimg--fill,
.aj-arch__shot:hover .aj-brimg--fill {
	transform: scale(1.04);
}

/* ----- 01 · Key visual ----- */
.aj-kv__hero {
	margin: 0 0 clamp(1rem, 2vw, 1.6rem);
}

.aj-kv__hero .aj-kv__shot {
	aspect-ratio: 16 / 9;
}

/* Justified row: --ar sets both the frame's shape and how much of the row it
   claims, so a 1.79 landscape and a 0.80 portrait finish on the same height
   with neither cropped. Basis and grow both carry the ratio — grow alone would
   only distribute the leftover space proportionally, not the row itself. */
.aj-jrow {
	display: flex;
	flex-wrap: wrap;
	gap: clamp(1rem, 2vw, 1.6rem);
}

.aj-jrow__cell {
	flex: var(--ar, 1) 1 calc(var(--ar, 1) * 180px);
	min-width: 0;
	margin: 0;
}

.aj-jrow__shot {
	aspect-ratio: var(--ar, 1);
}

.aj-kv__cap {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: .15rem .7rem;
	margin: .85rem 0 0;
}

.aj-kv__cap strong {
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	font-weight: 700;
	color: var(--ink);
}

.aj-kv__cap span {
	font-size: .88rem;
	color: color-mix(in srgb, var(--ink) 62%, transparent);
}

/* ----- Rail: one component, three uses -----
   Fixed height, width from the ratio: the items run from a 2.07 corridor to a
   0.56 portrait, and a fixed width would crop half of them. The viewport runs
   full-bleed while the track is padded back to the container measure, so the
   first frame still starts on the section's left edge and the strip still runs
   off both sides of the screen. */
.aj-rail {
	--rail-h: clamp(210px, 24vw, 280px);
	--rail-inset: calc((100% - var(--container)) / 2 + var(--gutter));
	margin-top: clamp(2.4rem, 4.5vw, 3.6rem);
}

.aj-rail--sm {
	--rail-h: clamp(300px, 34vw, 380px);
}

.aj-rail--lg {
	--rail-h: clamp(320px, 38vw, 460px);
}

.aj-rail__head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	margin-bottom: 1.1rem;
}

.aj-rail__label {
	margin: 0;
}

.aj-rail__nav {
	display: flex;
	gap: .5rem;
}

.aj-rail__btn {
	width: 38px;
	height: 38px;
	display: grid;
	place-items: center;
	padding: 0;
	border-radius: 999px;
	border: 1px solid rgba(42, 33, 27, .16);
	background: rgba(255, 255, 255, .6);
	color: var(--ink);
	font-family: var(--font-display);
	font-size: 1.2rem;
	line-height: 1;
	cursor: pointer;
	transition:
		transform .4s var(--ease),
		border-color .3s,
		background .3s,
		opacity .3s;
}

.aj-rail__btn:hover:not(:disabled) {
	transform: translateY(-2px);
	border-color: var(--gold-deep);
	background: #fff;
}

.aj-rail__btn:disabled {
	opacity: .3;
	cursor: default;
}

.aj-rail__viewport {
	position: relative;
}

/* Edge fades, so the strip reads as continuing past the screen rather than
   stopping at it. */
.aj-rail__viewport::before,
.aj-rail__viewport::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	z-index: 2;
	width: clamp(24px, 5vw, 90px);
	pointer-events: none;
}

.aj-rail__viewport::before {
	left: 0;
	background: linear-gradient(90deg, var(--paper), rgba(250, 247, 241, 0));
}

.aj-rail__viewport::after {
	right: 0;
	background: linear-gradient(270deg, var(--paper), rgba(250, 247, 241, 0));
}

.aj-rail__track {
	display: flex;
	gap: clamp(.7rem, 1.4vw, 1.1rem);
	overflow-x: auto;
	overscroll-behavior-x: contain;
	scroll-snap-type: x proximity;
	padding-block: .25rem 1rem;
	padding-inline: var(--rail-inset);
	scroll-padding-inline: var(--rail-inset);
	/* The rail states its position through the bar below it, so the native bar
	   is redundant here — and it is the one scroller on the page that would sit
	   under a row of captions. */
	scrollbar-width: none;
}

.aj-rail__track::-webkit-scrollbar {
	display: none;
}

.aj-rail__track:focus-visible {
	outline: 2px solid var(--gold-deep);
	outline-offset: 2px;
}

/* Every direct child, not just .aj-rail__item — the photo sets put the shot
   straight into the track, and without this flex-shrink squeezed a 0.56 frame
   down to about half its width. */
.aj-rail__track > * {
	flex: none;
	scroll-snap-align: start;
}

.aj-rail.is-dragging .aj-rail__track {
	cursor: grabbing;
	scroll-snap-type: none;
	scroll-behavior: auto;
}

.aj-rail.is-dragging .aj-rail__shot {
	pointer-events: none;
}

.aj-rail__bar {
	height: 2px;
	border-radius: 999px;
	background: rgba(42, 33, 27, .1);
	overflow: hidden;
}

.aj-rail__fill {
	display: block;
	height: 100%;
	border-radius: inherit;
	background: linear-gradient(90deg, var(--gold), var(--gold-deep));
	transform: scaleX(var(--rail-p, 0));
	transform-origin: left;
	transition: transform .15s linear;
}

.aj-rail__item {
	margin: 0;
}

.aj-rail__shot {
	display: block;
	position: relative;
	box-sizing: border-box;
	height: var(--rail-h);
	aspect-ratio: var(--ar, 1);
	overflow: hidden;
	border-radius: 14px;
	background: #fff;
	border: 1px solid rgba(42, 33, 27, .1);
	box-shadow: 0 16px 34px -28px rgba(42, 33, 27, .5);
}

.aj-rail__shot .aj-brimg {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform .7s var(--ease);
}

.aj-rail__shot:hover .aj-brimg {
	transform: scale(1.05);
}

/* Cut-out renders keep their transparency and sit inset, so the artwork reads
   as an object on the card rather than a photograph filling it. */
.aj-rail__item--cutout .aj-rail__shot {
	padding: 10px;
	background: linear-gradient(160deg, #fff, #F4EFE6);
}

.aj-rail__item--cutout .aj-brimg {
	object-fit: contain;
}

.aj-rail__item figcaption {
	display: flex;
	align-items: baseline;
	gap: .55rem;
	margin-top: .6rem;
	font-family: var(--font-mono);
	font-size: .64rem;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: color-mix(in srgb, var(--ink) 58%, transparent);
}

.aj-rail__num {
	color: var(--gold-deep);
}

.aj-rail__shot--photo {
	padding: 0;
	border-radius: 12px;
}

/* ----- 02 · Brand architecture: the drawing, then the built thing -----
   One composition on a shared blueprint ground. Two columns with a gap between
   them is exactly what this chapter used to be, and with only two assets — one
   of them 361px wide — the gap was most of what you saw. */
/* Solid cream, edge to edge. The old gradient faded to 45% cream over paper, so
   the lower half of the panel washed out to something indistinguishable from the
   page — it read as a band of empty white sitting between two cream bands rather
   than as the blueprint ground it is. */
/* The cream ground moved up to .aj-brchapter--architecture so the chapter band
   stands on it too — the band no longer brings a slab of its own, and these two
   are one panel now. */
.aj-arch {
	position: relative;
	padding-block: clamp(2.8rem, 6vw, 5rem);
	overflow: hidden;
}

/* ----- Grid field -----
   The construction grid Brand Architecture is about, lifted out of that chapter
   so the rest of the section can share the texture. It scales and fades in
   rather than sliding, so it reads as being drawn onto the page.

   Two strengths, and the difference is deliberate. On the architecture panel the
   grid is the subject — that chapter is literally about a mark built on one — so
   it sits at .20 on cream. Everywhere else in Branding it is texture holding the
   section together, so it drops to .12 and stays on paper. Turning the other
   chapters cream as well would cost the chapter bands their identity: a band
   only reads as a band while the body around it is a different ground. */
.aj-gridfield {
	position: absolute;
	inset: 0;
	pointer-events: none;
	background-image:
		linear-gradient(to right, rgba(169, 139, 87, var(--grid-a, .12)) 1px, transparent 1px),
		linear-gradient(to bottom, rgba(169, 139, 87, var(--grid-a, .12)) 1px, transparent 1px);
	background-size: 56px 56px;
	/* Wide: on a solid ground the grid has to carry almost to the edges or the
	   corners read as bald. */
	-webkit-mask-image: radial-gradient(120% 92% at 50% 45%, #000 40%, transparent 92%);
	mask-image: radial-gradient(120% 92% at 50% 45%, #000 40%, transparent 92%);
	opacity: 0;
	transform: scale(1.07);
	transition:
		opacity 1.2s var(--ease),
		transform 1.6s var(--ease);
}

/* The blueprint chapter is the one place the grid is the subject rather than the
   texture, so it sits at .20. It used to be scoped to .aj-arch; the field belongs
   to the whole chapter now, band included. */
.aj-brchapter--architecture .aj-gridfield {
	--grid-a: .2;
}

/* The field is drawn as the chapter's band arrives. It cannot key off its own
   parent any more — .aj-brchapter spans band and body and carries no reveal of
   its own — so it keys off the band inside it, which is the right moment anyway:
   the grid is drawn onto the page as the chapter opens. */
html.js .is-in > .aj-gridfield,
html.js .aj-brchapter:has(> .aj-imgroup.is-in) > .aj-gridfield,
html:not(.js) .aj-gridfield {
	opacity: 1;
	transform: none;
}

.aj-arch__inner {
	--overlap: clamp(1.5rem, 4vw, 5rem);
	position: relative;
	z-index: 1;
	max-width: min(1240px, 100%);
	margin-inline: auto;
	padding-inline: var(--gutter);
	display: grid;
	/* Two columns, not two rows plus a spanning figure: auto rows stretch to the
	   spanning item's height, and the space that opened between the plate and the
	   index was the old gap in a new place.

	   The left track is deliberately narrower than the plate it holds — the plate
	   fills the track and the negative margin below then carries it over the
	   photograph. A wider track would leave the plate floating mid-column and the
	   overlap would never happen. */
	grid-template-columns: minmax(0, .78fr) minmax(0, 1fr);
	align-items: center;
	column-gap: 0;
	row-gap: clamp(1.8rem, 4vw, 3rem);
}

.aj-arch__side {
	min-width: 0;
}

/* The plate leans onto the photograph instead of standing across a gap from
   it — the same overhang move the case plates and the family stickers make. */
.aj-arch__concept {
	position: relative;
	z-index: 2;
	margin-inline-end: calc(var(--overlap) * -1);
}

.aj-arch__label {
	display: inline-flex;
	align-items: center;
	gap: .6rem;
	margin-bottom: .9rem;
	font-family: var(--font-mono);
	font-size: .66rem;
	letter-spacing: .2em;
	text-transform: uppercase;
	/* --gold-deep measures 2.69:1 on this ground; --cocoa is the tested one. */
	color: var(--cocoa);
}

.aj-arch__label span {
	display: inline-grid;
	place-items: center;
	width: 1.9em;
	height: 1.9em;
	border-radius: 999px;
	border: 1px solid var(--gold-deep);
	color: var(--gold-deep);
	font-size: .88em;
	letter-spacing: 0;
}

.aj-arch__plate {
	display: grid;
	place-items: center;
	width: 100%;
	box-sizing: border-box;
	padding: clamp(1.5rem, 3vw, 2.4rem);
	background: #fff;
	border: 1px solid rgba(42, 33, 27, .12);
	border-radius: var(--radius);
	/* Deeper than the old plate: it now floats over a photograph, and the
	   shadow is what tells the eye which of the two is in front. */
	box-shadow: 0 34px 62px -34px rgba(42, 33, 27, .62);
}

/* Shipped at its native 361px — the source is that size and no larger, so
   width:auto lets it stop there instead of being stretched to fill the plate.
   max-width still lets it shrink on narrow screens. */
.aj-brimg--native {
	width: auto;
	max-width: 100%;
	height: auto;
	margin-inline: auto;
}

.aj-arch__meta,
.aj-arch__built figcaption {
	display: flex;
	flex-direction: column;
	gap: .2rem;
	margin: 1.1rem 0 0;
}

.aj-arch__meta strong,
.aj-arch__built figcaption strong {
	font-size: 1rem;
	font-weight: 700;
	color: var(--ink);
}

.aj-arch__meta span,
.aj-arch__built figcaption span {
	font-size: .88rem;
	color: color-mix(in srgb, var(--ink) 62%, transparent);
}

.aj-arch__built {
	margin: 0;
	min-width: 0;
}

/* Held at 4:5, not its native 2:3: a full-height portrait forced the concept
   column to stretch beside it, and the stretch was the gap. */
.aj-arch__built .aj-arch__shot {
	aspect-ratio: 4 / 5;
}

.aj-arch__index {
	list-style: none;
	margin: clamp(1.8rem, 3.5vw, 2.8rem) 0 0;
	padding: 0;
	border-top: 1px solid rgba(169, 139, 87, .38);
}

.aj-arch__index li {
	display: grid;
	grid-template-columns: 2.6rem minmax(0, 1fr);
	align-items: baseline;
	gap: .15rem .8rem;
	padding: .8rem .2rem;
	border-bottom: 1px solid rgba(169, 139, 87, .22);
}

.aj-arch__inum {
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .18em;
	color: var(--gold-deep);
}

.aj-arch__iname {
	font-size: clamp(1rem, 1.6vw, 1.18rem);
	font-weight: 700;
	letter-spacing: -.015em;
}

.aj-arch__imeta {
	grid-column: 2;
	font-family: var(--font-mono);
	font-size: .64rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: color-mix(in srgb, var(--ink) 50%, transparent);
}

/* ----- 03 · Logo & branding -----
   Two mirrored rows and a full-width board, not a three-up card grid. The grid
   forced one height on all three, and Driva is not a mark at all — it is a whole
   identity sheet that the card cropped down to its logo. */
.aj-lrow {
	display: grid;
	grid-template-columns: minmax(0, .88fr) minmax(0, 1.12fr);
	gap: clamp(1.8rem, 4vw, 3.6rem);
	align-items: center;
}

.aj-lrow + .aj-lrow,
.aj-lrow + .aj-lboard,
.aj-lboard + .aj-lrow {
	margin-top: clamp(2.6rem, 5.5vw, 4.6rem);
	padding-top: clamp(2.6rem, 5.5vw, 4.6rem);
	border-top: 1px solid rgba(42, 33, 27, .1);
}

/* Mirrored, the same anatomy still reads as a rhythm — the move the web cases
   and the social panels already make. The track widths swap with the order:
   ordering alone left the applied work — the evidence — in the narrow column. */
.aj-lrow--reverse {
	grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr);
}

.aj-lrow--reverse .aj-lrow__lead {
	order: 2;
}

.aj-lrow--reverse .aj-lrow__apply {
	order: 1;
}

.aj-lrow__lead {
	min-width: 0;
}

.aj-lrow__name {
	margin: 0 0 .7rem;
	font-size: clamp(1.5rem, 2.6vw, 2.2rem);
	font-weight: 800;
	letter-spacing: -.02em;
	line-height: 1.08;
}

.aj-lrow__note {
	margin: 0;
	max-width: 44ch;
	font-size: clamp(.98rem, 1.3vw, 1.1rem);
	line-height: 1.6;
	color: color-mix(in srgb, var(--ink) 72%, transparent);
}

/* #FFFBF5 is the ground the supplied mockups were exported on, so the artwork
   meets the plate without a seam — and the letterboxing inside .aj-lrow__apply
   disappears for the same reason. */
.aj-lrow__plate {
	display: grid;
	justify-items: center;
	align-content: center;
	gap: 1.1rem;
	margin-top: clamp(1.4rem, 2.6vw, 2rem);
	padding: clamp(1.6rem, 3vw, 2.4rem);
	background: #FFFBF5;
	border: 1px solid rgba(42, 33, 27, .1);
	border-radius: var(--radius);
	box-shadow: 0 24px 48px -34px rgba(42, 33, 27, .5);
}

.aj-lrow__rule {
	width: min(200px, 62%);
	height: 1px;
	background: linear-gradient(90deg, transparent, rgba(169, 139, 87, .55), transparent);
}

.aj-lrow__plate .aj-brimg--mark {
	max-height: clamp(96px, 10vw, 148px);
	width: auto;
}

.aj-lrow__plate .aj-brimg--lockup {
	max-height: clamp(34px, 3.6vw, 50px);
	width: auto;
}

/* The frame carries the mockup's own ratio and only caps its height, so nothing
   is ever cropped and the letterboxing that is left lands on the same #FFFBF5
   the artwork was exported on. Block, not grid: a grid row sized to its content
   makes the image's own percentage height circular, and the image then overflows
   its frame at full size — which is what cropped the Jatelindo shirt. */
.aj-lrow__apply {
	display: block;
	box-sizing: border-box;
	aspect-ratio: var(--ar, 1);
	max-height: clamp(300px, 42vw, 620px);
	padding: clamp(.8rem, 1.8vw, 1.6rem);
	overflow: hidden;
	background: #FFFBF5;
	border: 1px solid rgba(42, 33, 27, .1);
	border-radius: var(--radius);
	box-shadow: 0 28px 54px -34px rgba(42, 33, 27, .45);
	transition: border-color .4s, box-shadow .5s var(--ease);
}

.aj-lrow__apply .aj-brimg--apply {
	width: 100%;
	height: 100%;
	object-fit: contain;
	transition: transform .7s var(--ease);
}

.aj-lrow__apply:hover {
	border-color: rgba(169, 139, 87, .5);
	box-shadow: 0 34px 62px -32px rgba(42, 33, 27, .5);
}

.aj-lrow__apply:hover .aj-brimg--apply {
	transform: scale(1.03);
}

/* ----- The identity board ----- */
.aj-lboard {
	display: grid;
	grid-template-columns: minmax(0, .74fr) minmax(0, 1.26fr);
	gap: clamp(1.8rem, 4vw, 3.6rem);
	align-items: center;
}

.aj-lboard__head {
	min-width: 0;
}

.aj-lboard__parts {
	margin-top: clamp(1.2rem, 2.4vw, 1.9rem);
}

/* The frame carries the board's own ratio, so object-fit:cover crops nothing —
   the palette, the typeface and the texture all stay on the sheet. */
.aj-lboard__shot {
	display: block;
	position: relative;
	overflow: hidden;
	aspect-ratio: var(--ar, 1.777);
	border-radius: var(--radius);
	border: 1px solid rgba(42, 33, 27, .12);
	box-shadow: 0 34px 64px -34px rgba(42, 33, 27, .55);
}

.aj-brimg--board {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* The board settles out of a slight scale as it is unveiled. Both rules have to
   out-specify the reveal state below, or the hover would never land. */
html.js .aj-lboard__shot[data-reveal] .aj-brimg--board {
	transform: scale(1.06);
	transition: transform 1.4s var(--ease);
}

html.js .aj-lboard__shot[data-reveal].is-in .aj-brimg--board {
	transform: none;
}

html.js .aj-lboard__shot[data-reveal].is-in:hover .aj-brimg--board {
	transform: scale(1.02);
}

/* ----- 04 · Photography ----- */
.aj-brbody--photo .aj-rail + .aj-rail {
	margin-top: clamp(2.6rem, 5vw, 4rem);
}
/* ---------- 19 · Web Development ----------
   Two projects, each given a whole row: the recording on one side, the
   narrative on the other, mirrored for the second. The section ground stays
   plain espresso on purpose — these cards are lit screens, and a lit screen
   only reads as lit in a dark room. */
.aj-wcase {
	position: relative;
	display: grid;
	/* The recording is the evidence, so it takes the wide track. */
	grid-template-columns: minmax(0, 1.45fr) minmax(300px, .8fr);
	gap: clamp(2rem, 4vw, 3.8rem);
	align-items: center;
	margin-top: clamp(3.5rem, 7vw, 6rem);
}

/* Mirroring needs the tracks reversed as well as the order: `order` alone
   moves the boxes but leaves the widths behind, which drops the screen into
   the narrow column — the same trap the mirrored NIVEA case fell into. */
.aj-wcase__screen { order: 1; }
.aj-wcase__lead   { order: 2; }

.aj-wcase--reverse {
	grid-template-columns: minmax(300px, .8fr) minmax(0, 1.45fr);
}

.aj-wcase--reverse .aj-wcase__screen { order: 2; }
.aj-wcase--reverse .aj-wcase__lead   { order: 1; }

/* ----- Ambient glow: light thrown off the screen onto the wall -----
   Colours arrive per-case as --glow-a / --glow-b, sampled from the recordings
   themselves. Kept low: this is a reflection, not a spotlight. */
.aj-wcase__glow {
	position: absolute;
	/* Spills above and below the card the way real light would, but stays
	   inside the container horizontally — a negative inline inset here is
	   wider than the page gutter and pushes past the viewport edge. The blur
	   does the sideways spreading instead, and blur does not grow the box. */
	inset: -20% 0;
	z-index: 0;
	pointer-events: none;
	background:
		radial-gradient(46% 52% at 32% 42%, rgba(var(--glow-a), .20), transparent 70%),
		radial-gradient(38% 44% at 72% 62%, rgba(var(--glow-b), .12), transparent 68%);
	filter: blur(18px);
	opacity: 0;
	transition: opacity 1.2s var(--ease);
}

.aj-wcase--reverse .aj-wcase__glow {
	background:
		radial-gradient(46% 52% at 68% 42%, rgba(var(--glow-a), .20), transparent 70%),
		radial-gradient(38% 44% at 28% 62%, rgba(var(--glow-b), .12), transparent 68%);
}

html:not(.js) .aj-wcase__glow,
.aj-wcase__glow.is-in {
	opacity: 1;
}

.aj-wcase__lead,
.aj-wcase__screen {
	position: relative;
	z-index: 1;
	min-width: 0;
}

.aj-wcase__logo {
	display: block;
	height: clamp(2.4rem, 3.6vw, 3.2rem);
	width: auto;
	max-width: 100%;
	margin: 1.4rem 0 1.6rem;
	object-fit: contain;
	object-position: left center;
}

/* Grey artwork flipped to white. Safe here because the counters and the
   "LABS" knockout are transparent, not white pixels — inverting fills only
   what is actually painted. */
.aj-wcase__logo--mono {
	filter: brightness(0) invert(1);
	opacity: .92;
}

/* Stacked marks carry their weight in height, not width. */
.aj-wcase__logo--badge {
	height: clamp(3.2rem, 5vw, 4.4rem);
}

.aj-wcase__title {
	margin: 0 0 1.1rem;
	font-size: clamp(1.7rem, 3vw, 2.6rem);
	line-height: 1.08;
	letter-spacing: -.02em;
	font-weight: 800;
}

.aj-wcase__copy {
	margin: 0;
	font-size: 1.02rem;
	line-height: 1.65;
	max-width: 46ch;
	color: rgba(243, 237, 227, .72);
}

.aj-wcase__feats {
	display: flex;
	flex-wrap: wrap;
	gap: .5rem;
	margin: 1.7rem 0 0;
	padding: 0;
	list-style: none;
}

/* ----- The screen itself -----
   Reuses .aj-vframe wholesale, so the chrome, the reveal, the hover lift and
   the lightbox binding all come across unchanged. Only three things differ:
   a free aspect ratio, a URL chip in the bar, and no caption inside the
   frame — the column beside it already carries the words. */
.aj-vframe--site .aj-vframe__screen {
	aspect-ratio: var(--ar, 16 / 9);
}

.aj-vframe__url {
	margin-left: .5rem;
	font-family: var(--font-mono);
	font-style: normal;
	font-size: .66rem;
	letter-spacing: .12em;
	color: rgba(23, 18, 16, .62);
	background: rgba(255, 255, 255, .35);
	padding: .3em .9em;
	border-radius: 999px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The screen idles a touch dim and wakes on approach — the same "resting
   state, then attention" language the client logo marquee uses. */
html.js .aj-vframe--site .aj-vframe__poster {
	filter: brightness(.86) saturate(.9);
	transition: opacity .7s var(--ease), filter .55s var(--ease);
}

html.js .aj-vframe--site:hover .aj-vframe__poster,
html.js .aj-vframe--site:focus-visible .aj-vframe__poster {
	filter: brightness(1) saturate(1);
}

.aj-vframe--site .aj-media__play {
	transition: transform .5s var(--ease), background-color .5s var(--ease), border-color .5s var(--ease);
}

.aj-vframe--site:hover .aj-media__play,
.aj-vframe--site:focus-visible .aj-media__play {
	transform: scale(1.08);
	background-color: rgba(209, 177, 128, .3);
	border-color: rgba(209, 177, 128, .85);
}

/* The chrome boots after the card has opened: three lights, then the URL —
   a browser coming up, not a decoration fading in. Delays stack on --d so a
   card further down the wall still boots after it arrives, never before. */
html.js .aj-vframe--site .aj-vframe__bar span {
	opacity: 0;
	transform: scale(.4);
	transition: opacity .35s var(--ease), transform .45s var(--ease);
}

html.js .aj-vframe--site .aj-vframe__url {
	opacity: 0;
	transform: translateX(-6px);
	transition: opacity .45s var(--ease), transform .55s var(--ease);
}

html.js .aj-vframe--site.is-in .aj-vframe__bar span,
html.js .aj-vframe--site.is-in .aj-vframe__url {
	opacity: 1;
	transform: none;
}

html.js .aj-vframe--site.is-in .aj-vframe__bar span:nth-child(1) { transition-delay: calc(var(--d, 0s) + .30s); }
html.js .aj-vframe--site.is-in .aj-vframe__bar span:nth-child(2) { transition-delay: calc(var(--d, 0s) + .36s); }
html.js .aj-vframe--site.is-in .aj-vframe__bar span:nth-child(3) { transition-delay: calc(var(--d, 0s) + .42s); }
html.js .aj-vframe--site.is-in .aj-vframe__url { transition-delay: calc(var(--d, 0s) + .50s); }

/* The glow drifts rather than pulses — opacity is spoken for by the reveal,
   so the breathing lives on the transform channel. Two different periods so
   the pair never falls into step. */
/* Breathes inward from 0.88 to 1 rather than outward past 1: scaling up would
   grow the box past the gutter and reintroduce the overflow the inset above
   just fixed. Vertical drift is free — it cannot widen anything. */
@keyframes aj-glow-breathe {
	from { transform: scale(.88) translate3d(0, 2%, 0); }
	to   { transform: scale(1) translate3d(0, -2%, 0); }
}

html.js .aj-wcase__glow.is-in {
	animation: aj-glow-breathe 26s var(--ease) infinite alternate;
}

html.js .aj-wcase--reverse .aj-wcase__glow.is-in {
	animation-duration: 34s;
	animation-direction: alternate-reverse;
}

/* ---------- 20 · Our Family ---------- */
/* ----- The cutting mat, as the section itself -----
   This was a plate inside the section — a rounded object carrying a photograph
   of a mat, cream showing all around it — and the note here used to argue for
   keeping it that way, because darkening the whole band would leave three dark
   blocks running to the footer with no breath between them. The client asked
   for the mat to be the ground, so it is. The breath moved instead: the
   disciplines deck above now ends on cream, which is the pause that used to sit
   around the plate.

   Drawn, not stretched. mat.webp is 1200x800, and a full-bleed band is neither
   of those — scaled up the grid either blurs or goes out of square, and the
   ruler numbering along its edge turns to smudge. Two repeating gradients hold
   their line at any height and weigh nothing.

   The palette is measured off the file rather than eyeballed: the field is its
   commonest colour, and the two line values are the mean and the brightest of
   its yellow-greens. */
.aj-family {
	--mat-field: #4E756C;
	--mat-line: rgba(173, 203, 131, .20);
	--mat-line-major: rgba(246, 255, 171, .30);
	/* One cell of the fine grid. The major line falls every fifth one, which is
	   the half-inch/inch relationship the real mat is ruled to. */
	--mat-cell: clamp(20px, 2vw, 32px);

	background:
		radial-gradient(70% 50% at 50% 0%, rgba(23, 18, 16, .22), transparent 68%),
		radial-gradient(60% 44% at 84% 100%, rgba(23, 18, 16, .18), transparent 70%),
		var(--mat-field);
	overflow: clip;
}

/* The ruling. Beneath the container — .aj-section--dark lifts its own children
   to z-index 1 — and above the section's background, so the vignettes above
   darken the field while the grid stays even across it. */
.aj-family::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	background-image:
		repeating-linear-gradient(90deg, var(--mat-line) 0 1px, transparent 1px var(--mat-cell)),
		repeating-linear-gradient(180deg, var(--mat-line) 0 1px, transparent 1px var(--mat-cell)),
		repeating-linear-gradient(90deg, var(--mat-line-major) 0 1.5px, transparent 1.5px calc(var(--mat-cell) * 5)),
		repeating-linear-gradient(180deg, var(--mat-line-major) 0 1.5px, transparent 1.5px calc(var(--mat-cell) * 5));
	pointer-events: none;
}

.aj-fgrid {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: clamp(1.4rem, 2.4vw, 2.2rem);
	/* The room the stickers overhang into. It used to be the mat plate's top
	   padding; with the plate gone the grid has to carry it, or the Renyum mark —
	   the tallest of the three — lands on the lead paragraph above. */
	padding-top: clamp(3.5rem, 6vw, 5.5rem);
}

/* ----- The cards -----
   Cream rather than white: the stickers carry a white die-cut border, and on a
   white card that border disappears. */
/* Grid stretch only reaches .aj-plx, the direct child — without this the cards
   end at their own copy length, and combined with three different parallax
   factors the row reads as accidental rather than hand-placed. */
.aj-fgrid .aj-plx,
.aj-fgrid .aj-fitem {
	height: 100%;
}

/* The card re-asserts ink. The section is .aj-section--dark now, which sets a
   light colour on the whole band for the head to inherit — and it cascaded
   straight through into the cards, where the <h3> had no colour of its own and
   came out cream on cream. Invisible titles, and invisible in the one place a
   contrast checker aimed at the section's own type would never look. Set here
   rather than on the heading so anything added to a card later inherits ink by
   default instead of repeating this. */
.aj-sticker {
	position: relative;
	box-sizing: border-box;
	height: 100%;
	background: var(--cream);
	color: var(--ink);
	border: 2px solid var(--ink);
	border-radius: 18px;
	padding: 1.9rem 1.7rem 2.1rem;
	box-shadow: 6px 6px 0 rgba(42, 33, 27, .3);
	transform: rotate(var(--tilt, 0deg));
	transition: transform .5s var(--ease), box-shadow .5s var(--ease);
}

.aj-sticker:hover {
	transform: rotate(0) translateY(-5px);
	box-shadow: 8px 13px 0 rgba(42, 33, 27, .34);
}

/* ----- The sticker artwork -----
   A fixed-height box that the art overflows upward out of, so every card
   contributes the same amount to the flow and the copy starts at the same
   line — while the marks themselves stay different sizes. */
.aj-sticker__head {
	display: flex;
	align-items: flex-end;
	height: 3.8rem;
	/* Pulled up through the card's own top padding, otherwise the shorter two
	   marks never clear the card edge and the "stuck on" reading is lost. The
	   art's baseline lands ~24px below the card's top edge, so every mark
	   overhangs by its own height minus that — always visible, never equal. */
	margin-top: -4.2rem;
	margin-bottom: 1.15rem;
}

.aj-sticker__art {
	display: block;
	height: var(--art-h, 5rem);
	width: auto;
	max-width: 100%;
	object-fit: contain;
	object-position: left bottom;
	/* Two rotations, and the order matters as much as the values.
	   --art-skew is the correction for a tilt baked into the artwork itself and
	   is permanent; --art-tilt is the decorative hand-placed angle. They were one
	   variable until the Renyum mark turned out to be 26 degrees off inside its
	   own PNG: a correction that large cannot ride on the channel the hover state
	   below resets to zero, or the mark would snap crooked under the pointer —
	   exactly the fault it exists to fix. */
	transform: rotate(var(--art-skew, 0deg)) rotate(var(--art-tilt, 0deg));
	transform-origin: 50% 60%;
	transition: transform .5s var(--ease), filter .5s var(--ease);
}

/* Lifts further than the card does — the corner of a sticker peeling up. The
   hand tilt comes off; the skew stays, because it was never a tilt. */
.aj-sticker:hover .aj-sticker__art {
	transform: rotate(var(--art-skew, 0deg)) translateY(-7px) scale(1.05);
	filter: drop-shadow(0 12px 16px rgba(23, 18, 16, .34));
}

.aj-sticker__kicker {
	font-family: var(--font-mono);
	font-size: .68rem;
	letter-spacing: .18em;
	text-transform: uppercase;
	/* --gold-deep measures only 2.69:1 on this ground — below AA for text this
	   small. --cocoa is the same move the cream group band already makes. */
	color: var(--cocoa);
}

.aj-sticker h3 {
	margin: .7rem 0 .55rem;
	font-size: 1.45rem;
	font-weight: 800;
	letter-spacing: -.02em;
}

.aj-sticker p {
	margin: 0;
	font-size: .96rem;
	line-height: 1.6;
	color: color-mix(in srgb, var(--ink) 72%, transparent);
}

/* ---------- 21 · Responsive (Milestone 2) ---------- */

/* Video showcase: relax the compositions, then turn each wall into a
   swipeable snap rail once the columns would get too narrow to read. */
@media (max-width: 1080px) {
	/* Five 4:5 frames across is 210px each at this width — too narrow to read a
	   face in. Three per row, and the remaining two wrap onto a second row of
	   their own; every frame keeps the same width, so the rows still line up
	   with each other. */
	.aj-vwall--commercial {
		grid-template-columns: repeat(3, 1fr);
	}

	.aj-vwall--production {
		grid-template-columns: repeat(2, 1fr);
	}

	/* Campaign cases: narrative on top, media below at full width. */
	.aj-imcase--split,
	.aj-imcase--wall {
		grid-template-columns: minmax(0, 1fr);
	}

	.aj-imcase--reverse .aj-imcase__lead,
	.aj-imcase--reverse .aj-imcase__proof,
	.aj-imcase--reverse .aj-imcase__product {
		order: 0;
	}

	.aj-imcase--split .aj-imcase__proof {
		max-width: 460px;
	}

	.aj-imcase--split .aj-imcase__product {
		max-width: 300px;
		justify-self: start;
	}

}

@media (max-width: 820px) {
	.aj-vwall,
	.aj-vspot__wall,
	.aj-imcase__wall {
		grid-template-columns: none;
		grid-auto-flow: column;
		grid-auto-columns: minmax(220px, 66vw);
		overflow-x: auto;
		overscroll-behavior-x: contain;
		scroll-snap-type: x mandatory;
		scroll-padding-inline: var(--gutter);
		padding-inline: var(--gutter);
		margin-inline: calc(var(--gutter) * -1);
		padding-bottom: .5rem;
	}

	/* In a rail every frame is one swipe stop, in document order. The margin and
	   span resets that used to live here are gone from the desktop rules too —
	   nothing is offset or spanned any more, at any width. */
	.aj-vwall > *,
	.aj-vspot__wall > *,
	.aj-imcase__wall > * {
		scroll-snap-align: start;
	}

	.aj-vspot__wall {
		margin-inline: 0;
		padding-inline: 0;
		scroll-padding-inline: 0;
	}

	.aj-vindex a {
		grid-template-columns: 2.6rem 1fr;
		row-gap: .2rem;
	}

	.aj-vindex__meta {
		grid-column: 2;
	}
}

@media (max-width: 940px) {
	.aj-steps,
	.aj-fgrid {
		grid-template-columns: 1fr;
	}

	.aj-fgrid {
		max-width: 460px;
		margin-inline: auto;
		/* Stacked, each sticker needs its own clearance above its card, so the
		   row gap has to cover the tallest overhang rather than one block of
		   top padding doing it once for a whole row. */
		gap: 4.5rem;
		/* Only the first card's overhang is left for the padding to clear. */
		padding-top: clamp(3rem, 9vw, 4.5rem);
	}
}

/* Branding: the wide compositions give way before their columns get too narrow
   to read a wordmark in. The rails stay rails — they are already swipeable. */
@media (max-width: 1180px) {
	/* The numeral and specs drop under the copy rather than squeezing it. */
	.aj-brband {
		grid-template-columns: minmax(0, 1fr);
		gap: clamp(1.2rem, 3vw, 2rem);
	}

	.aj-brband__meta {
		justify-content: flex-start;
	}

	/* Photograph first, then the drawing under it: stacked, the overhang has
	   nothing to lean on, and the caption it would cover is the one thing that
	   names the built work. */
	.aj-arch__inner {
		grid-template-columns: minmax(0, 1fr);
		align-items: start;
	}

	.aj-arch__built {
		order: 1;
	}

	.aj-arch__side {
		order: 2;
	}

	.aj-arch__concept {
		margin-inline-end: 0;
	}

	.aj-arch__built .aj-arch__shot {
		aspect-ratio: 16 / 10;
	}

	.aj-lrow,
	.aj-lboard {
		grid-template-columns: minmax(0, 1fr);
	}

	/* Mirroring means nothing in a single column. */
	.aj-lrow--reverse .aj-lrow__lead,
	.aj-lrow--reverse .aj-lrow__apply {
		order: 0;
	}

	/* Single column: the cap exists to stop a near-square mockup from running
	   taller than the copy beside it, and there is nothing beside it now. Lifting
	   it lets the frame take the full measure instead of shrinking to the cap and
	   leaving the column half empty. */
	.aj-lrow__apply {
		max-height: none;
	}
}

@media (max-width: 860px) {
	/* Below this a 0.80 portrait in a justified row is about 150px wide, which
	   is a thumbnail, not a key visual. One frame per row instead.

	   The hero deliberately keeps its 16:9 at every width. It used to square up
	   to 4:3 here, and cover then took the crop out of both ends — which on a
	   phone cut "KOYO CABE" down to "NASNYA". A short hero beats a cropped one. */
	.aj-jrow__cell {
		flex-basis: 100%;
	}
}

/* Web cases collapse early: below this the narrative column is squeezed
   under its own 300px floor and the screen stops being the wide one. */
@media (max-width: 1080px) {
	.aj-wcase,
	.aj-wcase--reverse {
		grid-template-columns: minmax(0, 1fr);
		gap: clamp(1.6rem, 4vw, 2.2rem);
	}

	/* Screen first in every case once stacked — mirroring means nothing in
	   a single column, and the evidence should lead. */
	.aj-wcase__screen,
	.aj-wcase--reverse .aj-wcase__screen { order: 1; }
	.aj-wcase__lead,
	.aj-wcase--reverse .aj-wcase__lead { order: 2; }

	.aj-wcase__logo {
		margin-top: 1.1rem;
	}
}

/* ---------- 22 · Social Media Accounts ----------
   Nine managed brands, each given a full-bleed panel: the profile screen on one
   side, the narrative and the platform tracks on the other, mirrored every other
   panel so the same anatomy still reads as a rhythm.

   The ground stays espresso. Each brand's colour arrives as light — a soft radial
   thrown from behind its phone — rather than as a band of flat colour. Nine
   saturated fields back to back would turn the page into a slideshow, and gold has
   to stay the accent that carries the numbers. Same reasoning, and the same
   --glow-a / --glow-b channel, as the web development cases above. */

/* ----- Overture: the aggregate, stated before any of the detail ----- */
/* The bottom padding is not decoration: the first brand panel opens with its own
   gold hairline, and without this the rule landed a few pixels under "total views ·
   one month" and read as an underline on the label rather than the edge of the
   next panel. */
.aj-smsum {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: clamp(1.4rem, 3vw, 3rem);
	margin: 0;
	padding-top: clamp(1.8rem, 3vw, 2.6rem);
	padding-bottom: clamp(2.4rem, 5vw, 4rem);
	border-top: 1px solid rgba(209, 177, 128, .2);
}

/* column-reverse, because <dl> wants dt before dd but the eye wants the number
   first. Same trick on .aj-smstat below. */
.aj-smsum__item {
	display: flex;
	flex-direction: column-reverse;
	gap: .45rem;
	min-width: 0;
}

.aj-smsum__num {
	margin: 0;
	font-size: clamp(2rem, 3.4vw, 3rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -.03em;
	color: var(--gold);
	font-variant-numeric: tabular-nums;
}

.aj-smsum__label {
	font-family: var(--font-mono);
	font-size: .68rem;
	line-height: 1.5;
	letter-spacing: .15em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .5);
}

/* ----- Brand panel ----- */
/* Panels sit outside .aj-container so they are already full width; the inner
   element re-applies the container measure, which is what keeps every left edge on
   the same line as the section head. Same arrangement as .aj-impanel. */
/* No overflow clip here, deliberately. A clipping ancestor becomes the sticky
   containing block, which pinned the device to a box that does not scroll — it
   tracked the page 1:1 instead of holding at the nav offset. Everything that used
   to need the clip is now contained by its own box: the glow sits at inset 0, and
   the glare sweeps inside its own overflow-hidden wrapper. */
.aj-smpanel {
	position: relative;
	padding-block: clamp(3.2rem, 6vw, 5.4rem);
	isolation: isolate;
}

/* Hairline drawn left to right as the panel arrives, tinted with the brand's own
   light at its start so the colour is introduced before the glow is noticed. */
.aj-smpanel::before {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 1px;
	background: linear-gradient(90deg, rgba(var(--glow-a), .6), rgba(209, 177, 128, .26) 46%, transparent);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 1.2s var(--ease);
	pointer-events: none;
}

html.js .aj-smpanel.is-in::before,
html:not(.js) .aj-smpanel::before {
	transform: scaleX(1);
}

/* The light is strongest on the device side — the screen is what is throwing it. */
.aj-smpanel__glow {
	position: absolute;
	/* inset 0, not a negative one: the panel no longer clips (see above), so a
	   bleeding box would push light into its neighbours and nine brand colours
	   would start mixing at the seams. The radials already fade out well inside
	   the box, and the blur softens the last of it. */
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background:
		radial-gradient(38% 46% at 17% 40%, rgba(var(--glow-a), .22), transparent 70%),
		radial-gradient(32% 40% at 64% 70%, rgba(var(--glow-b), .12), transparent 68%);
	filter: blur(22px);
	opacity: 0;
	transition: opacity 1.2s var(--ease);
}

.aj-smpanel--reverse .aj-smpanel__glow {
	background:
		radial-gradient(38% 46% at 83% 40%, rgba(var(--glow-a), .22), transparent 70%),
		radial-gradient(32% 40% at 36% 70%, rgba(var(--glow-b), .12), transparent 68%);
}

html:not(.js) .aj-smpanel__glow,
.aj-smpanel__glow.is-in {
	opacity: 1;
}

.aj-smpanel__inner {
	position: relative;
	z-index: 1;
	max-width: var(--container);
	margin-inline: auto;
	padding-inline: var(--gutter);
	display: grid;
	grid-template-columns: minmax(240px, 330px) minmax(0, 1fr);
	grid-template-rows: auto 1fr;
	grid-template-areas:
		"sigil plate"
		"sigil tracks";
	column-gap: clamp(2rem, 4vw, 4.2rem);
	row-gap: clamp(1.8rem, 3.4vw, 2.8rem);
	align-items: start;
}

/* Mirroring needs the tracks reversed as well as the areas — swapping the areas
   alone would leave the sigil in the wide column. */
.aj-smpanel--reverse .aj-smpanel__inner {
	grid-template-columns: minmax(0, 1fr) minmax(240px, 330px);
	grid-template-areas:
		"plate sigil"
		"tracks sigil";
}

.aj-smplate { grid-area: plate; min-width: 0; }
.aj-smpanel__tracks { grid-area: tracks; min-width: 0; }

/* ----- The sigil -----
   The brand's mark, on a plate, inside a ring, lit from behind by the brand's own
   colour. It replaced a CSS-drawn handset holding the account's profile capture —
   inc/social-data.php's anjali_social_sigil() carries the reasoning; what follows is
   only how it is built.

   Sticky, so the mark stays in view while its Instagram and TikTok tracks scroll
   past beside it. It carries no data-parallax on purpose: the plate's transform
   channel is spoken for by the pointer tilt, and sticky is already the movement. */
.aj-smpanel__sigil {
	grid-area: sigil;
	position: sticky;
	top: calc(var(--nav-h) + 2rem);
	min-width: 0;
	/* The stacking context the ghost numeral is negative-z against. Without it the
	   numeral would fall behind .aj-smpanel__glow and stop being visible at all. */
	isolation: isolate;
}

/* The chapter number, ghosted, mostly hidden behind the plate — same device as
   .aj-ghost--quote on the LYMA panel. Outline only: a filled numeral this size
   competes with the mark, and the mark is the subject. */
.aj-smsigil__num {
	position: absolute;
	z-index: -1;
	top: -16%;
	left: -12%;
	font-size: clamp(5.5rem, 11vw, 9rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -.04em;
	color: transparent;
	-webkit-text-stroke: 1px rgba(209, 177, 128, .16);
	pointer-events: none;
	user-select: none;
}

/* Mirrored panels lean the other way, so the numeral goes out the other side. */
.aj-smpanel--reverse .aj-smsigil__num {
	left: auto;
	right: -12%;
}

/* ----- No 3D here any more -----
   This carried `transform-style: preserve-3d` and `perspective: 900px` so the
   plate could lean toward the pointer. Both are gone, and the reason is a
   rendering fault that was reproducible on demand: mid-tilt, the plate split
   along a hard diagonal with one half showing the panel ground through it.

   The plate is a `border-radius: 24%` box with `overflow: hidden`, holding an
   <img>, a veil, a rim built out of `mask-composite: exclude`, and a sheen with
   an overflow clip of its own. Rotate that quad and Chrome rasterises it as two
   triangles; the mask stack inside makes the two disagree, and the seam is where
   they meet. Flattening the rotation alone — every other layer left running —
   removes it completely.

   .aj-dtile runs the same tilt code and is clean, so this was never a fault in
   the tilt; it was this component's own layer stack. That tile keeps its lean.

   What replaces it is in section 22: the pointer moves the LIGHT now, not the
   tile. Which is also what this component already argued for — see the reveal
   note on why a trademark should not read as an object on a 3D card. */
.aj-smsigil {
	position: relative;
	display: grid;
	place-items: center;
	aspect-ratio: 1;
	isolation: isolate;

	/* ----- One shape, three scales -----
	   The client's other note: a squircle plate inside a circular outline reads as
	   two unrelated objects. So the ring is a squircle too, and its radius is
	   derived rather than guessed — a rounded rectangle nested inside another only
	   looks concentric if the outer radius is the inner radius PLUS the gap.

	     plate width          78%  of this box
	     plate radius         24%  of the plate  = 18.72% of this box
	     gap each side        (100 - 78) / 2     = 11%
	     concentric outer     18.72 + 11         = 29.72%

	   Get this wrong and the corners give it away: the gap reads even along the
	   flats and pinches or bulges as it comes round. */
	--sigil-plate: 78%;
	--sigil-r: 24%;
	--sigil-r-outer: 29.72%;
}

/* ----- The light behind the mark -----
   Wider than the sigil and blurred past its own edge, which is what makes it read
   as light thrown onto the ground rather than as a coloured disc sitting on it.

   THIS IS WHAT ANSWERS THE POINTER NOW. The plate used to lean; it holds still,
   and the light behind it leans instead — it drifts toward the reader and comes
   up a step in brightness.

   Putting the response here rather than on the plate is a decision about the
   artwork, not about performance. A highlight drawn ON the tile has to be visible
   over nine different grounds: warm white on Kandang Jarakan's white ground is
   invisible, and gold strong enough to read there tints a client's trademark. The
   light behind the mark has neither problem — it never touches the artwork, and
   it reads identically on a light tile and on LYMA's black one.

   Two elements because two things move: the box drifts, and ::before holds the
   colour, the blur and the breathe. Stacking the drift onto the same transform
   the breathe animates is the collision this file spends most of its comments
   avoiding. */
.aj-smsigil__aura {
	position: absolute;
	inset: -32%;
	/* --lx/--ly arrive from motion.js as percentages of the SIGIL; the shift is a
	   fraction of that, resolved against this box. .18 puts the far edge of the
	   sigil at ~9% of travel — enough to see the light move with you, little
	   enough that the glow never leaves the mark it belongs to. */
	transform:
		translate3d(
			calc((var(--lx, 50%) - 50%) * .18),
			calc((var(--ly, 50%) - 50%) * .18),
			0
		)
		scale(var(--aura-s, 1));
	pointer-events: none;
}

.aj-smsigil__aura::before {
	content: "";
	position: absolute;
	inset: 0;
	/* Squircle, like everything else here. Blur(24px) hides the outline either
	   way, but the falloff should belong to the object being lit rather than to
	   a circle that no longer exists anywhere in the component. */
	border-radius: var(--sigil-r-outer);
	background: radial-gradient(closest-side,
		rgba(var(--glow-a), .34),
		rgba(var(--glow-b), .13) 56%,
		transparent 78%);
	filter: blur(24px);
	transition: filter .5s var(--ease);
}

/* ----- The ring -----
   A 1px squircle, gold, brightest where the raking light would hit it. It was a
   circle until the client pointed out that a circle drawn round a rounded square
   reads as two unrelated objects; the radius that makes it concentric is derived
   on .aj-smsigil above.

   Built as a filled box masked down to its own edge, because a border cannot hold
   a gradient — same construction as .aj-dtile__rim, wrapped one level deeper so
   the entrance stays on the element and the orbit stays off its transform. */
.aj-smsigil__ring {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

/* --orbit, not a rotate on the box. The orbit used to be `transform: rotate()`
   on this pseudo, which is invisible while the ring is a circle — all you see is
   the gradient coming round. A squircle turned 360° is a rounded square visibly
   spinning, which is not the effect anyone wanted. So the angle moves and the
   box holds still. Same device --rim already uses on .aj-tcard__rim. */
.aj-smsigil__ring::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: var(--sigil-r-outer);
	padding: 1px;
	background: conic-gradient(from var(--orbit, 200deg),
		rgba(230, 211, 172, .58),
		rgba(209, 177, 128, .05) 40%,
		rgba(209, 177, 128, .30) 72%,
		rgba(230, 211, 172, .58));
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
}

/* ----- The plate -----
   Square, because every mark ships square and the ground it carries IS the tile —
   see the renderer on why the grounds were kept rather than knocked out. 24% is
   Apple's icon radius rounded up, which is the shape these marks already publish
   under on the platforms this chapter is about.

   78%, so the ring has room to sit outside it rather than on it. */
.aj-smsigil__plate {
	position: relative;
	display: block;
	width: var(--sigil-plate);
	aspect-ratio: 1;
	border-radius: var(--sigil-r);
	overflow: hidden;
	/* Both channels are written by the hover rule now. --tx/--ty used to be here
	   too, written by motion.js on pointer move, and taking the rotation out is
	   what fixed the diagonal seam — see the note on .aj-smsigil above.
	   Variables rather than a literal so the lift and the swell share one
	   transform instead of overwriting each other. */
	transform:
		translate3d(0, var(--lift, 0px), 0)
		scale(var(--plate-s, 1));
	box-shadow:
		0 26px 48px -22px rgba(0, 0, 0, .78),
		0 8px 18px -12px rgba(0, 0, 0, .6);
	transition:
		transform .5s var(--ease),
		box-shadow .5s var(--ease);
}

.aj-smsigil__mark {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	background: var(--espresso);
}

/* ----- The veil -----
   What makes nine unrelated grounds sit in one page. The marks arrived on white,
   cyan, teal and black; at full strength on espresso the light ones glare and the
   dark ones vanish. --veil is set per tone below, and lifts entirely on hover, so
   the brand's real colour is a reward rather than something the page never shows. */
.aj-smsigil__veil {
	position: absolute;
	inset: 0;
	z-index: 1;
	background: linear-gradient(158deg,
		rgba(23, 18, 16, calc(var(--veil, .12) * .55)),
		rgba(23, 18, 16, var(--veil, .12)));
	opacity: 1;
	transition: opacity .55s var(--ease);
	pointer-events: none;
}

/* A light tile has to come down to the palette; a dark one only needs its corners
   knocked back, and gets a brighter rim instead — on espresso it has no other edge. */
.aj-smsigil--light { --veil: .17; }
.aj-smsigil--dark  { --veil: .05; }

/* ----- The rim -----
   The plate's own edge, raked rather than even: brightest top-left, picking up gold
   as it comes round. An even outline reads as a stroke; a raked one reads as an edge
   catching the light. */
.aj-smsigil__rim {
	position: absolute;
	inset: 0;
	z-index: 2;
	border-radius: inherit;
	padding: 1px;
	background: linear-gradient(158deg,
		rgba(230, 211, 172, var(--rim, .55)) 0%,
		rgba(209, 177, 128, calc(var(--rim, .55) * .34)) 30%,
		rgba(209, 177, 128, calc(var(--rim, .55) * .16)) 60%,
		rgba(230, 211, 172, calc(var(--rim, .55) * .72)) 100%);
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
	transition: opacity .55s var(--ease);
	pointer-events: none;
}

.aj-smsigil--light { --rim: .42; }
.aj-smsigil--dark  { --rim: .72; }

/* One diagonal pass of gold across the plate as the mark lands. The element holds
   still and clips; the ::after is what travels, so the sweep can start off the left
   edge without anything escaping the tile.

   Deliberately still the only thing drawn over the artwork, and only for 1.6s on
   arrival. A pointer-tracked highlight was tried here and taken out again: over
   nine client grounds there is no one colour for it — warm white vanishes on a
   white tile, and gold strong enough to read there tints the trademark. The light
   that follows the reader lives behind the mark instead (.aj-smsigil__aura). */
.aj-smsigil__sheen {
	position: absolute;
	inset: 0;
	z-index: 3;
	overflow: hidden;
	border-radius: inherit;
	pointer-events: none;
}

.aj-smsigil__sheen::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(102deg,
		transparent 38%,
		rgba(230, 211, 172, .30) 50%,
		transparent 62%);
	transform: translateX(-130%);
}

/* The handle, and only the handle. The caption used to name a platform because the
   image was that platform's profile screen — a trademark belongs to the brand, and
   the tracks below already say which platforms are run. */
.aj-smsigil__cap {
	margin: clamp(1.1rem, 2vw, 1.6rem) 0 0;
	font-family: var(--font-mono);
	font-size: .64rem;
	line-height: 1.6;
	letter-spacing: .14em;
	text-transform: uppercase;
	text-align: center;
	color: rgba(243, 237, 227, .42);
}

/* ----- Hover: the colour arrives -----
   The veil comes off and the mark is finally shown at full strength, which is the
   whole argument for putting a veil there in the first place. Fine pointers only:
   on a touch screen the finger is on top of the thing being revealed, and hover
   would latch on after the tap and stay. */
@media (hover: hover) and (pointer: fine) {
	.aj-smsigil:hover .aj-smsigil__veil {
		opacity: 0;
	}

	/* The swell is what the 6° lean used to say: the tile has come nearer. 1.5% is
	   deliberately under the threshold where a mark starts to look like it is
	   being zoomed — the lift and the light do the talking, this only backs them. */
	.aj-smsigil:hover .aj-smsigil__plate {
		--lift: -6px;
		--plate-s: 1.015;
		box-shadow:
			0 36px 60px -24px rgba(0, 0, 0, .82),
			0 10px 22px -12px rgba(0, 0, 0, .66);
	}

	/* The edge sharpens with the lift — a panel that has come off the ground catches
	   more of the light, not the same amount. */
	.aj-smsigil:hover .aj-smsigil__rim {
		opacity: 1;
		filter: brightness(1.35);
	}

	/* The light behind the tile comes up to meet the reader. Brightness on the
	   pseudo, because the box it sits in is busy drifting toward the pointer and
	   the breathe is on this same element's animation — filter is the one channel
	   here nothing else has claimed. */
	.aj-smsigil:hover .aj-smsigil__aura::before {
		filter: blur(24px) brightness(1.35);
	}
}

/* ----- Nameplate: the brand is stated once, however many platforms follow ----- */
.aj-smplate__client {
	margin: 0 0 .9rem;
	font-size: clamp(1.7rem, 3.1vw, 2.6rem);
	font-weight: 800;
	letter-spacing: -.02em;
	line-height: 1.05;
}

.aj-smplate__on {
	display: flex;
	flex-wrap: wrap;
	gap: .5rem;
	margin: 0 0 clamp(1.3rem, 2.3vw, 1.8rem);
	padding: 0;
	list-style: none;
}

/* A step below the brand name: this is the angle, not the client. */
.aj-smplate__title {
	margin: 0 0 .7rem;
	font-size: clamp(1.15rem, 1.8vw, 1.45rem);
	font-weight: 700;
	letter-spacing: -.015em;
	line-height: 1.28;
	color: var(--gold-soft);
}

.aj-smplate__copy {
	margin: 0;
	max-width: 58ch;
	font-size: .98rem;
	line-height: 1.65;
	color: rgba(243, 237, 227, .68);
}

/* ----- Platform track ----- */
.aj-smtrack {
	position: relative;
}

.aj-smtrack + .aj-smtrack {
	margin-top: clamp(2.2rem, 4vw, 3.4rem);
	padding-top: clamp(2.2rem, 4vw, 3.4rem);
}

/* Only between siblings — the panel edge already separates one brand from the
   next, so a rule above every track would just add noise. */
.aj-smtrack + .aj-smtrack::before {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 1px;
	background: linear-gradient(90deg, var(--gold-deep), rgba(209, 177, 128, .06) 72%, transparent);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 1.1s var(--ease);
}

html.js .aj-smpanel.is-in .aj-smtrack + .aj-smtrack::before,
html:not(.js) .aj-smtrack + .aj-smtrack::before {
	transform: scaleX(1);
}

.aj-smtrack__label {
	display: flex;
	align-items: center;
	gap: .65rem;
	margin: 0 0 clamp(1.3rem, 2.2vw, 1.8rem);
	font-family: var(--font-mono);
	font-size: .72rem;
	letter-spacing: .2em;
	text-transform: uppercase;
	color: var(--gold);
}

/* The advertising tiles size this glyph as a percentage of their box; here it sits
   in a line of text and has to be sized to the type. */
.aj-smtrack__label .aj-plat__icon {
	width: 15px;
	height: 15px;
	flex: none;
}

.aj-smtrack__handle {
	letter-spacing: .1em;
	text-transform: none;
	color: rgba(243, 237, 227, .42);
}

.aj-smtrack__label::after {
	content: "";
	flex: 1;
	height: 1px;
	background: linear-gradient(90deg, rgba(209, 177, 128, .4), transparent);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform 1s var(--ease) .12s;
}

html.js .aj-smtrack__label.is-in::after,
html:not(.js) .aj-smtrack__label::after {
	transform: scaleX(1);
}

/* The media is the evidence, so it takes the wide track; the numbers sit beside it
   the way they do on the deck slides. */
.aj-smtrack__grid {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(140px, .3fr);
	gap: clamp(1.4rem, 2.8vw, 2.6rem);
	align-items: start;
}

/* One frame size across every rail, whether it holds two items or three. A 1fr
   track let a two-clip rail inflate its frames by half again over a three-clip one,
   which read as though some accounts counted for more — so the track is a third of
   the rail either way, and a pair simply leaves the last third open. */
.aj-smrail {
	--rail-gap: clamp(.7rem, 1.4vw, 1.05rem);
	display: grid;
	grid-auto-flow: column;
	grid-auto-columns: minmax(0, calc((100% - 2 * var(--rail-gap)) / 3));
	justify-content: start;
	gap: var(--rail-gap);
}


/* Gradient bed behind a still while it lazy-loads. Neutral rather than per-brand:
   it is visible for a moment, and the panel glow already carries the colour. */
.aj-vscreen--social {
	background:
		radial-gradient(66% 52% at 32% 16%, rgba(209, 177, 128, .26), transparent 62%),
		linear-gradient(160deg, #4A382A, #221A15 72%);
}

/* ----- The numbers ----- */
.aj-smstats {
	display: grid;
	gap: clamp(1.1rem, 2vw, 1.6rem);
	margin: 0;
}

.aj-smstat {
	display: flex;
	flex-direction: column-reverse;
	gap: .3rem;
	min-width: 0;
	padding-top: clamp(.8rem, 1.4vw, 1.1rem);
	border-top: 1px solid rgba(209, 177, 128, .16);
}

/* The first number needs no rule above it — the track label is already there. */
.aj-smstat:first-child {
	padding-top: 0;
	border-top: 0;
}

.aj-smstat__num {
	margin: 0;
	font-size: clamp(1.9rem, 3.1vw, 2.7rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -.03em;
	color: var(--gold);
	font-variant-numeric: tabular-nums;
}

.aj-smstat__label {
	font-family: var(--font-mono);
	font-size: .63rem;
	line-height: 1.5;
	letter-spacing: .15em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .5);
}

/* ----- Responsive ----- */
/* Below this the numbers column starves the media, so it turns into a row under
   the rail and the rail takes the track's full width. */
@media (max-width: 1399px) {
	.aj-smtrack__grid {
		grid-template-columns: minmax(0, 1fr);
	}

	.aj-smstats {
		grid-template-columns: repeat(3, minmax(0, 1fr));
		gap: clamp(1rem, 2.4vw, 2rem);
	}

	/* As a row every number needs its own rule, including the first. */
	.aj-smstat:first-child {
		padding-top: clamp(.8rem, 1.4vw, 1.1rem);
		border-top: 1px solid rgba(209, 177, 128, .16);
	}

	/* Nothing left to stick to once the panel is this short. */
	.aj-smpanel__sigil {
		position: static;
	}
}

@media (max-width: 1099px) {
	.aj-smpanel__inner,
	.aj-smpanel--reverse .aj-smpanel__inner {
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: auto auto auto;
		grid-template-areas:
			"plate"
			"sigil"
			"tracks";
	}

	/* Between the nameplate and the tracks: the brand has been named, and the
	   numbers that follow are that brand's. Narrower than the phone it replaced —
	   a square centred in a single column reads at the size a 9:19.5 slab needed
	   the full 320 to reach. */
	.aj-smpanel__sigil {
		justify-self: center;
		width: min(260px, 56vw);
	}

	/* Centred, the numeral has copy on both sides of it rather than a margin to
	   hang in, and at 11vw it would sit under the plate on a tablet. */
	.aj-smsigil__num {
		display: none;
	}
}

@media (max-width: 959px) {
	.aj-smsum {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: clamp(1.4rem, 4vw, 2.2rem);
	}
}

/* The rail turns into a swipeable strip rather than squeezing three frames into a
   phone's width. Same treatment as the influencer walls. */
@media (max-width: 759px) {
	.aj-smrail {
		grid-auto-columns: minmax(190px, 60vw);
		justify-content: normal;
		overflow-x: auto;
		scroll-snap-type: x mandatory;
		margin-inline: calc(var(--gutter) * -1);
		padding: 0 var(--gutter) .6rem;
		overscroll-behavior-x: contain;
	}

	.aj-smrail > * {
		scroll-snap-align: start;
	}

	/* The stagger was a composition for a grid; in a scroller it just misaligns. */
	.aj-smrail > :nth-child(2),
	.aj-smrail > :nth-child(3) {
		margin-top: 0;
	}

	.aj-smstat__num {
		font-size: clamp(1.5rem, 6.5vw, 2rem);
	}

	.aj-smstat__label {
		font-size: .58rem;
		letter-spacing: .12em;
	}
}

/* ============================================================
   23 · Page shell — three pages, one document
   ------------------------------------------------------------
   The site was one scroll until the company-profile split; it is Home,
   /work-services/ and /our-family/ now, which means real page loads between
   sections that used to be a wheel turn apart. Everything here exists to keep
   that from reading as three separate sites.
   ============================================================ */

/* Opacity only, deliberately. A lift here would need a transform on <main>,
   and <main> is the ancestor of both sticky elements on this site — the hero
   stage and the chapter index. A transformed ancestor changes what sticky is
   measured against, and no entrance is worth that. The lift is already there:
   every section's own [data-reveal] fires a beat later. */
html.js .aj-page {
	animation: aj-page-in .52s var(--ease) backwards;
}

@keyframes aj-page-in {
	from {
		opacity: 0;
	}
}

/* The outgoing dip that used to live here (html.js.is-leaving) is gone: the
   curtain in section 26 covers the handover now, in both directions and with
   the brand on it. The fade above stays, and is what a load with no curtain
   still gets — every visit after the first of a session. */

/* /our-family/ is one section under a fixed nav — the section's own top padding
   is measured against a hero that is not there. */
.aj-page--solo > .aj-section:first-child {
	padding-top: calc(var(--section) + var(--nav-h));
}

/* Every scroll target's clearance is declared here and nowhere else. Lenis
   honours scroll-margin-top, so the smooth path and the plain path — reduced
   motion, JS off, a #hash pasted into the address bar — land in the same place.
   (Setting an offset in the Lenis call as well counted the nav twice.)
   On /work-services/ the chapter index is pinned under the nav, so its ~4.6rem
   has to be cleared too. */
.aj-page > [id],
.aj-footer {
	scroll-margin-top: calc(var(--nav-h) + 1rem);
}

.aj-page--work > [id] {
	scroll-margin-top: calc(var(--nav-h) + 5.6rem);
}

@media (prefers-reduced-motion: reduce) {
	html.js .aj-page {
		animation: none;
		opacity: 1;
		transition: none;
	}
}

/* ============================================================
   24 · Page lede & chapter index
   ============================================================ */

/* ----- The masthead for /work-services/ -----
   Cream because paper would run into .aj-services underneath and read as one
   block with two heads, and because espresso would cost a second nav theme:
   the nav is transparent until scroll and paints its links in --ink. */
.aj-pagelede {
	position: relative;
	background: var(--cream);
	padding-top: calc(var(--nav-h) + clamp(3.5rem, 7vw, 6rem));
	padding-bottom: clamp(2.6rem, 5vw, 4rem);
}

.aj-pagelede__title {
	max-width: 18ch;
}

/* ----- Chapter index -----
   This page is the longest thing on the site by a wide margin, so its index is
   the one that pins. Pills are .aj-brnav__link — Branding's own index, which
   sits inside this page further down, uses the same component and does not
   pin, so the two read as one object at two scales. */
.aj-pgnav {
	position: sticky;
	top: var(--nav-h);
	z-index: 40;
	background: var(--cream);
	transition: background .35s, box-shadow .35s;
}

.admin-bar .aj-pgnav {
	top: calc(var(--nav-h) + var(--wp-admin--admin-bar--height, 32px));
}

/* Always a rail, never a wrap. Seven pills clear 1180px comfortably and overflow
   below it; a sticky bar that reflows to two rows as the viewport narrows moves
   the content under it, which is the one thing a fixed reference must not do. */
.aj-pgnav__rail {
	display: flex;
	flex-wrap: nowrap;
	gap: .55rem;
	padding-block: .8rem;
	overflow-x: auto;
	overscroll-behavior-x: contain;
	scroll-snap-type: x proximity;
	scrollbar-width: none;
	margin-inline: calc(var(--gutter) * -1);
	padding-inline: var(--gutter);
}

.aj-pgnav__rail::-webkit-scrollbar {
	display: none;
}

.aj-pgnav__link {
	flex: none;
	scroll-snap-align: start;
}

/* Set by motion.js once the bar reaches the nav. Until then it is the bottom
   edge of the cream masthead and should look like it. */
.aj-pgnav.is-stuck {
	background: rgba(250, 247, 241, .88);
	-webkit-backdrop-filter: blur(16px) saturate(1.4);
	backdrop-filter: blur(16px) saturate(1.4);
	box-shadow:
		0 1px 0 rgba(42, 33, 27, .08),
		0 14px 26px -24px rgba(42, 33, 27, .55);
}

/* The Crosslink that used to close this section is gone. It was Home's one door
   out to Work & Services — a row naming the page and going there — and it was
   replaced by the five glass tiles in section 28, each of which opens the
   chapter it names. Nothing else on the site used the object, so it went with
   it rather than sitting here unreferenced. */

/* ============================================================
   25 · MOTION v2 — primitives, parallax, spotlight, lerp
   ============================================================ */

/* ----- Lenis (lerp smooth scroll) ----- */
html.lenis,
html.lenis body {
	height: auto;
}

.lenis.lenis-smooth {
	scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
	overscroll-behavior: contain;
}

.lenis.lenis-stopped {
	overflow: hidden;
}

html.has-lenis {
	scroll-behavior: auto;
}

/* ----- Parallax channel: CSS owns transform, JS only feeds --plx ----- */
[data-parallax] {
	transform: translate3d(0, var(--plx, 0px), 0);
	will-change: transform;
}

/* ----- Video frame: self-owned reveal + hover choreography -----
   data-anim="frame" deliberately has no generic variant, so the only rule the
   variant system aims at these cards is the neutral reset at (0,1,1); every
   property it touches is redeclared below at (0,2,1) or higher. Four layers
   land in sequence — card, chrome, still, caption — staggered across the wall
   by --d. Delays are per-channel: the wall stagger must never slow the card's
   answer to a pointer, so transform/shadow always start at 0s. */
html.js .aj-vframe {
	opacity: 1;
	transform: translateY(0);
	clip-path: inset(0 0 0 0 round var(--radius));
	transition:
		clip-path .9s cubic-bezier(.65, 0, .35, 1) var(--d, 0s),
		opacity .7s var(--ease) var(--d, 0s),
		transform .5s var(--ease),
		box-shadow .5s var(--ease),
		border-color .45s var(--ease);
}

html.js .aj-vframe:not(.is-in) {
	opacity: 0;
	transform: translateY(26px);
	clip-path: inset(10% 6% 10% 6% round var(--radius));
}

/* Same specificity as :not(.is-in) — declared after, so it wins and a card
   hovered mid-reveal eases rather than fights. */
html.js .aj-vframe:hover,
html.js .aj-vframe:focus-visible {
	transform: translateY(-6px);
}

/* Chrome draws itself left to right. clip-path, not scaleX, so the three
   window dots keep their shape while it opens. */
html.js .aj-vframe__bar {
	clip-path: inset(0 0 0 0);
	transition: clip-path .7s var(--ease) calc(var(--d, 0s) + .12s);
}

html.js .aj-vframe:not(.is-in) .aj-vframe__bar {
	clip-path: inset(0 100% 0 0);
}

/* The still settles slower than the card it sits in — that lag is the depth. */
html.js .aj-vframe__poster {
	transform: none;
	transition: opacity .7s var(--ease), transform .9s var(--ease);
}

html.js .aj-vframe:not(.is-in) .aj-vframe__poster {
	transform: scale(1.08);
}

html.js .aj-vframe:hover .aj-vframe__poster {
	transform: scale(1.045);
}

/* Caption arrives last. */
html.js .aj-vframe__info {
	opacity: 1;
	transform: none;
	transition:
		opacity .6s var(--ease) calc(var(--d, 0s) + .22s),
		transform .6s var(--ease) calc(var(--d, 0s) + .22s);
}

html.js .aj-vframe:not(.is-in) .aj-vframe__info {
	opacity: 0;
	transform: translateY(12px);
}

/* ----- Hero scrub: four rates against one number -----
   No transitions anywhere here. --hp already tracks the scroll position frame by frame, so
   easing it would put a lag between the wheel and the image — the one thing that makes a
   scrub feel broken. The differential rates are what read as depth: the copy leaves at
   roughly ten times the scene's rate, the footer sooner still. */
html.js .aj-hero.is-scrub .aj-hero__scene {
	transform: scale(calc(1 + var(--hp, 0) * .14));
}

html.js .aj-hero.is-scrub .aj-hero__inner {
	transform: translate3d(0, calc(var(--hp, 0) * -18%), 0);
	opacity: calc(1 - var(--hp, 0) * 1.15);
}

/* Goes first: it is the invitation to scroll, and it should not still be there once the
   reader has taken it. */
html.js .aj-hero.is-scrub .aj-hero__foot {
	opacity: calc(1 - var(--hp, 0) * 2.4);
}

html.js .aj-hero.is-scrub .aj-hero__veil {
	opacity: calc(var(--hp, 0) * .92);
}

/* ----- Deck: the push-dissolve -----
   The client's reference was a boxed slider that shunts its whole track sideways. This
   stage cannot do that: the headline is fixed on top of the plane, and a plane that
   travels 100% under stationary type drags the type with it perceptually. So the
   direction survives and the mechanism changes — the outgoing frame drifts left and
   blurs out of focus while the incoming one arrives from the right and settles out of a
   6% overscale. Reads as a camera cutting across a set rather than a deck of cards.

   --dir is written by motion.js: 1 for next, -1 for prev, so the same three rules run the
   move in both directions.

   Only .is-active and .is-leaving carry a transition. A frame that is neither has just
   been let go by the dissolve, and it has to snap back to its off-stage position — if it
   animated there it would sail back across the stage at opacity 0, which costs a
   composite every time for something nobody can see. */
html.js .aj-hero__slide {
	transform: translate3d(calc(9% * var(--dir, 1)), 0, 0) scale(1.06);
}

/* Only the two frames taking part get the hint. Standing it on all four would keep four
   screen-sized layers promoted for the whole visit to buy a 1.1s move twice a minute. */
html.js .aj-hero__slide.is-active,
html.js .aj-hero__slide.is-leaving {
	will-change: transform, opacity;
}

html.js .aj-hero__slide.is-active {
	transform: none;
	filter: none;
	transition:
		opacity 1.1s var(--ease),
		transform 1.1s var(--ease),
		filter 1.1s var(--ease);
}

html.js .aj-hero__slide.is-leaving {
	opacity: 0;
	transform: translate3d(calc(-7% * var(--dir, 1)), 0, 0) scale(1.02);
	filter: blur(6px);
	transition:
		opacity .95s var(--ease),
		transform 1.1s var(--ease),
		filter 1.1s var(--ease);
}

/* ----- Deck: Ken Burns -----
   Six seconds is long enough for a still to go dead. The drift is slow enough that it is
   never the thing you notice and only obvious if you look away and back — which is the
   point. Directions alternate down the deck so four frames in a row do not all creep the
   same way and turn the loop into a tic.

   It runs on the media inside the frame, not on the frame, because the frame is already
   carrying the dissolve's transform and the two would overwrite each other. */
@keyframes aj-ken-in {
	from { transform: scale(1); }
	to { transform: scale(1.08); }
}

@keyframes aj-ken-out {
	from { transform: scale(1.08); }
	to { transform: scale(1); }
}

@keyframes aj-ken-pan {
	from { transform: scale(1.07) translate3d(1.5%, 0, 0); }
	to { transform: scale(1.07) translate3d(-1.5%, 0, 0); }
}

html.js .aj-hero__slide.is-active .aj-hero__media {
	animation: aj-ken-in 14s linear both;
}

html.js .aj-hero__slide[data-ken="out"].is-active .aj-hero__media {
	animation-name: aj-ken-out;
}

html.js .aj-hero__slide[data-ken="pan"].is-active .aj-hero__media {
	animation-name: aj-ken-pan;
}

/* Everything the deck drives stops on one class, including the drift mid-frame. A pause
   that froze the rotation but left the image creeping would not read as paused. */
html.js .aj-hero.is-paused .aj-hero__slide.is-active .aj-hero__media,
html.js .aj-hero.is-paused .aj-hero__tick.is-active .aj-hero__tickfill {
	animation-play-state: paused;
}

/* ----- Deck: the dwell readout -----
   An animation rather than a transition, purely so pausing works: animation-play-state
   freezes a bar mid-fill, and there is no equivalent for a transition that does not
   involve reading the computed value back and writing it out again every time. --dwell is
   set per frame by motion.js, because a video frame's bar has to match the length of the
   clip rather than the still dwell. */
@keyframes aj-tick {
	from { transform: scaleX(0); }
	to { transform: scaleX(1); }
}

html.js .aj-hero__tick.is-active .aj-hero__tickfill {
	animation: aj-tick var(--dwell, 6000ms) linear both;
}

/* ----- Nav entrance ----- */
html.js .aj-nav {
	animation: aj-nav-in .9s var(--ease) .15s backwards;
}

@keyframes aj-nav-in {
	from {
		opacity: 0;
		transform: translateY(-12px);
	}
}

/* Hero foot sits at the very bottom of a 100svh scene — reveal it on load
   (IO with negative rootMargin would never fire for it until scroll). */
html.js .aj-hero__foot {
	animation: aj-fade-in 1.1s var(--ease) 1s backwards;
}

@keyframes aj-fade-in {
	from {
		opacity: 0;
	}
}

/* ----- Variant reset: variants own their hidden states ----- */
html.js :where([data-reveal][data-anim]) {
	opacity: 1;
	transform: none;
	transition: none;
}

/* ----- fade (opacity only — safe to combine with parallax) ----- */
html.js :where([data-reveal][data-anim="fade"]) {
	transition: opacity .9s var(--ease);
	transition-delay: var(--d, 0s);
}

html.js :where([data-reveal][data-anim="fade"]:not(.is-in)) {
	opacity: 0;
}

/* ----- blur-in ----- */
html.js :where([data-reveal][data-anim="blur"]) {
	transition:
		opacity .9s var(--ease),
		transform .9s var(--ease),
		filter .9s var(--ease);
	transition-delay: var(--d, 0s);
}

html.js :where([data-reveal][data-anim="blur"]:not(.is-in)) {
	opacity: 0;
	transform: translateY(22px);
	filter: blur(10px);
}

/* ----- clip (clip-path + opacity only — transform stays free) ----- */
html.js :where([data-reveal][data-anim="clip"]) {
	transition:
		clip-path 1.05s cubic-bezier(.65, 0, .35, 1),
		opacity .55s var(--ease);
	transition-delay: var(--d, 0s);
	clip-path: inset(0% 0% 0% 0% round 28px);
}

html.js :where([data-reveal][data-anim="clip"]:not(.is-in)) {
	opacity: 0;
	clip-path: inset(14% 9% 14% 9% round 28px);
}

/* ----- plate (settles with an overshoot) -----
   For objects that are meant to read as physically placed rather than faded in:
   the construction-grid plate that lands on the Aeroville photograph, and the
   mark plates in Logo & branding. Same easing the family stickers settle on. */
html.js :where([data-reveal][data-anim="plate"]) {
	transition:
		opacity .7s var(--ease),
		transform .85s cubic-bezier(.34, 1.56, .64, 1);
	transition-delay: var(--d, 0s);
}

html.js :where([data-reveal][data-anim="plate"]:not(.is-in)) {
	opacity: 0;
	transform: translateY(26px) scale(.965);
}

/* ----- unveil (clip opens from the centre) -----
   Used once, on the Driva identity board: the sheet is drawn back from the
   middle rather than wiped in from an edge. clip-path only, so the board's own
   slow settle out of scale keeps the transform channel to itself. */
html.js :where([data-reveal][data-anim="unveil"]) {
	transition:
		clip-path 1.15s cubic-bezier(.65, 0, .35, 1),
		opacity .6s var(--ease);
	transition-delay: var(--d, 0s);
	clip-path: inset(0 0 0 0 round var(--radius));
}

html.js :where([data-reveal][data-anim="unveil"]:not(.is-in)) {
	opacity: 0;
	clip-path: inset(0 42% 0 42% round var(--radius));
}

/* ----- mask (block slides out of an overflow-hidden wrapper) ----- */
.aj-mask {
	display: block;
	overflow: hidden;
	/* room for descenders (j, p, y) inside the clip, cancelled out of layout */
	padding-bottom: .12em;
	margin-bottom: -.12em;
}

.aj-mask__inner {
	display: block;
	transition: transform 1s cubic-bezier(.65, 0, .35, 1);
	transition-delay: var(--d, 0s);
}

html.js :where([data-anim="mask"]:not(.is-in)) .aj-mask__inner {
	transform: translateY(112%);
}

/* ----- words (per-word stagger; <mark> stays one unit) ----- */
.aj-w {
	display: inline-block;
}

html.js [data-anim="words"] .aj-w {
	transition:
		opacity .65s var(--ease),
		transform .65s var(--ease);
	transition-delay: calc(var(--d, 0s) + var(--wd, 0) * 1ms);
}

html.js [data-anim="words"]:not(.is-in) .aj-w {
	opacity: 0;
	transform: translateY(.45em);
}

/* ----- pop (container staggers its direct children) ----- */
html.js :where([data-reveal][data-anim="pop"]) > * {
	transition:
		opacity .7s var(--ease),
		transform .7s var(--ease);
	transition-delay: calc(var(--d, 0s) + var(--i, 0) * 70ms);
}

html.js :where([data-reveal][data-anim="pop"]:not(.is-in)) > * {
	opacity: 0;
	transform: scale(.92) translateY(10px);
}

html.js [data-anim="pop"] > *:nth-child(1) { --i: 0; }
html.js [data-anim="pop"] > *:nth-child(2) { --i: 1; }
html.js [data-anim="pop"] > *:nth-child(3) { --i: 2; }
html.js [data-anim="pop"] > *:nth-child(4) { --i: 3; }
html.js [data-anim="pop"] > *:nth-child(5) { --i: 4; }
html.js [data-anim="pop"] > *:nth-child(6) { --i: 5; }
html.js [data-anim="pop"] > *:nth-child(7) { --i: 6; }
html.js [data-anim="pop"] > *:nth-child(8) { --i: 7; }

/* ----- Trifecta card: component-owned cascade -----
   data-anim="tcard" follows the data-anim="frame" convention — a variant name
   deliberately left without a generic definition, so the only rule that reaches
   it is the neutral reset at (0,1,1) and the component declares everything else
   here at (0,2,1).

   Two cascades, nested. Card to card the gap has widened twice — .05/.15/.25,
   then 0/.16/.32, now 0/.34/.68 (declared in patterns/trifecta.php). Each time
   for the same reason and each time it was not enough: at .16s the second card
   began before the first had finished its own chip → title → copy → edge run, so
   three arrivals overlapped into one block appearing. .34s clears that run, which
   is what finally makes the set read as three cards switching on in turn.

   THE ENTRANCE GAINED A DIRECTION AND A FOCUS PULL. It used to be translateY
   alone: correct, but it says nothing about where in the row a card sits, so the
   only thing carrying "left to right" was the stagger. -30px of X gives the
   sequence a heading, and the blur is what makes the arrival read as a camera
   finding the plate rather than a box fading up. Both are small on purpose —
   at -60px and 14px of blur the three cards swim, which is a different section.

   filter is on the transition list here and nowhere else on this card, so the
   blur has a channel of its own and never argues with the hover lift below. */
html.js .aj-tcard[data-reveal] {
	opacity: 0;
	transform: translate3d(-30px, 52px, 0) scale(.965);
	filter: blur(8px);
	transition:
		opacity 1.4s var(--ease),
		transform 1.4s var(--ease),
		filter 1.4s var(--ease),
		border-color .4s;
	transition-delay: var(--d, 0s);
}

/* Only while the card is still off-stage. Promoting a blurred, shadowed plate is
   worth it for the 1.4s it is moving and pure cost for the rest of the visit —
   and the property is dropped by the same class change that lands the card, so
   nothing has to remember to clear it. */
html.js .aj-tcard[data-reveal]:not(.is-in) {
	will-change: opacity, transform, filter;
}

html.js .aj-tcard[data-reveal].is-in {
	opacity: 1;
	transform: none;
	filter: none;
}

/* Hover has to win the transform channel back once the card has landed, and it
   can only do that if its own delay is zero — otherwise the lift inherits the
   card's stagger and answers the pointer a third of a second late. */
html.js .aj-tcard[data-reveal].is-in:hover {
	transform: translateY(-10px);
	transition-delay: 0s;
}

/* Slowed with the card and spread wider inside it: .6s over .12/.20/.30 packed
   the three lines into 180ms of each other, which at the card's new pace read as
   the contents popping in after the plate had already settled. .85s over
   .20/.34/.48 keeps the inside of the card moving for as long as the card is. */
html.js .aj-tcard[data-reveal] .aj-tcard__num,
html.js .aj-tcard[data-reveal] :is(h2, h3),
html.js .aj-tcard[data-reveal] p {
	transition:
		opacity .85s var(--ease),
		transform .85s var(--ease);
}

html.js .aj-tcard[data-reveal] .aj-tcard__num { transition-delay: calc(var(--d, 0s) + .20s); }
html.js .aj-tcard[data-reveal] :is(h2, h3) { transition-delay: calc(var(--d, 0s) + .34s); }
html.js .aj-tcard[data-reveal] p { transition-delay: calc(var(--d, 0s) + .48s); }

html.js .aj-tcard[data-reveal]:not(.is-in) .aj-tcard__num {
	opacity: 0;
	transform: translateY(14px);
}

html.js .aj-tcard[data-reveal]:not(.is-in) :is(h2, h3) {
	opacity: 0;
	transform: translateY(18px);
}

html.js .aj-tcard[data-reveal]:not(.is-in) p {
	opacity: 0;
	transform: translateY(16px);
}

html.js .aj-tcard[data-reveal]::before {
	transform: scaleX(0);
	transition: transform 1.15s var(--ease) calc(var(--d, 0s) + .62s);
}

html.js .aj-tcard[data-reveal].is-in::before {
	transform: scaleX(1);
}

/* ----- The rim's one arrival turn -----
   The gold rim was a hover ornament and nothing else, which left the card's own
   cascade ending on a line being drawn. It now also fires once as the card lands:
   the frame lights, takes a single turn, and goes out again.

   Same idiom as .aj-shine on the display heads and .aj-vframe__bar::after on the
   browser cards — one pass of gold, once, at the end of an entrance. It is timed
   to open with the lit edge above (+.62s) and outlast it, so the last thing each
   card does is come up to full brightness and settle.

   Written as an animation rather than a transition because there is no resting
   state to transition to: opacity has to go up AND back down, and --rim has to
   turn all the way round from wherever @property left it. */
html.js .aj-tcard[data-reveal].is-in .aj-tcard__rim {
	animation: aj-rim-arrive 1.7s var(--ease) calc(var(--d, 0s) + .62s) 1 both;
}

@keyframes aj-rim-arrive {
	0% {
		opacity: 0;
		--rim: 0deg;
	}

	38% {
		opacity: .55;
	}

	100% {
		opacity: 0;
		--rim: 360deg;
	}
}

/* Hover has to be able to interrupt the arrival, so it is declared after it and
   claims the same channel. Without this the pointer would be answered only once
   the one-shot had run itself out. */
html.js .aj-tcard[data-reveal].is-in:hover .aj-tcard__rim {
	opacity: 1;
	animation: aj-rim 8s linear infinite;
}

/* ----- Stacked, the stagger is lag -----
   --d is written per card in patterns/trifecta.php and exists to separate three
   plates that enter the viewport on the same frame. Below 820px the grid is one
   column and they do not: each card arrives at its own scroll position, so an
   inherited .34s or .68s is just a card sitting blank for two thirds of a second
   after the reader has already reached it. This was wrong at the old .16/.32 too
   — it only cost less.

   !important because --d is an inline style, and an important author declaration
   is the one thing that outranks one.

   The lateral offset goes with it. -30px reads as direction across a row of
   three; down a single column, where every card comes from the same place, it
   reads as the card having slipped. The blur comes down for the same reason a
   phone is not a cinema — less distance travelled, less focus to pull through.

   Declared here rather than beside the layout rules in section 14: this
   overrides the hidden state above at equal specificity, so it has to come
   after it in the file. */
@media (max-width: 820px) {
	html.js .aj-tcard[data-reveal] {
		--d: 0s !important;
		transform: translate3d(0, 44px, 0) scale(.975);
		filter: blur(5px);
	}
}

/* ----- Trifecta head -----
   The rule draws from the centre outwards. transform-origin is the whole point:
   every other drawn line on this page runs left-to-right because it sits under
   ranged-left type, and the same origin under a centred title reads as the line
   having slipped. Its delay lands it just after the title has finished rising. */
html.js .aj-trifecta__rule[data-reveal] {
	transform: scaleX(0);
	transform-origin: center;
	transition: transform 1s var(--ease);
	transition-delay: var(--d, 0s);
}

html.js .aj-trifecta__rule[data-reveal].is-in {
	transform: scaleX(1);
}

/* The diamond arrives last and on its own channel — it is the full stop on the
   head, and a mark that scales in with its own line never registers as one. */
html.js .aj-trifecta__rule[data-reveal]::after {
	opacity: 0;
	transform: rotate(45deg) scale(.2);
	transition:
		opacity .55s var(--ease),
		transform .7s var(--ease);
	transition-delay: calc(var(--d, 0s) + .55s);
}

html.js .aj-trifecta__rule[data-reveal].is-in::after {
	opacity: 1;
	transform: rotate(45deg) scale(1);
}

/* ----- Trifecta aurora -----
   Lights and opens as the set arrives, ahead of the cards' own stagger, so the
   ground is already lit when the first plate lands on it. Slower than anything
   on the cards (2.1s against .85s) because light coming up in a room is not an
   object arriving — if it moves at the cards' pace it reads as a fourth card.

   Slower again, and starting earlier (.2s → .1s), now that the cards themselves
   run at 1.4s. The relationship is what matters rather than the numbers: the bed
   has to still be brightening while card 01 arrives, or the light stops being
   what the plates land into and becomes something that happened before them. */
html.js .aj-trifecta__grid[data-reveal]::before {
	opacity: 0;
	transform: scale(.86);
	transition:
		opacity 1.8s var(--ease) .1s,
		transform 2.1s var(--ease) .1s;
}

html.js .aj-trifecta__grid[data-reveal].is-in::before {
	opacity: 1;
	transform: none;
}

/* ----- marquee entrance ----- */
html.js :where([data-reveal][data-anim="slide-l"]),
html.js :where([data-reveal][data-anim="slide-r"]) {
	transition:
		opacity 1.1s var(--ease),
		transform 1.1s var(--ease);
	transition-delay: var(--d, 0s);
}

html.js :where([data-reveal][data-anim="slide-l"]:not(.is-in)) {
	opacity: 0;
	transform: translateX(70px);
}

html.js :where([data-reveal][data-anim="slide-r"]:not(.is-in)) {
	opacity: 0;
	transform: translateX(-70px);
}

/* ----- auto draw-lines (no markup needed) ----- */
html.js .aj-eyebrow[data-reveal]::before {
	transform: scaleX(0);
	transform-origin: left;
	transition: transform .7s var(--ease) calc(var(--d, 0s) + .25s);
}

html.js .aj-eyebrow[data-reveal].is-in::before {
	transform: scaleX(1);
}

html.js .aj-mono-label[data-reveal]::after {
	transform: scaleX(0);
	transform-origin: left;
	transition: transform .9s var(--ease) calc(var(--d, 0s) + .3s);
}

html.js .aj-mono-label[data-reveal].is-in::after {
	transform: scaleX(1);
}

/* Services rows: border-bottom becomes a drawn line. */
.aj-service {
	border-bottom: 0;
	position: relative;
}

.aj-service::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 1px;
	background: rgba(42, 33, 27, .12);
	transform-origin: left;
}

html.js .aj-service[data-reveal]::after {
	transform: scaleX(0);
	transition: transform 1s var(--ease) calc(var(--d, 0s) + .2s);
}

html.js .aj-service[data-reveal].is-in::after {
	transform: scaleX(1);
}

/* Case stat: gold underline draws after the number lands. */
.aj-stat__num::after {
	content: "";
	display: block;
	width: 64px;
	height: 3px;
	margin-top: .6rem;
	border-radius: 2px;
	background: linear-gradient(90deg, var(--gold), transparent);
	transform-origin: left;
}

html.js .aj-case__stats[data-reveal] .aj-stat__num::after {
	transform: scaleX(0);
	transition: transform .8s var(--ease) .55s;
}

html.js .aj-case__stats[data-reveal].is-in .aj-stat__num::after {
	transform: scaleX(1);
}

/* Ads: connectors draw between the three steps. */
.aj-step {
	position: relative;
}

.aj-step:nth-child(1)::after,
.aj-step:nth-child(2)::after {
	content: "";
	position: absolute;
	top: 50%;
	right: -1.1rem;
	width: 1.1rem;
	height: 1px;
	background: rgba(169, 139, 87, .55);
	transform-origin: left;
}

html.js .aj-step[data-reveal]::after {
	transform: scaleX(0);
	transition: transform .7s var(--ease) .55s;
}

html.js .aj-step[data-reveal].is-in::after {
	transform: scaleX(1);
}

/* ----- LYMA highlighter: gold sweeps left→right after the words land ----- */
html.js [data-anim="words"] mark {
	background-repeat: no-repeat;
	background-size: 0% 100%;
	color: rgba(243, 237, 227, .85);
	transition:
		background-size .9s cubic-bezier(.65, 0, .35, 1) .95s,
		color .5s ease 1.2s;
}

html.js [data-anim="words"].is-in mark {
	background-size: 100% 100%;
	color: var(--espresso);
}

/* ----- one-shot gold shimmer (hero + footer titles) ----- */
.aj-shine {
	background-image:
		linear-gradient(115deg, #E8D6AF 5%, #D1B180 45%, #B3925F 90%),
		linear-gradient(100deg, transparent 42%, rgba(255, 255, 255, .5) 50%, transparent 58%);
	background-size: 100% 100%, 220% 100%;
	background-position: 0 0, -140% 0;
	background-repeat: no-repeat;
	-webkit-background-clip: text;
	background-clip: text;
}

html.js .aj-shine.is-in {
	animation: aj-shine 1.5s var(--ease) calc(1.1s + var(--d, 0s)) 1;
}

@keyframes aj-shine {
	to {
		background-position: 0 0, 240% 0;
	}
}

/* ----- spotlight cards (class added by JS on fine pointers) -----
   ::after, not ::before. JS only ever adds .aj-spot to .aj-tcard, and that card
   spends ::before on its lit top edge — declared earlier in the file, so this
   block used to win and quietly delete the edge on every pointer device. The two
   are separate elements now and both survive. */
.aj-spot {
	position: relative;
	z-index: 0;
}

.aj-spot::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: inherit;
	/* Wider and warmer than it was: on a plate that is now ~20% larger the old
	   240px pool was a coin of light in the middle of a card rather than the card
	   responding to the pointer. */
	background: radial-gradient(340px circle at var(--mx, 50%) var(--my, 40%), rgba(209, 177, 128, .22), transparent 66%);
	opacity: 0;
	transition: opacity .45s;
	pointer-events: none;
}

.aj-spot:hover::after {
	opacity: 1;
}

/* ----- gold button sheen ----- */
.aj-btn--gold {
	position: relative;
	overflow: hidden;
}

.aj-btn--gold::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .45) 50%, transparent 60%);
	transform: translateX(-130%);
	transition: transform .7s var(--ease);
	pointer-events: none;
}

.aj-btn--gold:hover::after {
	transform: translateX(130%);
}

/* ----- play ring pulse ----- */
.aj-vframe:hover .aj-media__play {
	animation: aj-pulse 1.5s ease-out infinite;
}

@keyframes aj-pulse {
	0% {
		box-shadow: 0 0 0 0 rgba(243, 237, 227, .35);
	}

	100% {
		box-shadow: 0 0 0 22px rgba(243, 237, 227, 0);
	}
}

/* ----- video lightbox (markup built on first open by motion.js) ----- */
html.lightbox-open {
	overflow: hidden;
}

.aj-lightbox {
	position: fixed;
	inset: 0;
	z-index: 200;
	display: grid;
	place-items: center;
	padding: clamp(1rem, 4vw, 3rem);
	opacity: 0;
	visibility: hidden;
	transition: opacity .35s var(--ease), visibility 0s linear .35s;
}

.aj-lightbox.is-open {
	opacity: 1;
	visibility: visible;
	transition: opacity .35s var(--ease);
}

.aj-lightbox__backdrop {
	position: absolute;
	inset: 0;
	border: 0;
	padding: 0;
	background: rgba(12, 9, 8, .84);
	-webkit-backdrop-filter: blur(14px);
	backdrop-filter: blur(14px);
	cursor: zoom-out;
}

/* --lb-ar / --lb-arn are written by motion.js from the frame's data-ar, so a
   clip that is not 16/9 (the web recordings are 2:1) fills the player instead
   of sitting inside black bars. Both default to 16/9 when absent. */
.aj-lightbox__stage {
	position: relative;
	width: min(100%, 1080px);
	width: min(100%, calc((100vh - 11rem) * var(--lb-arn, 1.7778)), 1080px);
	width: min(100%, calc((100svh - 11rem) * var(--lb-arn, 1.7778)), 1080px);
}

.aj-lightbox--portrait .aj-lightbox__stage {
	width: min(100%, 440px);
	width: min(100%, calc((100vh - 11rem) * 9 / 16), 440px);
	width: min(100%, calc((100svh - 11rem) * 9 / 16), 440px);
}

.aj-lightbox__frame {
	position: relative;
	aspect-ratio: var(--lb-ar, 16 / 9);
	border-radius: var(--radius);
	overflow: hidden;
	background: #000;
	border: 1px solid rgba(243, 237, 227, .14);
	box-shadow: 0 50px 100px -34px rgba(0, 0, 0, .85);
	transform-origin: var(--ox, 50%) var(--oy, 50%);
	transform: scale(.93);
	opacity: 0;
	transition: transform .45s var(--ease), opacity .3s var(--ease);
}

.aj-lightbox--portrait .aj-lightbox__frame {
	aspect-ratio: 9 / 16;
}

.aj-lightbox.is-open .aj-lightbox__frame {
	transform: none;
	opacity: 1;
}

.aj-lightbox__frame iframe {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	border: 0;
}

/* ----- Photo mode -----
   The same overlay, carrying a picture instead of a player. Only three things
   differ, and each is there because a photograph is not a video:

   · object-fit is contain, not cover. The whole point of the client's request
     is that the reader can finally see the frame the 4:5 thumbnail cropped, so
     nothing here may crop it again.
   · the ground is warm dark rather than #000. A player's letterbox should be
     black because that is what a screen's own bars are; a photograph shown
     whole is an object on a surface, and the surface is the site's.
   · the stage is sized from the picture's own ratio (--lb-ar, written by
     motion.js from data-ar before the file loads), so the frame never opens at
     16/9 and then jumps.

   Both media live in the DOM at once, so each mode has to actively hide the
   other — otherwise a stale iframe sits behind the photograph. */
.aj-lightbox__img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
	opacity: 0;
	transition: opacity .45s var(--ease);
}

.aj-lightbox__img.is-loaded {
	opacity: 1;
}

.aj-lightbox:not(.aj-lightbox--photo) .aj-lightbox__img {
	display: none;
}

.aj-lightbox--photo .aj-lightbox__frame iframe {
	display: none;
}

.aj-lightbox--photo .aj-lightbox__frame {
	background: #14100E;
}

/* Wider than the 1080px player: a still has no 16/9 habit to respect, and the
   branding boards and shopfront strips are the reason this exists — those run
   to 2.13:1 and were the frames the old 4:5 window hurt most. */
.aj-lightbox--photo .aj-lightbox__stage {
	width: min(100%, 1320px);
	width: min(100%, calc((100vh - 11rem) * var(--lb-arn, 1.3333)), 1320px);
	width: min(100%, calc((100svh - 11rem) * var(--lb-arn, 1.3333)), 1320px);
}

.aj-lightbox__meta {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	align-items: baseline;
	gap: .4rem 1.2rem;
	margin-top: 1rem;
	color: var(--light);
}

.aj-lightbox__text {
	display: grid;
	gap: .2rem;
	min-width: 0;
}

.aj-lightbox__title {
	font-weight: 700;
	letter-spacing: -.01em;
}

.aj-lightbox__note {
	font-family: var(--font-mono);
	font-size: .7rem;
	letter-spacing: .14em;
	text-transform: uppercase;
	color: rgba(243, 237, 227, .6);
}

.aj-lightbox__btn {
	display: grid;
	place-items: center;
	width: 46px;
	height: 46px;
	border-radius: 50%;
	border: 1px solid rgba(243, 237, 227, .28);
	background: rgba(23, 18, 16, .5);
	-webkit-backdrop-filter: blur(8px);
	backdrop-filter: blur(8px);
	color: var(--light);
	font-size: 1.1rem;
	line-height: 1;
	cursor: pointer;
	transition: background .35s var(--ease), border-color .35s var(--ease), transform .35s var(--ease);
}

.aj-lightbox__btn:hover {
	background: var(--gold);
	border-color: var(--gold);
	color: var(--espresso);
	transform: scale(1.06);
}

.aj-lightbox__close {
	position: absolute;
	top: -3.6rem;
	right: 0;
}

.aj-lightbox__nav {
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
}

.aj-lightbox__nav:hover {
	transform: translateY(-50%) scale(1.06);
}

.aj-lightbox__nav--prev {
	left: clamp(-4rem, -5vw, -1rem);
}

.aj-lightbox__nav--next {
	right: clamp(-4rem, -5vw, -1rem);
}

.aj-lightbox__nav[hidden] {
	display: none;
}

.aj-lightbox__controls {
	display: flex;
	gap: .6rem;
}

/* Too narrow for side arrows — they drop into the caption row instead. */
@media (max-width: 900px) {
	.aj-lightbox__nav {
		position: static;
		transform: none;
	}

	.aj-lightbox__nav:hover {
		transform: scale(1.06);
	}

	.aj-lightbox__meta {
		align-items: center;
		flex-wrap: nowrap;
	}

	.aj-lightbox__close {
		top: -3.4rem;
	}

	.aj-lightbox__btn {
		width: 42px;
		height: 42px;
	}
}

/* ----- panel backdrop settles out of a slow push-in ----- */
html.js .aj-impanel:not(.is-in) .aj-impanel__bg img {
	transform: scale(1.12);
	opacity: .5;
}

/* ----- product shots float, suspended on the dark ground -----
   Three channels, three owners so nothing collides: the .aj-vplx wrapper holds
   the parallax transform, this animation owns the image's own transform (an
   animation outranks the variant reset's `transform: none`), and data-anim
   ="fade" only ever touches opacity. */
.aj-imcase__shot {
	animation: aj-float 6s var(--ease) infinite alternate;
}

@keyframes aj-float {
	from {
		transform: translateY(-9px);
	}

	to {
		transform: translateY(9px);
	}
}

/* ----- sticker pop-rotate (settles into its resting tilt) -----
   Animation, not transition, so the hover transition on the same element is
   left free: once the keyframes finish the element falls back to its base
   rule and :hover can take the transform channel over cleanly. */
html.js .aj-fitem[data-reveal].is-in .aj-sticker {
	animation: aj-sticker-in .9s cubic-bezier(.34, 1.56, .64, 1) var(--d, 0s) backwards;
}

@keyframes aj-sticker-in {
	from {
		opacity: 0;
		transform: scale(.82) rotate(4deg);
	}
}

/* The artwork is pressed on after the card has landed — same trick, later. */
html.js .aj-fitem[data-reveal].is-in .aj-sticker__art {
	animation: aj-sticker-art-in .8s cubic-bezier(.34, 1.56, .64, 1) calc(var(--d, 0s) + .32s) backwards;
}

/* The skew rides along, or the mark arrives crooked and straightens as it lands —
   the one moment where the correction would be most obvious. */
@keyframes aj-sticker-art-in {
	from {
		opacity: 0;
		transform: rotate(var(--art-skew, 0deg)) rotate(calc(var(--art-tilt, 0deg) * 3.5)) translateY(18px) scale(.62);
	}
}

/* The mat used to unroll from its centre — a clip-path wipe on .aj-mat__bg, the
   photograph layer. Both are gone: the mat is the section's own background now,
   and a section-wide ground has nothing to unroll from. What is left of that
   choreography is the stickers pressing on one after another, which was always
   the part doing the work. */

/* ----- the proof arrives from off the edge -----
   data-anim="proof" has no generic variant, so the only rule the variant system aims at
   it is the neutral reset — everything it does is declared here, the same contract
   data-anim="sigil" follows below.

   It replaced data-anim="pop-out", which is gone from this file: that variant rose,
   overshot and settled onto a -7deg tilt, choreography for an object small enough to be
   picked up and put down. At 46vw the same bounce reads as cheap, and the lean now comes
   from the perspective in the photograph rather than from a rotate.

   So it slides in from the edge it is flush against and decelerates for a second and
   a half — long enough that the reader watches it settle instead of finding it
   already there. The mask on .aj-cproof does not move with it, so the 7% of travel
   happens behind a falloff that stays welded to the section's edges.

   Opacity finishes well before the transform, deliberately: the proof is legible
   while the movement is still resolving, so the panel never looks like it is waiting
   for an animation to finish before it can be read. */
html.js .aj-cproof__img[data-reveal] {
	opacity: 1;
	transform: translateX(0) scale(1);
	transition:
		opacity 1.15s var(--ease) .06s,
		transform 1.5s cubic-bezier(.16, 1, .3, 1) .06s;
}

html.js .aj-cproof__img[data-reveal]:not(.is-in) {
	opacity: 0;
	transform: translateX(7%) scale(1.05);
}

/* ----- brief 01: the family lands, the lamb leaps in over them -----
   Scoped by the variant name, not by the class. .aj-cstage__base is shared with the
   Changcuters band, which arrives on a plain fade and should keep doing so — the band
   stands in front of a lit screen where a rise would read as the whole panel shifting.

   The family used to fade too, which was the right answer at 719px and the wrong one at
   877: at that size a fade reads as the picture switching on. It rises instead — 24px,
   bottom-anchored, so the movement resolves into the floor line and reads as weight
   settling rather than as a slide.

   No scale on it, deliberately. Scaling a cut-out of people through its entrance makes
   the perspective breathe, and at this size that is visible. */
html.js .aj-cstage__base[data-anim="land"] {
	opacity: 1;
	transform: translateY(0);
	transition:
		opacity 1.1s var(--ease) var(--d, 0s),
		transform 1.4s cubic-bezier(.16, 1, .3, 1) var(--d, 0s);
}

html.js .aj-cstage__base[data-anim="land"]:not(.is-in) {
	opacity: 0;
	transform: translateY(24px);
}

/* The lamb inherits the character of the retired pop-out variant with a third more
   travel: 26px read as a hop on the animal at its old size, and it reveals against a
   family half again as large now. Overshooting easing, which is the point — this is the
   one thing on the page that is mid-air, and a spring landing is what says so.

   Its resting tilt is declared here rather than in the layout rules because this is the
   only place that writes this element's transform channel; a rotate set anywhere else
   would be wiped the moment it reveals. */
html.js .aj-cstage__pop[data-anim="leap"] {
	opacity: 1;
	transform: translateY(0) scale(1) rotate(var(--leap-tilt, -2deg));
	transition:
		opacity .9s var(--ease) var(--d, 0s),
		transform 1.25s cubic-bezier(.34, 1.56, .64, 1) var(--d, 0s);
}

html.js .aj-cstage__pop[data-anim="leap"]:not(.is-in) {
	opacity: 0;
	transform: translateY(34px) scale(.92) rotate(calc(var(--leap-tilt, -2deg) - 6deg));
}

/* ----- brief 03: the board comes to rest -----
   Scale only, and downward: it arrives a touch large and settles, which on a suspended
   object reads as coming to rest at its own distance. No translate — the board has no
   floor to land on, and no rotate, because the lean is in the photograph and turning it
   would move where the light falls. Long, because 516px of board is slow. */
html.js .aj-guarantee__shot[data-reveal] {
	opacity: 1;
	transform: scale(1);
	transition:
		opacity 1.1s var(--ease) var(--d, 0s),
		transform 1.6s cubic-bezier(.16, 1, .3, 1) var(--d, 0s);
}

html.js .aj-guarantee__shot[data-reveal]:not(.is-in) {
	opacity: 0;
	transform: scale(1.05);
}

/* The pair arrives after the board, one behind the other — .34s and .46s off the
   markup. Order is the argument: the board is what the panel is about and the two
   photographs are what backs it up, so they cannot land first or together.

   A rise and nothing else. No scale, no tilt, no overshoot. This is the panel that
   carries the promise about not getting paid, and everything in it is deliberately the
   most controlled motion on the page — the two springs and the bounce belong to brief
   01, where the subject is a lamb in a jumper. */
html.js .aj-gstill[data-reveal] {
	opacity: 1;
	transform: translateY(0);
	transition:
		opacity .9s var(--ease) var(--d, 0s),
		transform 1.1s cubic-bezier(.16, 1, .3, 1) var(--d, 0s);
}

html.js .aj-gstill[data-reveal]:not(.is-in) {
	opacity: 0;
	transform: translateY(22px);
}

/* The subject standing on the ground settles a beat behind the panel itself; that lag is
   what reads as depth. Opacity only here — the base is placed with insets and has no
   transform of its own to preserve, but writing to that channel anyway would cost the
   drop-shadow its compositing layer for nothing. */
html.js .aj-cstage__base[data-reveal] {
	transition: opacity 1s var(--ease);
}

/* ----- LYMA brisket: suspended, with the board's own warm light ----- */
html.js .aj-guarantee__glow.is-in {
	animation: aj-glow-breathe 30s var(--ease) infinite alternate;
}

/* ----- social panels: brand light breathes, the sigil ignites -----
   The glow reuses the web cases' keyframes: opacity is spoken for by the reveal, so
   the breathing lives on the transform channel, and two periods keep a mirrored pair
   of panels from falling into step. */
html.js .aj-smpanel__glow.is-in {
	animation: aj-glow-breathe 28s var(--ease) infinite alternate;
}

html.js .aj-smpanel--reverse .aj-smpanel__glow.is-in {
	animation-duration: 36s;
	animation-direction: alternate-reverse;
}

/* ----- data-anim="sigil" -----
   The handset that used to stand here arrived like an object: turned away, then
   landing on a resting tilt. A trademark is not an object and reading it as one is
   what makes a logo on a 3D card look cheap — so this arrives as light instead.
   Four layers in sequence, each on its own element, no shared channel:

     aura  0s     the light is thrown first, before there is anything to light
     ring  .12s   the circle opens out of it
     plate .20s   the tile resolves out of blur
     mark  .40s   the mark comes into focus inside it
     sheen .95s   one pass of gold, once
     cap   1.05s  the handle annotates what has landed

   The variant system aims nothing at data-anim="sigil" but the neutral reset, so
   every value it uses is declared here. */
html.js .aj-smsigil[data-reveal] {
	opacity: 1;
	transition: opacity .5s var(--ease);
}

html.js .aj-smsigil[data-reveal]:not(.is-in) {
	opacity: 0;
}

/* The light, first and slowest — it is the thing the rest arrives into.
   --aura-s rather than a literal scale(): this element's transform also carries
   the pointer drift, and the entrance has to be able to share it. */
html.js .aj-smsigil__aura {
	opacity: 1;
	--aura-s: 1;
	transition:
		opacity 1.3s var(--ease),
		transform 1.3s var(--ease);
}

html.js .aj-smsigil:not(.is-in) .aj-smsigil__aura {
	opacity: 0;
	--aura-s: .78;
}

/* Once the reader is on it, the drift has to answer the pointer rather than the
   entrance — 1.3s of easing on a value that changes every frame is the lag the
   old tilt had. Short and linear while tracking, and the entrance curve is long
   finished by then anyway. */
html.js .aj-smsigil:hover .aj-smsigil__aura {
	transition: transform .25s linear;
}

/* Then it breathes, from the size the entrance left it at — a keyframe starting
   anywhere else would jump the moment the delay expires.
   ---
   On ::before now, not on the box. The box's transform belongs to the drift and
   the entrance; an animation on the same channel outranks both and would pin the
   aura dead centre the moment the breathe started. Scaling the pseudo scales the
   light inside the box, which is what was ever wanted.
   Two periods so a mirrored pair of panels never falls into step. */
html.js .aj-smsigil.is-in .aj-smsigil__aura::before {
	animation: aj-sigil-breathe 22s var(--ease) 1.4s infinite alternate;
}

html.js .aj-smpanel--reverse .aj-smsigil.is-in .aj-smsigil__aura::before {
	animation-duration: 30s;
}

@keyframes aj-sigil-breathe {
	from { transform: scale(1); }
	to   { transform: scale(1.09); }
}

/* Then the circle, opening out of the light it was drawn in. */
html.js .aj-smsigil__ring {
	opacity: 1;
	transform: scale(1);
	transition:
		opacity 1.1s var(--ease) .12s,
		transform 1.1s var(--ease) .12s;
}

html.js .aj-smsigil:not(.is-in) .aj-smsigil__ring {
	opacity: 0;
	transform: scale(.84);
}

/* The orbit lives on the pseudo so it never touches the entrance transform above.
   48 seconds is slow enough that nobody watches it happen and long enough that the
   edge is never lit the same way twice on the way down the page. */
html.js .aj-smsigil.is-in .aj-smsigil__ring::before {
	animation: aj-sigil-orbit 48s linear infinite;
}

/* Registered so the angle can be interpolated at all — an unregistered custom
   property is a string and jumps from one keyframe to the next. Browsers without
   @property hold it at 200deg and get a static gold frame, which is the same
   ornament minus the turn; exactly the trade --rim already makes in the Trifecta. */
@property --orbit {
	syntax: "<angle>";
	inherits: false;
	initial-value: 200deg;
}

/* ----- Idle animations stop when the sigil is not on screen -----
   Measured before this existed: the nine sigils on Work & Services accounted for
   eighteen of the page's forty never-ending animations, and aj-sigil-breathe is
   the expensive one — it scales a surface carrying filter: blur(24px), which has
   to be re-rastered every frame it moves. All of that ran forever once .is-in
   landed, whether or not the mark was anywhere near the viewport.

   Paused rather than removed: the class is toggled both ways by an observer in
   motion.js, so a sigil scrolled back into view resumes mid-phase instead of
   restarting. Nothing about how it looks changes. */
html.js .aj-smsigil.is-idle .aj-smsigil__aura::before,
html.js .aj-smsigil.is-idle .aj-smsigil__ring::before {
	animation-play-state: paused;
}

/* The angle travels, the box does not. Rotating the element was fine while the
   ring was a circle — a turning circle is only its gradient turning — but the
   ring is a squircle now, and a rounded square spinning through 360° is a
   different, much worse ornament. 200 → 560 is one full turn from where the
   gradient is declared. */
@keyframes aj-sigil-orbit {
	to { --orbit: 560deg; }
}

/* The plate resolves rather than lands — blur out, no travel. The mark inside it
   carries the movement, which is the right way round: the tile is a window and the
   thing arriving is what is seen through it. */
html.js .aj-smsigil__plate {
	opacity: 1;
	filter: blur(0);
	transition:
		transform .5s var(--ease),
		box-shadow .5s var(--ease),
		opacity .9s var(--ease) .2s,
		filter .9s var(--ease) .2s;
}

html.js .aj-smsigil:not(.is-in) .aj-smsigil__plate {
	opacity: 0;
	filter: blur(9px);
}

/* .is-tilting used to live here, swapping the entrance curves for a .12s linear
   one while the pointer drove the plate's rotation frame by frame. Nothing writes
   the plate's transform per frame any more — the lift and the swell are two hover
   states on a .5s ease, and the thing that follows the pointer is a background
   position on .aj-smsigil__sheen, which carries no transition at all. So there is
   no target to chase and no second timing to switch to.

   The class was also the second half of the bug the client reported. It was
   toggled by pointerenter/leave on the PLATE — 78% of the box, with rounded
   corners — while every hover style above it keys off :hover on .aj-smsigil, the
   full square. Crossing a corner turned the lean off while the tile stayed lifted,
   and moving the tile changed its own hit area, so the two states argued at the
   edges. Both listeners are on .aj-smsigil now: one region, one truth. */

/* The mark comes into focus inside the tile, a beat after it. */
html.js .aj-smsigil__mark {
	opacity: 1;
	transform: scale(1);
	transition:
		opacity .85s var(--ease) .4s,
		transform 1.1s var(--ease) .4s;
}

html.js .aj-smsigil:not(.is-in) .aj-smsigil__mark {
	opacity: 0;
	transform: scale(1.06);
}

/* One pass of gold across the plate, once, after the mark has resolved. No forwards
   fill: when the animation ends the sheen returns to its parked position off the
   left edge and stays there. */
html.js .aj-smsigil.is-in .aj-smsigil__sheen::after {
	animation: aj-sigil-sheen 1.6s var(--ease) .95s backwards;
}

@keyframes aj-sigil-sheen {
	from { transform: translateX(-130%); }
	to   { transform: translateX(130%); }
}

/* The ghost numeral drifts in from behind the plate rather than fading in place —
   the only entrance in the column with any travel in it, which is what keeps it
   reading as scenery rather than as a second subject. */
html.js .aj-smsigil__num {
	opacity: 1;
	transform: translate3d(0, 0, 0);
	transition:
		opacity 1.2s var(--ease) .3s,
		transform 1.4s var(--ease) .3s;
}

html.js .aj-smpanel:not(.is-in) .aj-smsigil__num {
	opacity: 0;
	transform: translate3d(0, 26px, 0);
}

/* The caption is the last thing to arrive — it annotates what has landed. */
html.js .aj-smsigil__cap {
	opacity: 1;
	transition: opacity .8s var(--ease) 1.05s;
}

html.js .aj-smpanel:not(.is-in) .aj-smsigil__cap {
	opacity: 0;
}

/* ----- ghost words (decorative parallax type) ----- */
.aj-ghost {
	position: absolute;
	top: 5%;
	right: -1%;
	z-index: 0;
	font-size: clamp(7rem, 20vw, 17rem);
	font-weight: 800;
	line-height: 1;
	letter-spacing: -.04em;
	color: transparent;
	-webkit-text-stroke: 1px rgba(209, 177, 128, .13);
	white-space: nowrap;
	pointer-events: none;
	user-select: none;
}

/* Grown, because it is the only thing standing in brief 03's right-hand ground and at
   26rem it was not standing so much as sitting in the corner. A closing quote's ink is
   about a quarter of its own em box — so 26rem put roughly 116px of mark into a void
   600px wide, which reads as an afterthought rather than as an ornament.

   The stroke doubles with it. Outline type does not scale: 1px around a 416px glyph
   already read as faint, and the same hairline around a 768px one would disappear.
   Alpha comes down a notch in exchange, so the mark gains presence without gaining
   weight against the type across from it.

   Dropped from -4% to 2% as well. It has 0.1 of parallax on it, which walks it down
   the panel as the reader scrolls, and starting it lower means the drift lands it in
   the middle of the empty ground rather than carrying it out of the panel. */
.aj-ghost--quote {
	top: 2%;
	right: 4%;
	font-size: clamp(12rem, 36vw, 48rem);
	-webkit-text-stroke: 2px rgba(209, 177, 128, .15);
}

/* ----- clients "50+" recolor (count span stays ink, plus stays gold) ----- */
.aj-clients__num span {
	color: inherit;
}

.aj-clients__num .aj-plus {
	color: var(--gold-deep);
}

/* ----- mobile drawer: the choreography -----
   The panel wipes, then the contents arrive behind it. Two beats, not one: a
   list that fades in with its own ground reads as a dissolve, and this site
   does not dissolve — it draws things.

   Delays live only under .menu-open, so opening is staggered and closing is
   not. Everything leaves together, faster than it came, and the wipe catches up
   with it. That asymmetry is the same one --curtain-ease-close exists for.

   --i comes off the markup (style="--i:0|1|2"), so adding a fourth destination
   needs no CSS. The foot picks up where the list stopped. */
@media (max-width: 880px) {
	html.js .aj-menu__link,
	html.js .aj-menu__foot > * {
		opacity: 0;
		transform: translateY(18px);
		transition:
			opacity .5s var(--ease),
			transform .5s var(--ease);
	}

	html.js.menu-open .aj-menu__link,
	html.js.menu-open .aj-menu__foot > * {
		opacity: 1;
		transform: none;
	}

	html.js.menu-open .aj-menu__link {
		transition-delay: calc(.14s + var(--i, 0) * .07s);
	}

	/* The rule under each row draws with its row, a beat behind the type. */
	html.js .aj-menu__link::after {
		transition-delay: 0s;
	}

	html.js.menu-open .aj-menu__link::after {
		transition-delay: calc(.22s + var(--i, 0) * .07s);
	}

	html.js.menu-open .aj-menu__foot > :nth-child(1) { transition-delay: .38s; }
	html.js.menu-open .aj-menu__foot > :nth-child(2) { transition-delay: .45s; }
}

/* Nothing moves, but the panel still has to arrive and leave — otherwise the
   drawer is permanently visible or permanently gone. clip-path stays as the
   one carrier, cut to a duration that reads as a state change rather than
   as an animation. */
@media (prefers-reduced-motion: reduce) {
	.aj-menu {
		transition:
			clip-path .01s linear,
			visibility 0s linear .01s !important;
	}

	html.menu-open .aj-menu {
		transition: clip-path .01s linear, visibility 0s !important;
	}

	html.js .aj-menu__link,
	html.js .aj-menu__foot > *,
	html.js .aj-menu__link::after {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}

	.aj-menu__link::after {
		transform: scaleX(1) !important;
	}
}

/* ----- responsive & motion hygiene ----- */
@media (max-width: 879px) {
	.aj-ghost {
		display: none;
	}

	.aj-step:nth-child(1)::after,
	.aj-step:nth-child(2)::after {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	html.js .aj-nav,
	html.js .aj-hero__foot,
	html.js .aj-shine.is-in,
	html.js .aj-fitem[data-reveal].is-in .aj-sticker,
	html.js .aj-guarantee__glow.is-in,
	.aj-vframe:hover .aj-media__play {
		animation: none !important;
	}

	html.js [data-anim] .aj-w,
	html.js [data-anim] .aj-mask__inner,
	html.js :where([data-reveal][data-anim]),
	html.js :where([data-reveal][data-anim]) > * {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		clip-path: none !important;
		transition: none !important;
	}

	html.js [data-anim="words"] mark {
		background-size: 100% 100% !important;
		color: var(--espresso) !important;
		transition: none !important;
	}

	/* ----- The deck, without motion -----
	   The deck is not switched off here, it is de-animated. Rotation stops (motion.js
	   never arms the timer), the drift stops, and the push becomes a plain 200ms
	   cross-fade — but the arrows and the ticks still work, because a reader who has
	   asked for less movement has not asked to be shown one frame out of four. */
	html.js .aj-hero__slide {
		transform: none !important;
		filter: none !important;
	}

	html.js .aj-hero__slide.is-active,
	html.js .aj-hero__slide.is-leaving {
		transition: opacity .2s linear !important;
	}

	html.js .aj-hero__slide.is-active .aj-hero__media,
	html.js .aj-hero__tick.is-active .aj-hero__tickfill {
		animation: none !important;
	}

	/* The bar cannot fill against a clock that is not running, so it reports position
	   instead of progress: this frame is the one you are on. */
	html.js .aj-hero__tick.is-active .aj-hero__tickfill {
		transform: scaleX(1);
	}

	/* !important outranks the frame choreography whatever its specificity. */
	.aj-vplx,
	.aj-vframe,
	.aj-vframe__bar,
	.aj-vframe__bar span,
	.aj-vframe__url,
	.aj-vframe__poster,
	.aj-vframe__info,
	.aj-imcase__shot,
	.aj-impanel__bg img,
	.aj-wcase__glow,
	.aj-lightbox .aj-lightbox__frame {
		opacity: 1 !important;
		transform: none !important;
		clip-path: none !important;
		transition: none !important;
		animation: none !important;
	}

	/* The screen stays awake rather than waiting to be approached. */
	html.js .aj-vframe--site .aj-vframe__poster {
		filter: none !important;
	}

	/* Branding: no hover zoom, no drift. The two board rules are stated with
	   their full specificity because the ones they undo are, too. */
	.aj-brimg,
	.aj-kv__shot,
	.aj-jrow__shot,
	.aj-arch__shot,
	.aj-arch__plate,
	.aj-rail__shot,
	.aj-lrow__plate,
	.aj-lrow__apply,
	.aj-lboard__shot,
	html.js .aj-lboard__shot[data-reveal] .aj-brimg--board,
	html.js .aj-lboard__shot[data-reveal].is-in:hover .aj-brimg--board {
		transform: none !important;
		transition: none !important;
		animation: none !important;
		opacity: 1 !important;
		clip-path: none !important;
	}

	/* The blueprint is already drawn, and the rail's progress bar stops
	   chasing the scroll. */
	.aj-gridfield {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}

	.aj-rail__fill {
		transition: none !important;
	}

	.aj-imcase + .aj-imcase::before,
	.aj-imgroup::after,
	.aj-smpanel::before,
	.aj-smtrack + .aj-smtrack::before,
	html.js .aj-smtrack__label::after,
	.aj-cpanel::before,
	.aj-lightband::after,
	.aj-case__num::after {
		transform: scaleX(1) !important;
		transition: none !important;
	}

	/* The stage's second subject is already seated, the proof already at the edge,
	   the brisket's light already on. The resting tilt goes with it: it is carried on
	   the same transform channel as the arrival, and there is no way to keep one
	   without the other. The feather on .aj-cproof is not motion and stays — without
	   it the phone would be cut straight across at both seams. */
	html.js .aj-cstage__base[data-reveal],
	html.js .aj-cstage__base[data-reveal]:not(.is-in),
	html.js .aj-cstage__pop[data-reveal],
	html.js .aj-cstage__pop[data-reveal]:not(.is-in),
	html.js .aj-cproof__img[data-reveal],
	html.js .aj-cproof__img[data-reveal]:not(.is-in),
	html.js .aj-gstill[data-reveal],
	html.js .aj-gstill[data-reveal]:not(.is-in),
	html.js .aj-guarantee__shot[data-reveal],
	html.js .aj-guarantee__shot[data-reveal]:not(.is-in),
	html.js .aj-guarantee__glow {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
		animation: none !important;
	}

	/* The brand light is already on, the mark already resolved and square to the
	   reader. blur has to be named as well as transform: the plate arrives out of a
	   9px blur, and a filter left running is the one channel a transform reset does
	   not reach. */
	html.js .aj-smpanel__glow,
	html.js .aj-smsigil[data-reveal],
	html.js .aj-smsigil[data-reveal]:not(.is-in),
	html.js .aj-smsigil__aura,
	html.js .aj-smsigil:hover .aj-smsigil__aura,
	html.js .aj-smsigil:not(.is-in) .aj-smsigil__aura,
	html.js .aj-smsigil__aura::before,
	html.js .aj-smsigil.is-in .aj-smsigil__aura::before,
	html.js .aj-smsigil__ring,
	html.js .aj-smsigil:not(.is-in) .aj-smsigil__ring,
	html.js .aj-smsigil.is-in .aj-smsigil__ring::before,
	html.js .aj-smsigil__plate,
	html.js .aj-smsigil:not(.is-in) .aj-smsigil__plate,
	html.js .aj-smsigil__mark,
	html.js .aj-smsigil:not(.is-in) .aj-smsigil__mark,
	html.js .aj-smsigil__num,
	html.js .aj-smpanel:not(.is-in) .aj-smsigil__num,
	html.js .aj-smsigil__cap,
	html.js .aj-smpanel:not(.is-in) .aj-smsigil__cap {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
		animation: none !important;
	}

	/* The aura is light, not motion — its blur is what makes it light, so only the
	   plate's entrance blur is cleared. */
	html.js .aj-smsigil__plate,
	html.js .aj-smsigil:not(.is-in) .aj-smsigil__plate {
		filter: none !important;
	}

	.aj-smsigil__sheen {
		display: none;
	}

	/* The stickers are already pressed on. transform is reset to the skew rather
	   than to none: the skew is not motion, it is the correction that makes the
	   Renyum wordmark level, and zeroing it here would leave that one mark 26
	   degrees crooked for every reader who asked for less movement. */
	html.js .aj-fitem[data-reveal].is-in .aj-sticker__art,
	.aj-sticker__art {
		animation: none !important;
		transform: rotate(var(--art-skew, 0deg)) !important;
		transition: none !important;
		opacity: 1 !important;
	}

	.aj-imgroup::before {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}

	/* The brown field holds still. */
	html.js .aj-vrun,
	html.js .aj-vrun::before,
	.aj-marquee__logo img {
		animation: none !important;
		transform: none !important;
		transition: none !important;
	}

	.aj-vframe__bar::after,
	.aj-vindex a::after {
		display: none;
	}

	.aj-vindex a:hover .aj-vindex__name,
	.aj-lightbox__btn:hover,
	.aj-lightbox__nav:hover {
		transform: none !important;
	}

	html.js .aj-eyebrow[data-reveal]::before,
	html.js .aj-mono-label[data-reveal]::after,
	html.js .aj-service[data-reveal]::after,
	html.js .aj-case__stats[data-reveal] .aj-stat__num::after,
	html.js .aj-tcard[data-reveal]::before,
	html.js .aj-trifecta__rule[data-reveal],
	html.js .aj-step[data-reveal]::after {
		transform: scaleX(1) !important;
		transition: none !important;
	}

	/* The Trifecta cascade hides children that carry no [data-reveal] of their
	   own, so the global reveal reset above never reaches them. Same for the head:
	   the rule's diamond and the aurora bed are both pseudo-elements, which the
	   reset cannot see either. They ship in their finished state. */
	html.js .aj-tcard[data-reveal] .aj-tcard__num,
	html.js .aj-tcard[data-reveal] :is(h2, h3),
	html.js .aj-tcard[data-reveal] p,
	html.js .aj-trifecta__grid[data-reveal]::before {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}

	html.js .aj-trifecta__rule[data-reveal]::after {
		opacity: 1 !important;
		transform: rotate(45deg) !important;
		transition: none !important;
	}

	/* Pointer ornaments come off entirely: both answer a moving cursor, and a
	   turning rim is exactly the kind of continuous motion this query is set. */
	.aj-spot::after,
	.aj-tcard__rim {
		display: none !important;
	}

	[data-parallax] {
		transform: none !important;
		will-change: auto;
	}
}

/* ============================================================
   26 · Intro curtain & page handover
   ------------------------------------------------------------
   Two uses of one object. On the first visit of a session the curtain closes,
   holds on the badge, and opens — the brand moment the client asked for. On
   every internal link after that it closes, the document is swapped behind it,
   and it opens again carrying the same badge, smaller and without the overture.

   The whole choreography is @keyframes. JS decides *which* state to run and
   stamps a class on <html> before first paint; it never animates anything.
   Same division of labour as --hp, --plx and --rp elsewhere in this file.

   States (set by the inline head script in functions.php):
     html.aj-intro    close → six beats → open, full-size badge
     html.aj-enter    open only, small badge  (arrived from an internal link)
     html.aj-leaving  close only, small badge (about to navigate away)

   Light, not espresso — reworked at the client's request, and it was the right
   note anyway: all three pages open on a pale band (the hero's #FCFAF5, the two
   inner ledes' --cream), so the old dark overlay was parting to reveal a page
   two hundred points lighter than itself. The cost of going light is that a
   white sheet has no edge against a near-white ground and a white disc has no
   edge against a white sheet. Both are solved below, and neither is solved with
   an outline: the sheets cast a shadow ahead of themselves, and the disc gets
   the shadow the client asked for plus a hairline ring that draws itself.
   ============================================================ */

:root {
	/* One source for both readings, so they stay in step. --curtain-close is
	   also what motion.js waits out before it swaps the document (CURTAIN_CLOSE
	   = 420), which is why the leaving state below reads the token rather than
	   carrying its own literal. */
	--curtain-close: .42s;
	--curtain-open: .55s;
	/* Weighted going out, released coming back. A curtain that closes on the
	   same ease it opens with reads as a wipe, not as a curtain. */
	--curtain-ease-close: cubic-bezier(.76, 0, .24, 1);
	/* The shadow the disc floats on: contact, mid, cast. Brown-based, never
	   grey — on a page whose entire palette is warm neutrals a grey shadow is
	   the only cold thing on screen and it reads as dirt under the logo. */
	--curtain-shadow:
		0 2px 8px rgba(42, 33, 27, .06),
		0 16px 36px -14px rgba(107, 79, 58, .16),
		0 44px 88px -30px rgba(42, 33, 27, .30);
}

/* Default is display:none, so the markup printed at wp_body_open is inert until
   a class arrives. Only the inline script can set those classes — which means
   JS off, and ?qa=1 which never prints the markup at all, both get the page
   exactly as it was before this section existed. No extra rule needed. */
.aj-curtain {
	display: none;
	position: fixed;
	inset: 0;
	z-index: 900;
	overflow: hidden;
	pointer-events: none;
}

html.aj-intro .aj-curtain,
html.aj-enter .aj-curtain,
html.aj-leaving .aj-curtain {
	display: block;
}

/* Swallow clicks only while the reader cannot see what they would be hitting.
   Released the moment the panels start parting, so the page is live before it
   is uncovered rather than a beat after. */
html.aj-intro .aj-curtain,
html.aj-leaving .aj-curtain {
	pointer-events: auto;
}

html.aj-intro,
html.aj-intro body {
	overflow: hidden;
}

/* When a curtain runs it owns the entrance, so the page-shell fade from
   section 23 has to stand down: the panels parting already uncovers the page,
   and every section has its own [data-reveal] behind that. The fade stays the
   right answer when no curtain runs, which is now its only job.

   Keyed on aj-curtained — permanent — and NOT on the state classes, which are
   removed the moment the curtain finishes. Keyed on those, this rule stopped
   applying at the end of the intro, animation-name went from none back to
   aj-page-in, and a CSS animation starts the instant its name becomes non-none:
   so the finished, fully visible page dropped to opacity 0 and faded up again.
   Measured at 4489-4864ms after a cold load and 767-1156ms after a nav click —
   a 0.4s white flash of the paper body background, several seconds after the
   reader was already looking at the page.

   The same reasoning is why motion.js must not clear aj-curtained on bfcache
   restore: doing so would re-arm the identical flash on the back button. */
html.aj-curtained .aj-page {
	animation: none;
}

/* ---------- The ground ----------
   Opaque from the first byte. On a cold load the panels sweep over *this*,
   never over a half-painted document — which is the whole reason it exists.
   Cream, the site's own band ground, so the intro reads as part of the site
   rather than as a splash screen bolted in front of it. */
.aj-curtain__field {
	position: absolute;
	inset: 0;
	/* Deeper than the panels, deliberately, which is the same relationship the
	   espresso version had and it holds for the same reason: during the close
	   the gap between the sheets is the only place this shows, and if it matches
	   them the whole gesture disappears — two white panels sliding over a white
	   ground is a fade wearing a curtain's costume. Against cream they read as
	   lit sheets converging. Cream is already what .aj-family and the
	   /work-services/ lede sit on, so the reveal lands on a related tone. */
	background:
		radial-gradient(72% 54% at 50% 46%, rgba(209, 177, 128, .16), transparent 72%),
		var(--cream);
}

.aj-curtain__field::after {
	content: "";
	position: absolute;
	inset: 0;
	background-image: var(--grain);
	/* multiply, not overlay. Overlay grain works by lightening and darkening
	   around mid-grey; on a ground this pale there is almost nothing above the
	   pivot for it to act on and the texture vanishes. Multiply only darkens,
	   which is what tooth on paper actually does. */
	opacity: .04;
	mix-blend-mode: multiply;
}

/* On the enter/leave states the field would be a second dark layer behind the
   panels doing nothing — and on leave it would flash over the outgoing page
   before the panels ever arrive. There, the panels carry their own ground. */
html.aj-enter .aj-curtain__field,
html.aj-leaving .aj-curtain__field {
	opacity: 0;
}

/* ---------- The panels ----------
   50.4% each: a hair of overlap so no sub-pixel line of page shows through
   where they meet. Lighter than the field on purpose — two sheets travelling
   across a deeper stage is what makes this read as a curtain and not a fade. */
.aj-curtain__panel {
	position: absolute;
	top: 0;
	bottom: 0;
	width: 50.4%;
	background: linear-gradient(var(--pdir, 90deg), #FFF 0%, #FCFAF6 78%);
	/* The leading-edge problem, solved physically rather than drawn. A white
	   sheet crossing a near-white ground has no edge of its own, and the obvious
	   fix — a hairline down the leading side — is wrong twice over: it is still
	   sitting there at rest, and because the sheets overlap by 0.8% it would put
	   two gold lines about 11px apart with the seam between them, which reads as
	   a rendering fault. A cast shadow has neither problem. It is only visible
	   where it falls on the ground ahead of the sheet, and at rest it falls on
	   the other panel and takes itself off screen. */
	box-shadow: var(--pshadow);
}

.aj-curtain__panel--l {
	left: 0;
	--pdir: 90deg;
	--pshadow: 18px 0 40px -10px rgba(42, 33, 27, .20);
}

.aj-curtain__panel--r {
	right: 0;
	--pdir: 270deg;
	--pshadow: -18px 0 40px -10px rgba(42, 33, 27, .20);
}

/* The leading edge: a soft gold wash travelling ahead of each sheet — the move
   .aj-imgroup::before makes across its cream band, stood on its end. Raised
   from .22 to .30 with the ground: --gold carries roughly nine times the
   contrast on espresso that it does on white, so the old value was a wash
   nobody would have seen. */
.aj-curtain__panel::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	width: clamp(60px, 9vw, 150px);
	background: linear-gradient(var(--pdir, 90deg), transparent, rgba(209, 177, 128, .30));
}

.aj-curtain__panel--l::after {
	right: 0;
}

.aj-curtain__panel--r::after {
	left: 0;
}

/* ---------- The seam ----------
   The hairline this site draws under every band it owns, stood vertical. */
.aj-curtain__seam {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 50%;
	width: 2px;
	margin-left: -1px;
	/* Deepest in the middle, not brightest. On espresso the seam read as a lit
	   filament and --gold-soft was the right end of the ramp; on white the ramp
	   inverts — --gold measures 1.9:1 here and --gold-deep 3.3:1 — so the darker
	   token carries the centre and the lighter one fades it out at the ends. */
	background: linear-gradient(180deg, transparent, var(--gold) 14%, var(--gold-deep) 50%, var(--gold) 86%, transparent);
	opacity: 0;
	transform: scaleY(0);
}

/* ---------- The stage ---------- */
.aj-curtain__stage {
	position: absolute;
	inset: 0;
	display: grid;
	place-content: center;
	justify-items: center;
	padding: var(--gutter);
	opacity: 0;
}

/* ----- The badge -----
   One asset, at the client's request, replacing the mark and wordmark this
   stage used to assemble from two crops. It is a true circle in the file —
   measured off the source, not assumed: a 1866px disc of white with the lockup
   set inside it and everything outside the circle transparent. That matters
   twice over. border-radius:50% on the image traces its edge exactly, and
   anything round stacked behind it lines up without needing a mask.

   Sizing is set by what is *inside* the circle rather than by the circle. The
   lockup occupies 78.8% of the diameter, so a 560px disc renders it at 441px —
   about the optical weight the old min(460px, 66vw) pair carried. The vh cap is
   for short landscape viewports, where 78vw alone would push the disc past the
   top and bottom edges.

   This element owns one transform channel: the slow breathing drift. The
   release lives on __stage above it and the entrance on the image below it.
   Stacking two channels on one box is the collision that has already cost this
   project four separate bugs. */
.aj-curtain__badge {
	position: relative;
	/* Declared as a token because the ring below has to do arithmetic with it,
	   and a second copy of this expression is a second thing to forget. */
	--badge-d: min(560px, 78vw, 62vh);
	width: var(--badge-d);
	aspect-ratio: 1;
}

.aj-curtain__badge-img {
	position: relative;
	z-index: 2;
	width: 100%;
	height: auto;
	border-radius: 50%;
}

/* ----- The shadow -----
   The thing the client asked for, and the thing that makes a white disc legible
   on a white sheet at all. It is a separate element rather than a box-shadow on
   the image because here the shadow is a *beat*: it has to bloom from tight to
   wide while the disc lifts, and animating box-shadow repaints a 560px box with
   an 88px blur on every frame of that. Held on its own layer the identical
   bloom is a scale and an opacity, which the compositor already does for free.

   Base state is the settled one — visible, full size — so the two transition
   states get a correct shadow without declaring anything. Only the intro
   overrides it, and only because the intro is where it has somewhere to go. */
.aj-curtain__shade {
	position: absolute;
	inset: 0;
	z-index: 0;
	border-radius: 50%;
	box-shadow: var(--curtain-shadow);
}

/* ----- The ring -----
   Beat V, and the second half of the white-on-white answer: the shadow says the
   disc is floating, the ring says where its edge is. Sits 2.4% clear of the
   image rather than on it, so the hairline never has to compete with the raster
   circle's own antialiased edge.

   non-scaling-stroke is load-bearing. The viewBox is 100 units across and the
   element renders at up to 560px, so a plain stroke-width:1 would come out at
   5.6px — a band, not a hairline. */
.aj-curtain__ring {
	position: absolute;
	inset: -2.4%;
	z-index: 1;
	overflow: visible;
	/* Start the draw at twelve o'clock rather than at three. */
	transform: rotate(-90deg);
}

.aj-curtain__ring circle {
	fill: none;
	stroke: var(--gold-deep);
	stroke-width: 1;
	/* The dash has to be in screen pixels, not in viewBox units. non-scaling-
	   stroke moves the *whole* stroke into screen space — which is the point of
	   it for stroke-width and a trap for stroke-dasharray. The obvious literal,
	   311.02 (2πr for r=49.5 in user units), rendered as a 311px dash repeating
	   around a path 1817px long: three arcs chasing each other instead of one
	   line drawing itself.
	   So it is derived from the rendered diameter instead. The ring box is the
	   disc inset by -2.4% a side and the circle sits at r=49.5 of 100, giving a
	   circumference of 2π × .495 × 1.048 × --badge-d = 3.2595 × --badge-d — and
	   then the breathing drift on the parent scales that by up to 1.018, so the
	   real worst case is 3.318. 3.36 is that plus about 1% of headroom, because
	   the two errors are not symmetrical: a dash marginally longer than the path
	   closes the ring cleanly and finishes a few frames early, while a dash
	   marginally shorter leaves a permanent gap at twelve o'clock. */
	--ring-c: calc(var(--badge-d) * 3.36);
	stroke-dasharray: var(--ring-c);
	stroke-dashoffset: var(--ring-c);
}

/* ----- The ripple -----
   Beat VI. One ring, once, expanding off the drawn one after it closes — the
   gesture that lets the composition settle instead of simply stopping. Not a
   loop: a pulse that repeats is a spinner, and a spinner is a promise that
   something is still loading. */
.aj-curtain__ripple {
	position: absolute;
	inset: -2.4%;
	z-index: 1;
	border: 1px solid var(--gold-deep);
	border-radius: 50%;
	opacity: 0;
}

/* The transition carries the disc alone. The ring and the ripple were written
   for a hold that only exists once a session; in front of a 0.42s gesture they
   would be a four-beat overture attached to a door swing. */
html.aj-enter .aj-curtain__ring,
html.aj-enter .aj-curtain__ripple,
html.aj-leaving .aj-curtain__ring,
html.aj-leaving .aj-curtain__ripple {
	display: none;
}

html.aj-enter .aj-curtain__badge,
html.aj-leaving .aj-curtain__badge {
	--badge-d: min(230px, 46vw);
}

/* Scaled to the disc. At 230px the intro's 88px cast is most of the disc again
   in soft brown, which stops reading as lift and starts reading as blur. */
html.aj-enter .aj-curtain__shade,
html.aj-leaving .aj-curtain__shade {
	box-shadow:
		0 2px 6px rgba(42, 33, 27, .05),
		0 10px 24px -10px rgba(107, 79, 58, .14),
		0 22px 44px -20px rgba(42, 33, 27, .22);
}

/* ============================================================
   The three states
   ============================================================ */

/* The curtain is opaque from the first byte, but it does not start moving until
   the inline script says the document is parsed (it adds .aj-run). Without this
   the choreography races the load it exists to cover — on a slow response the
   panels finish parting before there is anything behind them to reveal, and the
   reader gets a curtain opening onto a blank page.

   Scoped to the overlay's own six elements, so it cannot reach page content. */
html.aj-intro .aj-curtain *,
html.aj-enter .aj-curtain * {
	animation-play-state: paused;
}

html.aj-run .aj-curtain * {
	animation-play-state: running;
}

/* ----- The intro, in six beats -----
   6.26s end to end, up from 4.34s, because the client found the old one too
   quick to register. Every duration and delay below is mirrored by the timeline
   the inline head script uses to schedule its own teardown — INTRO_OPEN 5600,
   INTRO_END 6300. Change one, change both.

   The extra 1.9s arrived at the same time as a constraint that removed two of
   the old beats: the rule and the label are gone, because the badge asset now
   carries the whole lockup and the client wanted nothing beside it. That is
   1.4s of choreography deleted from a timeline being asked to grow by 1.9s, so
   the naive answer — hold the finished logo for the difference — would have
   produced about 2.5s of a static image. A held frame does not read as
   deliberate; it reads as loading, which is the exact note being complained
   about, only longer.

   So the time went back into the two elements the composition already had. The
   shadow and the ring stop being decoration and become beats of their own, each
   slower than anything the old timeline ran:

     I    0.00–0.78  the sheets close
     II   0.70–1.42  the seam ignites and settles
     III  1.10–2.40  the disc lands
     IV   1.15–2.85  its shadow blooms — finishing *after* the disc stops
     V    2.55–3.85  the ring draws itself, one revolution
     VI   3.80–4.90  one ripple leaves the closed ring
     VII  4.90–5.20  hold
     VIII 5.20–6.26  release, then the sheets part

   III and IV are deliberately out of step, and that offset is where the whole
   gesture lives. The disc stops at 2.40s but its shadow keeps spreading until
   2.85s, which is what a thing coming to rest above a surface actually looks
   like. Matched, they would read as one fade with a blur attached.

   V starts at 2.55s rather than overlapping III, because the disc is still
   travelling 22px until 2.40s and a ring drawn around a moving target lands
   around where the target used to be.

   What this buys, measured: the disc is on screen for 4.4s (2.86s before), and
   nothing on the stage is asked to sit still for longer than 0.3s. */

/* I · close */
html.aj-intro .aj-curtain__panel--l {
	animation:
		aj-curtain-close-l .78s var(--curtain-ease-close) both,
		aj-curtain-open-l .66s var(--ease) 5.6s both;
}

html.aj-intro .aj-curtain__panel--r {
	animation:
		aj-curtain-close-r .78s var(--curtain-ease-close) both,
		aj-curtain-open-r .66s var(--ease) 5.6s both;
}

/* II · ignition — the seam flares as the sheets land, then settles to a
   hairline and goes out with the ground. A gold line left hanging over the page
   as the panels part would read as a rendering fault, not as a seam. */
html.aj-intro .aj-curtain__seam {
	animation:
		aj-curtain-seam .72s var(--ease) .7s both,
		aj-curtain-seam-out .2s linear 5.56s both;
}

/* The stage is only the envelope: its children own their own entrances, so all
   it does is expose them and, at the end, release everything at once. */
html.aj-intro .aj-curtain__stage {
	animation: aj-curtain-stage .36s var(--ease) 5.2s both;
}

/* Breathing, running from the disc's arrival to the release. Nothing here is
   static for long, but even 0.3s of a frozen frame reads as a paused video —
   the same reason the roster scrub carries a drift term. Kept under 2% so it is
   felt rather than seen. */
html.aj-intro .aj-curtain__badge {
	animation: aj-curtain-breathe 4.1s linear 1.1s both;
}

/* III · the disc lands */
html.aj-intro .aj-curtain__badge-img {
	animation: aj-curtain-badge-in 1.3s var(--ease) 1.1s both;
}

/* IV · the shadow blooms out from under it */
html.aj-intro .aj-curtain__shade {
	animation: aj-curtain-shade-in 1.7s var(--ease) 1.15s both;
}

/* V · the ring draws itself */
html.aj-intro .aj-curtain__ring circle {
	animation: aj-curtain-ring 1.3s var(--ease) 2.55s both;
}

/* VI · one ripple off the closed ring */
html.aj-intro .aj-curtain__ripple {
	animation: aj-curtain-ripple 1.1s var(--ease) 3.8s both;
}

/* VIII · the ground goes before the sheets do, so the panels part onto the page
   rather than onto the field sitting behind them. Without this the reveal never
   happens on screen: the panels would open onto the field and the page would
   only appear when the whole overlay is switched off. */
html.aj-intro .aj-curtain__field {
	animation: aj-curtain-field-out .2s linear 5.56s both;
}

/* ----- Arriving from an internal link: open only ----- */
html.aj-enter .aj-curtain__panel--l {
	animation: aj-curtain-open-l var(--curtain-open) var(--ease) both;
}

html.aj-enter .aj-curtain__panel--r {
	animation: aj-curtain-open-r var(--curtain-open) var(--ease) both;
}

html.aj-enter .aj-curtain__stage {
	animation: aj-curtain-disc-out .34s var(--ease) both;
}

html.aj-enter .aj-curtain__seam {
	animation: aj-curtain-seam-out .4s var(--ease) both;
}

/* ----- Leaving: close only; the document is swapped underneath.
   The close reads the token so it cannot drift from CURTAIN_CLOSE in motion.js,
   which is the timer that decides when the swap actually happens. ----- */
html.aj-leaving .aj-curtain__panel--l {
	animation: aj-curtain-close-l var(--curtain-close) var(--curtain-ease-close) both;
}

html.aj-leaving .aj-curtain__panel--r {
	animation: aj-curtain-close-r var(--curtain-close) var(--curtain-ease-close) both;
}

html.aj-leaving .aj-curtain__seam {
	animation: aj-curtain-seam .7s var(--ease) .36s both;
}

html.aj-leaving .aj-curtain__stage {
	animation: aj-curtain-disc-in .3s var(--ease) .34s both;
}

/* ============================================================
   Keyframes
   ============================================================ */

@keyframes aj-curtain-close-l {
	from { transform: translate3d(-100%, 0, 0); }
	to { transform: translate3d(0, 0, 0); }
}

@keyframes aj-curtain-close-r {
	from { transform: translate3d(100%, 0, 0); }
	to { transform: translate3d(0, 0, 0); }
}

@keyframes aj-curtain-open-l {
	from { transform: translate3d(0, 0, 0); }
	to { transform: translate3d(-100%, 0, 0); }
}

@keyframes aj-curtain-open-r {
	from { transform: translate3d(0, 0, 0); }
	to { transform: translate3d(100%, 0, 0); }
}

/* Flares as the sheets land, then settles back to a hairline. */
@keyframes aj-curtain-seam {
	0% { opacity: 0; transform: scaleY(0); }
	42% { opacity: 1; transform: scaleY(1); }
	100% { opacity: .5; transform: scaleY(1); }
}

@keyframes aj-curtain-seam-out {
	from { opacity: .5; transform: scaleY(1); }
	to { opacity: 0; transform: scaleY(1); }
}

@keyframes aj-curtain-field-out {
	from { opacity: 1; }
	to { opacity: 0; }
}

/* Release only — the children own their entrances now, so all the stage does is
   let go of the finished composition. Outward, 1.03 not 0.97: a logo that
   shrinks on exit reads as dismissed, one that grows reads as let go. */
@keyframes aj-curtain-stage {
	from { opacity: 1; transform: scale(1); }
	to { opacity: 0; transform: scale(1.03); }
}

/* Under 2% across 4.1s. Enough that the composition is alive, not enough that
   anyone could describe what is moving. */
@keyframes aj-curtain-breathe {
	from { transform: scale(1); }
	to { transform: scale(1.018); }
}

/* III · the disc arrives from further down and further back than anything else
   on this site moves — it has the screen to itself, so it can afford the
   distance. --ease has a long tail, which is what makes the last 20% of the
   travel read as settling rather than as stopping. */
@keyframes aj-curtain-badge-in {
	from { opacity: 0; transform: translate3d(0, 22px, 0) scale(.78); }
	to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
}

/* IV · from tight and close to wide and soft. The shadow element does not move
   with the disc — it stays put while the disc rises off it, which is the whole
   reason the lift reads as a lift. */
@keyframes aj-curtain-shade-in {
	from { opacity: 0; transform: scale(.55); }
	to { opacity: 1; transform: scale(1); }
}

/* V · one revolution, clockwise from twelve. --ring-c is resolved on the circle
   itself, so the draw is correct at every badge size without a second literal. */
@keyframes aj-curtain-ring {
	from { stroke-dashoffset: var(--ring-c); }
	to { stroke-dashoffset: 0; }
}

/* VI · leaves at its brightest and thins out as it goes, so it reads as one
   ring travelling rather than as a second ring fading in on top of the first.
   14% is as far as it gets: any wider and it stops being related to the disc. */
@keyframes aj-curtain-ripple {
	0% { opacity: 0; transform: scale(1); }
	18% { opacity: .55; }
	100% { opacity: 0; transform: scale(1.14); }
}

@keyframes aj-curtain-disc-in {
	from { opacity: 0; transform: scale(.9); }
	to { opacity: 1; transform: scale(1); }
}

@keyframes aj-curtain-disc-out {
	from { opacity: 1; transform: scale(1); }
	to { opacity: 0; transform: scale(1.06); }
}

/* ---------- Reduced motion ----------
   The head script already declines to stamp a class at all under this query,
   so nothing above should ever run. This is here because a scroll lock that
   outlives its animation is the one failure in this section a reader cannot
   recover from without reloading. */
@media (prefers-reduced-motion: reduce) {
	.aj-curtain,
	html.aj-intro .aj-curtain,
	html.aj-enter .aj-curtain,
	html.aj-leaving .aj-curtain {
		display: none;
	}

	html.aj-intro,
	html.aj-intro body {
		overflow: visible;
	}
}

/* ============================================================
   27 · WhatsApp button
   ------------------------------------------------------------
   A persistent shortcut to the number the footer already dials, so a reader
   who decides to get in touch on the way down does not have to reach the
   bottom of a very long page to do it.

   Espresso and gold rather than WhatsApp green. What makes the button
   recognisable is the glyph, not the colour — a white mark reversed out of a
   dark disc reads as WhatsApp immediately — and on a page whose entire palette
   is warm neutrals, #25D366 would be the only saturated thing on screen and
   would pull the eye off the work it sits next to.
   ============================================================ */

.aj-wa {
	position: fixed;
	right: var(--fab-inset);
	bottom: var(--fab-inset);
	/* Above every scroller on the page, below all four things that are meant to
	   cover it: the mobile menu (65), the skip link (100), the lightbox (200)
	   and the curtain (900). That one number is why none of those needs a rule
	   here — the button is simply behind them when they are up. */
	z-index: 55;
	display: flex;
	align-items: center;
	gap: .7rem;
	text-decoration: none;
	-webkit-tap-highlight-color: transparent;
}

/* Reachable by Tab only when it can actually be seen. The overlays above are
   opaque, so without this the button stays in the focus order underneath them. */
html.menu-open .aj-wa,
html.lightbox-open .aj-wa {
	visibility: hidden;
}

.aj-wa__btn {
	position: relative;
	width: var(--fab-size);
	height: var(--fab-size);
	border-radius: 50%;
	background: var(--espresso);
	color: #fff;
	display: grid;
	place-items: center;
	flex: none;
	box-shadow: 0 12px 32px rgba(23, 18, 16, .26);
	transition: transform .45s var(--ease), box-shadow .45s var(--ease);
}

.aj-wa__glyph {
	display: block;
	position: relative;
	z-index: 1;
}

/* The ripple. Gold hairline, the same one this site draws under every band it
   owns, released outward on a slow cycle. Deliberately quiet: a button that
   pulses hard is a button that nags. */
.aj-wa__btn::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: 50%;
	border: 1px solid var(--gold);
	animation: aj-wa-ripple 3.4s var(--ease) infinite;
	pointer-events: none;
}

/* The hover ring, held just off the disc so it reads as a halo rather than as
   a border that thickened. */
.aj-wa__btn::after {
	content: "";
	position: absolute;
	inset: -4px;
	border-radius: 50%;
	border: 1px solid var(--gold);
	opacity: 0;
	transform: scale(.94);
	transition: opacity .4s var(--ease), transform .4s var(--ease);
	pointer-events: none;
}

.aj-wa:hover .aj-wa__btn,
.aj-wa:focus-visible .aj-wa__btn {
	transform: translateY(-3px);
	box-shadow: 0 18px 40px rgba(23, 18, 16, .34);
}

.aj-wa:hover .aj-wa__btn::after,
.aj-wa:focus-visible .aj-wa__btn::after {
	opacity: 1;
	transform: scale(1);
}

/* One invitation at a time: while the reader is on the button, the ripple has
   done its job and the ring takes over. */
.aj-wa:hover .aj-wa__btn::before,
.aj-wa:focus-visible .aj-wa__btn::before {
	animation-play-state: paused;
	opacity: 0;
}

/* ----- The label -----
   The eyebrow voice — Space Mono, uppercase, wide tracking — so the pill reads
   as part of this site rather than as a widget's tooltip. */
.aj-wa__label {
	font-family: var(--font-mono);
	font-size: .62rem;
	letter-spacing: .22em;
	text-transform: uppercase;
	color: var(--ink);
	background: var(--paper);
	border: 1px solid rgba(42, 33, 27, .10);
	box-shadow: 0 8px 22px rgba(23, 18, 16, .12);
	padding: .62rem .9rem;
	border-radius: 999px;
	white-space: nowrap;
	opacity: 0;
	transform: translateX(10px);
	transition: opacity .4s var(--ease), transform .4s var(--ease);
	pointer-events: none;
}

.aj-wa:hover .aj-wa__label,
.aj-wa:focus-visible .aj-wa__label {
	opacity: 1;
	transform: none;
}

/* No hover below this width and no room for it either. */
@media (max-width: 900px) {
	.aj-wa__label {
		display: none;
	}
}

/* ----- Entrance -----
   Gated on html.js so that with scripting off the button is simply present:
   the class that would reveal it is never coming, so it must not be what makes
   it visible. Same contract as [data-reveal] everywhere else in this file. */
html.js .aj-wa {
	opacity: 0;
	transform: translateY(14px) scale(.86);
	pointer-events: none;
	transition:
		opacity .5s var(--ease),
		transform .5s var(--ease);
}

html.js .aj-wa.is-in {
	opacity: 1;
	transform: none;
	pointer-events: auto;
}

@keyframes aj-wa-ripple {
	0% { opacity: .35; transform: scale(1); }
	70% { opacity: 0; transform: scale(1.55); }
	100% { opacity: 0; transform: scale(1.55); }
}

@media (prefers-reduced-motion: reduce) {
	.aj-wa__btn::before {
		animation: none;
		opacity: 0;
	}

	.aj-wa,
	.aj-wa__btn,
	.aj-wa__label,
	.aj-wa__btn::after {
		transition: none;
	}

	html.js .aj-wa {
		opacity: 1;
		transform: none;
		pointer-events: auto;
	}
}

/* ============================================================
   28 · Disciplines deck — Home's Work & Services
   ============================================================
   Five glass tiles over a warm aurora, three across and two below. The client
   asked for Apple's ecosystem material — translucency, a squircle, a hairline
   glass outline — and for an emoji at the centre of each tile.

   THE AURORA IS LOAD-BEARING, NOT DECORATION. backdrop-filter blurs whatever is
   behind it, and behind flat cream there is nothing to blur: the first build of
   this sat on plain paper and the tiles read as white boxes at 40% opacity.
   Glass needs something with structure underneath it before it looks like glass
   at all. The blooms give the blur colour to move around, which is the entire
   reason Apple's own glass is always shown over photography or a saturated
   ground.

   AND IT IS THE DAYLIGHT TWIN OF THE TRIFECTA. That section, two screens up, is
   the same material in the dark: gold on espresso, hairline rim, spotlight
   under the pointer. Repeating it here would have given Home two dark glass
   grids within a screen of each other and flattened both. Same object, opposite
   time of day. */

.aj-deck {
	position: relative;
	overflow: clip;
	/* Runs paper into cream, so it separates from the briefs above (paper) and
	   hands over to Our Family below (cream) without a seam at either end. It
	   dips deeper than either in the middle, where the tiles are: white glass on
	   near-white paper has no edges, and the first build of this read as five
	   flat cream rectangles for exactly that reason. */
	background: linear-gradient(180deg,
			var(--paper) 0%,
			#F7F0E2 20%,
			#EFE2CB 56%,
			#EADDC6 82%,
			var(--cream) 100%);
}

.aj-deck::before {
	content: "";
	position: absolute;
	inset: -12%;
	z-index: 0;
	/* Tighter and stronger than blooms usually want to be. These are not mood —
	   they are the thing the tiles refract, and a wash too even to have structure
	   gives backdrop-filter nothing to blur. Each one is sized to sit partly
	   behind a tile and partly in the open, so the same bloom reads twice: sharp
	   in the gap, softened through the glass. */
	background:
		radial-gradient(17% 22% at 11% 30%, rgba(178, 133, 71, .66), transparent 62%),
		radial-gradient(15% 20% at 34% 16%, rgba(107, 79, 58, .40), transparent 64%),
		radial-gradient(19% 24% at 58% 40%, rgba(209, 177, 128, .78), transparent 62%),
		radial-gradient(16% 21% at 86% 24%, rgba(160, 122, 74, .46), transparent 64%),
		radial-gradient(20% 25% at 74% 82%, rgba(193, 152, 92, .70), transparent 62%),
		radial-gradient(15% 19% at 28% 88%, rgba(107, 79, 58, .34), transparent 66%),
		radial-gradient(13% 17% at 50% 66%, rgba(230, 211, 172, .72), transparent 68%);
	filter: blur(8px);
	pointer-events: none;
}

/* The grain the rest of the site wears, over the blooms rather than under them —
   it is what stops four soft radials from reading as a gradient mesh. */
.aj-deck::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	background-image: var(--grain);
	opacity: .05;
	mix-blend-mode: multiply;
	pointer-events: none;
}

.aj-deck > .aj-container {
	position: relative;
	z-index: 1;
}

/* ----- The grid -----
   Six columns and every tile spanning two: three fill the top row exactly, and
   the last pair is offset by one column so it centres while staying the same
   width as the row above. Centring by any other means (justify-content, auto
   margins, a 2-column subgrid) makes the bottom pair wider or narrower than the
   top three, and the client asked for five equal tiles. */
.aj-dgrid {
	display: grid;
	grid-template-columns: repeat(6, minmax(0, 1fr));
	gap: clamp(1rem, 1.8vw, 1.7rem);
	list-style: none;
	margin: 0;
	padding: 0;
}

.aj-dgrid__cell {
	grid-column: span 2;
	/* The tilt's vanishing point. On the cell rather than the grid: one shared
	   perspective across an 1800px row would swing the outer tiles far harder
	   than the middle one, because they sit further from its centre. */
	perspective: 1000px;
	/* A grid of one, purely so the tile stretches to the row without asking for
	   height: 100%. It did ask, once, and the result was a circular measurement —
	   the row sized itself from the tile's content while the tile sized itself
	   from the row — that settled on a 430px cell holding a 501px tile, which
	   overhung into the row below by 71px. */
	display: grid;
}

.aj-dgrid__cell:nth-child(4) {
	grid-column: 2 / span 2;
}

.aj-dgrid__cell:nth-child(5) {
	grid-column: 4 / span 2;
}

/* ----- The tile ----- */
.aj-dtile {
	position: relative;
	display: flex;
	flex-direction: column;
	/* This theme is content-box by default, and that bit here exactly as it bit the
	   hero stage: a height was being read as the height of the *content*, so the
	   tile came out its padding taller than asked for at each end. border-box is
	   also what makes the square below actually square — under content-box,
	   aspect-ratio would size the content area and the padding would be added
	   outside it, giving a 1:1 box that is 70px oblong. */
	box-sizing: border-box;
	/* Square, at the client's request. The flex column below is what lets it stay
	   square without the copy overflowing: the emoji band absorbs whatever height
	   the two text blocks do not use, so a one-line title and a three-line one
	   both sit on the same baseline from the bottom. */
	aspect-ratio: 1;
	padding: clamp(1.7rem, 2.2vw, 2.4rem);
	border-radius: 34px;
	text-decoration: none;
	color: var(--ink);
	/* Deliberately NOT isolation: isolate, which is the obvious way to contain the
	   z-index:-1 glow below and is the wrong one here — isolation forces
	   transform-style to flat, which would quietly kill the emoji's translateZ and
	   with it the whole reason the tile is a 3D object. It is not needed anyway:
	   the transform below already opens a stacking context, and a negative-z pseudo
	   inside one paints above the element's own background and under its text. */
	transform-style: preserve-3d;
	/* Two channels, both written from elsewhere: --lift by the hover rule below,
	   --tx/--ty by motion.js on pointer move. Keeping them as variables is what
	   lets hover and tilt share one transform without overwriting each other. */
	transform:
		translate3d(0, var(--lift, 0px), 0)
		rotateX(var(--ty, 0deg))
		rotateY(var(--tx, 0deg));
	/* Thin enough to be see-through. The first pass ran .66 white at the top and
	   the tile read as a solid cream card with a nice edge — which is the failure
	   mode of every glass panel that is really just a light fill. What sells it is
	   the ground moving underneath, so the fill gets out of the way and the blur
	   and the saturation boost do the work. */
	background:
		linear-gradient(158deg,
			rgba(255, 255, 255, .50) 0%,
			rgba(255, 255, 255, .16) 54%,
			rgba(255, 255, 255, .30) 100%);
	-webkit-backdrop-filter: blur(26px) saturate(1.9);
	backdrop-filter: blur(26px) saturate(1.9);
	box-shadow:
		/* Inner top highlight and inner bottom bounce — the two lines that make a
		   translucent panel read as having thickness rather than being a hole. */
		inset 0 1px 0 rgba(255, 255, 255, .9),
		inset 0 -1px 0 rgba(255, 255, 255, .34),
		/* The faint dark ring outside the bright rim. Apple's light glass carries
		   both, and it is the outer one that gives the panel a boundary — without
		   it a white tile on a warm ground has a lit top edge and then simply
		   dissolves into the page down its sides. */
		0 0 0 1px rgba(107, 79, 58, .08),
		/* Contact shadow, then the cast. Two shadows rather than one: a single
		   soft shadow floats the tile without ever landing it. */
		0 4px 10px -6px rgba(66, 48, 34, .24),
		0 28px 52px -26px rgba(66, 48, 34, .44);
	transition:
		transform .5s var(--ease),
		box-shadow .5s var(--ease);
}

/* Set by motion.js while the pointer is inside the tile. The .5s ease above is
   the right curve for the lift, and for the tilt springing back to flat on the
   way out — and completely wrong while the pointer is moving, where every frame
   would be chasing a target half a second old. That reads as lag, not as weight.
   Short and linear while tracking, back to the long ease on exit. */
.aj-dtile.is-tilting {
	transition:
		transform .12s linear,
		box-shadow .5s var(--ease);
}

/* ----- The glass outline -----
   The thing the client named specifically. A border cannot hold a gradient, so
   the rim is a filled box masked down to its own 1px frame — same construction
   as .aj-tcard__rim in the Trifecta, different light. It is brightest at the
   top-left and picks up gold as it comes round, which is a single raking light
   rather than an even glow: an even outline reads as a stroke, a raked one
   reads as an edge catching the light. */
.aj-dtile__rim {
	position: absolute;
	inset: 0;
	z-index: 3;
	border-radius: inherit;
	padding: 1px;
	background: linear-gradient(158deg,
			rgba(255, 255, 255, .98) 0%,
			rgba(255, 255, 255, .42) 26%,
			rgba(209, 177, 128, .30) 58%,
			rgba(255, 255, 255, .55) 82%,
			rgba(255, 255, 255, .90) 100%);
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
	transition: opacity .45s var(--ease);
	pointer-events: none;
}

/* The specular streak across the top third — the highlight a curved glass panel
   throws. Kept off the rim so the two can be lit independently. */
.aj-dtile::before {
	content: "";
	position: absolute;
	inset: 1px 1px auto;
	z-index: 2;
	height: 42%;
	border-radius: 33px 33px 60% 60% / 33px 33px 22% 22%;
	background: linear-gradient(180deg, rgba(255, 255, 255, .55), transparent);
	opacity: .8;
	pointer-events: none;
}

/* The pointer glow, behind the content. z-index -1 against the tile's own
   isolation, so it stays inside the tile and under the type. */
.aj-dtile::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: inherit;
	background: radial-gradient(360px circle at var(--mx, 50%) var(--my, 35%),
			rgba(209, 177, 128, .40), transparent 68%);
	opacity: 0;
	transition: opacity .45s;
	pointer-events: none;
}

/* ----- The emoji -----
   Live text, not an image. Apple's emoji artwork is proprietary and cannot be
   redistributed on a website, so the only honest way to show it is to ask for
   the font and let each platform answer: Apple devices — the client's own, and
   a large share of this audience — render Apple Color Emoji, Windows renders
   Segoe UI Emoji, Android renders Noto. Every one of them is a full-colour
   glyph at the size below, so the tile never falls back to a mono outline.

   It sits on its own layer in the tile's 3D space (see --ez on hover), which is
   what makes it lift off the glass rather than scale on it. */
.aj-dtile__emoji {
	/* 1 1 auto, not the 1 1 0% that `flex: 1` is shorthand for. With a zero basis
	   the glyph stops contributing its own height to the tile's intrinsic size, so
	   the tile measured short, stretched to the row, and then let the emoji grow
	   into 345px of the result. It claims its own size first and takes the leftover
	   second. */
	flex: 1 1 auto;
	min-height: 0;
	display: grid;
	place-items: center;
	font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
		"Twemoji Mozilla", "EmojiOne Color", sans-serif;
	font-size: clamp(3.8rem, 5.8vw, 5.8rem);
	line-height: 1;
	padding-block: clamp(.6rem, 1.4vw, 1.2rem);
	/* A colour glyph has no shadow of its own; this is what seats it above the
	   glass instead of printing it onto the surface. */
	filter: drop-shadow(0 14px 18px rgba(66, 48, 34, .28));
	transform: translateZ(var(--ez, 0px)) scale(var(--es, 1));
	transition:
		transform .55s var(--ease),
		filter .55s var(--ease);
}

.aj-dtile__body {
	display: block;
	position: relative;
	z-index: 1;
}

.aj-dtile__title {
	margin: 0 0 .55rem;
	font-size: clamp(1.15rem, 1.5vw, 1.42rem);
	font-weight: 800;
	letter-spacing: -.018em;
	line-height: 1.18;
	color: var(--ink);
}

.aj-dtile__blurb {
	margin: 0;
	font-size: clamp(.92rem, 1.05vw, 1rem);
	line-height: 1.55;
	color: color-mix(in srgb, var(--ink) 68%, transparent);
	/* Room for the arrow to arrive without the copy reflowing under it. */
	padding-right: 2.2rem;
}

.aj-dtile__go {
	position: absolute;
	right: clamp(1.7rem, 2.2vw, 2.4rem);
	bottom: clamp(1.7rem, 2.2vw, 2.4rem);
	z-index: 2;
	font-size: 1.25rem;
	line-height: 1;
	color: var(--gold-deep);
	opacity: 0;
	transform: translateX(-8px);
	transition:
		opacity .4s var(--ease),
		transform .5s var(--ease);
}

/* ----- Hover & focus -----
   Focus gets everything hover gets. The tile is a link, and a keyboard reader
   should not be told less about where they are than a mouse user. */
.aj-dtile:hover,
.aj-dtile:focus-visible {
	--lift: -10px;
	--ez: 34px;
	--es: 1.07;
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 1),
		inset 0 -1px 0 rgba(255, 255, 255, .42),
		0 0 0 1px rgba(209, 177, 128, .38),
		0 6px 14px -8px rgba(66, 48, 34, .28),
		0 38px 68px -28px rgba(107, 79, 58, .48);
}

.aj-dtile:hover .aj-dtile__emoji,
.aj-dtile:focus-visible .aj-dtile__emoji {
	filter: drop-shadow(0 24px 26px rgba(66, 48, 34, .32));
}

.aj-dtile:hover::after,
.aj-dtile:focus-visible::after {
	opacity: 1;
}

.aj-dtile:hover .aj-dtile__go,
.aj-dtile:focus-visible .aj-dtile__go {
	opacity: 1;
	transform: translateX(0);
}

/* ----- Responsive -----
   The 3-over-2 arrangement is a wide-screen shape. Below it the six-column
   scaffold stops earning its keep, so the grid drops to two and then to one,
   and the odd tile out takes the full width rather than sitting alone in a
   half-empty row. */
@media (max-width: 1180px) {
	.aj-dgrid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.aj-dgrid__cell,
	.aj-dgrid__cell:nth-child(4) {
		grid-column: span 1;
	}

	.aj-dgrid__cell:nth-child(5) {
		grid-column: span 2;
	}

	/* The one tile that must not be square. It spans both columns here, so at a
	   typical tablet width a 1:1 box would be a ~970px cliff holding four lines of
	   text. It keeps the square's material and loses its proportion. */
	.aj-dgrid__cell:nth-child(5) .aj-dtile {
		aspect-ratio: auto;
		min-height: clamp(240px, 30vw, 300px);
	}

	.aj-dgrid__cell:nth-child(5) .aj-dtile__emoji {
		flex: none;
		padding-block: 0 1.4rem;
	}
}

@media (max-width: 700px) {
	.aj-dgrid {
		grid-template-columns: minmax(0, 1fr);
	}

	.aj-dgrid__cell,
	.aj-dgrid__cell:nth-child(4),
	.aj-dgrid__cell:nth-child(5) {
		grid-column: span 1;
	}

	/* Still square in one column — a full-width squircle is a big, calm target and
	   it gives the emoji the room to be the thing you notice. The radius comes down
	   because 34px on a 360px box reads as a pill rather than a squircle. */
	.aj-dtile {
		border-radius: 28px;
	}

	/* The fifth tile stops being the exception. It is only wider than its siblings
	   in the two-column band above; down here every tile is one column, so the one
	   that was let out of the square goes back in. */
	.aj-dgrid__cell:nth-child(5) .aj-dtile {
		aspect-ratio: 1;
		min-height: 0;
	}

	/* The arrow is a hover affordance and there is no hover here. It reads as a
	   permanent ornament on a phone, so it goes. */
	.aj-dtile__go {
		display: none;
	}

	.aj-dtile__blurb {
		padding-right: 0;
	}

	.aj-dtile__emoji {
		flex: none;
		padding-block: .2rem 1.2rem;
	}
}

/* ----- Motion -----
   Gated on html.js like the rest of the site's entrances, so a no-JS reader
   gets five tiles that are simply there. */
/* THE ENTRANCE IS ON THE CELL, NOT ON THE TILE, and that is not tidiness. The
   tile's transform belongs to hover and to the pointer tilt; an entrance
   declared on the same element wins on specificity the moment .is-in lands and
   pins the transform to its end state, so the lift and the tilt would never move
   again. The cell has no other transform duty, so it takes the entrance and the
   tile keeps its own.

   Every property here settles on a neutral value — opacity 1, transform none,
   filter none — for a second reason. Opacity below 1 and any filter at all turn
   an ancestor into a backdrop root, and a backdrop root above the tile cuts the
   tile's backdrop-filter off from the aurora it exists to refract. Leaving the
   filter at blur(0) instead of none would be enough to switch the glass off
   permanently. */
html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"] {
	opacity: 0;
	transform: translate3d(0, 30px, 0) scale(.94);
	filter: blur(8px);
	transition:
		opacity .8s var(--ease) var(--d, 0s),
		transform .9s var(--ease) var(--d, 0s),
		filter .8s var(--ease) var(--d, 0s);
}

html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"].is-in {
	opacity: 1;
	transform: none;
	filter: none;
}

/* The emoji arrives after the tile it sits on, and overshoots. It is the only
   place on this page something is allowed to bounce — a mark landing on a
   surface can, a panel of glass cannot.

   An animation rather than a transition, for the same reason the entrance moved
   to the cell: the emoji's transform is also hover's (--ez/--es), and a
   transition carrying a var(--d) delay would still be carrying that delay on
   every hover afterwards. animation-fill-mode: backwards holds the start frame
   through the delay and then hands the element back to its normal styles when
   the animation ends, so hover arrives with a clean transform. */
html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"]:not(.is-in) .aj-dtile__emoji {
	opacity: 0;
}

html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"].is-in .aj-dtile__emoji {
	animation: aj-emoji-in .75s cubic-bezier(.34, 1.56, .64, 1) calc(var(--d, 0s) + .2s) backwards;
}

@keyframes aj-emoji-in {
	from {
		opacity: 0;
		transform: translateZ(0) scale(.5);
	}
}

@media (prefers-reduced-motion: reduce) {
	/* The tilt is the first thing to go — it is the one effect here that moves
	   the whole panel in response to something the reader did not ask for. */
	.aj-dtile {
		transform: none !important;
		transition: box-shadow .3s !important;
	}

	.aj-dtile::after {
		display: none;
	}

	.aj-dtile__emoji {
		transform: none !important;
		transition: none !important;
	}

	/* The tiles are simply present. No rise, no scale, no blur — and critically no
	   leftover filter or sub-1 opacity on the cell, which would leave the glass
	   switched off for the whole visit. */
	html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"] {
		opacity: 1 !important;
		transform: none !important;
		filter: none !important;
		transition: none !important;
	}

	html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"] .aj-dtile__emoji,
	html.js .aj-dgrid__cell[data-reveal][data-anim="dtile"].is-in .aj-dtile__emoji {
		opacity: 1;
		animation: none !important;
	}

	.aj-dtile__go {
		transition: opacity .2s !important;
		transform: none !important;
	}
}

/* Chrome and Safari both keep backdrop-filter; older engines drop it entirely.
   Without the blur the tiles would be 26%-white panes with the aurora legible
   straight through them, so the fallback trades translucency for an opaque
   plate that still carries the rim, the highlight and the shadow. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.aj-dtile {
		background: linear-gradient(158deg,
				rgba(255, 253, 249, .96) 0%,
				rgba(250, 246, 238, .9) 54%,
				rgba(252, 249, 243, .94) 100%);
	}
}
