/* ================================================================
   SITE
   ================================================================
   Layout. Every color, size, space and radius comes from tokens.css
   through var(), so a redesign replaces that file rather than this
   one.
   ================================================================ */

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

/* The scrollbar's lane is held open whether or not a scrollbar is in it, so
   that taking the page's overflow away never changes how wide the page is.
   The interstitial locks scrolling while it is up; without the reserved lane
   the classic 15px scrollbar leaves and comes back, which slides the centered
   column 7.5px and the full-bleed header 15px at both ends of the notice.
   It also settles the same jump between a page long enough to scroll and one
   that is not. Overlay scrollbars reserve nothing, so a touch device pays no
   strip for this. */
html {
	-webkit-text-size-adjust: 100%;
	scrollbar-gutter: stable;
}

/* the ground every page shares: a fine grid with a heavier line every
   fifth one. Four gradients rather than an image, so it costs no request
   and follows the theme, and on body rather than a per-page element, so
   the pages inherit it instead of each restating it. The major pair is
   listed first because a background's first layer paints on top, which
   is what settles the intersections.

   The origin stays at the corner rather than centered: centering lands the
   tile edges on a half pixel whenever the body width is odd -- which the
   scrollbar alone is enough to do -- and every line comes out two pixels
   of blend instead of one of the token's color. From the corner the two
   grids also start on the same line by construction, --grid-major being a
   whole multiple of --grid-cell, so the symmetry that centering was for
   costs a measurably softer grid */
body {
	margin: 0;
	background-color: var(--bg);
	background-image: linear-gradient(to right, var(--grid-line-major) var(--grid-line-width), transparent var(--grid-line-width)), linear-gradient(to bottom, var(--grid-line-major) var(--grid-line-width), transparent var(--grid-line-width)), linear-gradient(to right, var(--grid-line) var(--grid-line-width), transparent var(--grid-line-width)), linear-gradient(to bottom, var(--grid-line) var(--grid-line-width), transparent var(--grid-line-width));
	background-size:
		var(--grid-major) var(--grid-major),
		var(--grid-major) var(--grid-major),
		var(--grid-cell) var(--grid-cell),
		var(--grid-cell) var(--grid-cell);
	color: var(--text);
	font-family: var(--font-body);
	font-size: var(--size-step-0);
	line-height: var(--leading-body);
}

/* ---------------- skip link ---------------- */

.skip-link {
	position: absolute;
	left: var(--space-2xs);
	top: var(--space-2xs);
	z-index: 10;
	padding: var(--space-2xs) var(--space-s);
	border-radius: var(--radius);
	background: var(--accent);
	color: var(--on-accent);
	transform: translateY(-200%);
}

.skip-link:focus {
	transform: translateY(0);
}

/* ---------------- focus ---------------- */

:focus-visible {
	outline: var(--focus-width) solid var(--focus-ring);
	outline-offset: var(--focus-offset);
}

/* ---------------- page frame ---------------- */

.page {
	max-width: var(--page-max);
	margin-inline: auto;
	padding-inline: var(--page-gutter);
}

.site-header,
.site-footer {
	border-block-end: var(--border-width) solid var(--border);
}

.site-footer {
	border-block: var(--border-width) solid var(--border);
	border-block-end: 0;
	margin-block-start: var(--space-xl);
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

.site-header > .page,
.site-footer > .page {
	padding-block: var(--space-s);
}

/* the mark and the nav share one row and wrap together on a narrow screen */
.site-header > .page {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--space-s);
}

/* the same file the browser already fetched for the tab, so the mark in
   the bar costs a line of markup and no request. It carries its own dark
   ground, which reads as a tile on the light theme and disappears into
   the dark one, leaving the mark either way */
.site-logo {
	display: flex;
	flex: none;
}

.site-logo img {
	display: block;
	width: 1.75rem;
	height: 1.75rem;
	border-radius: var(--radius);
}

/* the two legal lines sit at opposite ends of one row on a wide screen
   and stack on a narrow one. max-width is released because this row is
   furniture rather than prose, so the reading measure does not apply */
