/* ============================================================================
   Constrict.io -- page shell. "Neon Vivarium" (DESIGN.md section 3).

   One committed visual world: a neon arcade screen. There is deliberately no
   light theme and no theme toggle -- a blacklight terrarium does not have a
   daylight mode, and prefers-color-scheme is not consulted anywhere below.

   Layout notes that are load-bearing rather than taste:
     * The four elements js/ui.js toggles (#nameBox #ref1 #ref2 #YouTuberDiv)
       get an inline `display: initial`, so this file never sets `display` on
       them -- their children do the flex work. See the contract comment in
       index.html.
     * Spacing is gap-driven. There are no per-element margins in the menu
       except the three viewport-anchored offsets that bridge #YouTuberDiv and
       #name, which have to be lengths because the two are separate elements.
   ========================================================================= */

:root {
	color-scheme: dark;

	/* --- ground, DESIGN.md section 3 ------------------------------------ */
	--ground:       #0a0d16;   /* near-black indigo */
	--ground-deep:  #05070d;   /* vignette floor */
	--ground-lift:  #101728;   /* the one step up from ground */
	--lattice:      #1c2338;   /* hex etching */
	--mote:         #2a3355;   /* deep parallax motes */

	/* --- figure --------------------------------------------------------- */
	--brand:        #2ecc40;   /* own-snake green */
	--brand-lit:    #6ff581;
	--brand-core:   #e6ffec;   /* the white-hot core of a light ribbon */
	--brand-deep:   #17722a;
	--brand-ink:    #06240d;   /* type on brand green */
	--danger:       #ff3b30;
	--accent:       #79e;      /* inherited from the 2018 markup, kept */
	--accent-lit:   #a9c4ff;

	/* --- neutrals, pulled toward the ground's indigo -------------------- */
	--ink:          #eaeefb;
	--ink-2:        #a4aecb;
	--ink-3:        #8996b8;   /* 6.5:1 on --ground */
	--ink-4:        #6b7aa4;   /* 4.5:1 on --ground -- the floor, not below it */
	--hair:         rgba(126, 146, 208, 0.16);
	--sunk:         rgba(28, 35, 56, 0.55);

	/* --- type ----------------------------------------------------------- */
	/* Fredoka is the wordmark's own lineage (Fredoka One drew that logo's
	   letterforms); Outfit is the geometric text/HUD companion. Both inlined
	   as woff2 data URIs in type.css. */
	--display: "Fredoka", "Baloo 2", "Trebuchet MS", system-ui, sans-serif;
	--text: "Outfit", "Avenir Next", "Segoe UI", system-ui, -apple-system, sans-serif;

	/* --- one scale knob -------------------------------------------------
	   The menu is a full-bleed 16:9 composition, not a document, so it has to
	   grow with the *scene* rather than with width alone. --k is one percent
	   of the widest 16:9 box that fits the viewport.

	   At 1920x1080 it is exactly 1vw, so every ramp below reproduces the tuned
	   1080p layout to the pixel. At 3840x2160 it is worth 2vw and the whole
	   composition doubles -- type, gutters, the column, the lattice, the mote
	   tiles -- instead of stranding a 900px block against 3km of empty screen.
	   On a short viewport it shrinks with the height, which is the axis that
	   actually runs out in this layout (see the height queries at the bottom).

	   Every ceiling is set at roughly twice its 1080p value: past 4K the
	   composition should stop growing rather than turn into signage. */
	--k: min(1vw, 1.7778vh);

	--fs-hero:  clamp(24px, calc(1.55 * var(--k)), 62px);     /* the name you type */
	--fs-lead:  clamp(16px, calc(1.05 * var(--k)), 42px);     /* thesis */
	--fs-body:  clamp(14px, calc(0.78 * var(--k)), 31px);
	--fs-small: clamp(13px, calc(0.72 * var(--k)), 29px);
	--fs-label: clamp(11.5px, calc(0.62 * var(--k)), 25px);   /* uppercase, letterspaced */
	--fs-play:  clamp(20px, calc(1.5 * var(--k)), 60px);
	--fs-death: clamp(19px, calc(1.2 * var(--k)), 48px);      /* the death card */

	/* --- rhythm --------------------------------------------------------- */
	--gutter:   clamp(20px, calc(5.2 * var(--k)), 300px);
	/* the one place height gets a vote of its own: on a phone the corners want
	   the taller of the two measures, not the smaller */
	--edge-y:   clamp(18px, max(3.4vh, calc(1.91 * var(--k))), 92px);
	--col:      min(calc(100vw - 2 * var(--gutter)), clamp(300px, calc(30 * var(--k)), 1160px));
	--field-h:  clamp(54px, calc(3.4 * var(--k)), 132px);
	--play-h:   clamp(56px, calc(3.7 * var(--k)), 144px);
	--above:    clamp(14px, calc(0.73 * var(--k)), 28px);   /* field label -> field */
	--below:    clamp(26px, calc(1.35 * var(--k)), 52px);   /* field -> play */

	--s1: clamp(6px,  calc(0.31 * var(--k)), 12px);
	--s2: clamp(10px, calc(0.52 * var(--k)), 20px);
	--s3: clamp(16px, calc(0.83 * var(--k)), 32px);
	--s4: clamp(26px, calc(1.35 * var(--k)), 52px);

	--r-chip: clamp(7px, calc(0.36 * var(--k)), 14px);
	--rule:   clamp(2px, calc(0.104 * var(--k)), 4px);   /* the field's lit underline */
	--lip:    clamp(5px, calc(0.26 * var(--k)), 10px);   /* the play button's hard offset */

	--death-top: var(--edge-y);
	--fade: 420ms;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
	/* gameIO sizes its canvas off document.body.clientWidth/Height. The 2018
	   page got a viewport-sized body for free from quirks mode; with a doctype
	   it has to be asked for, or the canvas comes back 0x0. */
	height: 100%;
	margin: 0;
	padding: 0;
}

