/* ==========================================================================
   Structured Cloud Studios, stylesheet
   ==========================================================================

   Dark only, on purpose. The brand is a vermilion seal on a near-black void
   (see splash.html, which is where the palette comes from), and a games site is
   read like a poster rather than a document. There is no light mode to switch
   to, so there are no theme tokens to maintain and nothing to flash on load.

   Structure:
     0  Fonts
     1  Tokens
     2  Reset and base
     3  Layout primitives: .wrap, .sec, .eyebrow
     4  Controls: buttons, pills, tags
     5  Header, nav, footer
     6  Hero and game hero
     7  Modules: cards, features, gallery, video, timeline, factsheet…
     8  Forms
     9  Intro overlay
    10  Motion, reveals, reduced motion
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Fonts: self-hosted, latin variable subsets, preloaded in layout.php.
   Relative URLs, not /fonts/…: the site has to resolve identically at a vhost
   root and inside a folder under htdocs, and a stylesheet's relative URLs
   resolve against the stylesheet.
   -------------------------------------------------------------------------- */
@font-face {
	font-family: 'Archivo';
	font-style: normal;
	font-weight: 600 800;
	font-display: swap;
	src: url(../fonts/archivo-latin-var.woff2) format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: 'Space Grotesk';
	font-style: normal;
	font-weight: 300 600;
	font-display: swap;
	src: url(../fonts/space-grotesk-latin-var.woff2) format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: 'JetBrains Mono';
	font-style: normal;
	font-weight: 400 500;
	font-display: swap;
	src: url(../fonts/jetbrains-mono-ui-var.woff2) format('woff2');
	/* Exactly what this cut of the file holds, printable ASCII and the
	   punctuation the copy uses. Anything outside it falls back rather than
	   pulling a file that cannot render it. */
	unicode-range: U+0020-007E, U+00A0, U+00A9, U+00AE, U+00B0, U+00B7, U+00D7, U+2013-2014, U+2018-2019, U+201C-201D, U+2022, U+2026, U+2122;
}

/* Chakra Petch, the Heatsink typeface. Declared here beside the others rather
   than in the game's own stylesheet so that every @font-face on the site points
   at ../fonts/ from the same directory and the whole set keeps working at a
   vhost root and under htdocs alike. It costs the studio site nothing: a face
   no rule selects is a face no browser fetches. */
@font-face {
	font-family: 'Chakra Petch';
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url(../fonts/chakra-petch-latin-regular.woff2) format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: 'Chakra Petch';
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url(../fonts/chakra-petch-latin-bold.woff2) format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
	color-scheme: dark;

	/* Brand, straight off the splash card. --seal never changes; --accent is
	   what everything else is drawn with, and a game subsite overrides that one
	   token to take on its own colour. */
	--void: #06070B;
	--seal: #e4573f;
	--paper: #DCE6F0;
	--accent: var(--seal);

	/* Surfaces, lifted off the void rather than tinted grey. A flat grey on a
	   near-black background reads as dirt. */
	--bg: var(--void);
	--surface: #0B0E15;
	--surface-2: #10141D;
	--line: rgb(220 230 240 / 10%);
	--line-strong: rgb(220 230 240 / 20%);

	--text: var(--paper);
	--muted: rgb(220 230 240 / 64%);
	--dim: rgb(220 230 240 / 42%);

	--display: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
	--body: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
	--mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

	/* Fluid scale. Locked at the ends so a 4K display does not get 90px body
	   copy and a 320px phone does not get a two-word headline per line. */
	--t-hero: clamp(2.6rem, 1.4rem + 5.2vw, 5.6rem);
	--t-h2: clamp(1.9rem, 1.3rem + 2.4vw, 3.1rem);
	--t-h3: clamp(1.15rem, 1rem + 0.6vw, 1.45rem);
	--t-lead: clamp(1.02rem, 0.95rem + 0.4vw, 1.22rem);

	/* One measure for the whole site. A wider container for grids was tried and
	   dropped: it put section headings on a different left edge from the hero
	   above them, and a heading that does not line up with the headline reads
	   as a mistake however much room it buys the cards. */
	--wrap: 1200px;
	--gutter: clamp(1.15rem, 4vw, 3rem);
	--sec-y: clamp(4rem, 9vw, 8rem);

	--r: 4px;              /* corners stay tight; this is a studio, not an app */
	--cut: 14px;           /* the clipped corner used on cards and buttons */
	--ease: cubic-bezier(0.22, 0.61, 0.36, 1);

	/* Film grain, drawn once and shared by every backdrop on every host. It is
	   atmosphere, but it is also the fix for a real problem: gradients as wide
	   as the ones below, over a ground this close to black, band visibly on an
	   8-bit panel, and noise is what breaks the bands up. Sparse light specks
	   rather than a grey haze: the colour matrix flattens the turbulence to
	   white and drives alpha off its luminance with a negative bias, so about
	   half the pixels clamp to nothing and the void stays void. */
	--grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .05 .05 .05 0 -.066'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
	-webkit-text-size-adjust: 100%;
	scroll-behavior: smooth;
	/* The header is sticky, so an anchor jump would otherwise land its target
	   underneath it. */
	scroll-padding-top: 6rem;
}

body {
	margin: 0;
	background: var(--bg);
	color: var(--text);
	font-family: var(--body);
	font-size: 1rem;
	font-weight: 350;
	line-height: 1.65;
	-webkit-font-smoothing: antialiased;
	overflow-x: hidden;
}

/* The backdrop -------------------------------------------------------------
   Two fixed layers under the whole site, and neither of them is a grid. A
   square lattice is what every dark site has been wearing for two years, it
   says nothing about this studio in particular, and on a page whose components
   are already rectangles it arrives as a fourth border on each of them.

   What replaces it is in the mark: a flight of stairs, climbing to the right.
   The tile is 360x240 and holds one flight of five, each tread 72px long and
   each riser 48px tall, so the tile rises exactly its own height across exactly
   its own width. That is the whole trick, and it is why the flight runs
   unbroken off every edge of the viewport: the tile that continues a step is
   the one up and to the right, which the repeat puts there. The riser that
   lands on the seam is drawn as the two halves the seam cuts it into. The tick
   that precedes an eyebrow is one tread off the same flight.

     ::before   the air. An ember low and left, a cold haze high and right, and
                grain over both. Asymmetric deliberately: the hero already
                carries a bloom at top centre (.hero-glow), and a second one
                behind it would only make the first look like an accident.
     ::after    the flight itself, masked out of the middle. The structure
                lives in the margins and the reading column stays empty.

   Both are fixed rather than scrolling, so they read as a surface the content
   moves across rather than as part of the page, and the flight climbs its own
   line while the content crosses it. That is the parallax further down.
   -------------------------------------------------------------------------- */
