/**
 * experiment-morph: single dark stage, particles as the interface.
 * No Fraunces, no Inter/Geist/Roboto, no em dashes in user-facing copy.
 */

* {
	box-sizing: border-box;
}

html,
body {
	margin: 0;
	overflow: hidden;
	overscroll-behavior: none;
	background: var(--mc-page-bg);
}

body {
	position: fixed;
	inset: 0;
	color: var(--mc-stage-ink);
	font-family: var(--mc-font-body, "Hanken Grotesk", -apple-system, sans-serif);
	-webkit-font-smoothing: antialiased;
}

:root {
	--mc-font-display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
	--mc-font-body:
		"Hanken Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

/* ROUND4.md R4.1: light is the new default theme (html[data-theme] set at boot by
   app.js from ?theme=, default "light"); ?theme=dark preserves every round-3 value
   byte-identical. Single source for every theme-dependent custom property used
   below — no per-rule duplication. `--mc-stage-ink` is a dedicated token (NOT
   monaco-tokens.css's `--mc-paper`, which onboarding.css still uses unrelatedly for
   its red-button text) so the stage UI's foreground-on-current-ground color can flip
   independently of anything else. */
:root[data-theme="dark"] {
	--mc-page-bg: #0f0d10;
	--mc-stage-ink: #f6f6f9;
	--mc-red-text: #eb4256;
	--mc-scrim-rgb: 4, 3, 5;
	--mc-focus-ring: var(--mc-ink-rim);
	/* Owner fix (2026-07-03): annotation legibility — a soft theme-aware pill
	   behind each caption so text never sits directly on dense particle fill. */
	--mc-annotation-pill-bg: rgba(15, 13, 16, 0.86);
}

:root[data-theme="light"],
:root {
	--mc-page-bg: #f6f3ec;
	--mc-stage-ink: var(--mc-ink-deep);
	--mc-red-text: var(--mc-red-text-light);
	--mc-scrim-rgb: 246, 243, 236;
	--mc-focus-ring: #14545f;
	--mc-annotation-pill-bg: rgba(246, 243, 236, 0.88);
}

button {
	font: inherit;
	cursor: pointer;
	touch-action: manipulation;
}

#app {
	position: relative;
	height: 100svh;
	height: var(--app-h, 100svh);
	width: 100%;
	overflow: hidden;
}

#app::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
	pointer-events: none;
	opacity: 0;
}

#stage {
	position: absolute;
	inset: 0;
	z-index: 0;
	display: block;
	width: 100%;
	height: 100%;
	touch-action: manipulation;
}

#stage[hidden] {
	display: none;
}

/* ── Scrims: the only permitted overlays, always gradient-to-transparent ── */
.scrim {
	position: absolute;
	left: 0;
	right: 0;
	z-index: 1;
	pointer-events: none;
}

/* REVISION.md R17/R18: veils restored to direction-a's exact height/alpha.
   ROUND4.md R4.1: scrims invert on light theme (ivoire-to-transparent via
   --mc-scrim-rgb) — same geometry, alpha unchanged. */
.scrim--top {
	top: 0;
	height: 30svh;
	background: linear-gradient(
		to bottom,
		rgba(var(--mc-scrim-rgb), 0.58),
		transparent
	);
}

.scrim--bottom {
	bottom: 0;
	height: 26svh;
	background: linear-gradient(
		to top,
		rgba(var(--mc-scrim-rgb), 0.62),
		transparent
	);
}

.scrim--left {
	top: 0;
	bottom: 0;
	left: 0;
	right: auto;
	width: 42%;
	background: linear-gradient(
		to right,
		rgba(var(--mc-scrim-rgb), 0.5),
		transparent
	);
	display: none;
}

/* ── Grid ─────────────────────────────────────────────────────────────── */
#ui {
	position: absolute;
	inset: 0;
	z-index: 2;
	display: grid;
	grid-template-rows:
		max(env(safe-area-inset-top), 12px) auto
		auto
		1fr
		auto max(env(safe-area-inset-bottom), 12px);
	grid-template-columns: 1fr;
	padding-inline: var(--mc-space-lg);
	pointer-events: none;
	transition: opacity 200ms var(--mc-easing-smooth);
}

#ui > * {
	pointer-events: none;
}

#ui button,
#ui a,
#ui input {
	pointer-events: auto;
}

/* Owner directive (2026-07-03): onboarding composition — nothing of the underlying
   state (header/panel/dock/studio labels/annotations, all inside #ui) may be
   readable behind the form card (app.js toggles this on mountOnboardingOverlay/
   onOnboardingClosed). Higher specificity than `#ui button/a/input` above so the
   whole layer is truly inert, not just invisible. */
body.mc-onboarding-open #ui {
	opacity: 0;
	pointer-events: none;
}

body.mc-onboarding-open #ui button,
body.mc-onboarding-open #ui a,
body.mc-onboarding-open #ui input,
body.mc-onboarding-open #ui .studio-hitzone {
	pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
	#ui {
		transition: none;
	}
}

#header-zone {
	grid-row: 2;
	padding-top: var(--mc-space-sm);
}

/* ROUND9.md R9.3/R9.4 (iPhone real-device review 2026-07-04): the figure-zone box
   is pinned to a COMPACT height right under the header (`align-self: start` + an
   explicit `height`) — the figure's projected center sits close under the header
   on every viewport. ROUND10.md R10.1 (owner iPhone review 2026-07-04): the row
   used to be `1fr` (absorbing all leftover vertical space as empty ground between
   the compact box and the panel, ~25-30% of the screen reading as "dead"). The row
   is now `auto` (grid-template-rows above) — it shrinks to exactly the box's own
   height — and the freed 1fr instead lives on #panel-zone's row below, so the
   reclaimed space visibly belongs to the panel instead of an empty gap. Same
   safe-area-overage-to-figure logic as before, just against the smaller 28svh
   base. */