.site-footer__legal {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: var(--space-3xs) var(--space-m);
	max-width: none;
}

/* the nav takes the rest of the row and wraps INSIDE it, so a narrow
   screen does not spend a whole line on the mark alone */
.site-nav {
	flex: 1 1 0;
	min-width: 0;
}

.site-nav ul {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-s);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* a nav link carrying an icon. The icon takes the link's color, so it
   follows the theme and the hover state without tokens of its own */
.site-nav__link {
	display: inline-flex;
	align-items: center;
	gap: var(--space-3xs);
}

.site-nav__icon {
	flex: none;
}

main {
	padding-block: var(--space-l) var(--space-xl);
}

/* the nav grows to fill the row, so this sits hard against the right edge
   without a rule of its own. Ko-fi's coral rather than --accent: the
   primary action on the page is downloading a build, and a support button
   in the same blue would read as a second one of those */
.site-support {
	flex: none;
	display: inline-flex;
	align-items: center;
	gap: var(--space-3xs);
	padding: var(--space-3xs) var(--space-s);
	border-radius: var(--pill);
	background: var(--support);
	color: var(--on-support);
	font-size: var(--size-step--1);
	font-weight: 700;
	text-decoration: none;
	/* box-shadow: 0 2px 10px var(--support-glow); */
	transition: box-shadow 200ms ease;
}

/* the button does not move under the cursor; the glow widens instead, so
   the feedback is there without the target sliding out from under it */
.site-support:hover,
.site-support:focus-visible {
	/* box-shadow: 0 4px 22px var(--support-glow); */
	text-decoration: none;
}

.site-support__cup {
	flex: none;
}

/* the steam is the only thing moving at rest, at two wisps of one pixel
   over four seconds -- enough to catch an eye crossing the bar, small
   enough not to nag. The motion block at the foot of this file stops it
   for anyone who has asked for that */
.site-support__steam {
	transform-origin: 50% 100%;
	animation: support-steam 4s ease-in-out infinite;
}

@keyframes support-steam {
	0%,
	100% {
		opacity: 0.35;
		transform: translateY(0.5px) scaleY(0.85);
	}

	50% {
		opacity: 1;
		transform: translateY(-0.5px) scaleY(1.1);
	}
}

/* on a phone the mark and the button take the first row and the nav takes
   the whole of the second. Sharing one row with the button left the nav
   119px wide, which wrapped it to four lines and grew the header to 183px.
   Only the painting order moves; the tab order is the source order still */
@media (max-width: 40rem) {
	.site-nav {
		order: 1;
		flex-basis: 100%;
	}

	.site-support {
		margin-inline-start: auto;
	}
}

/* ---------------- headings and prose ---------------- */

h1,
h2,
h3 {
	line-height: var(--leading-tight);
	margin-block: 0 var(--space-s);
	text-wrap: balance;
}

h1 {
	font-size: var(--size-step-3);
}

h2 {
	font-size: var(--size-step-2);
	margin-block-start: var(--space-xl);
}

/* muted, so a subsection does not read as another section */
h3 {
	font-size: var(--size-step-1);
	margin-block-start: var(--space-m);
	color: var(--text-muted);
}

p,
li {
	max-width: var(--measure);
}

p {
	margin-block: 0 var(--space-s);
}

/* one topic to the next, and the only place that gap is stated -- a bordered
   box is a topic too. Not left to the opening heading: that heading is the
   section's :first-child, and the rule below zeroes it */
main section {
	margin-block-start: var(--space-xl);
}

/* a block that opens or closes its container states no outer margin:
   that edge belongs to the container's padding */
:is(h1, h2, h3, p, ul, ol, dl, section):first-child {
	margin-block-start: 0;
}

:is(h1, h2, h3, p, ul, ol, dl, section):last-child {
	margin-block-end: 0;
}

a {
	color: var(--accent);
}

/* the opening paragraph of a page, set slightly larger than body copy */
.lede {
	font-size: var(--size-step-1);
	color: var(--text-muted);
}

