/* The site's current design: the game bands, the glass header, the studio
 * crest, the listing, the contact form and the 404.
 *
 * Separate from main.css on purpose. That sheet still dresses the pages this
 * rebuild does not touch, and merging the two would make every rule in it
 * load-bearing for two layouts at once. Both are loaded; neither has to know
 * about the other.
 *
 * Colours come from the theme's tokens rather than being restated here, so
 * there is one place that decides what the studio's purple is.
 */

/* ───────────────────────────────────────────────────────────────────────────
   The first front page was a dark column: headline, paragraph, three rows.
   Correct, and indistinguishable from any other dark site. The games — which
   are the entire reason a publisher is on this page — appeared as three 60px
   icons in a settings list.

   This takes the opposite position. The page is still black, but each game
   owns a full-width band in its own colour, at a scale that lets the artwork
   do the arguing. Nothing here is decoration invented to fill space; the
   colours were read out of the studio's own store artwork and the artwork is
   the studio's own.
   ─────────────────────────────────────────────────────────────────────────── */

/* ── One typeface, three widths ───────────────────────────────────────────────
   Archivo is a variable font and its width axis is the reason it was chosen.
   The axis is also the reason the site had started to look like it was set in
   several different faces: measured across every page, type was rendering at
   74, 78, 92, 94, 100 and 118 — six widths, most of them arrived at one rule at
   a time rather than decided. At small sizes a widened Archivo genuinely has
   different letterforms, so a footer heading at 118 reads as a second typeface
   to anyone looking at the page rather than at the CSS.

   Three, each with a job:

     78   display — headlines, band titles, the 404. Condensed so it can be set
                    very large without the line becoming a wall.
     92   sub     — taglines, lead lines, headings inside long documents. A
                    half-step in from body, which is what marks them as a
                    different voice without changing size or colour.
     100  text    — everything else. Body, labels, navigation, the footer.

   `main.css` sets h1–h4 to 118 for the pages this rebuild does not touch. That
   is where the footer's headings were picking it up. Restated here, and this
   sheet loads second. */
:root {
	--ga-wdth-display: 78;
	--ga-wdth-sub: 92;
	--ga-wdth-text: 100;

	/* The small label — section numbers, column headings, the address heading.
	   Four places were using it at 0.70/0.74/0.78rem and 0.20/0.24em, which is
	   four settings of one idea and reads as four different treatments. */
	--ga-label-size: 0.74rem;
	--ga-label-track: 0.24em;
}

h1, h2, h3, h4, h5, h6 { font-variation-settings: "wdth" var(--ga-wdth-text); }

/* An absolute weight, because the browser's default for these is `bolder` — a
   relative keyword that climbs a step each time one is nested inside something
   already bold. Three <strong>s in the terms of service had escalated to 900
   that way, which is a fifth weight arriving on the page by accident. */
strong, b { font-weight: 700; }

.display {
	font-variation-settings: "wdth" var(--ga-wdth-display);
	font-weight: 800;
	line-height: 0.86;
	letter-spacing: -0.03em;
	text-transform: uppercase;
}

/* ── Header ─────────────────────────────────────────────────────────────── */

/* A floating pane rather than a bar welded to the top edge. It needs something
   behind it to refract, and a full-width bar has the page's own margin behind
   most of its length. */
.head {
	position: fixed;
	top: clamp(10px, 1.6vw, 20px);
	left: 50%;
	translate: -50% 0;
	z-index: 40;
	width: min(calc(100% - 2 * var(--ga-gutter)), var(--ga-max));
}

/* The frosted panel is drawn here, in CSS, and stands on its own. glass.js adds
   refraction on top where the engine allows it — Chromium only — so a browser
   that drops the SVG-filtered backdrop still gets real glass, not a bare box. */
.head__in {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 20px;
	min-height: 58px;
	padding-inline: 20px;
	border-radius: 999px;
	overflow: hidden;
	background: rgb(10 10 18 / 0.68);
	border: 1px solid rgb(255 255 255 / 0.10);
	backdrop-filter: blur(16px) saturate(160%);
	-webkit-backdrop-filter: blur(16px) saturate(160%);
	box-shadow:
		0 1px 0 rgb(255 255 255 / 0.10) inset,      /* the lit top rim */
		0 -1px 0 rgb(255 255 255 / 0.04) inset,     /* faint bounce underneath */
		0 18px 46px rgb(0 0 0 / 0.44);
}

/* The sheen. A real pane catches light along its upper edge; without this the
   panel reads as a grey rectangle with blur behind it. */
.head__in::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: inherit;
	background: linear-gradient(160deg, rgb(255 255 255 / 0.14), transparent 42%);
	pointer-events: none;
}

/* Progress, read straight from the document's own scroll. No listener. */
.head__in::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: 0;
	height: 2px;
	width: 100%;
	transform-origin: 0 50%;
	transform: scaleX(0);
	background: linear-gradient(90deg, var(--ga-yb), var(--ga-ec) 52%, var(--ga-md));
	pointer-events: none;
}

/* `flex: none` is load-bearing. A flex item shrinks by default, and on a 320px
   phone the nav wants more room than the pill has — so the mark was shrunk all
   the way to zero width and the logo simply was not on the page. It did not
   register as an overflow, because nothing overflowed: the header quietly gave
   up its own logo to make the arithmetic work. Below, the nav is made to fit
   instead. */