#figure-zone {
	grid-row: 3;
	align-self: start;
	height: 28svh;
	height: calc(
		28svh - max(0px, env(safe-area-inset-top, 0px) - 12px) - max(
				0px,
				env(safe-area-inset-bottom, 0px) - 12px
			)
	);
}

/* ROUND10.md R10.3: studio's two brains render bigger than any other sub-state
   (MOBILE_SUBSTATE_FIGURE_SCALE.studio = 1.15, app.js) — centered on the SAME
   compact top-pinned box every other state uses, that extra scale pushed them up
   into the header/lockup zone (owner screenshot). Push the box itself down with
   real clearance; labels stay above their brain, everything else stays below the
   lockup. Mobile only (desktop's two-column layout already has headroom). */
@media (max-width: 899px) {
	body.is-studio #figure-zone {
		margin-top: 96px;
	}

	/* Owner 2026-07-04 (iPhone screenshot): on site/ia the figure's chrome bar sat
	   ON the PRAGMA lockup — same clearance treatment as studio, and the
	   annotations follow via projection with zero JS change. */
	body.is-site #figure-zone,
	body.is-ia #figure-zone {
		margin-top: 44px;
	}

	/* ROUND11.md R11.4b: a resolved portrait's panel copy (kicker + name + one-line
	   bio + links, no bullets) is much shorter than the panel's usual body text —
	   `align-self: center` on #panel-zone's shared 1fr row split the row's leftover
	   space evenly above/below, but "above" is empty ground under the portrait
	   (nothing to visually balance against) while "below" reads as a large dead gap
	   before RETOUR (owner iPhone screenshot). Bottom-anchor the panel in that row
	   instead while a portrait is active, closing the gap against the dock; the
	   portrait itself already fills the space above via app.js's
	   computeStudioPortraitMobileRect band, so nothing is lost up there. */
	body.is-studio.is-portrait-active #panel-zone {
		align-self: end;
		/* Owner 2026-07-05: the founder card rises off the dock a step. */
		margin-bottom: var(--mc-space-xl);
	}
}

/* ROUND10.md R10.1: this row is `1fr` now (grid-template-rows above) — the space
   the figure-zone row used to hog is reclaimed here. `align-self: center`
   vertically balances the panel's own (short, content-sized) box in the middle of
   that band instead of it hugging the top or being stretched to fill it. The
   max-height ceiling is raised (38svh -> 44svh) purely as headroom so the body
   copy can never truncate even if it grows (a longer future edit, a taller
   line-height from a real webfont) — `overflow: hidden` stays as the safety net,
   verified empty (scrollHeight === clientHeight) via the ben DOM-rect audit. */
#panel-zone {
	grid-row: 4;
	align-self: center;
	max-height: 44svh;
	/* ROUND3.md §4.3: widened 34rem -> 36rem to fit the larger fs-body/fs-h1 text
	   without extra wraps. */
	max-width: 36rem;
	overflow: hidden;
}

#dock-zone {
	grid-row: 5;
}

/* ROUND10.md R10.1: condense the bottom stack on mobile — merge Retour into the
   same row as the segmented chips (2-col grid: back button left, chips right)
   instead of three stacked rows, and cut the gaps/padding-bottom hard. The safe-
   area floor (grid-template-rows above) already reserves real clearance below the
   CTA, so the dock itself only needs a small breathing pad, not a second helping
   of --mc-space-xl on top of it ("the whole stack should end just above the
   safe-area"). #dock-zone (ID) beats the unconditional `.dock` class rule above.
   Desktop (>=900px) redefines `#dock-zone` to `flex-direction: row` in its own
   media query below and is unaffected. */
@media (max-width: 899px) {
	#dock-zone {
		display: grid;
		grid-template-columns: auto 1fr;
		align-items: center;
		column-gap: var(--mc-space-sm);
		row-gap: var(--mc-space-sm);
		padding-bottom: var(--mc-space-sm);
	}

	.dock-secondary {
		grid-column: 1;
		grid-row: 1;
	}

	.dock-segmented {
		grid-column: 2;
		grid-row: 1;
		justify-content: flex-end;
	}

	.dock-segmented[hidden] {
		display: none;
	}

	.btn-primary {
		grid-column: 1 / -1;
		grid-row: 2;
	}
}

/* ROUND12.md: owner directive (2026-07-04) — mobile-only (<900px) accueil reorder.
   Today's mobile order is eyebrow+H1+sub (one #header-zone block) -> figure -> dock;
   the new order is eyebrow (stays top-left, unchanged slot) -> figure -> H1+sub ->
   dock. Desktop (its own >=900px block further below) is completely untouched.
   Implementation: #header-zone/#header-full dissolve via `display:contents` so their
   leaf children (#eyebrow, #h1, .sub) become independent items of #ui's own grid,
   each placed on its own explicit row — zero DOM change, zero risk to the desktop
   composition. Every headerZone.getBoundingClientRect() consumer in app.js
   (positionStudioLabels, computeStudioPortraitMobileRect, isFigureBandTight,
   annotationObstacleRects) is gated to non-accueil states, so none of them ever
   observes #header-zone as `contents` — verified by reading app.js before writing
   this rule, not assumed. */
