/*
Stardust tutorials theme: a space HUD after StarCraft II: Legacy of the Void.

Frames are 96x96 SVGs in frames/, cut as 9-slices (border-image slice 24) so
their chamfered corners keep their shape at any size. Michroma sets display
type and HUD labels; Exo 2 sets everything else. Both are OFL licensed
(fonts/*-OFL.txt). The /theme page shows every component.
*/

@font-face {
	font-family: "Michroma";
	src: url("/assets/fonts/michroma-293991f2d789221189652d436a23918a6f18ced153b257fddda55bce970ceb25.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

/* Code, terminals and data; its programming ligatures (=>, !=, ::) are on. */
@font-face {
	font-family: "Fira Code";
	src: url("/assets/fonts/firacode-d1a7a65a3af91d30210a50920494dac01d56d453d2c0e2e57421251c2f084329.woff2") format("woff2");
	font-weight: 300 700;
	font-style: normal;
	font-display: swap;
}

/* Datastar's brand face, for the wordmark on the team card. Self-hosted like
every other face here: data-star.dev pulls it from Google Fonts, which would be
a third-party request on a page that makes none. */
@font-face {
	font-family: "Audiowide";
	src: url("/assets/fonts/audiowide-e21fd195dd9dcdafc5a0f162a8fc252703f3683179861afb057cd58f9d27dbe5.woff2") format("woff2");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Exo 2";
	src: url("/assets/fonts/exo2-27fcd2040d80e4db71edce62bd3fbcb97e5253b62582f7ddd3960efd6c009406.woff2") format("woff2");
	font-weight: 100 900;
	font-style: normal;
	font-display: swap;
}

@font-face {
	font-family: "Exo 2";
	src: url("/assets/fonts/exo2-italic-f882f28fd1536d21307b092a8b29e92ac18862cf7642194b89ad04647ea10e8e.woff2") format("woff2");
	font-weight: 100 900;
	font-style: italic;
	font-display: swap;
}

:root {
	color-scheme: dark;

	/* Every translucent tint in this sheet is rgb(var(--ch-*) / a%), so a theme
	redefines a channel once and each tint that uses it follows. The tokens
	below name colours; these name the channels those colours are mixed from. */
	--ch-void: 2 5 11;
	--ch-ink: 2 9 19;
	--ch-deep: 6 17 38;
	--ch-surface: 6 20 42;
	--ch-edge: 47 140 255;
	--ch-glow: 92 200 255;
	--ch-info: 98 210 255;
	--ch-prompt: 125 255 154;
	--ch-success: 111 224 168;
	--ch-action: 214 162 91;
	--ch-action-high: 255 170 60;
	--ch-warning: 255 193 77;
	--ch-danger: 255 107 107;
	--ch-paper: 255 255 255;
	--ch-shadow: 0 0 0;

	/* Code colours the hl- classes use that are not interface colours. */
	--color-code-string: #9fe6b8;
	--color-code-constant: #ff9f7a;
	--color-code-escape: #d7a6ff;
	--color-code-variable: #ff9d3c;
	--color-code-tag: #ff9fd0;
	/* VS Code's dark bracket colors, so DUST matches from editor to page. */
	--color-code-rainbow-1: #ffd700;
	--color-code-rainbow-2: #da70d6;
	--color-code-rainbow-3: #87cefa;

	/* Frame art. A theme swaps the file; the 9-slice geometry never changes. */
	--frame-src-modal: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3Cdefs%3E %3ClinearGradient id='rim' x1='0' y1='0' x2='0' y2='1'%3E %3Cstop offset='0' stop-color='%238fdcff'/%3E %3Cstop offset='.5' stop-color='%232f8cff'/%3E %3Cstop offset='1' stop-color='%231b56a8'/%3E %3C/linearGradient%3E %3Cfilter id='glow' x='-10%25' y='-10%25' width='120%25' height='120%25'%3E %3CfeGaussianBlur stdDeviation='2'/%3E %3C/filter%3E %3C/defs%3E %3Cpath fill='%23040b18' fill-opacity='.95' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%232f8cff' stroke-opacity='.75' stroke-width='5' filter='url(%23glow)' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%2301040a' stroke-width='3' d='M17 10h62l7 7v62l-7 7H17l-7-7V17z'/%3E %3Cpath fill='none' stroke='url(%23rim)' stroke-width='3' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%239fe0ff' stroke-opacity='.3' d='M18 13h60l5 5v60l-5 5H18l-5-5V18z'/%3E %3Cpath fill='%23bfe9ff' d='M6 16 16 6h6l-3 3h-2l-8 8v2l-3 3z'/%3E %3Cpath fill='%23bfe9ff' fill-opacity='.7' d='M90 80 80 90h-6l3-3h2l8-8v-2l3-3z'/%3E %3C/svg%3E") /* frames/modal.svg */;
	--frame-src-panel: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3Cpath fill='%2306142a' fill-opacity='.86' d='M14 2h68l12 12v68L82 94H14L2 82V14z'/%3E %3Cpath fill='none' stroke='%231f5fae' stroke-width='2' d='M14 3h68l11 11v68L82 93H14L3 82V14z'/%3E %3Cpath fill='none' stroke='%235cc8ff' stroke-width='2' d='M3 22v-8L14 3h8M74 3h8l11 11v8M93 74v8L82 93h-8M22 93h-8L3 82v-8'/%3E %3C/svg%3E") /* frames/panel.svg */;
	--frame-src-code-brackets: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- HUD targeting brackets at the corners, no edges between. The dim fill and its pattern come from pre.panel's background in style.css. --%3E %3Cpath fill='none' stroke='%235cc8ff' stroke-width='3' d='M2 20V2h18M76 2h18v18M94 76v18H76M20 94H2V76'/%3E %3Cpath fill='none' stroke='%232f8cff' stroke-opacity='.5' d='M9 14V9h5M82 9h5v5M87 82v5h-5M14 87H9v-5'/%3E %3C/svg%3E") /* frames/code-brackets.svg */;
	--frame-src-row: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3Cpath fill='%230a1a30' fill-opacity='.92' d='M10 4h76l6 6v76l-6 6H10l-6-6V10z'/%3E %3Cpath fill='none' stroke='%231f5fae' stroke-width='2' d='M10 5h76l5 5v76l-5 5H10l-5-5V10z'/%3E %3Cpath fill='%236f8399' d='M10 1h76l7 7v6h-3V9l-5-5H11L6 9v5H3V8z'/%3E %3Cpath fill='%23d7e3ee' fill-opacity='.55' d='M11 1h74v1H11z'/%3E %3Cpath fill='%232f8cff' fill-opacity='.35' d='M5 86v-8h1v8l4 4h8v1h-8z'/%3E %3C/svg%3E") /* frames/row.svg */;
	--frame-src-field: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3Cpath fill='%23030a14' d='M8 3h80l5 5v80l-5 5H8l-5-5V8z'/%3E %3Cpath fill='none' stroke='%23000' stroke-opacity='.6' stroke-width='4' d='M8 7h80l1 1v80l-1 1H8l-1-1V8z'/%3E %3Cpath fill='none' stroke='%232a73d6' stroke-width='3' d='M8 4h80l4 4v80l-4 4H8l-4-4V8z'/%3E %3Cpath fill='none' stroke='%238fdcff' stroke-opacity='.35' d='M9 2h78'/%3E %3C/svg%3E") /* frames/field.svg */;
	--frame-src-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='default-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23061a33'/%3E%3Cstop offset='.45' stop-color='%230f3160'/%3E%3Cstop offset='.62' stop-color='%230f3160'/%3E%3Cstop offset='1' stop-color='%23061a33'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23default-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%2301060d' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23default-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%239fd8ff' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/button.svg */;
	--frame-src-button-action: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='action-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%232a1708'/%3E%3Cstop offset='.45' stop-color='%236b3f17'/%3E%3Cstop offset='.62' stop-color='%236b3f17'/%3E%3Cstop offset='1' stop-color='%232a1708'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23action-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23120a03' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23action-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23ffd08a' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/button-action.svg */;
	--frame-src-button-success: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='success-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%230d2b20'/%3E%3Cstop offset='.45' stop-color='%2317513a'/%3E%3Cstop offset='.62' stop-color='%2317513a'/%3E%3Cstop offset='1' stop-color='%230d2b20'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23success-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%2304120c' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23success-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23a8ecd0' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/button-success.svg */;
	--frame-src-button-danger: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='danger-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23350d11'/%3E%3Cstop offset='.45' stop-color='%236b1a20'/%3E%3Cstop offset='.62' stop-color='%236b1a20'/%3E%3Cstop offset='1' stop-color='%23350d11'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23danger-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23140507' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23danger-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23ffb0ab' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/button-danger.svg */;
	--frame-src-tabs: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Tab strip: one frame around connected tabs, chamfered top-left and bottom-right like the LotV commander tabs. --%3E %3Cpath fill='%23040f20' fill-opacity='.92' d='M16 3h77v77L80 93H3V16z'/%3E %3Cpath fill='none' stroke='%232a73d6' stroke-width='2' d='M16 4h76v76L80 92H4V16z'/%3E %3Cpath fill='none' stroke='%238fdcff' stroke-opacity='.55' stroke-width='2' d='M4 22v-6L16 4h6M92 74v6L80 92h-6'/%3E %3C/svg%3E") /* frames/tabs.svg */;

	--color-void: #02050b;
	--color-deep: #061126;
	--color-line: #1f5fae;
	--color-line-low: rgb(var(--ch-edge) / 22%);
	--color-edge: #2f8cff;
	--color-glow: #5cc8ff;
	--color-text: #c9ddf2;
	--color-text-strong: #f2f8ff;
	--color-text-dim: #7d97b8;
	--color-sand: #fff4dc;
	--color-info: #62d2ff;
	/* The prototype site's action gold, which is also the Stardust logo's. */
	--color-action: #d6a25b;
	--color-action-high: #ffc76a;
	--color-success: #6fe0a8;
	--color-danger: #ff6b6b;
	--color-warning: #ffc14d;
	--color-prompt: #7dff9a;

	/* A control's face comes from its frame, so these name what has to agree
	with it: the label on it, the selected tab's lit segment and its rim, and
	what a hover and a press do to it. */
	--color-control-text: var(--color-text-strong);
	--color-action-label: var(--color-sand);
	/* The selected tab's face. Its lighter end is as light as it can be and
	still carry --color-control-text at WCAG AA: the label sits somewhere in
	the gradient, so the top of it, not the middle, is what has to pass. It
	used to start at #2f7fe0, which is 3.9:1 at that end. */
	--color-tab-face: linear-gradient(#2a6cc0, #0d3a78);
	--color-tab-rim: #9fe2ff;
	/* A dark face lights up under the pointer and lights up more under a
	press. The whole filter is the token, not a number, because a light face
	answers with colour instead (see the light theme). */
	--control-hover: brightness(1.3);
	--control-press: brightness(1.6);
	/* The unfilled part of a progress track, and the scanlines over an
	avatar: both sit under bright marks, so neither follows the surfaces. */
	--color-track: rgb(var(--ch-ink) / 90%);
	--color-scanline: rgb(var(--ch-shadow) / 30%);

	--font-display: "Michroma", "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
	--font-text: "Exo 2", system-ui, sans-serif;
	--font-data: "Fira Code", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

	--space-1: 0.25rem;
	--space-2: 0.5rem;
	--space-3: 0.75rem;
	--space-4: 1rem;
	--space-5: 1.5rem;
	--space-6: 2rem;
	--space-7: 3rem;
	--space-8: 4rem;

	/* Rendered width of a frame's 24 unit slice. */
	--frame-modal: 22px;
	--frame-panel: 18px;
	--frame-row: 14px;
	--frame-control: 10px;
	/* Buttons use the prototype's per-type frames, whose corner ornaments
	need a wider slice to read. */
	--frame-button: 0.75rem;

	--content-wide: 78rem;
	/* Match the topbar's one/two-row thresholds so anchors and sticky rails
	stay below the controls. The extra language control needs the wider row. */
	--topbar-height: 6.75rem;
	--motion-fast: 140ms;

	@media (min-width: 85rem) {
		--topbar-height: 3.5rem;
	}
}

:root:has(.topbar-two-toggles) {
	@media (max-width: 64rem) {
		--topbar-height: 10.5rem;
	}
	@media (min-width: 85rem) and (max-width: 111.99rem) {
		--topbar-height: 6.75rem;
	}
}

/*
The light theme. The glass turns to paper and the controls turn with it: a
button, a selected tab and a field take a pale face of their own hue, an ink
label, and a rim dark enough to bound them against the page. The art stays,
under a paper shade. Every pair here meets WCAG 2.1 AA for its job: 4.5:1 for
text, 3:1 for a control's boundary and for large display type, and
tests/browser/theme_switch.py measures that from the rendered pixels.

<theme-switch> (rocket/theme-switch.js) sets data-theme on <html>: light, or
dark for the dark theme, resolving "system" from prefers-color-scheme itself.
The attribute is always present, so this file needs no media query, and a
session without JavaScript keeps the dark theme it had before.
*/
:root[data-theme="light"] {
	color-scheme: light;

	--ch-void: 247 244 238;
	/* Surfaces that are near-black scrims in the dark theme. */
	--ch-ink: 253 252 249;
	--ch-deep: 236 231 221;
	--ch-surface: 255 255 255;
	/* The one white inset highlight, which has to darken on paper. */
	--ch-paper: 23 19 14;
	--ch-edge: 47 106 168;
	--ch-glow: 29 79 134;
	--ch-info: 20 83 143;
	--ch-prompt: 37 107 50;
	--ch-success: 28 100 59;
	--ch-action: 138 90 18;
	--ch-action-high: 122 74 0;
	--ch-warning: 122 82 0;
	--ch-danger: 163 34 24;
	--ch-shadow: 0 0 0;

	--color-void: #f7f4ee;
	--color-deep: #ece7dd;
	--color-line: #8a8377;
	--color-line-low: rgb(94 87 76 / 30%);
	--color-edge: #2f6aa8;
	--color-glow: #1d4f86;
	--color-text: #35302a;
	--color-text-strong: #17130e;
	--color-text-dim: #5e574c;
	--color-sand: #2a241b;
	--color-info: #14538f;
	--color-action: #8a5a12;
	--color-action-high: #7a4a00;
	/* Just dark enough to clear WCAG AA on the light code panel, which the
	previous green missed at 4.46. */
	--color-success: #1c643b;
	--color-danger: #a32218;
	--color-warning: #7a5200;
	--color-prompt: #256b32;

	--color-code-string: #1a6538;

	/* The diagram's service colours, dark enough for their labels to clear
	WCAG AA on paper. */
	--service-redis: #b83128;
	--service-postgres: #395cc5;
	--service-nats: #186b8e;
	--service-eventstore: #387133;
	--service-backblaze: #c61a24;
	--color-code-constant: #9a3d12;
	--color-code-escape: #6a34ad;
	--color-code-variable: #8a4a00;
	--color-code-tag: #9c2f6b;
	/* VS Code's light bracket colors, which hold AA on paper. */
	--color-code-rainbow-1: #0431fa;
	--color-code-rainbow-2: #319331;
	--color-code-rainbow-3: #7b3814;

	/* The controls are light too: a pale face of the type's own hue (the
	frames in frames/paper/), an ink label on it, and a hover that darkens
	the face instead of brightening it. */
	--color-control-text: #141b25;
	--color-action-label: #33220a;
	--color-tab-face: linear-gradient(#cfe1f6, #a6c6e8);
	--color-tab-rim: #1c5f9e;
	/* A light face has no room to brighten and must not darken under the
	hand: it deepens its colour instead, and the press adds a little light
	on top, so the control never reads as switched off while it is held. */
	--control-hover: saturate(1.5);
	--control-press: saturate(1.9) brightness(1.04);
	--color-track: rgb(var(--ch-shadow) / 14%);

	--frame-src-modal: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../modal.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3Cdefs%3E %3ClinearGradient id='rim' x1='0' y1='0' x2='0' y2='1'%3E %3Cstop offset='0' stop-color='%235e8fc4'/%3E %3Cstop offset='.5' stop-color='%232f6aa8'/%3E %3Cstop offset='1' stop-color='%2315406f'/%3E %3C/linearGradient%3E %3Cfilter id='glow' x='-10%25' y='-10%25' width='120%25' height='120%25'%3E %3CfeGaussianBlur stdDeviation='2'/%3E %3C/filter%3E %3C/defs%3E %3Cpath fill='%23fffefb' fill-opacity='.95' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-opacity='.75' stroke-width='5' filter='url(%23glow)' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%23cfc8bb' stroke-width='3' d='M17 10h62l7 7v62l-7 7H17l-7-7V17z'/%3E %3Cpath fill='none' stroke='url(%23rim)' stroke-width='3' d='M16 6h64l10 10v64L80 90H16L6 80V16z'/%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-opacity='.3' d='M18 13h60l5 5v60l-5 5H18l-5-5V18z'/%3E %3Cpath fill='%232f6aa8' d='M6 16 16 6h6l-3 3h-2l-8 8v2l-3 3z'/%3E %3Cpath fill='%232f6aa8' fill-opacity='.7' d='M90 80 80 90h-6l3-3h2l8-8v-2l3-3z'/%3E %3C/svg%3E") /* frames/paper/modal.svg */;
	--frame-src-panel: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../panel.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3Cpath fill='%23fffefb' fill-opacity='.86' d='M14 2h68l12 12v68L82 94H14L2 82V14z'/%3E %3Cpath fill='none' stroke='%238a8377' stroke-width='2' d='M14 3h68l11 11v68L82 93H14L3 82V14z'/%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-width='2' d='M3 22v-8L14 3h8M74 3h8l11 11v8M93 74v8L82 93h-8M22 93h-8L3 82v-8'/%3E %3C/svg%3E") /* frames/paper/panel.svg */;
	--frame-src-code-brackets: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../code-brackets.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- HUD targeting brackets at the corners, no edges between. The dim fill and its pattern come from pre.panel's background in style.css. --%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-width='3' d='M2 20V2h18M76 2h18v18M94 76v18H76M20 94H2V76'/%3E %3Cpath fill='none' stroke='%235e574c' stroke-opacity='.5' d='M9 14V9h5M82 9h5v5M87 82v5h-5M14 87H9v-5'/%3E %3C/svg%3E") /* frames/paper/code-brackets.svg */;
	--frame-src-row: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../row.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3Cpath fill='%23fffefb' fill-opacity='.92' d='M10 4h76l6 6v76l-6 6H10l-6-6V10z'/%3E %3Cpath fill='none' stroke='%238a8377' stroke-width='2' d='M10 5h76l5 5v76l-5 5H10l-5-5V10z'/%3E %3Cpath fill='%23b9b2a4' d='M10 1h76l7 7v6h-3V9l-5-5H11L6 9v5H3V8z'/%3E %3Cpath fill='%23ffffff' fill-opacity='.55' d='M11 1h74v1H11z'/%3E %3Cpath fill='%232f6aa8' fill-opacity='.35' d='M5 86v-8h1v8l4 4h8v1h-8z'/%3E %3C/svg%3E") /* frames/paper/row.svg */;
	--frame-src-field: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../field.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3Cpath fill='%23ffffff' d='M8 3h80l5 5v80l-5 5H8l-5-5V8z'/%3E %3Cpath fill='none' stroke='%235e574c' stroke-opacity='.6' stroke-width='4' d='M8 7h80l1 1v80l-1 1H8l-1-1V8z'/%3E %3Cpath fill='none' stroke='%235e574c' stroke-width='3' d='M8 4h80l4 4v80l-4 4H8l-4-4V8z'/%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-opacity='.35' d='M9 2h78'/%3E %3C/svg%3E") /* frames/paper/field.svg */;
	--frame-src-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../button.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='default-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23dfeaf8'/%3E%3Cstop offset='.45' stop-color='%23b7cfec'/%3E%3Cstop offset='.62' stop-color='%23b7cfec'/%3E%3Cstop offset='1' stop-color='%23dfeaf8'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23default-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23f7f4ee' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23default-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%231c5f9e' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/paper/button.svg */;
	--frame-src-button-action: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../button-action.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='action-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23f8e7c8'/%3E%3Cstop offset='.45' stop-color='%23e3c288'/%3E%3Cstop offset='.62' stop-color='%23e3c288'/%3E%3Cstop offset='1' stop-color='%23f8e7c8'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23action-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23f7f4ee' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23action-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%238a5a12' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/paper/button-action.svg */;
	--frame-src-button-success: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../button-success.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='success-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23daf1e4'/%3E%3Cstop offset='.45' stop-color='%23a6dcc1'/%3E%3Cstop offset='.62' stop-color='%23a6dcc1'/%3E%3Cstop offset='1' stop-color='%23daf1e4'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23success-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23f7f4ee' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23success-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%230c7245' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/paper/button-success.svg */;
	--frame-src-button-danger: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../button-danger.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- One diagonal cut, a lifted face and a hairline rim, after the mood boards. A vertical gradient survives the 9-slice, because every cell in a row stretches the same way. --%3E %3Cdefs%3E%3ClinearGradient id='danger-face' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23f9dcda'/%3E%3Cstop offset='.45' stop-color='%23eeb3ae'/%3E%3Cstop offset='.62' stop-color='%23eeb3ae'/%3E%3Cstop offset='1' stop-color='%23f9dcda'/%3E%3C/linearGradient%3E%3C/defs%3E %3Cpath fill='url(%23danger-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23f7f4ee' stroke-width='14' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='url(%23danger-face)' d='M23 7H89v66l-16 16H7V23z'/%3E %3Cpath fill='none' stroke='%23a3221a' stroke-width='3.0' d='M23 7H89v66l-16 16H7V23z'/%3E %3C/svg%3E") /* frames/paper/button-danger.svg */;
	--frame-src-tabs: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E %3C!-- Generated from ../tabs.svg for the light theme: see the frames/paper table in apps/web/DESIGN.md. Keep the geometry identical to the dark frame; only the colours differ. --%3E %3C!-- Tab strip: one frame around connected tabs, chamfered top-left and bottom-right like the LotV commander tabs. --%3E %3Cpath fill='%23fffefb' fill-opacity='.92' d='M16 3h77v77L80 93H3V16z'/%3E %3Cpath fill='none' stroke='%238a8377' stroke-width='2' d='M16 4h76v76L80 92H4V16z'/%3E %3Cpath fill='none' stroke='%232f6aa8' stroke-opacity='.55' stroke-width='2' d='M4 22v-6L16 4h6M92 74v6L80 92h-6'/%3E %3C/svg%3E") /* frames/paper/tabs.svg */;

	/* The art stays, under a paper shade instead of a black one: the same
	accretion behind the same glass, lit from the other side. It needs more
	of the shade than the dark theme does, because the art is dark and the
	text on it is not.

	The shade is rgb(var(--ch-void) / a%) in .page, so it already turned to
	paper with the channel; only the alphas change. */
	.page {
		--page-shade: rgb(var(--ch-void) / 93%);

		&.art-desert {
			--page-shade: rgb(var(--ch-void) / 86%);
		}
	}

	/* On paper a heading needs no glow, and the blue one reads as a smear. */
	h1 {
		text-shadow: none;
	}
}

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

html {
	background-color: var(--color-void);
	color: var(--color-text);
	font-family: var(--font-text);
	font-size: 17px;
	line-height: 1.55;
	scrollbar-color: var(--color-line) var(--color-void);
	/* An anchor scrolls into view instead of jumping, and lands below the
	sticky top bar. A section link in the rail, a heading link, the browser's
	own history and a link from another page all follow this. */
	scroll-behavior: smooth;
	scroll-padding-block-start: calc(var(--topbar-height) + var(--space-4));
}

body {
	min-block-size: 100dvh;
	background: var(--color-void);
}

:is(a, button, input, select, summary, [tabindex]):focus-visible {
	outline: 2px solid var(--color-glow);
	outline-offset: 3px;
}

a {
	color: var(--color-info);
	text-decoration-color: rgb(var(--ch-info) / 40%);
	text-underline-offset: 0.2em;

	&:hover {
		color: var(--color-text-strong);
	}
}

/*
Display type: the face, the tracking and the uppercasing the HUD wears.

Seventeen rules carried all three declarations. They are written once here, and
each of those rules keeps only its own --track, beside its size and colour. The
tracking stays with the rule because it is the part that varies -- with the
component, and on the top bar with the viewport -- and a media query can set a
custom property where it cannot read one.

This is a selector list and not a class, because a class would have to be added
to seventeen templates, which changes what the pages render. Inside a list each
selector keeps its own specificity, so every rule that takes the treatment back
off -- a label in a terminal caption, a file block's path, a Mermaid diagram's
label group -- still wins exactly as it did.
*/
h1,
h2,
h3,
h4,
blockquote > p:last-child:has(> strong:only-child) strong,
.kicker,
.label,
.topbar-brand,
.topbar-nav a,
.menu-summary,
.button,
.tab,
.copy-popover,
.briefing li,
.tree-badge,
.app-node,
.site-footer a,
.article-nav a,
.article-nav span,
.article-nav-category,
.article-pagination a,
.article-pagination span[aria-disabled],
.toggle-label,
th {
	font-family: var(--font-display);
	letter-spacing: var(--track);
	text-transform: uppercase;
}

h1,
h2,
h3,
h4 {
	--track: 0.04em;
	color: var(--color-sand);
	font-weight: 400;
	line-height: 1.2;
	overflow-wrap: anywhere;
}

h1 {
	font-size: clamp(1.8rem, 1.2rem + 2.4vw, 3rem);
	text-shadow: 0 0 18px rgb(var(--ch-glow) / 35%);
}

h2 {
	font-size: 1.2rem;
}

h3 {
	font-size: 0.9rem;
}

code,
pre {
	font-family: var(--font-data);
	font-size: 0.9em;
}

ul {
	padding-inline-start: 1.1rem;
}

li::marker {
	color: var(--color-info);
}

/* A last paragraph that is only bold text is the quote's credit. */
blockquote {
	position: relative;
	display: grid;
	gap: var(--space-3);
	border-inline-start: 3px solid var(--color-info);
	padding: var(--space-4) var(--space-5) var(--space-4) var(--space-7);
	background: linear-gradient(90deg, rgb(var(--ch-info) / 8%), transparent);
	color: var(--color-text-strong);
	font-size: 1.1rem;
	font-style: italic;

	&::before {
		content: "\201C";
		position: absolute;
		inset-block-start: var(--space-2);
		inset-inline-start: var(--space-3);
		color: var(--color-info);
		font-size: 3rem;
		font-style: normal;
		line-height: 1;
		opacity: 0.6;
	}

	> p:last-child:has(> strong:only-child) {
		text-align: end;

		strong {
			--track: 0.1em;
			color: var(--color-info);
			font-size: 0.7rem;
			font-style: normal;
			font-weight: 400;

			&::before {
				content: "\2014\00A0";
			}
		}
	}
}

/* Type roles */

.kicker,
.label {
	--track: 0.12em;
	font-size: 0.7rem;
	color: var(--color-info);
}

.label {
	color: var(--color-text-dim);
}

.dim {
	color: var(--color-text-dim);
}

.prompt {
	color: var(--color-prompt);
	font-family: var(--font-data);
	text-shadow: 0 0 6px rgb(var(--ch-prompt) / 45%);

}

/* The block cursor that blinks after a command, like a waiting terminal. Two
places wear it: a .prompt line, and the last line of a live terminal. */
.prompt::after,
.terminal.live .terminal-line:last-child::after {
	content: "";
	display: inline-block;
	inline-size: 0.6em;
	block-size: 1.1em;
	margin-inline-start: 0.2em;
	vertical-align: text-bottom;
	background: var(--color-prompt);
	box-shadow: 0 0 6px rgb(var(--ch-prompt) / 45%);
	animation: prompt-cursor 1.1s steps(1) infinite;

	/* The global reduced-motion rule only shortens animations, which would
	make an infinite blink flicker; the cursor stays solid instead. */
	@media (prefers-reduced-motion: reduce) {
		animation: none;
	}
}

@keyframes prompt-cursor {
	50% {
		opacity: 0;
	}
}

.stat {
	color: var(--color-text-strong);
	font-family: var(--font-display);
	font-size: 1.8rem;
	line-height: 1;
	text-align: center;
}

/* Layout */

/*
Pages sit on the prototype tutorials' accretion art, darkened by --page-shade
so text and frames stay readable. The page picks its art with art-desert or
art-closeup; the art itself, --page-art, is named in the backgrounds package's
page-art.css, so a binary without it (apps/talks) gets a plain void instead.
*/
.page {
	--page-shade: rgb(var(--ch-void) / 88%);

	display: flex;
	flex-direction: column;
	min-block-size: 100dvh;
	background-color: var(--color-void);
	background-image:
		linear-gradient(var(--page-shade), var(--page-shade)),
		var(--page-art, none);
	background-position: center;
	background-size: cover;
	background-attachment: fixed;

	&.art-desert {
		--page-shade: rgb(var(--ch-void) / 64%);
	}
}

.content {
	display: flex;
	flex-direction: column;
	gap: var(--space-7);
	inline-size: min(100% - 2rem, var(--content-wide));
	margin-inline: auto;
	padding-block: var(--space-7) var(--space-8);
}

/* The last control of a row, held against its far end: a row reads left to
right until this, which belongs to the row but not to the group before it. */
.push-end {
	margin-inline-start: auto;
}

.stack {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
}

.cluster {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--space-3);
}

/* A label and the control it names are one thing. The row they sit in wraps
   between pairs and never inside one, so a narrow window cannot leave "To" on
   a line by itself with its toggle on the next. */
.control-pair {
	flex-wrap: nowrap;
}

/* The result controls stay together at the end of the row. The output format
   is last, even when the JSON layout selector appears. */
.control-group {
	justify-content: flex-end;
}

/* Keep the selected format or JSON layout visible on narrow screens, where
   the top bar hides its own toggle labels. */
.dustfmt-page .toggle-label {
	display: grid;
}

.grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
	gap: var(--space-5);
}

.section-title {
	display: flex;
	align-items: center;
	gap: var(--space-3);

	&::after {
		flex: 1;
		border-block-start: 1px solid var(--color-line-low);
		content: "";
	}
}

/* Top bar */

/* The top bar stays in view; --topbar-height keeps sticky and anchored content below it. */
.topbar {
	position: sticky;
	inset-block-start: 0;
	z-index: 10;
	/* A narrow bar has two rows: the brand and the tools, then the links
	across the full width. The four-format bar needs 85rem for one row. */
	display: grid;
	/* The brand takes the width it needs and the tools take the rest. An auto
	tools column sizes to the row unwrapped, which leaves nothing for the brand
	and spills the controls over the wordmark instead of wrapping them. */
	grid-template-columns: auto minmax(0, 1fr);
	/* The first row grows past 3.5rem when it holds the format tabs, whose
	touch targets are taller on a narrow bar; --topbar-height covers both. */
	grid-template-rows: minmax(3.5rem, auto) 2.75rem;
	grid-template-areas:
		"brand tools"
		"nav nav";
	min-block-size: var(--topbar-height);
	border-block-end: 1px solid var(--color-line-low);
	/* Translucent glass: content scrolling under the bar is blurred, not hidden. */
	background: linear-gradient(rgb(var(--ch-deep) / 72%), rgb(var(--ch-void) / 58%));
	backdrop-filter: blur(8px) saturate(1.2);


	/* Six language marks and four format controls need 112rem for one row.
	Between the two it stays a grid, so the
	toggles keep the brand's row instead of wrapping under the links: a control
	that moves from below the links to above them as the window narrows is
	harder to find than one that never moves. */
	&.topbar-two-toggles {
		@media (min-width: 85rem) and (max-width: 111.99rem) {
			display: grid;
		}
	}

	@media (min-width: 85rem) {
		display: flex;
		align-items: stretch;
		/* The cells do not shrink -- a label that wrapped or a link that
		scrolled would be worse than a second row -- so a bar that carries
		every part at once (/theme: four links, a More menu, the format
		selector, an action and the switch) takes one. Every real page's bar
		fits on one row and is unchanged. */
		flex-wrap: wrap;
	}
}

.topbar-brand {
	grid-area: brand;
	display: flex;
	align-items: center;
	gap: var(--space-2);
	padding-inline: var(--space-3);
	color: var(--color-text-strong);
	--track: 0.1em;
	text-decoration: none;

	@media (min-width: 75rem) {
		padding-inline: var(--space-5);
	}
}

/* On the narrowest screens the brand shows only the logo; the name stays for
assistive technology. */
.topbar-name {
	@media (max-width: 30rem) {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}
}

/* The Stardust logo (assets/stardust-logo.svg, from the prototype tutorials)
drawn as a mask in the action gold, before the brand name. */
.topbar-logo {
	flex: none;
	inline-size: 1.75rem;
	block-size: 1.75rem;
	background-color: var(--color-action);
	mask: url("/assets/stardust-logo-26d20ffecf9b4974380794fb1bf9ccc63b27030b23d1fd26479991681d5ce89a.svg") center / contain no-repeat;
}

/* Page-wide controls at the end of the top bar: the theme switch, the format
selector, and the page's one action. */
.topbar-tools {
	grid-area: tools;
	display: flex;
	align-items: center;
	gap: var(--space-3);
	margin-inline-start: auto;
	padding-inline: var(--space-3);

	/* Both are hosts around a control, so they lay it out in the bar rather
	than boxing it. */
	theme-switch,
	.topbar-languages,
	.topbar-formats {
		display: flex;
		align-items: center;
		/* A toggle wraps whole. Squeezed instead, its strip would scroll its
		own options out of reach. */
		flex: none;
	}

	/* An empty host has no width: the island renders the button on upgrade,
	and the bar must not jump when it does. */
	theme-switch:not(:has(button)) {
		inline-size: 2.75rem;
	}

	/* A phone's bar holds the brand, the theme, the format selector and the
	page's action. The theme button is already as small as a touch target
	goes, so the action gives up its width first and wraps its label; if
	that is still not enough the tools wrap onto a second line, which the
	bar's first row (minmax(3.5rem, auto)) grows to hold. */
	@media (max-width: 74.99rem) {
		flex-wrap: wrap;
		justify-content: flex-end;
		row-gap: var(--space-2);

		/* On the narrowest bar the icon-only toggles, the logo and the theme
		switch are within a few pixels of one line. The gap closes rather than
		let them wrap, which would make the bar taller the narrower it gets. */
		@media (max-width: 30rem) {
			column-gap: var(--space-2);
			padding-inline: var(--space-2);
		}

		.button {
			flex-shrink: 1;
		}
	}

	@media (min-width: 75rem) {
		padding-inline: var(--space-5);
	}

	/* A narrow bar gives the action button only the room its label needs, so
	the brand and the tools share the row instead of the button filling it. */
	.button {
		min-inline-size: 0;
		font-size: 0.7rem;

		@media (min-width: 75rem) {
			min-inline-size: 9rem;
			font-size: 0.78rem;
		}
	}

	> .tabs {
		align-self: center;
	}
}

/*
The two panes are one grid, not two stacks side by side.

    dochead | reshead      auto
    doc     | res          1fr

The headings share the auto row and the bodies share the 1fr row, so the box
being typed into and the panel beside it are the same height because they are
the same grid row -- not because something under one of them was padded to
match. An earlier version reserved a row under each pane for the document's
status line, which cost both panes that height on every page view and left a
band of nothing along the bottom.

The status line lives beside its heading instead, where it costs no height at
all and is still next to the thing it describes.

One column on a phone, in reading order: heading, document, heading, result.
*/
.grid.panes {
	grid-template-columns: minmax(0, 1fr);
	grid-template-areas:
		"dochead"
		"doc"
		"reshead"
		"res";
	gap: var(--space-3) var(--space-5);
}

.pane-head-document {
	grid-area: dochead;
}

.pane-body-document {
	grid-area: doc;
}

.pane-head-result {
	grid-area: reshead;
}

.pane-body-result {
	grid-area: res;
}

.pane-head {
	display: flex;
	align-items: center;
	gap: var(--space-3);
	min-inline-size: 0;

	> .section-title {
		flex: 1;
		min-inline-size: 0;
	}
}

.pane-status:empty {
	display: none;
}

.pane-body {
	display: flex;
	flex-direction: column;
	min-inline-size: 0;
}

@media (min-width: 48rem) {
	.grid.panes {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		grid-template-areas:
			"dochead reshead"
			"doc res";
	}
}

/*
dustfmt fills the window, and the panes scroll rather than the page.

A tool is not an article: the document goes in one pane and comes out of the
other, and a reader who scrolls the page to see the result loses sight of what
they typed. So the page is exactly the window, the panes take the row that is
left, and each scrolls its own content.

Only where there is room for it. On a phone the panes are stacked, and a window
one screen tall cannot hold a heading, the controls and two useful panes; there
the page is an ordinary page and scrolls, which is the right answer on a phone.

min-block-size: 0 on every box down the chain, because a flex or grid item will
not shrink below its content without it, and one that will not shrink pushes
the pane it is in off the bottom of the window.
*/
@media (min-width: 48rem) and (min-height: 40rem) {
	.dustfmt-page {
		block-size: 100dvb;
		min-block-size: 0;
		overflow: hidden;
	}

	/* The panes take the room that is left, with a hairline of space under
	them so they do not sit flush against the bottom of the window. It is
	padding for the eye only: the height it costs comes off the panes, which
	is why it is the smallest step rather than the page's usual one. */
	.dustfmt-page > .content {
		flex: 1;
		min-block-size: 0;
		gap: var(--space-5);
		padding-block: var(--space-5) var(--space-3);
	}

	.dustfmt-page .tool-panes {
		flex: 1;
		min-block-size: 0;
	}

	/* The heading row takes what it needs; the bodies take the rest. */
	.dustfmt-page .grid.panes {
		flex: 1;
		min-block-size: 0;
		grid-template-rows: auto minmax(0, 1fr);
	}

	.dustfmt-page .pane-body {
		min-block-size: 0;
	}

	/* The box the document is typed into fills its cell, and offers no drag
	handle that would fight the layout. */
	.dustfmt-page textarea.field {
		flex: 1;
		/* The field caps itself at 60dvh, so that a box on an ordinary page
		cannot run away down the screen. Here the box IS the screen: its cell
		is the grid row, and the cap stopped it filling that row on any window
		taller than about 1200px -- 90px short at 1440, and the taller the
		window the worse, which is why it looked fine until it did not. */
		max-block-size: none;
		min-block-size: 0;
		resize: none;
	}

	/* The result is a figure holding a panel, or an alert; whichever it is
	fills the cell, and the panel is what scrolls. */
	.dustfmt-page .pane-body-result > * {
		flex: 1;
		min-block-size: 0;
	}

	.dustfmt-page .pane-body-result pre.panel {
		flex: 1;
		min-block-size: 0;
		overflow: auto;
	}
}

/*
How the home page breaks its lines.

Headings, labels and one-line statements balance: they are short, they are read
at a glance, and one word alone on the last line of a heading reads as a
mistake before it reads as a sentence.

Paragraphs do not. Balance was tried on them and it looks wrong -- worst in the
team card, where a column of three-line paragraphs each cut to an even width
reads as a block of shapes rather than as prose. Evening the lines of running
text draws attention to the shape of the paragraph, which is the opposite of
what running text is for.

So paragraphs get pretty instead, which leaves the ragging alone and only
refuses to leave the last line a stub. Nothing here reaches a code panel:
text-wrap has no effect where white-space is pre.
*/
.site-home {
	:is(h1, h2, h3, h4, figcaption, summary, .kicker, .label, .lede, .section-title, .team-lede) {
		text-wrap: balance;
	}

	p,
	li,
	blockquote {
		text-wrap: pretty;
	}
}

/*
Moving between pages.

Every page here is a document the server renders whole, and the format tabs,
the article rail and the pagination are all ordinary links. That is the right
model -- it works without JavaScript, every page has its own address, and a
format can be sent to someone -- but it means a switch cuts hard from one
document to the next.

Cross-document view transitions let the browser animate between two ordinary
page loads. No script, no single-page app, no second copy of the document in
the markup: a browser that does not support it simply cuts, as it did before.

What is named here holds still while the rest changes. The bar and the rail are
the same object on both sides of a format switch, and cross-fading them against
themselves is a flicker for no reason; naming them tells the browser they are
the same thing in a new position. The article is deliberately not named, so it
is the part that fades.
*/
@view-transition {
	navigation: auto;
}

.topbar {
	view-transition-name: site-bar;
}

.article-nav {
	view-transition-name: article-rail;
}

::view-transition-old(root),
::view-transition-new(root) {
	animation-duration: 180ms;
}

/* A reader who asked for less motion gets the cut they had before. */
@media (prefers-reduced-motion: reduce) {
	@view-transition {
		navigation: none;
	}
}

/* Text only for assistive technology, such as a role="status" announcement. */
.visually-hidden {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* No scroll container here: it would clip the menu panel that drops out of the
bar. A narrow bar wraps instead. */
.topbar-nav {
	grid-area: nav;
	display: flex;
	min-inline-size: 0;
	border-block-start: 1px solid var(--color-line-low);

	@media (min-width: 75rem) {
		border-block-start: 0;
	}

	a {
		display: flex;
		align-items: center;
		/* Four cells share a phone's bar: a tight gutter keeps every label,
		and the More caret, inside it. */
		padding-inline: var(--space-1);
		border-inline-start: 1px solid var(--color-line-low);
		color: var(--color-text);
		/* No tracking and the smallest label on a phone. The bar carries four
		cells, and the longest set is the documentation's -- Tutorials, Theory,
		Reference, More -- which needs about 13px more than a 320px bar has at
		the sizes the shorter home set fits in. Tracking is the first thing to
		give up: it costs 0.04em on every character of every label, which is
		more than the shortfall, and nobody reads a bar label by its tracking.
		A clipped label is the alternative, and "Referen" is not a word. */
		--track: 0;
		font-size: 0.62rem;
		text-decoration: none;
		white-space: nowrap;

		/* A 320px bar is the narrowest phone still sold, and the
		documentation's four labels do not fit it even without tracking. */
		@media (max-width: 21.99rem) {
			font-size: 0.56rem;
		}

		/* Past the widest failing bar, the label gets its air back. */
		@media (min-width: 34rem) {
			--track: 0.06em;
			font-size: 0.7rem;
		}

		@media (min-width: 75rem) {
			--track: 0.08em;
			padding-inline: var(--space-5);
			font-size: 0.8rem;
		}

		&:hover {
			background: rgb(var(--ch-edge) / 8%);
			color: var(--color-text-strong);
		}

		&[aria-current="page"] {
			background: linear-gradient(transparent 50%, rgb(var(--ch-edge) / 24%));
			box-shadow: inset 0 -2px 0 var(--color-glow);
			color: var(--color-text-strong);
		}
	}

	/* On a narrow bar the top-level links share the row. */
	& > a,
	& > .menu {
		flex: 1;
		min-inline-size: 0;

		@media (min-width: 75rem) {
			flex: none;
		}
	}

	& > a {
		justify-content: center;
	}

	& > :first-child,
	& > .menu:first-child > .menu-summary {
		border-inline-start: 0;

		@media (min-width: 75rem) {
			border-inline-start: 1px solid var(--color-line-low);
		}
	}

	& > .menu > .menu-summary {
		flex: 1;
		justify-content: center;
		gap: var(--space-1);
		padding-inline: var(--space-1);
		font-size: 0.66rem;
		letter-spacing: 0.04em;

		@media (min-width: 22rem) {
			font-size: 0.7rem;
			letter-spacing: 0.06em;
		}

		@media (min-width: 75rem) {
			flex: none;
			gap: var(--space-2);
			padding-inline: var(--space-5);
			font-size: 0.8rem;
			letter-spacing: 0.08em;
		}
	}
}

/* 9-slice frames */

.modal,
.panel,
.row,
.field,
.button {
	border-style: solid;
	border-color: transparent;
	background: none;
}

.modal {
	border-width: var(--frame-modal);
	border-image: var(--frame-src-modal) 24 fill / var(--frame-modal) stretch;
	padding: var(--space-3) var(--space-4);
	filter: drop-shadow(0 0 24px rgb(var(--ch-edge) / 22%));
}

.panel {
	border-width: var(--frame-panel);
	border-image: var(--frame-src-panel) 24 fill / var(--frame-panel) stretch;
	padding: var(--space-2) var(--space-3);
}

/* Code panels are HUD targeting brackets over a faint hex grid on a
translucent fill, not glass panels. */
/* A panel holding a sentence rather than code.

Code earns a sideways scroll: a line of it is one whole thing, and breaking it
changes what it says. A sentence earns nothing of the sort, and the result
panel's empty state was one line of prose in a pre -- so the page opened with a
scrollbar under a message that was itself cut off at the edge. It wraps. */
pre.panel.panel-note {
	white-space: pre-wrap;
}

pre.panel {
	font-variant-ligatures: common-ligatures contextual;
	overflow-x: auto;
	border-image: var(--frame-src-code-brackets) 24 / var(--frame-panel) stretch;
	/* The bracket border already insets the code; keep only a little more. */
	padding: var(--space-1) var(--space-2);
	background:
		url("/assets/patterns/hex-c2c83d179d58aa11ca11bda559153a3287e40b548ec4d5ad47eba64de9bdb381.svg") 0 0 / 24px auto,
		rgb(var(--ch-ink) / 82%);
	color: var(--color-text-strong);
	font-size: 0.85rem;
	line-height: 1.6;
}

.row {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: center;
	gap: var(--space-4);
	border-width: var(--frame-row);
	border-image: var(--frame-src-row) 24 fill / var(--frame-row) stretch;
	padding: var(--space-2) var(--space-3);
}

/*
A split is the theme's one two-column layout: something beside a sidecar --
an article beside its rail, an argument beside its diagram, a hero beside its
console. It is one column on a narrow screen and two from 60rem.

--split-side is the second column's track and --split-main the first's, so a
split says what its columns are and nothing about when they appear. A track is
either a cap (minmax(0, 28rem)) or a share (minmax(0, 1.3fr)); the first column
starts at minmax(0, 1fr), which is what stops a long line of code from making
the column, and the page, wider than the viewport.

Every split turns at the same width because a media query cannot read a custom
property -- there is no @media (min-width: var(--split)) -- so one utility has
to pick one value, and it is 60rem. Before this the eight splits turned
at 44rem, 60rem and 68rem; they all turn at 60rem now. The gap and the vertical
alignment stay with each split, because those are what it looks like, not when
it turns.

It sits here, after .row, rather than with .stack and .cluster: a price row is
both, taking its frame from .row and its columns from .split, and .row is a
grid of its own. Neither class outweighs the other, so the later rule wins, and
the later rule has to be this one. A component grid added below here and then
combined with .split would take the columns back.
*/
.split {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--space-6);

	@media (min-width: 60rem) {
		grid-template-columns: var(--split-main, minmax(0, 1fr)) var(--split-side, minmax(0, 1fr));
	}
}

.field {
	min-block-size: 2.6rem;
	border-width: var(--frame-control);
	border-image: var(--frame-src-field) 24 fill / var(--frame-control) stretch;
	padding: 0 var(--space-2);
	color: var(--color-text-strong);
	font: inherit;

	&::placeholder {
		color: var(--color-text-dim);
	}
}

/* A document the visitor writes: a field that holds code, so it takes the
data font and grows with what is pasted into it, between a few lines and a
screenful. */
textarea.field {
	display: block;
	inline-size: 100%;
	min-block-size: 8lh;
	max-block-size: 60dvh;
	padding: var(--space-2);
	font-family: var(--font-data);
	font-size: 0.9rem;
	line-height: 1.5;
	tab-size: 2;
	resize: vertical;
}

select.field {
	appearance: none;
	padding-inline-end: 2rem;
	background-image:
		linear-gradient(45deg, transparent 50%, var(--color-glow) 50%),
		linear-gradient(135deg, var(--color-glow) 50%, transparent 50%);
	background-position:
		calc(100% - 0.95rem) 50%,
		calc(100% - 0.55rem) 50%;
	background-size: 0.4rem 0.4rem;
	background-repeat: no-repeat;

	/* The option list is the browser's own popup: without these it keeps a
	light background under the field's light text. */
	option {
		background-color: var(--color-deep);
		color: var(--color-text);
	}

	option:checked {
		background-color: var(--color-line);
		color: var(--color-text-strong);
	}
}

/*
Each button type has its own frame, from the prototype tutorials: gold action
with notched corner bars, blue default with double chevrons, green success
with brackets and a check, red danger with brackets and diamonds. Buttons
brighten and glow with CSS filters: more on hover, most while pressed. The
action button glows gold, the rest blue.
*/
.button {
	--track: 0.08em;
	/* The glow a point turns on: a variant names its channel, how far the
	glow reaches and how strong it is, so the drop-shadow itself is written
	once. A press is the same distance and strength on every variant, so only
	the channel follows it. */
	--button-glow: var(--ch-glow);
	--button-glow-size: 8px;
	--button-glow-alpha: 45%;
	display: inline-flex;
	flex-shrink: 0;
	align-items: center;
	justify-content: center;
	gap: var(--space-2);
	min-block-size: 2.75rem;
	/* A long label wraps inside the frame on a phone instead of pushing the
	button past its column; a button with room to spare is unchanged. */
	min-inline-size: min(9rem, 100%);
	max-inline-size: 100%;
	border-width: var(--frame-button);
	border-image: var(--frame-src-button) 24 fill / var(--frame-button) stretch;
	padding: 0 var(--space-3);
	color: var(--color-control-text);
	font-size: 0.78rem;
	text-align: center;
	text-decoration: none;
	text-wrap: balance;
	/* A label on a control is not text to select, so a drag or a double click
	presses the button instead of highlighting its word. */
	user-select: none;
	cursor: pointer;
	transition:
		filter var(--motion-fast),
		transform var(--motion-fast);

	&:hover:not(:disabled) {
		filter: var(--control-hover)
			drop-shadow(0 0 var(--button-glow-size) rgb(var(--button-glow) / var(--button-glow-alpha)));
	}

	&:active:not(:disabled) {
		filter: var(--control-press) drop-shadow(0 0 14px rgb(var(--button-glow) / 65%));
		transform: translateY(1px);
	}

	&:disabled {
		cursor: not-allowed;
		filter: grayscale(0.85) brightness(0.65);
	}

	/* The theme button carries an icon and no words, so it is square: the
	frame's chamfers need the same room on both sides. */
	&.theme-button {
		min-inline-size: 2.75rem;
		padding-inline: 0;

		.icon {
			inline-size: 1.35rem;
			block-size: 1.35rem;
		}
	}

	&.action {
		--button-glow: var(--ch-action-high);
		--button-glow-size: 10px;
		--button-glow-alpha: 50%;
		border-image-source: var(--frame-src-button-action);
		color: var(--color-action-label);
	}

	&.success {
		--button-glow: var(--ch-success);
		--button-glow-size: 10px;
		--button-glow-alpha: 50%;
		border-image-source: var(--frame-src-button-success);
	}

	&.danger {
		--button-glow: var(--ch-danger);
		--button-glow-size: 10px;
		--button-glow-alpha: 50%;
		border-image-source: var(--frame-src-button-danger);
	}

	/* A button that started a request keeps .loading until the next frame from
	the server draws the result: a dim label behind a sweeping scan line, and no
	second press. */
	&.loading {
		position: relative;
		overflow: hidden;
		cursor: progress;
		filter: grayscale(0.4) brightness(0.8);
		pointer-events: none;

		&::after {
			position: absolute;
			inset-block: 0;
			inset-inline-start: 0;
			animation: button-loading 900ms linear infinite;
			background: linear-gradient(90deg, transparent, var(--color-info), transparent);
			content: "";
			inline-size: 40%;
			opacity: 0.55;
		}
	}
}

@keyframes button-loading {
	from {
		transform: translateX(-100%);
	}

	to {
		transform: translateX(350%);
	}
}

/*
Tabs are connected: one 9-slice strip (frames/tabs.svg) holds every tab, a
slanted divider separates neighbours, and the selected tab lights a slanted
segment that the strip clips at its ends.
*/
.tabs {
	display: inline-flex;
	align-self: flex-start;
	/* The lit segment of a tab is wider than the tab, because a skewed
	rectangle needs the room. The gap holds that width, so a selected tab
	stops short of the tab beside it. */
	gap: 0.5rem;
	max-inline-size: 100%;
	border: var(--frame-control) solid transparent;
	border-image: var(--frame-src-tabs) 24 fill / var(--frame-control) stretch;
	/* More tabs than the strip can show scroll inside it: clipped tabs would
	be unreachable on a phone. The bar has no room for a scrollbar, and the
	strip still clips the lit segment at its ends. */
	overflow-x: auto;
	overflow-y: hidden;
	scrollbar-width: none;

	@media (min-width: 60rem) {
		overflow: hidden;
	}
}

/* A toggle is an icon strip with its label inside the frame, after the options.
The options carry no text, so the label is what names the choice: it reads the
chosen one rather than the control. */
.toggle {
	display: inline-flex;
	align-items: center;

	> .tabs {
		align-self: center;
		margin-block-end: 0;
	}

	/* An icon-only tab is square: without a label to set its width, the
	padding that a labelled tab uses would leave a lopsided box. It holds that
	one height at every width, unlike a tab in the page, which grows to a touch
	target below 60rem: this one lives in the bar, where growing as the window
	crossed that width would change the control's shape under the reader. */
	.tab {
		justify-content: center;
		min-inline-size: 2.5rem;
		min-block-size: 2rem;
		padding-inline: var(--space-2);
	}
}

/* The label rides inside the strip's frame, after the last option. It is not a
tab, so it takes the strip's padding but none of a tab's segment or divider. */
.toggle-label {
	--track: 0.12em;
	display: grid;
	align-self: center;
	padding-inline: var(--space-2);
	font-size: 0.7rem;
	color: var(--color-text-dim);
	/* Centred in the width the widest option reserves, so a short name sits
	under the middle of the label rather than adrift at its start. */
	text-align: center;
	white-space: nowrap;

	/* The icons stand alone once the labelled toggles stop sharing a line. Two
	of them and the brand need about 53rem; below that they used to wrap, which
	doubled the height of the bar's tools and made the bar taller as the window
	narrowed. Dropping the label keeps them on one line instead, and the
	role="status" beside the toggle still announces the choice. */
	@media (max-width: 52.99rem) {
		display: none;
	}

	/* The value and a hidden copy of the longest option share one cell, so the
	label is always as wide as the widest choice and switching cannot move the
	bar. The copy comes from the same registry the options do, so registering a
	longer language name widens the label with it. */
	> * {
		grid-area: 1 / 1;
	}

	> .toggle-sizer {
		visibility: hidden;
	}
}


.tab {
	position: relative;

	/* A tab is a <button> in the SDK language group and an <a> in the format
	selector, which is a link because the format is part of the URL. */
	text-decoration: none;
	isolation: isolate;
	display: inline-flex;
	align-items: center;
	/* A tab is a touch target on a phone and a compact HUD control on a
	pointer screen. */
	min-block-size: 2.75rem;
	border: 0;
	padding: 0 var(--space-4);
	background: none;
	color: var(--color-text);
	--track: 0.1em;
	font-size: 0.7rem;
	white-space: nowrap;
	user-select: none;
	cursor: pointer;

	@media (min-width: 60rem) {
		min-block-size: 2rem;
	}

	/* The slanted segment behind the label. */
	&::before {
		position: absolute;
		z-index: -1;
		inset: -2px -0.4rem;
		border: 1px solid transparent;
		transform: skewX(-24deg);
		transition:
			background var(--motion-fast),
			border-color var(--motion-fast);
		content: "";
	}

	/* The slanted divider from the previous tab, in the middle of the gap. */
	& + .tab::after {
		position: absolute;
		inset-block: 0.35rem;
		inset-inline-start: -0.25rem;
		border-inline-start: 1px solid var(--color-line);
		transform: skewX(-24deg);
		content: "";
	}

	&:hover::before {
		background: rgb(var(--ch-edge) / 14%);
	}

	/* A toggle strip (role="group", aria-pressed buttons) lights the same way,
	and so does a strip of links that choose a document format, whose own page
	is marked aria-current. */
	&:is([aria-selected="true"], [aria-pressed="true"], [aria-current="true"]) {
		color: var(--color-control-text);

		&::before {
			border-color: var(--color-tab-rim);
			background: var(--color-tab-face);
			box-shadow: 0 0 10px rgb(var(--ch-glow) / 45%);
		}

		/* The selected segment covers its own divider and the next one. */
		&::after,
		& + .tab::after {
			border-color: transparent;
		}
	}
}

/*
Icons come from game-icons.net (CC BY 3.0, icons/game-icons-LICENSE.txt),
vendored as SVGs in icons/ and drawn as masks so they take the text color:
<span class="icon icon-rocket" aria-hidden="true"></span>. Add a class below
for each icon you vendor.
*/
.icon {
	display: inline-block;
	flex: none;
	inline-size: 1.25em;
	block-size: 1.25em;
	background-color: currentcolor;
	mask: var(--icon) center / contain no-repeat;
}

.icon-info {
	--icon: url("/assets/icons/info-4a09577956195ed8c668eef03c9faa1cdc60590630f62df1de9084e182573e65.svg");
}

.icon-hazard-sign {
	--icon: url("/assets/icons/hazard-sign-f9384e466c6d9bdba25c8b09557f31d69d4dd158494bd5bd3f5dfa4621002b31.svg");
}

.icon-skull-crossed-bones {
	--icon: url("/assets/icons/skull-crossed-bones-2a335353a65576afdcec90b8d9af1ff269c39eb35e91a04a3067db098fe3e800.svg");
}

.icon-rocket {
	--icon: url("/assets/icons/rocket-9fe345fcbbfb3be124e0785f62d3c4e70ae5942a75570ef7c67392d3ac0c77f6.svg");
}

.icon-ringed-planet {
	--icon: url("/assets/icons/ringed-planet-1c6e1599e40aab7febfbcf3a70f18ab2d8f4f5c3e7e0b6050b2f06ed418de263.svg");
}

.icon-radar-sweep {
	--icon: url("/assets/icons/radar-sweep-375ebfe11608197984c05c27cf2b33f6e2489f03f54f9a965d7f77b2d47b4a6b.svg");
}

.icon-processor {
	--icon: url("/assets/icons/processor-3eccc14edee9aa462d251a32d4aa80b3304600ea6cd73893d4d5d8db111223bb.svg");
}

.icon-open-book {
	--icon: url("/assets/icons/open-book-915ddcf24eaa809ee9fafe63c8b9295d8bd4156e1868d0a827946ec1ed0e40c2.svg");
}

.icon-trophy {
	--icon: url("/assets/icons/trophy-dddccb09640bdb679c340de74a477eb3a128043a1242b4bd8f1ccb022e1c6c44.svg");
}

.icon-padlock {
	--icon: url("/assets/icons/padlock-4fc29f4e02fd3add4164c91c717df5e7c0cd2596ba9ef87792c0606c189d2aee.svg");
}

.icon-cog {
	--icon: url("/assets/icons/cog-f13a1b21ebc0f0c700d00bb114efe387e95d44bce8ba63888045a6d84358721b.svg");
}

/* The colour theme's three stops, on the one button that cycles them. */
.icon-sun {
	--icon: url("/assets/icons/sun-956b4c3127f67b3bdd7658d6ae64f8131d9053a3f758b846b64ade1ce62caa9a.svg");
}

.icon-moon {
	--icon: url("/assets/icons/moon-172da9b7bf0fe558c2dcccc3e9df2b0b63bafc486b3b47b5e93005cfd3b455c5.svg");
}

.icon-eclipse {
	--icon: url("/assets/icons/eclipse-5fd8861c89e5d254a99eada5dc62ef3c591bd60162388f6f3fa8507ed8feec25.svg");
}

.icon-magnifying-glass {
	--icon: url("/assets/icons/magnifying-glass-154ffdd4b41ac31b03f0e10fa66ca841aba8eedfff3b153f263929ad11ac5f1b.svg");
}

.icon-content-copy-outline {
	--icon: url("/assets/icons/content-copy-outline-1a433a5231f994ef3d57fd90f0ddbdafa63144ca9a36ce4cac05b556c52519a8.svg");
}

.icon-check-mark {
	--icon: url("/assets/icons/check-mark-1a839f00340eeb68a4db830ad87b3a7f45e936b2c1bda74931d89c29dbd2c0b1.svg");
}

.icon-fast-forward-button {
	--icon: url("/assets/icons/fast-forward-button-a7e20e2e631ca34f69b8e1869a51dce4139b3fa13eb29c30c33f82c73750354d.svg");
}

.icon-play-button {
	--icon: url("/assets/icons/play-button-0e33b61efa9cf8fd56179636fb28757fa53e138ccfe039e3b957354864505d7f.svg");
}

.icon-abacus {
	--icon: url("/assets/icons/abacus-b766bcefb31cf654038aad2f8b3d9bc777b974aaea4e41291d44b84eb08f534f.svg");
}

.icon-astronaut-helmet {
	--icon: url("/assets/icons/astronaut-helmet-d135f3292b78318eb83df62a7f6af53eb9f3d2ea01693acfed51de413ca0b946.svg");
}

.icon-spaceship {
	--icon: url("/assets/icons/spaceship-3f8c395fb74ac68c81799cc9d7aa63cd86c5f14282b5885cdc4393bc6643751b.svg");
}

.icon-defense-satellite {
	--icon: url("/assets/icons/defense-satellite-21d3d74b098f9e8540dcd6b95c073fbea21425e53adfc98064c4d66974d86dda.svg");
}

.icon-star-formation {
	--icon: url("/assets/icons/star-formation-3415a4e149bf25c04d7081309ad4e9ea332f7671b65f9cfec7244dbb1ba5fc21.svg");
}

/* The document formats. DUST reuses star-formation: the same mark the VS Code
   extension used for a .dust file, so the toggle and the file agree. */
.icon-dust {
	--icon: url("/assets/icons/star-formation-3415a4e149bf25c04d7081309ad4e9ea332f7671b65f9cfec7244dbb1ba5fc21.svg");
}

.icon-json {
	--icon: url("/assets/icons/json-braces-e339c4b0f09b38cb11769844895069a4fbfdb48e417001c4cd81404d9fc9eb78.svg");
}

/* Material Symbols show compact JSON as one line and pretty JSON as several. */
.icon-contract {
	--icon: url("/assets/icons/contract-d2e33580fdc156448ed0c31a7ed8e9ac529cc4b1024cc343ed695f3efbe699fe.svg");
}

.icon-expand {
	--icon: url("/assets/icons/expand-c9ee130c9384c26038ff31afc66796cff9aed5d5fd9bd322cc158d5f309c87e1.svg");
}

/* The reader with no picture (pixelarticons, MIT), and the sign-in providers
   (Simple Icons, CC0), as masks like the interface icons. */
.icon-user {
	--icon: url("/assets/icons/pixelarticons-user-fef1d73c7c476f0e8af33880ec5f39c04c3642b2a7157f531543f374cfb34995.svg");
}

.icon-github {
	--icon: url("/assets/icons/simple-icons-github-e563cfa0d526656ba7d7ae3861fe6e688364b0ffcf5a48e074b1822b68c362f9.svg");
}

.icon-gitlab {
	--icon: url("/assets/icons/simple-icons-gitlab-cca94b503311a8cc089c2e8fd52bd7f5630f548c9f7ef045f6a56d1decadc380.svg");
}

.icon-google {
	--icon: url("/assets/icons/simple-icons-google-0b809da5532989b56c2569c632514697bea09e83be2a45a51169dc9aab343230.svg");
}

/* Language marks retain Devicon's brand colours instead of using a mask. */
.icon-devicon-go,
.icon-devicon-typescript {
	background-color: transparent;
	background-position: center;
	background-repeat: no-repeat;
	background-size: contain;
	mask: none;
}

.icon-devicon-go {
	background-image: url("/assets/icons/devicon-go-1bdc3f78b1d71fdb20cdba8ebfce6715c166d0ac20535086542eb0eb59658c5e.svg");
}

.icon-devicon-typescript {
	background-image: url("/assets/icons/devicon-typescript-6b97556b48c36b6f4615b8b5b87733c81a9990f28a015c5c9412e29bfa8590e8.svg");
}

.icons {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
	gap: var(--space-3);

	> span {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--space-2);
		color: var(--color-text-dim);
		font-size: 0.75rem;
		text-align: center;

		> .icon {
			inline-size: 2.25rem;
			block-size: 2.25rem;
			color: var(--color-glow);
			filter: drop-shadow(0 0 6px rgb(var(--ch-glow) / 40%));
		}
	}
}

/*
Code: pre.panel holds a <code class="highlight"> from hyperlith's highlight
package, whose spans carry hl- classes named after tree-sitter captures
(string.special.key -> hl-string hl-string-special hl-string-special-key).
Later, more specific families override earlier ones.
*/
/* The label and optional Download link sit on the top edge. The copy control
sits in the middle of the right edge. */
.code-block {
	position: relative;
	display: flex;
	flex-direction: column;
	min-inline-size: 0;

	> figcaption > .panel-caption-heading {
		inset-block-start: 0;
		inset-inline-end: var(--space-4);
		transform: translateY(-50%);
		padding: 2px 2px 2px var(--space-2);
	}
}

/* TRON keeps byte fields and their meaning together inside the article. */
.tron {
	max-inline-size: 100%;
	padding: var(--space-4);
	border: 1px solid var(--color-line-low);
	background: rgb(var(--ch-ink) / 92%);
	.tron-bytes {
		width: auto;
		table-layout: auto;
		max-inline-size: 100%;
		border-collapse: collapse;
		font-size: 0.85rem;
		td { padding: var(--space-1); vertical-align: top; border: 0; }
		.tron-offset, .tron-hex { font-family: var(--font-data); font-variant-numeric: tabular-nums; }
		.tron-hex { white-space: pre-wrap; overflow-wrap: anywhere; }
		.tron-meaning { font-family: var(--font-text); overflow-wrap: anywhere; }
		/* Byte colors follow the hl- classes of the text formats: strings, numbers
		and builtin constants keep their colors, while containers, offsets and
		framing stay dim like punctuation. The meaning column is body text and
		its address links read as keys. */
		.tron-offset, .tron-magic .tron-hex, .tron-footer .tron-hex, .tron-unreferenced .tron-hex { color: var(--color-text-dim); }
		.tron-txt .tron-hex, .tron-bin .tron-hex { color: var(--color-code-string); }
		.tron-i64 .tron-hex, .tron-f64 .tron-hex { color: var(--color-warning); }
		.tron-bit .tron-hex, .tron-nil .tron-hex { color: var(--color-code-constant); }
		:is(.tron-arr_leaf, .tron-arr_branch, .tron-map_leaf, .tron-map_branch) .tron-hex { color: var(--color-text-dim); }
		.tron-meaning { color: var(--color-text); }
		.tron-meaning a { color: var(--color-info); }
		.is-linked, tr:focus-visible { outline: 1px solid var(--color-glow); background: rgb(var(--ch-glow) / 10%); }
	}
	@media (max-width: 34rem) {
		.tron-bytes {
			display: block;
			tbody { display: block; }
			tr { display: grid; grid-template-columns: auto minmax(0, 1fr); }
			.tron-meaning { grid-column: 2; }
		}
	}
	@media (forced-colors: active) {
		.tron-bytes { color: CanvasText; }
		.tron-bytes tr { color: CanvasText; }
		.tron-bytes .is-linked, .tron-bytes tr:focus-visible { outline-color: Highlight; }
	}
}

/* The chip a captioned block hangs on its own edge: a bordered, blurred pill
that sits over the frame, with the copy button beside it. A code panel hangs it
from the top edge and a result from the bottom, so each of them keeps its own
edge, offset and padding, and everything they agree on is here. */
:is(.code-block, .result) > figcaption {
	position: static;

	> .panel-caption-heading {
		position: absolute;
		z-index: 1;
		display: inline-flex;
		align-items: center;
		gap: var(--space-1);
		border: 1px solid var(--color-line-low);
		background: rgb(var(--ch-ink) / 92%);
		backdrop-filter: blur(4px);
	}
}

.code-block > figcaption > .panel-size {
	position: absolute;
	z-index: 1;
	inset-block-end: 0;
	inset-inline-end: var(--space-4);
	transform: translateY(50%);
	padding: 2px var(--space-2);
	line-height: 1;
	background: rgb(var(--ch-ink) / 92%);
	font-family: var(--font-data);
	font-size: 0.75rem;
	color: var(--color-text-dim);
	pointer-events: none;
}

:is(.code-block, .result) > figcaption > copy-button {
	position: absolute;
	inset-block-start: 50%;
	inset-inline-end: 0;
	z-index: 1;
	transform: translate(50%, -50%);

	.copy-popover {
		inset-block-start: auto;
		inset-block-end: calc(100% + var(--space-1));
	}
}

.copy-button.success,
.result > figcaption > copy-button .copy-button {
	color: var(--color-success);

	&:hover {
		filter: var(--control-hover) drop-shadow(0 0 6px rgb(var(--ch-success) / 50%));
	}

	&:active {
		filter: var(--control-press) drop-shadow(0 0 10px rgb(var(--ch-success) / 65%));
	}
}

.panel-caption-heading {
	display: contents;

	/* The icon is an inline-block, so on its own it sits on the text baseline
	and hangs below the cap height beside a short uppercase label like JSON.
	The label centres it instead. */
	> .label {
		display: inline-flex;
		align-items: center;
		gap: var(--space-1);
	}
}

/* <copy-button code="..."> (assets/rocket/copy-button.js) in a panel caption. */
copy-button {
	display: inline-flex;
}

.copy-button-wrapper {
	position: relative;
	display: inline-flex;
}

.copy-button {
	display: inline-grid;
	place-items: center;
	border: 1px solid var(--color-line-low);
	padding: var(--space-1);
	background: rgb(var(--ch-deep) / 80%);
	color: var(--color-info);
	user-select: none;
	cursor: pointer;
	transition: filter var(--motion-fast);

	&:hover {
		filter: var(--control-hover) drop-shadow(0 0 6px rgb(var(--ch-glow) / 50%));
	}

	&:active {
		filter: var(--control-press) drop-shadow(0 0 10px rgb(var(--ch-glow) / 65%));
	}
}

.copy-popover {
	position: absolute;
	inset-block-start: calc(100% + var(--space-1));
	inset-inline-end: 0;
	border: 1px solid var(--color-line);
	padding: 0.1em var(--space-2);
	background: var(--color-deep);
	color: var(--color-success);
	--track: 0.12em;
	font-size: 0.6rem;
	white-space: nowrap;
	pointer-events: none;
}

.highlight {
	font: inherit;
	tab-size: 2;
}

.hl-comment {
	color: var(--color-text-dim);
	font-style: italic;
}

.hl-keyword,
.hl-operator {
	color: var(--color-action-high);
}

.hl-string {
	color: var(--color-code-string);
}

.hl-string-special-key,
.hl-property,
.hl-attribute {
	color: var(--color-info);
}

/* A Go type names what the code defines, so it reads like a head without the
weight of a call. */
.hl-type {
	color: var(--color-glow);
}

.hl-number,
.hl-constant {
	color: var(--color-warning);
}

.hl-constant-builtin {
	color: var(--color-code-constant);
}

/* The head of a compact list (`[format ...]`) is the call, so it reads ahead of
its arguments. A known head (an operator, an aggregate, a window function) is
brighter again, because Stardust defines its meaning. */
.hl-function {
	color: var(--color-glow);
	font-weight: 700;
}

.hl-function-builtin {
	color: var(--color-action-high);
	font-weight: 700;
}

.hl-escape,
.hl-embedded {
	color: var(--color-code-escape);
}

/* Query variables (?e) in orange, far from the green of plain strings; then
DUST entity tags (#link) and brackets. */
.hl-variable {
	color: var(--color-code-variable);
}

.hl-tag {
	color: var(--color-code-tag);
}

.hl-punctuation {
	color: var(--color-text-dim);
}

/* Rainbow brackets (DUST): each depth its own color, an opener and its closer
alike, so a bracket is matched by eye. Three hues, VS Code's own, repeated over the six
depth classes the highlighter cycles through. */
.rainbow-bracket.depth-0,
.rainbow-bracket.depth-3,
.hl-rainbow-1,
.hl-rainbow-4 {
	color: var(--color-code-rainbow-1);
}

.rainbow-bracket.depth-1,
.rainbow-bracket.depth-4,
.hl-rainbow-2,
.hl-rainbow-5 {
	color: var(--color-code-rainbow-2);
}

.rainbow-bracket.depth-2,
.rainbow-bracket.depth-5,
.hl-rainbow-3,
.hl-rainbow-6 {
	color: var(--color-code-rainbow-3);
}

/* A run of closers (the ]]} that ends a nested query) reads as one stop, not
three marks: set tight and a step back from the words, the colors kept so
each one still finds its opener. */
.hl-closers {
	letter-spacing: -0.08em;
	opacity: 0.75;
}

/*
Quickstart: a briefing hero beside the finished app's terminal, then numbered
steps beside a sticky step rail. Steps mix code panels with terminals, file
trees and a flow.
*/
.lede {
	max-inline-size: 48ch;
	color: var(--color-text);
	font-size: 1.2rem;
	line-height: 1.6;
}

.quickstart-hero {
	--split-side: minmax(0, 28rem);

	align-items: center;
}

.briefing {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	padding: 0;
	list-style: none;

	li {
		border: 1px solid var(--color-line-low);
		padding: var(--space-1) var(--space-3);
		background: rgb(var(--ch-ink) / 60%);
		--track: 0.1em;
		color: var(--color-text-dim);
		font-size: 0.66rem;
	}

	strong {
		color: var(--color-sand);
		font-weight: inherit;
	}
}

/* The step rail numbers its links and joins them with a faint line. */
.step-rail ol {
	position: relative;
	counter-reset: step;

	&::before {
		content: "";
		position: absolute;
		inset-block: var(--space-3);
		inset-inline-start: calc(var(--space-3) + 0.55rem);
		border-inline-start: 1px solid var(--color-line-low);
	}

	a {
		display: grid;
		grid-template-columns: 1.6rem 1fr;
		align-items: center;
		gap: var(--space-2);

		&::before {
			counter-increment: step;
			content: counter(step);
			position: relative;
			display: grid;
			place-items: center;
			block-size: 1.2rem;
			inline-size: 1.2rem;
			border: 1px solid var(--color-line);
			background: var(--color-deep);
			color: var(--color-info);
			font-size: 0.62rem;
		}

		&:hover::before {
			border-color: var(--color-glow);
			box-shadow: 0 0 8px rgb(var(--ch-edge) / 45%);
		}
	}
}

.step-rail {
	.step-rail-home {
		border: 0;
		padding: 0;
		color: var(--color-info);
		font-size: 0.7rem;
	}

	a[aria-current="step"] {
		border-color: var(--color-glow);
		background: linear-gradient(90deg, rgb(var(--ch-edge) / 22%), transparent);
		color: var(--color-text-strong);

		&::before {
			border-color: var(--color-glow);
			background: rgb(var(--ch-edge) / 35%);
			box-shadow: 0 0 8px rgb(var(--ch-edge) / 55%);
			color: var(--color-text-strong);
		}
	}
}

/* The briefing's step list: numbered rows linking to each step page. */
.step-list {
	display: grid;
	gap: var(--space-2);
	padding: 0;
	list-style: none;
	counter-reset: none;

	.row {
		grid-template-columns: 3rem minmax(0, 1fr) auto;
		align-items: center;
		color: inherit;
		text-decoration: none;

		&:hover {
			filter: brightness(1.2);
		}
	}

	.step-number {
		font-size: 1.8rem;
		text-align: center;
	}
}

.step-list-title {
	color: var(--color-sand);
	font-family: var(--font-display);
	font-size: 1rem;
}

.step-header {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	align-items: start;
	gap: var(--space-4);
	padding-block-start: var(--space-3);

	.article & h1 {
		margin: 0;
		font-size: 1.8rem;
	}
}

/* One segment per step: passed steps lit, the current one glowing. */
.step-progress {
	display: grid;
	grid-auto-columns: minmax(0, 1fr);
	grid-auto-flow: column;
	gap: 4px;
	padding: 0;
	list-style: none;

	li {
		block-size: 4px;
		background: var(--color-track);
		box-shadow: inset 0 0 0 1px var(--color-line-low);
	}

	.done {
		background: var(--color-edge);
	}

	.current {
		background: var(--color-glow);
		box-shadow: 0 0 8px rgb(var(--ch-glow) / 60%);
	}

	/* Articles cap lists at a reading width; this spans the article. */
	.article & {
		max-inline-size: none;
	}

	.article & li + li {
		margin: 0;
	}
}

/* Outlined display numerals with the glow behind them. */
.step-number {
	color: transparent;
	font-family: var(--font-display);
	font-size: 2.8rem;
	line-height: 1;
	-webkit-text-stroke: 1px var(--color-glow);
	text-shadow: 0 0 18px rgb(var(--ch-edge) / 40%);
}

/* A code panel under a label naming its file; the panel's own caption keeps
only the language. */
.file-block {
	display: grid;
	gap: var(--space-1);
	min-inline-size: 0;
}

/* A path's root, such as <mount>, in action gold like a file tree's caption. */
.path-root {
	color: var(--color-action);
}

.file-block-path {
	color: var(--color-info);
	font-family: var(--font-data);
	font-size: 0.8rem;
	/* A path has no spaces to break at, so a deep one is a single 400px
	word that would push the page sideways on a phone. It breaks anywhere
	instead; the panel under it scrolls, but the caption above it must not.
	(/theme found this: the quickstart step pages carry such paths and were
	not in mobile_layout.py's routes.) */
	overflow-wrap: anywhere;
	letter-spacing: 0;
	text-transform: none;
	/* A mount path has no spaces, so on a phone it is one unbreakable word that
	sets the width of the panel it captions and pushes the whole page sideways
	(<mount>/format/dust/definitions/queries/todos.dust is 400px at 320px wide).
	The caption is words to read, not code to scroll, so it wraps. */
	overflow-wrap: anywhere;
}

/*
Terminals are shell sessions, unlike code panels: a green top rule, static
scanlines, "$" before each command and dim comments. A live terminal ends in
the .prompt cursor.
*/
.terminal {
	position: relative;
	margin: 0;
	border: 1px solid var(--color-line-low);
	border-block-start: 2px solid rgb(var(--ch-prompt) / 55%);
	background:
		repeating-linear-gradient(to bottom, rgb(var(--ch-prompt) / 3%) 0 1px, transparent 1px 3px),
		rgb(var(--ch-ink) / 92%);
	box-shadow: 0 -6px 18px -12px rgb(var(--ch-prompt) / 60%);

	> figcaption {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-2);
		border-block-end: 1px solid var(--color-line-low);
		padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
		font-family: var(--font-data);

		.label {
			font-family: var(--font-data);
			letter-spacing: 0;
			text-transform: none;
		}
	}
}

.terminal-screen {
	margin: 0;
	overflow-x: auto;
	padding: var(--space-3);
	color: var(--color-text);
	font-family: var(--font-data);
	font-size: 0.88rem;
	line-height: 1.6;
}

.terminal-command {
	color: var(--color-prompt);
	text-shadow: 0 0 6px rgb(var(--ch-prompt) / 35%);

	&::before {
		content: "$ ";
		color: var(--color-text-dim);
		text-shadow: none;
	}
}

.terminal-comment {
	color: var(--color-text-dim);
	font-style: italic;
}

/*
A result readout: what an example gives, below the example. The label, the
parameter chips, an equals sign and the value read from left to right. It is
not an alert, because an example that gives a value reports no success.
*/
.result {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
	min-inline-size: 0;
	margin: 0;
	border-inline-start: 2px solid var(--color-success);
	padding: var(--space-3) var(--space-4);
	background: linear-gradient(90deg, rgb(var(--ch-success) / 10%), transparent);

	> figcaption > .panel-caption-heading {
		inset-block-end: 0;
		inset-inline-end: 0;
		transform: translateY(50%);
		padding: 2px 0;
		color: var(--color-success);
		line-height: 1;
	}
}

.result-params {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	margin: 0;

	> div {
		display: flex;
		align-items: flex-start;
		gap: var(--space-1);
		max-inline-size: 100%;
		min-block-size: 1.6rem;
		border: 1px solid var(--color-line-low);
		padding: var(--space-1) var(--space-2);
		line-height: 1.3;
		background: rgb(var(--ch-ink) / 60%);
		font-family: var(--font-data);
		font-size: 0.8rem;
	}

	dt {
		color: var(--color-info);

		&::after {
			content: ":";
		}
	}

	dd {
		min-inline-size: 0;
		margin: 0;
		color: var(--color-text-strong);
	}
}

.protocol-contract .result-params code.highlight {
	border: 0;
	padding: 0;
	background: transparent;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

/* The value follows the parameters, after an equals sign, so the line reads
from left to right. */
.result-value {
	display: block;
	min-inline-size: 0;
	margin: 0;
	overflow-x: auto;
	white-space: pre;
	line-height: 1.5;
	color: var(--color-success);
	font-family: var(--font-data);
	text-shadow: 0 0 8px rgb(var(--ch-success) / 35%);
}

/* File trees are code panels (pre.panel.tree, captioned with where they live): one line per
entry with an indent guide per level, directories in info blue, entries a step
added in success green with a "new" chip, and notes dim after an arrow. */
.tree {
	--indent: 1.25rem;
}

/* The caption names where the tree lives, in action gold so it reads apart
from the language captions of code panels. */
.tree-label {
	color: var(--color-action);
}

.tree-line {
	display: flex;
	align-items: baseline;
	gap: var(--space-2);
	padding-inline-start: calc(var(--depth, 0) * var(--indent));
	background-image: repeating-linear-gradient(
		to right,
		var(--color-line-low) 0 1px,
		transparent 1px var(--indent)
	);
	background-position: 0.35rem 0;
	background-repeat: no-repeat;
	background-size: calc(var(--depth, 0) * var(--indent)) 100%;
	white-space: pre;
}

.tree-depth-1 { --depth: 1; }
.tree-depth-2 { --depth: 2; }
.tree-depth-3 { --depth: 3; }
.tree-depth-4 { --depth: 4; }
.tree-depth-5 { --depth: 5; }
.tree-depth-6 { --depth: 6; }
.tree-depth-7 { --depth: 7; }

.tree-dir .tree-name {
	color: var(--color-info);
}

.tree-added .tree-name {
	color: var(--color-success);
	text-shadow: 0 0 6px rgb(var(--ch-prompt) / 30%);
}

.tree-badge {
	border: 1px solid rgb(var(--ch-prompt) / 45%);
	padding: 0 0.35em;
	color: var(--color-success);
	--track: 0.1em;
	font-size: 0.55rem;
}

.tree-note {
	color: var(--color-text-dim);

	&::before {
		content: "\2190\00A0";
	}
}

/* A flow is a short pipeline of stages joined by glowing links. */
.flow {
	display: grid;
	gap: var(--space-4);
	padding: 0;
	list-style: none;

	@media (min-width: 48rem) {
		grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
		grid-auto-flow: column;
	}

	li {
		position: relative;
		display: grid;
		gap: var(--space-1);
		border: 1px solid var(--color-line);
		padding: var(--space-3);
		background: linear-gradient(180deg, rgb(var(--ch-edge) / 12%), rgb(var(--ch-ink) / 70%));

		@media (min-width: 48rem) {
			& + li::before {
				content: "";
				position: absolute;
				inset-block-start: 50%;
				inset-inline-end: 100%;
				inline-size: var(--space-4);
				border-block-start: 1px solid var(--color-glow);
				box-shadow: 0 0 6px var(--color-glow);
			}
		}
	}

	/* Articles cap lists at a reading width; this spans the article. */
	.article & {
		max-inline-size: none;
	}

	.article & li + li {
		margin: 0;
	}
}

.featured {
	grid-template-columns: auto minmax(0, 1fr) auto;
	align-items: center;

	> .icon {
		inline-size: 2.5rem;
		block-size: 2.5rem;
		color: var(--color-action);
	}
}

.next-card {
	display: grid;
	gap: var(--space-1);
	text-decoration: none;

	&:hover {
		filter: brightness(1.15);
	}
}

.next-title {
	color: var(--color-sand);
	font-family: var(--font-display);
	font-size: 1.1rem;
}

/*
Site (the home page, src/site): marketing sections in the theme's frames. A
hero beside a query console, a video, a live demo, comparison diagrams that
animate under Datastar, a capability ledger, pricing rows, founders, an FAQ and
the waitlist.
*/
:root {
	/* Service brand colors, only for the comparison diagram's routes. An active
	node writes its label in one, so each is the nearest shade of its brand
	colour that clears WCAG AA against the page it sits on. That is a different
	shade per theme, which is why the light theme redefines all five: the
	diagram lights one service at a time, so a colour that missed only showed up
	when that service happened to be lit. */
	--service-redis: #ff4438;
	--service-postgres: #5e85f9;
	--service-nats: #27aae1;
	--service-eventstore: #5ab552;
	--service-backblaze: #fa4953;
}

/* The home page's More menu: a native disclosure holding the links to the rest
of the site, so the bar keeps only this page's sections. */
.menu {
	position: relative;
	display: flex;
}

/* The trigger is a bar cell like the links beside it, with a caret. */
.menu-summary {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	border: 0;
	border-inline-start: 1px solid var(--color-line-low);
	padding-inline: var(--space-5);
	background: none;
	--track: 0.08em;
	color: var(--color-text);
	font-size: 0.8rem;
	white-space: nowrap;
	user-select: none;
	cursor: pointer;

	&::after {
		content: "";
		inline-size: 0.4rem;
		block-size: 0.4rem;
		border-inline-end: 1px solid currentcolor;
		border-block-end: 1px solid currentcolor;
		transform: translateY(-0.1em) rotate(45deg);
	}

	&:hover,
	&[aria-expanded="true"] {
		background: rgb(var(--ch-edge) / 8%);
		color: var(--color-text-strong);
	}
}

/* The panel drops from the bottom edge of the bar, under its trigger.

It hangs from the trigger's end edge, not its start edge. The trigger is the
last cell in the bar, and a panel anchored at the start could only grow towards
the edge of the screen: on a narrow bar it ran off the right, cutting its own
links in half and giving the whole page a horizontal scrollbar.

The width cap is the screen, for the case the start edge is near the far edge
too. */
.menu-list {
	position: absolute;
	z-index: 30;
	inset-block-start: 100%;
	inset-inline-start: auto;
	inset-inline-end: 0;
	display: grid;
	min-inline-size: 12rem;
	max-inline-size: calc(100vw - var(--space-4));
	border: 1px solid var(--color-line);
	border-block-start: 0;
	background-color: rgb(var(--ch-ink) / 96%);
	backdrop-filter: blur(8px) saturate(1.2);
	box-shadow: 0 12px 30px -14px rgb(var(--ch-shadow) / 85%);

	a {
		border-inline-start: 0;
		padding: var(--space-3) var(--space-4);
		font-size: 0.72rem;

		& + a {
			border-block-start: 1px solid var(--color-line-low);
		}

		&:hover {
			background: rgb(var(--ch-edge) / 14%);
		}
	}
}

/* The reader's account.

Signed out, the ways to sign in are the last rows of the More menu, after a
.menu-divider when the menu has links of its own; each .menu-sign-in row is
an icon and one label, the way a toggle names its options. Signed in,
.topbar-account ends the bar's tools: the avatar, 34px so it sits in the
bar's row, linking to the account page, where Sign out is. A label that
opens with a provider mark (the account page's rows) keeps the mark on the
text's centre line with a gap before the name. */
.label > .icon {
	vertical-align: -0.3em;
	margin-inline-end: var(--space-2);
}

.menu-list .menu-divider {
	border-block-start: 1px solid var(--color-line);
}

.menu-list .menu-sign-in {
	display: flex;
	align-items: center;
	gap: var(--space-2);
}

.topbar-account {
	display: flex;
	align-items: center;
	gap: var(--space-3);
}

.topbar-avatar {
	display: block;
	flex: none;
	inline-size: 34px;
	line-height: 0;

	&:hover .avatar,
	&:focus-visible .avatar {
		filter: brightness(1.3);
	}
}

/* An account whose provider gave no picture: the user mark on the avatar's
frame, in the action gold, over the same scanlines. */
.avatar-fallback {
	display: grid;
	place-items: center;

	&::before {
		content: "";
		position: absolute;
		inset: 2px;
		clip-path: var(--chamfer);
		background: rgb(var(--ch-ink) / 92%);
	}

	.icon {
		position: relative;
		z-index: 1;
		inline-size: 62%;
		block-size: 62%;
		color: var(--color-action);
	}
}

.site-hero {
	--split-side: minmax(0, 34rem);

	align-items: center;

	/* One column until there is room for two. .split turns at 60rem, but this
	hero's other side takes 34rem, which leaves the copy about 234px -- not a
	column for a headline, and the width at which the heading used to break
	its words. It turns at 75rem instead, with the bar. */
	grid-template-columns: minmax(0, 1fr);

	@media (min-width: 75rem) {
		grid-template-columns: var(--split-main, minmax(0, 1fr)) var(--split-side);
	}

	/*
	The hero's type is fitted to its column, not to the viewport.

	It used to be clamp(..., 3.4vw, ...), which reads the viewport while the
	hero copy is one side of a split: at a tablet width the column is about
	340px and that clamp asks for 52px of a wide display face, so words broke
	across lines in the middle -- "CHAN / GE.".

	<fit-text> sets an exact px size per line so each line fills the
	column on one line. The cqi fallback below is what a reader without
	JavaScript gets, and it is container-relative too, so the type is honest
	about its column either way and the line never breaks mid-word.
	*/
	h1 {
		/* Block, not grid: a grid's auto track sizes to max-content, and these
		lines do not wrap, so the track grew to the width of the longest line
		and each line then fitted itself to that overflow instead of to the
		column. */
		font-size: clamp(1.5rem, 11cqi, 4rem);
		line-height: 1.05;

		.hero-accent {
			color: var(--color-action);
			text-shadow: 0 0 18px rgb(var(--ch-action) / 35%);
		}
	}
}

/* The fitter sets white-space itself: nowrap while it measures, and nowrap
   afterwards only when the line can hold one. Setting it here instead would
   make a reader without JavaScript -- who gets no fitting -- overflow the
   panel on a phone rather than wrap. */
.hero-line {
	display: block;
	/* Never narrower than its column, so clientWidth is the room the line has
	to fill rather than the room its text happens to want. */
	inline-size: 100%;
	min-inline-size: 0;
}

.hero-copy {
	/* The unit the hero's type is measured in: cqi is 1% of this box. */
	container-type: inline-size;
	padding: var(--space-4);

	@media (min-width: 44rem) {
		padding: var(--space-5) var(--space-6);
	}
}

/*
The console's panels, named so the format swap animates.

Both formats of a card are in the document and the selector shows one, so
without a name the browser sees one element disappear and another appear. With
the same name on both, and only ever one of them displayed, it sees a single
panel whose contents changed, and cross-fades it.

The swap itself runs inside document.startViewTransition, from the
__viewtransition modifier on the selector's buttons.
*/
.query-result {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: 0;
	view-transition-name: query-panel;
}

::view-transition-old(query-panel),
::view-transition-new(query-panel) {
	animation-duration: 160ms;
}

.query-console > .tabs {
	align-self: center;
}

.query-format {
	display: flex;
	justify-content: center;
	border-block-start: 1px solid var(--color-line-low);
	padding-block-start: var(--space-3);
}

/* Glass goes behind text that sits straight on background art: it darkens and
blurs the art under it. Only the background color is set, so a component's own
background image stays. */
.glass {
	background-color: rgb(var(--ch-ink) / 72%);
	backdrop-filter: blur(8px) saturate(1.2);
}

.site-quote {
	max-inline-size: 52rem;
	margin-inline: auto;
	font-size: clamp(1.2rem, 1rem + 1vw, 1.7rem);
}

.video {
	position: relative;
	display: grid;
	place-items: center;
	margin: 0;
	aspect-ratio: 16 / 9;
	overflow: hidden;
	cursor: pointer;

	/* The picture sits recessed in the panel: inset from the frame, with a dark
	upper lip, a lit lower edge and an inner shadow over it. */
	img,
	&::after {
		position: absolute;
		inset: var(--space-3);
		inline-size: calc(100% - 2 * var(--space-3));
		block-size: calc(100% - 2 * var(--space-3));
	}

	img {
		object-fit: cover;
		opacity: 0.6;
	}

	&::after {
		content: "";
		border: 1px solid var(--color-line-low);
		border-block-start: 2px solid rgb(var(--ch-shadow) / 70%);
		border-block-end-color: rgb(var(--ch-glow) / 40%);
		box-shadow:
			inset 0 6px 18px rgb(var(--ch-shadow) / 75%),
			inset 0 0 40px rgb(var(--ch-shadow) / 45%);
		pointer-events: none;
	}

	&:hover img {
		opacity: 0.75;
	}
}

.video-play {
	position: relative;
	z-index: 1;
	min-block-size: 4.5rem;
	min-inline-size: 4.5rem;

	.icon {
		inline-size: 1.8rem;
		block-size: 1.8rem;
	}
}

.demo {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	align-items: center;
	gap: var(--space-6);
	padding: var(--space-4);

	@media (min-width: 44rem) {
		padding: var(--space-6);
	}

	@media (min-width: 68rem) {
		grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
	}

	h2 {
		font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.6rem);
	}
}

.demo-logo {
	justify-self: center;
	inline-size: 9rem;
	block-size: auto;
	filter: drop-shadow(0 0 18px rgb(var(--ch-action) / 35%));
}

.demo-details {
	align-items: center;
	text-align: center;

	.stat {
		font-size: clamp(2.4rem, 2rem + 2vw, 4rem);
	}
}

/* A comparison pairs the argument with its system diagram. */
.comparison {
	--split-side: minmax(0, 30rem);

	align-items: center;
	padding: var(--space-4);

	@media (min-width: 44rem) {
		padding: var(--space-6);
	}

	h2 {
		font-size: clamp(1.6rem, 1.2rem + 1.6vw, 2.6rem);
	}

	p {
		max-inline-size: 60ch;
		line-height: 1.7;
	}

	strong {
		color: var(--color-text-strong);
	}
}

/* System diagrams sit on the blueprint grid of .diagram. Routes are faint
lines that take a service's color while it is active; the server warms to
warning and runs hot in danger. */
/* <service-diagram> (site/assets/rocket/service-diagram.js) is the element; it
has no shadow root and renders nothing, so the diagram below is this page's own
markup and these rules reach it. */
.system-diagram {
	/*
	One unit, and every length in the drawing is a multiple of it, so the
	diagram scales as a whole instead of overflowing.

	It used to be built from fixed rem: the app node, the wires and the service
	stack come to about 27rem with the padding, so on any screen narrower than
	that the diagram grew its own sideways scrollbar and a phone saw a third of
	a picture whose point is the shape of the whole thing.

	cqi is a percentage of this element's own width, so the unit follows the
	column the diagram is in rather than the window. The drawing is 27.2 units
	across and the 4px is the hairline border down each side -- the one thing
	here that does not scale -- so the unit is what is left of the column
	divided by the units that have to fit in it. It is capped at
	1rem, the size the drawing was designed at, and floored at the size that
	still fits the narrowest phone still sold: at 320px the diagram's column is
	216px, and the drawing's 26 units have to fit in that. Below the floor
	overflow-x is still there to catch it.
	*/
	--u: clamp(0.45rem, calc((100cqi - 4px) / 27.2), 1rem);

	container-type: inline-size;
	display: flex;
	align-items: center;
	justify-content: center;
	min-block-size: calc(20 * var(--u));
	overflow-x: auto;
	border: 1px solid var(--color-line);
	border-block-end: 2px solid var(--color-glow);
	padding: calc(1.5 * var(--u));
	background:
		repeating-linear-gradient(90deg, rgb(var(--ch-edge) / 9%) 0 1px, transparent 1px 24px),
		repeating-linear-gradient(rgb(var(--ch-edge) / 9%) 0 1px, transparent 1px 24px),
		radial-gradient(ellipse at 50% 100%, rgb(var(--ch-glow) / 18%), transparent 70%),
		rgb(var(--ch-ink) / 88%);
	box-shadow: 0 6px 18px -8px var(--color-glow);
}

.app-node {
	display: grid;
	flex: none;
	justify-items: center;
	gap: calc(0.5 * var(--u));
	inline-size: calc(8 * var(--u));
	color: var(--color-info);
	--track: 0.1em;
	font-size: calc(0.66 * var(--u));
	text-align: center;
}

.server-icon {
	inline-size: calc(3.5 * var(--u));
	block-size: calc(3.5 * var(--u));
	color: var(--color-text-dim);

	:is(rect, path):last-of-type {
		stroke: var(--color-info);
	}

	&.warm :is(rect, path) {
		stroke: var(--color-warning);
	}

	&.hot :is(rect, path) {
		stroke: var(--color-danger);
	}
}

.system-lines {
	flex: none;
	inline-size: calc(7 * var(--u));
	block-size: calc(14 * var(--u));
	margin-inline-start: calc(-1.5 * var(--u));

	svg {
		inline-size: 100%;
		block-size: 100%;
		overflow: visible;
	}

	line,
	polyline {
		fill: none;
		stroke: var(--color-line);
		stroke-width: 2;
		transition: stroke 120ms ease;
	}

	.route-activity.active {
		stroke: var(--active-route-color, var(--color-glow));
	}
}

.service-stack {
	display: grid;
	flex: 0 1 calc(11 * var(--u));
	gap: calc(0.5 * var(--u));
	min-inline-size: calc(8 * var(--u));
}

.service-node {
	--service-color: var(--color-glow);

	display: flex;
	align-items: center;
	gap: calc(0.75 * var(--u));
	min-block-size: calc(2.7 * var(--u));
	color: var(--color-text-dim);
	font-family: var(--font-data);
	font-size: calc(0.8 * var(--u));
	transition: color 120ms ease;

	&.active {
		color: var(--service-color);
	}
}

.service-icon {
	display: grid;
	flex: none;
	place-items: center;
	inline-size: calc(2.6 * var(--u));
	block-size: calc(2.6 * var(--u));
	border: 1px solid currentcolor;

	&::before {
		content: "";
		inline-size: calc(1.6 * var(--u));
		block-size: calc(1.6 * var(--u));
		background: currentcolor;
		mask: var(--service-icon) center / contain no-repeat;
	}
}

.redis {
	--service-color: var(--service-redis);
	--service-icon: url("/assets/icons/services/redis-fc830b00425ad2ae59cbbecb5c95214bfe9bcc9e8b890abcdd2cea426f017900.svg");
}

.postgres {
	--service-color: var(--service-postgres);
	--service-icon: url("/assets/icons/services/postgresql-45205af869a6c0963ec3dead930aca5ed55f917662f9c96558aa0ebd8c193381.svg");
}

.nats {
	--service-color: var(--service-nats);
	--service-icon: url("/assets/icons/services/nats-7faf50c7934f39ecc657a1a6ff4bc69f997fd7620c0a5fe0ceb259addae2db64.svg");
}

.eventstore {
	--service-color: var(--service-eventstore);
	--service-icon: url("/assets/icons/services/event-store-d9eca1ebc77469f25dccf57567c62a459fbde80dd5bcea6e95d1021f0eb4899b.svg");
}

.backblaze {
	--service-color: var(--service-backblaze);
	--service-icon: url("/assets/icons/services/backblaze-0b13bf00aebae137427fa54c80c39ef0efb1783728215a2e4dbff58ea2d43c8f.svg");
}

/* The Stardust side is gold: one service and its backup route. */
.stardust-system .system-lines line {
	stroke: var(--color-action);
}

.stardust-services {
	gap: 0;
	margin-block-start: calc(-6 * var(--u));
	color: var(--color-action);

	.service-node:last-child,
	.backup-node {
		--service-color: var(--color-action-high);

		color: var(--color-action);
	}
}

.backup-route {
	display: block;
	inline-size: calc(2.6 * var(--u));
	block-size: calc(3 * var(--u));

	svg {
		inline-size: 100%;
		block-size: 100%;
	}

	line {
		stroke: var(--color-line);
		stroke-width: 2;
	}

	&.active line {
		stroke: var(--color-action);
	}
}

.stardust-icon {
	flex: none;
	inline-size: calc(2.6 * var(--u));
	block-size: calc(2.6 * var(--u));
	border: 1px solid currentcolor;
	padding: calc(0.25 * var(--u));
	background: var(--color-deep);
}

/* The capability ledger: ruled rows of index, heading and explanation. */
.ledger {
	margin-block-start: var(--space-5);
	border-block-start: 1px solid var(--color-line);
	padding: 0;
	list-style: none;

	li {
		display: grid;
		grid-template-columns: 3rem minmax(0, 1fr);
		align-items: baseline;
		gap: var(--space-2) var(--space-5);
		border-block-end: 1px solid var(--color-line-low);
		padding: var(--space-4) var(--space-3);

		@media (min-width: 60rem) {
			grid-template-columns: 4rem minmax(12rem, 0.7fr) minmax(0, 1.3fr);
		}

		&:hover {
			background: rgb(var(--ch-edge) / 8%);
		}
	}

	h3 {
		font-size: 1rem;
	}

	p {
		grid-column: 2;
		color: var(--color-text-dim);

		@media (min-width: 60rem) {
			grid-column: auto;
		}
	}
}

.ledger-index {
	color: var(--color-action);
	font-family: var(--font-data);
	font-size: 0.8rem;
}

/* Pricing: an introduction that sticks beside framed tier rows; the featured
tier takes the modal frame. */
.pricing {
	--split-main: minmax(0, 0.7fr);
	--split-side: minmax(0, 1.3fr);

	align-items: start;

	> .stack:not(.pricing-intro) {
		container-type: inline-size;
	}
}

/* Glass behind the pricing introduction that fades out downward, so the text
reads over the art without a hard edge. */
.pricing-intro {
	position: relative;
	isolation: isolate;
	padding: var(--space-4) var(--space-4) var(--space-6);

	@media (min-width: 44rem) {
		padding: var(--space-5) var(--space-5) var(--space-7);
	}

	&::before {
		content: "";
		position: absolute;
		z-index: -1;
		inset: 0;
		border-block-start: 2px solid var(--color-action);
		background: rgb(var(--ch-ink) / 78%);
		backdrop-filter: blur(8px) saturate(1.2);
		mask-image: linear-gradient(180deg, black 55%, transparent);
	}

	@media (min-width: 68rem) {
		position: sticky;
		inset-block-start: calc(var(--topbar-height) + var(--space-5));
	}
}

.price-row {
	grid-template-columns: minmax(0, 1fr);

	@container (min-width: 38rem) {
		grid-template-columns: minmax(0, 1fr) minmax(0, 14rem);
	}

	align-items: center;
	gap: var(--space-4);
	padding: var(--space-4) var(--space-3);

	@media (min-width: 44rem) {
		padding: var(--space-4) var(--space-5);
	}

	h3 {
		font-size: 1.05rem;
		overflow-wrap: normal;
	}
}

/* A tier's icon in action gold beside its name and audience. */
.price-tier {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	align-items: center;
	gap: var(--space-4);

	> .icon {
		inline-size: 2.4rem;
		block-size: 2.4rem;
		color: var(--color-action);
	}
}

.price {
	display: grid;
	justify-items: start;
	gap: var(--space-1);

	/* The renewal note runs to two or three lines in a card, and ragged
	wrapping left "year." alone on the last of them. Balanced lines keep the
	note looking like a note rather than like a mistake. */
	p {
		text-wrap: balance;
	}

	@media (min-width: 44rem) {
		justify-items: end;
		text-align: end;
	}

	strong {
		color: var(--color-action);
		font-family: var(--font-display);
		font-size: clamp(1.8rem, 1.4rem + 1.4vw, 2.8rem);
		font-weight: 400;
	}

	s {
		color: var(--color-text-dim);
	}
}

/* Avatars take the theme's chamfer (top left and bottom right), a glowing
edge, a cool tone and faint scanlines. */
.avatar {
	--chamfer: polygon(20% 0, 100% 0, 100% 80%, 80% 100%, 0 100%, 0 20%);

	position: relative;
	display: block;
	aspect-ratio: 1;
	padding: 2px;
	background: linear-gradient(135deg, var(--color-glow), var(--color-edge) 45%, var(--color-action));
	clip-path: var(--chamfer);

	img {
		display: block;
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
		clip-path: var(--chamfer);
		filter: saturate(0.6) contrast(1.1);
	}

	&::after {
		content: "";
		position: absolute;
		inset: 2px;
		background:
			repeating-linear-gradient(transparent 0 2px, var(--color-scanline) 2px 3px),
			linear-gradient(160deg, transparent 40%, rgb(var(--ch-edge) / 35%));
		clip-path: var(--chamfer);
		pointer-events: none;
	}
}

/*
The team card, drawn as a Datastar panel.

It replaces two founder panels with portraits and quotes. Who is on a team
changes, and a visitor deciding whether to trust a data engine is asking what
has been shipped rather than who shipped it -- so the card leads with the work,
and the only credentials on it are ones a reader can go and verify.

It is the one card on the page that wears another product's colours, on purpose:
the claim is "we built Datastar", and the card is a piece of Datastar sitting on
a Stardust page. The palette is the logo's own -- the Apollo ramp the rocket is
drawn in -- so the card and the mark on it are the same picture.

The ramp has a light variant, below the card's own rules. A reader who asked
for the light theme and then met one panel of night in the middle of the page
is reading a bug, not a brand: the card follows the theme like everything else,
and keeps its identity in the shape of the ramp rather than in its darkness --
the same eight steps, the same hues, the same order, lifted onto paper.

The frame is the card's own background showing through its padding, rather than
a border, because a border cannot be clipped into steps. The steps are the
point: at this size the corners read as pixels, which is what the logo is made
of.
*/
.team-story {
	/* The rocket's palette, read out of datastar-logo.svg. */
	--ds-void: #172038;
	--ds-deep: #253a5e;
	--ds-line: #3c5e8b;
	--ds-blue: #4f8fba;
	--ds-cyan: #73bed3;
	--ds-pale: #a4dddb;
	--ds-gold: #e8c170;
	--ds-flame: #cf573c;
	/* The grid the rocket is drawn on, and the lede's ink: both follow the
	ground under them, so the light ramp below can move them with it. */
	--ds-grid: rgb(255 255 255 / 3%);
	--ds-ink: #f2f0e5;
	/* The wordmark, in the homepage's --orange-5. */
	--ds-mark: oklch(0.63 0.13 63.23);

	/* The same ramp on paper: the hues and their order are the logo's, lifted
	so the darkest step is the ground and the lightest is the ink. Every pair
	the card sets clears WCAG AA, the lowest being a link at 5.21. */
	:root[data-theme="light"] & {
		--ds-void: #eaf1f6;
		--ds-deep: #d7e4ef;
		--ds-line: #b9cfe0;
		--ds-blue: #2f6a91;
		--ds-cyan: #186b8e;
		--ds-pale: #1b2f4a;
		--ds-gold: #6f5310;
		--ds-flame: #a33a24;
		--ds-grid: rgb(27 47 74 / 6%);
		--ds-ink: #1b2f4a;
		/* Two steps down the same hue: the orange as written reads at 2.81 on
		the light band, and the band cannot lift to meet it without going the
		colour of the ground and flattening the card. */
		--ds-mark: oklch(0.59 0.13 63.23);
	}
	/* One pixel of the drawing, at the size the card draws it. */
	--px: 6px;

	/*
	Two columns: who built it, and why. The identity sits on the lighter
	ground and the argument on the darker one, and the seam between them is
	the frame showing through.

	It was a full-width banner over a full-width body, and the body held its
	text to a measure -- so a desktop card was 40% empty down the right. The
	argument fills its column instead, and the column is sized so the measure
	comes out right without anything being left over.

	The card is centred and held to 60rem rather than run to the width of the
	section. At the content width the argument is around a hundred characters
	a line, which nobody reads to the end of, and the two columns stop reading
	as a pair and start reading as two things that happen to be side by side.
	*/
	display: grid;
	grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
	gap: var(--px);
	max-inline-size: 60rem;
	margin-inline: auto;
	border-radius: 0;
	padding: var(--px);
	background: var(--ds-line);
	color: var(--ds-pale);
	clip-path: polygon(
		0 var(--px),
		var(--px) var(--px),
		var(--px) 0,
		calc(100% - var(--px)) 0,
		calc(100% - var(--px)) var(--px),
		100% var(--px),
		100% calc(100% - var(--px)),
		calc(100% - var(--px)) calc(100% - var(--px)),
		calc(100% - var(--px)) 100%,
		var(--px) 100%,
		var(--px) calc(100% - var(--px)),
		0 calc(100% - var(--px))
	);

	a {
		color: var(--ds-cyan);
		text-decoration-color: var(--ds-blue);
	}

	strong {
		color: var(--ds-gold);
	}
}

/* The mark and whose team this is, on the lighter of the two grounds. */
/* The mark and the sentence it belongs to, stacked and centred in their own
   column. cqi sizes the wordmark against this column, not the window. */
/* Centred: the column is a mark and the sentence that belongs to it, which is
   a plaque rather than a paragraph. Ranged left it read as the start of a
   column of text that then stopped. */
.team-identity {
	container-type: inline-size;

	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--space-4);
	padding: var(--space-5);
	background: var(--ds-deep);
	text-align: center;
}