.head__mark { display: flex; flex: none; align-items: center; gap: 11px; font-weight: 700; letter-spacing: -0.01em; text-decoration: none; position: relative; }
.head__mark img { width: 26px; }
.head nav { display: flex; gap: 6px; font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; color: #d2d2e2; position: relative; }
.head nav a {
	text-decoration: none;
	padding: 8px 14px;
	border-radius: 999px;
	transition: color 0.22s, background 0.22s;
}
.head nav a:hover { color: var(--ga-text); background: rgb(255 255 255 / 0.09); }
.head nav a:focus-visible { outline: 2px solid var(--ga-text); outline-offset: 2px; }

/* Below 560 the pill cannot hold both. Measured: the nav costs a fixed 278px
   at every width, and the pill is only 335px wide on a 375px phone — the
   wordmark has to go before anything else does. The mark stays, so the header
   is still a logo and still goes home. */
@media (max-width: 560px) {
	.head__word { display: none; }
	.head nav { gap: 0; font-size: 0.7rem; letter-spacing: 0.06em; }
	.head nav a { padding: 8px 9px; }
	.head__in { padding-inline: 14px; }
}

/* The narrowest phones still in use. Measured at 320: the pill has 252px inside
   its padding, the mark takes 26 and the gap 20, which leaves 206 for a nav
   that wanted 278. Tightening the letter-spacing and the horizontal padding
   brings it to about 190 and everything fits with the logo still there. */
@media (max-width: 380px) {
	.head__in { gap: 10px; padding-inline: 12px; }
	.head nav { font-size: 0.66rem; letter-spacing: 0.03em; }
	.head nav a { padding: 8px 6px; }
}

/* ── Hero ───────────────────────────────────────────────────────────────── */

.hero {
	position: relative;
	min-height: 88svh;
	display: grid;
	align-content: center;
	/* Asymmetric: the panel floats over the top of the page, so the first
	   screen owes it room that the bottom does not. Declared as one
	   `padding-block` rather than a later `padding-top` override, which the
	   shorthand below it would have silently undone. */
	padding-block: clamp(104px, 17vh, 200px) clamp(60px, 12vh, 140px);
	overflow: clip;
}

/* Three soft fields of the three game colours, sitting behind the headline.
   This is the one piece of pure atmosphere on the page, and it is allowed
   because the colours are the games' own — the hero is literally tinted by
   the catalogue it introduces. */
.hero::before {
	content: "";
	position: absolute;
	inset: -20% -10% auto -10%;
	height: 120%;
	background:
		radial-gradient(38vw 38vw at 12% 22%, color-mix(in srgb, var(--ga-yb) 26%, transparent), transparent 68%),
		radial-gradient(42vw 42vw at 88% 18%, color-mix(in srgb, var(--ga-ec) 24%, transparent), transparent 66%),
		radial-gradient(40vw 40vw at 62% 88%, color-mix(in srgb, var(--ga-md) 20%, transparent), transparent 68%);
	filter: blur(30px);
	z-index: -1;
	animation: drift 26s ease-in-out infinite alternate;
}

@keyframes drift {
	from { transform: translate3d(-2%, -1%, 0) scale(1); }
	to   { transform: translate3d(2%, 2%, 0) scale(1.08); }
}

/* Two columns so the mascot has the right half rather than the headline having
   dead space beside it. The mascot column is sized in its own right — letting
   it be a fraction of the row made it collapse to nothing on narrow desktops. */
.hero__in {
	display: grid;
	grid-template-columns: minmax(0, 1fr) clamp(200px, 22vw, 330px);
	align-items: center;
	gap: clamp(28px, 5vw, 68px);
}

/* Sized to the sentence, not to the largest number that still fits. The
   headline is six words; at 8.4rem and a 12ch measure it broke into five lines
   and pushed everything under it off the first screen, which is a headline
   winning an argument with the page it introduces. */
.hero__title {
	font-size: clamp(2.9rem, 6.4vw, 5.6rem);
	max-width: 15ch;
}
.hero__title em {
	font-style: normal;
	display: block;
	color: transparent;
	background: linear-gradient(96deg, var(--ga-yb), var(--ga-ec) 52%, var(--ga-md));
	-webkit-background-clip: text;
	background-clip: text;
}

/* An A/B, not a decision. "Purple gradients" sit on every list of things that
   mark a page as generated, and this headline is a gradient — game-derived
   rather than the default violet, but a gradient. Rather than argue about it,
   add `?plain` to the URL and look at both.
   `-webkit-text-fill-color` has to be reset too: it is what is actually
   painting the glyphs, so setting `color` alone changes nothing. */
[data-headline="plain"] .hero__title em {
	background: none;
	color: var(--ga-text);
	-webkit-text-fill-color: var(--ga-text);
}

.hero__lede {
	margin-top: clamp(24px, 3vw, 38px);
	font-size: clamp(1.05rem, 1.6vw, 1.3rem);
	color: var(--ga-text-muted);
	max-width: 46ch;
}

.hero__meta {
	margin-top: clamp(28px, 4vw, 48px);
	display: flex;
	flex-wrap: wrap;
	gap: 12px 40px;
	/* The same label as the section numbers and the footer headings. It was set
	   at 0.8rem/0.14em, close enough to the others to look like a mistake rather
	   than a distinction — one idiom, one setting. */
	font-size: var(--ga-label-size);
	letter-spacing: var(--ga-label-track);
	text-transform: uppercase;
	color: var(--ga-text-muted);
}
.hero__meta b { color: var(--ga-text); font-variation-settings: "wdth" var(--ga-wdth-sub); font-weight: 700; }

/* ── Mascot ─────────────────────────────────────────────────────────────────
   Class names are the theme's, unchanged, so the script runs here exactly as it
   runs on the site and porting is a copy rather than a rewrite.

   The image is the whole feature until the script upgrades it in place: the
   still is only hidden once the canvas is actually running, never by a rule
   that fires before the script has proved itself. */

.ga-mascot {
	position: relative;
	display: block;
	width: 100%;
	aspect-ratio: 877 / 931;
	touch-action: none;
}
.ga-mascot.is-live { cursor: grab; }
.ga-mascot.is-held { cursor: grabbing; }

.ga-mascot img,
.ga-mascot__canvas { display: block; width: 100%; height: 100%; }
.ga-mascot.is-live img { visibility: hidden; }
/* Taller than its box and hanging over the top, so a jump has somewhere to go.
   The script draws the character standing on the canvas's bottom edge, which is
   the one that still lines up with the element. The 18% here and the HEADROOM
   there are the same number and have to stay that way. */
.ga-mascot__canvas { position: absolute; left: 0; top: -18%; width: 100%; height: 118%; }

/* ── Game bands ─────────────────────────────────────────────────────────── */

.band {
	position: relative;
	padding-block: clamp(72px, 12vh, 150px);
	border-top: 1px solid var(--ga-border);
	overflow: clip;
	/* Each band publishes its own scroll progress so its parts can be driven
	   from how far the band itself has travelled, rather than from the page. */
	view-timeline: --band block;
}

/* The colour arrives with the band rather than being painted on it. At the top
   of the viewport the wash is absent; by the middle it is fully present. */
.band::before {
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(70vw 60vh at 78% 12%, color-mix(in srgb, var(--hue) 30%, transparent), transparent 70%),
		linear-gradient(180deg, color-mix(in srgb, var(--hue) 9%, transparent), transparent 58%);
	z-index: -1;
	opacity: 0;
}

.band__in {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: clamp(24px, 5vw, 64px);
}

.band__index {
	font-size: var(--ga-label-size);
	letter-spacing: var(--ga-label-track);
	color: color-mix(in srgb, var(--hue) 78%, white);
	margin-bottom: 18px;
	display: flex;
	align-items: center;
	gap: 14px;
}
.band__index::after {
	content: "";
	height: 1px;
	width: clamp(40px, 8vw, 120px);
	background: linear-gradient(90deg, color-mix(in srgb, var(--hue) 70%, transparent), transparent);
}

.band__title {
	font-size: clamp(2.6rem, 7.6vw, 6.4rem);
	margin-bottom: 6px;
}
.band__sub {
	font-size: clamp(1.05rem, 1.8vw, 1.45rem);
	color: color-mix(in srgb, var(--hue) 32%, var(--ga-text));
	font-variation-settings: "wdth" var(--ga-wdth-sub);
	font-weight: 600;
	margin-bottom: 20px;
}
.band__line { color: var(--ga-text-muted); max-width: 52ch; }

/* The icon is the link. It is the largest, most recognisable thing in the band
   and the one a visitor already wants to press — routing them through a text
   link beside it would be asking them to aim somewhere worse. */
.band__iconlink {
	display: block;
	border-radius: 22%;
	transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.band__iconlink:hover { transform: translateY(-6px) scale(1.03); }
.band__iconlink:focus-visible { outline: 2px solid var(--ga-text); outline-offset: 8px; }

.band__icon {
	width: clamp(96px, 15vw, 210px);
	border-radius: 22%;
	border: 1px solid color-mix(in srgb, var(--hue) 40%, var(--ga-border));
	box-shadow: 0 0 0 1px rgb(0 0 0 / 0.4);
}

.band__cta {
	margin-top: 28px;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}
.btn {
	display: inline-flex;
	align-items: center;
	padding: 11px 22px;
	border-radius: 999px;
	border: 1px solid var(--ga-border);
	background: var(--ga-surface);
	font-size: 0.9rem;
	font-weight: 600;
	text-decoration: none;
	transition: border-color 0.2s, transform 0.2s, background 0.2s;
}
.btn:hover { transform: translateY(-2px); border-color: color-mix(in srgb, var(--hue) 60%, var(--ga-border)); }
.btn--lead { background: var(--hue); border-color: transparent; color: #0b0b10; }

/* The rail. Uniform height, width from the artwork — these games are not the
   same shape and matching on width would crop the landscape one or letterbox
   the other two. */
.rail {
	margin-top: clamp(36px, 6vw, 72px);
	display: flex;
	gap: 18px;
	width: max-content;
	will-change: transform;
}
/* Bounded by a box rather than by a height.

   Height alone works for the two portrait games — at 420px tall a 9:16 shot is
   237px wide and six of them cross the screen. Your Balls is played in
   landscape: the same 420px makes it 745px wide, so two of them are wider than
   a 1440px window and the second one is permanently half off the right edge.
   Taller the window, worse it gets, because the height clamp grows with it.

   Capping the width as well as the height, with both dimensions auto, keeps
   every shot at its own ratio and stops one game's artwork from taking half
   the screen. Landscape shots come out shorter than portrait ones — which is
   what they are. */
.rail img {
	height: auto;
	width: auto;
	max-height: clamp(260px, 42vh, 420px);
	max-width: min(34vw, 520px);
	border-radius: 16px;
	border: 1px solid color-mix(in srgb, var(--hue) 26%, var(--ga-border));
}

.rail-clip {
	margin-inline: calc(50% - 50vw);
	padding-inline: var(--ga-gutter);
	overflow: clip;
}

/* ── Studio ─────────────────────────────────────────────────────────────── */

.studio { padding-block: clamp(80px, 14vh, 170px); border-top: 1px solid var(--ga-border); }
/* The heavier column is the writing now, not the picture: the heading and the
   text belong together, and splitting them across the gap made the heading look
   like a label for the illustration rather than for what it introduces. */
.studio__grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: clamp(28px, 6vw, 80px); align-items: center; }
.studio__grid > div:first-child > p:first-of-type { margin-top: clamp(18px, 2.5vw, 30px); }

/* The opening line is the hook, so it is set as one — larger, in full-strength
   text rather than the muted grey the body runs in. Left at body size it read
   as the first of three equal paragraphs and the joke landed on nobody. */
/* Written as `.studio .studio__lead` on purpose. `.studio p` sets the body grey
   and is one point more specific than a lone class, so the white here was being
   overridden — the line only looked white because it is large. Measured, not
   eyeballed. */
.studio .studio__lead {
	font-size: clamp(1.25rem, 2.1vw, 1.75rem);
	line-height: 1.35;
	color: var(--ga-text);
	font-variation-settings: "wdth" var(--ga-wdth-sub);
	font-weight: 600;
	letter-spacing: -0.015em;
	text-wrap: balance;
	/* Wide enough for the longest of the two sentences to hold one line. At 26ch
	   the measure was 393px inside a 628px column — 235px going unused while
	   "There is no avocado in any of our games." broke across two lines and its
	   companion sat on one. Sentences on their own lines only reads as
	   deliberate when each one actually fits. */
	max-width: 34ch;
}

.studio__grid .crest { margin-top: 0; margin-inline: auto; }
.studio h2 { font-size: clamp(2.2rem, 5vw, 4rem); }
.studio p { color: var(--ga-text-muted); margin-top: 18px; max-width: 56ch; }
.studio p a { color: var(--ga-lime); }

.facts { margin-top: 40px; display: grid; gap: 1px; background: var(--ga-border); border: 1px solid var(--ga-border); border-radius: 16px; overflow: hidden; }
.fact { background: var(--ga-ink); padding: 20px 22px; display: flex; justify-content: space-between; gap: 20px; align-items: baseline; }
.fact dt { color: var(--ga-text-muted); font-size: 0.86rem; letter-spacing: 0.06em; text-transform: uppercase; }
.fact dd { font-variation-settings: "wdth" var(--ga-wdth-sub); font-weight: 700; font-size: 1.05rem; text-align: right; }

.foot { border-top: 1px solid var(--ga-border); padding-block: 44px; color: var(--ga-text-muted); font-size: 0.88rem; }

/* ── Between pages ──────────────────────────────────────────────────────────
   The reason an Apple product page feels like an app rather than a website is
   that objects survive navigation: the thing you tapped is still the thing you
   are looking at a moment later. The browser will now do this across a real
   document load, so it costs a rule and a name — no router, no single-page
   app, no framework.

   Each icon is named, and the game page names its own hero icon the same
   thing. The browser matches them and animates between the two positions
   itself; nothing here says how, because nothing here knows where either page
   will put them. */

@view-transition { navigation: auto; }

/* The names themselves are written by PHP, from each game's slug, onto the
   icon on every surface that shows it — the band, the listing card and the
   game page. Three CSS rules used to assign them here by position
   (`icon-yb`, `icon-ec`, `icon-md`, from the prototype) and were dead: the
   inline style always won, and the positions no longer matched the order the
   games are published in either. */

/* Everything that is not the travelling icon cross-fades under it. The default
   is a hard cut on the root, which reads as a page load — exactly the thing
   being hidden. */
::view-transition-old(root) { animation: vt-out 220ms cubic-bezier(0.4, 0, 1, 1) both; }
::view-transition-new(root) { animation: vt-in 340ms cubic-bezier(0, 0, 0.2, 1) both; }

@keyframes vt-out { to { opacity: 0; } }
@keyframes vt-in { from { opacity: 0; } }

/* The icon's own flight. Slightly slower than the fade so it stays legible as
   the page changes underneath it, and eased so it settles rather than stops. */
/* Every named element, whatever it is called. Naming the three from the
   prototype meant this timing applied to nothing at all — the real names are
   `icon-endless-colonies` and its siblings, and a fourth game would have
   invented a fourth. The wildcard needs no list and cannot fall behind one. */
::view-transition-group(*) {
	animation-duration: 520ms;
	animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Reduced motion stops the animation rather than the transition.
   `@view-transition` used to be declared a second time inside this block with
   `navigation: none`, which is the shape the spec allows and which Chrome does
   not honour as written: the later declaration won regardless of whether the
   condition matched, so the transition never ran for anyone. Turning the
   animations off gets the same result for the people who asked for it without
   an at-rule whose media condition is ignored. */
@media (prefers-reduced-motion: reduce) {
	::view-transition-group(*),
	::view-transition-old(root),
	::view-transition-new(root) { animation: none; }
}

/* ── Motion ─────────────────────────────────────────────────────────────────
   All of it is scroll-driven CSS. No listener, no library, and it runs on the
   compositor rather than in a scroll handler, so it cannot jank the way a
   JavaScript version can. Verified supported before being relied on. */

@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		/* The panel's progress line, driven by the document's own scroll rather
		   than by a listener recomputing a percentage on every frame. */
		.head__in::after {
			animation: progress linear both;
			animation-timeline: scroll(root block);
		}
		@keyframes progress { to { transform: scaleX(1); } }

		.band::before {
			animation: wash linear both;
			animation-timeline: --band;
			animation-range: entry 0% cover 48%;
		}
		@keyframes wash { to { opacity: 1; } }

		.band__text > * {
			animation: rise linear both;
			animation-timeline: --band;
			animation-range: entry 12% entry 78%;
		}
		@keyframes rise {
			from { opacity: 0; transform: translateY(26px); }
			to   { opacity: 1; transform: none; }
		}

		.band__icon {
			animation: pop linear both;
			animation-timeline: --band;
			animation-range: entry 6% entry 82%;
		}
		@keyframes pop {
			from { opacity: 0; transform: translateY(40px) scale(0.9) rotate(-5deg); }
			to   { opacity: 1; transform: none; }
		}

		/* The parallax. The rail is wider than the viewport — deliberately, and
		   it is the one number this effect depends on. The first attempt used
		   six shots at 340px and Endless Colonies' rail came out 1126px inside
		   a 1440px window: narrower than the screen it was supposed to travel
		   across, so it slid around in open space instead of streaming past.
		   Every rail is now wider than the viewport by construction.

		   The offsets are symmetrical so the rail sits exactly at the gutter
		   when the band is centred — which is when it is being read. Starting
		   at zero instead would mean the first shot is already half gone by
		   then, and a cropped first frame reads as a bug rather than motion. */
		.rail {
			animation: slide linear both;
			animation-timeline: --band;
			animation-range: cover 0% cover 100%;
		}
		/* How far the rail travels decides how much of it is ever seen. The
		   window sweeps rail x from -pW to pW + V, so everything past pW + V
		   is downloaded, laid out, and never shown at any scroll position.

		   At ±16% that was four of Your Balls' eight frames — its landscape
		   shots are 672px wide, so the rail ran to 5492px and the sweep only
		   reached 2319. Measured, not guessed.

		   Full coverage needs p ≥ 1 − V/W. Across the three rails at the
		   narrowest desktop checked, the worst case wanted 0.44; 45 covers all
		   three with a little to spare, and still passes through zero at the
		   band's centre so the first frame sits on the gutter when it is read. */
		@keyframes slide {
			from { transform: translate3d(45%, 0, 0); }
			to   { transform: translate3d(-45%, 0, 0); }
		}
	}
}

/* ── Small screens ──────────────────────────────────────────────────────── */

@media (max-width: 860px) {
	.hero__in { grid-template-columns: minmax(0, 1fr); justify-items: start; }
	/* Two classes on purpose: `.ga-mascot { width: 100% }` is declared earlier
	   and at equal specificity would leave the character 400px wide inside a
	   390px phone. */
	.hero__in .ga-mascot { order: -1; width: clamp(150px, 44vw, 230px); }
	.band__in { grid-template-columns: minmax(0, 1fr); }
	.studio__grid { grid-template-columns: minmax(0, 1fr); }
	.head nav { gap: 16px; font-size: 0.72rem; }
	@keyframes slide {
		from { transform: translate3d(48%, 0, 0); }
		to   { transform: translate3d(-48%, 0, 0); }
	}
}

@media (prefers-reduced-motion: reduce) {
	.hero::before { animation: none; }
	.rail { transform: none; }
	.rail-clip { overflow-x: auto; }
}

/* ── Title as a link ────────────────────────────────────────────────────────
   The icon already navigates. The name is the other thing a visitor reaches
   for, and making them aim at a 210px square when a 6rem word is right there
   is a worse target for no reason. Both go to the same place, and the icon
   still carries the view-transition-name, so the flight happens either way. */

.band__titlelink {
	text-decoration: none;
	display: inline-block;
	transition: color 0.28s;
}
.band__titlelink:hover { color: color-mix(in srgb, var(--hue) 55%, var(--ga-text)); }
.band__titlelink:focus-visible { outline: 2px solid var(--hue); outline-offset: 6px; border-radius: 4px; }

/* The index line lost "Live on iOS and Android": the store buttons are eighty
   pixels below it saying the same thing with somewhere to click. */
.band__index { margin-bottom: 14px; }

/* ── Studio crest ───────────────────────────────────────────────────────────
   The left half of this section was empty. Rather than fill it with stock
   atmosphere, it holds the studio's mark with its three games arranged around
   it — every element already belongs to the studio.

   Sized off the container rather than the viewport so the arrangement holds
   together at any width instead of scattering. */

.crest {
	position: relative;
	margin-top: clamp(28px, 5vw, 52px);
	width: min(100%, 420px);
	aspect-ratio: 1;
	container-type: inline-size;
}

.crest__glow {
	position: absolute;
	inset: 6%;
	border-radius: 50%;
	background:
		radial-gradient(circle at 28% 26%, color-mix(in srgb, var(--ga-ec) 46%, transparent), transparent 58%),
		radial-gradient(circle at 76% 40%, color-mix(in srgb, var(--ga-yb) 42%, transparent), transparent 58%),
		radial-gradient(circle at 50% 84%, color-mix(in srgb, var(--ga-md) 42%, transparent), transparent 58%);
	filter: blur(26px);
}

.crest__mark {
	position: absolute;
	left: 50%;
	top: 50%;
	width: 46%;
	translate: -50% -50%;
}

.crest__game {
	position: absolute;
	width: 27cqw;
	border-radius: 22%;
	border: 1px solid rgb(255 255 255 / 0.12);
	box-shadow: 0 14px 34px rgb(0 0 0 / 0.5);
}

/* Placed by hand, not by a formula. Three points on a circle at exact
   intervals reads as a diagram; nudging them apart reads as arranged. */

@media (prefers-reduced-motion: no-preference) {
}

/* Nothing is hidden until the script has said it can put it back. `is-armed`
   is added from JavaScript, so a browser that never runs it — or a visitor who
   asked for less motion — gets the finished arrangement rather than an empty
   circle and a button that does nothing. */
.crest.is-armed .crest__game { opacity: 0; }
.crest.is-armed .crest__game.is-out { opacity: 1; }

/* What comes out once the games have: the work rather than the product. Sits
   above everything so it reads as leaving the character, not passing behind. */
.crest__spark {
	position: absolute;
	left: 50%;
	top: 76%;
	translate: -50% -50%;
	z-index: 3;
	padding: 6px 11px;
	border-radius: 10px;
	background: rgb(14 14 22 / 0.94);
	border: 1px solid rgb(255 255 255 / 0.14);
	color: var(--ga-lime);
	font-size: 0.82rem;
	font-weight: 700;
	font-variation-settings: "wdth" var(--ga-wdth-sub);
	letter-spacing: 0.02em;
	white-space: nowrap;
	pointer-events: none;
}

/* Art comes out in a different colour from code, so a run of presses reads as
   two kinds of work rather than one stream of tokens. */
.crest__spark--art { color: var(--ga-yb); }

/* The character speaking. A bubble with a tail rather than a control strip
   underneath: the studio section is about the studio, and a plain button below
   the illustration reads as interface bolted to a picture.

   Positioned top-right of the mark, which is the one part of the circle no icon
   claims — Endless Colonies sits top-left and Your Balls comes in on the right
   below it. */
.crest__say {
	position: absolute;
	left: 56%;
	top: 1%;
	z-index: 4;
	padding: 9px 16px;
	min-height: 40px;
	border-radius: 16px;
	border: 1px solid rgb(255 255 255 / 0.16);
	background: rgb(16 16 26 / 0.96);
	color: var(--ga-text);
	font: inherit;
	font-size: 0.8rem;
	font-variation-settings: "wdth" var(--ga-wdth-sub);
	font-weight: 600;
	letter-spacing: 0.01em;
	white-space: nowrap;
	cursor: pointer;
	box-shadow: 0 10px 26px rgb(0 0 0 / 0.5);
	transition: transform 0.24s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.24s, color 0.24s;
	animation: say-bob 3.4s ease-in-out infinite alternate;
}

/* The tail. Two triangles, the darker one a pixel inside the lighter, so the
   border appears to continue around the point instead of stopping at it. */
.crest__say::before,
.crest__say::after {
	content: "";
	position: absolute;
	left: 16px;
	width: 0;
	height: 0;
	border-style: solid;
}
.crest__say::before {
	bottom: -11px;
	border-width: 11px 10px 0 0;
	border-color: rgb(255 255 255 / 0.16) transparent transparent transparent;
}
.crest__say::after {
	bottom: -9px;
	border-width: 9px 8px 0 0;
	border-color: rgb(16 16 26 / 0.96) transparent transparent transparent;
}

.crest__say:hover { transform: translateY(-3px) scale(1.04); border-color: color-mix(in srgb, var(--ga-lime) 55%, transparent); color: var(--ga-lime); }
.crest__say:active { transform: translateY(1px) scale(0.98); }
.crest__say:focus-visible { outline: 2px solid var(--ga-lime); outline-offset: 3px; }

/* A bubble that sits perfectly still is a label. */
@keyframes say-bob {
	from { translate: 0 -3%; }
	to   { translate: 0 3%; }
}

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

.crest__game--1 { left: 1%;  top: 6%;   rotate: -9deg; }
/* Pushed out and down from where it started: at `top: 30%` it landed across
   the mark's arm and over part of the wordmark, which read as a stacking
   mistake rather than an arrangement. */
/* Raised from 38%. Measured boxes: the mark runs y 26–74 and its right mitten
   sits around y 54–60, which is exactly where this icon used to be. Moving the
   icon frees the hand rather than pushing the tool out to the corner, where it
   stopped reading as something held. */
.crest__game--2 { right: -3%; top: 26%; rotate: 7deg; }
.crest__game--3 { left: 20%; bottom: 0%; rotate: -4deg; }

/* ── Contact ────────────────────────────────────────────────────────────── */

.contact { padding-block: clamp(70px, 12vh, 150px); border-top: 1px solid var(--ga-border); }
.contact__grid {
	display: grid;
	grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
	gap: clamp(32px, 6vw, 90px);
	align-items: start;
}
.contact__lede { color: var(--ga-text-muted); margin-top: 18px; max-width: 44ch; }
.contact__alt { color: var(--ga-text-muted); margin-top: 20px; font-size: 0.94rem; }
.contact__alt a { color: var(--ga-lime); }

.form { display: grid; gap: 18px; }

.field { display: grid; gap: 7px; }
.field label { font-size: 0.82rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ga-text-muted); }
.field input,
.field textarea {
	font: inherit;
	color: var(--ga-text);
	background: rgb(255 255 255 / 0.04);
	border: 1px solid var(--ga-border);
	border-radius: 12px;
	padding: 12px 14px;
	transition: border-color 0.2s, background 0.2s;
}
.field textarea { resize: vertical; min-height: 120px; }
.field input:focus,
.field textarea:focus {
	outline: none;
	border-color: color-mix(in srgb, var(--ga-lime) 60%, var(--ga-border));
	background: rgb(255 255 255 / 0.07);
}
.field__hint { font-size: 0.8rem; color: var(--ga-text-muted); }
.field--check b { color: var(--ga-text); }