@media (max-width: 899px) {
	body.is-accueil #ui {
		grid-template-rows:
			max(env(safe-area-inset-top), 12px) auto
			auto
			auto
			auto
			1fr
			auto max(env(safe-area-inset-bottom), 12px);
	}

	body.is-accueil #header-zone,
	body.is-accueil #header-full {
		display: contents;
	}

	/* Row 2 — the exact slot #header-zone used to occupy; padding-top transferred
	   from the now-dissolved #header-zone (base rule above) since `display:contents`
	   drops box-only properties on the element that carries them. */
	/* Row 3 (#figure-zone) still lands directly under the eyebrow now that H1/sub no
	   longer sit between them, but its HEIGHT is grown here, accueil-mobile only: on
	   this state measureFigureZone() calls engine.resetFigurePlacement() (never
	   setFigureRect()), so the particle wordmark is placed by fixed camera math alone
	   (offset (0,0,0) = screen-center), completely independent of this box's real
	   size — the base 28svh height (tuned for OTHER states that DO read this rect)
	   badly undersold the actual footprint here. Ben audit (r13, MONACO dwell,
	   393x852): the pool ellipse (resetFigurePlacement's ACCUEIL_POOL_CENTER/SIZE_
	   DEFAULT, morph-engine.js) sits at 51% viewport center +-9.2% half-height, i.e.
	   its own hard edge lands at ~60.2svh — the 28svh box put H1 right in the middle
	   of that at the base margin, a real overlap (confirmed on-screen: MONACO's ink
	   crossing "Monaco." and reaching into the sub line). 52svh clears the ink's
	   real visible bottom edge with a real margin (ben-verified) without pushing
	   #dock-zone past the viewport (60svh overflowed the 393x852 frame by ~30px,
	   cropping the dock). Rows 4/5 below keep their own modest margin on top of
	   that. */
	/* Owner 2026-07-04: with the wordmark+pool lifted 14% of viewport height
	   (morph-engine.js resetFigurePlacement), the 52svh band left the CTA below
	   the fold — give the same 14svh back to the rows below (52 -> 40, keeping
	   2svh of the original clearance margin over the lifted pool edge). */
	body.is-accueil #figure-zone {
		height: 34svh;
		height: calc(
			34svh - max(0px, env(safe-area-inset-top, 0px) - 12px) - max(
					0px,
					env(safe-area-inset-bottom, 0px) - 12px
				)
		);
	}

	/* Owner 2026-07-04 (épuré pass): consistent breathing room between every text
	   body — headline gets a full lg step off the particle band, the sub a clear
	   sm step off the headline. */
	body.is-accueil #mini-wordmark {
		grid-row: 2;
		grid-column: 1;
	}

	body.is-accueil #h1 {
		grid-row: 4;
		grid-column: 1;
		align-self: start;
		margin-top: var(--mc-space-lg);
	}

	body.is-accueil .sub {
		grid-row: 5;
		grid-column: 1;
		align-self: start;
		margin-top: var(--mc-space-sm);
	}

	/* #panel-zone/#dock-zone shift down one row each to make room for the new
	   headline rows above them (#panel-zone stays empty/hidden on accueil regardless
	   — `.panel{display:none}` below — this is just keeping its grid slot honest). */
	body.is-accueil #panel-zone {
		grid-row: 6;
	}

	body.is-accueil #dock-zone {
		grid-row: 7;
	}

	/* `display:contents` drops #header-zone's own box, so its .pre-intro/.swap-out/
	   .swap-in classes (opacity/transform, boot() + applyTextSwap() in app.js) no
	   longer visually apply to it — reapply them to the now-independent leaf elements
	   so the boot reveal and state-transition fades still read as one cohesive header
	   unit (SPEC.md §1.4 / ROUND6 intro loader), exactly as before the split. No JS
	   change needed: these still key off the same classes app.js already toggles. */
	body.is-accueil #header-zone.pre-intro #mini-wordmark,
	body.is-accueil #header-zone.pre-intro #h1,
	body.is-accueil #header-zone.pre-intro .sub {
		opacity: 0;
	}

	body.is-accueil #header-zone.swap-out #mini-wordmark,
	body.is-accueil #header-zone.swap-out #h1,
	body.is-accueil #header-zone.swap-out .sub {
		transition:
			opacity 220ms var(--mc-easing-smooth),
			transform 220ms var(--mc-easing-smooth);
		opacity: 0;
		transform: translateY(-12px);
	}

	body.is-accueil #header-zone.swap-in #mini-wordmark,
	body.is-accueil #header-zone.swap-in #h1,
	body.is-accueil #header-zone.swap-in .sub {
		animation: swap-in-kf 280ms var(--mc-easing-elegant) both;
	}
}

@media (max-width: 899px) and (prefers-reduced-motion: reduce) {
	body.is-accueil #header-zone.swap-out #h1,
	body.is-accueil #header-zone.swap-out .sub,
	body.is-accueil #header-zone.swap-in #mini-wordmark,
	body.is-accueil #header-zone.swap-in #h1,
	body.is-accueil #header-zone.swap-in .sub {
		transition: none;
		animation: none;
		opacity: 1;
		transform: none;
	}
}

/* ── Header ───────────────────────────────────────────────────────────── */
/* Owner 2026-07-05: the lockup is the WORDMARK itself, drawn EXACTLY like the
   particle PRAGMA — Hanken 800, the raster's tight ~0.03em tracking (not the old
   airy 0.16em), petrol ink, red P. */
.mini-wordmark {
	display: block;
	margin: 0;
	/* Match the PARTICLE wordmark exactly: targets.js rasterizes it in
	   900 Helvetica Neue with ~0.033em tracking (6px at 180px), NOT Hanken. */
	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
	font-size: var(--mc-fs-body);
	font-weight: 900;
	letter-spacing: 0.033em;
	color: var(--mc-ink-deep);
}

:root[data-theme="dark"] .mini-wordmark {
	color: var(--mc-stage-ink);
}

.mini-wordmark .brand-p {
	color: var(--mc-red);
}

.header-full {
	display: block;
}

body:not(.is-accueil) .header-full {
	display: none;
}

/* ── ROUND6 intro loader: flight + fade (base geometry/colors are inline in
   index.html so they paint pre-CSS; only the transition behavior lives here). ── */
#boot.is-leaving {
	pointer-events: none;
}

#boot .boot-ground {
	transition: opacity 700ms var(--mc-easing-smooth);
}

#boot.is-leaving .boot-ground {
	opacity: 0;
}

#boot.is-hidden {
	display: none;
}

@media (prefers-reduced-motion: reduce) {
	#boot .boot-ground {
		transition: opacity 300ms ease;
	}
}

/* ROUND3.md §4.2: one step up from direction-a's h2 — this is a full-screen app with
   a single reading surface, so the money element (accueil h1) gets fs-h1. */