/*
The Datastar wordmark, set in type rather than shipped as a picture.

data-star.dev draws it the same way: Audiowide, uppercase, the name in orange
over the tagline in near-white, the pair tilted back. Setting it here rather
than pasting a screenshot means it stays sharp at any size, it is selectable
and searchable, and the reader downloads a font they can read rather than a
picture of words.
*/
/* The rocket sits above the wordmark rather than beside it. Beside it, the
   rocket took a share of the column that the wordmark's size could not know
   about, so the name ran past the column's padding by about 9px at every
   width. Stacked, the wordmark has the whole column to size itself against. */
.datastar-mark {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-3);
	text-decoration: none;
}

.datastar-wordmark {
	justify-items: center;

	/*
	One size drives both lines, and it is measured against the banner rather
	than the window. The wordmark does not wrap -- a hypermedia framework
	broken across two lines is not a wordmark -- so on a narrow card it has to
	shrink instead, and the window's width says nothing about how much room
	this card has.
	*/
	/* DATASTAR sets about 12.4px wide for every 1px of this size, measured, so
	   it fits a column at 8.06cqi. 7.8 leaves a few pixels rather than landing
	   exactly on the padding. */
	font-size: clamp(0.62rem, 7.8cqi, 1.5rem);

	display: grid;
	gap: 0.15em;
	/* Tilted back, the way the homepage sets it. It comes off on a narrow card,
	   where the type is small enough that the perspective only blurs it. */
	transform: perspective(40rem) rotateX(14deg);
	transform-origin: center bottom;
	user-select: none;
}