/* Not `display: none`: a bot reading the DOM sees that and skips the field,
   which is the whole point of it. Moved out of sight instead, and out of the
   tab order and the accessibility tree by the attributes on the markup. */
.trap {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.form__send { justify-self: start; margin-top: 4px; --hue: var(--ga-lime); }

/* Name beside email, topic beside subject. Four short fields stacked in one
   column makes the form look longer than it is, and length is what stops
   someone filling one in. */
.form__row {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 18px;
}

/* The select is styled with the inputs rather than left to the platform, or the
   one control that routes the message to a different mailbox would be the one
   that looks like it belongs to another site. */
.field select {
	font: inherit;
	color: var(--ga-text);
	background: rgb(255 255 255 / 0.04);
	border: 1px solid var(--ga-border);
	border-radius: 12px;
	padding: 12px 40px 12px 14px;
	appearance: none;
	background-image: linear-gradient(45deg, transparent 50%, var(--ga-text-muted) 50%),
		linear-gradient(135deg, var(--ga-text-muted) 50%, transparent 50%);
	background-position: calc(100% - 20px) center, calc(100% - 15px) center;
	background-size: 5px 5px, 5px 5px;
	background-repeat: no-repeat;
	transition: border-color 0.2s, background-color 0.2s;
}
.field select:focus {
	outline: none;
	border-color: color-mix(in srgb, var(--ga-lime) 60%, var(--ga-border));
}
/* The menu itself is drawn by the OS, so it needs its own colours or it opens
   as white text on white. */
.field select option { background: var(--ga-surface); color: var(--ga-text); }

/* The two mailboxes, stated before the form rather than discovered after it.
   Someone who would rather just send an email should not have to fill in a
   form to find the address. */
.routes { margin-top: 26px; display: grid; gap: 1px; background: var(--ga-border); border: 1px solid var(--ga-border); border-radius: 14px; overflow: hidden; }
.route { background: var(--ga-ink); padding: 16px 18px; display: flex; flex-wrap: wrap; gap: 8px 20px; justify-content: space-between; align-items: baseline; }
.route dt { font-size: 0.82rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ga-text-muted); }
/* A <dd> carries a 40px start margin by default. Side by side with its <dt>
   that margin is invisible — the flex row puts the address on the right anyway.
   Wrapped onto its own line on a phone it is suddenly a 40px indent nobody
   asked for, which is why the addresses looked pushed into the middle of the
   box while the labels sat at the edge. */