#h1 {
	margin: 0;
	max-width: 15ch;
	font-family: var(--mc-font-display);
	font-weight: 500;
	font-size: var(--mc-fs-h1);
	line-height: 1.12;
	letter-spacing: -0.01em;
}

/* REVISION.md R16: restored to direction-a's 66% (was 78%). ROUND3.md §4.2: bumped
   to fs-body. */
.sub {
	margin: var(--mc-space-sm) 0 0;
	max-width: 34ch;
	color: color-mix(in srgb, var(--mc-stage-ink) 66%, transparent);
	font-size: var(--mc-fs-body);
	line-height: 1.5;
}

/* ── Panel ────────────────────────────────────────────────────────────── */
.panel {
	display: none;
	flex-direction: column;
	gap: var(--mc-space-sm);
}

body:not(.is-accueil) .panel {
	display: flex;
}

.panel-kicker {
	margin: 0;
	font-size: var(--mc-fs-small);
	text-transform: uppercase;
	letter-spacing: 0.14em;
	color: var(--mc-red-text);
}

/* 2026-07-04 founder cards: the real photo lives in the DOM (the particles do
   the emblem) — small, warm, business-card sized, soft-rounded, sitting between
   the kicker and the name. */
.panel-photo {
	display: block;
	width: clamp(72px, 9vw, 104px);
	height: clamp(72px, 9vw, 104px);
	margin: var(--mc-space-sm) 0;
	object-fit: cover;
	border-radius: 22%;
	box-shadow: 0 1px 0 color-mix(in srgb, var(--mc-stage-ink) 14%, transparent);
}

.panel-photo[hidden] {
	display: none;
}

/* Owner 2026-07-05 (founder-page layout review): the active-founder panel reads
   as a business CARD, not a flat stack — photo LEFT (spanning the identity
   lines), kicker + name + role + links stacked to its RIGHT. Mobile only; the
   desktop side column keeps the simple stack. */
@media (max-width: 899px) {
	body.is-studio.is-portrait-active .panel {
		display: grid;
		grid-template-columns: auto 1fr;
		grid-template-areas:
			"photo kicker"
			"photo title"
			"photo body"
			"photo meta";
		column-gap: var(--mc-space-md);
		align-items: center;
		row-gap: 2px;
	}

	body.is-studio.is-portrait-active .panel-photo {
		grid-area: photo;
		width: clamp(88px, 24vw, 112px);
		height: clamp(88px, 24vw, 112px);
		margin: 0;
		align-self: center;
	}

	body.is-studio.is-portrait-active .panel-kicker {
		grid-area: kicker;
		align-self: end;
	}

	body.is-studio.is-portrait-active .panel-title {
		grid-area: title;
	}

	body.is-studio.is-portrait-active .panel-body {
		grid-area: body;
	}

	body.is-studio.is-portrait-active .panel-meta {
		grid-area: meta;
		align-self: start;
	}

	body.is-studio.is-portrait-active .panel-bullets {
		display: none;
	}
}

/* ROUND3.md §4.2: fs-h1 (= direction-a's .mc-a__service-title) — the name/title is
   the loudest element of a state while its panel is up. */
.panel-title {
	margin: 0;
	font-family: var(--mc-font-display);
	font-weight: 500;
	font-size: var(--mc-fs-h1);
	line-height: 1.1;
}

.panel-body {
	margin: 0;
	max-width: 46ch;
	font-size: var(--mc-fs-body);
	line-height: 1.55;
	color: color-mix(in srgb, var(--mc-stage-ink) 82%, transparent);
}

.panel-bullets {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.panel-bullets li {
	position: relative;
	padding-left: 1.1em;
	font-size: var(--mc-fs-small);
	color: color-mix(in srgb, var(--mc-stage-ink) 82%, transparent);
}

.panel-bullets li::before {
	content: "";
	position: absolute;
	left: 0;
	top: 0.5em;
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: var(--mc-red);
}

@media (max-height: 700px) {
	.panel-bullets {
		display: none;
	}

	/* iPhone audit 2026-07-04 (375x667 spot-check): the 34svh/38svh split was
	   tuned against the 852px-tall iPhone 16 -- on a much shorter viewport the
	   same percentages leave too little room for the panel, and panel-body text
	   overlapped Retour/the segmented control. Give the figure less floor and the
	   panel more ceiling specifically below this height (iPhone SE-class and
	   shorter); untouched above it, including the primary 393x852 target.
	   ROUND9.md R9.3: same compact/top-aligned box as the base rule, just a
	   smaller height on short viewports. */
	#figure-zone {
		height: 22svh;
	}

	#panel-zone {
		max-height: 46svh;
	}
}

.panel-meta {
	margin: 0;
	font-size: var(--mc-fs-small);
	color: color-mix(in srgb, var(--mc-stage-ink) 70%, transparent);
}

.panel-meta a {
	color: inherit;
}

.panel-meta:empty,
.panel-bullets:empty,
.panel-body:empty,
.panel-kicker:empty,
.panel-title:empty {
	display: none;
}

/* ── Dock ─────────────────────────────────────────────────────────────── */
/* REVISION.md R22: bottom spacing >= --mc-space-xl so the composition breathes
   like the direction-a original (was --mc-space-md). ROUND3.md §4.3: internal gap
   bumped to --mc-space-lg so the larger CTA breathes. */
.dock {
	display: flex;
	flex-direction: column;
	gap: var(--mc-space-lg);
	padding-bottom: var(--mc-space-xl);
}

.dock-secondary {
	display: flex;
	gap: var(--mc-space-lg);
	order: 1;
}

.dock-segmented {
	display: flex;
	gap: var(--mc-space-sm);
	order: 1;
}

.dock-segmented[hidden] {
	display: none;
}

/* REVISION.md R20: understated underline CTA restored verbatim from direction-a's
   .mc-a__cta (style.css:161-196) — the filled red pill is deleted, it was the single
   loudest style drift from the original. */