.datastar-name,
.datastar-tagline {
	font-family: "Audiowide", var(--font-display);
	line-height: 1;
	text-transform: uppercase;
	white-space: nowrap;
}

.datastar-name {
	color: var(--ds-mark);
	font-size: 1.9em;
	letter-spacing: 0.02em;
	text-shadow: 0 2px 0 rgb(0 0 0 / 35%);
}

.datastar-tagline {
	color: var(--ds-ink);
	font-size: 0.64em;
	letter-spacing: 0.04em;
}

/* Pixel art, drawn without smoothing: the rocket is a 24 unit grid, and a
   browser's default interpolation turns that to mush when it is scaled up. */
.datastar-mark img {
	flex: none;
	/* Whole steps of the 24 unit grid, so the pixels stay square. */
	inline-size: clamp(24px, 14cqi, 56px);
	block-size: clamp(24px, 14cqi, 56px);
	image-rendering: pixelated;
}

/* The rocket flies, or holds still for a reader who asked for less motion. */
.datastar-mark .rocket-static {
	display: none;
}

@media (prefers-reduced-motion: reduce) {
	.datastar-mark .rocket-animated {
		display: none;
	}

	.datastar-mark .rocket-static {
		display: block;
		image-rendering: auto;
	}
}

/*
The two names, each in its own voice.

This card is the one place the two identities meet, so the typography says so:
the Stardust sentence is set in Stardust's display face and the mark beside it
in Datastar's, with the space between them left as space. It is a statement
rather than a caption, so it is sized like one.

Held to a short measure on purpose. It wraps to two or three lines and reads as
a block against the mark, instead of running the width of the card as one thin
line of text.
*/
.team-lede {
	margin: 0;
	color: var(--ds-ink);
	font-family: var(--font-display);
	font-size: clamp(0.95rem, 5.2cqi, 1.35rem);
	line-height: 1.35;
	text-wrap: balance;
}