body::before {
	content: '';
	position: fixed;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background:
		var(--grain) 0 0 / 180px 180px,
		radial-gradient(46rem 32rem at 14% 94%, rgb(228 87 63 / 11%), transparent 68%),
		radial-gradient(54rem 38rem at 90% 4%, rgb(150 180 216 / 5%), transparent 70%);
}

body::after {
	content: '';
	position: fixed;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='240'%3E%3Cg fill='%23DCE6F0' fill-opacity='.065'%3E%3Crect x='0' y='216' width='72' height='1'/%3E%3Crect x='72' y='168' width='72' height='1'/%3E%3Crect x='144' y='120' width='72' height='1'/%3E%3Crect x='216' y='72' width='72' height='1'/%3E%3Crect x='288' y='24' width='72' height='1'/%3E%3Crect x='72' y='168' width='1' height='48'/%3E%3Crect x='144' y='120' width='1' height='48'/%3E%3Crect x='216' y='72' width='1' height='48'/%3E%3Crect x='288' y='24' width='1' height='48'/%3E%3Crect x='0' y='216' width='1' height='24'/%3E%3Crect x='0' y='0' width='1' height='24'/%3E%3C/g%3E%3C/svg%3E") 0 0 / 360px 240px;
	mask-image: radial-gradient(74% 58% at 50% 40%, rgb(0 0 0 / 16%) 0%, #000 82%);
}

/* On a phone there are no margins for the flight to live in: the reading
   column is the viewport. Halved rather than switched off, because a phone
   that gets a different design is a phone that gets a worse one. */
@media (max-width: 720px) {
	body:not(.game-site)::after { opacity: .55; }
}

/* The parallax.

   Fixed is one plane, and one plane holds no depth: the content moves and the
   backdrop does not. This lifts the flight off that plane and leaves the air on
   it, so the page carries three rates instead of two, the content, the
   structure behind it, and the air behind that. The air is the far one and it
   holds still, because something has to. A backdrop where every layer drifts is
   not depth, it is only motion.

   The flight travels along its own repeat vector. The tile that continues a
   step is the one up and to the right, so a shift of exactly 360 across and 240
   up lands the pattern back on itself: two of those over the length of the
   page, and the whole flight runs up its own line and arrives where it started,
   with no seam anywhere in the travel. It is the only direction available. Any
   other slides a staircase sideways past the reader; this one climbs it.

   Up, because that is the whole of what parallax is: the content leaves the top
   of the screen and the thing behind it follows, slower.

   It moves the background and not the layer. The mask is the reason the reading
   column is empty, and a transform would carry the mask along with it and drag
   that hollow up the page behind the stairs.

   Scroll-driven, so it costs nothing while nothing is scrolling, and where the
   browser cannot drive an animation off scroll the @supports leaves the still
   backdrop that was here before. The prefers-reduced-motion guard is its own
   because the global reset at the foot of this file cannot stand in for it:
   that reset collapses animation-duration, and an animation on a scroll
   timeline reads a collapsed duration as being over already, so it would settle
   on the end of the travel rather than hold still. */
@keyframes scs-flight-climb {
	from { background-position: 0 0; }
	to   { background-position: 720px -480px; }
}

@supports (animation-timeline: scroll()) {
	@media (prefers-reduced-motion: no-preference) {
		body:not(.game-site)::after {
			animation: scs-flight-climb linear both;
			animation-timeline: scroll(root block);
		}
	}
}

/* A game subsite that has not brought a backdrop of its own does not inherit
   the stairs. The flight is the studio's mark, and a game wearing it reads as
   the studio's page with the game's name on it. What it gets instead is the
   air without the structure, in its own accent, which is one token and works
   for a game whose colour nobody has picked yet. Its key art carries the rest.
   A game that wants more replaces both layers in games/<slug>/style.css, the
   way Heatsink does. */
.game-site::before {
	background:
		var(--grain) 0 0 / 180px 180px,
		radial-gradient(52rem 34rem at 16% 102%,
			color-mix(in oklab, var(--accent) 14%, transparent), transparent 70%),
		radial-gradient(56rem 40rem at 90% -2%, rgb(150 180 216 / 5%), transparent 72%);
}
.game-site::after { content: none; }

/* Both backdrop layers sit at z-index 0, so everything the page prints needs a
   stacking context of its own to land above them. */
body > * { position: relative; z-index: 1; }

img, svg, video { max-width: 100%; display: block; }
img { height: auto; }

h1, h2, h3, h4 {
	font-family: var(--display);
	font-weight: 800;
	line-height: 1.06;
	letter-spacing: -0.025em;
	margin: 0;
	text-wrap: balance;
}

p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }

a { color: inherit; text-decoration-color: rgb(220 230 240 / 35%); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--accent); }

:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 3px;
	border-radius: 2px;
}

::selection { background: var(--accent); color: #fff; }

.muted { color: var(--muted); }

.skip-link {
	position: absolute;
	left: -9999px;
	top: 0;
	z-index: 200;
	background: var(--accent);
	color: #fff;
	padding: .7rem 1.1rem;
	font: 500 .85rem/1 var(--mono);
}
.skip-link:focus { left: 0; }

/* Read aloud, never drawn. For the half of a label that only a screen reader
   needs: the product name inside a heading whose wordmark is an image, the word
   "redacted" beside a line that is only struck through visually. Not
   display:none, which would take it out of the accessibility tree too. */
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */
.wrap {
	width: min(100% - var(--gutter) * 2, var(--wrap));
	margin-inline: auto;
}
.sec { padding-block: var(--sec-y); }

/* A section that opens with its own heading earns the full band above it. One
   that drops straight into content does not: it is the hero still talking, and
   the hero's bottom padding has already made the break. Without this the
   contact form, the studio's opening prose and the news list all began a
   screenful below the lead that introduced them. */
.hero + .sec--nohead { padding-top: calc(var(--sec-y) * .3); }

/* Two sections that are one chapter: the shelf under the featured game, a
   grid under the block that introduced it. A full band between them reads
   as a subject change that has not happened. */
.sec--tight { padding-top: calc(var(--sec-y) * .4); }

/* A panel is the same section on a lifted surface, edge to edge, with hairlines
   top and bottom. It breaks a long page into chapters without a colour change. */
.sec--panel {
	background: linear-gradient(180deg, var(--surface) 0%, rgb(11 14 21 / 0%) 100%), var(--bg);
	border-block: 1px solid var(--line);
}
.sec--center { text-align: center; }
.sec--center .sec-head { margin-inline: auto; }

.sec-head { max-width: 46rem; margin-bottom: clamp(2rem, 4vw, 3.4rem); }
.sec-title { font-size: var(--t-h2); }
.sec-lead {
	margin-top: 1rem;
	font-size: var(--t-lead);
	color: var(--muted);
	max-width: 42rem;
	text-wrap: pretty;
}
.sec-actions { margin-top: clamp(2rem, 4vw, 3rem); }

.eyebrow {
	font: 500 clamp(.66rem, .6rem + .12vw, .74rem)/1.4 var(--mono);
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--accent);
	margin: 0 0 1rem;
	display: flex;
	align-items: center;
	gap: .7rem;
	flex-wrap: wrap;
}
/* The tick before an eyebrow: a single tread from the mark, at label size. */
.eyebrow::before {
	content: '';
	width: 1.6rem;
	height: 2px;
	background: var(--accent);
	flex: none;
}
.sec--center .eyebrow { justify-content: center; }