.route dd { margin-inline-start: 0; }
.route dd a { color: var(--ga-lime); text-decoration: none; font-size: 0.94rem; }
.route dd a:hover { text-decoration: underline; }

@media (max-width: 560px) {
	.form__row { grid-template-columns: minmax(0, 1fr); }
}

/* ── Footer ─────────────────────────────────────────────────────────────────
   A brand block and four columns rather than one long row. The row was seven
   links wide and read as a sentence: App Store Google Play LinkedIn YouTube
   Privacy Terms Contact, with nothing telling you where one idea ended and the
   next began. */

.foot {
	border-top: 1px solid var(--ga-border);
	padding-block: clamp(48px, 7vw, 76px) 0;
	color: var(--ga-text-muted);
	font-size: 0.9rem;
}

.foot__in {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr);
	gap: clamp(32px, 6vw, 80px);
	align-items: start;
	padding-bottom: clamp(36px, 5vw, 56px);
}

.foot__brand { display: grid; gap: 16px; justify-items: start; }
.foot__mark { width: 62px; opacity: 0.9; }
.foot__line { max-width: 30ch; margin: 0; line-height: 1.6; }

/* The first sentence carries the weight; the "made in" half is the quiet note
   after it. put.io does the same — one line you read, one you glance at.

   On its own line, though. Flowing as one paragraph, the tagline wrapped and
   left "playing." stranded at the start of the second line with "Made in
   Istanbul" running on directly after it — two separate statements colliding
   in the middle of a line, which reads as one broken sentence. */
