/* ==========================================================================
   Teconya — effects layer.
   Visual states the runtime toggles. Every effect has a resting state that is
   correct with JavaScript absent.
   ========================================================================== */

/* --- Typer -------------------------------------------------------------- */

.word { display: inline-block; white-space: nowrap; }
/* A run of Latin words inside a typer; dir="ltr" on it keeps their order. */
.run { unicode-bidi: isolate; }

.char {
	display: inline-block;
	border: var(--border-width) solid transparent;
	border-radius: var(--border-radius-text);
}

.charInit { opacity: 0; }

.charFill { background: var(--foreground); color: var(--background); }
.charInverse { background: var(--foreground-dimmed); color: var(--foreground); }
.charAccent { color: var(--accent); }
.charAccentFill { background: var(--accent); color: var(--white); }
.charAccentInverse { background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--accent); }
.charBorder { border-color: var(--foreground-dim); }

/* --- Reveal ------------------------------------------------------------- */

[data-in], .section-title { --progress: 0; }

/* --- Printer ------------------------------------------------------------ */

[data-printer] svg { width: 100%; height: auto; overflow: hidden; }
[data-printer] .line { will-change: transform; }

/* --- Quadtree ----------------------------------------------------------- */

[data-quadtree] { position: relative; overflow: hidden; }

[data-quadtree] canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

[data-quadtree] img,
[data-quadtree] video {
	width: 100%;
	height: auto;
	transition: opacity var(--duration-base) var(--ease-out);
}

/* --- Generative tiles --------------------------------------------------- */

.tile__canvas { position: relative; }

.tile__canvas canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

/* --- Spinner ------------------------------------------------------------ */

[data-spinner] { display: inline-block; width: 1ch; }

/* --- No-JS and reduced-motion resting states ---------------------------- */

.no-js .charInit,
.no-js [data-quadtree] img { opacity: 1; }

.no-js .experiment .video,
.no-js .reel .text { transform: none; }

@media (prefers-reduced-motion: reduce) {
	.charInit { opacity: 1; }
	.experiment .video { transform: none !important; }
}

/* --- Intro overlay ------------------------------------------------------ */

.intro {
	--foreground: var(--grey);
	--background: var(--black);

	position: fixed;
	top: 0;
	inset-inline-start: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 100dvh;
	padding: var(--pad);
	z-index: 501;
	color: var(--foreground);

	/* Nothing in the loader is for tapping, so it must never catch a tap:
	   if its timeline ever stalls (a background tab, a frozen frame), the
	   header and the page beneath stay usable. */
	pointer-events: none;
}

.intro .background {
	position: absolute;
	top: 0;
	inset-inline-start: 0;
	width: 100%;
	height: 100dvh;
	background: var(--black);
}

.intro .logo {
	position: absolute;
	top: var(--pad);
	inset-inline-start: var(--pad);
	display: flex;
	align-items: center;
	justify-content: center;
	width: calc( 100% - var(--pad) * 2 );
	height: calc( 100% - var(--pad) * 2 );
	color: var(--foreground-dim);
}

/* The wordmark spans the whole pad box, as the group system's does: the
   caption is meant to read across a full-width mark rather than sit beside a
   small one. The SVG carries a viewBox but no intrinsic width, so its own
   `width: 100%` resolved to the SVG default of 300px inside this shrink-to-fit
   flex item — sizing the wrapper is what actually controls it. */
.intro .logo .printer { width: 100%; }

.intro .text {
	position: relative;
	display: flex;
	justify-content: space-between;
	width: 100%;
	margin-bottom: 0;
	font-family: var(--font-mono);
	font-size: var(--font-lg);
	font-variation-settings: "wght" var(--wght-regular);
	text-transform: uppercase;
}

@media only screen and (max-width: 1024px) { .intro .text { font-size: var(--font-sd); } }
@media only screen and (max-width: 584px)  { .intro .text { font-size: var(--font-sm); } }

/* Bands start off-screen; the intro sweeps them across and away. */
.intro [data-printer] .line { transform: translateX(-100%); }

/* Nothing may cover the page when scripting or motion is unavailable. */
.no-js .intro { display: none; }

@media (prefers-reduced-motion: reduce) {
	.intro { display: none; }
}

/* --- Video -------------------------------------------------------------- */

.video {
	opacity: 0;
	transition: opacity 0.5s var(--ease-out);
	border-radius: var(--border-radius);
	overflow: hidden;
	line-height: 0;
}