/* The notched corner ------------------------------------------------------
   The one shape the whole site shares, cards, buttons, panels. It is what
   makes a rectangle read as a component rather than a box.

   clip-path removes the corner after the border is painted, so the 45 degree
   edge is the one side a border cannot draw and the corner reads as open. The
   ::before paints it back: a --cut square in the bottom-right corner, filled
   from 1px inside the diagonal outwards, which the same clip-path then trims
   along the diagonal. What survives is a 1px line sitting exactly on the notch.
   inset: -1px takes that layer out to the border box, which is what clip-path
   measures against. --notch-line is registered as a colour so it can fade in
   step with the border it continues.

   New notched components join the two selector lists below. */
@property --notch-line {
	syntax: '<color>';
	inherits: true;
	initial-value: transparent;
}

.btn,
.nav .nav-cta,
.game-card {
	position: relative;
	clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%);
}
.btn::before,
.nav .nav-cta::before,
.game-card::before {
	content: '';
	position: absolute;
	inset: -1px;
	pointer-events: none;
	background: linear-gradient(to bottom right,
			transparent 0 calc(50% - 1px), var(--notch-line) calc(50% - 1px) 100%)
		100% 100% / var(--cut) var(--cut) no-repeat;
}

/* --------------------------------------------------------------------------
   4. Controls
   -------------------------------------------------------------------------- */
.btn-row { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: 2rem; }