.btn-primary {
	order: 2;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--mc-space-sm);
	width: auto;
	min-height: var(--mc-touch-min);
	padding: 0 var(--mc-space-xs);
	background: none;
	border: none;
	border-bottom: 1px solid currentColor;
	color: var(--mc-stage-ink);
	font-size: var(--mc-fs-body);
	text-transform: uppercase;
	letter-spacing: 0.14em;
	transition:
		border-color var(--mc-duration-base) var(--mc-easing-smooth),
		color var(--mc-duration-base) var(--mc-easing-smooth),
		transform var(--mc-duration-base) var(--mc-easing-smooth);
}

.btn-primary::after {
	content: "\2192";
	transition: transform var(--mc-duration-base) var(--mc-easing-smooth);
}

@media (hover: hover) {
	.btn-primary:hover {
		/* ROUND3.md §4.1: red TEXT uses --mc-red-text (WCAG AA on near-black); the
		   border stays --mc-red (non-text, 3:1 minimum only). */
		color: var(--mc-red-text);
		border-color: var(--mc-red);
	}

	.btn-primary:hover::after {
		transform: translateX(3px);
	}
}

/* REVISION.md R20: secondary at paper 66% (direction-a has no exact equivalent —
   this keeps parity with the restored .sub color). ROUND3.md §4.2: bumped to
   fs-body/paper 72%. */
.btn-secondary {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0 var(--mc-space-xs);
	background: none;
	border: none;
	border-bottom: 1px solid currentColor;
	color: color-mix(in srgb, var(--mc-stage-ink) 72%, transparent);
	font-size: var(--mc-fs-body);
	text-transform: uppercase;
	letter-spacing: 0.12em;
	transition:
		border-color var(--mc-duration-base) var(--mc-easing-smooth),
		color var(--mc-duration-base) var(--mc-easing-smooth);
}

@media (hover: hover) {
	.btn-secondary:hover {
		color: var(--mc-red-text);
		border-color: var(--mc-red);
	}
}

.chip {
	/* ROUND11.md R11.4a: unlike .btn-primary/.btn-secondary above, this never set
	   display:inline-flex + align-items:center — a plain button's text follows normal
	   line-box flow inside min-height, not vertical centering, so RETOUR (flex-
	   centered in its own 44px box) and the chips (top-of-line text in a 48px box)
	   never shared a baseline on the dock row (owner iPhone screenshot: site/ia's
	   "RETOUR" reads visibly lower than "SITE WEB"/"LOGICIEL IA"). Match the other
	   dock buttons' pattern so all three read as one line regardless of height.
	*/
	display: inline-flex;
	align-items: center;
	min-height: 48px;
	padding: 0 var(--mc-space-lg);
	background: transparent;
	border: 1px solid color-mix(in srgb, var(--mc-stage-ink) 30%, transparent);
	border-radius: var(--mc-radius-full);
	color: var(--mc-stage-ink);
	font-size: var(--mc-fs-small);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	transition:
		border-color var(--mc-duration-fast) var(--mc-easing-out),
		color var(--mc-duration-fast) var(--mc-easing-out);
}

.chip.is-active {
	border-color: var(--mc-red);
	color: var(--mc-red-text);
}

/* ── Focus ────────────────────────────────────────────────────────────── */
/* ROUND4.md R4.1: focus ring becomes #14545f on light; dark keeps --mc-ink-rim. */
:focus-visible {
	outline: 2px solid var(--mc-focus-ring);
	outline-offset: 3px;
}

/* ── Pre-intro hidden state: avoids the visible-then-snap-to-0 blink on first paint
   (SPEC.md §1.4: text/dock fade IN at 0.8s/1.4s — they must start hidden). ── */
.pre-intro {
	opacity: 0;
}

/* ── State text swap (SPEC.md §1.4: 220ms fade-down out, 280ms fade-up in) ── */
.swap-out {
	transition:
		opacity 220ms var(--mc-easing-smooth),
		transform 220ms var(--mc-easing-smooth);
	opacity: 0;
	transform: translateY(-12px);
}

.swap-in {
	animation: swap-in-kf 280ms var(--mc-easing-elegant) both;
}