.video.video--loaded { opacity: 1; }

.video video { display: block; max-width: 100%; }

.no-js .video { opacity: 1; }

/* --- Scroll-linked section state ---------------------------------------- */

[data-in], .section-title { --progress: 0; }

.experiment { --experiment-progress: 0; }
.project-item { --project-progress: 0; }

/* --- Page transition overlay -------------------------------------------- */

.page-transition {
	position: fixed;
	inset: 0;

	/* The group system puts this at 500. The intro must sit above it: the
	   loader runs while the shader is already covering the page, so a higher
	   shader would hide the sequence entirely. */
	z-index: 500;
	pointer-events: none;
	opacity: 0;
}

.page-transition.is-active { opacity: 1; }

.page-transition canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

/* Swup adds this while a visit is in flight. */
html.is-animating { cursor: progress; }

/* --- Fluid manifesto text ------------------------------------------------ */

/* The character grid only aligns in a monospace face with a fixed advance. */
.manifesto .texts [data-fluid] { position: relative; }

.manifesto .texts [data-fluid] p {
	font-family: var(--font-mono);
	font-size: var(--font-sd);
	font-variation-settings: "wght" var(--wght-light);
	line-height: 1.2;
	padding-inline-end: var(--pad);
	margin: 0;
}

/* `visibility: hidden` rather than display/clip: the box must survive so the
   effect can measure where the text wraps. */
.manifesto .texts [data-fluid] [data-fluid-input] {
	visibility: hidden;
	position: absolute;
}

.manifesto .texts [data-fluid] [data-fluid-output] {
	white-space: pre;
	position: absolute;
	top: 0;
	inset-inline-start: 0;
}

[data-fluid] canvas { display: block; max-width: 100%; }

/* With no scripting, or with motion suppressed, show the real paragraph. */
.no-js .manifesto .texts [data-fluid] [data-fluid-input],
.manifesto .texts [data-fluid].is-static [data-fluid-input] {
	visibility: visible;
	position: static;
}

/* --- Lazy figures -------------------------------------------------------- */

[data-lazy] {
	opacity: 0;
	transition: opacity 0.6s var(--ease-out);
}

[data-lazy].figure--loaded.figure--inview,
.no-js [data-lazy] { opacity: 1; }

/* --- Columns ------------------------------------------------------------- */

[data-columns] .column {
	--column-progress: 0;

	transform: translate3d( 0, calc( 12vh * var(--column-progress) ), 0 );
	will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
	[data-columns] .column { transform: none; }
	[data-lazy] { opacity: 1; }
}

/* --- Transition status line ---------------------------------------------- */

.transition-typer {
	position: fixed;
	bottom: var(--pad);
	inset-inline-start: var(--pad);
	z-index: 502;
	font-family: var(--font-mono);
	font-size: var(--font-sd);
	font-variation-settings: "wght" var(--wght-regular);
	text-transform: uppercase;
	color: var(--foreground);
	mix-blend-mode: difference;
	pointer-events: none;
}

/* --- Fluid text: the cursive pass ----------------------------------------
   Arabic cannot be rasterised a character at a time, so the paragraph is shown
   as set text and the band sweeps it a word at a time instead. A word at the
   band reads plainly; the further from it, the fainter and the further off its
   line it sits. `--word-distance` is set per word by the effect. */

.manifesto .texts [data-fluid].is-words [data-fluid-input] {
	visibility: visible;
	position: static;
}

.manifesto .texts [data-fluid].is-words [data-fluid-output] { display: none; }

.word-unit {
	--word-distance: 1;

	display: inline-block;
	opacity: calc( 1 - var(--word-distance) * 0.6 );
	transform: translateY( calc( var(--word-distance) * 0.35em ) );
	transition: opacity 0.45s var(--ease-out), transform 0.45s var(--ease-out);
	will-change: opacity, transform;
}

@media (prefers-reduced-motion: reduce) {
	.word-unit {
		opacity: 1;
		transform: none;
		transition: none;
	}
}

/* --- Dot rule -------------------------------------------------------------
   A hairline printed a dot at a time as it comes into view, in place of a
   plain border. The ink matches the borders it stands in for; the head flares
   a step brighter as it passes. */

.dot-rule {
	--rule-ink: var(--foreground-dimmed);
	--rule-hot: var(--foreground-dim);
	--rule-accent: var(--accent);

	position: relative;
	display: block;

	/* Auto, not 100%: these carry side margins where they stand in for an
	   inset border, and a full width on top of those overflows the row. */
	width: auto;
	height: 3px;
}