.btn {
	--btn-bg: transparent;
	--notch-line: var(--line-strong);
	display: inline-flex;
	align-items: center;
	gap: .6rem;
	padding: .85rem 1.5rem;
	font: 500 .84rem/1 var(--mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	text-decoration: none;
	white-space: nowrap;
	background: var(--btn-bg);
	border: 1px solid var(--notch-line);
	color: var(--text);
	cursor: pointer;
	transition: background .25s var(--ease), border-color .25s var(--ease),
	            --notch-line .25s var(--ease), color .25s var(--ease),
	            transform .25s var(--ease);
}
.btn:hover { transform: translateY(-2px); --notch-line: var(--accent); }
.btn:active { transform: translateY(0); }

.btn-primary {
	--btn-bg: var(--accent);
	--notch-line: var(--accent);
	color: #fff;
}
.btn-primary:hover { --btn-bg: color-mix(in oklab, var(--accent) 84%, white); }

.btn-ghost:hover { --btn-bg: rgb(220 230 240 / 6%); }

/* A destination that is planned but does not exist yet: shown, clearly inert,
   never a link to nowhere. */
.btn.is-pending {
	color: var(--dim);
	border-style: dashed;
	cursor: not-allowed;
}
/* Dash the notch edge to match the dashed border: the mask runs along the
   diagonal, so its bands cut the line into segments the length of the dashes
   the browser draws on the other three sides. */
.btn.is-pending::before {
	mask-image: repeating-linear-gradient(to top right, #000 0 3px, transparent 3px 6px);
}
.btn.is-pending:hover { transform: none; --notch-line: var(--line-strong); }

.pill {
	display: inline-flex;
	align-items: center;
	gap: .45rem;
	padding: .38rem .7rem;
	font: 500 .68rem/1 var(--mono);
	letter-spacing: .12em;
	text-transform: uppercase;
	background: rgb(6 7 11 / 78%);
	border: 1px solid var(--line-strong);
	color: var(--text);
	backdrop-filter: blur(6px);
}
.pill::before {
	content: '';
	width: .45rem;
	height: .45rem;
	border-radius: 50%;
	background: currentColor;
}
.pill.is-released { color: #6ee7a8; }
.pill.is-early { color: #f5c451; }
.pill.is-dev { color: var(--accent); }
.pill.is-announced { color: #7cc4ff; }
.pill.is-proto,
.pill.is-shelved { color: var(--dim); }

.tag {
	font: 400 .7rem/1 var(--mono);
	letter-spacing: .08em;
	text-transform: uppercase;
	color: var(--muted);
	border: 1px solid var(--line);
	padding: .3rem .55rem;
}
.tag--kind { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 40%, transparent); }

/* --------------------------------------------------------------------------
   5. Header, nav, footer
   -------------------------------------------------------------------------- */
.site-header {
	position: sticky;
	top: 0;
	z-index: 60;
	border-bottom: 1px solid transparent;
	transition: background .3s var(--ease), border-color .3s var(--ease);
}
.site-header.scrolled {
	background: rgb(6 7 11 / 82%);
	backdrop-filter: blur(14px) saturate(140%);
	border-bottom-color: var(--line);
}
.header-inner {
	display: flex;
	align-items: center;
	gap: 1.5rem;
	min-height: 4.6rem;
}

.brand {
	display: flex;
	align-items: center;
	gap: .8rem;
	text-decoration: none;
	margin-right: auto;
}
.brand-mark { width: 2.1rem; height: auto; flex: none; }
.brand-text { display: grid; line-height: 1.1; }
.brand-text b {
	font: 800 .95rem/1.1 var(--display);
	letter-spacing: -.01em;
}
.brand-text small {
	font: 500 .62rem/1.4 var(--mono);
	letter-spacing: .22em;
	text-transform: uppercase;
	color: var(--accent);
}

.nav { display: flex; align-items: center; gap: 1.7rem; }
.nav a {
	font: 500 .82rem/1 var(--mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	text-decoration: none;
	color: var(--muted);
	padding-block: .4rem;
	transition: color .2s var(--ease);
	position: relative;
}
.nav a:hover, .nav a.is-active { color: var(--text); }
.nav a.is-active::after {
	content: '';
	position: absolute;
	left: 0;
	right: 0;
	bottom: -.15rem;
	height: 2px;
	background: var(--accent);
}
.nav .nav-cta {
	--cut: 8px;
	--notch-line: var(--line-strong);
	color: var(--text);
	border: 1px solid var(--notch-line);
	padding: .6rem 1rem;
}
.nav .nav-cta:hover { --notch-line: var(--accent); background: rgb(220 230 240 / 5%); }

/* On a game subdomain: whose game this is, and the way back up. */
.header-studio {
	display: grid;
	text-align: right;
	text-decoration: none;
	line-height: 1.25;
	padding-left: 1.5rem;
	border-left: 1px solid var(--line);
}
.header-studio span { font: 400 .6rem/1 var(--mono); letter-spacing: .16em; text-transform: uppercase; color: var(--dim); }
.header-studio b { font: 600 .78rem/1.4 var(--body); color: var(--muted); }
.header-studio:hover b { color: var(--text); }

.burger {
	display: none;
	width: 2.75rem;
	height: 2.75rem;
	background: none;
	border: 1px solid var(--line-strong);
	cursor: pointer;
	padding: 0;
	place-content: center;
	gap: 5px;
}
.burger span {
	display: block;
	width: 18px;
	height: 1.5px;
	background: var(--text);
	margin-inline: auto;
	transition: transform .3s var(--ease), opacity .2s var(--ease);
}
.burger[aria-expanded='true'] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.burger[aria-expanded='true'] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded='true'] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.nav-panel {
	position: sticky;
	top: 4.6rem;
	z-index: 55;
	background: rgb(6 7 11 / 96%);
	backdrop-filter: blur(14px);
	border-bottom: 1px solid var(--line);
}
.nav-panel .wrap { display: grid; padding-block: 1rem; }
.nav-panel a {
	padding: .85rem 0;
	font: 500 .95rem/1 var(--mono);
	letter-spacing: .08em;
	text-transform: uppercase;
	text-decoration: none;
	border-bottom: 1px solid var(--line);
}
.nav-panel a:last-child { border-bottom: 0; }
.nav-panel .np-studio { color: var(--accent); }

@media (max-width: 900px) {
	.nav, .header-studio { display: none; }
	.burger { display: grid; }
}

.site-footer {
	border-top: 1px solid var(--line);
	background: var(--surface);
	padding-block: clamp(3rem, 6vw, 5rem) 2rem;
	margin-top: var(--sec-y);
}
/* The three link columns are auto tracks, sized to their own content, so the
   longest item in a column sets that column's width. Equal 1fr tracks gave
   every column a third of the space regardless of what was in it, which wrapped
   hello@structuredcloudstudios.com onto a second line while the Games and
   Studio columns sat half empty. The brand column keeps the fr and absorbs
   whatever is left over. */
.footer-grid {
	display: grid;
	grid-template-columns: 1.6fr repeat(3, auto);
	gap: clamp(2rem, 4vw, 3.5rem);
}
.footer-brand img { width: clamp(150px, 16vw, 210px); height: auto; opacity: .92; }
.footer-brand p { margin-top: 1.2rem; color: var(--muted); font-size: .92rem; max-width: 30ch; }
/* min-width:0 and a break rule, because an email address is one unbreakable
   token as far as min-content sizing is concerned, and no track is narrower
   than its min-content. Without these, hello@structuredcloudstudios.com sets
   the width of a footer column, that column widens the grid past the container,
   and the last link in the footer sits off the right edge of a 320px screen.
   With them the email breaks only once the columns genuinely cannot fit, which
   on the desktop grid above is never. */
.footer-col { display: grid; align-content: start; gap: .55rem; min-width: 0; }
.footer-col h2 {
	font: 500 .68rem/1 var(--mono);
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--dim);
	margin-bottom: .5rem;
}
.footer-col a {
	font-size: .92rem;
	color: var(--muted);
	text-decoration: none;
	width: fit-content;
	max-width: 100%;
	overflow-wrap: anywhere;
}
.footer-col a:hover { color: var(--text); }

.footer-base {
	margin-top: clamp(2.5rem, 5vw, 4rem);
	padding-top: 1.5rem;
	border-top: 1px solid var(--line);
	display: flex;
	flex-wrap: wrap;
	gap: .6rem 1.75rem;
	font: 400 .76rem/1.6 var(--mono);
	color: var(--dim);
}
.footer-legal { display: flex; flex-wrap: wrap; gap: .6rem 1.75rem; }
.footer-base a { color: var(--dim); text-decoration: none; }
.footer-base a:hover { color: var(--text); }
.footer-motto { margin-left: auto; color: var(--accent); }

@media (max-width: 820px) {
	.footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
	.footer-brand { grid-column: 1 / -1; }
	.footer-motto { margin-left: 0; }
}

/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */
.hero { position: relative; padding-block: clamp(4rem, 11vw, 9rem) clamp(3rem, 7vw, 6rem); }
.hero--home { padding-block: clamp(5rem, 14vw, 11rem) clamp(3rem, 7vw, 6rem); }

/* The accent bloom behind the headline. Sized in vmax so it stays a glow on a
   phone rather than becoming a coloured background. */
.hero-glow {
	position: absolute;
	inset: 0;
	overflow: hidden;
	pointer-events: none;
	mask-image: linear-gradient(180deg, #000 55%, transparent 100%);
}
.hero-glow::after {
	content: '';
	position: absolute;
	top: -30vmax;
	left: 50%;
	width: 78vmax;
	height: 60vmax;
	transform: translateX(-50%);
	background: radial-gradient(closest-side,
		color-mix(in oklab, var(--accent) 26%, transparent) 0%,
		transparent 100%);
	opacity: .55;
}

.hero-inner { display: grid; gap: clamp(2.5rem, 5vw, 4rem); }
/* The measure is held on the copy, not on the wrap around it. Capping the
   wrap centred the whole column and left the headline starting further in
   than every section heading below it. */
.hero--home .hero-copy { max-width: 54rem; }
.hero-title { font-size: var(--t-hero); }
.hero-title em,
.hero-lead em {
	font-style: normal;
	color: var(--accent);
}
.hero-lead {
	margin-top: clamp(1.2rem, 2vw, 1.8rem);
	font-size: var(--t-lead);
	color: var(--muted);
	max-width: 46ch;
	text-wrap: pretty;
}

.hero-meta {
	list-style: none;
	margin: 2.5rem 0 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 1.5rem 2.5rem;
}
.hero-meta li { display: grid; gap: .3rem; }
.hero-meta span {
	font: 400 .64rem/1 var(--mono);
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--dim);
}
.hero-meta b { font: 600 .95rem/1.3 var(--body); }

.hero--post .hero-title { font-size: clamp(2rem, 1.3rem + 3vw, 3.4rem); }
.post-meta a { text-decoration: none; }
.post-meta time { color: var(--muted); }

/* Game hero: key art behind, copy over it. */
.ghero { position: relative; padding-block: clamp(6rem, 18vw, 13rem) clamp(3rem, 6vw, 5rem); }
.ghero-art { position: absolute; inset: 0; overflow: hidden; }
.ghero-art img, .ghero-art .art-ph {
	width: 100%;
	height: 100%;
	object-fit: cover;
	opacity: .5;
}
/* Two stops, not one: the art has to fade into the void at the bottom AND lose
   enough contrast at the top for white type to sit on it at any crop. */
.ghero-fade {
	position: absolute;
	inset: 0;
	background:
		linear-gradient(180deg, rgb(6 7 11 / 72%) 0%, rgb(6 7 11 / 30%) 45%, var(--void) 100%),
		linear-gradient(90deg, var(--void) 0%, rgb(6 7 11 / 20%) 70%);
}
/* Positioned, and explicitly above the art. .ghero-art is absolute and comes
   first in the markup; without this the copy is static, so the art, and the
   black gradient over it, paints on top of the title rather than behind it. */
.ghero-inner { position: relative; z-index: 1; max-width: 44rem; }
.ghero-status { color: var(--muted); }
.ghero-title { font-size: var(--t-hero); margin-block: .4rem .6rem; }
.ghero-title--logo img { width: min(100%, 26rem); height: auto; }
.ghero-tagline {
	font: 600 clamp(1.1rem, .95rem + .8vw, 1.6rem)/1.35 var(--display);
	color: var(--accent);
	margin: 0 0 1rem;
}
.ghero-summary { font-size: var(--t-lead); color: var(--muted); max-width: 46ch; }

/* --------------------------------------------------------------------------
   7. Modules
   -------------------------------------------------------------------------- */

/* Game cards ------------------------------------------------------------- */
.game-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr));
	gap: clamp(1.2rem, 2.5vw, 2rem);
}
.game-grid--lead .game-card--featured { grid-column: 1 / -1; }

