/* ─────────────────────────────────────────────────────────────
   Quiet Order — animated typographic composition.

   A 6×5 grid of small typographic fragments drifts in from the
   borders of the frame and settles into a faint table.

   Self-contained: every selector and keyframe is namespaced
   under .composition / composition-* so the component can be
   dropped into an existing page without collisions.

   ─────────────────────────────────────────────────────────── */

/* ── Root ──────────────────────────────────────────────────── */

.composition {
	position: relative;
	width: 100%;
	aspect-ratio: 6 / 5;

	font-family: var(--font-sans);
	font-feature-settings:
		"onum" 1,
		"kern" 1,
		"liga" 1;
	font-variant-numeric: oldstyle-nums;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;

	/* Edge fade — neither field nor grid has a hard boundary. */
	-webkit-mask-image: radial-gradient(
		ellipse at center,
		#000 38%,
		transparent 96%
	);
	mask-image: radial-gradient(ellipse at center, #000 38%, transparent 96%);
}

/* ── Grid layer ────────────────────────────────────────────────
   Static SVG behind the fragments. Lines fade in during arrival,
   then continue a barely-perceptible opacity breath. */

.composition__grid {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	overflow: visible;
	pointer-events: none;
}

/* Settled color: parchment at half strength (--parchment-a50), so the
   grid and landed glyphs read as a watermark on the ink section. */
.composition__grid line {
	stroke: var(--parchment-a50);
	stroke-width: 0.85;
	vector-effect: non-scaling-stroke;
	opacity: 0;
	animation:
		composition-line-emerge 2.8s cubic-bezier(0.4, 0, 0.2, 1)
		var(--line-delay, 0s) forwards,
		composition-line-breathe 11s ease-in-out calc(var(--line-delay, 0s) + 4s)
		infinite;
}

@keyframes composition-line-emerge {
	from {
		opacity: 0;
	}
	to {
		opacity: 1;
	}
}

@keyframes composition-line-breathe {
	0%,
	100% {
		opacity: 1;
	}
	50% {
		opacity: 0.55;
	}
}

/* ── Fragment field ───────────────────────────────────────────
   CSS Grid centres each fragment in its cell. */

.composition__field {
	position: absolute;
	inset: 0;
	display: grid;
	grid-template-columns: repeat(6, 1fr);
	grid-template-rows: repeat(5, 1fr);
}

.composition__fragment {
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: visible;
}

/* ── Animation layers ────────────────────────────────────────
   Two nested elements per fragment so transforms compose
   without conflict:
     .composition__glyph    one-time arrival (drift → cell)
     .composition__breath   perpetual sub-pixel sway
   ────────────────────────────────────────────────────────── */

.composition__glyph {
	display: inline-block;
	line-height: 1;
	font-size: var(--size, 1rem);
	color: var(--ink);
	opacity: var(--start-opacity, 0.35);
	transform: translate(var(--drift-x, 0), var(--drift-y, 0));
	animation: composition-arrive var(--dur, 4s) cubic-bezier(0.19, 1, 0.22, 1)
		var(--delay, 0s) both;
	/* transform and opacity only: color isn't compositable, so listing
	   it bought nothing */
	will-change: transform, opacity;
}

.composition__fragment--italic .composition__glyph {
	font-style: italic;
}

@keyframes composition-arrive {
	0% {
		transform: translate(var(--drift-x), var(--drift-y));
		opacity: var(--start-opacity, 0.35);
		color: var(--ink);
	}
	100% {
		transform: translate(var(--in-cell-x), var(--in-cell-y));
		opacity: var(--target-opacity, 0.85);
		color: var(--parchment-a50);
	}
}

.composition__breath {
	display: inline-block;
	animation: composition-breathe var(--breath-dur, 11s) ease-in-out
		var(--breath-delay, 0s) infinite;
}

@keyframes composition-breathe {
	0%,
	100% {
		transform: translate(var(--breath-x1, 0), var(--breath-y1, 0));
	}
	50% {
		transform: translate(var(--breath-x2, 0), var(--breath-y2, 0));
	}
}

/* ── Activation gate ──────────────────────────────────────────
   By default every animation in the composition is paused.
   composition.js adds .is-active once the element scrolls into
   view (after a short grace period); only then do the fragments
   begin their inward motion and the grid lines fade in.

   Until activation, fragments sit at their drift-state positions
   (visible thanks to animation-fill-mode: both) and grid lines
   remain at opacity: 0. */

.composition__glyph,
.composition__breath,
.composition__grid line {
	animation-play-state: paused;
}

.composition.is-active .composition__glyph,
.composition.is-active .composition__breath,
.composition.is-active .composition__grid line {
	animation-play-state: running;
}

/* Parked again while scrolled fully offscreen (composition.js toggles
   .is-parked): the settled breath loops are ambient, so the phase
   shift a pause causes is untrackable across an offscreen gap. Must
   follow the .is-active block — equal specificity, later rule wins. */
.composition.is-parked .composition__glyph,
.composition.is-parked .composition__breath,
.composition.is-parked .composition__grid line {
	animation-play-state: paused;
}

/* ── Reduced motion ──────────────────────────────────────────
   Skip straight to the settled state. */

@media (prefers-reduced-motion: reduce) {
	.composition__glyph {
		animation: none;
		opacity: var(--target-opacity, 0.85);
		color: var(--parchment-a50);
		transform: translate(var(--in-cell-x), var(--in-cell-y));
	}
	.composition__breath {
		animation: none;
	}
	.composition__grid line {
		animation: none;
		opacity: 1;
	}
}