/* ---------------- the build panel ---------------- */

/* the panel owns the gaps between its rows, because :last-child cannot
   reach the row that is last on screen when hidden siblings follow it.
   minmax(0, 1fr) keeps the column from being pinned open by its
   contents, so the status text still wraps at 320px */
.build {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--space-s);
	padding: var(--space-m);
	border: var(--border-width) solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
}

.build > * {
	margin-block: 0;
}

/* the rows are wrapped so they hide as one, and a grid gap reaches only DIRECT
   children -- so the wrapper restates it. :not([hidden]) is load-bearing: an
   author `display` beats the UA sheet's [hidden] rule and would un-hide this */
.build__detail:not([hidden]) {
	display: grid;
	gap: var(--space-s);
}

.build__detail > * {
	margin-block: 0;
}

.build__status {
	color: var(--text-muted);
	/* the failure message embeds the index URL, which has no break
	   opportunity and pushes past a 320px viewport without this */
	overflow-wrap: anywhere;
}

.build__platforms {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-s);
}

.build__action {
	display: inline-flex;
	align-items: center;
	gap: var(--space-2xs);
	padding: var(--space-xs) var(--space-m);
	border: var(--border-width) solid transparent;
	border-radius: var(--radius);
	background: var(--accent);
	color: var(--on-accent);
	font: inherit;
	font-weight: 600;
	text-decoration: none;
}

.build__action:focus-visible,
.build__action:hover {
	text-decoration: underline;
}

/* a platform with no build is a disabled button rather than a styled
   span, so assistive technology is told it is unavailable instead of
   being offered something inert */
.build__action--soon {
	background: var(--surface);
	color: var(--text-muted);
	border-color: var(--border);
	cursor: default;
}

.build__action--soon:hover {
	text-decoration: none;
}

/* sized from the text, so it tracks the label rather than a fixed px */
.build__icon {
	width: 1em;
	height: 1em;
	flex: none;
}

.build__arch {
	font-weight: 400;
}