.game-card {
	--notch-line: var(--line);
	background: var(--surface);
	border: 1px solid var(--notch-line);
	transition: border-color .3s var(--ease), --notch-line .3s var(--ease),
	            transform .3s var(--ease), background .3s var(--ease);
}
.game-card:hover {
	--notch-line: color-mix(in oklab, var(--accent) 55%, transparent);
	transform: translateY(-4px);
}
.game-card-link { display: grid; text-decoration: none; height: 100%; }

.game-card-art { position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--surface-2); }
.game-card-art img, .game-card-art .art-ph {
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform .6s var(--ease);
}
.game-card:hover .game-card-art img,
.game-card:hover .game-card-art .art-ph { transform: scale(1.04); }
.game-card-art .pill { position: absolute; top: .9rem; left: .9rem; }

.game-card-body { padding: clamp(1.2rem, 2.5vw, 1.8rem); display: grid; align-content: start; gap: .5rem; }
.game-card-body h3 { font-size: var(--t-h3); }
.game-card-tag { color: var(--accent); font-weight: 500; margin: 0; }
.game-card-summary { color: var(--muted); margin: .4rem 0 0; max-width: 52ch; }
.game-card-meta { font: 400 .74rem/1.5 var(--mono); color: var(--dim); margin: .3rem 0 0; }
.game-card-cta {
	margin-top: .9rem;
	font: 500 .74rem/1 var(--mono);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--accent);
}

/* The featured card runs art beside copy instead of above it. */
@media (min-width: 900px) {
	.game-card--featured .game-card-link { grid-template-columns: 1.25fr 1fr; }
	.game-card--featured .game-card-art { aspect-ratio: auto; height: 100%; min-height: 24rem; }
	.game-card--featured .game-card-body { align-content: center; padding: clamp(2rem, 4vw, 3.5rem); gap: .7rem; }
	.game-card--featured h3 { font-size: clamp(1.8rem, 1.2rem + 2vw, 2.8rem); }
}

/* Drawn stand-in for art that is not on disk yet. */
.art-ph { width: 100%; height: 100%; display: block; }

/* Features --------------------------------------------------------------- */
.feature-grid {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 1px;
	background: var(--line);
	border: 1px solid var(--line);
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.feature-grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); }
.feature {
	background: var(--bg);
	padding: clamp(1.5rem, 3vw, 2.4rem);
	display: grid;
	align-content: start;
	gap: .6rem;
}
.feature-num {
	font: 500 .72rem/1 var(--mono);
	letter-spacing: .12em;
	color: var(--accent);
}
.feature h3 { font-size: var(--t-h3); }
.feature p { color: var(--muted); margin: 0; }

/* Stats ------------------------------------------------------------------ */
.stat-band {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 1.6rem;
}
.stat-band li { border-left: 2px solid var(--accent); padding-left: 1.1rem; }
.stat-band b {
	display: block;
	font: 800 clamp(2rem, 1.4rem + 2.4vw, 3.2rem)/1 var(--display);
	letter-spacing: -.03em;
	font-variant-numeric: tabular-nums;
}
.stat-band span { font: 400 .78rem/1.5 var(--mono); color: var(--muted); }

/* Split ------------------------------------------------------------------ */
.split { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
.split-title { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.2rem); margin-bottom: 1.2rem; }
/* The media column takes whatever a page hands it, a stat band as often as a
   picture, so art is framed here rather than by the caller: the same square
   frame the cards use, and a fixed 16/9 box so a portrait file cannot set the
   height of the row beside it. */
.split-media > img, .split-media > .art-ph {
	width: 100%;
	aspect-ratio: 16 / 9;
	object-fit: cover;
	background: var(--surface-2);
	border: 1px solid var(--line);
}
@media (min-width: 860px) {
	.split { grid-template-columns: 1.1fr .9fr; }
	/* Visual order only. The markup keeps the copy first, so it is still read
	   first when the columns stack and by anyone not seeing the layout. */
	.split--flip .split-copy { order: 2; }
}