@keyframes swap-in-kf {
	from {
		opacity: 0;
		transform: translateY(12px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

@media (prefers-reduced-motion: reduce) {
	.swap-out,
	.swap-in {
		transition: none;
		animation: none;
		opacity: 1;
		transform: none;
	}
}

/* ── Canvas fade (reduced motion + boot) ──────────────────────────────── */
#stage {
	opacity: 1;
	transition: opacity 300ms ease;
}

#stage.is-warming {
	opacity: 0;
}

/* ── Desktop layout ───────────────────────────────────────────────────── */
@media (min-width: 900px) {
	/* Owner 2026-07-07 ("center the text, find a better layout"): the ROUND5
	   asymmetric poster (headline column top-left, wordmark biased right) read as two
	   competing anchors — a loaded left third against a centered particle mass, with
	   dead ground top-right. Replaced by a MONUMENTAL CENTERED composition: one
	   vertical axis shared by the lockup, the headline, the particle wordmark and the
	   dock — masthead over engraving, like a plate. The wordmark keeps the whole
	   central band to itself (never occluded, scrims only), text above, actions
	   below. #figure-zone is still the invisible measurement rect (app.js
	   measureFigureZone); centering it zeroes the horizontal offset, the safest spot
	   inside setFigureRect's clamp. */
	body.is-accueil #ui {
		grid-template-columns: 1fr;
		grid-template-rows:
			max(env(safe-area-inset-top), 12px) auto
			1fr
			auto max(env(safe-area-inset-bottom), 12px);
		text-align: center;
	}

	/* Owner 2026-07-07: no lockup above the headline — the particle wordmark IS the
	   brand mark on accueil; a second PRAGMA on the same centered axis reads as an
	   echo. Desktop accueil only: every other state keeps the mini-wordmark as its
	   persistent brand anchor, and the mobile accueil lockup was validated on-device
	   (ROUND9-11). */
	body.is-accueil #mini-wordmark {
		display: none;
	}

	body.is-accueil #header-zone {
		grid-column: 1;
		grid-row: 2;
		justify-self: center;
		align-self: start;
		/* Owner 2026-07-07 ("descends un peu le texte"): the centered headline hugged
		   the top edge — drop it a real step. svh-based so it breathes with the
		   viewport but never collapses below the old 2xl step. */
		padding-top: max(var(--mc-space-2xl), 9svh);
	}

	/* ~24ch centered measure: "Studio de développement / AI-native à Monaco." breaks
	   into two balanced lines; text-wrap: balance keeps them even if the copy moves. */
	body.is-accueil #h1 {
		max-width: 24ch;
		margin-inline: auto;
		text-wrap: balance;
	}

	/* One line under the centered headline (the base 34ch measure breaks it as
	   "…livrés / en 48 à 72h.", an orphaned second line the centered axis magnifies). */
	body.is-accueil .sub {
		margin-inline: auto;
		max-width: 60ch;
	}

	body.is-accueil #figure-zone {
		grid-column: 1;
		/* Starts BELOW the header row (row 3, not 2) so the measured band never
		   vertically overlaps the headline regardless of how wide the wordmark gets. */
		grid-row: 3 / 5;
		min-height: 0;
		height: 55svh;
		align-self: center;
		width: 100%;
		justify-self: center;
	}

	body.is-accueil #dock-zone {
		grid-column: 1;
		grid-row: 4;
		flex-direction: row;
		align-items: center;
		justify-content: center;
		gap: var(--mc-space-xl);
	}

	body.is-accueil .dock-secondary {
		order: 0;
	}

	body.is-accueil .btn-primary {
		width: auto;
		order: 1;
	}

	body:not(.is-accueil) #ui {
		/* ROUND4.md R4.8: narrower panel column, more room for the figure — same
		   #panel-zone max-width 36rem (§4.3), it just sits in a narrower column. */
		grid-template-columns: minmax(300px, 30%) 1fr;
		grid-template-rows:
			max(env(safe-area-inset-top), 12px) auto
			1fr
			auto max(env(safe-area-inset-bottom), 12px);
	}

	body:not(.is-accueil) #header-zone {
		grid-column: 1 / -1;
		grid-row: 2;
	}

	body:not(.is-accueil) #panel-zone {
		grid-column: 1;
		grid-row: 3;
		align-self: center;
		max-height: none;
	}

	body:not(.is-accueil) #figure-zone {
		grid-column: 2;
		grid-row: 2 / 5;
		min-height: 0;
		/* ROUND9.md R9.3: the base (mobile) rule now pins a compact top-aligned
		   height — desktop's right-column band must keep its full row-span stretch
		   (regression guard: desktop layouts must not move). */
		height: auto;
		align-self: stretch;
	}

	body:not(.is-accueil) #dock-zone {
		grid-column: 1;
		grid-row: 4;
		flex-direction: row;
		align-items: center;
	}

	body:not(.is-accueil) .btn-primary {
		width: auto;
	}

	body:not(.is-accueil) .scrim--left {
		display: block;
	}

	/* Owner 2026-07-07 ("sur ordi tu dois agrandir vraiment la photo"): the resolved
	   founder card's photo reads business-card small next to the display-size name on
	   desktop — give it real portrait presence. The panel column is minmax(300px, 30%)
	   (~430px at 1440), so 18vw caps comfortably inside it. Mobile keeps its own
	   validated card grid (max-width 899 block). */
	body.is-studio.is-portrait-active .panel-photo {
		width: clamp(180px, 18vw, 280px);
		height: clamp(180px, 18vw, 280px);
	}
}

@media (max-width: 899px) {
	/* Mobile background cleanup: keep the particle activity reading on the right
	   while protecting the text column with a soft ivory wash instead of a hard
	   flat split. */
	#app::before {
		opacity: 1;
		background:
			radial-gradient(
				90% 62% at 82% 36%,
				rgba(var(--mc-scrim-rgb), 0) 0,
				rgba(var(--mc-scrim-rgb), 0.08) 54%,
				rgba(var(--mc-scrim-rgb), 0.22) 100%
			),
			linear-gradient(
				to bottom,
				rgba(var(--mc-scrim-rgb), 0.1),
				transparent 22%,
				transparent 78%,
				rgba(var(--mc-scrim-rgb), 0.16)
			);
	}

	body.is-accueil #figure-zone {
		width: min(90vw, 28rem);
		height: clamp(15rem, 35svh, 22rem);
		justify-self: end;
		align-self: center;
	}

	body:not(.is-accueil) #ui {
		grid-template-rows:
			max(env(safe-area-inset-top), 12px) auto
			auto
			auto
			1fr
			auto max(env(safe-area-inset-bottom), 12px);
	}

	body:not(.is-accueil) #header-zone {
		grid-row: 2;
	}

	body:not(.is-accueil) #figure-zone {
		grid-row: 3;
		height: clamp(12.5rem, 30svh, 19rem);
	}

	body.is-studio #figure-zone {
		height: clamp(11rem, 25svh, 16rem);
	}

	body:not(.is-accueil) #panel-zone {
		grid-row: 4;
		max-height: none;
		margin-top: var(--mc-space-sm);
	}

	body:not(.is-accueil) #dock-zone {
		grid-row: 6;
	}
}

/* ── Studio v3 "les deux cerveaux": display-type labels, attached (ROUND3.md §3.1 +
   V3.4) ── DOM, not particles — each label is absolutely positioned by app.js's
   studio rAF loop via engine.projectWorldPoint(), tracking the live figure
   transform + camera drift so it reads as an attached caption under its brain. */
.studio-labels {
	position: absolute;
	inset: 0;
	z-index: 3;
	pointer-events: none;
}