.build__note {
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

/* a description list that reflows to one column on a narrow screen */
.build__facts {
	display: grid;
	grid-template-columns: max-content minmax(0, 1fr);
	gap: var(--space-3xs) var(--space-s);
	/* the wrapper's gap owns the space above; grid-item margins add to a gap
	   rather than collapse with it */
	margin-block: 0;
}

.build__facts dt {
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

.build__facts dd {
	margin: 0;
	min-width: 0;
}

.build__facts dd code {
	font-family: var(--font-mono);
	font-size: var(--size-step--1);
	overflow-wrap: anywhere;
}

@media (max-width: 30rem) {
	.build__facts {
		grid-template-columns: minmax(0, 1fr);
	}

	.build__facts dd {
		margin-block-end: var(--space-2xs);
	}
}

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

/* the intrinsic width/height on the <img> or <video> reserves the row
   before the media arrives, so nothing below it jumps when it loads */
.shot {
	margin: var(--space-l) 0;
}

.shots {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
	gap: var(--space-m);
	align-items: start;
}

.shots .shot {
	margin: 0;
}

.shot figcaption {
	margin-block-start: var(--space-2xs);
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

/* the project a showcase figure shows, named in the paragraph above it */
.shot-subject {
	color: var(--text-muted);
}

.shot :is(img, video) {
	display: block;
	width: 100%;
	height: auto;
	border: var(--border-width) solid var(--border);
	border-radius: var(--radius);
}

/* ---------------- team ---------------- */

/* auto-fit rather than a stated number of columns, so the row reflows on
   a narrow screen without a breakpoint of its own */
.team {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(7rem, max-content));
	gap: var(--space-m);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* the whole card is the link, so the hit target is the picture and the
   name together rather than a few characters of text */
.team__member {
	display: grid;
	justify-items: center;
	gap: var(--space-3xs);
	text-align: center;
	text-decoration: none;
}

.team__member:hover .team__name,
.team__member:focus-visible .team__name {
	text-decoration: underline;
}

/* object-fit keeps a replacement that is not square from stretching; the
   circle is the only reason the site has a radius that is not --radius */
.team__avatar {
	display: block;
	width: 4.5rem;
	height: 4.5rem;
	border: var(--border-width) solid var(--border);
	border-radius: 50%;
	object-fit: cover;
}

.team__name {
	font-weight: 600;
}

/* what the person does, kept at body color so it reads before the muted
   line below it -- the role is the point of the card, the destination is
   a footnote on the link */
.team__role {
	color: var(--text);
	font-size: var(--size-step--1);
}

/* where the link goes, since the two members are not on the same site */
.team__where {
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

/* ---------------- notice ---------------- */

.notice {
	padding: var(--space-m);
	border: var(--border-width) solid var(--notice-border);
	border-radius: var(--radius);
	background: var(--notice-bg);
	color: var(--notice-text);
}

.notice a {
	color: inherit;
}

/* ---------------- work-in-progress badge ---------------- */

/* pinned to the viewport rather than to the page, so it stays put while
   reading. It takes no pointer events, so the art and the card cannot
   swallow a click meant for the page beneath them -- the link takes them
   back for itself. The interstitial is a modal <dialog>, which the top
   layer puts above this whatever the z-index says */
.wip-badge {
	position: fixed;
	inset-block-end: var(--space-s);
	inset-inline-start: var(--space-s);
	z-index: 5;
	display: flex;
	align-items: flex-start;
	gap: var(--tail-size);
	pointer-events: none;
}

/* the width is stated and the height follows, because the file is cropped
   to the drawing and its aspect ratio is not a number this file should
   carry. The attributes on the tag reserve the box before it arrives.

   Mirrored, because the drawing faces left and the bubble can only be on
   the right: the badge sits in the left corner, so a bubble on the other
   side would hang off the screen. Flipped, the muzzle is the edge nearest
   the tail, and the pair reads as speech rather than as a caption */
.wip-badge__art {
	display: block;
	width: 6.5rem;
	height: auto;
	transform: scaleX(-1);
}

.wip-badge__card {
	position: relative;
	max-width: 10rem;
	padding: var(--space-2xs) var(--space-xs);
	border: var(--border-width) solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	font-size: var(--size-step--1);
	line-height: var(--leading-tight);
}

/* the tail, as two triangles rather than one rotated square: the outer is
   the border, and the ground laid over it a border-width further in leaves
   the outline unbroken around the point. How far down the edge it sits is
   where the muzzle is, so the bubble comes off the mouth and not the mane */
.wip-badge__card::before,
.wip-badge__card::after {
	content: "";
	position: absolute;
	inset-block-start: 1.75rem;
	width: 0;
	height: 0;
	border-block: var(--tail-size) solid transparent;
	border-inline-end: var(--tail-size) solid var(--border);
}

.wip-badge__card::before {
	inset-inline-start: calc(-1 * (var(--tail-size) + var(--border-width)));
}

.wip-badge__card::after {
	inset-inline-start: calc(-1 * var(--tail-size));
	border-inline-end-color: var(--surface);
}

/* furniture rather than prose, so the reading measure does not apply */
.wip-badge__text {
	margin-block: 0 var(--space-3xs);
	max-width: none;
	color: var(--text-muted);
}

.wip-badge__link {
	pointer-events: auto;
	font-weight: 600;
}

/* a phone has no room for the full badge beside what it covers, so the
   art gives way first and the sentence and the link stay */
@media (max-width: 40rem) {
	.wip-badge {
		inset-block-end: var(--space-2xs);
		inset-inline-start: var(--space-2xs);
	}

	.wip-badge__art {
		width: 3.75rem;
	}

	/* the tail follows the smaller head up, the drawing having shrunk
	   while the bubble's own type did not */
	.wip-badge__card::before,
	.wip-badge__card::after {
		inset-block-start: 0.875rem;
	}

	.wip-badge__card {
		max-width: 10rem;
	}
}

/* ---------------- work-in-progress interstitial ---------------- */

/* the dialog paints the scrim itself and fills the viewport rather
   than relying on ::backdrop, which has not always inherited custom
   properties from the originating element */
.wip {
	position: fixed;
	inset: 0;
	width: 100%;
	max-width: 100%;
	height: 100%;
	max-height: 100%;
	margin: 0;
	padding: var(--space-s);
	border: 0;
	display: grid;
	place-items: center;
	overscroll-behavior: contain;
	background: var(--scrim);
	color: var(--text);
}

/* the dialog already paints the scrim across the whole viewport. Painting
   it here too composites the two, and two passes of the same translucent
   color leave the page behind all but opaque */
.wip::backdrop {
	background: transparent;
}

.wip__panel {
	position: relative;
	max-width: 34rem;
	padding: var(--space-m);
	border: var(--border-width) solid var(--border);
	border-radius: var(--radius);
	background: var(--bg);
	display: grid;
	gap: var(--space-s);
}

/* the panel takes focus on open so a reader starts at the notice rather
   than on Leave, whose Enter key leaves the site. Nothing reaches it by
   Tab, so it draws no ring: an outline offset outside the panel's own
   border reads as a second border */
.wip__panel:focus,
.wip__panel:focus-visible {
	outline: none;
}

.wip__panel > * {
	margin-block: 0;
}

.wip__title {
	font-size: var(--size-step-2);
}

/* what accepting does, in the panel's smaller voice */
.wip__note {
	color: var(--text-muted);
	font-size: var(--size-step--1);
}

.wip__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-s);
	justify-content: flex-end;
	margin-block-start: var(--space-2xs);
}

.wip__leave,
.wip__accept {
	padding: var(--space-xs) var(--space-m);
	border: var(--border-width) solid var(--border);
	border-radius: var(--radius);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
}

.wip__leave {
	background: var(--surface);
	color: var(--text);
}

.wip__leave:hover {
	text-decoration: underline;
}

/* held, an ordinary disabled control: a full-strength label on a ground
   a shade off --surface read as a second Leave. Unlocked, the accent
   button every other page wears -- the transition is what makes that a
   state change rather than a flicker */
.wip__accept {
	position: relative;
	overflow: hidden;
	display: grid;
	background: var(--accent);
	color: var(--on-accent);
	border-color: transparent;
	transition:
		background-color 160ms ease,
		color 160ms ease,
		border-color 160ms ease;
}

/* the hidden sizer and the live label share one grid cell, so the button
   is always as wide as its widest state and the row cannot reflow: the
   counter costs 24px and vanishes at the end, which shunted Leave sideways
   the moment the hold finished. tabular figures hold the label still
   inside that fixed width while the digit changes */
.wip__accept-sizer,
.wip__accept-label {
	grid-area: 1 / 1;
	justify-self: center;
	font-variant-numeric: tabular-nums;
}

.wip__accept-sizer {
	visibility: hidden;
}

.wip__accept:not(:disabled):hover {
	text-decoration: underline;
}

.wip__accept:disabled {
	background: var(--hold-track);
	color: var(--text-muted);
	border-color: var(--border);
	cursor: default;
}

/* a bar along the bottom edge, not a fill behind the label: no text has
   to stay legible over a moving edge, and it is already the color the
   button becomes. Its width comes from a frame callback rather than a CSS
   animation, which prefers-reduced-motion removes outright -- and the
   label counts down, so the wait is legible with no movement at all */
.wip__accept-fill {
	position: absolute;
	inset-block-end: 0;
	inset-inline-start: 0;
	width: 0;
	height: var(--hold-bar);
	background: var(--accent);
}

/* both are positioned, so source order puts the label over the bar */
.wip__accept-label {
	position: relative;
}

/* the announcement when the hold ends. The sighted cue is the button
   coming to life, so printing it in the panel said nothing twice */
.wip__status {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* the page behind the notice does not scroll. It costs no reflow: the rule
   on html above keeps the scrollbar's lane reserved either way */
html.wip-open {
	overflow: hidden;
}

/* ---------------- motion ---------------- */

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}