/* Prose ------------------------------------------------------------------ */
.prose { max-width: 62ch; font-size: 1.03rem; color: var(--muted); }
.prose--wide { max-width: 80ch; }
.prose > :first-child { margin-top: 0; }
.prose h2 { font-size: clamp(1.4rem, 1.1rem + 1.2vw, 2rem); margin: 2.4em 0 .7em; color: var(--text); }
.prose h3 { font-size: var(--t-h3); margin: 2em 0 .6em; color: var(--text); }
.prose strong { color: var(--text); font-weight: 600; }
.prose a { color: var(--text); text-decoration-color: var(--accent); }
.prose ul, .prose ol { margin: 0 0 1.2em; padding-left: 1.3em; }
.prose li { margin-bottom: .45em; }
.prose code {
	font: 400 .88em/1 var(--mono);
	background: var(--surface-2);
	border: 1px solid var(--line);
	padding: .15em .4em;
	color: var(--text);
}
.prose blockquote {
	margin: 2em 0;
	padding-left: 1.4rem;
	border-left: 2px solid var(--accent);
	font-size: 1.12rem;
	color: var(--text);
}
.prose img { margin: 2em 0; border: 1px solid var(--line); }

/* Divider ---------------------------------------------------------------- */
/* A full-bleed band between sections, tiled with the mark at low opacity. The
   tile is sized off the band's own height so the rhythm holds at every width;
   mark.svg has a viewBox and no intrinsic size, so sizing the tile wider than
   the mark's ratio letterboxes it inside that tile and the leftover becomes the
   gap between stamps. The mark is drawn on a pseudo-element rather than on the
   band itself because a background-image cannot be faded on its own, and the
   band under it has to stay solid. */
.divider {
	--divider-h: clamp(3.5rem, 6vw, 4.75rem);
	position: relative;
	height: var(--divider-h);
	background: var(--surface);
	border-block: 1px solid var(--line);
	overflow: hidden;
}
.divider::before {
	content: '';
	position: absolute;
	inset: 0;
	background: url(../img/mark.svg) center / calc(var(--divider-h) * .8) calc(var(--divider-h) * .8) repeat-x;
	opacity: .6;
	/* The tiles are cut off at both ends of a full-bleed band; fading them out
	   reads as an edge rather than as a crop. */
	mask-image: linear-gradient(90deg, transparent, #000 45%, #000 55%, transparent);
}

/* Press blurbs: the same copy at two or three lengths, each one a block a
   journalist can select and lift whole. Separated by a rule rather than by a
   heading, so the label stays a label and does not read as a section of its
   own in the document outline. */
.blurb { max-width: 62ch; }
.blurb + .blurb {
	margin-top: clamp(1.6rem, 3vw, 2.4rem);
	padding-top: clamp(1.6rem, 3vw, 2.4rem);
	border-top: 1px solid var(--line);
}
.blurb > p:not(.eyebrow) { color: var(--muted); margin: 0; }

/* Gallery and lightbox --------------------------------------------------- */
.gallery {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
	gap: clamp(.8rem, 1.6vw, 1.2rem);
}
.shot { margin: 0; }
.shot-btn {
	display: block;
	width: 100%;
	padding: 0;
	border: 1px solid var(--line);
	background: var(--surface-2);
	cursor: zoom-in;
	overflow: hidden;
	aspect-ratio: 16 / 9;
	transition: border-color .3s var(--ease);
}
/* A portrait game is photographed in portrait. Cropping 1080x1920 captures into
   a 16:9 frame throws away the half of the screen the HUD lives in, and a strip
   of landscape frames also quietly promises a platform a phone game may not
   have. Narrower columns, because a 20rem-wide portrait tile is most of a
   laptop screen tall. */
/* Counted, not auto-fitted. A phone game's strip is six frames, one per stage,
   and the whole point of it is that it reads as one series: auto-fit lands on
   five columns at a laptop width and leaves the sixth stranded on its own row,
   which is exactly the shape that stops it reading as a series. 2, 3 and 6 all
   divide six, so the strip stays whole at every width. */
.gallery--portrait { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 560px)  { .gallery--portrait { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .gallery--portrait { grid-template-columns: repeat(6, 1fr); } }
.gallery--portrait .shot-btn { aspect-ratio: 9 / 16; }

.shot-btn:hover { border-color: var(--accent); }
.shot-btn[disabled] { cursor: default; }
.shot-btn img, .shot-btn .art-ph { width: 100%; height: 100%; object-fit: cover; }

/* display goes on [open], never on the element itself.
   
   The browser closes a <dialog> by way of its own `dialog:not([open])
   { display: none }`, and that rule is in the UA stylesheet: any author
   declaration beats it whatever the specificity, because origin is decided
   before specificity is looked at. So a bare `.lightbox { display: grid }`
   leaves a closed dialog painted over the page, no longer modal and no longer
   dismissible, which looks exactly like a close button that does not work.
   The `display: none` below is redundant against a correct UA sheet and is
   here to say the state is deliberate. */
.lightbox {
	position: fixed;
	inset: 0;
	z-index: 120;
	display: none;
	place-items: center;
	padding: clamp(1rem, 4vw, 3rem);
	background: rgb(6 7 11 / 94%);
	backdrop-filter: blur(8px);
	border: 0;
}
.lightbox[open] { display: grid; }
/* max-width as well as max-height: a dialog is width:fit-content, so a wide
   image on a narrow screen sizes the box past the viewport instead of being
   held inside it. */
.lightbox img {
	max-width: 100%;
	max-height: 88vh;
	width: auto;
	height: auto;
	border: 1px solid var(--line-strong);
}
.lightbox::backdrop { background: rgb(6 7 11 / 90%); }
.lightbox-close {
	position: absolute;
	top: 1rem;
	right: 1rem;
	background: none;
	border: 1px solid var(--line-strong);
	color: var(--text);
	width: 2.75rem;
	height: 2.75rem;
	font-size: 1.2rem;
	cursor: pointer;
}
.lightbox-close:hover { border-color: var(--accent); }

/* Video facade ----------------------------------------------------------- */
.video { position: relative; aspect-ratio: 16 / 9; border: 1px solid var(--line); overflow: hidden; }
.video-btn {
	display: block;
	width: 100%;
	height: 100%;
	padding: 0;
	border: 0;
	background: var(--surface-2);
	cursor: pointer;
	position: relative;
}
.video-btn img, .video-btn .art-ph { width: 100%; height: 100%; object-fit: cover; opacity: .62; transition: opacity .3s var(--ease); }
.video-btn:hover img, .video-btn:hover .art-ph { opacity: .78; }
.video-play {
	position: absolute;
	inset: 0;
	margin: auto;
	width: 5rem;
	height: 5rem;
	display: grid;
	place-items: center;
	background: var(--accent);
	color: #fff;
	clip-path: polygon(0 0, 100% 0, 100% calc(100% - 16px), calc(100% - 16px) 100%, 0 100%);
	transition: transform .3s var(--ease);
}
.video-btn:hover .video-play { transform: scale(1.08); }
.video-play svg { width: 2rem; height: 2rem; }
.video-label {
	position: absolute;
	left: 1.2rem;
	bottom: 1.2rem;
	font: 500 .76rem/1 var(--mono);
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--text);
}
.video iframe { width: 100%; height: 100%; border: 0; display: block; }

/* News ------------------------------------------------------------------- */
.news-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; background: var(--line); border-block: 1px solid var(--line); }
.news-item { background: var(--bg); transition: background .25s var(--ease); }
.news-item:hover { background: var(--surface); }
.news-item a { display: grid; gap: .5rem; padding: clamp(1.4rem, 3vw, 2.2rem); text-decoration: none; }
.news-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; margin: 0; font: 400 .74rem/1 var(--mono); color: var(--dim); letter-spacing: .06em; }
.news-item h3 { font-size: var(--t-h3); }
.news-excerpt { color: var(--muted); margin: 0; max-width: 68ch; }
.news-more { font: 500 .74rem/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--accent); }
.news-list--compact .news-item a { padding-block: 1.1rem; }