@media (max-width: 43.99rem) {
	.datastar-wordmark {
		transform: none;
	}
}

.team-body {
	padding: var(--space-5);
	/* The grid the rocket is drawn on, at the size the card draws it. */
	background:
		repeating-linear-gradient(90deg, var(--ds-grid) 0 1px, transparent 1px calc(var(--px) * 4)),
		repeating-linear-gradient(var(--ds-grid) 0 1px, transparent 1px calc(var(--px) * 4)),
		var(--ds-void);

	p {
		color: var(--ds-pale);
	}

	/* The first line is the claim and the rest is the case for it, so it opens
	   a step above them. */
	p:first-child {
		color: var(--ds-ink);
		font-size: 1.1rem;
	}

	/* Block rather than the stack's flex column, so it can set in more than
	   one column; the paragraphs carry their own spacing instead of a gap. */
	.argument {
		display: block;
		max-inline-size: none;
	}

	.argument > p {
		margin: 0 0 var(--space-4);
		break-inside: avoid;
	}

	.argument > p:last-child {
		margin-block-end: 0;
	}
}

/* Below the width where two columns of anything are readable, the identity
   becomes the card's header and the argument follows it. */
@media (max-width: 47.99rem) {
	.team-story {
		grid-template-columns: minmax(0, 1fr);
	}
}