.foot__tag { color: var(--ga-text); display: block; }

.foot__cols {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: clamp(20px, 3vw, 40px);
}

/* The group labels. Small, spaced and quiet — they are signposts, not headings
   competing with the links under them. */
.foot__h {
	font-size: var(--ga-label-size);
	font-weight: 600;
	letter-spacing: var(--ga-label-track);
	text-transform: uppercase;
	color: #6f6f86;
	margin: 0 0 14px;
}

.foot__col ul,
.foot__legal { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.foot__col a,
.foot__legal a {
	display: inline-block;
	padding-block: 5px;
	text-decoration: none;
	border-bottom: 1px solid transparent;
	transition: color 0.2s, border-color 0.2s;
}

.foot__col a:hover,
.foot__legal a:hover { color: var(--ga-text); border-bottom-color: currentColor; }
.foot__col a:focus-visible,
.foot__legal a:focus-visible { outline: 2px solid var(--ga-text); outline-offset: 3px; border-radius: 2px; }

/* Marks the links that leave the site, so nobody has to hover to find out. */
.foot__out { font-size: 0.72em; margin-left: 5px; opacity: 0.55; vertical-align: 0.15em; }

/* The registered name and the city. Set in the mono face at the smallest size
   on the page: this is a matter of record, not something being sold. */
.foot__bar {
	border-top: 1px solid var(--ga-border);
	padding-block: 20px 26px;
	font-size: 0.76rem;
	letter-spacing: 0.02em;
	color: #62627a;
}

@media (max-width: 860px) {
	.foot__in { grid-template-columns: minmax(0, 1fr); }
	.foot__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 20px; }
}

@media (max-width: 420px) {
	.foot__cols { grid-template-columns: minmax(0, 1fr); }
}

.studio__h { font-size: clamp(2.2rem, 5vw, 4rem); }

@media (max-width: 860px) {
	.contact__grid { grid-template-columns: minmax(0, 1fr); }
	.crest { width: min(100%, 320px); }
}

/* ── WordPress joins ─────────────────────────────────────────────────────────
   The prototype wrote its nav as bare anchors; a WordPress menu always wraps
   each item in an <li>. `display: contents` removes the wrapper from layout
   without removing it from the document, so the anchors become direct children
   of the flex row and the CSS above needs no branch for it. */

.head nav ul,
.head nav li { display: contents; }

/* Link colour. The prototype set `a { color: inherit }` once and then coloured
   the few links that needed it; the theme's own sheet paints every anchor brand
   purple instead, which turned the header wordmark and nav purple the moment
   these two sheets met.

   Written as named rules rather than a blanket `.head a, .foot a { … }`,
   because a selector broad enough to catch them all also catches `.btn--lead`
   — and that one carries dark text on a lime fill deliberately.

   `.lost a` was the one that slipped through anyway: the 404's only link is
   that button, and `.lost a` (a class plus a type) outranks `.btn--lead` (a
   class), so `inherit` won and the button drew near-white text on lime. The
   `:not(.btn)` is what the note above was asking for. */

.head__mark,
.head nav a,
.band__titlelink,
.card__title a,
.foot a,
.lost a:not(.btn) { color: inherit; }