/* Timeline --------------------------------------------------------------- */
.timeline { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.timeline li {
	display: grid;
	gap: .4rem 2rem;
	padding: clamp(1.4rem, 3vw, 2rem) 0;
	border-top: 1px solid var(--line);
}
.timeline li:last-child { border-bottom: 1px solid var(--line); }
.timeline-when { font: 500 .8rem/1.4 var(--mono); letter-spacing: .1em; color: var(--accent); }
.timeline-body h3 { font-size: var(--t-h3); margin-bottom: .3rem; }
.timeline-body p { color: var(--muted); margin: 0; }
@media (min-width: 720px) { .timeline li { grid-template-columns: 8rem 1fr; } }

/* Fact sheet ------------------------------------------------------------- */
.factsheet dl { margin: 0; display: grid; border-top: 1px solid var(--line); }
.factsheet dl > div {
	display: grid;
	gap: .2rem 2rem;
	padding: .9rem 0;
	border-bottom: 1px solid var(--line);
}
.factsheet dt { font: 400 .74rem/1.5 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--dim); }
.factsheet dd { margin: 0; font-weight: 500; }
.factsheet-press { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-top: 2rem; }
.factsheet-press .btn-ghost { text-transform: none; letter-spacing: .02em; }
@media (min-width: 640px) { .factsheet dl > div { grid-template-columns: 12rem 1fr; align-items: baseline; } }

/* Legal pages ------------------------------------------------------------ */
/* A policy is read by two people who want opposite things: a player looking for
   one answer, and a store reviewer checking one clause against a form they are
   filling in. Both are served by a jump list and neither by 2000 words of
   uninterrupted prose, so the dateline and the contents are part of the page
   rather than a courtesy. */
.legal-date {
	font: 400 .74rem/1.5 var(--mono);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--dim);
	margin: 0;
}
.legal-toc { columns: 2; column-gap: 2.4rem; max-width: 46rem; margin: 0; padding: 0; list-style: none; }
.legal-toc li { break-inside: avoid; margin-bottom: .5rem; }
.legal-toc a { color: var(--muted); text-decoration-color: var(--line); }
.legal-toc a:hover { color: var(--text); }
@media (max-width: 560px) { .legal-toc { columns: 1; } }

/* Data disclosure -------------------------------------------------------- */
/* One block per kind of information collected. The heading names it, the note
   under it says what it actually is, and the pairs below answer why, who and
   how long. See inc/modules/data-disclosure.php. */
.disclose-item + .disclose-item { margin-top: clamp(2.4rem, 5vw, 3.6rem); }
.disclose-item h3 { font-size: var(--t-h3); margin: 0 0 .5rem; }
.disclose-note { max-width: 62ch; color: var(--muted); margin: 0 0 1.4rem; }

/* Press assets ----------------------------------------------------------- */
.asset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1.2rem; }
.asset { margin: 0; border: 1px solid var(--line); background: var(--surface); }
.asset-preview { display: grid; place-items: center; padding: 2rem; min-height: 12rem; background: var(--bg); }
.asset figcaption { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: .9rem 1.1rem; border-top: 1px solid var(--line); font-size: .85rem; }
.asset figcaption a { font: 500 .72rem/1 var(--mono); letter-spacing: .1em; color: var(--accent); text-decoration: none; }

/* CTA -------------------------------------------------------------------- */
.cta {
	margin-top: var(--sec-y);
	padding-block: clamp(3rem, 6vw, 5rem);
	border-block: 1px solid var(--line);
	background:
		radial-gradient(70% 140% at 88% 50%, color-mix(in oklab, var(--accent) 18%, transparent) 0%, transparent 70%),
		var(--surface);
}
.cta-inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 2rem; }
.cta h2 { font-size: clamp(1.6rem, 1.2rem + 1.8vw, 2.4rem); }
.cta p { margin: .8rem 0 0; color: var(--muted); max-width: 46ch; }
.cta .btn-row { margin-top: 0; }
.cta-note { margin-top: 2rem; font: 400 .78rem/1.6 var(--mono); color: var(--dim); max-width: 60ch; }

/* --------------------------------------------------------------------------
   8. Forms
   -------------------------------------------------------------------------- */
.form { max-width: 44rem; }

/* A field is a <p>, so the prose rule near the top of this file zeroes the
   margin on the last one in a row. Left alone, that field has no margin to
   stretch against and grows to the height its neighbour's margin set, and its
   own auto rows split the difference: the email input sat lower and taller
   than the name one beside it. The row owns the spacing instead, and
   align-items keeps a field that sprouts an error message from dragging its
   neighbour's input down with it. */
.field-row { display: grid; align-items: start; gap: 1.4rem 1.2rem; margin-bottom: 1.4rem; }
.field-row > .field { margin-bottom: 0; }
@media (min-width: 640px) { .field-row { grid-template-columns: 1fr 1fr; } }