.faq {
	--split-main: minmax(0, 0.6fr);
	--split-side: minmax(0, 1.4fr);

	border-block: 1px solid var(--color-line-low);
	padding: var(--space-4);

	@media (min-width: 44rem) {
		padding: var(--space-6);
	}

	details {
		border-block-start: 1px solid var(--color-line-low);

		&:last-child {
			border-block-end: 1px solid var(--color-line-low);
		}

		p {
			max-inline-size: 45rem;
			padding: 0 var(--space-2) var(--space-4);

			@media (min-width: 44rem) {
				padding: 0 var(--space-6) var(--space-4) var(--space-2);
			}
			color: var(--color-text-dim);
		}
	}

	summary {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto;
		gap: var(--space-4);
		padding: var(--space-4) var(--space-2);
		color: var(--color-text-strong);
		cursor: pointer;
		list-style: none;

		&::-webkit-details-marker {
			display: none;
		}

		&::after {
			content: "+";
			color: var(--color-action);
			font-family: var(--font-data);
		}

		&:hover {
			color: var(--color-sand);
		}
	}

	details[open] summary::after {
		content: "\2212";
	}
}

.waitlist {
	--split-side: minmax(0, 26rem);

	align-items: center;
	padding: var(--space-4);

	@media (min-width: 44rem) {
		padding: var(--space-6);
	}
}