/* Buttons state their own, so neither sheet's default reaches them. */
.btn { color: var(--ga-text); }
.btn--lead { color: #0b0b10; }

/* The theme underlines links on hover; inside these the movement is the
   feedback, and an underline appearing under a 6rem title is a different
   design. */
.head a:hover,
.band__titlelink:hover,
.card__title a:hover,
.btn:hover,
.crest a:hover { text-decoration: none; }

/* ── Documents ───────────────────────────────────────────────────────────────
   The agreements, and any other page that is mostly running text.

   One decision matters here and it is the measure. The privacy policy is over
   six thousand words; set to the full column that is a wall, and both app
   stores link to it. 66 characters is the width the eye can return from
   without losing its place. */

.doc { padding-block: clamp(120px, 20vh, 200px) clamp(70px, 12vh, 140px); }

.doc__title {
	font-size: clamp(2.2rem, 6vw, 4.4rem);
	margin-bottom: clamp(28px, 4vw, 48px);
	max-width: 18ch;
}

/* 66ch is the width a reader returns from without losing their place, and it
   is the right number for something read start to finish. An agreement is not
   read that way — it is scanned for the clause someone came for, and at 66ch on
   a wide screen that meant 43% of the window in use and a page a quarter longer
   than it needed to be. Widened to the point where the measure still holds but
   the scrolling stops being the dominant experience. */
.doc__body .ga-prose { max-width: 86ch; }

/* Headings inside the document need air above them and almost none below —
   a heading closer to the paragraph it introduces than to the one it follows
   is how a reader knows which is which without thinking about it. */
.doc__body .ga-prose h2,
.doc__body .ga-prose h3 {
	margin-top: clamp(36px, 5vw, 60px);
	margin-bottom: 10px;
	font-variation-settings: "wdth" var(--ga-wdth-sub);
	letter-spacing: -0.01em;
	/* Sized so the document's own headings sit under the page title rather than
	   argue with it. Unset, they inherited the theme's h2 and came out large
	   enough to wrap — the agreement announcing itself twice, the second time
	   across two lines. */
	font-size: clamp(1.15rem, 1.7vw, 1.45rem);
	font-weight: 700;
	line-height: 1.25;
}

/* The first one is the document restating its own title, which the page has
   already said in the h1 above it. Kept in the content — it belongs to the
   agreement — but set at the size of a section heading, not a second title. */
.doc__body .ga-prose > h2:first-child {
	margin-top: 0;
	font-size: clamp(1rem, 1.4vw, 1.2rem);
	color: var(--ga-text-muted);
	font-weight: 600;
}

.doc__body .ga-prose p { margin-bottom: 16px; }
.doc__body .ga-prose ul,
.doc__body .ga-prose ol { margin: 0 0 18px 1.2em; }
.doc__body .ga-prose li { margin-bottom: 6px; }
.doc__body .ga-prose a { color: var(--ga-lime); }

/* ── Games listing ──────────────────────────────────────────────────────────
   What the nav's Games link opens. Rows rather than a three-up grid: a grid of
   equal cards is the shape every generated page reaches for, it gives each game
   a caption's worth of room, and it breaks the day a fourth game ships. A list
   survives that, and gives each title enough space to say something.

   The listing has no single hue of its own, so each row carries the colour of
   the game in it. */

.listing { padding-block: clamp(120px, 20vh, 200px) clamp(50px, 8vh, 90px); }
.listing__h { font-size: clamp(2.6rem, 8vw, 6.4rem); margin-block: 8px 16px; }
.listing__lede { color: var(--muted); max-width: 46ch; font-size: clamp(1rem, 1.5vw, 1.2rem); }

.cards { list-style: none; display: grid; gap: 0; margin-top: clamp(30px, 6vw, 64px); }

.card {
	position: relative;
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	gap: clamp(20px, 4vw, 52px);
	align-items: center;
	padding-block: clamp(28px, 5vw, 54px);
	border-top: 1px solid var(--border);
}
.card:last-child { border-bottom: 1px solid var(--border); }

/* The colour arrives on hover rather than sitting there: three saturated washes
   stacked down a page compete with each other and with the artwork. */
.card::before {
	content: "";
	position: absolute;
	inset: 0;
	background: radial-gradient(50vw 40vh at 8% 50%, color-mix(in srgb, var(--hue) 20%, transparent), transparent 70%);
	opacity: 0;
	transition: opacity 0.45s;
	pointer-events: none;
}
.card:hover::before,
.card:focus-within::before { opacity: 1; }

.card__iconlink { display: block; border-radius: 22%; transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
.card:hover .card__iconlink { transform: translateY(-5px) scale(1.03); }
.card__iconlink:focus-visible { outline: 2px solid var(--text); outline-offset: 8px; }

.card__icon {
	width: clamp(92px, 13vw, 168px);
	border-radius: 22%;
	border: 1px solid color-mix(in srgb, var(--hue) 40%, var(--border));
}

.card__title { font-size: clamp(1.9rem, 4.4vw, 3.4rem); margin-bottom: 4px; }
.card__title a { text-decoration: none; transition: color 0.28s; }
.card__title a:hover { color: color-mix(in srgb, var(--hue) 55%, var(--text)); }
.card__title a:focus-visible { outline: 2px solid var(--hue); outline-offset: 6px; border-radius: 4px; }

@media (max-width: 720px) {
	.card { grid-template-columns: minmax(0, 1fr); }
}

/* ── Stacked order: number, icon, title ──────────────────────────────────────
   Side by side the icon is a column of its own and needs no ordering. Stacked
   it does, and the order it wants is the order a person reads an entry in: the
   number opens it, the icon says which game this is, then the name.

   Both surfaces put the icon and the text in separate wrappers so the wide
   layout can be two columns, which leaves them at different depths — `order`
   only sorts siblings. `display: contents` drops the text wrapper out of the
   box tree for the width where it isn't wanted, so all six pieces become
   siblings and sort against each other directly. The markup does not change,
   and neither does the wide layout.

   This replaces `.band__icon { order: -1 }`, which named the <img> rather than
   the link that is actually the grid item — so it sorted nothing, and the icon
   sat wherever the source happened to leave it: last on the front page, first
   in the listing. */
@media (max-width: 860px) {
	.band__text { display: contents; }

	.band__index    { order: 1; }
	.band__iconlink { order: 2; }
	.band__title    { order: 3; }
	.band__sub      { order: 4; }
	.band__line     { order: 5; }
	.band__cta      { order: 6; }

	/* Full width it would dominate the entry it is introducing. */
	.band__iconlink { width: clamp(84px, 22vw, 128px); margin-bottom: 18px; }

	/* The grid gap separated two columns; flattened it separates all six pieces,
	   so a 64px column gutter became 64px between the title and its own tagline.
	   Each element already carries the margin the design gave it — 18 under the
	   number, 6 under the title, 20 under the tagline — so the gap goes to zero
	   and that rhythm comes back. */
	.band__in { row-gap: 0; }
}

@media (max-width: 720px) {
	.card__body { display: contents; }

	.card__iconlink { order: 2; width: clamp(76px, 20vw, 116px); margin-bottom: 16px; }
	.card__title    { order: 3; }

	/* Same reason as the band above: the gap was a column gutter. */
	.card { row-gap: 0; }

	/* The listing shares .band__index, .band__sub, .band__line and .band__cta
	   with the front page, so those carry the orders set above. */
}

/* The 404 gets the same treatment as everything else rather than a stripped
	   apology page. Someone lands here from a stale link or a typo, and the
	   thing that helps is a route onward — so the routes are the loudest thing
	   after the number.

	   The character is the point of the joke. It is looking around because it
	   is idling, and idling happens to look exactly like searching. */

	.lost {
		position: relative;
		min-height: 84svh;
		display: grid;
		align-content: center;
		padding-block: clamp(120px, 20vh, 220px) clamp(60px, 10vh, 120px);
		overflow: clip;
	}

	/* The three game colours again, so even the error page belongs to the
	   catalogue. Same fields as the front page, dimmer. */
	.lost::before {
		content: "";
		position: absolute;
		inset: -20% -10% auto -10%;
		height: 120%;
		background:
			radial-gradient(36vw 36vw at 16% 24%, color-mix(in srgb, var(--ga-yb) 20%, transparent), transparent 68%),
			radial-gradient(40vw 40vw at 84% 20%, color-mix(in srgb, var(--ga-ec) 18%, transparent), transparent 66%),
			radial-gradient(38vw 38vw at 58% 86%, color-mix(in srgb, var(--ga-md) 16%, transparent), transparent 68%);
		filter: blur(30px);
		z-index: -1;
	}

	.lost__in {
		display: grid;
		justify-items: center;
		text-align: center;
		gap: clamp(4px, 1vw, 12px);
	}

	/* Centred, so the measure has to be held deliberately — a centred paragraph
	   running the full column width is the hardest thing on a page to read. */
	.lost__in > * { max-width: min(100%, 46ch); }
	.lost__num { max-width: none; }

	.lost__num {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 0.02em;
		/* The number is a paragraph, and a paragraph's default margin is 1em —
		   which at this font size is 208px above and 208px below. That is where
		   the empty half-screen between the digits and the sentence came from:
		   not a layout decision, just the browser's paragraph spacing scaled up
		   by a 13rem type size. The gap below is set by .lost__line instead. */
		margin: 0;
		font-variation-settings: "wdth" var(--ga-wdth-display);
		/* 800, the same as every other piece of display type. This was the one
		   element on the site set at 900 — a fifth weight introduced for a single
		   heading, and at 208px the difference is barely a hairline. */
		font-weight: 800;
		font-size: clamp(5rem, 19vw, 13rem);
		line-height: 0.8;
		letter-spacing: -0.05em;
	}

	/* Only the digits take the gradient. The mark is artwork and keeps its own
	   white, which is what makes it read as a thing standing in the number
	   rather than a glyph the font happened to draw. */
	.lost__num > span[aria-hidden] {
		color: transparent;
		background: linear-gradient(96deg, var(--ga-yb), var(--ga-ec) 52%, var(--ga-md));
		-webkit-background-clip: text;
		background-clip: text;
	}

	/* Sized in em so it tracks the digits at every viewport without a second
	   clamp to keep in step. */
	/* Two classes on purpose. `.ga-mascot { width: 100% }` is declared further
	   down this same sheet, and at equal specificity the later rule wins — the
	   mark filled the whole line and pushed the digits off screen. */
	.lost__num .lost__o {
		width: 0.74em;
		flex: none;
		margin-inline: 0.04em;
	}

	.lost__line {
		font-size: clamp(1.2rem, 2.2vw, 1.8rem);
		font-variation-settings: "wdth" var(--ga-wdth-sub);
		font-weight: 600;
		letter-spacing: -0.015em;
		margin-top: clamp(10px, 1.6vw, 20px);
		max-width: 22ch;
	}

	.lost__sub { color: var(--ga-text-muted); margin-top: 14px; max-width: 44ch; }

	.lost__ways { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: clamp(26px, 4vw, 40px); }
	.lost__ways .btn--lead { --hue: var(--ga-lime); }

/* ── Three things the sweep across 9 pages × 4 widths turned up ──────────── */

/* Form controls do not inherit type. The browser hands them its own UI font —
   Arial here — so every input and button on the site was set in a different
   face from the words next to them. Nobody names a second font; the engine
   supplies one unless it is told not to. */
input, select, textarea, button {
	font: inherit;
	letter-spacing: inherit;
}

/* The header is fixed, so the first thing on a page has to be told about it.
   Only the front page knew: its hero carries its own generous top padding,
   while the contact page opened at 56px against a header whose lower edge sits
   at 68 — the h1 rendered twelve pixels behind the glass. Stated once, as the
   space the header actually occupies, so a page added later inherits it. */
:root { --ga-head-clear: calc(clamp(10px, 1.6vw, 20px) + 58px + 28px); }

main > *:first-child { padding-top: max(var(--ga-head-clear), 3vw); }
/* The hero already reserves its own and is taller than the token asks for. */
main > .hero:first-child { padding-top: clamp(104px, 17vh, 200px); }

/* Long unbroken strings — an email address, a URL — in six thousand words of
   policy pushed the 320px layout 32px wider than the screen. Prose wraps
   inside a word only when the word alone would not fit, which is exactly the
   case here and nowhere else. */
.ga-prose { overflow-wrap: anywhere; }
.ga-prose table { display: block; overflow-x: auto; max-width: 100%; }

/* ── Contact page ───────────────────────────────────────────────────────── */

.contact--page { padding-block: 0 clamp(70px, 12vh, 140px); }
.contact__h { font-size: clamp(2.4rem, 6vw, 4.6rem); margin-bottom: clamp(18px, 3vw, 30px); }

.contact__where { margin-top: clamp(30px, 5vw, 52px); }
.contact__where h2 {
	font-size: var(--ga-label-size);
	letter-spacing: var(--ga-label-track);
	text-transform: uppercase;
	color: var(--ga-text-muted);
	margin-bottom: 10px;
}
.contact__where p { color: var(--ga-text-muted); line-height: 1.7; }

/* ── Line breaking ──────────────────────────────────────────────────────────
   The browser's default breaks a paragraph greedily: it fills each line as
   full as it can and whatever is left lands on the last one. That is how
   "…are the ones costing / you." and "…nobody to / blame." happen — a single
   word alone on a final line, which reads as a mistake even to someone who
   could not name it. A dash at the end of a line comes from the same habit.

   `pretty` makes the engine look at the paragraph as a whole and shuffle the
   earlier breaks to avoid the short last line. `balance` evens the lines of a
   heading against each other, which is what a headline wants and what a long
   paragraph does not — it is capped at a few lines by the spec.

   Doing this in CSS rather than by hand matters: hand-placed breaks are
   correct at exactly one width and wrong at every other, and this site is read
   from 320px to 1440. */

.display,
.hero__title, .band__title, .card__title,
.listing__h, .doc__title, .contact__h, .lost__line, .studio__h {
	text-wrap: balance;
}

/* Named by structure rather than by class. The first version of this rule
   listed the classes it knew about and missed the studio paragraph, which
   carries no class at all — and a rule that has to be extended every time
   someone adds a paragraph is a rule that will be wrong by next week. Running
   text is running text wherever it appears.

   `balance`, not `pretty`, and that was measured rather than assumed: on the
   studio paragraph `pretty` changed nothing at all — four lines of 340, 340,
   340 and a 130px stub — while `balance` returned four even lines of 289. The
   copy on this site is short enough for it; balance is capped at a handful of
   lines by the spec, which is the case it is built for. */
p, li, dd, figcaption, .hero__lede, .band__sub, .field__hint {
	text-wrap: balance;
}

/* Long-form is the case balance is not built for, and past its line cap the
   engine simply drops back to greedy. The agreements run to six thousand
   words, so they get the treatment that works at that length.

   Scoped to the document pages rather than to `.ga-prose`, because that class
   dresses both. A game's description is `.ga-prose` too and runs to a single
   short paragraph — thirty-four words, which balance sets as five even lines
   and pretty leaves ending on a 59px stub. The length is what decides here,
   and the page type is the honest proxy for it. */
.doc__body p, .doc__body li {
	text-wrap: pretty;
}

/* ── Screenshot showcase ─────────────────────────────────────────────────────
   Replaces the horizontal strip on a game page. Same store artwork, except one
   frame is on screen at a time and there is a sentence next to it.

   `--hue` is whatever the game page already set, so this takes the colour of
   the game it is showing without being told which game that is. */

.show-wrap { padding-block: clamp(14px, 2vh, 28px) 0; }

.show {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	gap: clamp(30px, 6vw, 80px);
	align-items: start;
	/* Each step declares a timeline; they are read by the images, which sit in
	   a different subtree. `timeline-scope` is what makes a name declared on a
	   child visible to a sibling — without it every image would have to be a
	   descendant of its own step, which is the layout this is avoiding. */
	timeline-scope: --s1, --s2, --s3, --s4, --s5, --s6, --s7, --s8, --s9, --s10;
}

/* Sticky across a full viewport with the artwork centred inside it.

   An earlier version stuck at `top: 50%` and pulled itself back with
   `translate: 0 -50%`. That centred it and also painted it several hundred
   pixels above its own grid area, straight over the page title: a transform
   moves where a box is drawn, not where layout thinks it is, so sticky never
   knew it had left the room. Giving the sticky element the viewport's height
   and centring its contents gets the same result without leaving the box. */
.show__frame {
	position: sticky;
	top: 0;
	height: 100svh;
	display: grid;
	place-items: center;
}

/* One cell, every shot stacked in it. Grid rather than absolute positioning so
   each image keeps its intrinsic size: two of these games are portrait and one
   is landscape, and a box with a single fixed ratio crops the store caption off
   whichever shape it was not built for. Bounding by max-width and max-height
   with auto sizing is the only way to fit an image in a box without distorting
   it — the same fault that squashed the landscape shots in the old strip. */
.show__phone {
	position: relative;
	display: grid;
	place-items: center;
	width: 100%;
}

.show__shot {
	grid-area: 1 / 1;
	max-width: min(100%, 340px);
	max-height: 74svh;
	width: auto;
	height: auto;
	border-radius: 22px;
	border: 1px solid color-mix(in srgb, var(--hue) 34%, var(--ga-border));
	box-shadow: 0 30px 80px rgb(0 0 0 / 0.55);
	opacity: 0;
}

/* Landscape artwork gets the width instead: capped at a phone's width it would
   be a third of the height of the portrait games and look like a mistake. */
.show__phone:has(.show__shot[width="1778"]) .show__shot { max-width: min(100%, 560px); }

.show__phone::before {
	content: "";
	position: absolute;
	inset: -18%;
	background: radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, var(--hue) 58%, transparent), transparent 68%);
	filter: blur(52px);
	z-index: -1;
}