/* Owner 2026-07-05 layout rework (mobile): the names are a FIXED selector row
   pinned under the header in BOTH modes (brains and emblem) — a founder tab bar.
   No projection, no inline styles (app.js clears them), no overlap ever. */
@media (max-width: 899px) {
	body.is-studio .studio-labels {
		inset: auto 0 auto 0;
		top: calc(max(env(safe-area-inset-top), 12px) + 58px);
		display: flex;
		justify-content: center;
		align-items: center;
		gap: var(--mc-space-sm);
	}

	body.is-studio .studio-label {
		position: static;
	}
}

.studio-labels[hidden] {
	display: none;
}

/* Owner 2026-07-05 (supersedes V3.4's display-heading treatment): the names are
   clearly TAPPABLE CHIPS now — the same visual grammar as the dock's segmented
   .chip (thin bordered pill, 44px+ target, fs-small uppercase), so the site's
   existing "bordered pill = tappable" language carries over to the studio. They
   dock BELOW each brain (app.js positionStudioLabels). */
.studio-label {
	position: absolute;
	top: 0;
	left: 0;
	display: inline-flex;
	align-items: center;
	pointer-events: auto;
	cursor: pointer;
	background: transparent;
	border: 1px solid color-mix(in srgb, var(--mc-stage-ink) 30%, transparent);
	border-radius: var(--mc-radius-full);
	padding: 0 var(--mc-space-lg);
	min-height: 48px;
	font-family: var(--mc-font-display);
	font-weight: 500;
	font-size: var(--mc-fs-small);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: color-mix(in srgb, var(--mc-stage-ink) 85%, transparent);
	transition:
		color var(--mc-duration-base) var(--mc-easing-smooth),
		border-color var(--mc-duration-base) var(--mc-easing-smooth),
		opacity var(--mc-duration-base) var(--mc-easing-smooth);
	animation: studio-label-pulse 2.6s ease-in-out infinite;
}

/* ROUND3.md §3.2 idle pulse, retuned 2026-07-05 for the pill border (was
   border-bottom-color on the old underline treatment). Suppressed under reduced
   motion and while a portrait is active (JS toggles .no-pulse). */
@keyframes studio-label-pulse {
	0%,
	100% {
		border-color: color-mix(in srgb, var(--mc-stage-ink) 30%, transparent);
	}
	50% {
		border-color: color-mix(in srgb, var(--mc-stage-ink) 70%, transparent);
	}
}

/* Owner 2026-07-05: invisible per-brain hit zones (app.js sizes/positions them to
   each brain's projected bounds every label-loop tick) — clicking a brain acts
   exactly like clicking its name chip. Purely a pointer affordance: aria-hidden,
   not focusable; the chips remain the accessible controls. */
.studio-hitzone {
	position: absolute;
	top: 0;
	left: 0;
	pointer-events: auto;
	cursor: pointer;
	background: transparent;
	border-radius: 50%;
	-webkit-tap-highlight-color: transparent;
}

.studio-label.no-pulse {
	animation: none;
}

/* Inactive partner dims while the other is active or a portrait is up. */
.studio-label.is-dimmed {
	color: color-mix(in srgb, var(--mc-stage-ink) 45%, transparent);
}

/* 2026-07-04: a clicked name whose emblem hasn't lazily built yet (app.js's
   activateStudioPortrait() queues instead of no-oping) — a distinct faster
   opacity pulse + wait cursor so the click visibly registered instead of looking
   broken, without claiming the portrait is actually up (.is-active owns that).
   Opacity (not the idle pulse's border tint) so it reads clearly at a glance on
   both viewports. Resolves to .is-active automatically once the build lands
   (resolvePendingLazyTargets()). */
.studio-label.is-pending {
	cursor: wait;
	color: color-mix(in srgb, var(--mc-stage-ink) 65%, transparent);
	animation: studio-label-pending-pulse 1s ease-in-out infinite;
}

@keyframes studio-label-pending-pulse {
	0%,
	100% {
		opacity: 0.55;
	}
	50% {
		opacity: 0.95;
	}
}

/* ROUND6.md R6.6: while a portrait is resolved, the other partner has no figure of
   its own to track anymore — it docks beside the active (below-emblem) chip
   instead of floating over the portrait's shoulder, sized down so the hierarchy
   (active name = the loud one) still reads. Chip grammar since 2026-07-05: the
   size-down happens via padding/min-height only (font is already fs-small). */
.studio-label.is-aside {
	min-height: 40px;
	padding: 0 var(--mc-space-sm);
}

@media (max-width: 899px) {
	.studio-label.is-aside {
		/* iPhone audit 2026-07-04: was 36px, under the 44px WCAG touch-target floor —
		   this is a real tappable control (switches the active partner), not inline
		   text, so it gets the same minimum as every other control. Visual size stays
		   compact via padding; positionStudioPortraitLabels() (app.js) reads
		   offsetHeight live, so it re-centers around the new box with no JS change. */
		min-height: var(--mc-touch-min);
	}
}

/* Same active grammar as the dock's .chip.is-active: red border + red ink. */
.studio-label.is-active,
.studio-label:focus-visible {
	color: var(--mc-red-text);
	border-color: var(--mc-red);
	animation: none;
}

/* Owner directive (2026-07-03): hover is a pure visual affordance — the hovered
   name rises to full ink with a firmer pill border and the sibling dims — with NO
   particle/morph effect (app.js's click handler is the only activation path).
   Skipped once a name is actually click-activated (.is-active already owns red).
   Brain hit-zone hover deliberately does NOT restyle the chip — over the figure
   the pointer repulsor is the affordance; only direct chip hover restyles it. */