.waitlist-field {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--space-2);

	@media (min-width: 44rem) {
		grid-template-columns: minmax(0, 1fr) auto;
	}
}

.site-footer {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--space-4);
	border-block-start: 1px solid var(--color-line-low);
	padding-block-start: var(--space-5);

	a {
		--track: 0.1em;
		color: var(--color-text-dim);
		font-size: 0.66rem;
		text-decoration: none;

		&:hover {
			color: var(--color-info);
		}
	}
}

/*
Articles (Theory): a list of articles beside one article in a panel, stacked
on narrow screens. The list stays in view while an article scrolls; headings
link to themselves.
*/
.article-layout {
	--split-main: 14rem;

	align-items: start;
}

.article-nav {
	display: grid;
	gap: var(--space-3);

	@media (min-width: 60rem) {
		position: sticky;
		inset-block-start: calc(var(--topbar-height) + var(--space-5));
		max-block-size: calc(100dvh - var(--topbar-height) - 2 * var(--space-5));
		overflow-y: auto;
		scrollbar-color: rgb(var(--ch-glow) / 22%) transparent;
		scrollbar-width: thin;

		&::-webkit-scrollbar {
			inline-size: 3px;
		}

		&::-webkit-scrollbar-track {
			background: transparent;
		}

		&::-webkit-scrollbar-thumb {
			border-radius: 999px;
			background: rgb(var(--ch-glow) / 22%);
		}

		&::-webkit-scrollbar-thumb:hover {
			background: rgb(var(--ch-glow) / 48%);
		}
	}

	ol {
		display: grid;
		gap: 2px;
		padding: 0;
		list-style: none;
	}

	a,
	span {
		display: block;
		border-inline-start: 2px solid transparent;
		--track: 0.08em;
		padding: var(--space-1) var(--space-3);
		font-size: 0.72rem;
		text-decoration: none;
	}

	a {
		color: var(--color-text-dim);

		&:hover {
			border-color: var(--color-line);
			color: var(--color-text-strong);
		}

		&[aria-current="page"] {
			border-color: var(--color-glow);
			background: linear-gradient(90deg, rgb(var(--ch-edge) / 22%), transparent);
			color: var(--color-text-strong);
		}
	}

	span {
		color: var(--color-text-dim);
		opacity: 0.55;
	}
}