.dot-rule canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

/* --- Logo: struck in ------------------------------------------------------
   The header mark prints itself on arrival, in the order a head would work:
   across the bar, then down the stem. The rects are already in that order in
   the markup, so the delay is a nth-child step.

   Deliberately a CSS animation rather than a tween: the mark is brand identity
   and must not wait on JavaScript or on the shared GSAP ticker to appear.
   Turning the animation off — reduced motion below, or a client that ignores
   the property — leaves the rects at their resting opacity of 1. It does still
   depend on the browser actually running the animation it accepted; nothing
   short of an unpainted frame breaks that, and an unpainted frame shows
   nothing either way.

   Order follows the column index each cell carries: the name prints left to
   right, and the two dots of ت land last — the head lifting to add them. */

@keyframes teconya-strike {
	from { opacity: 0; transform: scale( 0.2 ); }
	to   { opacity: 1; transform: none; }
}

.logo-mark rect {
	transform-box: fill-box;
	transform-origin: center;
	animation: teconya-strike 0.26s var(--ease-out) both;
}

/* Each cell carries its column in --i (see inc/brand.php); the head prints
   the name left to right at twelve milliseconds a column, and the two dots
   land a few columns after the last letter. */
.logo-mark rect { animation-delay: calc( var(--i, 0) * 0.012s ); }

@media (prefers-reduced-motion: reduce) {
	.logo-mark rect { animation: none; }
}

/* --- Dot charts -----------------------------------------------------------
   The figures print in when they scroll into view: each struck cell carries
   its order in --i (inc/dot-charts.php) and the strike-in above runs with that
   stagger. Cells only hide while the effect is pending, so a page without the
   script, or with motion reduced, shows the figure at rest. */
[data-dot-chart].is-pending rect:not(.dot-grid__field rect) { opacity: 0; }

[data-dot-chart].is-printed rect:not(.dot-grid__field rect) {
	transform-box: fill-box;
	transform-origin: center;
	animation: teconya-strike 0.3s var(--ease-out) both;

	/* Capped: a thousand-cell figure would otherwise hold a thousand
	   animations pending for twelve seconds. Past the cap the cells land
	   together, which reads as the head finishing its pass. */
	animation-delay: calc( min( var(--i, 0), 48 ) * 0.012s );
}

@media (prefers-reduced-motion: reduce) {
	[data-dot-chart].is-printed rect { animation: none; }
}

/* The Sadu band: five cell rows at the rule's seven-pixel pitch; the Najdi
   parapet, three. */
.dot-rule--sadu { height: 35px; }
.dot-rule--najdi { height: 21px; }

/* --- Skyline --------------------------------------------------------------
   Riyadh at the foot of the loader and the 404 page. The tokens are the
   footer game's, so the two cities match. */
.skyline {
	--art-ink: var(--foreground-dimmed);
	--art-hot: var(--foreground-dim);
	--art-accent: var(--accent);

	position: relative;
	width: 100%;
	height: clamp( 90px, 14vw, 180px );
}

.skyline canvas { position: absolute; inset: 0; width: 100%; height: 100%; }

/* In the loader the city stands along the bottom edge, under the caption. */
.intro .skyline {
	position: absolute;
	inset-inline: var(--pad);
	bottom: calc( var(--pad) * 2 + var(--font-lg) * 1.2 );
	width: auto;

	/* Capped against the viewport height as well: the wordmark is centred
	   and a short, wide screen would otherwise bring the two together. */
	height: min( clamp( 90px, 14vw, 180px ), 12vh );
	pointer-events: none;
}

@media only screen and (max-width: 1024px) { .intro .skyline { bottom: calc( var(--pad) * 2 + var(--font-sd) * 1.2 ); } }
@media only screen and (max-width: 584px)  { .intro .skyline { bottom: calc( var(--pad) * 2 + var(--font-sm) * 1.2 ); } }

/* On a page it sits between the hero and the copy, or closes a page. */
.page .skyline { margin: 0 var(--pad) 6vh; width: auto; }

/* --- Scenes ---------------------------------------------------------------
   The service pages' pixel art (effects/scene.js): same tokens as the
   skyline, same grid, with moving parts drawn while the box is on screen. */
.scene {
	--art-ink: var(--foreground-dimmed);
	--art-hot: var(--foreground-dim);
	--art-accent: var(--accent);

	position: relative;
	width: 100%;
}

.scene canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