body {
	background: var(--ground);
	color: var(--ink);
	font-family: var(--text);
	font-size: var(--fs-body);
	line-height: 1.5;
	-webkit-font-smoothing: antialiased;
	-webkit-text-size-adjust: 100%;
}

/* Texture sources for the renderer. Never displayed, always loaded. */
#bg, #body, #head, #tongue, #circle, #magnet, #logo { display: none; }

.a11y {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Overlay roots. z-index sits above the renderer's canvas (position:absolute,
   z-index:auto) with room underneath for anything the renderer stacks later.
   Never set `display` here: ui.js owns it.
   ------------------------------------------------------------------------ */

/* No opacity, no transition and no animation on this element, deliberately.
   app.js fades the whole menu in over ~1.2s after a death -- the death animation
   used to be invisible because an opaque menu arrived on the same frame -- and it
   drives that fade from JS. A CSS animation here would win the cascade outright
   (animations beat inline styles) and pin opacity to 1 for the entire fade, and a
   CSS transition would fight a JS rAF ramp. So the staged entrance lives on the
   children below, which composite under whatever opacity JS sets on this root. */
#YouTuberDiv {
	position: fixed;
	inset: 0;
	z-index: 30;
	overflow: hidden;
}

.world   { animation: menu-in var(--fade) cubic-bezier(0.22, 0.9, 0.28, 1) both; }
.lede    { animation: menu-in var(--fade) 40ms cubic-bezier(0.22, 0.9, 0.28, 1) both; }
.launch  { animation: menu-in var(--fade) 90ms cubic-bezier(0.22, 0.9, 0.28, 1) both; }
.creator, #YouTuberDiv > a { animation: menu-in var(--fade) 150ms cubic-bezier(0.22, 0.9, 0.28, 1) both; }

#name {
	position: fixed;
	top: 50%;
	left: var(--gutter);
	width: var(--col);
	z-index: 31;
	font-size: 0;     /* kill the strut: the field's top edge must sit exactly */
	line-height: 0;   /* on the 50% mid-line that .lede and .launch anchor to */
	/* app.js rewrites this every menu frame to scale(1/renderer.ratio), which
	   pinned the field to the canvas's 1920x1080 virtual space and made it
	   twice as large on a 4K monitor. The shell has its own type scale now. */
	transform: none !important;
	/* and no animation here either: applyMenuFade() writes style.opacity on
	   #name as well as on the four elements showName toggles */
}

.field { animation: menu-in var(--fade) 60ms cubic-bezier(0.22, 0.9, 0.28, 1) both; }

/* #ref1 and #ref2 are two of the four elements ui.js toggles, so for the same
   reason as #YouTuberDiv their entrance is carried by the .corner inside them. */
#ref1, #ref2 {
	position: fixed;
	bottom: var(--edge-y);
	z-index: 32;
}

#ref1 { left: var(--gutter); }
#ref2 { right: var(--gutter); }