/* A category title above its pages in the rail. The first one sits under the
   kicker; a later one leaves space after the pages above it. */
.article-nav-category {
	margin-block: var(--space-2) calc(-1 * var(--space-2));
	border-inline-start: 2px solid transparent;
	padding-inline: var(--space-3);
	--track: 0.14em;
	color: var(--color-sand);
	font-size: 0.62rem;
	opacity: 0.8;
}

/* The current page's own sections, under its link in the rail. */
.article-subnav {
	margin-block: 2px;
	margin-inline-start: var(--space-3);
	border-inline-start: 1px solid var(--color-line-low);

	a {
		padding-block: 1px;
		padding-inline: var(--space-3);
		border-inline-start: 2px solid transparent;
		color: var(--color-text-dim);
		font-family: var(--font-text);
		font-size: 0.78rem;
		letter-spacing: 0;
		text-transform: none;

		&:hover {
			border-color: var(--color-glow);
			color: var(--color-text-strong);
		}
	}
}

.article {
	min-inline-size: 0;
	max-inline-size: 52rem;
	padding: var(--space-4) var(--space-3);

	@media (min-width: 44rem) {
		padding: var(--space-4) var(--space-6);
	}

	/* .article-body holds the article in one document format. */
	:is(&, .article-body) > * + * {
		margin-block-start: var(--space-4);
	}

	/* A result belongs to the example above it, so it sits against that
	example and leaves more space before the next block. */
	.result {
		margin-block-start: var(--space-2);
		margin-block-end: var(--space-6);
	}

	/* A result or an error belongs to the example above it. Another alert
	after an example, such as a note or a checkpoint, keeps its own space. */
	:is(.code-block + .result, .code-block + .alert.danger) {
		margin-block-start: 0;
	}

	.code-block + .alert.danger {
		margin-block-end: var(--space-6);
	}

	:is(&, .article-body) > :where(p, ul, ol, blockquote) {
		max-inline-size: 65ch;
	}

	p,
	li {
		line-height: 1.7;
	}

	li + li {
		margin-block-start: var(--space-2);
	}

	h1 {
		margin-block-start: var(--space-5);
	}

	h2 {
		margin-block-start: var(--space-7);
		border-block-start: 1px solid var(--color-line-low);
		padding-block-start: var(--space-5);
		font-size: 1.3rem;
	}

	:not(pre) > code {
		border: 1px solid var(--color-line-low);
		padding: 0.05em 0.3em;
		background: rgb(var(--ch-info) / 8%);
		color: var(--color-info);
		overflow-wrap: anywhere;
	}

}