@media (hover: hover) {
	.studio-labels:has(.studio-label:hover)
		.studio-label:not(:hover):not(.is-active) {
		color: color-mix(in srgb, var(--mc-stage-ink) 45%, transparent);
	}

	.studio-label:hover:not(.is-active) {
		color: var(--mc-stage-ink);
		border-color: var(--mc-stage-ink);
		animation: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	.studio-label,
	.studio-label.is-pending {
		animation: none;
	}
}

/* 2026-07-05 chip pass, mobile: same pill, slightly tighter padding; the soft
   translucent ground (annotation-pill token) keeps the name legible over the
   particle fill, exactly like the annotation chips. min-height stays >=44px
   (touch target). */
@media (max-width: 899px) {
	.studio-label {
		padding: 0 var(--mc-space-md);
		min-height: 44px;
		background: var(--mc-annotation-pill-bg);
	}
}

/* Owner 2026-07-05: hint promoted out of the panel flow — absolutely positioned by
   app.js's label loop, centered UNDER the two name chips (translate keeps the
   projected x as its center). fs-small ink 60%, fades permanently after the first
   activation (existing hint-faded machinery). */
/* ── ROUND4.md R4.6: dynamic annotations (bullets retired on site/ia) ──────────
   DOM layer, not particles — each annotation is absolutely positioned by app.js's
   annotation rAF loop via engine.projectWorldPoint(), tracking the live figure
   transform + camera drift, exactly like #studio-labels. */
.annotations {
	position: absolute;
	inset: 0;
	z-index: 3;
	pointer-events: none;
}

.annotations[hidden] {
	display: none;
}

.annotation {
	position: absolute;
	top: 0;
	left: 0;
	opacity: 0;
	transform: translateY(8px);
	transition:
		opacity var(--mc-duration-base) var(--mc-easing-smooth),
		transform var(--mc-duration-base) var(--mc-easing-smooth);
}

.annotation.is-visible {
	opacity: 1;
	transform: translateY(0);
}

/* Mobile chip fallback (band < 40svh): the dot stays projected on the figure, the
   floating text + leader hide (the chip row below the figure carries the copy). */
.annotations.is-chip-mode .annotation__text,
.annotations.is-chip-mode .annotation__leader {
	display: none;
}

/* Owner fix (2026-07-03): soft pill behind each caption — legible over the dense
   particle core (e.g. ia's "hub"/"spark" anchors), NOT just negative space. The dot
   + leader (below) stay outside the pill, on the particle figure itself. */
.annotation__text {
	position: absolute;
	max-width: 18ch;
	margin: 0;
	padding: var(--mc-space-xs) var(--mc-space-sm);
	border-radius: var(--mc-radius-sm);
	background: var(--mc-annotation-pill-bg);
	font-size: var(--mc-fs-small);
	line-height: 1.3;
	color: color-mix(in srgb, var(--mc-stage-ink) 92%, transparent);
}

/* ROUND9.md R9.3: mobile pills share the same near-anchor compass placement as
   desktop now (app.js's findAnnotationSlot), plus the pinned-on-locus mode for
   site's semantic captions — tighter type, tighter box only. 10rem (was 9): lets
   « Nom de domaine inclus » sit on the address bar in 2 lines instead of 3.
   ROUND10.md R10.2: `width: max-content` — the pill's absolute containing block
   is its own `.annotation` wrapper, a 0x0 positioned box, so shrink-to-fit
   resolved to MIN-content (ben DOM-rect audit: 58px wide, one word per line,
   3-4 lines tall — the cramped column look on the owner's screenshots).
   max-content (still capped by the max-width above) lays the text out
   naturally: « Nom de domaine inclus » reads as a wide short pill sitting
   along the address bar. Mobile-only on purpose — desktop pills keep their
   long-validated geometry. */
@media (max-width: 899px) {
	.annotation__text {
		width: max-content;
		max-width: 10rem;
		padding: 3px 6px;
		font-size: var(--mc-fs-xs);
		line-height: 1.25;
	}
}

.annotation__leader {
	position: absolute;
	height: 1px;
	background: color-mix(in srgb, var(--mc-stage-ink) 45%, transparent);
	transform-origin: 0 50%;
}

.annotation__dot {
	position: absolute;
	width: 4px;
	height: 4px;
	border-radius: 50%;
	background: var(--mc-red);
	transform: translate(-50%, -50%);
	box-shadow: 0 0 6px color-mix(in srgb, var(--mc-red) 60%, transparent);
}

@media (prefers-reduced-motion: reduce) {
	.annotation {
		transition: none;
	}
}

/* Mobile fallback row: shown instead of the floating labels when the figure band is
   under 40svh (app.js toggles .is-active). */
.annotation-chips {
	display: none;
	flex-wrap: wrap;
	gap: var(--mc-space-sm);
	margin: 0 0 var(--mc-space-sm);
	padding: 0;
}

.annotation-chips.is-active {
	display: flex;
}

.annotation-chip {
	display: inline-flex;
	align-items: center;
	min-height: 36px;
	padding: 0 var(--mc-space-md);
	border: 1px solid color-mix(in srgb, var(--mc-stage-ink) 25%, transparent);
	border-radius: var(--mc-radius-full);
	font-size: var(--mc-fs-xs);
	color: color-mix(in srgb, var(--mc-stage-ink) 82%, transparent);
	opacity: 0;
	transform: translateY(8px);
	transition:
		opacity var(--mc-duration-base) var(--mc-easing-smooth),
		transform var(--mc-duration-base) var(--mc-easing-smooth);
}

.annotation-chip.is-visible {
	opacity: 1;
	transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
	.annotation-chip {
		transition: none;
	}
}

/* ── No-WebGL fallback ─────────────────────────────────────────────────── */
#no-webgl-fallback {
	position: absolute;
	inset: 0;
	z-index: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--mc-page-bg);
}

#no-webgl-fallback[hidden] {
	display: none;
}

.no-webgl-wordmark {
	margin: 0;
	font-family: var(--mc-font-display);
	font-size: var(--mc-fs-hero);
	font-weight: 500;
	color: var(--mc-stage-ink);
	letter-spacing: 0.04em;
}

/* ── Debug overlay hook (styled inline in perf.js) ────────────────────── */
#debug-overlay {
	user-select: none;
}