.field { display: grid; align-content: start; gap: .45rem; margin-bottom: 1.4rem; }
.field label { font: 500 .74rem/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.field input, .field select, .field textarea {
	font: 350 1rem/1.5 var(--body);
	color: var(--text);
	background: var(--surface);
	border: 1px solid var(--line-strong);
	padding: .85rem 1rem;
	width: 100%;
	transition: border-color .2s var(--ease), background .2s var(--ease);
}
.field textarea { resize: vertical; min-height: 9rem; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); background: var(--surface-2); }
.field [aria-invalid='true'] { border-color: #ff6b6b; }
.field-error { margin: 0; font-size: .84rem; color: #ff8f8f; }

/* The honeypot. Hidden from people without display:none, which some bots
   check for, and kept out of the tab order by the field's own attributes. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.2rem 2rem; margin-top: .5rem; }
.form-note { margin: 0; font-size: .88rem; color: var(--muted); }

.notice { border: 1px solid var(--line-strong); border-left-width: 3px; padding: 1.3rem 1.5rem; max-width: 44rem; }
.notice h3 { font-size: var(--t-h3); margin-bottom: .4rem; }
.notice p { margin: 0; color: var(--muted); }
.notice--ok { border-left-color: #6ee7a8; }
.notice--bad { border-left-color: #ff6b6b; margin-bottom: 2rem; }

/* The mailing list signup (inc/modules/notify-form.php).

   Centred, which nothing else on this site is: it is the one block that asks
   for something rather than showing something, and centring it stops it
   reading as another column of copy. The logo above it is the same move the
   Heatsink mock made, and a game's own stylesheet overrides these rules to
   keep that look on its own subsite. */
.notify { text-align: center; max-width: 44rem; margin-inline: auto; }
.notify-logo { display: block; width: min(300px, 70vw); height: auto; margin: 0 auto 1.4rem; }
.notify .eyebrow { justify-content: center; }
.notify-title { font-size: var(--t-h2); font-family: var(--display); letter-spacing: -.01em; }
.notify-text { margin: .9rem auto 0; color: var(--muted); max-width: 46ch; }

.notify-form { margin-top: 2rem; text-align: left; }
.notify-label {
	display: block;
	margin-bottom: .5rem;
	font: 500 .74rem/1 var(--mono);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--muted);
}

/* One column on a phone, field and button side by side once there is room for
   both. The button is sized by its own text rather than stretched: a submit
   the width of the screen looks like a banner and gets ignored like one. */
.notify-row { display: grid; gap: .8rem; }
@media (min-width: 560px) { .notify-row { grid-template-columns: 1fr auto; align-items: start; } }

.notify-row input {
	font: 350 1rem/1.5 var(--body);
	color: var(--text);
	background: var(--surface);
	border: 1px solid var(--line-strong);
	padding: .85rem 1rem;
	width: 100%;
	min-height: 3.25rem;
	transition: border-color .2s var(--ease), background .2s var(--ease);
}
.notify-row input:focus { border-color: var(--accent); background: var(--surface-2); }
.notify-row input[aria-invalid='true'] { border-color: #ff6b6b; }
.notify-row .btn { min-height: 3.25rem; }

/* The status line is the note until there is a result, so it holds its own
   height from the first paint and the block does not jump when one arrives. */
.notify-status { margin: .8rem 0 0; font-size: .9rem; color: var(--dim); min-height: 1.4em; }
.notify-status.is-ok { color: #6ee7a8; }
.notify-status.is-bad { color: #ff8f8f; }
.notify-legal { margin: .5rem 0 0; font-size: .82rem; }
.notify-legal a { color: var(--dim); text-decoration-color: var(--line); }
.notify-legal a:hover { color: var(--muted); }

/* A checkbox is the one field whose label belongs beside it rather than above,
   and at sentence case rather than the shouty caps every other label uses. */
.field--check label {
	display: flex;
	gap: .7rem;
	align-items: start;
	font: 350 .95rem/1.5 var(--body);
	letter-spacing: 0;
	text-transform: none;
	color: var(--muted);
}
.field--check input { width: auto; margin-top: .15rem; }

/* --------------------------------------------------------------------------
   9. Intro overlay
   The animation itself is scoped inside inc/partials/intro.php (generated from
   splash.html); what is here is only the overlay it sits in.
   -------------------------------------------------------------------------- */
.intro {
	position: fixed;
	inset: 0;
	z-index: 999;
	display: grid;
	place-items: center;
	background: var(--void);
	/* The card's own last keyframe fades it out; this fades the black behind
	   it a beat later so the site is not revealed through a half-lit logo. */
	animation: intro-out .7s var(--ease) 3.9s forwards;
}
@keyframes intro-out { to { opacity: 0; visibility: hidden; } }

/* Scroll is locked only while the overlay is up. main.js removes the class
   with the node, and does so on any click, key or wheel event as well as when
   the animation ends. The intro is never something to sit through. */
body.has-intro { overflow: hidden; }

.intro-skip {
	position: absolute;
	right: clamp(1rem, 4vw, 2.5rem);
	bottom: clamp(1rem, 4vw, 2.5rem);
	background: none;
	border: 1px solid var(--line-strong);
	color: var(--muted);
	padding: .7rem 1.2rem;
	font: 500 .74rem/1 var(--mono);
	letter-spacing: .14em;
	text-transform: uppercase;
	cursor: pointer;
}
.intro-skip:hover { color: var(--text); border-color: var(--accent); }

/* --------------------------------------------------------------------------
   10. Motion
   -------------------------------------------------------------------------- */
/* Hidden only under html.js, which an inline script in the head sets before
   the first paint (see layout.php). Without it the finished state is what
   renders: the reveal is decoration on server-rendered markup, so a browser
   with JavaScript off, a failed script, or a renderer that does not scroll
   gets the whole page rather than a body stuck at opacity 0. */
.reveal {
	transition: opacity .7s var(--ease) var(--d, 0ms), transform .7s var(--ease) var(--d, 0ms);
}
html.js .reveal {
	opacity: 0;
	transform: translateY(18px);
}
html.js .reveal.visible { opacity: 1; transform: none; }

/* Nothing here is load-bearing: every animation is decoration over content
   that is already in place. Under prefers-reduced-motion they all resolve to
   their finished state rather than to a shorter version of themselves. */
@media (prefers-reduced-motion: reduce) {
	html { scroll-behavior: auto; }
	*, *::before, *::after {
		animation-duration: .001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: .001ms !important;
	}
	html.js .reveal { opacity: 1; transform: none; }
	.btn:hover, .game-card:hover { transform: none; }
}

/* Development only: mod() prints this when a module name does not resolve. It
   is never rendered on the canonical host. See scs_is_dev(). */
.mod-missing {
	font: 500 .8rem/1.4 var(--mono);
	color: #ff8f8f;
	border: 1px dashed #ff6b6b;
	padding: .8rem 1rem;
	margin: 1rem 0;
}