.option-cards {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3);
}

.option-card {
	flex: 1 1 19rem;
	min-inline-size: 0;
	padding: var(--space-3);
	border: 1px solid var(--color-line-low);
	background: rgb(var(--ch-info) / 4%);
	overflow-wrap: anywhere;

	h3 {
		margin: 0;
		font-size: 1rem;
		font-family: var(--font-data);
		text-transform: none;
		letter-spacing: normal;
	}

	> * + * {
		margin-block-start: var(--space-2);
	}

	ul {
		padding: 0;
		list-style: none;
	}

	li + li {
		margin-block-start: 0;
	}
}

/*
Tables are HUD readouts: header cells are display labels over a glowing rule,
rows sit on faint lines with a light stripe and a hover lift, and .num cells
align figures. Wrap a table in .table-scroll (Markdown tables already are) so
wide ones scroll inside their frame.
*/
.table-scroll {
	max-inline-size: 100%;
	overflow-x: auto;
	overscroll-behavior-inline: contain;
	border: 1px solid var(--color-line-low);
	background: rgb(var(--ch-ink) / 70%);
}

table {
	inline-size: 100%;
	border-collapse: collapse;
	font-size: 0.9rem;
}

.table-scroll > table {
	min-inline-size: 42rem;
}

.reference .table-scroll > table {
	min-inline-size: 0;
	table-layout: auto;
}

th,
td {
	padding: var(--space-2) var(--space-3);
	overflow-wrap: normal;
	word-break: normal;
	text-align: start;
	vertical-align: top;
}

th {
	border-block-end: 1px solid var(--color-glow);
	background: linear-gradient(rgb(var(--ch-edge) / 16%), rgb(var(--ch-edge) / 4%));
	--track: 0.1em;
	color: var(--color-info);
	font-size: 0.68rem;
	font-weight: 400;
	white-space: nowrap;
}

td {
	border-block-end: 1px solid var(--color-line-low);
}

/* Identifiers are one unit. The table scrolls rather than splitting a member
name into arbitrary fragments on a narrow screen. */
.table-scroll code {
	overflow-wrap: normal;
	white-space: nowrap;
}

tr:nth-child(even) td {
	background: rgb(var(--ch-edge) / 3%);
}

tr:hover td {
	background: rgb(var(--ch-edge) / 10%);
	color: var(--color-text-strong);
}

tr:last-child td {
	border-block-end: 0;
}

:is(th, td).num {
	text-align: end;
	font-variant-numeric: tabular-nums;
}

/* Mermaid SVGs from task web:theory:diagrams, which drops Mermaid's own colors.
Diagrams are holographic blueprints: a grid with glow ticks at two corners,
projected from a glowing bottom emitter with faint scanlines, so they read
apart from glass panels and bracketed code. */
.diagram {
	overflow-x: auto;
	border: 1px solid var(--color-line);
	border-block-end: 2px solid var(--color-glow);
	padding: var(--space-5);
	background:
		linear-gradient(var(--color-glow), var(--color-glow)) top left / 14px 2px no-repeat,
		linear-gradient(var(--color-glow), var(--color-glow)) top left / 2px 14px no-repeat,
		linear-gradient(var(--color-glow), var(--color-glow)) bottom right / 14px 2px no-repeat,
		linear-gradient(var(--color-glow), var(--color-glow)) bottom right / 2px 14px no-repeat,
		repeating-linear-gradient(transparent 0 3px, rgb(var(--ch-glow) / 4%) 3px 4px),
		radial-gradient(ellipse at 50% 100%, rgb(var(--ch-glow) / 18%), transparent 70%),
		repeating-linear-gradient(90deg, rgb(var(--ch-edge) / 9%) 0 1px, transparent 1px 24px),
		repeating-linear-gradient(rgb(var(--ch-edge) / 9%) 0 1px, transparent 1px 24px),
		rgb(var(--ch-ink) / 88%);
	box-shadow: 0 6px 18px -8px var(--color-glow);

	svg {
		display: block;
		margin-inline: auto;
		block-size: auto;
		font-family: var(--font-text);
	}

	.node :is(rect, polygon, circle, path) {
		fill: var(--color-deep);
		stroke: var(--color-edge);
		stroke-width: 1px;
	}

	/* Mermaid's label groups are not the theme's HUD .label. */
	.label {
		color: inherit;
		font: inherit;
		letter-spacing: normal;
		text-transform: none;
	}

	foreignObject {
		overflow: visible;
	}

	/* Mermaid measured labels in a wider fallback font, so a label can take
	fewer lines here than its box allows: center it (overriding inline styles). */
	foreignObject > div {
		display: flex !important;
		align-items: center;
		justify-content: center;
		block-size: 100%;
	}

	/* HTML labels were measured at Mermaid's 1.5 line height. */
	.nodeLabel {
		color: var(--color-text);

		p {
			margin: 0;
			line-height: 1.5;
		}
	}

	.edgeLabel,
	.edgeLabel p,
	.labelBkg {
		background: var(--color-void);
		color: var(--color-text-dim);
	}

	text {
		fill: var(--color-text);
	}

	.flowchart-link {
		fill: none;
		stroke: var(--color-glow);
		stroke-width: 1.5px;
		filter: drop-shadow(0 0 3px rgb(var(--ch-glow) / 55%));
	}

	marker path {
		fill: var(--color-glow);
		stroke: none;
	}

	.edgeLabel rect {
		fill: var(--color-void);
	}

	.edgeLabel text {
		fill: var(--color-text-dim);
	}
}

.heading-anchor {
	color: inherit;
	text-decoration: none;

	&:hover .heading-anchor-mark,
	&:focus-visible .heading-anchor-mark {
		opacity: 1;
	}
}

.heading-anchor-mark {
	margin-inline-start: 0.35em;
	color: var(--color-info);
	font-size: 0.6em;
	opacity: 0.4;
}

.article-pagination {
	display: flex;
	/* The two links carry the titles of the pages either side, and on a phone
	the pair is wider than the column they share: the quickstart's Next is a
	button, which never shrinks. They take a line each instead of pushing the
	document sideways, and each may shrink below the width of its own title. */
	flex-wrap: wrap;
	justify-content: space-between;
	gap: var(--space-4);
	padding-block: var(--space-3);

	&:first-child {
		border-block-end: 1px solid var(--color-line-low);
	}

	&:last-child {
		margin-block-start: var(--space-7);
		border-block-start: 1px solid var(--color-line-low);
	}

	a,
	span[aria-disabled] {
		display: grid;
		min-inline-size: 0;
		--track: 0.06em;
		gap: var(--space-1);
		font-size: 0.8rem;
		text-decoration: none;
	}

	span[aria-disabled] {
		color: var(--color-text-dim);
		opacity: 0.55;
	}

	.article-next {
		margin-inline-start: auto;
		text-align: end;
	}
}

/* Indicators */

.progress {
	block-size: 6px;
	background: var(--color-track);
	box-shadow: inset 0 0 0 1px var(--color-line-low);

	> span {
		display: block;
		block-size: 100%;
		background: linear-gradient(90deg, var(--color-edge), var(--color-glow));
		box-shadow: 0 0 8px rgb(var(--ch-glow) / 60%);
	}
}

/* The base is the info alert, so `class="alert info"` and `class="alert"`
paint the same; success, warning and danger recolour the edge, the wash and
the icon. */
.alert {
	/* A variant is a colour and its channel, and nothing else: the edge, the
	tint behind the text and the icon are written once below. */
	--alert-accent: var(--color-info);
	--alert-tint: var(--ch-info);

	display: flex;
	align-items: flex-start;
	gap: var(--space-3);
	border-inline-start: 3px solid var(--alert-accent);
	padding: var(--space-3) var(--space-4);
	background: linear-gradient(90deg, rgb(var(--alert-tint) / 12%), transparent);

	> .icon {
		inline-size: 1.75rem;
		block-size: 1.75rem;
		color: var(--alert-accent);
	}

	> div {
		display: flex;
		flex-direction: column;
		gap: var(--space-1);
		min-inline-size: 0;
	}

	/* A named variant colours all four edges, as it always has, though only
	the inline-start one has width. The base alert is the info alert; .info
	names it, so the class the runner writes (src/runner/panel.html.runes) is
	not silently inert. */
	&:is(.info, .success, .warning, .danger) {
		border-color: var(--alert-accent);
	}

	&.info {
		--alert-accent: var(--color-info);
		--alert-tint: var(--ch-info);
	}

	&.success {
		--alert-accent: var(--color-success);
		--alert-tint: var(--ch-success);
	}

	&.warning {
		--alert-accent: var(--color-warning);
		--alert-tint: var(--ch-warning);
	}

	&.danger {
		--alert-accent: var(--color-danger);
		--alert-tint: var(--ch-danger);
	}
}

.swatches {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
	gap: var(--space-3);
}

.swatch {
	display: flex;
	flex-direction: column;
	gap: var(--space-1);
	font-size: 0.8rem;

	&::before {
		block-size: 2.5rem;
		background: var(--swatch);
		box-shadow: inset 0 0 0 1px rgb(var(--ch-paper) / 12%);
		content: "";
	}
}

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

	/* A duration does not reach scrolling, so an anchor jumps again here. */
	html {
		scroll-behavior: auto;
	}
}

@media (forced-colors: active) {
	.modal,
	.panel,
	.row,
	.field,
	.button,
	.tabs {
		border-image: none;
		border-color: CanvasText;
	}

	.tab:is([aria-selected="true"], [aria-pressed="true"], [aria-current="true"]) {
		outline: 2px solid Highlight;
	}
}

.icon-edn { --icon: url("/assets/icons/mdi-code-parentheses-b4022aa393f742348452c3a8f634f78b6b0e8349a3f9fd753eedd561613b38c3.svg"); }
.icon-tron { --icon: url("/assets/icons/lucide-binary-0e3edeae0e0db04efdefc52fb5642578c3cca4a31a1a0312bcc318c8bd2a6e99.svg"); }
/* Devicon icons: revision 7330accdbc47e2dc0c19789a48533c4a3c50fe58; MIT. */
.icon-devicon-rust, .icon-devicon-clojure, .icon-devicon-c, .icon-devicon-python { background-color: transparent; background-position: center; background-repeat: no-repeat; background-size: contain; mask: none; }
.icon-devicon-rust { background-image: url("/assets/icons/devicon-rust-16c6c6a98e990944fc5e1cc64cc8fc7b8be3a662ec4ee7951b7995d2e52f5e12.svg"); }
:root:not([data-theme="light"]) .icon-devicon-rust { filter: invert(1); }
.icon-devicon-clojure { background-image: url("/assets/icons/devicon-clojure-405b1b05bbbdd7ddfdbe331fec17360de2b6b482b7f67aa66540fa12e6e73e25.svg"); }
.icon-devicon-c { background-image: url("/assets/icons/devicon-c-8cce48da91084256f15653e02bfd7046ccb34489d030cb1aa34dee766daad6b7.svg"); }
.icon-devicon-python { background-image: url("/assets/icons/devicon-python-71493b4a732f0532b3a03a7a95da3ee11a65766367c8303b901ffbe7bf91a3a2.svg"); }
.icon-odin { background: transparent; mask: none; font-weight: 700; text-align: center; }
/* The six-language strip keeps the prototype's local tab scrolling on phones. */
.topbar-languages { max-inline-size: 100%; min-inline-size: 0; }
.topbar-languages .toggle { max-inline-size: 100%; }
.topbar-tools { min-inline-size: 0; max-inline-size: 100%; box-sizing: border-box; }
.language-example { display: grid; grid-template-columns: minmax(0, 1fr); min-inline-size: 0; gap: var(--space-2); }
.language-example .code-block { margin-block: 0; }

/* Material Icon Theme language marks retain their original colours. */
[class*="icon-material-icon-theme-"] {
	background-color: transparent;
	background-position: center;
	background-repeat: no-repeat;
	background-size: contain;
	mask: none;
}
.icon-material-icon-theme-go { background-image: url("/assets/icons/material-icon-theme-go-0daa0ded4505e9feacdeddab0c0275766755cce861c622b2c5814a7f59038e25.svg"); }
.icon-material-icon-theme-rust { background-image: url("/assets/icons/material-icon-theme-rust-4205a1ae5d92cd4b2f171124715931011facb497c4e08dc5368a21bc37179736.svg"); }
.icon-material-icon-theme-clojure { background-image: url("/assets/icons/material-icon-theme-clojure-191be9806af22f272a68362c0cb6b5df9af34bf64b09130d7202991d4ca6f4ae.svg"); }
.icon-material-icon-theme-odin { background-image: url("/assets/icons/material-icon-theme-odin-31cdb7833afc283f8ea3fb05b2452a27626a091cef01ef356b379f7656baa190.svg"); }
.icon-material-icon-theme-c { background-image: url("/assets/icons/material-icon-theme-c-291a7efdcccd45e5ff16ef93c145d2c1d66bc6f6ca33442e22f862b8b93858e4.svg"); }
.icon-material-icon-theme-python { background-image: url("/assets/icons/material-icon-theme-python-1b0ac39749aabc11bdff036e7f7676fe95533573b411bc8fbdc938a9aef34af0.svg"); }

/* Converter controls belong to their pane, not a wrapping page-wide toolbar. */
.dustfmt-page {
	.dustfmt-example {
		display: flex;
		align-items: center;
		gap: var(--space-3);
		flex-wrap: wrap;

		> label {
			align-items: center;
		}
	}

	.dustfmt-input-controls {
		grid-area: doccontrols;
		min-inline-size: 0;
	}

	.dustfmt-output-controls {
		grid-area: rescontrols;
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: var(--space-3);
		min-inline-size: 0;

	}

	input[type="file"] {
		min-inline-size: 0;
		max-inline-size: 100%;
	}

	.pane-head {
		flex-wrap: wrap;
	}

	.pane-body-document > [data-tron-view="visualizer"] {
		flex: 1;
		min-block-size: 0;
		min-inline-size: 0;
		overflow: auto;
	}

	.dustfmt-result {
		display: flex;
		flex-direction: column;
		flex: 1;
		min-block-size: 0;
		min-inline-size: 0;
		padding: var(--space-4);
		overflow: hidden;

		> [data-tron-view] {
			display: flex;
			flex-direction: column;
			flex: 1;
			min-block-size: 0;
			min-inline-size: 0;
			overflow: visible;
		}

		figure.code-block {
			display: flex;
			flex-direction: column;
			flex: 1;
			min-block-size: 0;
			min-inline-size: 0;
			overflow: visible;

			pre.panel {
				flex: 1;
				min-block-size: 0;
				min-inline-size: 0;
				overflow: auto;
			}

			&.tron {
				overflow: visible;

				> :is(table, details) {
					flex-shrink: 0;
				}

				> table:first-of-type {
					display: block;
					flex: 1;
					min-block-size: 0;
					overflow: auto;
				}
			}
		}
	}

	.control-pair {
		flex-wrap: nowrap;

		/* Four format buttons fit the phone pane when its label has its own row. */
		@media (max-width: 47.999rem) {
			flex-direction: column;
			align-items: flex-start;
		}
	}

	.grid.panes {
		grid-template-areas:
			"doccontrols"
			"dochead"
			"doc"
			"rescontrols"
			"reshead"
			"res";

		@media (min-width: 48rem) {
			grid-template-areas:
				"doccontrols rescontrols"
				"dochead reshead"
				"doc res";
		}

		@media (min-width: 48rem) and (min-height: 40rem) {
			grid-template-rows: auto auto minmax(0, 1fr);
		}
	}
}