.step {
	/* A full viewport each. At 92svh a step was only 0.479 of a timeline behind
	   the next one rather than 0.5, and that missing 2% was enough to leave two
	   frames lit at once — measured, not guessed: 20 of 209 sampled scroll
	   positions had a second shot at full opacity. */
	min-height: 100svh;
	display: grid;
	align-content: center;
	padding-block: 2vh;
}

.step__n {
	font-size: var(--ga-label-size);
	letter-spacing: var(--ga-label-track);
	color: color-mix(in srgb, var(--hue) 78%, white);
	margin: 0 0 12px;
	display: flex;
	align-items: center;
	gap: 12px;
}
.step__n::after {
	content: "";
	height: 1px;
	width: 46px;
	background: color-mix(in srgb, var(--hue) 55%, transparent);
}

.step__h { font-size: clamp(1.7rem, 3.2vw, 2.8rem); margin: 0 0 14px; }
.step__p { color: var(--ga-text-muted); margin: 0; max-width: 46ch; }

@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		.step:nth-of-type(1)  { view-timeline-name: --s1; }
		.step:nth-of-type(2)  { view-timeline-name: --s2; }
		.step:nth-of-type(3)  { view-timeline-name: --s3; }
		.step:nth-of-type(4)  { view-timeline-name: --s4; }
		.step:nth-of-type(5)  { view-timeline-name: --s5; }
		.step:nth-of-type(6)  { view-timeline-name: --s6; }
		.step:nth-of-type(7)  { view-timeline-name: --s7; }
		.step:nth-of-type(8)  { view-timeline-name: --s8; }
		.step:nth-of-type(9)  { view-timeline-name: --s9; }
		.step:nth-of-type(10) { view-timeline-name: --s10; }

		/* The window each shot is lit in, and its width is arithmetic.

		   A step is one viewport tall, so consecutive steps are exactly half a
		   timeline apart. A window of cover 20%→80% is 0.6 wide, which means
		   neighbouring windows overlap by 0.6 − 0.5 = 0.1 — and that overlap is
		   sized to be precisely the fade at each end (a sixth of 0.6). One shot
		   fades out over exactly the scroll where the next fades in.

		   Both neighbouring values were tried and measured across ~220 scroll
		   positions per page. Too wide (26–74 with a full-range animation) put
		   two frames at full opacity together and left two burnt-in captions
		   readable at once. Too narrow (a strict half, no overlap) removed the
		   ghosting and left the frame empty for a quarter of the scroll. */
		.show__shot { animation: lit linear both; animation-range: cover 20% cover 80%; }

		/* The first one is lit from the moment the section exists rather than
		   from 20% into its pass. Without this the frame sat empty at the top of
		   the page: it was there, correctly placed and correctly sized, with all
		   ten shots at zero opacity because no step had entered its range yet.
		   The result was a game page that looked finished under the buttons and
		   gave no sign the screenshots were below. */
		.show__shot:nth-of-type(1) {
			animation-name: lit-first;
			animation-range: cover 0% cover 80%;
		}

		@keyframes lit-first {
			0%, 66% { opacity: 1; transform: scale(1); }
			100%    { opacity: 0; transform: scale(0.985); }
		}
		.show__shot:nth-of-type(1)  { animation-timeline: --s1; }
		.show__shot:nth-of-type(2)  { animation-timeline: --s2; }
		.show__shot:nth-of-type(3)  { animation-timeline: --s3; }
		.show__shot:nth-of-type(4)  { animation-timeline: --s4; }
		.show__shot:nth-of-type(5)  { animation-timeline: --s5; }
		.show__shot:nth-of-type(6)  { animation-timeline: --s6; }
		.show__shot:nth-of-type(7)  { animation-timeline: --s7; }
		.show__shot:nth-of-type(8)  { animation-timeline: --s8; }
		.show__shot:nth-of-type(9)  { animation-timeline: --s9; }
		.show__shot:nth-of-type(10) { animation-timeline: --s10; }

		/* Opaque across the middle of the step's pass, faded at both ends.

		   The 26/74 split is arithmetic, not taste. A step is centred in the
		   viewport at exactly 50% of its own view timeline, and each step is
		   half a timeline behind the one after it — so step N sits at progress
		   p + 0.5 whenever step N+1 sits at p. For only one shot to be lit at a
		   time the hold has to close before the next one opens:

		       holdEnd ≤ holdStart + 0.5,  and holdEnd = 1 − holdStart

		   which resolves to holdStart ≥ 0.25. Widening the hold to 14–86% to
		   shorten the fades did the opposite of what it looked like it would do:
		   it put two frames at full opacity simultaneously, and with captions
		   burnt into this artwork that means two headlines readable at once.
		   26/74 is as much hold as the geometry allows. */
		@keyframes lit {
			0%       { opacity: 0; transform: scale(1.03); }
			17%, 83% { opacity: 1; transform: scale(1); }
			100%     { opacity: 0; transform: scale(0.985); }
		}

		.step__h, .step__p, .step__n { animation: rise linear both; animation-range: entry 12% entry 62%; }
		.step:nth-of-type(1)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s1; }
		.step:nth-of-type(2)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s2; }
		.step:nth-of-type(3)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s3; }
		.step:nth-of-type(4)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s4; }
		.step:nth-of-type(5)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s5; }
		.step:nth-of-type(6)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s6; }
		.step:nth-of-type(7)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s7; }
		.step:nth-of-type(8)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s8; }
		.step:nth-of-type(9)  :is(.step__h, .step__p, .step__n) { animation-timeline: --s9; }
		.step:nth-of-type(10) :is(.step__h, .step__p, .step__n) { animation-timeline: --s10; }

		@keyframes rise {
			from { opacity: 0; transform: translateY(16px); }
			to   { opacity: 1; transform: none; }
		}
	}
}