@keyframes menu-in {
	from { opacity: 0; transform: translateY(10px); }
	to   { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   The world: ground, hex lattice, three parallax mote layers, the world line,
   vignette. Painted bottom-up in document order.
   ------------------------------------------------------------------------ */

.world { position: absolute; inset: 0; }

.world__ground {
	position: absolute;
	inset: 0;
	background:
		radial-gradient(58% 54% at 20% 44%, rgba(46, 204, 64, 0.075) 0%, transparent 72%),
		radial-gradient(120% 92% at 16% 4%, var(--ground-lift) 0%, transparent 62%),
		radial-gradient(96% 84% at 94% 92%, #0d1422 0%, transparent 64%),
		var(--ground);
}

/* Replaces BG_yes.png's 725 muddy tiles. Same hex motif, drawn clean at ~15%
   contrast, faded off at the edges so the field has a middle. */
.world__lattice {
	position: absolute;
	inset: 0;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='55.426' viewBox='0 0 96 55.426'%3E%3Cg fill='none' stroke='%231c2338' stroke-width='1.4'%3E%3Cpath d='M32 0L16 27.713L-16 27.713L-32 0L-16 -27.713L16 -27.713Z'/%3E%3Cpath d='M32 55.426L16 83.139L-16 83.139L-32 55.426L-16 27.713L16 27.713Z'/%3E%3Cpath d='M128 0L112 27.713L80 27.713L64 0L80 -27.713L112 -27.713Z'/%3E%3Cpath d='M128 55.426L112 83.139L80 83.139L64 55.426L80 27.713L112 27.713Z'/%3E%3Cpath d='M80 27.713L64 55.426L32 55.426L16 27.713L32 0L64 0Z'/%3E%3C/g%3E%3C/svg%3E");
	/* scaled toward the renderer's own hex field so the menu reads as the same
	   world you are about to drop into, not a different one */
	background-size: clamp(80px, calc(7.4 * var(--k)), 300px) auto;
	opacity: 0.72;
	-webkit-mask-image: radial-gradient(118% 104% at 46% 42%, #000 12%, transparent 86%);
	mask-image: radial-gradient(118% 104% at 46% 42%, #000 12%, transparent 86%);
}

/* Oversized and translated by exactly one tile so the loop is seamless and the
   whole thing stays on the compositor -- no repaint next to a running game. */
.world__motes {
	position: absolute;
	inset: -26%;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Cg fill='%232a3355'%3E%3Ccircle cx='23' cy='47' r='1.6'/%3E%3Ccircle cx='91' cy='18' r='1.1'/%3E%3Ccircle cx='152' cy='73' r='2'/%3E%3Ccircle cx='211' cy='29' r='1.3'/%3E%3Ccircle cx='277' cy='61' r='1.5'/%3E%3Ccircle cx='341' cy='22' r='1.1'/%3E%3Ccircle cx='378' cy='96' r='1.8'/%3E%3Ccircle cx='57' cy='118' r='1.3'/%3E%3Ccircle cx='129' cy='145' r='1.7'/%3E%3Ccircle cx='193' cy='112' r='1.1'/%3E%3Ccircle cx='248' cy='158' r='2.1'/%3E%3Ccircle cx='312' cy='131' r='1.4'/%3E%3Ccircle cx='366' cy='176' r='1.2'/%3E%3Ccircle cx='34' cy='192' r='1.9'/%3E%3Ccircle cx='98' cy='227' r='1.3'/%3E%3Ccircle cx='161' cy='203' r='1.5'/%3E%3Ccircle cx='223' cy='246' r='1.1'/%3E%3Ccircle cx='289' cy='219' r='1.8'/%3E%3Ccircle cx='351' cy='262' r='1.4'/%3E%3Ccircle cx='19' cy='281' r='1.2'/%3E%3Ccircle cx='77' cy='314' r='1.7'/%3E%3Ccircle cx='141' cy='289' r='1.1'/%3E%3Ccircle cx='204' cy='332' r='1.6'/%3E%3Ccircle cx='266' cy='305' r='1.3'/%3E%3Ccircle cx='330' cy='348' r='1.9'/%3E%3Ccircle cx='383' cy='318' r='1.1'/%3E%3Ccircle cx='49' cy='371' r='1.4'/%3E%3Ccircle cx='112' cy='392' r='1.2'/%3E%3Ccircle cx='176' cy='363' r='1.7'/%3E%3Ccircle cx='239' cy='388' r='1.1'/%3E%3Ccircle cx='302' cy='371' r='1.5'/%3E%3Ccircle cx='359' cy='396' r='1.3'/%3E%3C/g%3E%3C/svg%3E");
	background-size: var(--tile) var(--tile);
	will-change: transform;
}

/* The tile size is a variable so the drift keyframes can translate by exactly one
   tile -- that is what makes the loop seamless -- while the tile itself scales with
   --k. Left at a fixed 560px a 4K screen would show four times the mote density at
   a quarter of the apparent size, which is a different, busier sky. */
.world__motes--far  { --tile: clamp(320px, calc(29.2 * var(--k)), 1120px); opacity: 0.66; animation: drift-far 170s linear infinite; }
.world__motes--mid  { --tile: clamp(220px, calc(18.8 * var(--k)), 720px);  opacity: 0.4;  animation: drift-mid 105s linear infinite; }
.world__motes--near { --tile: clamp(150px, calc(12.1 * var(--k)), 464px);  opacity: 0.2;  animation: drift-near 62s linear infinite; }

@keyframes drift-far  { to { transform: translate3d(var(--tile), calc(-1 * var(--tile)), 0); } }
@keyframes drift-mid  { to { transform: translate3d(calc(-1 * var(--tile)), calc(-1 * var(--tile)), 0); } }
@keyframes drift-near { to { transform: translate3d(var(--tile), calc(-1 * var(--tile)), 0); } }

.world__vignette {
	position: absolute;
	inset: 0;
	background: radial-gradient(112% 102% at 46% 44%, transparent 26%, rgba(5, 7, 13, 0.58) 74%, rgba(5, 7, 13, 0.9) 100%);
}

/* --- the world line ------------------------------------------------------ */

.worldline {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	-webkit-mask-image: linear-gradient(to top, transparent 0, #000 22%);
	mask-image: linear-gradient(to top, transparent 0, #000 22%);
}
.worldline--tall { display: none; }

.rib { fill: none; stroke-linecap: round; }

/* Luminance hierarchy, DESIGN.md section 6.3: a hard-edged core inside a soft
   halo. The halo never eats the silhouette. */
.rib--halo  { stroke: var(--brand); stroke-width: 152; opacity: 0.055; filter: blur(46px); }
.rib--haze  { stroke: var(--brand); stroke-width: 88;  opacity: 0.1;   filter: blur(20px); }
.rib--body  { stroke: url(#ribbon); stroke-width: 54; opacity: 0.46; }
.rib--core  { stroke: var(--brand-core); stroke-width: 14; opacity: 0.13; }
.rib--distant { stroke: var(--brand); stroke-width: 13; opacity: 0.15; filter: blur(1.5px); }

/* The constriction wave: a bright bulge travelling head to tail. This is the
   game's name rendered as animation (DESIGN.md section 4) and the reason the
   menu reads as this game rather than any dark game. */
.rib--pulse {
	stroke: var(--brand-core);
	stroke-width: 36;
	opacity: 0.24;
	filter: blur(9px);
	stroke-dasharray: 90 880;
	animation: constrict 5.4s linear infinite;
}

@keyframes constrict {
	from { stroke-dashoffset: 970; }
	to   { stroke-dashoffset: 0; }
}

.worldline--tall .rib--halo  { stroke-width: 40; filter: blur(12px); }
.worldline--tall .rib--haze  { stroke-width: 23; filter: blur(5px); }
.worldline--tall .rib--body  { stroke-width: 15; }
.worldline--tall .rib--core  { stroke-width: 4; }
.worldline--tall .rib--pulse { stroke-width: 10; filter: blur(2px); stroke-dasharray: 24 231; }

/* --- the mascot ---------------------------------------------------------- */

.mascot { filter: drop-shadow(0 0 10px rgba(46, 204, 64, 0.5)) drop-shadow(0 0 34px rgba(46, 204, 64, 0.28)); }
.mascot__skull, .mascot__spike { stroke: #093f14; stroke-width: 3.5; stroke-linejoin: round; }
.mascot__skull   { fill: #38c94a; }
.mascot__spike   { fill: #229a33; }
.mascot__nostril { fill: #06240d; opacity: 0.7; }
.mascot__white   { fill: #f2f7ff; }
.mascot__pupil   { fill: #0a0d16; animation: saccade 8.5s ease-in-out infinite; }
.mascot__tongue  { fill: var(--danger); }

.mascot__eye {
	transform-box: fill-box;
	transform-origin: center;
	animation: blink 7.2s ease-in-out infinite;
}
.mascot__eye--b { animation-delay: 40ms; }

.mascot__tongue {
	transform-box: fill-box;
	transform-origin: left center;
	animation: flick 8.8s ease-in-out infinite;
}

@keyframes saccade {
	0%, 34%   { transform: translate(0, 0); }
	40%, 58%  { transform: translate(3px, -2.5px); }
	64%, 86%  { transform: translate(-2px, 3px); }
	92%, 100% { transform: translate(0, 0); }
}

@keyframes blink {
	0%, 90%, 100% { transform: scaleY(1); }
	94%           { transform: scaleY(0.08); }
}

@keyframes flick {
	0%, 86%, 100% { transform: scaleX(0); opacity: 0; }
	89%           { transform: scaleX(1); opacity: 1; }
	93%           { transform: scaleX(0.55); opacity: 1; }
	97%           { transform: scaleX(0); opacity: 0; }
}

/* ---------------------------------------------------------------------------
   The hero column. Left-anchored, not centred -- the world line owns the
   right-hand two thirds, so the page has a direction to read in.
   ------------------------------------------------------------------------ */

.lede, .launch {
	position: absolute;
	left: var(--gutter);
	width: var(--col);
	display: flex;
	flex-direction: column;
	align-items: flex-start;
}

.lede   { bottom: calc(50% + var(--above)); gap: var(--s4); }
.launch { top: calc(50% + var(--field-h) + var(--below)); gap: var(--s4); }

/* align-self: stretch on both, or the wordmark stops growing at 749px.
   .lede's align-items: flex-start sizes its items to fit-content, and the
   lockup's max-content is the logo's intrinsic width -- so past the viewport
   where the column overtakes 749px (about 2500px wide) the brand quietly froze
   while the field and the button beside it kept scaling, inverting the whole
   hierarchy. Below that width fit-content already equalled --col, so this
   changes nothing at 1080p and everything at 4K. */
.lede__lockup {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	align-self: stretch;
	gap: var(--s2);
}

.wordmark { align-self: stretch; }
.wordmark picture { display: block; }

.kicker {
	margin: 0;
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.22em;
	text-transform: uppercase;
	color: var(--ink-3);
}

/* The 2018 logo is the brand and it stays (DESIGN.md asset table). What it did
   not have was luminance: three stacked drop-shadows turn flat cartoon green
   into neon without touching a pixel of the artwork. */
.wordmark { margin: 0; line-height: 0; }

.wordmark img {
	display: block;
	width: 100%;
	height: auto;
	filter:
		drop-shadow(0 0 calc(0.104 * var(--k)) rgba(46, 204, 64, 1))
		drop-shadow(0 0 calc(1.04 * var(--k)) rgba(46, 204, 64, 0.62))
		drop-shadow(0 0 calc(3.54 * var(--k)) rgba(46, 204, 64, 0.34));
}

.thesis {
	margin: 0;
	max-width: min(100%, 42ch);
	font: 400 var(--fs-lead) / 1.45 var(--text);
	color: var(--ink-2);
	text-wrap: balance;
}

.fieldlabel {
	display: block;
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--ink-3);
	cursor: text;
}

/* --- the name field: an invitation, not a form --------------------------- */

#nameBox {
	width: 100%;
	height: var(--field-h);
	padding: 0 var(--s3);
	margin: 0;
	vertical-align: top;
	font: 500 var(--fs-hero) / 1 var(--text);
	color: var(--ink);
	background: rgba(4, 6, 12, 0.5);
	border: 0;
	border-bottom: var(--rule) solid var(--ink-3);
	box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.45);
	border-radius: 5px 5px 0 0;
	outline: none;
	caret-color: var(--brand-lit);
	transition: background-color 170ms ease, border-color 170ms ease, box-shadow 170ms ease;
}

#nameBox::placeholder { color: var(--ink-4); font-weight: 400; }
#nameBox:hover { border-bottom-color: var(--ink-3); }

/* The lit underline is the focus state -- brightness is how this world says
   "active", and it is far more visible than a UA ring. */
#nameBox:focus, #nameBox:focus-visible {
	background: rgba(10, 20, 34, 0.62);
	border-bottom-color: var(--brand);
	box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.4), 0 10px 30px -14px rgba(46, 204, 64, 0.8);
}

/* --- the play affordance ------------------------------------------------- */

.play {
	display: inline-flex;
	align-items: center;
	gap: var(--s4);
	justify-content: space-between;
	min-width: min(100%, max(17rem, calc(14 * var(--k))));
	height: var(--play-h);
	padding: 0 var(--s4) 0 calc(var(--s4) + 4px);
	border: 0;
	border-radius: 999px;   /* a segment of the snake, hence the only pill here */
	font: 600 var(--fs-play) / 1 var(--display);
	letter-spacing: 0.03em;
	text-transform: uppercase;
	color: var(--brand-ink);
	background: linear-gradient(180deg, #6bef7e 0%, var(--brand) 52%, #23a835 100%);
	/* the hard offset shadow is this site's own 2018 idiom, kept on purpose */
	box-shadow:
		0 var(--lip) 0 var(--brand-deep),
		0 calc(0.83 * var(--k)) calc(1.98 * var(--k)) calc(-0.63 * var(--k)) rgba(46, 204, 64, 0.7),
		inset 0 1px 0 rgba(255, 255, 255, 0.5);
	cursor: pointer;
	transition: transform 90ms ease, box-shadow 180ms ease, filter 180ms ease;
}

.play:hover {
	filter: brightness(1.07);
	box-shadow:
		0 var(--lip) 0 var(--brand-deep),
		0 calc(1.04 * var(--k)) calc(2.71 * var(--k)) calc(-0.52 * var(--k)) rgba(46, 204, 64, 0.95),
		inset 0 1px 0 rgba(255, 255, 255, 0.55);
}

.play:active {
	transform: translateY(calc(0.8 * var(--lip)));
	box-shadow:
		0 calc(0.2 * var(--lip)) 0 var(--brand-deep),
		0 calc(0.52 * var(--k)) calc(1.25 * var(--k)) calc(-0.73 * var(--k)) rgba(46, 204, 64, 0.6),
		inset 0 1px 0 rgba(255, 255, 255, 0.4);
}

.play:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }

/* " again" is revealed by :has() once the death card exists, so the button reads
   as a retry on the death screen without app.js needing to know about it. */
.play__again { display: none; }
body:has(#constrictDeathCard) .play__again { display: inline; }

/* Chip padding is in em from here down so it tracks its own type ramp -- at 4K a
   fixed 6px inset turns every chip into a sticker with the label rattling in it. */
.play .keycap {
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	padding: 0.5em 0.76em 0.42em;
	border-radius: var(--r-chip);
	color: #0b3d17;
	background: rgba(6, 36, 13, 0.22);
	box-shadow: inset 0 -1px 0 rgba(6, 36, 13, 0.25);
}

.controls {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--s2) var(--s4);
	font-size: var(--fs-small);
	color: var(--ink-3);
}

.controls li { display: flex; align-items: center; gap: var(--s1); }

.controls .key {
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--ink-2);
	padding: 0.42em 0.67em 0.34em;
	border-radius: var(--r-chip);
	background: var(--sunk);
	box-shadow: inset 0 0 0 1px var(--hair), 0 2px 0 rgba(0, 0, 0, 0.35);
}

/* ---------------------------------------------------------------------------
   The death card.

   js/ui.js creates this element and hands it a whole inline cssText, so anything
   the shell needs to own has to be !important to reach it. What the shell owns
   is the composition. It used to be what ui.js made it: a single red line
   centred 24px off the top of the viewport, belonging to nothing.

   It is now the hero column's header -- the same left edge, the same maximum
   width, the same type ramp, and the same sunk-panel language as the name field
   and the key chips, with the danger rail and the pip as the only new notes. It
   is deliberately no longer centred across the whole viewport: the rest of this
   page is read left to right down one column, and a line floating over the
   middle of the screen is in a different composition from the one under it.

   The fade is app.js's. #YouTuberDiv carries no CSS opacity of its own (see the
   note on the overlay roots), so a JS-driven fade on the menu root carries the
   card with it; the card's own entrance is delayed past the start of that fade
   so it lands last instead of racing it.
   ------------------------------------------------------------------------ */

/* #constrictConnCard rides along on every rule the card's SHAPE lives in -- the slot,
   the panel, the rail, the pip, the short-viewport step down, the reduced-motion stop
   -- and takes its own colour ramp in the one block after this one. It is a separate id
   on purpose: `body:has(#constrictDeathCard)` below turns Play into "Play again" and
   stands the kicker and the thesis down, and a connection notice must not do either.
   ui.js removes whichever of the two is not wanted, so they never occupy the slot at
   the same time. */
#constrictDeathCard, #constrictConnCard {
	display: flex;
	align-items: baseline;
	gap: 0.6em;
	top: var(--death-top) !important;
	left: var(--gutter) !important;
	right: auto !important;
	width: fit-content;
	max-width: var(--col);
	margin: 0 !important;
	padding: var(--s3) calc(1.3 * var(--s3)) calc(1.06 * var(--s3)) var(--s3);
	text-align: left !important;
	font-family: var(--display) !important;
	font-size: var(--fs-death) !important;
	font-weight: 500;
	line-height: 1.24 !important;
	letter-spacing: 0.005em !important;
	/* the white core of DESIGN.md's danger ramp rather than the mid-red ui.js
	   ships, which sat at 6.9:1 flat -- this is 12:1 on the card's own ground and
	   leaves the red to the rail, the glow and the pip, where it reads as heat
	   instead of as body copy */
	color: #ffd9d4 !important;
	text-shadow: 0 0 calc(1.1 * var(--k)) rgba(255, 59, 48, 0.4) !important;
	text-wrap: balance;
	font-variant-numeric: tabular-nums;
	/* Warm at the top, back to ground at the bottom, and the red kept mostly in the
	   rail and the pip. Pushed much further it stops being a card in this world and
	   starts being an operating system's error dialog. */
	background: linear-gradient(180deg, rgba(38, 14, 18, 0.9) 0%, rgba(10, 13, 24, 0.93) 100%);
	border: 1px solid rgba(255, 59, 48, 0.26);
	border-left: var(--rule) solid var(--danger);
	border-radius: var(--r-chip);
	box-shadow:
		0 calc(1.1 * var(--k)) calc(3.2 * var(--k)) calc(-1.4 * var(--k)) rgba(0, 0, 0, 0.9),
		0 0 calc(2.6 * var(--k)) calc(-1.1 * var(--k)) rgba(255, 59, 48, 0.32),
		inset 0 1px 0 rgba(255, 255, 255, 0.06);
	backdrop-filter: blur(7px);
	-webkit-backdrop-filter: blur(7px);
	animation: card-in 620ms 340ms cubic-bezier(0.22, 0.9, 0.28, 1) both;
}

/* The one live thing on the card: a hot pip sitting on the first baseline, the
   same mark the border rail is made of. It breathes rather than blinks -- this
   card appears at the exact moment input stopped mattering. */
#constrictDeathCard::before, #constrictConnCard::before {
	content: "";
	flex: none;
	width: 0.4em;
	height: 0.4em;
	border-radius: 50%;
	background: var(--danger);
	box-shadow: 0 0 0 0.16em rgba(255, 59, 48, 0.15), 0 0 0.8em rgba(255, 59, 48, 0.95);
	transform: translateY(-0.06em);
	animation: death-pip 2.6s ease-in-out infinite;
}

@keyframes card-in {
	from { opacity: 0; transform: translateY(-10px); }
	to   { opacity: 1; transform: none; }
}

@keyframes death-pip {
	0%, 100% { opacity: 1; }
	50%      { opacity: 0.4; }
}

/* A long killer name wraps to three lines, and a short viewport is where this
   column has the least to give. Both of those are worst at once, so the card
   loses a step of its own type ramp before it can reach the lockup. */
@media (max-height: 700px) {
	#constrictDeathCard, #constrictConnCard {
		font-size: clamp(15px, calc(1.15 * var(--k)), 24px) !important;
		padding: calc(0.62 * var(--s3)) var(--s3);
	}
}

/* The connection notice is the death card's shape in the warn ramp rather than the
   danger one. Red is this game's word for "you died", and a dropped socket is not a
   death -- it is a state the player is waiting out, and the pip breathing amber says
   "working on it" where a red one would say "over".

   The 340ms entrance delay it inherits from card-in is load-bearing here and not an
   accident: `both` fill holds the card at opacity 0 until then, so an ordinary
   handshake that lands inside that window paints nothing at all and only a genuinely
   slow or dead connect ever becomes visible. */
#constrictConnCard {
	color: #ffe6c2 !important;
	text-shadow: 0 0 calc(1.1 * var(--k)) rgba(255, 176, 58, 0.4) !important;
	background: linear-gradient(180deg, rgba(38, 27, 14, 0.9) 0%, rgba(10, 13, 24, 0.93) 100%);
	border-color: rgba(255, 176, 58, 0.26);
	border-left-color: #ffb03a;
	box-shadow:
		0 calc(1.1 * var(--k)) calc(3.2 * var(--k)) calc(-1.4 * var(--k)) rgba(0, 0, 0, 0.9),
		0 0 calc(2.6 * var(--k)) calc(-1.1 * var(--k)) rgba(255, 176, 58, 0.32),
		inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

#constrictConnCard::before {
	background: #ffb03a;
	box-shadow: 0 0 0 0.16em rgba(255, 176, 58, 0.15), 0 0 0.8em rgba(255, 176, 58, 0.95);
}

/* app.js sets .disabled from the socket state. It has to read as "not yet" rather than
   as a broken button: the lip and the silhouette stay, the green drains out. No opacity
   here -- #YouTuberDiv's opacity is app.js's menu fade and multiplying a second one into
   it during the post-death fade would make the button the only thing that fades twice. */
.play:disabled {
	cursor: default;
	filter: grayscale(0.72) brightness(0.72);
	box-shadow:
		0 var(--lip) 0 var(--brand-deep),
		inset 0 1px 0 rgba(255, 255, 255, 0.3);
}

.play:disabled:hover { filter: grayscale(0.72) brightness(0.72); }
.play:disabled:active { transform: none; }

/* The card takes the kicker's job -- an uppercase micro-label introducing the
   lockup -- and the thesis is a pitch the player has just finished living, so
   both step aside for it. That is also what buys the card its headroom on a
   short viewport, where this column is already the tight case.

   Hung off body rather than off #YouTuberDiv on purpose: ui.js appends the card
   to the menu root today, but the only thing these rules actually need to know
   is that a death card exists somewhere on the page. Written this way they keep
   working if it is ever parented elsewhere, and they cannot silently stop
   hiding the thesis and leave the card sitting on top of the wordmark. */
body:has(#constrictDeathCard) .kicker,
body:has(#constrictDeathCard) .thesis { display: none; }

/* ---------------------------------------------------------------------------
   Quiet corners.
   ------------------------------------------------------------------------ */

.corner {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--s2);
	animation: menu-in var(--fade) 200ms cubic-bezier(0.22, 0.9, 0.28, 1) both;
}
.corner--right { align-items: flex-end; text-align: right; }

.corner__label {
	margin: 0;
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--ink-4);
}

.corner__name { margin: 0; font-size: var(--fs-small); color: var(--ink-2); }
.corner__note { margin: 0; font-size: var(--fs-label); color: var(--ink-4); }
.ver { font-variant-numeric: tabular-nums; color: var(--ink); }

.social { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--s1); }

/* Quiet by default -- indigo chips with the glyph dimmed -- and the brand
   colour only arrives on hover. Four saturated logo squares would out-shout
   the play button, and the luminance hierarchy is law here too. */
.social__chip {
	display: block;
	width: clamp(32px, calc(1.67 * var(--k)), 64px);
	height: clamp(32px, calc(1.67 * var(--k)), 64px);
	border-radius: var(--r-chip);
	background-color: var(--sunk);
	background-repeat: no-repeat;
	background-position: center;
	background-size: 53%;
	box-shadow: inset 0 0 0 1px var(--hair);
	opacity: 0.62;
	transition: opacity 160ms ease, background-color 160ms ease, transform 160ms ease;
}

.social__chip:hover, .social__chip:focus-visible { opacity: 1; transform: translateY(-2px); }
.social__chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.social__chip--discord { background-image: url(images/discord.png); }
.social__chip--twitter { background-image: url(images/twitter.png); }
.social__chip--youtube { background-image: url(images/youtube.png); }
.social__chip--reddit  { background-image: url(images/reddit.png); }

.social__chip--discord:hover { background-color: #5865f2; }
.social__chip--twitter:hover { background-color: #1b95e0; }
.social__chip--youtube:hover { background-color: #e52d27; }
.social__chip--reddit:hover  { background-color: #ff5722; }

.ref {
	font-size: var(--fs-small);
	color: var(--ink-3);
	text-decoration: none;
	border-bottom: 1px solid var(--ink-4);
	padding-bottom: 1px;
	transition: color 150ms ease, border-color 150ms ease;
}

.ref:hover { color: var(--ink); border-bottom-color: var(--brand); }
.ref:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

/* The featured creator. The <a> is appended by app.js as the last child of
   #YouTuberDiv, so it is placed from here rather than wrapped. app.js also
   sets float/width/margin inline; float is inert on an absolutely positioned
   box, and its inline colour is --accent already. */
.creator {
	position: absolute;
	top: var(--edge-y);
	right: var(--gutter);
	margin: 0;
	font: 500 var(--fs-label) / 1 var(--text);
	letter-spacing: 0.2em;
	text-transform: uppercase;
	color: var(--ink-4);
}

#YouTuberDiv > a {
	position: absolute;
	/* under the label, which is one --fs-label line tall at line-height 1 */
	top: calc(var(--edge-y) + var(--fs-label) + var(--s2));
	right: var(--gutter);
	font-family: var(--text);
	font-size: var(--fs-small) !important;
	font-weight: 500;
	line-height: 1.3;
	text-align: right;
}

#YouTuberDiv > a:hover { color: var(--accent-lit) !important; }
#YouTuberDiv > a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

/* ---------------------------------------------------------------------------
   Fits. The composition is anchored to the mid-line, so short viewports are
   the tight case, not narrow ones.
   ------------------------------------------------------------------------ */

@media (orientation: portrait) {
	.worldline--wide { display: none; }
	.worldline--tall { display: block; opacity: 0.72; }
	.thesis { max-width: none; }
}

@media (max-width: 560px) {
	/* the ideal-measure column leaves a lopsided right margin at phone widths;
	   below this it just fills the gutters */
	:root {
		--s4: 20px;
		--fs-lead: 15px;
		--col: calc(100vw - 2 * var(--gutter));
		/* the column now runs the full width, so the death card would pass under
		   the featured-creator chip in the opposite corner: drop it below that
		   block instead of shortening it. The offset is the chip's own height --
		   label line, gap, link line -- plus one step of air. */
		--death-top: calc(var(--edge-y) + var(--fs-label) + var(--s2) + 1.3 * var(--fs-small) + var(--s4));
	}
	.corner__note { display: none; }
	.play { width: 100%; justify-content: center; }
	.controls { gap: var(--s1) var(--s3); }
}

/* Short viewports are the tight case, not narrow ones -- the composition is
   anchored to the mid-line and grows both ways from it, so height runs out
   first. Each step drops the least load-bearing thing left, and each threshold
   is the height at which the hero column would otherwise touch the corners. */
@media (max-height: 780px) {
	:root { --s4: 18px; --below: 20px; --edge-y: 20px; }
}

@media (max-height: 680px) {
	:root { --s4: 15px; }
	.controls { display: none; }
}

@media (max-height: 580px) {
	:root { --fs-play: 18px; --field-h: 48px; --play-h: 48px; --below: 14px; --edge-y: 14px; }
	.thesis, .kicker, .corner__note, .corner__label { display: none; }
}

/* Ambient motion is decoration; the constriction pulse, the drift, the blink,
   the menu's staged entrance and the death card's pip all stop. Nothing here
   carries information. app.js's own death fade is not this file's to cancel. */
@media (prefers-reduced-motion: reduce) {
	.field, .world, .lede, .launch, .creator, #YouTuberDiv > a, .corner,
	.world__motes, .rib--pulse, .mascot__eye, .mascot__pupil, .mascot__tongue,
	#constrictDeathCard, #constrictDeathCard::before,
	#constrictConnCard, #constrictConnCard::before {
		animation: none !important;
	}
	.rib--pulse { opacity: 0; }
	.mascot__tongue { opacity: 0; }
	.play, .social__chip, .ref, #nameBox { transition: none; }
}