/* Without scroll-driven animation — Safari today, and anyone who asked for less
   motion — every shot is simply visible with its sentence beneath it. Nothing
   is hidden behind a capability the browser might not have. */
@supports not (animation-timeline: view()) {
	.show { grid-template-columns: minmax(0, 1fr); }
	.show__frame { display: none; }
	.step { min-height: 0; padding-block: clamp(26px, 5vh, 54px); }
	.step::before {
		content: "";
		display: block;
		width: min(100%, 320px);
		aspect-ratio: var(--shot-ratio, 562 / 1000);
		margin: 0 0 20px;
		border-radius: 20px;
		background: var(--shot) center / contain no-repeat;
	}
}

/* Reduced motion takes the same shape as a browser without scroll-driven
   animation: no sticky frame, each shot shown inline above its own sentence.

   Simply forcing the shots opaque was wrong and looked deliberate. All ten sit
   in one grid cell, so making them all visible stacks them — the reader gets
   whichever happens to be on top, permanently, while the writing scrolls past
   underneath talking about the other nine. */
@media (prefers-reduced-motion: reduce) {
	.show { grid-template-columns: minmax(0, 1fr); }
	.show__frame { display: none; }
	.step { min-height: 0; padding-block: clamp(26px, 5vh, 54px); }
	.step::before {
		content: "";
		display: block;
		width: min(100%, 320px);
		aspect-ratio: var(--shot-ratio, 562 / 1000);
		margin: 0 0 20px;
		border-radius: 20px;
		background: var(--shot) center / contain no-repeat;
	}
}

/* On a phone the sticky frame is dropped and this becomes the horizontal strip
   the game pages already used — the browser handles touch, momentum, snapping
   and the scrollbar, and every one of those would have to be rebuilt worse to
   gain nothing.

   What changed is what a slide holds. On the wide layout the sentence sits
   beside its shot; here it sits under it, inside the same slide, so swiping
   moves both together. A strip on top with a list below would leave the reader
   matching picture six to paragraph six by counting. */
@media (max-width: 860px) {
	.show { grid-template-columns: minmax(0, 1fr); }
	.show__frame { display: none; }

	.show__steps {
		display: flex;
		gap: 16px;
		overflow-x: auto;
		overscroll-behavior-x: contain;
		scroll-snap-type: x mandatory;
		margin-inline: calc(50% - 50vw);
		padding-inline: var(--ga-gutter);
		scroll-padding-inline: var(--ga-gutter);
	}

	.step {
		flex: 0 0 min(76vw, 320px);
		scroll-snap-align: center;
		min-height: 0;
		align-content: start;
		padding-block: 0;
	}

	.step::before {
		content: "";
		display: block;
		width: 100%;
		aspect-ratio: var(--shot-ratio, 562 / 1000);
		margin: 0 0 20px;
		border-radius: 18px;
		background: var(--shot) center / contain no-repeat;
		border: 1px solid color-mix(in srgb, var(--hue) 30%, var(--ga-border));
	}

	.step__p { max-width: none; }
}

/* ── Tap targets ─────────────────────────────────────────────────────────────
   Measured across 9 pages at 320, 390 and 430: the footer's seven links were
   23px tall, the wordmark 28, and the two contact addresses 16. Apple asks for
   44pt and Android for 48dp, and the footer is the worst case — seven links in
   a wrapped row, close enough together that a thumb lands between two of them.

   The height is added as padding rather than as a taller line, so the text
   still sits where it did. Only the reachable box grows.

   Deliberately excluded: links inside the running text of the agreements. An
   inline link in a paragraph cannot be padded without opening up the line
   spacing around it, and nobody taps a mailto in the middle of a sentence by
   accident — the convention exempts them, and so does this. */

@media (max-width: 700px) {
	/* The footer's own columns already give their links a 5px block padding and
	   a 2px row gap; on a phone that is not enough on its own. */
	.foot__col a,
	.foot__legal a { padding-block: 10px; }

	.head__mark { padding-block: 8px; }

	/* The two addresses in the contact routes. They are a list of destinations,
	   not prose, so they get the same treatment as any other standalone link. */
	.route dd a {
		display: inline-block;
		padding-block: 10px;
	}
	.routes { row-gap: 2px; }

	/* Label above, address below, both against the left edge.

	   Side by side with `space-between`, each row's behaviour depended on how
	   long its label happened to be: "Game support" pushed its address onto a
	   second line while the shorter "Business" kept its own on the first and
	   flung it to the right margin. Two rows of the same list, laid out two
	   different ways, because of a word length. Stacking removes the variable. */
	.route {
		display: grid;
		justify-content: start;
		gap: 2px;
	}
}

/* ── Game page ───────────────────────────────────────────────────────────────
   This whole block was left behind in the port. Without it `.game__icon` had
   no rule at all, so the icon rendered at the file's own 768px — 53% of a
   1440px window — and the two columns never formed, which is why the right
   half of the page was empty. The classes were in the markup the whole time;
   nothing was styling them. */

.game {
	position: relative;
	/* The bottom half of this was 80px of nothing on top of the showcase's own
	   90px — a 300px gap on a 900px screen with no sign that the page carried
	   on. Tightened at both ends so the first screenshot breaks the fold and
	   says so itself; a page that looks finished at the fold gets read as
	   finished. */
	padding-block: clamp(104px, 15vh, 170px) clamp(20px, 3vh, 40px);
	overflow: clip;
}

/* The game's own colour, poured in from the top left behind the title. */
.game::before {
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(64vw 56vh at 18% 0%, color-mix(in srgb, var(--hue) 34%, transparent), transparent 68%),
		linear-gradient(180deg, color-mix(in srgb, var(--hue) 10%, transparent), transparent 62%);
	z-index: -1;
}

.game__in {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	align-items: center;
	gap: clamp(24px, 5vw, 60px);
}

/* Sized deliberately rather than left at whatever the file happens to be. */
.game__icon {
	width: clamp(110px, 17vw, 230px);
	border-radius: 22%;
	border: 1px solid color-mix(in srgb, var(--hue) 40%, var(--ga-border));
}

.game__title { font-size: clamp(2.4rem, 6.4vw, 5.4rem); margin-bottom: 6px; }

/* Stacked on a phone, the icon leads and the name follows — the same order the
   bands and the listing use. */
@media (max-width: 720px) {
	.game__in { grid-template-columns: minmax(0, 1fr); }
}

/* What is left below the showcase: the team-only build notes, and nothing else
   for a visitor. */
.detail { padding-block: clamp(50px, 8vh, 100px); }
.detail h2 {
	font-size: var(--ga-label-size);
	letter-spacing: var(--ga-label-track);
	text-transform: uppercase;
	color: var(--ga-text-muted);
	margin-bottom: 10px;
}

/* Sentences that hold their own line.

   The studio copy wraps as prose, so a break can land anywhere — including
   immediately after a full stop, which leaves the next sentence's first word
   dangling at the end of the line before it. `text-wrap: balance` makes that
   more likely rather than less: evening the line lengths is precisely what
   moves a word across a sentence boundary.

   Each sentence sits in its own block, so a break between two sentences is the
   only break the layout can make there — at any width, without a hard <br>
   that would be right at one size and wrong at every other. */
.studio__lead > span,
.studio__grid p > span { display: block; }

/* The rail on a phone.

   The desktop cap is 34vw, which is ~490px on a laptop and a sensible third of
   the screen. On a 390px phone the same rule gives 133px — the Your Balls shots
   came out 133×75, less than half the size of the same artwork on the game's
   own page. A proportion that reads as restraint on a wide screen reads as a
   thumbnail on a narrow one.

   76vw here puts a landscape shot at 296px, which is exactly what the detail
   page shows. Portrait shots stay bounded by the height instead. */
@media (max-width: 720px) {
	.rail img {
		max-width: 76vw;
		/* A floor under the height, because a percentage of a short screen is a
		   short number: 46vh on a 568px phone is 261px, which put the portrait
		   shots at 148px wide — 46% of that screen against 56% of a taller one.
		   The smaller the phone, the smaller the artwork got, which is backwards. */
		max-height: max(46vh, 340px);
	}
}
