/* Roswaal editor styling. Light and dark are the same layout with swapped
   tokens; only the palette lives in the media query. */

:root {
	color-scheme: light dark;

	--bg-app: #eceef2;
	--bg-panel: #f6f7f9;
	--bg-canvas: #d9dce3;
	--bg-input: #ffffff;
	--bg-hover: rgba(0, 0, 0, 0.06);
	--bg-active: rgba(0, 0, 0, 0.12);

	--fg: #1c1f24;
	--fg-muted: #5c636e;
	--fg-faint: #8b93a0;

	--border: #c6cad2;
	--border-strong: #a8aeb9;

	--grid-fine: rgba(0, 0, 0, 0.07);
	--grid-coarse: rgba(0, 0, 0, 0.14);
	--watermark: rgba(0, 0, 0, 0.16);

	--node-body: #fbfbfd;
	--node-border: #b3b9c4;
	--node-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);

	/* Pin geometry. One slot size for every pin, because the slot is what lines
	   the labels up down the side of a node; the shape inside it varies by kind.
	   The slot is also what the row *reserves*: a pin is drawn on the node's
	   edge rather than inside it, and the space it used to occupy stays, so the
	   labels and value fields keep their columns. `--pin-pad` is the row's own
	   side padding, which is how far the slot starts in from the edge.

	   `NODE.pinSlot`, `NODE.rowPadding` and `NODE.execAspect` in layers.ts are
	   the same three numbers; the documentation's node previews are drawn from
	   those. Change one and change the other. */
	--pin-slot: 16px;
	--pin-pad: 6px;
	/* The node's own border, named because a pin has to be placed *through* it.
	   A wire ends on the outside of the border; a row is laid out inside it, so
	   every offset below carries this back or the pin lands a pixel off the
	   thing it is wired to. One pixel, and visible: the border shows as a sliver
	   down one side of the dot. */
	--node-stroke: 1px;
	/* How much of the row a pin still takes, now that most of it is outside the
	   node. The dot reaches 6px in from the border; this is that plus air, and
	   the label starts where the pin visibly stops rather than where the old
	   16px slot used to end. It is the *layout*, not the pin: the pin is still
	   drawn `--pin-slot` across. */
	--pin-lane: 10px;
	/* An execution pin's triangle: equilateral, pointing right, as tall as the
	   slot -- so √3/2 as wide. This plus the gap below is how far it hangs past
	   the node. */
	--exec-width: calc(var(--pin-slot) * 0.866);
	/* Daylight between the triangle and the node it belongs to. Touching, the
	   two read as one shape with a bite out of it; apart, the pin reads as
	   something hung on the node, and the wire is visible arriving at it. */
	--exec-gap: 5px;
	/* How much smaller the hollow inside an unwired execution pin is than the
	   arrow around it. An equilateral triangle shrinks towards its centre, which
	   sits a third of the way in from the base, so the four insets differ while
	   the wall they leave does not; 0.433 is a 2px wall at a 16px slot. */
	--exec-shrink: 0.433;
	/* A node's corner. A preference rather than a theme token — `theme.ts` sets
	   it to 0 when square corners are chosen — which is why it is a variable at
	   all rather than the literal it used to be. Capsules and reroute knots
	   deliberately do not read it: their shapes are what say "value" and "just a
	   bend", and squaring those would delete the signal rather than restyle it. */
	--node-radius: 7px;
	/* What shows through an unwired execution triangle: the surface it is drawn
	   against, so it reads as empty rather than as a lighter shade of itself.
	   A knot and a capsule override it, because they are not a node body. */
	--pin-hollow: var(--node-body);
	/* The well a data pin sits in, and the thin ring of it drawn just outside
	   the pin's coloured edge.
	 *
	   Darker than the canvas in both themes, and deliberately not either surface
	   the pin touches: the pin straddles the node's border, so a fill taken from
	   the body or the canvas would change shade across the middle of the dot.
	   The ring does the same job on the outside — the pin keeps its colour
	   against a pale node body and a dark canvas alike — and is thinner than the
	   coloured edge, so it reads as a socket the pin is set into rather than as
	   a second outline competing with the first. */
	--pin-well: #2a2f37;

	--comment-fill: 0.1;
	--comment-header-alpha: 0.75;

	--wire-exec: #5a616d;

	--accent: #3b6ea5;
	--danger: #c0392b;
	--warning: #b8860b;
	--ok: #2e7d47;

	--select: #ff9c2e;
}

@media (prefers-color-scheme: dark) {
	:root {
		color-scheme: dark;

		--bg-app: #16181d;
		--bg-panel: #1b1e24;
		--bg-canvas: #101216;
		--bg-input: #23272f;
		--bg-hover: rgba(255, 255, 255, 0.07);
		--bg-active: rgba(255, 255, 255, 0.13);

		--fg: #e4e7ec;
		--fg-muted: #9aa2af;
		--fg-faint: #6b7280;

		--border: #2d323b;
		--border-strong: #414855;

		--grid-fine: rgba(255, 255, 255, 0.05);
		--grid-coarse: rgba(255, 255, 255, 0.1);
		--watermark: rgba(255, 255, 255, 0.14);

		--node-body: #262a32;
		--node-border: #3a404b;
		--node-shadow: 0 3px 10px rgba(0, 0, 0, 0.45);
		/* Still darker than the canvas, or the well stops reading as one. */
		--pin-well: #06080b;

		--comment-fill: 0.14;

		--wire-exec: #b9c0cc;

		--accent: #5b9bd5;
		--danger: #e06c5a;
		--warning: #d8a83a;
		--ok: #5cb87a;
	}
}

* {
	box-sizing: border-box;
}

html,
body,
#root {
	height: 100%;
	margin: 0;
}

body {
	background: var(--bg-app);
	color: var(--fg);
	font: 13px/1.45 "Segoe UI", system-ui, -apple-system, sans-serif;
	overflow: hidden;
	-webkit-font-smoothing: antialiased;
}

button,
input,
select,
textarea {
	font: inherit;
	color: inherit;
}

/* The list a <select> opens is a browser widget, not part of this page, and it
 * was rendering light under a dark app — unhovered entries close to invisible,
 * because only the hovered one got a system highlight to rescue it.
 *
 * **The colours here are literals on purpose.** Chromium paints that popup
 * outside the document, and custom properties do not resolve there: with
 * `var(--bg-input)` the popup stays light, and with the same colour written out
 * it does not. The computed style on the option is correct either way, so this
 * is invisible to devtools and only shows up by opening the thing and looking.
 *
 * Which means these four values are a copy of four tokens and will not follow
 * them. They are the input surface in each theme; change one, change the other.
 */
select option,
select optgroup {
	background-color: #ffffff;
	color: #1c1f24;
}

@media (prefers-color-scheme: dark) {
	select option,
	select optgroup {
		background-color: #23272f;
		color: #e4e7ec;
	}
}

/* ------------------------------------------------------------------ shell */

/* A column rather than a grid with named rows, because one of the rows comes
   and goes: the document bar is only there when a document is open, and fixed
   `grid-template-rows` would hand the workspace the wrong row when it is not. */
.app {
	display: flex;
	flex-direction: column;
	height: 100%;
}

/* ------------------------------------------------------------- workspace */

/* Three docks around a centre. The track sizes come from `gridTemplate()` as
   inline styles, because they are state; only the *shape* is here.

   Every area is named, and every child names the area it sits in. That is not
   tidiness: `display: none` stops an element being a grid item, and
   auto-placement then closes up behind it — with both docks hidden, an
   unpinned centre lands in the left dock's track and takes its width, while
   every piece of state remains correct. See docs/PANELS.md. */
.workspace {
	flex: 1;
	display: grid;
	position: relative;
	grid-template-areas:
		"left        split-left   centre        split-right  right"
		"split-bottom split-bottom split-bottom split-bottom split-bottom"
		"bottom      bottom       bottom        bottom       bottom";
	min-height: 0;
	min-width: 0;
}

/* A dock is a container and nothing more: its panels carry their own look, so
   moving one between docks does not change how it is drawn. The exception is
   the left dock, below, which inherited the old sidebar's surface. */
.dock {
	display: flex;
	flex-direction: column;
	/* Both of these are load-bearing. A grid item's automatic minimum size is
	   its content, so a dock holding one long path would refuse to shrink below
	   the width of that path — the splitter would drag outwards and never come
	   back. */
	min-width: 0;
	min-height: 0;
	overflow: hidden;
}

.dock.left {
	background: var(--bg-panel);
	overflow: auto;
}

/* The bottom dock is sized by its content -- see `gridTemplate` -- which was
   safe while its only panel capped its own height. Once panels can be dragged
   into it, anything can be down there: a graph with thirty variables would grow
   the row until the centre had nothing left, and the centre's minimum is only
   enforced for sizes the layout stores.
   
   So the row is content-sized up to a share of the window, and scrolls past it. */
.dock.bottom {
	max-height: 45vh;
	overflow: auto;
}

.panel {
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
}

/* The tree grows; the variables panel takes what it needs. Without this the
   two split the column evenly and the tree gets half a screen it does not
   want. */
.dock.left .panel-tree {
	flex: 1;
}

/* The tree scrolls inside its own panel. Without this the panel could shrink
   and the tree inside it would not, so a tree taller than its share ran down
   over the Variables panel beneath -- on an iPad's drawer every time, and on
   a desktop whenever the window was short enough. Variables keeps the height
   of what it lists, up to a share of the dock, and scrolls past that. */
.dock .panel-tree .tree {
	min-height: 0;
	overflow: auto;
}

.dock.left .panel-variables {
	flex: 0 1 auto;
	max-height: 60%;
	overflow: auto;
}

.dock.right .panel,
.dock.bottom .panel {
	flex: 1;
	overflow: hidden;
}

/* The grab handle between a dock and the centre. Five pixels wide, with a
   larger hit area pushed out either side by a transparent border — a strip you
   can see is thinner than a strip you can reliably hit, and making the visible
   one wider costs space from the graph. */
.splitter {
	background: var(--border);
	position: relative;
	z-index: 1;
	touch-action: none;
}

.splitter:hover,
.splitter:active {
	background: var(--accent);
}

.splitter.col {
	cursor: col-resize;
	border-left: 3px solid transparent;
	border-right: 3px solid transparent;
	background-clip: padding-box;
	margin: 0 -3px;
}

.splitter.row {
	cursor: row-resize;
	border-top: 3px solid transparent;
	border-bottom: 3px solid transparent;
	background-clip: padding-box;
	margin: -3px 0;
}

.splitter.left { grid-area: split-left; }
.splitter.right { grid-area: split-right; }
.splitter.bottom { grid-area: split-bottom; }

/* The centre always exists and is what the docks surround. Relative so the
   compile toast anchors to the graph area itself — which makes "bottom right
   of the graph" true by construction rather than by subtracting the
   inspector's width from the workspace. */
.centre {
	grid-area: centre;
	position: relative;
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
}

/* The graph, node map, source view or placeholder — whatever the centre is
   showing, under the tab strip. Its own element so the strip can come and go
   without the content needing to know which grid row it is in. */
.centre-body {
	flex: 1;
	display: grid;
	grid-template-rows: minmax(0, 1fr);
	min-width: 0;
	min-height: 0;
	/* The floating tools are positioned against this, over whatever it shows. */
	position: relative;
}

/* ---------------------------------------------------------- floating tools */

/* Along the top edge of the view. The container takes no pointer events, so
   the gaps between groups are still the canvas. See FloatingTools.tsx. */
.floating-tools {
	position: absolute;
	top: 8px;
	left: 10px;
	right: 10px;
	z-index: 5;
	display: flex;
	align-items: flex-start;
	flex-wrap: wrap;
	gap: 8px;
	pointer-events: none;
}

.floating-tools .spacer {
	flex: 1;
}

.tool-group {
	pointer-events: auto;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 6px;
	padding: 4px 6px;
	background: color-mix(in srgb, var(--bg-panel) 94%, transparent);
	border: 1px solid var(--border);
	border-radius: 8px;
	box-shadow: 0 4px 14px rgb(0 0 0 / 28%);
}

.tool-group .divider {
	width: 1px;
	align-self: stretch;
	margin: 3px 2px;
	background: var(--border-strong);
}

.tool-group .doc-name {
	font-weight: 600;
	margin: 0 2px 0 4px;
}

.tool-group .dirty::after {
	content: "•";
	color: var(--warning);
	margin-left: 5px;
}

.tool-group .doc-of {
	font-weight: 400;
	color: var(--fg-muted);
}

/* Unsaved edits, when the name that would carry the dot is hidden. */
.tool-group .doc-dirty {
	width: 6px;
	height: 6px;
	margin: 0 2px 0 4px;
	border-radius: 50%;
	background: var(--warning);
}

.tool-group select.doc-target.lune {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 60%, transparent);
}

/* While a panel is in flight, nothing in the workspace is selectable. The
   selection is also cleared once when the drag begins; this stops a new one
   starting under the pointer as it travels. */
.workspace.dragging {
	user-select: none;
	cursor: grabbing;
}

/* Where a dragged panel would land. Over everything, hit by nothing — an
   overlay that accepted pointer events would be the answer `elementFromPoint`
   gave for every position under it, which is every position. */
.drop-preview {
	position: absolute;
	z-index: 30;
	pointer-events: none;
	background: color-mix(in srgb, var(--accent) 22%, transparent);
	border: 2px solid var(--accent);
}

/* A panel heading is a handle, so it says so on the way past. The panels'
   headings keep every job they already had — the Add button still adds, the
   diagnostics bar still collapses — because nothing is a drag until the
   pointer has actually moved. */
.dock h2,
.dock .bar,
.float-panel h2,
.float-panel .bar {
	cursor: grab;
}

.dock h2:active,
.dock .bar:active,
.float-panel h2:active,
.float-panel .bar:active {
	cursor: grabbing;
}

/* ------------------------------------------------------------- graph tabs */

.graph-tabs {
	display: flex;
	align-items: stretch;
	gap: 1px;
	flex: none;
	background: var(--bg-app);
	border-bottom: 1px solid var(--border);
	overflow-x: auto;
	overflow-y: hidden;
}

.graph-tab {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 5px 8px 5px 11px;
	background: var(--bg-panel);
	border-right: 1px solid var(--border);
	color: var(--fg-muted);
	cursor: pointer;
	white-space: nowrap;
	max-width: 220px;
}

.graph-tab:hover {
	background: var(--bg-hover);
}

/* The active tab reads as continuous with the canvas below it — the accent
   sits on top rather than filling the tab, so a row of tabs does not compete
   with the graph for attention. */
.graph-tab.on {
	background: var(--bg-canvas);
	color: var(--fg);
	box-shadow: inset 0 2px 0 var(--accent);
}

.graph-tab .name {
	overflow: hidden;
	text-overflow: ellipsis;
}

.graph-tab.dirty .name::after {
	content: "•";
	color: var(--warning);
	margin-left: 5px;
}

.graph-tab .tab-fn {
	flex: none;
	color: var(--accent);
}

.graph-tab .close {
	background: none;
	border: 0;
	color: var(--fg-faint);
	cursor: pointer;
	font-size: 13px;
	line-height: 1;
	padding: 0 2px;
	border-radius: 3px;
	visibility: hidden;
}

/* Shown on the tab you are pointing at, and always on the active one. A row of
   close buttons is a row of things to hit by accident. */
.graph-tab:hover .close,
.graph-tab.on .close {
	visibility: visible;
}

.graph-tab .close:hover {
	background: var(--bg-active);
	color: var(--danger);
}

/* A tab being dragged fades rather than moving: the row is short and the tabs
   are narrow, so animating positions would be motion nobody can follow. The
   line marking where it lands is what says what will happen. */
.graph-tabs.reordering .graph-tab {
	cursor: grabbing;
}

.graph-tab.dragging {
	opacity: 0.45;
}

/* Drawn inside the leading edge rather than as a margin, so inserting the
   marker never widens the row and shifts every tab under the pointer. */
.graph-tab.drop-before,
.tab-rest.drop-before {
	box-shadow: inset 2px 0 0 var(--accent);
}

/* The end of the row: a drop target, and the home of the list button. It grows
   to fill what the tabs leave, which is also what makes "drop at the end"
   something you can hit. */
.tab-rest {
	display: flex;
	align-items: center;
	flex: 1 0 44px;
	padding-left: 2px;
	background: var(--bg-app);
}

.tab-list {
	position: relative;
	display: flex;
	align-items: center;
}

.tab-list > button {
	padding: 2px 5px;
	line-height: 1;
}

/* Fixed to the viewport rather than placed inside the row, because the row
   scrolls sideways and therefore clips its children -- an absolutely-placed
   menu is cut off at the row's bottom edge. `left` and `top` come from the
   button's measured position, as the node palette's do. */
.tab-list-menu {
	position: fixed;
	min-width: 220px;
	max-height: 60vh;
	overflow-y: auto;
	padding: 4px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 6px;
	box-shadow: var(--shadow-menu, 0 8px 24px rgba(0, 0, 0, 0.35));
	display: flex;
	flex-direction: column;
	gap: 1px;
}

.tab-list-item {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 4px 8px;
	background: none;
	border: 0;
	border-radius: 4px;
	color: var(--fg-muted);
	cursor: pointer;
	text-align: left;
	white-space: nowrap;
}

.tab-list-item:hover {
	background: var(--bg-hover);
	color: var(--fg);
}

.tab-list-item.on {
	color: var(--fg);
	background: var(--bg-active);
}

.tab-list-item .name {
	overflow: hidden;
	text-overflow: ellipsis;
	flex: 1;
}

.tab-list-item .dot {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--warning);
	flex: none;
}

/* ----------------------------------------------------------- value picker */

/* Search at the top, everything below it in columns. Sized to the window
   rather than to its contents: the list is six hundred long and the useful
   thing is how much of it you can see at once. */
.value-picker {
	/* The shared backdrop is a grid that centres on one axis only, because the
	   docs overlay fills the other. This one is a fixed width and has to place
	   itself across. */
	justify-self: center;
	display: flex;
	flex-direction: column;
	width: min(1040px, 92vw);
	height: min(660px, 84vh);
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 8px;
	box-shadow: var(--shadow-menu, 0 12px 40px rgba(0, 0, 0, 0.45));
	overflow: hidden;
}

.value-search {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 10px;
	border-bottom: 1px solid var(--border);
	color: var(--fg-faint);
	flex: none;
}

.value-search input {
	flex: 1;
	min-width: 0;
}

.value-list {
	flex: 1;
	overflow-y: auto;
	padding: 10px 12px;
}

.value-group {
	break-inside: avoid;
	margin-bottom: 10px;
}

.value-group h3 {
	margin: 0 0 4px;
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
}

/* Columns rather than one tall list, which is what makes six hundred names
   scannable — and what Studio's Insert Object does for the same reason. They
   are sized rather than counted, so a narrow window gets fewer of them. */
.value-options {
	column-width: 210px;
	column-gap: 14px;
}

.value-option {
	display: block;
	width: 100%;
	text-align: left;
	padding: 2px 6px;
	border: 0;
	border-radius: 4px;
	background: none;
	color: var(--fg-muted);
	cursor: pointer;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.value-option.on {
	background: var(--bg-active);
	color: var(--fg);
}

/* The value the pin already holds, marked even when the highlight is elsewhere
   -- otherwise a picker opened to browse loses track of where it started. */
.value-option.current {
	color: var(--accent);
}

.value-empty {
	margin: 12px 4px;
	color: var(--fg-muted);
}

/* The inheritance chain, or the count. A fixed row so the list above it does
   not jump by a line every time the highlight moves. */
.value-detail {
	flex: none;
	padding: 7px 12px;
	border-top: 1px solid var(--border);
	color: var(--fg-faint);
	font-size: 12px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The pin's own control: what it holds, and a chevron saying there is more. */
.literal.picker {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 4px;
	cursor: pointer;
	text-align: left;
}

.literal.picker .preview {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* --------------------------------------------------------- floating panels */

/* A panel out of its dock, over the graph. Positioned inside `.centre`, so its
   coordinates are the graph's own and opening a dock does not drag it sideways. */
.float-panel {
	position: absolute;
	display: flex;
	flex-direction: column;
	min-width: 200px;
	min-height: 140px;
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
	overflow: hidden;
	/* Over the canvas and its wires, under the menus and dialogs. */
	z-index: 60;
}

/* The panel's own heading is the window's title bar, so it says what a title
   bar says and carries what the panel already put there. The room for the
   button over its right end is in `.dock h2`, which both shapes share. */
.float-panel .float-body h2 {
	user-select: none;
}



/* The two buttons that move a panel between a dock and a window: ⇥ out of the
   dock, ⇤ back into it.
   
   Sized to the Add button beside them rather than to the toolbar's icon
   buttons, which are 28px and tower over a heading. They sit on the heading's
   own row, so they have to be that row's height. */
button.tb.icon-only.float-dock,
button.tb.icon-only.panel-float {
	width: 20px;
	height: 20px;
	min-width: 0;
	padding: 0;
	font-size: 12px;
	line-height: 1;
	z-index: 2;
}

/* How far down the button sits, so it is centred on the row it shares.

   One value per shape of header, because there are two. Every panel heads
   itself with an `h2`, 32px tall; the diagnostics panel heads itself with the
   errors-and-warnings bar, which is 25px with a rule above it. This was a flat
   10px, measured off the `h2` and applied to both -- so on the bar the button
   sat 6px low and hung four pixels out of the bottom of the row it belongs to.

   Written as the offset rather than derived from a row height, so each number
   says what it is for instead of being the output of arithmetic that would
   also need explaining. */
.panel,
.float-panel {
	--dock-button-top: 6px;
}
.panel-analysis,
.float-analysis {
	--dock-button-top: 3px;
}

/* Over the heading, level with the button already in it. */
.float-panel .float-dock {
	position: absolute;
	top: var(--dock-button-top);
	right: 8px;
}

/* The same button in a dock, pointing the other way.

   Sticky rather than absolute, and above the heading in the flow with a
   negative margin pulling the heading back up under it: the dock scrolls, the
   heading is sticky, and a button pinned to the panel would scroll away from
   the heading it belongs to. The bottom margin is the button's whole block
   height -- its own 20px and the offset above it -- so the heading lands where
   it would have without it, whatever that offset is. */
.panel .panel-float {
	position: sticky;
	top: var(--dock-button-top);
	align-self: flex-end;
	margin: var(--dock-button-top) 8px calc(-20px - var(--dock-button-top)) 0;
}


/* The window's scrolling half, which is the job `.dock.left` does for a dock:
   one scroll container round the panel rather than one per panel. */
.float-body {
	flex: 1;
	min-height: 0;
	display: flex;
	flex-direction: column;
	overflow: auto;
}

/* The panel fills the window, the way it fills a dock — and *only* the panel.
   This was `> *`, which reached every element a panel renders: the project
   tree draws its outline and its tree as two siblings, and both grew to half
   the window, leaving a screen of nothing above the tree. */
.float-body > .panel {
	flex: 1;
	min-height: 0;
}

.float-size {
	position: absolute;
	width: 16px;
	height: 16px;
	cursor: nwse-resize;
	/* Over the panel's own content, or a list that reaches the corner takes the
	   drag and the window cannot be resized from that end. */
	z-index: 3;
}

.float-size::after {
	content: "";
	position: absolute;
	width: 8px;
	height: 8px;
}

.float-size.se { right: 0; bottom: 0; }

.float-size.se::after {
	right: 3px;
	bottom: 3px;
	border-right: 2px solid var(--border-strong);
	border-bottom: 2px solid var(--border-strong);
}

.float-size.nw { left: 0; top: 0; }

.float-size.nw::after {
	left: 3px;
	top: 3px;
	border-left: 2px solid var(--border-strong);
	border-top: 2px solid var(--border-strong);
}

/* The type control: what it holds, and a chevron saying the list is there. The
   same shape as a pin's class picker, because it opens the same window. */
.tb.type-picker {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 4px;
	width: 100%;
	min-width: 0;
	text-align: left;
}

.tb.type-picker .preview {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* A constant, marked in the Variables list. Named `const` on the badge because
   that is the word it writes into the file — a mark that reads as the keyword
   is one fewer thing to learn. */
.variable-head .badge.const {
	font-size: 9px;
	font-family: var(--mono, ui-monospace, monospace);
	color: var(--warning);
	border: 1px solid color-mix(in srgb, var(--warning) 55%, transparent);
	background: color-mix(in srgb, var(--warning) 12%, transparent);
	border-radius: 3px;
	padding: 0 4px;
	flex: none;
}

/* ------------------------------------------------------------- node picker */

/* Two columns: the list you walk and the node you are walking towards. The
   picture is the whole reason this exists beside the palette. */
.node-picker {
	width: min(880px, 100%);
	max-height: 72vh;
	display: flex;
	flex-direction: column;
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 10px;
	box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
	overflow: hidden;
}

.node-picker-field {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 12px;
	border-bottom: 1px solid var(--border);
}

.node-picker-field input {
	flex: 1;
	min-width: 0;
	background: none;
	border: 0;
	color: var(--fg);
	font-size: 15px;
	outline: none;
}

.node-picker-field .count {
	font-size: 11px;
	color: var(--fg-faint);
}

.node-picker-body {
	flex: 1;
	min-height: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
}

.node-picker-list {
	overflow: auto;
	padding: 6px;
	border-right: 1px solid var(--border);
	display: flex;
	flex-direction: column;
	gap: 1px;
}

.node-picker-group .head {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	padding: 8px 8px 4px;
}

.node-picker-list .empty {
	padding: 12px 8px;
	color: var(--fg-faint);
	font-size: 12px;
}

.node-picker-hit {
	display: flex;
	align-items: baseline;
	gap: 8px;
	width: 100%;
	background: none;
	border: 0;
	border-radius: 5px;
	padding: 5px 8px;
	text-align: left;
	cursor: pointer;
	color: var(--fg-muted);
	font-size: 12px;
}

.node-picker-hit.on {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	color: var(--fg);
}

.node-picker-hit .hint {
	margin-left: auto;
	font-size: 9px;
	color: var(--fg-faint);
	/* Same as the menu's: this row is baseline-aligned too. */
	align-self: center;
}

/* The canvas's own background under the picture, so a node is read against
   what it will sit on rather than against a panel. */
.node-picker-preview {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 16px;
	overflow: auto;
	background: var(--bg-canvas, var(--bg-input));
}

.node-picker-preview .shot {
	display: flex;
	justify-content: center;
	align-items: center;
	min-height: 120px;
}

.node-picker-preview .name { font-size: 14px; color: var(--fg); }
.node-picker-preview .where {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
}

.node-picker-preview .summary {
	margin: 6px 0 0;
	font-size: 12px;
	color: var(--fg-muted);
	line-height: 1.5;
}

.node-picker-foot {
	display: flex;
	gap: 14px;
	padding: 8px 12px;
	border-top: 1px solid var(--border);
	font-size: 10px;
	color: var(--fg-faint);
}

.node-picker-foot kbd {
	font-family: inherit;
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
	margin-right: 3px;
}

.node-picker-foot {
	flex-wrap: wrap;
}

.node-picker-foot-touch {
	display: none;
}

@media (hover: none) and (pointer: coarse) {
	.node-picker-foot-keys {
		display: none;
	}

	.node-picker-foot-touch {
		display: flex;
	}
}

.node-picker-spawn {
	align-self: flex-start;
}

/* A row being dragged out: the picker steps aside so the graph can be seen
   and dropped on, and lets the pointer through to it. */
.picker-backdrop.picker-backdrop-dragging {
	pointer-events: none;
	opacity: 0.15;
	background: transparent;
}

/* ---------------------------------------------------------------- toolbar */

.toolbar {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
	flex-wrap: wrap;
}

/* The mark, and whatever sits with it. Centred rather than baseline-aligned:
   an SVG has no baseline, so `baseline` drops it to the bottom of the line box
   and it rides low against the text next to it. */
.toolbar .logo {
	font-weight: 600;
	letter-spacing: 0.04em;
	margin-right: 4px;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	/* A <button> since 0.18.2, so the browser's own button paint has to be
	   taken off before it looks like the mark it replaced. */
	background: none;
	border: 1px solid transparent;
	border-radius: 6px;
	padding: 3px 6px;
	color: inherit;
	font-family: inherit;
	cursor: pointer;
}

/* It does something now, so it has to look like it can. Left invisible until
   the pointer is on it: a bordered button sitting where the mark used to be
   reads as the toolbar having grown a control, rather than as the mark having
   gained a menu. */
.toolbar .logo:hover,
.toolbar .logo:focus-visible {
	background: var(--bg-hover);
	border-color: var(--border);
}

/* The version, deliberately quiet. It only matters when something is wrong —
   and then it is the first thing anyone asks for. */
.toolbar .logo .version {
	font-weight: 400;
	font-size: 10px;
	letter-spacing: 0;
	color: var(--fg-faint);
	font-variant-numeric: tabular-nums;
}

.toolbar .spacer {
	flex: 1;
}

/* Names what a segmented control sets, where the buttons alone cannot. Quiet,
   because it is a caption rather than a control: it should be readable when
   you are looking for it and invisible when you are not. */
.toolbar .group-label {
	font-size: 11px;
	color: var(--fg-muted);
	margin-right: -3px;
	white-space: nowrap;
}

/* ------------------------------------------------------- the document bar */

/* The second row: the open document, its own settings, and the tools that only
   mean anything while it is open.
 *
 * Quieter than the bar above it and separated by a rule, because it is a
 * different kind of thing rather than more of the same — the top row acts on
 * the project and is always there, this one acts on what you are looking at and
 * disappears when you are not looking at anything. */
.docbar {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 5px 10px;
	background: var(--bg-app);
	border-bottom: 1px solid var(--border);
	flex-wrap: wrap;
}

.docbar .spacer {
	flex: 1;
}

/* Separates the document's own settings from the tools that act on it. */
.docbar .divider {
	width: 1px;
	align-self: stretch;
	margin: 3px 3px;
	/* The strong border, not the ordinary one: this row sits on the app
	   background rather than the panel, and `--border` against it is a line
	   nobody can see. */
	background: var(--border-strong);
}

.toolbar .doc-name,
.docbar .doc-name {
	font-weight: 600;
	margin-right: 2px;
}

/* What kind of document this is, where it is not already obvious from the
   controls beside it — a graph says so with its script class, a node map has
   no settings of its own and would otherwise be a bare name. */
.docbar .doc-kind {
	font-size: 11px;
	color: var(--fg-faint);
}

.toolbar .dirty::after,
.docbar .dirty::after {
	content: "•";
	color: var(--warning);
	margin-left: 5px;
}

button.tb,
select.tb,
input.tb {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 4px 9px;
	cursor: pointer;
}

button.tb:hover:not(:disabled),
select.tb:hover {
	background: var(--bg-hover);
	border-color: var(--border-strong);
}

button.tb:disabled {
	opacity: 0.45;
	cursor: default;
}

/* A toolbar button that is a mode rather than an action. Held-down rather than
   filled, because it sits beside real buttons and should not read as the one
   thing on the bar you are meant to press. */
button.tb.on {
	background: color-mix(in srgb, var(--accent) 18%, var(--bg-input));
	border-color: var(--accent);
	color: var(--accent);
}

button.tb.on:hover:not(:disabled) {
	background: color-mix(in srgb, var(--accent) 26%, var(--bg-input));
	border-color: var(--accent);
}

button.tb.primary {
	background: var(--accent);
	border-color: transparent;
	color: #fff;
}

button.tb.primary:hover:not(:disabled) {
	filter: brightness(1.1);
	background: var(--accent);
}

.segmented {
	display: inline-flex;
	border: 1px solid var(--border);
	border-radius: 5px;
	overflow: hidden;
}

.segmented button {
	background: var(--bg-input);
	border: 0;
	padding: 4px 10px;
	cursor: pointer;
}

.segmented button.on {
	background: var(--accent);
	color: #fff;
}

/* ------------------------------------------------------------ panel headings */

/* Scoped to any dock rather than to one of them, because a panel keeps its
   look wherever it is docked -- which is the whole point of being able to move
   it. This was `.sidebar h2` until the sidebar became `.dock.left`, and the
   rename orphaned it: the project name and the Variables heading both fell
   back to the browser's default 2em bold, because `.variables h2` only sets
   its own flex properties and inherited everything else from here.
   
   And a second time in 0.46.0, when a panel could be in a window instead of a
   dock: the same heading, the same 2em bold. "Wherever it is docked" now has to
   include "and where it is not", so the window is named here too rather than
   restating the rule beside itself. */
.dock h2,
.float-panel h2 {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin: 0;
	/* Wide on the right: both shapes hang a button over that end — the window's
	   dock button, the dock's undock one — and a heading with an Add button in
	   it has no room to spare otherwise. */
	padding: 10px 34px 6px 12px;
	position: sticky;
	top: 0;
	background: var(--bg-panel);
	z-index: 1;
}

.tree {
	padding: 0 6px 12px;
	user-select: none;
	flex: 1;
}

.tree-row {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 3px 6px;
	border-radius: 4px;
	cursor: default;
	white-space: nowrap;
}

.tree-row:hover {
	background: var(--bg-hover);
}

.tree-row.selected {
	background: var(--bg-active);
}

.tree-row.open-doc {
	box-shadow: inset 2px 0 0 var(--accent);
}

.tree-row.drop-target {
	outline: 1px dashed var(--accent);
	outline-offset: -1px;
}

.tree-row .glyph {
	width: 14px;
	text-align: center;
	color: var(--fg-faint);
	flex: none;
}

.tree-row .label {
	overflow: hidden;
	text-overflow: ellipsis;
}

.tree-row.readonly .label {
	color: var(--fg-muted);
	font-style: italic;
}

.tree-row .badge {
	margin-left: auto;
	font-size: 10px;
	color: var(--fg-faint);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
}

/* ----------------------------------------------------------------- canvas */

.canvas {
	position: relative;
	overflow: hidden;
	background: var(--bg-canvas);
	touch-action: none;
	min-height: 0;
	contain: strict;
}

.canvas:focus {
	outline: none;
}

.canvas .grid {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.canvas .world {
	position: absolute;
	top: 0;
	left: 0;
	transform-origin: 0 0;
	width: 0;
	height: 0;
}

.canvas .watermark {
	position: absolute;
	right: 18px;
	bottom: 14px;
	font-size: 30px;
	font-weight: 700;
	letter-spacing: 0.02em;
	color: var(--watermark);
	pointer-events: none;
	user-select: none;
	text-align: right;
	line-height: 1.1;
}

.canvas .watermark small {
	display: block;
	font-size: 12px;
	font-weight: 500;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

.canvas .wires {
	position: absolute;
	left: 0;
	top: 0;
	width: 1px;
	height: 1px;
	overflow: visible;
	pointer-events: none;
}

.canvas .wires path {
	fill: none;
	stroke-linecap: round;
}

.canvas .wires path.hit {
	stroke: transparent;
	stroke-width: 12;
	pointer-events: stroke;
	cursor: pointer;
}

.canvas .marquee {
	position: absolute;
	border: 1px solid var(--select);
	background: color-mix(in srgb, var(--select) 14%, transparent);
	pointer-events: none;
}

/* ------------------------------------------------------------------- node */

.node {
	position: absolute;
	border-radius: var(--node-radius);
	background: var(--node-body);
	border: var(--node-stroke) solid var(--node-border);
	box-shadow: var(--node-shadow);
	user-select: none;
}

.node.selected {
	border-color: var(--select);
	box-shadow: 0 0 0 2px color-mix(in srgb, var(--select) 55%, transparent), var(--node-shadow);
}

/* The node a selection is anchored on -- the one Align lines the rest up
   with. A heavier ring rather than a second colour: it is a stronger case of
   being selected, not a different state, and a new hue on the canvas is one
   more thing to learn the meaning of. Restated for the capsule and the knot
   further down, where their own rules would otherwise win. */
.node.selected.anchor {
	box-shadow: 0 0 0 4px var(--select), var(--node-shadow);
}

.node.has-error {
	border-color: var(--danger);
}

.node .head {
	height: 30px;
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 0 9px;
	/* One pixel inside the body's own corner, so the header's arc sits within
	   the border rather than crossing it. `max()` keeps it at zero when corners
	   are square, instead of going negative and being ignored. */
	border-radius: max(0px, calc(var(--node-radius) - 1px)) max(0px, calc(var(--node-radius) - 1px)) 0 0;
	color: #fff;
	font-weight: 600;
	font-size: 12px;
	cursor: grab;
	overflow: hidden;
	text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
}

.node .head .title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.node .head .marker {
	font-size: 10px;
	opacity: 0.85;
}

.node .rows {
	position: relative;
}

.node .row {
	height: 24px;
	display: flex;
	align-items: center;
	position: relative;
}

.node .row .side {
	display: flex;
	align-items: center;
	gap: 5px;
	min-width: 0;
}

.node .row .side.left {
	flex: 1;
	padding-left: var(--pin-pad);
}

.node .row .side.right {
	margin-left: auto;
	padding-right: var(--pin-pad);
	justify-content: flex-end;
	/* One pin's worth, reserved whether or not this row has an output.
	 *
	 * Without it every row is measured independently, so the one row that
	 * happens to carry the node's output gets a narrower left side and its value
	 * sits further left than the rows below it. Reserving the column makes the
	 * input values line up down the node instead of stepping around the output
	 * pin. The lane is the whole footprint, padding included. */
	min-width: var(--pin-lane);
}

.node .pin {
	width: var(--pin-slot);
	height: var(--pin-slot);
	flex: none;
	position: relative;
	cursor: crosshair;
}

/* A pin sits *on* the node's edge rather than inside it.
 *
 * `left` on a relatively positioned element moves what is drawn and leaves the
 * layout alone, which is the whole trick: the row still reserves the slot, so
 * the labels and the value column do not move, and only the pin travels.
 *
 * A data pin is balanced halfway off the edge — its centre is exactly where a
 * wire attaches, which it never was before: the wire used to stop at the border
 * with the dot 14px inside it. An execution pin clears the edge altogether and
 * hangs beside the node, point first. */
.node .row .side .pin {
	/* Up by the border, for the same reason the sideways offsets carry it: a
	   row sits inside the border and a wire attaches outside it. */
	top: calc(-1 * var(--node-stroke));
	/* And the row gives back the space the pin no longer occupies, so the label
	   beside it starts at `--pin-lane` rather than a slot and a half in. */
	margin: 0 calc(var(--pin-lane) - var(--pin-pad) - var(--pin-slot));
}

.node .row .side.left .pin {
	left: calc(-1 * (var(--pin-pad) + var(--pin-slot) / 2 + var(--node-stroke)));
	margin-left: 0;
}

.node .row .side.right .pin {
	left: calc(var(--pin-pad) + var(--pin-slot) / 2 + var(--node-stroke));
	margin-right: 0;
}

.node .row .side.left .pin.exec {
	left: calc(-1 * (var(--pin-pad) + var(--exec-width) + var(--exec-gap) + var(--node-stroke)));
}

.node .row .side.right .pin.exec {
	left: calc(var(--pin-pad) + var(--pin-slot) + var(--exec-gap) + var(--node-stroke));
}

/* Asymmetric hit areas, on purpose.
 *
 * Starting a wire asks for aim: the grab area is the dot and a hair around it,
 * so the rest of the row belongs to the node and can be dragged. Landing a wire
 * asks for none: while one is in flight every pin widens, because by then you
 * know which pin you want and fighting for the last three pixels is nobody's
 * idea of a good time.
 *
 * The resting inset stops one pixel short of the next row: a 16px slot plus 3px
 * either side is 22px inside a 24px row, so no click is ever answered by two
 * pins at once. Widen the slot and this number has to come down with it. */
.node .pin::after {
	content: "";
	position: absolute;
	inset: -3px;
}

.canvas.wiring .node .pin::after {
	inset: -9px;
}

/* A data pin is a 10px circle centred in the slot — the size it has always
   been. The slot grew around it, not it.
 *
 * Filled with the surface behind it rather than left transparent, because the
 * circle now straddles the node's border: half of it is over the body and half
 * over the canvas, and a see-through middle would show the seam running through
 * the pin. One fill makes it read as a ring sitting on the edge. */
.node .pin .dot {
	position: absolute;
	inset: 3px;
	border-radius: 50%;
	border: 2px solid currentColor;
	background: var(--pin-well);
	box-shadow: 0 0 0 1px var(--pin-well);
}

.node .pin.connected .dot {
	background: currentColor;
}

/* An execution pin is an equilateral triangle pointing right, as tall as the
   slot, hung outside the node's border.
 *
 * It used to be a house-shaped arrow drawn inside the node, the shape Blueprints
 * uses. The triangle is the shape Bolt and Blender give a flow socket, and
 * putting it outside the border is what makes a run of steps read as a chain:
 * the pin belongs to the wire rather than to the box, and the eye follows it out
 * of one node and into the next instead of stopping at two walls.
 *
 * Empty versus wired is drawn the way node editors usually draw it — outline,
 * then solid.
 * That used to be a half-opacity fill, which reads as "disabled" rather than
 * "nothing here yet"; a hollow arrow says the same thing at full colour, so an
 * unwired exec pin stays as easy to find as a wired one.
 *
 * The hollow is a second triangle in the surface colour rather than a border,
 * because clip-path cuts straight through a border and leaves the point blunt
 * on the diagonal. */
.node .pin.exec .dot {
	position: absolute;
	inset: 0 auto 0 0;
	width: var(--exec-width);
	border: 0;
	border-radius: 0;
	background: currentColor;
	clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.node .pin.exec .dot::after {
	content: "";
	position: absolute;
	/* The same triangle shrunk towards its centre — see `--exec-shrink`. The
	   right inset is twice the left because the centre is a third of the way in
	   from the base, which is what keeps the wall even all round. */
	inset:
		calc(var(--pin-slot) / 2 * var(--exec-shrink))
		calc(var(--exec-width) * 2 / 3 * var(--exec-shrink))
		calc(var(--pin-slot) / 2 * var(--exec-shrink))
		calc(var(--exec-width) / 3 * var(--exec-shrink));
	background: var(--pin-hollow);
	clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.node .pin.exec.connected .dot::after {
	display: none;
}

/* The well's ring stays under it, or a legal target loses the outline every
   other pin has while a wire is in flight. */
.node .pin.compatible .dot {
	box-shadow: 0 0 0 1px var(--pin-well), 0 0 0 4px color-mix(in srgb, var(--ok) 45%, transparent);
}

.node .pin.incompatible {
	opacity: 0.3;
}

.node .pin-label {
	font-size: 11px;
	color: var(--fg-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* A component of a split struct pin. The rule is the tell: without something
   joining them, X, Y and Z read as three unrelated inputs rather than one
   Vector taken apart. */
.node .pin-label.part {
	position: relative;
	padding-left: 9px;
	color: var(--fg-faint);
}

.node .pin-label.part::before {
	content: "";
	position: absolute;
	left: 2px;
	top: 50%;
	width: 4px;
	height: 1px;
	background: var(--border-strong);
}

.node .side.right .pin-label.part {
	padding-left: 0;
	padding-right: 9px;
}

.node .side.right .pin-label.part::before {
	left: auto;
	right: 2px;
}

/* A constant the node declared, or one Roswaal folded back when a split pin was
   recombined. Shown flat and unboxed, because it is not a field: nothing here
   takes typing, and a box that cannot be typed in is a lie. */
.node .literal.constant {
	background: none;
	border: 1px solid transparent;
	color: var(--fg-faint);
	font-family: var(--mono, ui-monospace, monospace);
	font-size: 10px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	/* Pushed to the right edge and given its natural width. The label beside it
	   is short and known ("Up"); the value is the part worth reading, and
	   "Vector3.z…" does not distinguish `zero` from anything else. No padding,
	   because this is text rather than a field. */
	flex: none;
	margin-left: auto;
	padding: 0;
	max-width: 140px;
	text-align: right;
	cursor: default;
}

/* Every inline value sits in one column at the right of the input side, whether
   it is a field, a dropdown, a code button or a constant. Mixing left-aligned
   fields with right-aligned constants read as two unrelated things on the same
   node; one column reads as a value column. */
.node .row .side.left > .literal,
.node .row .side.left > input[type="checkbox"] {
	margin-left: auto;
}

/* Not on a pill, where the row is the whole node and the right of it is the
   symbol and the result pin. Pushed there, a field sat on top of both — and the
   pill's width already reserves a column for it beside the pins, which is where
   `operatorLayout` has always put it. */
.node.operator .row .side.left > .literal,
.node.operator .row .side.left > input[type="checkbox"] {
	margin-left: 0;
}

/* An optional pin nobody has set. Reads as the word rather than as a value,
   because the argument is genuinely not passed and showing a number here would
   claim otherwise. Dashed, so it looks like a placeholder rather than a field
   somebody already filled in. */
.node .literal.unset {
	background: none;
	border: 1px dashed var(--border-strong);
	color: var(--fg-faint);
	font-size: 10px;
	font-style: italic;
	padding: 0 5px;
	cursor: pointer;
}

.node .literal.unset:hover {
	border-color: var(--accent);
	color: var(--fg-muted);
}

/* A set optional pin: its editor, and the way back to unset. */
.node .optional-value {
	display: flex;
	align-items: center;
	gap: 2px;
	min-width: 0;
}

.node .optional-value .clear {
	background: none;
	border: 0;
	color: var(--fg-faint);
	cursor: pointer;
	font-size: 12px;
	line-height: 1;
	padding: 0 1px;
}

.node .optional-value .clear:hover {
	color: var(--danger);
}

.node .literal {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 1px 4px;
	font-size: 11px;
	width: 70px;
	min-width: 0;
}

.node .literal.wide {
	width: 92px;
}

.node .literal:focus {
	outline: 1px solid var(--accent);
}

/* A pill's corner is rounded away from where a square node's corner is, so a
   badge hung at -7px sits off the shape entirely rather than on its edge. */
.node.operator .badge-count,
.node.capsule .badge-count {
	top: -6px;
	right: -1px;
}

.node .badge-count {
	position: absolute;
	top: -7px;
	right: -7px;
	background: var(--danger);
	color: #fff;
	border-radius: 8px;
	font-size: 10px;
	padding: 0 5px;
	line-height: 15px;
}

/* Something to look at rather than something that is wrong: a Lune call whose
   module nothing requires will not compile, but the node itself is fine and the
   fix is one button away in the Inspector. Yellow and a single mark, in the
   corner the error count uses -- one place to glance at, and never both at
   once, because an error is the more urgent of the two and says how many. */
.node .badge-count.warn {
	background: var(--warning);
	color: #1c1f24;
	font-weight: 700;
	/* A round pip rather than the error's oval: one character wants a circle,
	   and the two should not be mistaken for each other at a glance. */
	min-width: 15px;
	padding: 0;
	text-align: center;
}

/* ---------------------------------------------------------------- comment */

.comment {
	position: absolute;
	border-radius: 6px;
	border: 1px solid color-mix(in srgb, var(--comment-color) 65%, transparent);
	background: color-mix(in srgb, var(--comment-color) calc(var(--comment-fill) * 100%), transparent);
	user-select: none;
}

.comment.selected {
	border-color: var(--select);
	box-shadow: 0 0 0 2px color-mix(in srgb, var(--select) 45%, transparent);
}

/* The header holds as much as it is given: a long line wraps and several lines
   each take one, so a comment can say what it is for rather than name it. */
.comment .bar {
	min-height: 26px;
	display: flex;
	align-items: flex-start;
	padding: 5px 8px;
	font-weight: 600;
	font-size: 12px;
	line-height: 16px;
	color: #fff;
	background: color-mix(in srgb, var(--comment-color) 82%, transparent);
	border-radius: 5px 5px 0 0;
	cursor: grab;
	text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

.comment .bar textarea {
	background: rgba(0, 0, 0, 0.28);
	border: 0;
	border-radius: 3px;
	color: #fff;
	font: inherit;
	line-height: inherit;
	width: 100%;
	padding: 1px 4px;
	/* Sized by its content from the keystroke, so the header grows as you type
	   rather than scrolling inside a box the size of one line. */
	resize: none;
	overflow: hidden;
}

.comment .bar textarea:focus {
	outline: 1px solid #fff;
}

.comment .resize {
	position: absolute;
	width: 16px;
	height: 16px;
	cursor: nwse-resize;
	/* Above the header bar, which the top-left corner sits on top of and which
	   would otherwise take the drag as a move. */
	z-index: 36;
}

.comment .resize::after {
	content: "";
	position: absolute;
	width: 8px;
	height: 8px;
}

.comment .resize.se {
	right: 0;
	bottom: 0;
}

.comment .resize.se::after {
	right: 3px;
	bottom: 3px;
	border-right: 2px solid color-mix(in srgb, var(--comment-color) 90%, transparent);
	border-bottom: 2px solid color-mix(in srgb, var(--comment-color) 90%, transparent);
}

.comment .resize.nw {
	left: 0;
	top: 0;
}

.comment .resize.nw::after {
	left: 3px;
	top: 3px;
	border-left: 2px solid color-mix(in srgb, var(--comment-color) 90%, transparent);
	border-top: 2px solid color-mix(in srgb, var(--comment-color) 90%, transparent);
}

/* ------------------------------------------------------------------- menu */

.menu {
	position: fixed;
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 7px;
	box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
	overflow: hidden;
	display: flex;
	flex-direction: column;
	max-height: 60vh;
	width: 280px;
}

/* Shown only when the palette was opened by dragging a wire. Above the search
   box, because it is the reason the list is short. */
.menu-from {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 7px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-hover);
	font-size: 11px;
	color: var(--fg-muted);
}

.menu-from .dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
	flex: none;
}

.menu input.search {
	border: 0;
	border-bottom: 1px solid var(--border);
	background: var(--bg-input);
	padding: 8px 10px;
	outline: none;
}

.menu .items {
	overflow: auto;
	padding: 4px;
}

.menu .group {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	padding: 6px 8px 3px;
}

/* A datatype inside Engine Types. Indented and not shouted, so the eye reads
   the category first and the type second — the other way round would make nine
   datatype headings compete with every category heading in the list. */
.menu .subgroup {
	font-size: 11px;
	font-weight: 600;
	color: var(--fg-muted);
	padding: 5px 8px 2px 16px;
}

.menu .item {
	display: flex;
	align-items: baseline;
	gap: 7px;
	padding: 4px 8px;
	border-radius: 4px;
	cursor: pointer;
}

.menu .item:hover,
.menu .item.active {
	background: var(--bg-active);
}

/* An item describing something this copy of Roswaal cannot do -- showing a file
   in a file manager, from a browser tab. Shown rather than removed, because it
   names a real thing the tool does and somebody trying the hosted editor is
   exactly who should learn that; its title says where it does work. Compound
   with `.item` so the rule cannot reach anything else calling itself disabled. */
.menu .item.item-unavailable {
	cursor: default;
	opacity: 0.45;
}

.menu .item.item-unavailable:hover {
	background: none;
}

.menu .item .swatch {
	width: 8px;
	height: 8px;
	border-radius: 2px;
	flex: none;
}

.menu .item .hint {
	margin-left: auto;
	font-size: 10px;
	color: var(--fg-faint);
	/* Centred, like everything else that trails a row. The row aligns on the
	   baseline, which is right for the swatch and the title and wrong for a
	   trailing chip -- `pure` and a bordered badge beside it sat two pixels
	   apart because one was on the baseline and the other was not. */
	align-self: center;
}

/* Only the first hint pushes. `margin-left: auto` on every one of them splits
   the free space between them, so on a row carrying both `pure` and a runtime
   badge the `pure` floated to a different x on every row -- 843 here, 827 the
   next row down -- and the column stopped being a column. The rest follow at
   the row's own gap, so the group right-aligns as one thing. */
.menu .item .hint ~ .hint {
	margin-left: 0;
}

.node-picker-hit .hint ~ .hint {
	margin-left: 0;
}

.menu .empty {
	padding: 12px;
	color: var(--fg-faint);
	text-align: center;
}

/* A field whose control has something beside it -- the variable picker and its
   "New..." button. The select takes the room and the button takes what it
   needs, so a long variable name does not push the button off the panel. */
.inspector .field-row {
	display: flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
}

/* The way out of a shortened description. Underlined rather than only coloured,
   because "See docs page" sitting at the end of a sentence has to read as a
   link on the first glance and not on the second. */
.inspector .docs-link {
	color: var(--accent);
	text-decoration: underline;
	white-space: nowrap;
	cursor: pointer;
}

/* A checkbox and the words that explain it, on one line. They were two: the
   control sat under its own heading and its text under that again, because a
   <label> nested inside the field's own <label> is not a row. */
.inspector .check-row {
	display: flex;
	align-items: center;
	gap: 6px;
	cursor: pointer;
	/* Both needed. Without the first, a long explanation refuses to wrap and
	   widens the whole panel until it scrolls sideways; without the second, the
	   text has nowhere to go when the dock is dragged narrow. */
	min-width: 0;
	flex-wrap: wrap;
}

/* `.inspector .field input` sets width:100% for the text fields, and a checkbox
   inherited it: 265px wide, which renders the tick centred in a box the width of
   the panel and pushes its own label onto the next line. That was the whole of
   the misalignment -- the row was a row all along. */
.inspector .check-row input[type="checkbox"] {
	margin: 0;
	flex: none;
	width: auto;
	cursor: pointer;
}

.inspector .check-row span {
	min-width: 0;
	overflow-wrap: anywhere;
}

/* The list rows are three controls wide and the dock can be dragged to 140px.
   Left alone they push the panel wider than it is and it scrolls sideways. */
.inspector .list-row {
	min-width: 0;
}

.inspector .list-row input {
	min-width: 0;
}

/* A Luau type written out, shown as the code it is and opened in the code
   editor on a click. A button that looks like a field: it is where the text
   is, and pressing it is how you change it. */
.inspector button.type-definition {
	display: flex;
	flex-direction: column;
	width: 100%;
	min-height: 3.2em;
	max-height: 14em;
	overflow: auto;
	padding: 5px 8px;
	border: 1px solid var(--border);
	border-radius: 5px;
	background: var(--bg-input);
	color: var(--fg);
	font-family: var(--mono);
	font-size: 11px;
	line-height: 1.5;
	text-align: left;
	white-space: pre;
	cursor: text;
}

.inspector button.type-definition:hover { border-color: var(--border-strong); }
.inspector button.type-definition.bad { border-color: var(--danger); }
.inspector button.type-definition .placeholder { color: var(--fg-faint); }

.inspector .type-definition-problem {
	margin: 4px 0 0;
	font-size: 11px;
	color: var(--danger);
}

/* The two halves of a project: what you author, and what Roswaal writes. Styled
   as a heading rather than as a folder, because it is not one -- there is no
   such directory, and making it look like a row would invite you to right-click
   it and wonder why nothing is offered. */
.tree-section {
	display: flex;
	align-items: center;
	gap: 5px;
	padding: 5px 6px;
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-muted);
	cursor: pointer;
	user-select: none;
	/* A band rather than a line of small text. These are the only dividers in
	   the tree and they have to survive being read past -- a heading that looks
	   like a very quiet row is one the eye scrolls straight over. */
	background: var(--bg-hover);
	border-top: 1px solid var(--border);
	border-bottom: 1px solid var(--border);
}

/* The first sits against the panel heading, which is already a boundary. */
.tree-section:first-child {
	border-top: none;
}

.tree-section:hover {
	color: var(--fg);
	background: var(--bg-active);
}

.tree-section .twist {
	font-size: 9px;
}

.tree-section .empty {
	margin-left: auto;
	text-transform: none;
	letter-spacing: 0;
	opacity: 0.7;
}

/* The folder a new graph would go into. Quieter than a selection -- it is
   context, not a choice you made, and it has to sit under the selected and
   open-document styles without fighting them. */
.tree-row.target-dir {
	box-shadow: inset 2px 0 0 var(--accent);
}

/* What is left when a render throws. Deliberately plain: it has to work when
   whatever it is reporting has already gone wrong, so it uses nothing but the
   colour tokens. */
.crash {
	max-width: 780px;
	margin: 0 auto;
	padding: 48px 24px;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.crash h1 {
	font-size: 20px;
	margin: 0;
}

.crash p {
	margin: 0;
	color: var(--fg-muted);
}

.crash-detail {
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 7px;
	padding: 12px;
	max-height: 40vh;
	overflow: auto;
	font-size: 11px;
	line-height: 1.5;
	white-space: pre-wrap;
	word-break: break-word;
	margin: 0;
}

.crash-actions {
	display: flex;
	gap: 8px;
}

/* The project menu: what is open, what was open before, and the way out. Narrow
   like the pin menu, because every row is one path and a wide menu of short
   names reads as mostly empty. */
.menu.project-menu {
	width: 260px;
	max-height: none;
}

.menu.project-menu .current {
	padding: 8px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-hover);
	display: flex;
	flex-direction: column;
	gap: 1px;
}

.menu.project-menu .current .name {
	font-weight: 600;
	font-size: 12px;
}

/* The path is the disambiguator, not the label -- two projects called `src` are
   ordinary. Truncated from the left in the markup, so what survives is the end.
   `direction: rtl` would do it in CSS and puts the ellipsis on the wrong side of
   a Windows path. */
.menu.project-menu .current .path,
.menu.project-menu .item .path {
	font-size: 10px;
	color: var(--fg-faint);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.menu.project-menu .item {
	align-items: center;
}

.menu.project-menu .item .name {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Only on the row the pointer is on. A column of crosses beside a list of
   projects makes deleting look like the point of the menu. */
.menu.project-menu .item .forget {
	margin-left: auto;
	background: none;
	border: none;
	color: var(--fg-faint);
	cursor: pointer;
	padding: 0 2px;
	font-size: 13px;
	line-height: 1;
	visibility: hidden;
}

.menu.project-menu .item:hover .forget {
	visibility: visible;
}

.menu.project-menu .item .forget:hover {
	color: var(--danger);
}

/* The pin menu is the node palette's small sibling: no search, a handful of
   entries, and a header naming the pin so there is never a doubt about which
   one was hit -- pin hit areas overlap their labels deliberately. */
.menu.pin-menu {
	width: 220px;
	max-height: none;
}

.menu.pin-menu .pin-head {
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 7px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-input);
}

.menu.pin-menu .pin-head .swatch {
	width: 9px;
	height: 9px;
	border-radius: 50%;
	flex: none;
}

/* An execution pin is drawn as its triangle here too, so the header matches the
   thing that was clicked rather than describing it. */
.menu.pin-menu .pin-head .swatch.exec {
	border-radius: 0;
	width: 8px;
	clip-path: polygon(0 0, 100% 50%, 0 100%);
}

.menu.pin-menu .pin-head .name {
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.menu.pin-menu .pin-head .type {
	margin-left: auto;
	font-size: 10px;
	color: var(--fg-faint);
	white-space: nowrap;
}

.menu.pin-menu .empty {
	padding: 10px;
	font-size: 11px;
	text-align: left;
}

/* -------------------------------------------------------------- diagnostics */

.status {
	border-top: 1px solid var(--border);
	background: var(--bg-panel);
	max-height: 190px;
	display: flex;
	flex-direction: column;
}

.status .bar {
	display: flex;
	align-items: center;
	gap: 10px;
	/* Room on the right for the undock button, as the headings have. */
	padding: 4px 34px 4px 10px;
	font-size: 12px;
	cursor: pointer;
}

.status .bar .count {
	display: inline-flex;
	align-items: center;
	gap: 4px;
}

.status .list {
	overflow: auto;
	border-top: 1px solid var(--border);
	padding: 4px 0;
}

.status .entry {
	display: flex;
	gap: 8px;
	padding: 3px 12px;
	cursor: pointer;
}

.status .entry:hover {
	background: var(--bg-hover);
}

.status .entry .sev {
	flex: none;
	font-weight: 600;
	width: 62px;
}

.status .entry.error .sev {
	color: var(--danger);
}

.status .entry.warning .sev {
	color: var(--warning);
}


.status .entry .where {
	color: var(--fg-faint);
	margin-left: auto;
	white-space: nowrap;
}

/* ------------------------------------------------------------------ misc */

.placeholder {
	display: grid;
	place-content: center;
	gap: 10px;
	text-align: center;
	color: var(--fg-muted);
	padding: 40px;
}

.placeholder h1 {
	font-size: 22px;
	margin: 0;
	color: var(--fg);
}

/* The opening screen keeps the name in text beside the mark, so this is the
   one heading that has to lay two things out. The container is centred by the
   grid above, so the flex box centres its own contents rather than stretching. */
.placeholder h1.logo {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
}

.placeholder code {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 4px;
	padding: 1px 5px;
}

.placeholder .row {
	display: flex;
	gap: 8px;
	justify-content: center;
}

/* ------------------------------------------------------------------ shell */

.placeholder.shell {
	place-content: start center;
	padding-top: 12vh;
	max-width: 640px;
	margin: 0 auto;
}

.placeholder.shell .shell-note {
	margin: 0;
	font-size: 12px;
	color: var(--fg-faint);
	min-height: 18px;
}

/* The host never answered. This is the screen instead of the folder picker,
   because a picker is an answer to a question nobody asked when the thing that
   would open the folder is the thing that is down. */
.placeholder.shell .shell-broken {
	color: var(--danger);
	margin: 0 0 4px;
}

.placeholder.shell .shell-row {
	display: flex;
	gap: 8px;
	justify-content: center;
	margin-top: 14px;
}

.shell-recent {
	margin-top: 18px;
	text-align: left;
	border-top: 1px solid var(--border);
	padding-top: 14px;
}

.shell-recent-head {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin-bottom: 6px;
}

.shell-recent-row {
	display: flex;
	align-items: stretch;
	gap: 4px;
}

.shell-recent-open {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 1px;
	align-items: flex-start;
	background: none;
	border: 1px solid transparent;
	border-radius: 5px;
	padding: 6px 8px;
	cursor: pointer;
	text-align: left;
	color: inherit;
	font: inherit;
}

.shell-recent-open:hover {
	background: var(--bg-hover);
	border-color: var(--border);
}

.shell-recent-open .name {
	font-size: 13px;
	color: var(--fg);
}

/* The full path is the thing that disambiguates two folders with the same
   name, so it is shown rather than hidden behind a tooltip — and truncated at
   the left, because the end of a path is the part that identifies it. */
.shell-recent-open .path {
	font-size: 11px;
	color: var(--fg-faint);
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	direction: rtl;
	text-align: left;
}

.shell-recent-forget {
	flex: none;
	background: none;
	border: 1px solid transparent;
	border-radius: 5px;
	padding: 0 8px;
	color: var(--fg-faint);
	cursor: pointer;
	font-size: 14px;
}

.shell-recent-forget:hover {
	background: var(--bg-hover);
	border-color: var(--border);
	color: var(--fg);
}

/* ------------------------------------------------------- .luau source view */

.source {
	display: grid;
	grid-template-rows: auto auto 1fr;
	min-height: 0;
	height: 100%;
	background: var(--bg-canvas);
}

.source-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
}

.source-head .name {
	font-size: 12px;
	font-weight: 600;
	color: var(--fg);
}

.source-head .badge {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 1px 7px;
	color: var(--fg-muted);
}

/* Generated files are Roswaal's, and saying so in the accent colour it uses
   for its own output everywhere else saves reading the word. */
.source-head .badge.generated {
	border-color: color-mix(in srgb, var(--accent) 55%, transparent);
	color: var(--accent);
}

.source-head .meta {
	font-size: 11px;
	color: var(--fg-faint);
}

.source-note {
	margin: 0;
	padding: 6px 12px;
	font-size: 11px;
	color: var(--fg-faint);
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
}

.source-body {
	min-height: 0;
	overflow: hidden;
}

/* -------------------------------------------------------------- inspector */

.inspector {
	background: var(--bg-panel);
	border-left: 1px solid var(--border);
	overflow: auto;
	min-height: 0;
}

.inspector h2 {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin: 0;
	padding: 10px 12px 6px;
	position: sticky;
	top: 0;
	background: var(--bg-panel);
}

.inspector-body {
	padding: 0 12px 20px;
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.inspector .node-heading {
	border-radius: 5px;
	padding: 6px 10px;
	color: #fff;
	font-weight: 600;
	text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
}

.inspector .summary {
	margin: 0;
	font-size: 11px;
	line-height: 1.5;
	color: var(--fg-muted);
}

.inspector .field {
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.inspector .field > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.inspector .field input {
	cursor: text;
	width: 100%;
}

/* A row of colour choices, which wraps rather than shrinking: a swatch smaller
   than this stops being a colour you can tell from its neighbour. */
.inspector .swatches {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
}

.inspector .swatch {
	width: 22px;
	height: 22px;
	padding: 0;
	border: 1px solid var(--border);
	border-radius: 5px;
	cursor: pointer;
}

.inspector .swatch.on {
	/* Two rings rather than a border, so the mark reads on a swatch of any
	   colour -- including one close to the panel's own background. */
	outline: 2px solid var(--fg);
	outline-offset: 1px;
}

.inspector .swatch:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

.inspector .list-editor {
	border-top: 1px solid var(--border);
	padding-top: 10px;
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.inspector .list-title {
	display: flex;
	align-items: center;
	justify-content: space-between;
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
}

.inspector .list-title button {
	padding: 1px 8px;
	font-size: 11px;
}

/* Name and type share the width 60/40 as the panel is resized, rather than
   the type staying 90px while the name takes the rest. */
.inspector .list-row {
	display: grid;
	grid-template-columns: minmax(0, 3fr) minmax(0, 2fr) 24px;
	gap: 4px;
}

.inspector .list-row input {
	cursor: text;
	min-width: 0;
}

/* The remove button. Not the type picker beside it, which is a button too and
   lost its padding to this. */
.inspector .list-row button:not(.type-picker) {
	padding: 0;
	text-align: center;
}

.inspector .pin-summary {
	display: flex;
	justify-content: space-between;
	font-size: 11px;
	color: var(--fg-muted);
	padding: 1px 0;
}

.inspector .pin-summary .type {
	color: var(--fg-faint);
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 10px;
}

/* Two-line node headers: a name on top, its signature or target underneath. */

.node .head.two-line {
	align-items: center;
}

.node .head .lines {
	display: flex;
	flex-direction: column;
	justify-content: center;
	min-width: 0;
	line-height: 1.25;
}

.node .head .subtitle {
	font-size: 10px;
	font-weight: 500;
	opacity: 0.78;
	font-family: "Cascadia Mono", Consolas, monospace;
	letter-spacing: -0.01em;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* -------------------------------------------------------------- variables */

.variables {
	border-top: 1px solid var(--border);
	flex: none;
}

.variables h2 {
	display: flex;
	align-items: center;
	justify-content: space-between;
	position: static;
}

.variables h2 button {
	padding: 1px 8px;
	font-size: 11px;
	text-transform: none;
	letter-spacing: 0;
}

.variable-list {
	padding: 0 6px 12px;
}

.variables .hint {
	margin: 0;
	padding: 2px 8px 6px;
	font-size: 11px;
	line-height: 1.5;
	color: var(--fg-faint);
}

/* A second list under the variables: the graph's locals, and its types. */
.variables-sub {
	margin: 10px 0 2px;
	padding: 0 6px;
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--fg-faint);
}

.variable-head {
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 3px 6px;
	border-radius: 4px;
	cursor: grab;
	user-select: none;
	white-space: nowrap;
}

.variable-head:hover {
	background: var(--bg-hover);
}

.variable.expanded .variable-head {
	background: var(--bg-active);
}

.variable-head .swatch {
	width: 9px;
	height: 9px;
	border-radius: 50%;
	flex: none;
}

.variable-head .name {
	overflow: hidden;
	text-overflow: ellipsis;
}

/* A type has no pin colour, so it takes the colour types have in code. Square,
   because it is not a value. */
.variable-head .type-swatch {
	border-radius: 2px;
	background: var(--code-type);
}

.variable-head .type {
	margin-left: auto;
	font-size: 10px;
	color: var(--fg-faint);
	font-family: "Cascadia Mono", Consolas, monospace;
}

/* Faint until pointed at, so a list of locals does not read as a list of
   deletes; still there for a finger, which cannot hover. */
.variable-head .variable-delete {
	flex: none;
	padding: 0 5px;
	line-height: 16px;
	background: transparent;
	border-color: transparent;
	color: var(--fg-faint);
	cursor: pointer;
}

.variable-head .variable-delete:hover,
.variable-head .variable-delete:focus-visible {
	color: var(--danger, #e06c6c);
	border-color: var(--border);
}

.variable-body {
	display: flex;
	flex-direction: column;
	gap: 7px;
	padding: 8px 8px 12px 22px;
}

.variable-body .field {
	display: grid;
	grid-template-columns: 84px 1fr;
	align-items: center;
	gap: 6px;
}

.variable-body .field > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.variable-body .field input[type="text"],
.variable-body .field input:not([type]),
.variable-body .field input[type="number"],
.variable-body .field select {
	cursor: text;
	min-width: 0;
	width: 100%;
}

.variable-footer {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
}

.variable-footer .hint {
	padding: 0;
}

.variable-footer button {
	padding: 1px 8px;
	font-size: 11px;
	color: var(--danger);
}

/* ------------------------------------------------------------- map editor */

.map-editor {
	display: grid;
	grid-template-columns: 1fr 380px;
	min-height: 0;
	overflow: hidden;
}

.map-tree {
	overflow: auto;
	background: var(--bg-canvas);
	padding-bottom: 16px;
}

.map-bar {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-panel);
	position: sticky;
	top: 0;
	z-index: 1;
	font-weight: 600;
}

.map-bar .dirty::after {
	content: "•";
	color: var(--warning);
	margin-left: 5px;
}

.map-row {
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 4px 10px;
	cursor: default;
	user-select: none;
	white-space: nowrap;
}

.map-row:hover {
	background: var(--bg-hover);
}

.map-row.selected {
	background: var(--bg-active);
	box-shadow: inset 2px 0 0 var(--accent);
}

.map-row .glyph {
	width: 12px;
	color: var(--fg-faint);
	cursor: pointer;
	flex: none;
}

.map-row .name {
	font-weight: 500;
}

.map-row .class,
.map-row .path {
	font-size: 11px;
	font-family: "Cascadia Mono", Consolas, monospace;
	color: var(--fg-faint);
}

.map-row .path {
	margin-left: auto;
	color: var(--accent);
}

.map-side {
	border-left: 1px solid var(--border);
	background: var(--bg-panel);
	overflow: auto;
	display: flex;
	flex-direction: column;
	min-height: 0;
}

/* The column scrolls; nothing in it gives way. A flex item shrinks when the
   column is shorter than its contents, and the project file -- a scroller of
   its own, so free to shrink to nothing -- took all of it: on an iPad it was
   one line high under a column that scrolled past it. */
.map-side > * {
	flex-shrink: 0;
}

.map-side h2 {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin: 0;
	padding: 10px 12px 6px;
}

.map-side .summary {
	margin: 0;
	padding: 0 12px 6px;
	font-size: 11px;
	color: var(--fg-muted);
}

.map-fields,
.map-side > .field {
	display: flex;
	flex-direction: column;
	gap: 7px;
	padding: 0 12px 8px;
}

.map-fields .field,
.map-side > .field {
	display: grid;
	grid-template-columns: 100px 1fr;
	align-items: center;
	gap: 6px;
}

.map-fields .field > span,
.map-side > .field > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.map-fields input,
.map-fields select,
.map-side > .field input {
	cursor: text;
	min-width: 0;
	width: 100%;
}

.map-actions {
	display: flex;
	gap: 6px;
	padding: 0 12px 10px;
	flex-wrap: wrap;
}

.map-preview {
	margin: 0 12px 12px;
	padding: 10px 12px;
	background: var(--bg-canvas);
	border: 1px solid var(--border);
	border-radius: 5px;
	font: 11px/1.55 "Cascadia Mono", Consolas, monospace;
	overflow: auto;
	max-height: 340px;
	white-space: pre;
}

.map-diagnostics {
	padding: 0 12px 16px;
}

.map-diagnostics .entry {
	display: flex;
	gap: 8px;
	font-size: 11px;
	padding: 2px 0;
}

.map-diagnostics .entry .sev {
	width: 56px;
	flex: none;
	font-weight: 600;
}

.map-diagnostics .entry.error .sev {
	color: var(--danger);
}

.map-diagnostics .entry.warning .sev {
	color: var(--warning);
}

.tree-menu {
	width: 170px;
	max-height: none;
}

.tree-menu .item.danger {
	color: var(--danger);
}

/* ------------------------------------------------------------ code editor */

:root {
	--code-keyword: #9a4f9a;
	--code-string: #2f7f4f;
	--code-number: #8a5a2f;
	--code-comment: #7a8290;
	--code-operator: #4a6fa5;
	--code-property: #2c6f8f;
}

@media (prefers-color-scheme: dark) {
	:root {
		--code-keyword: #c98fd0;
		--code-string: #8fce9b;
		--code-number: #d9a86c;
		--code-comment: #6b7280;
		--code-operator: #86b3e0;
		--code-property: #7fc4d6;
	}
}

/* The raw-literal button on a node: a peek at the code, not the code. */
.node .literal.code {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	max-width: 104px;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 1px 5px;
	font: 10px/1.5 "Cascadia Mono", Consolas, monospace;
	cursor: pointer;
	overflow: hidden;
}

.node .literal.code:hover {
	border-color: var(--accent);
}

.node .literal.code .preview {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--fg-muted);
}

.node .literal.code .more {
	flex: none;
	color: var(--fg-faint);
	border-left: 1px solid var(--border);
	padding-left: 4px;
}

.code-backdrop {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.45);
	display: grid;
	place-items: center;
	padding: 40px;
}

.code-modal {
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
	width: min(880px, 100%);
	height: min(600px, 100%);
	display: grid;
	grid-template-rows: auto 1fr auto;
	overflow: hidden;
}

.code-head {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 8px 12px;
	border-bottom: 1px solid var(--border);
}

.code-head .title {
	font-weight: 600;
}

.code-head .hint {
	font-size: 11px;
	color: var(--fg-faint);
}

.code-body {
	overflow: hidden;
	min-height: 0;
}

.code-body .cm-editor {
	height: 100%;
}

/* A Custom Code node's Luau, opened from a graph in the docs: the code
   editor's frame and look, read-only, and as tall as the code up to the
   editor's own height. The gutter and face are `luauTheme.ts`'s. */
.docs-code-backdrop {
	z-index: 1000;
}

.code-modal.read-only {
	height: auto;
	max-height: min(600px, 100%);
	grid-template-rows: auto minmax(0, 1fr);
}

.code-modal.read-only .code-head .hint {
	flex: 1;
}

.code-modal.read-only .code-body {
	overflow: auto;
	background: var(--bg-canvas);
}

.code-view {
	display: flex;
	min-height: 100%;
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 12px;
	line-height: 1.5;
}

.code-view pre {
	margin: 0;
	padding: 10px 0;
	font: inherit;
	line-height: inherit;
	white-space: pre;
	background: none;
	border: 0;
	border-radius: 0;
}

.code-view-gutter {
	flex: none;
	padding-left: 14px !important;
	padding-right: 10px !important;
	text-align: right;
	color: var(--fg-faint);
	background: var(--bg-panel) !important;
	border-right: 1px solid var(--border) !important;
	user-select: none;
}

.code-view-code {
	flex: 1;
	padding-left: 8px !important;
	padding-right: 14px !important;
}

.code-view-code code {
	font: inherit;
	padding: 0;
	background: none;
	color: var(--fg);
}

/* A node in a docs graph that opens its code: the pointer says so. */
.graph-viewport .docs-code-node {
	cursor: pointer;
}

.graph-viewport .docs-code-node:hover {
	filter: brightness(1.15);
}

.code-body .cm-scroller {
	overflow: auto;
}

.code-status {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 6px 12px;
	border-top: 1px solid var(--border);
	font-size: 11px;
	color: var(--fg-muted);
}

.code-status.bad {
	color: var(--danger);
}

.code-status .where {
	color: var(--fg-faint);
}

.code-status .keys {
	color: var(--fg-faint);
	font-size: 10px;
}

/* ---------------------------------------------------------- operator pills */

/* A comparison is `a == b`, and a full node says that in three rows under a
   header repeating what the symbol already says. The pill is the same node
   drawn as the expression, which is how a graph of conditions reads best. */
/* The corner comes from `NODE.operatorRadius`, set inline: the wire router and
   the documentation previews measure the same constant, and a second opinion
   here would put the shape and the pins a few pixels apart. */
.node.operator {
	cursor: grab;
}

.node.operator .operator-rows {
	position: absolute;
	left: 0;
	right: 0;
}

/* A and B say nothing the symbol between them has not. */
.node.operator .pin-label {
	display: none;
}

.node.operator .operator-symbol {
	position: absolute;
	top: 0;
	bottom: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 13px;
	font-weight: 700;
	color: var(--fg);
	/* The pill is dragged by its middle like any other node. */
	pointer-events: none;
}

.node.operator .operator-out {
	position: absolute;
	right: var(--pin-pad);
	display: flex;
}

/* A pill's result is not on a row, so it misses the rule that puts a data pin on
   the node's edge. Same offset, stated once more for the one pin outside a row.
   The pill's inputs are in rows and are already covered. */
.node.operator .operator-out .pin {
	left: calc(var(--pin-pad) + var(--pin-slot) / 2 + var(--node-stroke));
}

.node.operator .operator-grow {
	position: absolute;
	top: 0;
	bottom: 0;
	display: flex;
	flex-direction: column;
	justify-content: center;
	gap: 2px;
}

.node.operator .operator-grow button {
	width: 14px;
	height: 14px;
	padding: 0;
	border: 1px solid var(--border);
	border-radius: 3px;
	background: var(--bg-input);
	color: var(--fg-muted);
	font-size: 10px;
	line-height: 1;
	cursor: pointer;
}

.node.operator .operator-grow button:disabled {
	opacity: 0.3;
	cursor: default;
}

/* --------------------------------------------------------- capsule getters */

/* The usual node-editor shape for a variable getter: a dark pill, the name, one
   output. The form itself says "this is a value", so it needs no header and no
   title row. */
.node.capsule {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 0 10px 0 12px;
	/* A capsule's pin sits on the capsule, not on a node body, so its hollow has
	   to be the capsule's colour -- the same reason a knot sets this. */
	--pin-hollow: var(--capsule-bg);
	border-radius: 999px;
	background: var(--capsule-bg);
	border: 1px solid var(--capsule-border);
	box-shadow: var(--node-shadow);
	cursor: grab;
}

.node.capsule .capsule-label {
	flex: 1;
	min-width: 0;
	font-size: 12px;
	font-weight: 500;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.node.capsule .pin-label {
	display: none;
}

/* Balanced on the capsule's right edge, where the wire attaches, the same way
   a data pin sits on a node's. The 10px is the capsule's own right padding. */
.node.capsule .pin {
	flex: none;
	left: calc(10px + var(--pin-slot) / 2 + var(--node-stroke));
}

:root {
	--capsule-bg: #e9ebf0;
	--capsule-border: #b3b9c4;
}

@media (prefers-color-scheme: dark) {
	:root {
		--capsule-bg: #2b3038;
		--capsule-border: #3d434e;
	}
}

/* ----------------------------------------------------------------- dialog */

.dialog-backdrop {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.4);
	display: grid;
	place-items: center;
	padding: 40px;
}

.dialog {
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
	width: min(420px, 100%);
	padding: 16px 18px 14px;
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.dialog h3 {
	margin: 0;
	font-size: 14px;
}

.dialog p {
	margin: 0;
	color: var(--fg-muted);
	line-height: 1.5;
}

.dialog .field {
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.dialog .field > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.dialog .field input {
	cursor: text;
	width: 100%;
}

.dialog-actions {
	display: flex;
	justify-content: flex-end;
	gap: 8px;
}

/* The filled destructive button: the confirm on "Delete this?".

   It states all three of background, colour and border rather than only the
   one that differs from `.primary`. `.danger` on its own is the *outline*
   destructive button -- danger text, danger border, no fill -- and it sits
   later in this file than `.primary`, so at equal specificity its `color`
   won. A filled button that took its fill from here and its text from there
   rendered `--danger` on `--danger`: the word "Delete" was still in the
   button, in exactly the colour of the button.

   The hover needs saying for the same kind of reason. `button.tb.primary:hover`
   carries a pseudo-class and so outranks a plain `.primary.danger`, which
   turned the delete button accent-blue the moment the pointer reached it. */
button.tb.primary.danger {
	background: var(--danger);
	border-color: transparent;
	color: #fff;
}

button.tb.primary.danger:hover:not(:disabled) {
	background: var(--danger);
}

/* A pure node has no execution pins and can be wired anywhere. Green is the
   usual signal for that; a thin edge carries it here without fighting the
   category colour on the header. */
.node.pure {
	border-left: 3px solid var(--pure-edge);
}

:root {
	--pure-edge: #6aa84f;
}

@media (prefers-color-scheme: dark) {
	:root {
		--pure-edge: #7fbf5f;
	}
}

/* A pin with a known value set gets a dropdown rather than a text field. */
.node select.literal {
	cursor: pointer;
	appearance: none;
	padding-right: 14px;
	background-image: linear-gradient(45deg, transparent 50%, var(--fg-faint) 50%),
		linear-gradient(135deg, var(--fg-faint) 50%, transparent 50%);
	background-position: calc(100% - 8px) 6px, calc(100% - 4px) 6px;
	background-size: 4px 4px, 4px 4px;
	background-repeat: no-repeat;
	text-overflow: ellipsis;
}

/* Arity controls, for nodes that choose how many inputs they take. The header
   is where they fit without changing the node's geometry. */
.node .head .grow {
	display: flex;
	gap: 2px;
	margin-left: auto;
	flex: none;
}

.node .head .grow button {
	width: 16px;
	height: 16px;
	padding: 0;
	line-height: 14px;
	text-align: center;
	border: 1px solid rgba(255, 255, 255, 0.35);
	border-radius: 3px;
	background: rgba(0, 0, 0, 0.2);
	color: #fff;
	cursor: pointer;
	font-size: 12px;
}

.node .head .grow button:hover:not(:disabled) {
	background: rgba(255, 255, 255, 0.25);
}

.node .head .grow button:disabled {
	opacity: 0.3;
	cursor: default;
}

/* Opens a Declare Function's graph. Beside the grow buttons, and drawn like them. */
.node .head .open-graph {
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 18px;
	height: 18px;
	margin-left: auto;
	padding: 0;
	border: 1px solid rgba(255, 255, 255, 0.35);
	border-radius: 3px;
	background: rgba(0, 0, 0, 0.2);
	color: #fff;
	cursor: pointer;
}

.node .head .open-graph:hover {
	background: rgba(255, 255, 255, 0.25);
}

.node .head .open-graph + .grow {
	margin-left: 4px;
}

/* A node that a dragged wire can grow to accept. */
.node.grow-target {
	box-shadow: 0 0 0 2px var(--ok), var(--node-shadow);
}

/* ------------------------------------------------------------------ icons */

.icon {
	flex: none;
	display: block;
	transition: transform 0.12s ease;
}

/* The mark. It takes the colour of whatever it sits in — see logo.tsx — so it
   needs no colour of its own here, only the same layout the icons get. */
.logo-mark {
	flex: none;
	display: block;
}

.tree-row .twist {
	color: var(--fg-faint);
	cursor: pointer;
}

.tree-row .kind {
	color: var(--fg-muted);
}

/* Graphs are the editable thing here, so they carry the accent; generated
   Luau sits back with the rest of the repository. */
.tree-row .kind.nodescript {
	color: var(--accent);
}

.tree-row .kind.function {
	color: var(--accent);
}

.tree-row .function-twist {
	display: inline-flex;
	margin-left: -14px;
}

.tree-row .kind.nodemap {
	color: var(--ok);
}

.tree-row .kind.luau {
	color: var(--fg-faint);
}

.menu .item .icon {
	color: var(--fg-muted);
}

.menu .item.danger .icon {
	color: var(--danger);
}

button.tb .icon,
.toolbar .icon {
	color: currentColor;
}

/* A toolbar button that is an icon plus a label. */
button.tb.with-icon {
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

/* An icon on its own, which is most of the toolbar: the label is the tooltip,
   and the row keeps its width for the two buttons that say what they do. A
   square rather than a pill, so a row of them reads as a set of tools. */
button.tb.icon-only {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 5px;
	width: 28px;
	height: 28px;
	color: var(--fg-muted);
}

button.tb.icon-only:hover:not(:disabled) {
	color: var(--fg);
}

button.tb.icon-only.on {
	background: var(--bg-active);
	color: var(--fg);
	border-color: var(--border-strong);
}

/* Path field with a resolution hint: Rojo builds an empty instance for a path
   that is not there, so an unresolved one has to be loud here. */
.map-fields .path-field {
	display: flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
}

.map-fields .path-field input {
	flex: 1;
	min-width: 0;
}

.path-missing {
	display: inline-flex;
	align-items: center;
	gap: 3px;
	flex: none;
	font-size: 10px;
	color: var(--danger);
	white-space: nowrap;
}

.map-list {
	border-top: 1px solid var(--border);
	margin: 0 12px;
	padding: 8px 0;
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.map-list-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
}

.map-list-head button {
	padding: 1px 8px;
	font-size: 11px;
	text-transform: none;
	letter-spacing: 0;
}

.map-list .summary {
	padding: 0;
}

.map-list-row {
	display: grid;
	grid-template-columns: 1fr 24px;
	gap: 4px;
}

.map-list-row input {
	cursor: text;
	min-width: 0;
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 11px;
}

.map-list-row button {
	padding: 0;
	text-align: center;
}

/* The menu shown when a file is dragged from the tree onto the canvas. */
.drop-menu {
	width: 260px;
	max-height: none;
}

.drop-head {
	padding: 8px 10px;
	border-bottom: 1px solid var(--border);
	display: flex;
	flex-direction: column;
	gap: 3px;
}

.drop-head code {
	font: 10px/1.4 "Cascadia Mono", Consolas, monospace;
	color: var(--accent);
	word-break: break-all;
}

.drop-note {
	margin: 4px 8px 6px;
	font-size: 11px;
	line-height: 1.5;
	color: var(--fg-faint);
}

/* Extra token colours the Luau mode distinguishes. */
:root {
	--code-global: #2c6f8f;
	--code-function: #7a5a2f;
	--code-type: #2f7a6a;
}

@media (prefers-color-scheme: dark) {
	:root {
		--code-global: #7fc4d6;
		--code-function: #d9b06c;
		--code-type: #6fc2a8;
	}
}

/* Lint squiggles and the completion popup, themed with the rest of the app. */
.code-body .cm-lintRange-error {
	background: none;
	border-bottom: 2px wavy var(--danger);
}

.code-body .cm-diagnostic {
	border-left-color: var(--danger);
	background: var(--bg-panel);
	color: var(--fg);
	font-family: inherit;
}

.code-body .cm-tooltip,
.source-body .cm-tooltip {
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 6px;
	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
	color: var(--fg);
}

.code-body .cm-tooltip-autocomplete > ul > li {
	padding: 3px 8px;
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 11px;
}

.code-body .cm-tooltip-autocomplete > ul > li[aria-selected] {
	background: var(--accent);
	color: #fff;
}

.code-body .cm-completionDetail {
	margin-left: 10px;
	font-style: normal;
	opacity: 0.7;
	font-size: 10px;
}

.code-body .cm-lint-marker-error {
	content: none;
}

/* A wash across any line carrying a problem, so the bad line is findable
   without reading the gutter. Kept faint: it marks the line, it does not
   compete with the squiggle for attention. */
.code-body .cm-errorLine {
	background: color-mix(in srgb, var(--danger) 11%, transparent);
	box-shadow: inset 2px 0 0 color-mix(in srgb, var(--danger) 65%, transparent);
}

/* The active line still reads as active when it is also the broken one. */
.code-body .cm-errorLine.cm-activeLine {
	background: color-mix(in srgb, var(--danger) 17%, transparent);
}

/* A reroute knot: a dot the wire passes through, with both pins stacked at its
   centre so the wire enters and leaves at the same point. */
.node.reroute {
	border-radius: 50%;
	background: var(--capsule-bg);
	/* An exec knot draws a hollow arrow, and the hollow has to be the colour it
	   is actually sitting on — the capsule, not a node body. */
	--pin-hollow: var(--capsule-bg);
	border: 1px solid var(--capsule-border);
	box-shadow: var(--node-shadow);
	display: grid;
	place-items: center;
	cursor: grab;
}

/* A knot is 22px and cannot afford the slot a node's pin row uses.
 *
 * At 16px plus a 3px hit margin the pin covered the knot edge to edge, and the
 * knot stopped being draggable — there was no ring left to grab, only pin. The
 * slot shrinks back here so the two gestures stay separable: the middle starts
 * a wire, the rim moves the knot. */
.node.reroute .pin {
	--pin-slot: 12px;
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
}

.node.reroute .pin::after {
	inset: -1px;
}

/* A knot's pins stay at its centre -- it is a bend in a wire, not a node with
   sides to hang anything off. An execution triangle is narrower than its slot,
   so centre it in the slot rather than leaving it against the left edge, where
   the rows want it. */
.node.reroute .pin.exec .dot {
	left: calc((var(--pin-slot) - var(--exec-width)) / 2);
}

/* A knot is 22px across and its stacked pins take 14 of them, which left a
   4px ring of node to grab -- less than that zoomed out. Selecting or moving
   a knot was a precision task, and both are as common as dragging a new wire
   off one. The ring grows outwards rather than taking from the pin, so
   starting a wire is exactly as easy as it was. Behind everything, so it
   catches only what would have hit the canvas. */
.node.reroute::before {
	content: "";
	position: absolute;
	inset: -6px;
	border-radius: 50%;
	z-index: -1;
}

.canvas.wiring .node.reroute .pin::after {
	inset: -5px;
}

/* Both of a knot's pins sit at its centre, stacked. Drawing both means an
   unwired output's hollow ring shows the wired input's fill through the middle,
   which reads as a pin that will not let go of its colour. Only the first is
   drawn; the second keeps its hit area, so a wire can still be dragged out. */
.node.reroute .pin + .pin .dot {
	display: none;
}

.node.reroute .pin-label {
	display: none;
}

.node.reroute.selected {
	border-color: var(--select);
}

/* ------------------------------------------------------------------- docs
 *
 * The Docs panel. Wider and taller than the old Help dialog because it now
 * holds the whole reference — a nav of fourteen sections and a page per node —
 * rather than five prose panes.
 */

.docs-backdrop {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.5);
	display: grid;
	place-items: center;
	padding: 32px;
}

.docs {
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5);
	width: min(1080px, 100%);
	height: min(820px, 100%);
	display: grid;
	grid-template-rows: auto 1fr;
	overflow: hidden;
}

.docs-head {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 10px 14px;
	border-bottom: 1px solid var(--border);
}

.docs-head .sub {
	color: var(--fg-faint);
	font-size: 12px;
}

.docs-body {
	display: grid;
	grid-template-columns: 232px 1fr;
	min-height: 0;
}

/* ----------------------------------------------------------------- docs nav */

.docs-nav {
	border-right: 1px solid var(--border);
	padding: 10px 8px;
	overflow: auto;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.docs-nav .search {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 5px 8px;
	margin-bottom: 8px;
	outline: none;
	font: inherit;
	color: inherit;
}

.docs-nav .search:focus {
	border-color: var(--accent);
}

.docs-section-head {
	display: flex;
	align-items: center;
	gap: 6px;
	width: 100%;
	background: none;
	border: 0;
	text-align: left;
	padding: 5px 6px;
	border-radius: 5px;
	cursor: pointer;
	color: var(--fg);
	font-weight: 600;
	font-size: 12px;
}

.docs-section-head:hover { background: var(--bg-hover); }

/* Rotated rather than swapped for an icon, so the two states are obviously the
   same control in two positions. */
.docs-section-head .icon {
	transition: transform 0.12s ease;
	transform: rotate(-90deg);
	flex: none;
}

.docs-section-head[aria-expanded="true"] .icon { transform: none; }

@media (prefers-reduced-motion: reduce) {
	.docs-section-head .icon { transition: none; }
}

.docs-section-head .count {
	margin-left: auto;
	font-weight: 400;
	font-size: 10px;
	color: var(--fg-faint);
	font-variant-numeric: tabular-nums;
}

.docs-pages {
	display: flex;
	flex-direction: column;
	padding: 1px 0 4px 14px;
}

.docs-link,
.docs-hit {
	display: flex;
	align-items: baseline;
	gap: 6px;
	width: 100%;
	background: none;
	border: 0;
	text-align: left;
	padding: 4px 8px;
	border-radius: 4px;
	cursor: pointer;
	color: var(--fg-muted);
	font-size: 12px;
}

.docs-link:hover,
.docs-hit:hover { background: var(--bg-hover); color: var(--fg); }

.docs-link.on,
.docs-hit.on {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	color: var(--fg);
}

.docs-hit { flex-direction: column; gap: 0; align-items: stretch; }
.docs-hit .title { color: inherit; }
.docs-hit .where { font-size: 10px; color: var(--fg-faint); }

.docs-results { display: flex; flex-direction: column; gap: 1px; }
.docs-results .empty { padding: 12px 8px; color: var(--fg-faint); font-size: 12px; }

/* ------------------------------------------------------- docs search palette */

/* Over the page rather than beside it: the palette is what you reach for when
   you do not know which page you want, and the answer is worth the width. */
/* Shared by the two pickers that cover the page: the documentation's and the
   node one. The docs palette named it first; the class is the shape. */
.picker-backdrop,
.docs-palette-backdrop {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.45);
	display: flex;
	justify-content: center;
	/* High rather than centred: the list grows downwards, so a centred panel
	   would walk up the screen as you type. */
	align-items: flex-start;
	padding: 10vh 16px 16px;
}

.docs-palette {
	width: min(720px, 100%);
	max-height: 70vh;
	display: flex;
	flex-direction: column;
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 10px;
	box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
	overflow: hidden;
}

.docs-palette-field {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 12px;
	border-bottom: 1px solid var(--border);
}

.docs-palette-field input {
	flex: 1;
	min-width: 0;
	background: none;
	border: 0;
	color: var(--fg);
	font-size: 15px;
	outline: none;
}

.docs-palette-list {
	overflow: auto;
	padding: 6px;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.docs-palette-head {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	padding: 6px 8px 4px;
}

.docs-palette-list .empty {
	padding: 12px 8px;
	color: var(--fg-faint);
	font-size: 12px;
}

.docs-palette-hit {
	display: flex;
	align-items: flex-start;
	gap: 10px;
	width: 100%;
	background: none;
	border: 0;
	border-radius: 6px;
	padding: 8px;
	text-align: left;
	cursor: pointer;
	color: var(--fg-muted);
}

.docs-palette-hit.on {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	color: var(--fg);
}

/* Article or Node, in a fixed column so the titles line up down the list, and
   coloured so the two kinds separate before you have read either. Yellow is a
   node, blue is an article -- tinted rather than filled, because the row's own
   highlight is the accent and two solid blocks would compete for it. */
.docs-palette-hit .kind {
	flex: none;
	width: 52px;
	font-size: 9px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 1px 0;
	text-align: center;
	margin-top: 2px;
}

.docs-palette-hit .kind-node {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 55%, transparent);
	background: color-mix(in srgb, var(--warning) 12%, transparent);
}

.docs-palette-hit .kind-article {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 55%, transparent);
	background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* A heading each, so "Best match" and "Related" read as two lists rather than
   as one list with a line in it. */
.docs-palette-section {
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.docs-palette-section + .docs-palette-section {
	margin-top: 6px;
	border-top: 1px solid var(--border);
	padding-top: 4px;
}

.docs-palette-hit .body {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
}

.docs-palette-hit .title { font-size: 13px; color: var(--fg); }
.docs-palette-hit .where { font-size: 10px; color: var(--fg-faint); }

/* One line of it. The summary is there to tell two similar titles apart, and a
   paragraph per row would make the list unscannable. */
.docs-palette-hit .summary {
	font-size: 11px;
	color: var(--fg-muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.docs-palette-foot {
	display: flex;
	gap: 14px;
	padding: 8px 12px;
	border-top: 1px solid var(--border);
	font-size: 10px;
	color: var(--fg-faint);
}

.docs-palette-foot kbd {
	font-family: inherit;
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
	margin-right: 3px;
}

.docs .badge {
	font-size: 9px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
	white-space: nowrap;
}

/* ------------------------------------------------------------- docs content */

/* The last line of a page has to be able to reach the middle of the screen.
   With a bottom padding the height of the text, the end of a page sits at the
   very bottom of the window when you have scrolled as far as you can -- which
   is where a taskbar is, and is the worst place to read the paragraph you
   scrolled down for. A viewport-relative pad gives the same room on a laptop
   and a large display, and is capped so it does not become a page of nothing. */
.docs-content {
	overflow: auto;
	padding: 22px 30px 60px;
	/* Long-form reading, so the measure is capped rather than filling the pane. */
	max-width: 74ch;
}

.docs-content h1 {
	font-size: 22px;
	margin: 0 0 4px;
	display: flex;
	align-items: center;
	gap: 8px;
}

.docs-content .summary {
	margin: 0 0 22px;
	color: var(--fg-faint);
}

.docs-content h2 {
	font-size: 15px;
	margin: 28px 0 8px;
	padding-top: 14px;
	border-top: 1px solid var(--border);
}

.docs-content h3 { font-size: 13px; margin: 20px 0 6px; }
.docs-content p { margin: 0 0 12px; line-height: 1.6; }
.docs-content ul,
.docs-content ol { margin: 0 0 12px; padding-left: 20px; line-height: 1.6; }
.docs-content li { margin-bottom: 4px; }
.docs-content li::marker { color: var(--fg-faint); }

.docs-content code {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
	font-size: 11px;
}

.docs-content pre {
	background: var(--bg-canvas);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 10px 12px;
	overflow-x: auto;
	font-size: 11.5px;
	line-height: 1.55;
	margin: 0 0 14px;
}

.docs-table { overflow-x: auto; margin: 0 0 14px; }

.docs-content table {
	border-collapse: collapse;
	width: 100%;
	font-size: 12px;
}

.docs-content th {
	text-align: left;
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
	padding: 0 10px 5px 0;
	border-bottom: 1px solid var(--border);
	white-space: nowrap;
}

.docs-content td {
	padding: 7px 10px 7px 0;
	border-bottom: 1px solid var(--border);
	vertical-align: top;
	line-height: 1.5;
}

.docs-content td.mono { font-size: 11px; color: var(--fg-muted); white-space: nowrap; }
.docs-content td em { color: var(--fg-faint); font-style: normal; }

.docs-content table.pins td.swatch-cell { width: 14px; padding-right: 6px; }

.docs-swatch {
	display: inline-block;
	width: 9px;
	height: 9px;
	border-radius: 50%;
}

/* Execution pins keep their triangle here too, so the table matches the canvas. */
.docs-swatch.exec {
	border-radius: 0;
	width: 8px;
	clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* After Docusaurus's admonitions: the kind is a colour, a word and an icon,
   so the reader knows why the box is there before reading it. The colour is
   the theme's own, so a note takes whichever theme the reader has chosen. */
.docs-note {
	--note: var(--border-strong);
	border-left: 4px solid var(--note);
	background: color-mix(in srgb, var(--note) 9%, var(--bg-input));
	border-radius: 0 4px 4px 0;
	padding: 9px 12px;
	margin: 0 0 14px;
	line-height: 1.55;
}

.docs-note.note-info { --note: var(--accent); }
.docs-note.note-good { --note: var(--ok); }
.docs-note.note-warn { --note: var(--warning); }
.docs-note.note-danger { --note: var(--danger); }

.docs-note-head {
	display: flex;
	align-items: center;
	gap: 6px;
	margin: 0 0 4px;
	font-size: 11px;
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--note);
}

.docs-note-body > :last-child {
	margin-bottom: 0;
}

/* A list inside a note: several separate claims rather than one thought.
   Tightened against the paragraph above it, so the sentence introducing them
   and the points themselves read as one block rather than two. */
.docs-note ul {
	margin: 6px 0 0;
	padding-left: 18px;
}

.docs-note li {
	margin: 3px 0;
}


/* The docs as their own window, at /docs. Fills the viewport rather than
   floating over a canvas, because there is no canvas behind it. */
.docs-page {
	display: grid;
	grid-template-rows: auto 1fr;
	height: 100%;
	background: var(--bg-app);
}

/* Centred rather than baseline-aligned. This bar holds a mark, a version, a
 * warning chip and a link, and only two of those are text — an SVG's baseline
 * is its bottom edge, so `baseline` dropped the mark to the floor of the line
 * box and hung everything else off that. */
.docs-page-head {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 8px 14px;
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
}

.docs-page-head .logo {
	font-weight: 600;
	letter-spacing: 0.04em;
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

/* Says this is the browser build. Compound with `.version` so it inherits the
   size and numerals rather than restating them, and reads as a chip beside the
   number rather than as a second number.

   Every surface of that build carries it and every link into it is labelled
   with it -- see `src/app/previewBuild.tsx`. So the selector is every header
   that has a mark, plus the documentation site's header link, rather than the
   editor's toolbar alone: scoped to `.toolbar`, Node Design grew the component
   and rendered it invisible. */
.toolbar .logo .version.preview-chip,
.docs-page-head .logo .version.preview-chip,
.docs-page-head a.tb .version.preview-chip {
	/* The colour is the variant's, not this rule's. These three selectors are
	   scoped to a context and so outspecify any plain `.preview-chip.canary`,
	   which is how the canary chip first shipped painted accent blue while
	   reading CANARY. Reading a custom property moves the decision to the
	   element, where both variants set it at their own specificity and the more
	   specific one wins -- rather than to whichever selector happens to have
	   collected more classes. */
	color: var(--chip-ink);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 4px;
	margin-left: 2px;
	text-transform: uppercase;
	font-size: 9px;
	letter-spacing: 0.04em;
}

/* In the header's link it sits inside a button-shaped anchor, which sets its
   own colour and its own gap. The chip keeps the accent and takes the gap from
   the flex row rather than a margin of its own. */
.docs-page-head a.tb .version.preview-chip {
	margin-left: 0;
	font-weight: 400;
	line-height: 1.6;
}

/* Which ink a chip is drawn in. Set on the element, so the variant decides and
   no amount of context around it can overrule the claim it is making.

   The shape is the shape and only the colour is the claim: the canary wears the
   same chip in the warning colour -- the yellow the Lune `experimental` flag
   already uses, so the stylesheet means one thing by it. */
.version.preview-chip {
	--chip-ink: var(--accent);
}

/* The same claim as a colour, on the logo every window wears: see
   `MarkedLogo`. Plain for an installed build, blue for the web app, yellow for
   the canary. The class is doubled so no context rule can outrank it -- the
   projects panel had one painting its mark the accent, which turned the
   canary's mark blue there and nowhere else. */
.logo-mark.logo-mark.mark-preview {
	color: var(--accent);
}

.logo-mark.logo-mark.mark-canary {
	color: var(--warning);
}

.version.preview-chip.canary {
	--chip-ink: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 55%, transparent);
}

/* ------------------------------------------------------- the canary banner */

/* Across the top of every canary window, above the toolbar.
 *
 * The chip says which build you are on; this says what that means for you. On a
 * site anyone can reach, the reader who most needs telling is the one who did
 * not choose to be there -- which is also why it leads somewhere. A warning
 * with no way out is a warning that gets dismissed rather than acted on. */
.canary-banner {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 6px 14px;
	font-size: 12px;
	line-height: 1.5;
	color: var(--fg);
	background: color-mix(in srgb, var(--warning) 14%, var(--bg-panel));
	border-bottom: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
	/* It is chrome, not content: a long sentence must not push the way out off
	   the end of a narrow window. */
	flex-wrap: wrap;
}

.canary-banner-mark {
	text-transform: uppercase;
	letter-spacing: 0.06em;
	font-size: 10px;
	font-weight: 600;
	color: var(--warning);
	border: 1px solid color-mix(in srgb, var(--warning) 55%, transparent);
	border-radius: 3px;
	padding: 1px 6px;
	white-space: nowrap;
}

.canary-banner-text {
	flex: 1;
	min-width: 12rem;
}

.canary-banner-out {
	color: var(--accent);
	white-space: nowrap;
}

/* The copy at the site's old address: news rather than a warning, so the
   accent rather than the warning colour. */
.canary-banner.canary-banner-backup {
	background: color-mix(in srgb, var(--accent) 12%, var(--bg-panel));
	border-bottom-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.canary-banner-backup .canary-banner-mark {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

.docs-page-head .logo .version {
	font-weight: 400;
	font-size: 10px;
	letter-spacing: 0;
	color: var(--fg-faint);
	font-variant-numeric: tabular-nums;
}

.docs-page-head .sub { color: var(--fg-faint); font-size: 12px; }

/* Pushes the controls to the right edge. The editor's own docs window does the
   same thing with an inline style on a spacer span; the published site has no
   React to write one, so it gets a class. */
.docs-page-head .grow { flex: 1; }

.docs-page-head .warn {
	font-size: 11px;
	color: var(--warning);
	border: 1px solid var(--warning);
	border-radius: 3px;
	padding: 0 6px;
}

/* A link in the header is drawn as the button it acts as — Docs, Open Editor —
   so a row of them reads as one set of controls rather than buttons beside
   underlined text. The introduction panel's footer is the same row of controls
   in a different frame, so it is named here rather than given a second set of
   rules that would have to be kept level with these. */
.docs-page-head a.tb,
.intro-foot a.tb {
	text-decoration: none;
	color: inherit;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 4px 9px;
}

/* One height for every control in the header, links and buttons alike. A link
   given its own font size came out two pixels shorter than Settings beside it,
   and the row stopped lining up. */
.docs-page-head a.tb,
.docs-page-head button.tb,
.intro-foot a.tb,
.intro-foot button.tb {
	height: 29px;
	box-sizing: border-box;
}

.docs-page-head a.tb:hover,
.intro-foot a.tb:hover {
	background: var(--bg-hover);
}

/* An icon is a block, so an icon and a word in one button stack by default. */
.docs-page-head button.tb,
.docs-page-head a.tb,
.intro-foot button.tb,
.intro-foot a.tb {
	display: inline-flex;
	align-items: center;
	gap: 5px;
}

.docs-page-head a.tb.icon-only {
	justify-content: center;
	padding: 4px;
	width: 29px;
	color: var(--fg-muted);
}

.docs-page .docs-body {
	min-height: 0;
	grid-template-columns: 260px 1fr;
}

/* Full window, so the reading column can be centred rather than left-hugging. */
.docs-page .docs-content {
	max-width: none;
	padding: 26px 40px 80px;
}

.docs-page .docs-content > * {
	max-width: 78ch;
}

/* A capsule getter redeclares border and box-shadow further down this file than
   .node.selected, so it was quietly winning and the selection ring vanished on
   every Get Variable and Get Function node. Restated at the right specificity. */
.node.capsule.selected {
	border-color: var(--select);
	box-shadow: 0 0 0 2px color-mix(in srgb, var(--select) 55%, transparent), var(--node-shadow);
}

.node.capsule.selected.anchor,
.node.reroute.selected.anchor {
	box-shadow: 0 0 0 4px var(--select), var(--node-shadow);
}

/* ----------------------------------------------------- docs, second pass
 *
 * Closer to Roblox's Creator Hub and other engine documentation: three columns —
 * nav, a capped reading measure, and an outline of the page — with code blocks
 * that carry their language and a copy button rather than being bare <pre>.
 */

.docs-page .docs-body {
	grid-template-columns: 268px minmax(0, 1fr) 210px;
}

/* Below that width the outline is the first thing to go: it is a convenience,
   and the reading column is not. */
@media (max-width: 1100px) {
	.docs-page .docs-body { grid-template-columns: 268px minmax(0, 1fr); }
	/* Scoped, so it outranks `.docs-toc` further down. Unscoped, the later
	   rule won: the outline stayed, wrapped into a second grid row under the
	   nav, and halved the height of everything on a tablet or a phone. */
	.docs-page .docs-toc { display: none; }
}

.docs-group + .docs-group { margin-top: 14px; }

.docs-group-head {
	font-size: 10px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.09em;
	color: var(--fg-faint);
	padding: 6px 6px 4px;
	border-bottom: 1px solid var(--border);
	margin-bottom: 4px;
}

/* --------------------------------------------------------------- outline */

.docs-toc {
	border-left: 1px solid var(--border);
	padding: 26px 14px 40px;
	overflow: auto;
	display: flex;
	flex-direction: column;
	gap: 2px;
	align-content: start;
}

.docs-toc-head {
	font-size: 10px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.09em;
	color: var(--fg-faint);
	margin-bottom: 6px;
}

.docs-toc-link {
	color: var(--fg-muted);
	text-decoration: none;
	font-size: 11.5px;
	line-height: 1.4;
	padding: 3px 6px;
	border-left: 2px solid transparent;
	border-radius: 0 3px 3px 0;
}

.docs-toc-link:hover {
	color: var(--fg);
	border-left-color: var(--accent);
	background: var(--bg-hover);
}

/* ------------------------------------------------------------ code blocks */

/* ------------------------------------------------- node previews in docs */

/* A node picture sits on the canvas colour rather than the page colour, so it
   reads as "this is what you will see over there" instead of as an illustration
   drawn for the page. The grid is the canvas's own fine grid at rest, which is
   what puts the node in a place rather than on a background. */
.docs-preview {
	margin: 0 0 18px;
	padding: 18px 16px;
	border: 1px solid var(--border);
	border-radius: 6px;
	background: var(--bg-canvas);
	background-image:
		linear-gradient(var(--grid-fine) 1px, transparent 1px),
		linear-gradient(90deg, var(--grid-fine) 1px, transparent 1px);
	background-size: 24px 24px;
}

/* Wraps, so two nodes sit side by side on a wide page and stack on a narrow
   one. Aligned to the top: comparing two nodes means comparing their headers,
   and centring a tall one against a short one puts those on different lines. */
.docs-preview .row {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	gap: 20px;
}

.docs-preview .node-preview-frame {
	filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.18));
}

.docs-preview svg.node-preview {
	display: block;
	/* The canvas font, so a title in a preview is set the way the title on the
	   node is. `font-family: inherit` inside the SVG picks this up. */
	font-family: inherit;
}

.docs-preview figcaption {
	margin-top: 14px;
	font-size: 12px;
	line-height: 1.5;
	color: var(--fg-muted);
}

.docs-code {
	border: 1px solid var(--border);
	border-radius: 6px;
	overflow: hidden;
	margin: 0 0 16px;
	background: var(--bg-canvas);
}

.docs-code-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 4px 8px 4px 12px;
	background: var(--bg-panel);
	border-bottom: 1px solid var(--border);
}

.docs-code-head .lang {
	font-size: 10px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
}

.docs-code-head .copy {
	margin-left: auto;
	background: none;
	border: 1px solid transparent;
	border-radius: 4px;
	padding: 2px 8px;
	font-size: 11px;
	color: var(--fg-muted);
	cursor: pointer;
}

.docs-code-head .copy:hover {
	background: var(--bg-hover);
	border-color: var(--border);
	color: var(--fg);
}

.docs-code pre {
	margin: 0;
	border: 0;
	border-radius: 0;
	background: none;
	padding: 12px 14px;
	/* Selectable and copyable, never editable: nothing here is wired to
	   anything, so an edit would look like it did something and would not. */
	user-select: text;
}

/* --------------------------------------------------- content, restyled */

.docs-page .docs-content {
	padding: 30px 44px 90px;
}

.docs-page .docs-content h1 {
	font-size: 30px;
	letter-spacing: -0.015em;
	margin-bottom: 6px;
}

/* The rule belongs to the header block, which spans the article; the summary
   inside it keeps a prose measure. Putting the rule on the summary made it stop
   wherever the text did, which looked like a broken header rather than a
   deliberate one. */
.docs-page .docs-title {
	border-bottom: 1px solid var(--border);
	padding-bottom: 18px;
	margin-bottom: 30px;
}

.docs-page .docs-content .summary {
	font-size: 15px;
	line-height: 1.55;
	max-width: none;
	margin: 0;
	padding: 0;
	border: 0;
	color: var(--fg-muted);
}

.docs-page .docs-content h2 {
	font-size: 19px;
	letter-spacing: -0.01em;
	margin: 36px 0 10px;
	padding-top: 0;
	border-top: 0;
	scroll-margin-top: 20px;
}

.docs-page .docs-content h3 {
	font-size: 14px;
	margin: 24px 0 8px;
	color: var(--fg);
}

.docs-page .docs-content p,
.docs-page .docs-content li { font-size: 13.5px; line-height: 1.68; }

/* Creator Hub rules its tables top and bottom and leaves the inner lines
   light, which is what keeps a long mapping table readable. */
.docs-page .docs-content table {
	font-size: 13px;
	border-top: 1px solid var(--border-strong);
	border-bottom: 1px solid var(--border-strong);
}

.docs-page .docs-content th {
	padding: 9px 14px 9px 0;
	border-bottom: 1px solid var(--border-strong);
}

.docs-page .docs-content td { padding: 10px 14px 10px 0; }
.docs-page .docs-content tbody tr:last-child td { border-bottom: 0; }

.docs-page .docs-note {
	border-radius: 5px;
	padding: 12px 16px;
	font-size: 13.5px;
	margin-bottom: 16px;
}

/* Two, matching the generated files themselves — the default eight turns a
   nested loop body into a horizontal scroll. */
.docs-code pre {
	tab-size: 2;
	-moz-tab-size: 2;
}

/* When a page has no outline the column still holds its place, so the reading
   measure does not jump between pages. It just has nothing in it. */
.docs-toc:empty { border-left-color: transparent; }

/* ------------------------------------------------- docs: code and pin lists */

/* Luau, wherever it is shown as text.
 *
 * `highlightLuau` emits these classes from anywhere; what makes a block
 * coloured is being inside one of the containers listed here. That caught the
 * selection preview out on the day it was written -- the highlighter ran, the
 * spans carried the right classes, and every one of them inherited body text,
 * because the palette was scoped to the docs and nothing said so.
 *
 * So the container list is the thing to extend when a fourth place shows Luau,
 * and it is `:is()` rather than eight comma-separated pairs so that adding one
 * is a single edit rather than eight.
 *
 * The variables are the ones the Custom Code editor's HighlightStyle uses, so a
 * snippet in the docs, the same code in the editor and the same code in the
 * preview are one opinion about Luau rather than four. */
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-keyword { color: var(--code-keyword); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-string { color: var(--code-string); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-number { color: var(--code-number); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-comment { color: var(--code-comment); font-style: italic; }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-operator { color: var(--code-operator); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-global { color: var(--code-global); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-function { color: var(--code-function); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-type { color: var(--code-type); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-property { color: var(--code-property); }
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-bracket,
:is(.docs-code, .preview-code, .landing-code, .luau-hover code, .luau-signature code, .code-view) .tok-punctuation { color: var(--fg-muted); }

/* Pin lists, in the shape Roblox's own reference uses for properties: one
   bordered row per entry, the signature scannable down the left, detail
   underneath rather than in a column that is empty on most rows. */
.docs-pins {
	list-style: none;
	margin: 0 0 18px;
	padding: 0;
	border: 1px solid var(--border);
	border-radius: 6px;
	overflow: hidden;
}

.docs-pins li {
	padding: 10px 14px;
	margin: 0;
	border-bottom: 1px solid var(--border);
}

.docs-pins li:last-child { border-bottom: 0; }
.docs-pins li:nth-child(odd) { background: color-mix(in srgb, var(--fg) 2.5%, transparent); }

.docs-pins .sig {
	display: flex;
	align-items: baseline;
	gap: 7px;
	flex-wrap: wrap;
	font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
	font-size: 12.5px;
}

.docs-pins .sig .docs-swatch { align-self: center; flex: none; }
.docs-pins .name { color: var(--fg); font-weight: 600; }
.docs-pins .sep { color: var(--fg-faint); }

/* Underlined the way Creator Hub underlines a linked type. Nothing to link to
   yet, so it is not an anchor — the underline is the affordance the reader
   already reads as "this is the type". */
.docs-pins .type {
	color: var(--code-global);
	text-decoration: underline;
	text-decoration-color: var(--border-strong);
	text-underline-offset: 3px;
}

.docs-pins .def { color: var(--fg-faint); }
.docs-pins .def code { border: 0; background: none; padding: 0; color: var(--fg-muted); }

.docs-pins .badge {
	font-family: "Segoe UI", system-ui, sans-serif;
	font-size: 9.5px;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
	background: var(--bg-hover);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 1px 5px;
}

.docs-pins .badge.warn { color: var(--warning); border-color: var(--warning); }

.docs-pins .detail {
	margin-top: 5px;
	font-size: 12.5px;
	line-height: 1.55;
	color: var(--fg-muted);
}

/* ------------------------------------------------ docs: page measure
 *
 * Engine documentation, Roblox's included, centres the article in whatever
 * room the window gives it rather than hugging the nav, and lets it grow well
 * past a prose measure — those pages are mostly tables and signatures, and so are
 * ours. The nav and the outline stay pinned to the edges; only the middle
 * column centres.
 */
.docs-page .docs-content {
	display: flex;
	justify-content: center;
	/* So a figure can measure the room the page has, not only its column. */
	container-type: inline-size;
}

/* A picture at more than 100% grows past the article column, centred on it, up
   to the width of the pane it scrolls in. `--preview-scale` is set on the
   figure by the Docs window; the static site never sets it. */
.docs-preview.breakout {
	--frame: min(calc(100% * var(--preview-scale)), calc(100cqw - 48px));
	box-sizing: border-box;
	width: var(--frame);
	margin-left: calc((100% - var(--frame)) / 2);
}

/* A node picture never outgrows its frame: at a size the frame cannot hold, it
   shrinks to fit rather than spilling past the border. A graph's own SVG is
   left alone — the pan-and-zoom viewer positions that one. */
.docs-preview .node-preview-frame {
	min-width: 0;
	max-width: 100%;
}

.docs-preview svg.node-preview:not(.graph-preview) {
	max-width: 100%;
	height: auto;
}

.docs-page .docs-content > * {
	max-width: none;
}

/* One wrapper holds the measure, so every block inside shares an edge and the
   pin lists and tables line up with the prose above them. */
/* The last line of a page has to be able to reach the middle of the screen.
   Without this, scrolling as far as it goes leaves the end of a page at the very
   bottom of the window -- which is where a taskbar is, and the worst place to
   read the paragraph you scrolled down for.

   An empty box rather than bottom padding, on either the article or the scroll
   container. Both were tried and measured: 510px of padding between them bought
   a single pixel of anything to scroll past. A box with a height cannot be
   argued with. */
.docs-tail {
	height: min(45vh, 420px);
	flex: none;
}

/* What a confirmation is about, listed under its message. Capped, so a graph
   with forty of something scrolls rather than pushing the buttons off screen. */
.dialog-list {
	margin: 0 0 14px;
	padding: 6px 10px 6px 26px;
	max-height: 180px;
	overflow: auto;
	border: 1px solid var(--border);
	border-radius: 5px;
	background: var(--bg-input);
	font-size: 12.5px;
	line-height: 1.6;
}

/* What the open graph compiles for, beside the button that compiles it.
   Lune takes the warning colour, because it is experimental and it is where
   Roblox nodes fail. */
.docbar select.doc-target.lune {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 60%, transparent);
}

/* --------------------------------------------------------- editing a page */

/* The pencil sits with the title, where a page's own controls are looked for. */
.docs-title h1 .docs-edit {
	margin-left: 10px;
	vertical-align: middle;
}

.page-editor {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

/* Stays in reach while you work down a long page. */
.page-editor-bar {
	position: sticky;
	top: 0;
	z-index: 2;
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	padding: 8px 10px;
	margin-bottom: 8px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 8px;
}

.page-editor .hint,
.page-editor-said {
	font-size: 11px;
	line-height: 1.6;
	color: var(--fg-faint);
	margin: 0;
}

.page-editor-said {
	color: var(--ok);
}

/* Every block keeps its place; only the one being changed becomes a field. */
.page-block {
	position: relative;
	border: 1px dashed transparent;
	border-radius: 6px;
	padding: 2px 8px;
}

.page-block:hover {
	border-color: var(--border);
}

.page-block-tools {
	position: absolute;
	top: 2px;
	right: 4px;
	display: none;
	gap: 4px;
	z-index: 1;
}

.page-block:hover .page-block-tools {
	display: flex;
}

.page-block-tools button.tb {
	padding: 2px 6px;
	line-height: 1.2;
}

.page-block-add {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	padding: 6px 0;
}

.page-block-add.end {
	border-top: 1px solid var(--border);
	margin-top: 10px;
}

.page-block-shown {
	cursor: text;
}

.block-stack {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 6px 0;
}

.block-row {
	display: flex;
	gap: 6px;
	padding: 6px 0;
}

.block-row > input,
.block-row > select {
	flex: 1;
	min-width: 0;
}

.block-field {
	width: 100%;
	padding: 8px 10px;
	background: var(--bg-input);
	color: var(--fg);
	border: 1px solid var(--border);
	border-radius: 6px;
	font: inherit;
	line-height: 1.6;
	resize: vertical;
}

.block-field.mono {
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 12px;
}

.block-matches,
.block-chips,
.block-preview-row {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	align-items: center;
}

.page-editor-source {
	max-height: 320px;
	overflow: auto;
	padding: 10px 12px;
	background: var(--bg-canvas);
	border: 1px solid var(--border);
	border-radius: 6px;
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 11px;
	line-height: 1.5;
	white-space: pre;
}

/* ------------------------------------------------------------ node designer */

/* A form over a node definition, with the node drawn beside it. Its own page,
   like the docs: it is a thing you sit down to do rather than a panel you keep
   open while wiring. */
.designer {
	display: flex;
	flex-direction: column;
	height: 100vh;
	background: var(--bg-app);
	color: var(--fg);
}

.designer-body {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 360px;
	gap: 18px;
	padding: 18px;
	overflow: auto;
	align-items: start;
}

.designer-form,
.designer-side {
	display: flex;
	flex-direction: column;
	gap: 10px;
	min-width: 0;
}

.designer-side {
	position: sticky;
	top: 0;
}

.designer h2 {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin: 8px 0 0;
}

.designer .field {
	display: grid;
	grid-template-columns: 96px 1fr;
	align-items: center;
	gap: 8px;
}

.designer .field > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.designer .field textarea {
	font-family: "Cascadia Mono", Consolas, monospace;
	resize: vertical;
}

.designer-pins {
	display: flex;
	flex-direction: column;
	gap: 8px;
	border-top: 1px solid var(--border);
	padding-top: 10px;
}

.designer-pin {
	display: flex;
	flex-direction: column;
	gap: 5px;
	border-left: 2px solid var(--border);
	padding-left: 8px;
}

.designer-row {
	display: flex;
	gap: 6px;
}

.designer-row > * {
	min-width: 0;
	flex: 1;
}

.designer-row > button.tb {
	flex: none;
}

/* The node as the canvas will draw it, from the documentation's own generator. */
.designer-preview {
	display: flex;
	justify-content: center;
	padding: 14px;
	background: var(--bg-canvas);
	border: 1px solid var(--border);
	border-radius: 8px;
}

.designer-problems {
	margin: 0;
	padding-left: 18px;
	color: var(--danger);
	font-size: 12px;
	line-height: 1.6;
}

.designer-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
}

.designer-said {
	margin: 0;
	color: var(--ok);
	font-size: 12px;
}

.designer-failed {
	margin: 0;
	color: var(--danger);
	font-size: 12px;
}

.designer .hint {
	margin: 0;
	font-size: 11px;
	line-height: 1.6;
	color: var(--fg-faint);
}

/* ------------------------------------------------------------ pack browser */

/* A toast in the corner, not a bar under the header: a bar took a row from the
   pack view and the node editor every time something was saved. */
.designer-notice {
	position: fixed;
	right: 18px;
	bottom: 18px;
	z-index: 80;
	display: flex;
	align-items: center;
	gap: 10px;
	max-width: min(440px, calc(100vw - 36px));
	padding: 8px 8px 8px 14px;
	border: 1px solid var(--border);
	border-radius: 8px;
	font-size: 12px;
	background: var(--bg-panel);
	box-shadow: 0 8px 28px rgb(0 0 0 / 40%);
	animation: designer-toast-in 140ms ease-out;
}

@keyframes designer-toast-in {
	from {
		opacity: 0;
		transform: translateY(8px);
	}
}

.designer-notice.ok {
	border-color: color-mix(in srgb, var(--ok) 50%, transparent);
}

.designer-notice.failed {
	border-color: color-mix(in srgb, var(--danger) 60%, transparent);
	color: var(--danger);
}

.designer-notice span {
	flex: 1;
}

.pack-browser {
	padding: 16px 24px 32px;
	overflow: auto;
	display: flex;
	flex-direction: column;
	gap: 10px;
	min-height: 0;
}

.pack-toolbar {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
}

.pack-toolbar h1,
.pack-nodes h1 {
	font-size: 18px;
	margin: 0;
}

.pack-toolbar .spacer,
.pack-import .spacer {
	flex: 1;
}

.pack-naming {
	display: flex;
	gap: 6px;
}

.pack-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
	gap: 10px;
}

.pack-grid.list {
	grid-template-columns: 1fr;
	gap: 6px;
}

.pack-card {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 10px 12px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 8px;
	cursor: pointer;
	min-width: 0;
}

.pack-card:hover,
.pack-card:focus-visible {
	border-color: var(--accent);
	outline: none;
}

.pack-grid.list .pack-card {
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px 14px;
}

.pack-card-head {
	display: flex;
	align-items: center;
	gap: 8px;
}

.pack-card h3 {
	margin: 0;
	font-size: 14px;
}

.pack-path {
	font-size: 11px;
	color: var(--fg-faint);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.pack-badges {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
	font-size: 11px;
}

.designer .badge {
	padding: 1px 7px;
	font-size: 11px;
	border: 1px solid var(--border);
	border-radius: 999px;
	color: var(--fg-muted);
	white-space: nowrap;
}

.designer .badge.warn {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 60%, transparent);
}

.designer .count {
	color: var(--fg-faint);
}

/* The button that destroys something, when it is not already in a menu. */
button.tb.danger {
	color: var(--danger);
	border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

.pack-actions,
.pack-confirm {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px;
}

.pack-grid:not(.list) .pack-actions,
.pack-grid:not(.list) .pack-confirm {
	margin-top: auto;
	padding-top: 4px;
}

.pack-grid.list .pack-actions,
.pack-grid.list .pack-confirm {
	margin-left: auto;
}

.pack-actions .tb {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: 12px;
}

.pack-confirm p {
	margin: 0 6px 0 0;
	font-size: 12px;
	flex-basis: 100%;
}

.pack-import {
	width: min(720px, 92vw);
	max-height: 80vh;
	overflow: auto;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 10px;
	padding: 12px 14px;
	box-shadow: 0 12px 40px rgb(0 0 0 / 40%);
}

.pack-import-head {
	display: flex;
	align-items: center;
	gap: 8px;
}

.pack-import ul {
	list-style: none;
	margin: 10px 0 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.pack-import li {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 6px 8px;
	border: 1px solid var(--border);
	border-radius: 6px;
	font-size: 12px;
}

.pack-import .name {
	font-weight: 600;
}

/* ---------------------------------------------------------------- pack view */

.pack-view {
	flex: 1;
	display: grid;
	grid-template-columns: 280px minmax(0, 1fr);
	/* One row, held to the window, so the editor's logic panel stays on screen. */
	grid-template-rows: minmax(0, 1fr);
	min-height: 0;
}

.pack-nodes {
	display: flex;
	flex-direction: column;
	gap: 8px;
	padding: 14px;
	border-right: 1px solid var(--border);
	background: var(--bg-panel);
	overflow: auto;
}

.pack-back {
	align-self: flex-start;
	display: inline-flex;
	align-items: center;
	gap: 4px;
}

.pack-nodes ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.pack-node {
	display: grid;
	grid-template-columns: 8px 1fr;
	grid-template-rows: auto auto;
	column-gap: 8px;
	width: 100%;
	text-align: left;
	padding: 5px 8px;
	background: none;
	border: 1px solid transparent;
	border-radius: 6px;
	color: var(--fg);
	cursor: pointer;
}

.pack-node:hover {
	background: var(--bg-hover);
}

.pack-node.on {
	background: var(--bg-active);
	border-color: var(--border);
}

.pack-node .swatch {
	grid-row: 1 / 3;
	width: 8px;
	border-radius: 2px;
}

.pack-node .id {
	font-size: 11px;
	color: var(--fg-faint);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.designer-stage {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 12px;
	padding: 24px;
	background: var(--bg-canvas);
	overflow: auto;
	position: relative;
}

/* ------------------------------------------------------------- node editor */

.node-editor {
	display: flex;
	flex-direction: column;
	min-width: 0;
	min-height: 0;
}

/* The canvas the node sits on, with the graph's grid so it reads as one. */
.node-editor-stage {
	position: relative;
	flex: 1;
	min-height: 0;
	overflow: hidden;
	background-color: var(--bg-canvas);
	background-image:
		linear-gradient(to right, var(--grid-fine) 1px, transparent 1px),
		linear-gradient(to bottom, var(--grid-fine) 1px, transparent 1px);
	background-size: 38px 38px;
}

.node-editor-world {
	position: absolute;
	top: 0;
	left: 0;
	transform-origin: 0 0;
}

.node-plate {
	position: absolute;
	box-sizing: border-box;
	background: color-mix(in srgb, var(--bg-panel) 70%, transparent);
	border: 1px dashed var(--border-strong);
	border-radius: 14px;
	pointer-events: none;
}

/* The header's own title is hidden, and this input is typed into in its place. */
.node-editor-world .node .head .title {
	visibility: hidden;
}

.node-title-edit {
	position: absolute;
	top: 0;
	left: 0;
	/* Above the node, which sits at LAYER.node (50), or the header covers it. */
	z-index: 60;
	box-sizing: border-box;
	padding: 0 10px;
	background: transparent;
	border: 0;
	color: #fff;
	font: inherit;
	font-weight: 600;
	font-size: 12.5px;
	outline: none;
}

.node-title-edit::placeholder {
	color: rgb(255 255 255 / 55%);
}

/* In the designer a pin is something you edit, so what is drawn for it — dot,
   label, value — is a target: a tint on hover, and a stronger one with an
   outline while it is the pin the popover is about. Sized to that content with
   a little padding, which the negative margin gives back, so nothing moves.
   Only the designer draws `pin-target`. */
.node .pin-target {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	/* No max-width. An output's side is one pin wide (22px), and capping the
	   target to it with 12px of padding left the pin 4px, so it overflowed the
	   node's edge. `min-width: 0` is enough for a long label to shrink. */
	min-width: 0;
	padding: 2px 6px;
	margin: -2px -6px;
	border-radius: 5px;
	cursor: pointer;
	transition: background-color 80ms ease;
}

.node .pin-target:hover {
	background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.node .pin-target.pin-selected,
.node .pin-target.pin-selected:hover {
	background: color-mix(in srgb, var(--accent) 30%, transparent);
	box-shadow: inset 0 0 0 1px var(--accent);
}

.node .pin-target.pin-selected .pin-label {
	color: var(--fg);
}

/* A checkbox value sits beside its label, as it does in a row, rather than
   pushed to the edge of a side the target no longer fills. */
.node .pin-target > input[type="checkbox"] {
	margin-left: 4px;
}

.node .pin.highlighted .dot {
	box-shadow: 0 0 0 1px var(--pin-well), 0 0 0 3px var(--accent);
}

.pin-chip {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	padding: 3px 8px;
	font-size: 12px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--bg-input);
	cursor: grab;
	user-select: none;
}

.chip-dot {
	width: 9px;
	height: 9px;
	border: 2px solid currentColor;
	border-radius: 50%;
	box-sizing: border-box;
}

.chip-dot.exec {
	border-radius: 1px;
	transform: rotate(45deg) scale(0.85);
}

.tool-group .tool-label {
	font-size: 11px;
	color: var(--fg-faint);
}

.tool-group .badge {
	padding: 1px 7px;
	font-size: 11px;
	border: 1px solid var(--border);
	border-radius: 999px;
	color: var(--fg-muted);
}

.tool-group .badge.warn {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 60%, transparent);
}

.node-details,
.node-problems {
	position: absolute;
	bottom: 10px;
	z-index: 5;
	padding: 8px 10px;
	background: color-mix(in srgb, var(--bg-panel) 94%, transparent);
	border: 1px solid var(--border);
	border-radius: 8px;
	box-shadow: 0 4px 14px rgb(0 0 0 / 28%);
	font-size: 12px;
}

/* Expands out under the floating tools, from their Details button. */
.node-details {
	left: 10px;
	top: 56px;
	bottom: auto;
	display: grid;
	gap: 6px;
	width: min(380px, 45%);
}

.node-details-head {
	display: flex;
	align-items: center;
	gap: 6px;
	margin-bottom: 2px;
}

.node-details textarea.tb {
	resize: vertical;
	min-height: 54px;
	font: inherit;
	line-height: 1.45;
}

/* A choice fills its row the way the fields above and below it do, its options
   sharing the width, rather than stopping at the length of its labels. */
/* A segmented control in a stacked field fills the field, the way the select
   and the text input beside it do. Left to `inline-flex` it sits in the corner
   of a full-width box with the rest of the row empty, which reads as a control
   that failed to load rather than as a choice of two. */
.node-details .segmented,
.inspector .field .segmented,
.variable-body .field .segmented {
	display: flex;
	width: 100%;
}

/* Each option starts from its own label and they share what is left, so the
   row is filled without cutting a long label into two lines, which equal thirds
   did to "Roblox and Lune". */
.node-details .segmented button,
.inspector .field .segmented button,
.variable-body .field .segmented button {
	flex: 1 1 auto;
	text-align: center;
	white-space: nowrap;
}

.node-details label.summary {
	align-items: start;
}

.node-details label.summary > span {
	padding-top: 5px;
}

.node-details label,
.pin-popover label {
	display: grid;
	grid-template-columns: 70px minmax(0, 1fr);
	align-items: center;
	gap: 8px;
}

.node-details label > span,
.pin-popover label > span {
	font-size: 11px;
	color: var(--fg-faint);
}

.node-problems {
	right: 10px;
	max-width: min(420px, 45%);
	color: var(--ok);
}

.node-problems.bad {
	color: var(--danger);
}

.node-problems ul {
	margin: 0;
	padding-left: 16px;
	line-height: 1.6;
}

.pin-popover {
	position: absolute;
	z-index: 6;
	width: 270px;
	display: flex;
	flex-direction: column;
	gap: 7px;
	padding: 8px 10px 10px;
	background: var(--bg-panel);
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	box-shadow: 0 8px 24px rgb(0 0 0 / 38%);
	font-size: 12px;
}

.pin-popover-head,
.pin-popover-actions {
	display: flex;
	align-items: center;
	gap: 6px;
}

.pin-popover label.check {
	grid-template-columns: auto 1fr;
}

.pin-popover .hint code {
	font-size: 11px;
}

.logic-splitter {
	height: 5px;
	flex: none;
	cursor: row-resize;
	background: var(--border);
}

.logic-splitter:hover {
	background: var(--accent);
}

.node-logic {
	flex: none;
	display: flex;
	flex-direction: column;
	min-height: 0;
	background: var(--bg-panel);
}

.logic-head {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px;
	padding: 6px 12px;
	border-bottom: 1px solid var(--border);
}

.logic-tab {
	padding: 2px 10px;
	font-size: 12px;
	border-radius: 999px;
	border: 1px solid var(--border);
}

.logic-tab.on {
	border-color: var(--accent);
	color: var(--fg);
}

.luau-field {
	flex: 1;
	min-height: 0;
	overflow: hidden;
}

/* Logic built from nodes: the graph, and beside it the Luau it compiles to. */
.logic-split {
	flex: 1;
	min-height: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(220px, 30%);
}

.logic-canvas {
	position: relative;
	min-width: 0;
	min-height: 0;
}

.logic-canvas > .canvas {
	position: absolute;
	inset: 0;
}

/* A logic graph is not a script, so the script's name and class say nothing. */
.logic-canvas .watermark {
	display: none;
}

.logic-compiled {
	margin: 0;
	padding: 8px 12px;
	overflow: auto;
	border-left: 1px solid var(--border);
	background: var(--bg-canvas);
	font-family: "Cascadia Mono", Consolas, monospace;
	font-size: 12px;
	line-height: 1.5;
	white-space: pre;
	color: var(--fg-muted);
}

.node-problems ul.warnings {
	color: var(--warning);
	margin-top: 4px;
}

.pack-requires {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 5px;
}

.pack-requires .tool-label {
	font-size: 11px;
	color: var(--fg-faint);
	margin-right: 2px;
}

.pack-requires select.tb {
	padding: 1px 6px;
	font-size: 11px;
}

.badge .badge-remove {
	margin-left: 4px;
	padding: 0;
	background: none;
	border: 0;
	color: inherit;
	cursor: pointer;
	font-size: 11px;
}

.logic-empty {
	padding: 12px;
}

/* ---------------------------------------------------------------- docs tabs */

/* One switch over several routes through the same subject — three ways to
   define a custom node. The static site switches with radios and no script;
   the panel renders the chosen one. Both use these rules. */
.docs-tabs {
	margin: 16px 0;
	/* A containing block for the radios below, and it is load-bearing.
	 *
	 * Without it their nearest positioned ancestor is the *body*, so three
	 * absolutely-positioned inputs sitting at their static position deep inside
	 * a separately-scrolling column were contributing to the body's scrollable
	 * overflow -- on the Toolbars page they landed at y=2845 and y=3441 and
	 * stretched the document to 3454px when the content box is one viewport
	 * tall and scrolls internally.
	 *
	 * Which was survivable until somebody clicked a tab. A `<label for>` focuses
	 * its input, the browser scrolls a focused element into view, and the window
	 * scrolled to a radio two thousand pixels below anything that renders:
	 * **a blank page, with nothing in the console**. Reported twice and
	 * invisible to a scripted click, because `.click()` does not focus. */
	position: relative;
}

/* Hidden, and pinned to the top of their own tabs block so that focusing one
   scrolls to the switch the reader just used rather than into empty space.
   1px rather than `auto`: an input has an intrinsic width, and at its natural
   size it can still widen a narrow page. */
.docs-tabs > input {
	position: absolute;
	top: 0;
	left: 0;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

.docs-tabs-label {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
	margin-bottom: 6px;
}

.docs-tab-bar {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	border-bottom: 1px solid var(--border);
	padding-bottom: 8px;
}

.docs-tab-bar label,
.docs-tab-bar button {
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 4px 12px;
	font: inherit;
	font-size: 12px;
	color: var(--fg);
	cursor: pointer;
}

.docs-tab-bar label:hover,
.docs-tab-bar button:hover {
	background: var(--bg-hover);
}

.docs-tab-bar button.on {
	background: var(--accent);
	border-color: var(--accent);
	color: #fff;
}

/* The reader's own screen's tab, marked so the page opening on a tab that is
   not the first one explains itself. */
.docs-tab-here {
	margin-left: 6px;
	font-size: 10.5px;
	opacity: 0.75;
}

.docs-tab-here::before {
	content: "· ";
}

/* Hidden by default so the static site can show one; the panel renders only
   the chosen panel and marks it, so it is never hidden by these rules. */
.docs-tab-panel {
	display: none;
	padding-top: 14px;
}

.docs-tab-panel.on {
	display: block;
}

.docs-tabs > input:nth-of-type(1):checked ~ .docs-tab-panels > .docs-tab-panel:nth-child(1),
.docs-tabs > input:nth-of-type(2):checked ~ .docs-tab-panels > .docs-tab-panel:nth-child(2),
.docs-tabs > input:nth-of-type(3):checked ~ .docs-tab-panels > .docs-tab-panel:nth-child(3),
.docs-tabs > input:nth-of-type(4):checked ~ .docs-tab-panels > .docs-tab-panel:nth-child(4) {
	display: block;
}

.docs-tabs > input:nth-of-type(1):checked ~ .docs-tab-bar > label:nth-child(1),
.docs-tabs > input:nth-of-type(2):checked ~ .docs-tab-bar > label:nth-child(2),
.docs-tabs > input:nth-of-type(3):checked ~ .docs-tab-bar > label:nth-child(3),
.docs-tabs > input:nth-of-type(4):checked ~ .docs-tab-bar > label:nth-child(4) {
	background: var(--accent);
	border-color: var(--accent);
	color: #fff;
}

/* A picture is not prose, and a wide screen has room the measure does not use.
   A graph drawn at 78 characters wide is a graph nobody can read the pins of,
   so on a screen with the space these step outside the column -- centred, and
   never wider than the article's own maximum. */
@media (min-width: 1180px) {
	.docs-page .docs-article.narrow .docs-preview.graph,
	.docs-page .docs-article.narrow .docs-graph-tabs,
	.docs-page .docs-article.narrow .docs-tab-panel .docs-preview.graph {
		width: min(940px, calc(100vw - 420px));
		margin-left: calc((100% - min(940px, calc(100vw - 420px))) / 2);
	}
}

/* Taller too, once it is wider: the frame is a window onto the graph, and a
   940px window 260px tall crops what the width just made visible. */
@media (min-width: 1180px) {
	.docs-page .docs-article.narrow .docs-preview.graph .graph-viewport {
		height: 360px;
	}
}

/* Several graphs in one frame, a tab each — the shape the editor uses for two
   open documents, so an example that is two files reads as two files.
   Radios on the static site, state in the app; both hide the radios. */
.docs-graph-tabs > input {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

.docs-graph-tabs .docs-tab-bar {
	border-bottom: 0;
	padding-bottom: 0;
	gap: 4px;
}

/* Square-topped and joined to the frame below, as a document tab is: a pill
   floating over a picture would read as a filter on it rather than as which
   one of them you are looking at. */
.docs-graph-tabs .docs-tab-bar label,
.docs-graph-tabs .docs-tab-bar button {
	border-radius: 6px 6px 0 0;
	border-bottom-color: transparent;
}

/* Tabs with what the file declares beside them: the panel in its own column,
   level with the graph, and the tab bar over the graph it switches. The
   same columns as a single graph with a panel (`.with-panel` below). */
.docs-graph-tabs.with-panel {
	display: grid;
	grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
	grid-template-areas:
		"label label"
		".     bar"
		"panel graph";
	column-gap: 14px;
	align-items: start;
}

.docs-graph-tabs.with-panel > .docs-tabs-label { grid-area: label; }
.docs-graph-tabs.with-panel > .docs-tab-bar { grid-area: bar; }
.docs-graph-tabs.with-panel > .graph-declares { grid-area: panel; }
.docs-graph-tabs.with-panel > .docs-tab-panels,
.docs-graph-tabs.with-panel > .docs-preview { grid-area: graph; min-width: 0; }

@media (max-width: 860px) {
	.docs-graph-tabs.with-panel {
		grid-template-columns: minmax(0, 1fr);
		grid-template-areas: "label" "panel" "bar" "graph";
	}
	.docs-graph-tabs.with-panel > .graph-declares { margin-bottom: 14px; }
}

.docs-graph-tabs .docs-graph-panel {
	display: none;
	margin-top: 0;
	border-top-left-radius: 0;
}

.docs-graph-tabs.live .docs-graph-panel {
	display: block;
}

.docs-graph-tabs > input:nth-of-type(1):checked ~ .docs-tab-panels > .docs-graph-panel:nth-child(1),
.docs-graph-tabs > input:nth-of-type(2):checked ~ .docs-tab-panels > .docs-graph-panel:nth-child(2),
.docs-graph-tabs > input:nth-of-type(3):checked ~ .docs-tab-panels > .docs-graph-panel:nth-child(3),
.docs-graph-tabs > input:nth-of-type(4):checked ~ .docs-tab-panels > .docs-graph-panel:nth-child(4) {
	display: block;
}

.docs-graph-tabs > input:nth-of-type(1):checked ~ .docs-tab-bar > label:nth-child(1),
.docs-graph-tabs > input:nth-of-type(2):checked ~ .docs-tab-bar > label:nth-child(2),
.docs-graph-tabs > input:nth-of-type(3):checked ~ .docs-tab-bar > label:nth-child(3),
.docs-graph-tabs > input:nth-of-type(4):checked ~ .docs-tab-bar > label:nth-child(4) {
	/* The docs window's selected tab (`button.on`), so the site reads the same. */
	background: var(--accent);
	border-color: var(--accent);
	color: #fff;
}

/* Release history, folded by minor version: one summary line per fold, its
   count and date at the right where a heading keeps its aside. */
.docs-content details.docs-details {
	border-top: 1px solid var(--border);
	padding: 10px 0;
}

.docs-content details.docs-details:last-child {
	border-bottom: 1px solid var(--border);
}

.docs-content details.docs-details > summary {
	display: flex;
	align-items: baseline;
	gap: 10px;
	cursor: pointer;
	list-style: none;
	font-size: 16px;
	font-weight: 600;
}

.docs-content details.docs-details > summary::-webkit-details-marker {
	display: none;
}

.docs-content details.docs-details > summary::before {
	content: "▸";
	font-size: 11px;
	color: var(--fg-faint);
}

.docs-content details.docs-details[open] > summary::before {
	content: "▾";
}

.docs-content details.docs-details > summary .aside,
.docs-content h3 .aside {
	margin-left: auto;
	font-size: 12px;
	font-weight: 400;
	color: var(--fg-faint);
}

/* The current release, right beside its version number. */
.docs-content .badge.latest {
	padding: 1px 7px;
	border: 1px solid color-mix(in srgb, var(--accent) 60%, transparent);
	border-radius: 999px;
	color: var(--accent);
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	white-space: nowrap;
}

.docs-content details.docs-details h3 {
	display: flex;
	align-items: baseline;
	gap: 10px;
	margin-top: 22px;
}

.docs-page .docs-content h4 {
	margin: 14px 0 6px;
	font-size: 11.5px;
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--fg-muted);
}

/* The reader's chosen face for the docs, from Settings → Docs. Headings and
   prose follow it; code keeps its own monospace, which it sets for itself.
   Stacks of fonts an operating system already has — the docs run offline. */
:root[data-docs-font="serif"] .docs-article {
	font-family: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
}

:root[data-docs-font="wide"] .docs-article {
	font-family: Verdana, Tahoma, "DejaVu Sans", sans-serif;
}

:root[data-docs-font="mono"] .docs-article {
	font-family: "Cascadia Mono", Consolas, ui-monospace, monospace;
}

/* A slider and the value it is at, for Settings → Docs → Preview size. */
.settings-range {
	display: flex;
	align-items: center;
	gap: 10px;
}

.settings-range input[type="range"] {
	width: 180px;
	accent-color: var(--accent);
}

.settings-range .value {
	min-width: 4ch;
	color: var(--fg-muted);
	font-variant-numeric: tabular-nums;
}

/* A badge beside a page's title: a pack's page, or how far a page has been
   reviewed. Pending takes the warning colour because it is the one asking the
   reader to take care; Verified takes the colour the notes use for a promise
   being kept. */
.docs-title .badge {
	margin-left: 10px;
	vertical-align: middle;
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--fg-muted);
	border: 1px solid var(--border);
	border-radius: 999px;
	padding: 1px 8px;
	white-space: nowrap;
}

/* The review badge has a line of its own, under the summary. */
.docs-title p.docs-status {
	margin: 10px 0 0;
	line-height: 1;
}

.docs-title .docs-status .badge {
	margin-left: 0;
}

.docs-title .badge.review.pending {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 60%, transparent);
}

/* Which runtime this node needs, beside its name.
 *
 * The same words and the same colours the node menu puts on a row, so a badge
 * there and a badge here are recognisably one thing rather than two vocabularies
 * for one fact. Luau is stated rather than left off, which is the difference
 * from the menu: a list is scanned and an absence reads, but a reference page is
 * arrived at one at a time and a page that says nothing has not answered. */
.docs-title .badge.runtime.luau {
	color: var(--fg-muted);
	border-color: var(--border);
}

.docs-title .badge.runtime.roblox {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.docs-title .badge.runtime.lune {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 50%, transparent);
}

.docs-title .badge.review.reviewed {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 60%, transparent);
}

.docs-title .badge.review.verified {
	color: var(--ok);
	border-color: color-mix(in srgb, var(--ok) 60%, transparent);
}

/* Where to go next, at the foot of the page.
   Back on the left and on on the right, the way a book runs. Each says where
   it goes as well as which way: a reader deciding whether to carry on is
   deciding about the page, not about the direction. */
.docs-neighbours {
	display: flex;
	justify-content: space-between;
	gap: 12px;
	margin: 40px 0 0;
}

.docs-neighbour {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
	max-width: 48%;
	padding: 8px 12px;
	border: 1px solid var(--border);
	border-radius: 6px;
	text-decoration: none;
	color: var(--fg);
}

.docs-neighbour:hover {
	border-color: var(--border-strong);
	background: var(--bg-hover);
}

.docs-neighbour.next {
	margin-left: auto;
	text-align: right;
}

/* An end of the site has nothing on that side, and the other one stays put. */
.docs-neighbour.none {
	border: 0;
	padding: 0;
	pointer-events: none;
}

.docs-neighbour .way {
	font-size: 11px;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--fg-faint);
}

.docs-neighbour .title {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* "Last reviewed", at the foot of the page where GOV.UK puts it. */
.docs-content p.docs-reviewed {
	margin: 44px 0 0;
	padding-top: 12px;
	border-top: 1px solid var(--border);
	font-size: 12.5px;
	color: var(--fg-faint);
}

/* What a verified pass still needs, under the date. */
.docs-content p.docs-verify {
	margin: 4px 0 0;
	font-size: 12.5px;
	line-height: 1.55;
	color: var(--fg-faint);
}

.docs-page .docs-article {
	width: 100%;
	max-width: 940px;
}

/* Prose stays narrower than the tables it sits between: 100 characters of body
   text is tiring, 100 characters of table is fine. */
/* Structural lists — the pin reference — are not prose and want the full
   measure, the same as a table does. */
.docs-page .docs-article > p,
.docs-page .docs-article > ul:not(.docs-pins),
.docs-page .docs-article > ol {
	/* The article's own width is the measure. Capping prose inside it left
	   sentences wrapping at half the column beside tables that ran the full
	   width, which read as unfinished rather than as a reading measure. */
	max-width: none;
}

@media (min-width: 1500px) {
	.docs-page .docs-body { grid-template-columns: 288px minmax(0, 1fr) 240px; }
	.docs-page .docs-article { max-width: 1040px; }
}

/* ------------------------------------------------- docs: the static build
 *
 * The same markup as the in-app panel, with two differences the browser
 * supplies rather than React: the nav sections are <details>, and the links are
 * anchors rather than buttons.
 */
.docs-static .docs-link,
.docs-static .docs-hit,
.docs-static .docs-palette-hit { text-decoration: none; color: inherit; }

.docs-static .docs-section-head {
	list-style: none;
	display: flex;
	align-items: center;
	gap: 6px;
}

.docs-static .docs-section-head::-webkit-details-marker { display: none; }

/* The chevron is an icon component in the app and cannot be here, so the
   disclosure triangle is drawn in CSS and rotated the same way. */
.docs-static .docs-section-head::before {
	content: "";
	width: 0;
	height: 0;
	border-left: 4px solid currentColor;
	border-top: 3.5px solid transparent;
	border-bottom: 3.5px solid transparent;
	opacity: 0.55;
	transition: transform 0.12s ease;
	flex: none;
}

.docs-static details[open] > .docs-section-head::before { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
	.docs-static .docs-section-head::before { transition: none; }
}

.docs-static .docs-page-head .logo { text-decoration: none; color: inherit; }

/* A nav section holding one page is rendered as that page's link. No chevron,
   because there is nothing to unfold, and it takes the selected state a page
   link would. */
.docs-section-head.solo {
	padding-left: 6px;
	text-decoration: none;
}

.docs-static .docs-section-head.solo::before { content: none; }

.docs-section-head.solo.on {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	color: var(--fg);
}

/* ------------------------------------------------ docs: headings and measure */

/* A heading can carry a right-aligned aside — a version's date, a status. The
   heading stays the thing you read; the aside is the thing you check. */
.docs-page .docs-content h2 {
	display: flex;
	align-items: baseline;
	gap: 14px;
}

.docs-page .docs-content h2 .aside {
	margin-left: auto;
	font-size: 12px;
	font-weight: 400;
	color: var(--fg-faint);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* A page that is mostly prose sets its measure once, on the article, so the
   rules, notes and paragraphs all share one right edge. A wide page caps its
   paragraphs individually instead, because its tables want the full width. */
.docs-page .docs-article.narrow { max-width: 78ch; }

.docs-page .docs-article.narrow > p,
.docs-page .docs-article.narrow > ul,
.docs-page .docs-article.narrow > ol { max-width: none; }

/* Release entries are ruled apart, the way Roblox rules its own. The version is
   the heading; the date is the aside. */
.docs-page .docs-content h2 {
	border-top: 1px solid var(--border);
	padding-top: 20px;
	margin-top: 34px;
}

.docs-page .docs-content h2:first-of-type { border-top: 0; padding-top: 0; }

/* ---------------------------------------------------------- compile toast */

/* A project compile, in the bottom-right corner of the graph.
 *
 * Separate from the status panel on purpose: that panel is script analysis and
 * answers "what is wrong with this graph". A compile's progress is neither
 * about the open graph nor about anything being wrong, and a thousand rows of
 * good news in there buries the one diagnostic the panel exists for.
 *
 * It floats rather than taking a row of its own, because it is temporary — the
 * graph underneath is what you were looking at and will be again in a moment. */
/* Something the canvas did or would not do, said once at the foot of the
   graph. Centred, so it never sits on the compile toast in the corner. */
.centre-notice {
	position: absolute;
	left: 50%;
	bottom: 16px;
	transform: translateX(-50%);
	z-index: 6;
	display: flex;
	align-items: center;
	gap: 10px;
	max-width: min(460px, calc(100% - 32px));
	padding: 7px 7px 7px 12px;
	background: var(--bg-panel);
	border: 1px solid color-mix(in srgb, var(--warning) 55%, var(--border));
	border-radius: 6px;
	box-shadow: 0 4px 16px rgb(0 0 0 / 28%);
	font-size: 12px;
}

@keyframes centre-notice-in {
	from { opacity: 0; transform: translate(-50%, 8px); }
}

@media (prefers-reduced-motion: no-preference) {
	.centre-notice { animation: centre-notice-in 140ms ease-out; }
}

.compile-toast {
	position: absolute;
	right: 16px;
	bottom: 16px;
	width: 260px;
	padding: 8px 10px 9px;
	background: var(--bg-panel);
	border: 1px solid var(--border);
	border-radius: 5px;
	box-shadow: 0 4px 16px rgb(0 0 0 / 28%);
	font-size: 11px;
	cursor: pointer;
	z-index: 5;
}

.compile-toast .head {
	display: flex;
	align-items: baseline;
	gap: 8px;
}

.compile-toast .what {
	font-weight: 600;
	color: var(--fg);
}

.compile-toast .count {
	margin-left: auto;
	color: var(--fg-faint);
	font-variant-numeric: tabular-nums;
}

/* The file being written, or what happened once none is. One line, and clipped
   rather than wrapped: a toast that changes height as it counts is a toast
   that draws the eye every time a file finishes. */
.compile-toast .detail {
	margin-top: 2px;
	color: var(--fg-muted);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.compile-toast.has-failures .detail {
	color: var(--danger);
}

/* Determinate, because the number of files is known before the walk starts.
   An indeterminate spinner would throw away the one thing that tells slow
   apart from stuck, which is the whole reason this exists. */
.compile-toast .track {
	margin-top: 7px;
	height: 3px;
	border-radius: 2px;
	background: var(--bg-input);
	overflow: hidden;
}

.compile-toast .track .fill {
	display: block;
	height: 100%;
	background: var(--accent);
	transition: width 0.12s linear;
}

.compile-toast.has-failures .track .fill {
	background: var(--danger);
}

/* ------------------------------------------------------- the compile lock */

/* The graph while it is being compiled, outside hot reload.
 *
 * The border is on the canvas rather than on the overlay because `inset` on an
 * absolutely positioned child sits *inside* the scroll box and would slide
 * under the status panel; drawn here it frames the graph exactly. */
.canvas.locked {
	box-shadow: inset 0 0 0 2px var(--accent);
}

/* Swallows the pointer for the whole canvas, so no handler underneath has to
   check whether it is allowed to run. A second or two, and then it is gone. */
.canvas-lock {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: flex-start;
	justify-content: center;
	cursor: progress;
	/* No dimming. The graph is what you are waiting to get back, and greying it
	   out to say "wait" hides the thing the waiting is about. */
	background: color-mix(in srgb, var(--accent) 5%, transparent);
}

.canvas-lock span {
	/* Below the floating tools along the canvas's top edge, not under them. */
	margin-top: 52px;
	padding: 4px 12px;
	font-size: 11px;
	font-weight: 600;
	color: var(--bg-panel);
	background: var(--accent);
	border-radius: 0 0 4px 4px;
	box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
}

/* A panel that edits the graph, while the graph is refusing edits.
 *
 * The store is what actually refuses them; this is so the panel does not sit
 * there looking like it took one. Inert rather than disabled control by
 * control: there are eighteen of them across the two panels, and the one
 * somebody forgets to add `disabled` to next year is the bug. */
.editing-locked {
	pointer-events: none;
	opacity: 0.5;
}

/* ------------------------------------------- release notes: tags and rows */

/* What kind of release this was, under its version number.
 *
 * Derived from what the release contains — see `releaseTags` — so a tag cannot
 * claim something the entries below it do not show. Breaking is the exception
 * and the only one a release states for itself, so it is the only one that
 * borrows the warning colour. */
.docs-tags {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	margin: -4px 0 12px;
}

.docs-tag {
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.04em;
	padding: 2px 8px;
	border-radius: 3px;
	border: 1px solid color-mix(in srgb, var(--fg-faint) 40%, transparent);
	color: var(--fg-muted);
	white-space: nowrap;
}

.docs-tag.tag-feature {
	color: var(--ok);
	border-color: color-mix(in srgb, var(--ok) 45%, transparent);
	background: color-mix(in srgb, var(--ok) 10%, transparent);
}

.docs-tag.tag-fix {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 45%, transparent);
	background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.docs-tag.tag-breaking {
	color: var(--danger);
	border-color: color-mix(in srgb, var(--danger) 50%, transparent);
	background: color-mix(in srgb, var(--danger) 10%, transparent);
}

/* The surface tags read as a second row of information on one line, so they are
   deliberately quieter than the four that say what kind of change it is: an
   outline and no fill. Which part of the tool changed is context for the
   entries, not a claim about them. */
.docs-tag.tag-editor,
.docs-tag.tag-designer,
.docs-tag.tag-docs {
	color: var(--fg-faint);
	border-style: dashed;
}

/* A table with no header row: one release entry per row.
 *
 * A dot in front of four lines of prose stops separating them — the eye loses
 * which sentence started where. A ruled row is the same information with the
 * boundary drawn, and the box around it says these belong together without
 * needing a heading over a single column to say "Note". */
.docs-table.bare {
	border: 1px solid var(--border);
	border-radius: 5px;
	overflow: hidden;
	margin-bottom: 16px;
}

.docs-table.bare td {
	padding: 10px 12px;
	font-size: 12px;
	line-height: 1.55;
}

/* The last row's rule would sit on top of the container's own border. */
.docs-table.bare tr:last-child td {
	border-bottom: none;
}

/* A rule between releases.
 *
 * Scoped by `:has(+ .docs-tags)` — a heading immediately followed by tags is a
 * release heading, and nothing else on the site is. That avoids inventing a
 * page-level class to carry one border, and a browser without `:has()` loses
 * the rule and nothing else.
 *
 * Not on the first: the article's own title rule is directly above it, and two
 * lines a paragraph apart read as a mistake. */
.docs-content h2:has(+ .docs-tags):not(:first-of-type) {
	border-top: 1px solid var(--border);
	padding-top: 26px;
	margin-top: 34px;
}

/* A whole graph, rather than a row of separate nodes.
 *
 * It gets to run the full width and scroll, because a graph's shape is the
 * thing being shown and squeezing it to a reading measure would fold the wires
 * back on themselves. The frame is the same one a single node preview uses, so
 * the two read as the same kind of illustration. */
/* A graph you can move around in, rather than one squashed to fit.
 *
 * Fit-to-width was the first answer and the wrong one: a graph of any size
 * becomes unreadable, and the scrollbar it replaced was chrome on a page that
 * has none. This is a window onto the graph instead — `graphView.ts` gives it
 * the canvas's own wheel-zoom and drag-pan, and double-click refits.
 *
 * Everything here works without that script. The viewport clips, the graph
 * renders at its natural size, and the page is still readable — which is what
 * the static site promises. `interactive` is added by the script, so the
 * cursor only claims to be draggable once something is listening. */
/* The graph figure carries no frame of its own: the viewport below has one,
   and nesting them put a grid and a second border behind the caption, which
   read as part of the canvas when it is a sentence about it. */
.docs-preview.graph {
	padding: 0;
	border: none;
	border-radius: 0;
	background: none;
}

.docs-preview.graph figcaption {
	padding: 8px 2px 0;
}

.docs-preview.graph .graph-viewport {
	position: relative;
	overflow: hidden;
	height: 260px;
	border: 1px solid var(--border);
	border-radius: 5px;
	background: var(--bg-canvas);
	/* The canvas's grid, at rest, so the frame reads as a piece of the editor. */
	background-image:
		linear-gradient(to right, color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px),
		linear-gradient(to bottom, color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px);
	background-size: 24px 24px;
}

.docs-preview.graph .graph-viewport.interactive {
	cursor: grab;
	touch-action: none;
	/* The graph is full of `<text>`, so a drag across it would otherwise start a
	   text selection and the reader would highlight node titles instead of
	   panning. Nothing in a diagram is worth selecting — the Luau underneath is
	   the copyable part, and it still is. */
	user-select: none;
	-webkit-user-select: none;
}

.docs-preview.graph .graph-viewport.panning {
	cursor: grabbing;
}

.docs-preview.graph .graph-viewport svg {
	display: block;
	/* Positioned by `transform`; the script owns both. */
	position: absolute;
	top: 0;
	left: 0;
}

/* Says what the frame does, without a row of buttons.
 *
 * Only once the script has claimed the frame, because otherwise it would
 * promise something static markup cannot do — and only on hover, because a
 * permanent label sits over the bottom-right of every graph whether or not
 * anybody is looking for it. The `grab` cursor is the always-on affordance. */
.docs-preview.graph .graph-viewport.interactive::after {
	opacity: 0;
	transition: opacity 0.12s ease;
	content: "scroll to zoom · drag to pan · double-click to fit";
	position: absolute;
	right: 8px;
	bottom: 6px;
	font-size: 10px;
	color: var(--fg-faint);
	background: color-mix(in srgb, var(--bg-panel) 80%, transparent);
	border-radius: 3px;
	padding: 1px 6px;
	pointer-events: none;
}

.docs-preview.graph .graph-viewport.interactive:hover::after {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.docs-preview.graph .graph-viewport.interactive::after { transition: none; }
}

/* ------------------------------------------------------------- settings */

/* Settings borrows the Docs window's frame — same backdrop, same nav down the
   left — because a third overlay shape would be one too many. What it adds is
   the row: a label and its explanation on the left, the control on the right,
   at a fixed column so a column of controls lines up and can be scanned. */

.settings {
	width: min(900px, 100%);
	height: min(720px, 100%);
}

.settings .docs-head .spacer {
	flex: 1;
}

.settings-tab {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 1px;
	text-align: left;
}

.settings-tab-sub {
	font-size: 10px;
	color: var(--fg-faint);
}

.settings-page {
	overflow: auto;
	padding: 18px 22px 40px;
}

.settings-page h2 {
	margin: 0 0 4px;
	font-size: 15px;
}

.settings-page h3 {
	margin: 26px 0 4px;
	font-size: 13px;
}

/* Something a node needs that the script has not got, said in the Inspector
   where the node is selected -- and with the button that fixes it. Said and
   offered, never done: declaring a module because somebody picked a call would
   be the editor expanding what the project depends on without being asked. */
.inspector-warn {
	margin: 8px 0 4px;
	padding: 9px 10px;
	border-radius: 6px;
	border-left: 3px solid var(--warning);
	background: color-mix(in srgb, var(--warning) 10%, transparent);
	line-height: 1.5;
}

.inspector-warn p {
	margin: 0 0 8px;
}

/* -------------------------------------------------------- .luaurc document */

/* A `.luaurc`, opened from the tree like any other file. It borrows the
   settings rows because it is the same kind of thing to look at -- a name, what
   it does, and the control that changes it -- and the page around them is this
   file's own. */
.alias-doc {
	overflow: auto;
	padding: 20px 24px 40px;
	height: 100%;
}

/* Everything here shares one right edge, and it is the wide one.
   `.settings-warn` and the notes cap themselves at a reading measure, which is
   right inside a settings panel a third of the screen across and wrong here:
   this is a document, the rows already use the full width, and a paragraph that
   stops two-thirds short reads as a column that failed to load rather than as a
   line kept short on purpose. */
.alias-doc .settings-warn,
.alias-doc .settings-note,
.alias-doc .settings-problem,
.alias-doc .alias-scope {
	max-width: none;
}

/* A `.setting` is a wide label and a narrow control, which is right for a
   preference: most of the row is the sentence explaining it. An alias is the
   other shape -- a short name and a path -- so the columns are the other way
   round. Borrowing the settings row wholesale put the name in a column wide
   enough for a paragraph and the path in a box at the far end of it. */
.alias-doc .alias-row {
	grid-template-columns: minmax(140px, 220px) 1fr;
	gap: 12px;
	align-items: center;
}

/* The resolved path under the name, which is the thing being checked. */
.alias-doc .alias-row .setting-label {
	gap: 1px;
	min-width: 0;
}

.alias-doc .alias-row .setting-label span {
	overflow-wrap: anywhere;
}

.alias-doc-head h1 {
	margin: 0 0 4px;
	font-size: 16px;
}

.alias-doc h2 {
	margin: 28px 0 4px;
	font-size: 14px;
}

.alias-scope {
	margin: 0 0 18px;
	color: var(--fg-muted);
	line-height: 1.55;
	max-width: 64ch;
}

/* The value and the buttons that act on it, on one line: the path is the thing
   being edited and the rest is furniture beside it. */
.alias-control {
	display: flex;
	gap: 6px;
	align-items: center;
}

.alias-control input {
	flex: 1;
	min-width: 0;
}

.alias-row .setting-label input {
	margin-bottom: 3px;
}

/* Where an inherited alias was defined. Not a control: it belongs to another
   file, and the way to change it is to open that one. */
.alias-from {
	justify-content: flex-end;
	color: var(--fg-faint);
}

.alias-inherited .setting-control {
	opacity: 0.8;
}

/* Said where it happened rather than in a toast: an edit that was refused is
   about the file the reader is looking at, and a message that floats away takes
   the reason with it. */
.settings-warn {
	margin: 0 0 14px;
	padding: 10px 12px;
	border-radius: 6px;
	border-left: 3px solid var(--warning);
	background: color-mix(in srgb, var(--warning) 10%, transparent);
	line-height: 1.55;
	max-width: 64ch;
}

.settings-problem {
	margin: 4px 0;
	line-height: 1.5;
	max-width: 64ch;
}

.settings-problem.error { color: var(--danger); }
.settings-problem.warning { color: var(--warning); }

/* The published site's settings popover: the editor's panel, sized for the two
   preferences a reader of a page can change. Everything inside it is the app's
   own -- only the frame is smaller, because a full-height dialog for two rows
   is a dialog that looks like it failed to load the rest. */
.docs.docs-compact {
	width: min(560px, 100%);
	height: auto;
	max-height: min(520px, 100%);
}

.settings-note {
	margin: 0 0 14px;
	color: var(--fg-muted);
	line-height: 1.55;
	max-width: 64ch;
}

.settings-note code {
	background: var(--capsule-bg);
	border: 1px solid var(--capsule-border);
	border-radius: 3px;
	padding: 0 4px;
}

.setting {
	display: grid;
	grid-template-columns: 1fr 260px;
	gap: 16px;
	align-items: start;
	padding: 11px 0;
	border-top: 1px solid var(--border);
}

.setting-label {
	display: flex;
	flex-direction: column;
	gap: 3px;
}

.setting-label span {
	color: var(--fg-faint);
	line-height: 1.5;
}

.setting-control {
	display: flex;
	justify-content: flex-end;
}

.setting-control > * {
	width: 100%;
}

/* A segmented control is `inline-flex` everywhere else, because on the toolbar
   it should be exactly as wide as its labels. Here it sits in a fixed column
   beside a select and a checkbox, so it takes the column — and its buttons have
   to take the control, or the border stretches to the full width while the two
   halves huddle at the left end of it and the empty remainder reads as a third,
   broken option.

   `flex: 1` rather than natural widths, so the two halves are equal whatever
   they are labelled: "Manually" and "On every change" are very different
   lengths, and a divider that lands in a different place on every row is a
   worse answer than one that is always in the middle. */
.setting-control .segmented {
	display: flex;
}

.setting-control .segmented button {
	flex: 1;
	text-align: center;
}

.settings-toggle {
	display: flex;
	align-items: center;
	gap: 7px;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 4px 9px;
	cursor: pointer;
}

.settings-toggle:hover {
	border-color: var(--border-strong);
}

.path-list {
	display: flex;
	flex-direction: column;
	gap: 6px;
	max-width: 520px;
}

.path-row {
	display: flex;
	gap: 6px;
}

.path-row input {
	flex: 1;
}

/* ----------------------------------------------------------- theme cards */

.theme-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
	gap: 10px;
	margin-bottom: 16px;
}

.theme-card {
	display: flex;
	flex-direction: column;
	gap: 4px;
	padding: 6px;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 7px;
	cursor: pointer;
	text-align: left;
	color: inherit;
	font: inherit;
}

.theme-card:hover {
	border-color: var(--border-strong);
}

/* The chosen scheme is outlined rather than filled: the card's whole job is to
   show you a palette, and painting our accent across it would be showing you
   ours instead. */
.theme-card.on {
	border-color: var(--accent);
	box-shadow: 0 0 0 1px var(--accent);
}

.theme-swatch,
.theme-swatch-pair {
	display: block;
	width: 100%;
	aspect-ratio: 160 / 78;
	border-radius: 4px;
	overflow: hidden;
	border: 1px solid var(--border);
}

/* Follow-the-system has two palettes and no single answer, so it shows both,
   split down the middle the way the setting actually behaves. */
.theme-swatch-pair {
	position: relative;
}

.theme-swatch-pair .half {
	position: absolute;
	inset: 0;
	width: 50%;
	overflow: hidden;
}

.theme-swatch-pair .half svg {
	width: 200%;
	height: 100%;
}

.theme-swatch-pair .half.right {
	left: 50%;
}

.theme-swatch-pair .half.right svg {
	margin-left: -100%;
}

.theme-name {
	font-weight: 600;
	padding: 0 2px;
}

.theme-credit {
	font-size: 11px;
	color: var(--fg-faint);
	padding: 0 2px 2px;
	min-height: 14px;
}

.role-table {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
	gap: 2px 16px;
}

.role {
	display: grid;
	grid-template-columns: 14px 118px 1fr;
	gap: 8px;
	align-items: center;
	padding: 3px 0;
}

.role .chip {
	width: 14px;
	height: 14px;
	border-radius: 3px;
	border: 1px solid var(--border-strong);
}

.role span:last-child {
	color: var(--fg-faint);
}

/* -------------------------------------------------------------- licences */

.licence h3 .spdx {
	font-weight: 400;
	font-size: 11px;
	color: var(--fg-faint);
	border: 1px solid var(--capsule-border);
	background: var(--capsule-bg);
	border-radius: 3px;
	padding: 1px 5px;
	margin-left: 6px;
	vertical-align: middle;
}

.licence-text {
	margin: 0 0 18px;
	padding: 12px 14px;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 6px;
	font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
	font-size: 11px;
	line-height: 1.5;
	white-space: pre-wrap;
	color: var(--fg-muted);
	max-height: 260px;
	overflow: auto;
}

/* ------------------------------------------------- selection preview */

/* Deliberately narrower than the docs window: this is one column of code, and
   a wide box would set the eye scanning for a second column that is not there. */
.preview-luau {
	width: min(760px, 100%);
	height: min(620px, 100%);
	grid-template-rows: auto 1fr;
}

.preview-luau .docs-head .spacer {
	flex: 1;
}

.preview-toggle {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 11px;
	color: var(--fg-muted);
	cursor: pointer;
}

.preview-body {
	overflow: auto;
	padding: 12px 14px 20px;
	min-height: 0;
}

.preview-note {
	margin: 0 0 12px;
	padding: 9px 11px;
	background: var(--capsule-bg);
	border: 1px solid var(--capsule-border);
	border-radius: 6px;
	color: var(--fg-muted);
	line-height: 1.55;
}

.preview-code {
	margin: 0;
	font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
	font-size: 12px;
	line-height: 1.6;
	white-space: pre;
	overflow-x: auto;
}

.preview-code .ln {
	display: block;
}

.preview-code .num {
	display: inline-block;
	width: 3.5ch;
	margin-right: 12px;
	text-align: right;
	color: var(--fg-faint);
	user-select: none;
}

/* The lines these nodes actually produced. A left rule rather than a fill,
   because the code is already carrying colour and a highlight behind it would
   fight the syntax rather than frame it. */
.preview-code .ln.mine {
	background: color-mix(in srgb, var(--accent) 14%, transparent);
	box-shadow: inset 2px 0 0 var(--accent);
}

.preview-code .ln.mine .num {
	color: var(--accent);
}

/* Where an inlined pure node's value ended up. Marked differently from a line
   the node produced, because it is a different claim. */
.preview-code .ln.downstream {
	background: color-mix(in srgb, var(--ok) 12%, transparent);
	box-shadow: inset 2px 0 0 var(--ok);
}

.preview-code .ln.downstream .num {
	color: var(--ok);
}

.preview-code .fold {
	display: block;
	color: var(--fg-faint);
	padding-left: 4.5ch;
	user-select: none;
}

/* ------------------------------------------- toolbars drawn in the docs */

/* A bar of the tool, drawn on the page it is documented on. See
   `src/core/docs/toolbars.ts` for why it is a replica rather than a
   screenshot: it takes the reader's theme, and it goes stale visibly.

   Everything inside the picture is the real chrome's own class names, so
   these rules only have to do two things — put the bar in a frame, and make
   sure nothing in it can be pressed. */
.docs-bar {
	margin: 0 0 22px;
	border: 1px solid var(--border);
	border-radius: 6px;
	overflow: hidden;
	background: var(--bg-panel);
}

/* A bar is as wide as it is. On a narrow page it scrolls sideways rather than
   wrapping into a shape the reader will never see on their own screen —
   wrapping was the first version, and it put Settings on a second row, which
   is the one thing the page must not say. */
.docs-bar-picture {
	overflow-x: auto;
}

/* The bar itself. Nothing in it does anything: no handlers, no tab stops, and
   `aria-hidden` on the frame, because the legend underneath is what carries the
   content. It does take the pointer, though -- hovering a button lights its
   explanation, which is the whole point of the page.

   The bar sits *inside* the frame rather than welded to its edges. Flush
   against the legend it read as the top row of a table -- part of the list
   rather than the thing the list is about -- and a control nobody reads as a
   control is not one they think to hover. A little of the app's own background
   around it, and the chrome becomes an object on a surface.

   Padding on the frame rather than on the scroller around it: the frame is the
   scrolled content, so its padding is part of the scrollable width and the
   trailing edge survives being scrolled to. On a padded scroller it would
   not. */
.docs-bar-frame {
	user-select: none;
	min-width: min-content;
	padding: 12px;
	background: var(--bg-app);
}

/* The drawn chrome, as a thing with edges. In the tool these bars run the full
   width of a window and have no outer border to draw; here they are a picture
   of one, so they get the corners that say where they stop. */
.docs-bar-frame.bar > .toolbar,
.docs-bar-frame.head > .docs-page-head {
	border: 1px solid var(--border);
	border-radius: 7px;
}

/* A docked panel, drawn on a page.
 *
 * In the editor a panel fills its dock and scrolls; here it is a picture of
 * one, so it takes the width it needs and stops. The contents carry the
 * editor's own class names -- `.variable`, `.swatch`, `.variables-sub` -- which
 * is where the likeness comes from, so this rule only has to undo the dock. */
/* Centred in its frame: at its own width against the left edge, a wide page
   left it stranded beside an empty stage. A column, said here rather than
   inherited, because `.panel` and `.inspector` are the editor's own classes and
   bring their own flex direction with them -- centring on the wrong axis is
   what the first version of this did. `safe`, so a picture wider than the
   frame still scrolls from its left edge rather than losing it. */
.docs-bar-frame.panel,
.docs-bar-frame.inspector {
	display: flex;
	flex-direction: column;
	align-items: safe center;
}

.docs-bar-frame.panel > .variables {
	width: min(340px, 100%);
	padding: 4px 0 8px;
	border: 1px solid var(--border);
	border-radius: 7px;
	background: var(--bg-panel);
}

/* The Inspector, drawn on a page.
 *
 * As the docked panel above: the contents carry the editor's own class names --
 * `.field`, `.list-row`, `.check-row` -- so the likeness is the editor's own
 * stylesheet rather than a second description of it. This rule only has to give
 * the picture its edges and stop it filling the page. */
.docs-bar-frame.inspector > .inspector {
	width: min(360px, 100%);
	padding: 12px;
	border: 1px solid var(--border);
	border-radius: 7px;
	background: var(--bg-panel);
}

/* The rows the Inspector draws in a list editor, as pictures: no input to
   focus, so the boxes are spans and want the height a field has. */
/* The boxes. A drawn field is a span rather than an input -- nothing here is
   focusable -- so it needs the border and the fill an input paints itself. */
.docs-bar-frame.inspector .docs-bar-field,
.docs-bar-frame.inspector .docs-bar-select {
	display: block;
	box-sizing: border-box;
	width: 100%;
	min-width: 0;
	padding: 5px 8px;
	border: 1px solid var(--border);
	border-radius: 5px;
	background: var(--bg-input);
	color: var(--fg);
	text-align: left;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The select keeps its chevron, and the row it sits in does not grow for it. */
.docs-bar-frame.inspector .docs-bar-select {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
}

/* A picture of a panel does not scroll: a definition too long for the box is
   cut with an ellipsis, as the editor's own field cuts it. */
.docs-bar-frame.inspector > .inspector {
	overflow: hidden;
}

.docs-bar-frame.inspector .list-row button {
	padding: 0;
	text-align: center;
}

.docs-bar-frame.inspector .list-row {
	display: grid;
	grid-template-columns: minmax(0, 3fr) minmax(0, 2fr) 24px;
	gap: 4px;
	margin-top: 4px;
}

/* The panel's headings, restated in full.
 *
 * They carry the editor's own `.variables-sub`, which is how every other part
 * of this picture gets its likeness -- but an article styles its headings, and
 * `.docs-page .docs-content h3` outspecifies a lone class. So the drawn panel
 * came out with the article's h3: twice the size, in the article's colour, and
 * not remotely what the editor shows.
 *
 * Qualified by the element to match that specificity, and the type is restated
 * rather than inherited, because inheriting is exactly what did not happen. */
.docs-bar-frame.panel h2,
.docs-bar-frame.panel h3.variables-sub {
	display: flex;
	align-items: center;
	gap: 8px;
	margin: 10px 0 4px;
	padding: 0 10px;
	font-size: 10px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--fg-faint);
}

.docs-bar-frame.panel h2 button.tb,
.docs-bar-frame.panel h3.variables-sub button.tb {
	margin-left: auto;
	font-size: 11px;
	padding: 1px 8px;
}

.docs-bar-frame.panel .variable-head {
	padding: 3px 10px;
}

/* A section of a drawn panel: its heading and the rows under it.

   The picture has to be the whole panel -- a cropped one is a drawing of
   something nobody has -- but a page is about one section of it, and a reader
   given four equal sections has to find that out from the prose. So the page's
   own section keeps the light and the rest fall back: a scrim with a cutout,
   which is the same thing the words are already saying.

   Only the frame has the padding, so a section can carry a background edge to
   edge without a gap down either side. */
.docs-bar-frame.panel .panel-section {
	padding: 1px 0 3px;
	border-radius: 6px;
	transition: opacity 120ms ease;
}

.docs-bar-frame.panel .panel-section.dim {
	opacity: 0.4;
}

.docs-bar-frame.panel .panel-section.focus {
	background: color-mix(in srgb, var(--accent) 9%, transparent);
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Pointing wins over the scrim. Hovering a dimmed section's explanation lights
   it in the picture, and a lit section the reader cannot read is a highlight
   that hides the thing it is pointing at. */
.docs-bar-frame.panel .panel-section.dim:has(.lit) {
	opacity: 1;
}

.docs-bar-frame.panel .hint {
	padding: 2px 10px 4px;
	margin: 0;
}

/* The floating tools already float: they sit over a piece of canvas, and that
   canvas is the inset. Padding here would inset the inset.

   The grid is on the frame rather than on the strip inside it, so the whole
   containing element reads as canvas. On the strip alone it stopped where the
   tools stopped, which drew a second edge just inside the frame's own and made
   the picture look like two boxes -- and the point of this one is that the
   groups float over a view that carries on past them. */
.docs-bar-frame.float {
	/* Padding, not a margin on the strip inside. With `padding: 0` here the
	   strip's vertical margins collapsed straight out through this element:
	   the frame ended up exactly as tall as the tools, the space above and
	   below them belonged to the figure instead, and the grid stopped at the
	   tools with a bare band either side of it. The gap has to be *inside* the
	   thing that paints the canvas. */
	padding: 20px 14px;
	background-color: var(--bg-canvas);
	background-image:
		linear-gradient(var(--grid-fine) 1px, transparent 1px),
		linear-gradient(90deg, var(--grid-fine) 1px, transparent 1px);
	background-size: 24px 24px;
}

/* Which is also why the real hover paint is off in here. A drawn button lights
   the same way whether the pointer is on it or on its row in the list, and two
   different highlights for one relationship reads as two relationships. */
.docs-bar-frame [data-control],
.docs-bar-frame [data-control] button {
	cursor: help;
}

.docs-bar-frame button.tb:hover:not(:disabled),
.docs-bar-frame .segmented button:hover {
	background: var(--bg-input);
	border-color: var(--border);
}

.docs-bar-frame button.tb.primary:hover:not(:disabled) {
	background: var(--accent);
	filter: none;
}

.docs-bar-frame button.tb.on:hover:not(:disabled) {
	background: color-mix(in srgb, var(--accent) 18%, var(--bg-input));
	border-color: var(--accent);
}

.docs-bar-frame button.tb.icon-only:hover:not(:disabled) {
	color: var(--fg-muted);
}

/* The editor's bar is the full width of a window, so it keeps its own rule.
   Nothing to override: `.toolbar` is already a plain flex row. */
.docs-bar-frame.bar > .toolbar,
.docs-bar-frame.head > .docs-page-head {
	/* The real ones are separated from what is below them by this rule. A
	   drawn one has its own border instead, so the rule would be a second
	   edge in the same place. */
	border-bottom: 0;
	flex-wrap: nowrap;
}

/* The graph's tools really do float, absolutely positioned over the canvas.
   Here there is no canvas to float over, so the strip is laid out in flow —
   and given some of the canvas above and below it, because the gaps between
   the groups are the thing being explained. */
/* Laid out in flow rather than absolutely, since there is no canvas here for
   them to be positioned over -- the frame behind them is the canvas now, and
   its padding is the room they float in. */
.docs-bar-frame.float > .floating-tools {
	position: static;
	flex-wrap: nowrap;
	margin: 0;
}

/* A real <select> is a tab stop even with nothing to focus, and opens on a
   click the frame is trying to swallow. This is the box without either:
   `select.tb`'s paint, and the chevron drawn on. */
.docs-bar-frame .tb.docs-bar-select {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 4px 9px;
	white-space: nowrap;
}

.docs-bar-frame .tb.docs-bar-select::after {
	content: "";
	width: 0;
	height: 0;
	border: 4px solid transparent;
	border-top-color: var(--fg-muted);
	margin-top: 2px;
}

/* ---- the lit pair
 *
 * One control and its explanation, lit together. A ring rather than a fill: the
 * drawn control already uses fill to mean "this setting is on", and a second
 * meaning for the same paint would have Straighten look pressed because the
 * pointer was over its row in the list.
 *
 * `.linked` is set by the script, so the cursor and the ring only appear where
 * the pairing actually works. */
.docs-bar [data-control] {
	border-radius: 6px;
}

.docs-bar.linked [data-control] {
	transition: box-shadow 90ms ease, background-color 90ms ease;
}

.docs-bar-frame [data-control].lit {
	box-shadow: 0 0 0 2px var(--accent);
}

.docs-bar-legend li.lit {
	background: color-mix(in srgb, var(--accent) 12%, transparent);
	box-shadow: inset 2px 0 0 var(--accent);
}

.docs-bar.linked .docs-bar-legend li {
	cursor: help;
}

/* A link in an explanation is a link first -- the script lets it navigate, so
   it has to keep looking like one. */
.docs-bar.linked .docs-bar-legend li a {
	cursor: pointer;
}

/* Said once, under the first bar on the page, by the renderers. Quiet: it is an
   instruction for a thing the reader will find by doing it anyway. */
.docs-bar-hint {
	padding: 8px 14px;
	font-size: 11px;
	color: var(--fg-faint);
	border-top: 1px solid var(--border);
}

/* Nothing to hint at without a pointer, and nothing to hint at before the
   script has claimed the figure. */
.docs-bar:not(.linked) .docs-bar-hint {
	display: none;
}

@media (hover: none) {
	.docs-bar-hint::after {
		content: " Tap one to keep it lit.";
	}
}

/* What the bar is and where it sits, between the picture and the list. */
.docs-bar-summary {
	margin: 0;
	padding: 10px 14px;
	font-size: 12px;
	color: var(--fg-muted);
	border-top: 1px solid var(--border);
	background: var(--bg-panel);
}

/* Every control, in the order it is drawn. A list rather than a table: the
   name is the thing being scanned and the explanation is detail under it,
   which is a column that would be empty on no rows and wide on all of them. */
/* Scoped under `.docs-bar` so it outranks `.docs-content ul` and `li`, whose
   indent and row gap otherwise inset the legend from its frame. */
.docs-bar .docs-bar-legend {
	margin: 0;
	padding: 0;
	list-style: none;
	border-top: 1px solid var(--border);
}

.docs-bar .docs-bar-legend li {
	margin: 0;
	display: grid;
	grid-template-columns: minmax(9rem, 14rem) 1fr;
	gap: 4px 18px;
	padding: 9px 14px;
	border-bottom: 1px solid var(--border);
	font-size: 13px;
	line-height: 1.55;
}

.docs-bar-legend li:last-child {
	border-bottom: 0;
}

.docs-bar-name {
	font-weight: 600;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

/* Where to look for it. Under the name rather than in the sentence, because a
   reader who cannot find a button is scanning for this and nothing else. */
.docs-bar-where {
	font-weight: 400;
	font-size: 11px;
	color: var(--fg-faint);
}

.docs-bar-what {
	color: var(--fg-muted);
}

.docs-bar figcaption {
	padding: 10px 14px;
	border-top: 1px solid var(--border);
	font-size: 12px;
	line-height: 1.5;
	color: var(--fg-muted);
}

/* One column when the reading column is narrow -- a *container* query, not a
   media query. The window can be 780px wide with the article at 440 once the
   nav and the outline have taken their share, and a viewport query does not
   see that: the name column held its 9rem and left the explanation wrapping
   at four words. `.docs-page .docs-content` is the container. */
@container (max-width: 620px) {
	.docs-bar .docs-bar-legend li {
		grid-template-columns: 1fr;
		gap: 2px;
	}
}

/* A module's swatch. A variable's is its type's colour; a module has no type,
   so it gets the outline that says "a thing, not a value". */
.variables .swatch.module {
	background: none;
	border: 1px solid var(--fg-faint);
}

/* The Modules heading carries an Add button, which the other subheadings do
   not -- they list what the graph already declares, and this one declares. */
.variables h3.variables-sub {
	display: flex;
	align-items: center;
	gap: 8px;
}

.variables h3.variables-sub button.tb {
	margin-left: auto;
	font-size: 11px;
	padding: 1px 8px;
}

/* ------------------------------------------ the runtime filter and badge */

/* Which runtime, on top of the filter the graph's own target already applies.
   A row of chips above the search box in both the node menu and the node
   picker -- the same control in both, because it is the same question and the
   answer is one shared preference. */
.menu-runtimes {
	display: flex;
	gap: 4px;
	/* Room underneath as well as above. With `0` at the bottom the chips sat
	   directly on the search field -- which has its own background and a rule
	   under it, so the two read as one control stuck to another rather than as
	   a row above a box. */
	padding: 8px 8px;
	flex-wrap: wrap;
	align-items: center;
}

.menu-runtimes button {
	font: inherit;
	font-size: 11px;
	line-height: 1.6;
	padding: 1px 8px;
	border: 1px solid var(--border);
	border-radius: 999px;
	background: var(--bg-input);
	color: var(--fg-muted);
	cursor: pointer;
}

.menu-runtimes button:hover {
	border-color: var(--border-strong);
	color: var(--fg);
}

/* What the row says when there is only one runtime to say it about -- which is
   every Lune graph until the Lune library lands. Hiding the row entirely was
   the first answer and it read as the feature being missing rather than as
   there being nothing to choose. */
.menu-runtimes .only {
	font-size: 11px;
	line-height: 1.6;
	padding: 1px 2px;
	color: var(--fg-faint);
}

.menu-runtimes .only strong {
	font-weight: 600;
	color: var(--fg-muted);
}

.menu-runtimes button.on {
	background: color-mix(in srgb, var(--accent) 18%, var(--bg-input));
	border-color: var(--accent);
	color: var(--accent);
}

/* What a node needs, on its row. Base Luau is the unmarked case: badging four
   rows in five would be noise rather than information, and the absence is the
   claim -- this one runs anywhere.

   Compounded on `.hint`, which already sits at the right of a row in both
   lists and is where `pure` goes, so a row never grows a second alignment. */
.hint.runtime {
	border: 1px solid transparent;
	border-radius: 3px;
	padding: 0 4px;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	font-size: 9px;
	/* Centred wherever it lands, including the picker, whose rows do not share
	   the menu's rule. A bordered box on a baseline hangs below it. */
	align-self: center;
}

.hint.runtime.roblox {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.hint.runtime.lune {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 45%, transparent);
}

/* Something this graph declares rather than a node from the library -- a
   variable you named, a local, a function, one of its parameters. Quieter than
   the runtimes: it is a fact about where the entry came from, not a warning
   about where it will run. */
.hint.runtime.graph {
	color: var(--fg-muted);
	border-color: var(--border);
}

/* --------------------------------------------------------------------------
   The map editor, drawn in the documentation.

   Almost nothing here is about how the panel looks: it is drawn with the
   editor's own class names, so `.map-editor` and everything under it is
   already styled by the rules above and cannot be restyled out of step with
   the real thing. What is left is the frame it sits in, the height it takes
   when it is not filling a window, and the highlight that pairs a row with
   the lines it writes.
   ----------------------------------------------------------------------- */

/* The figure is wider than the measure.

   A page of prose caps itself at 78ch, which is right for sentences and far
   too narrow for a tree, an Inspector and a project file side by side. So the
   figure is sized against the content column rather than against the article
   -- `.docs-content` is already a container for exactly this, and this is the
   same breakout `.docs-preview.breakout` above makes.

   The negative left margin is the part that is easy to leave out. The article
   is centred in the column, so a child merely made wider grows to the right
   only and runs off the end of the page; re-centring it on the column is what
   makes it a breakout rather than an overflow. */
.docs-page .docs-article > .docs-map {
	--frame: min(1280px, calc(100cqw - 32px));
	box-sizing: border-box;
	width: var(--frame);
	margin: 0 0 18px calc((100% - var(--frame)) / 2);
	padding: 0;
	max-width: none;
}

/* The article's typography does not reach inside the panel.

   `.docs-page .docs-content h2` is two classes and an element; `.map-side h2`
   is one and an element, so the article won and the Inspector's section
   headings came out at body size in the article's colour -- and the hints
   under them came out as paragraphs, which between them ate most of the
   height the figure had to show a project file in.

   The same fix, and the same reason, as `.docs-bar-frame.panel h2` above:
   qualified enough to match, and restated rather than inherited, because
   inheriting is exactly what did not happen. */
.docs-page .docs-content .docs-map-frame h2 {
	margin: 0;
	padding: 10px 12px 6px;
	border: 0;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--fg-faint);
}

.docs-page .docs-content .docs-map-frame p.summary {
	margin: 0;
	padding: 0 12px 6px;
	max-width: none;
	font-size: 11px;
	line-height: 1.45;
	color: var(--fg-muted);
}

.docs-page .docs-content .docs-map-frame p.summary code {
	font-size: 10.5px;
	padding: 0 3px;
}

/* Panel and legend side by side. The panel is tall, and a column of eleven
   names underneath a tall picture is a column nobody reads beside the thing it
   names. */
.docs-map-body {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 290px);
	gap: 18px;
	/* Stretched, not started. The legend is eleven entries and usually the
	   taller of the two, and a panel that stopped at its own content height
	   left a column of empty page beside the second half of the list. The
	   panel takes the row's height instead, so the two columns end together. */
	align-items: stretch;
}

@media (max-width: 900px) {
	.docs-map-body {
		grid-template-columns: minmax(0, 1fr);
	}
}

.docs-map-frame {
	display: flex;
	/* The ceiling lives here rather than on the editor inside it. Stretched to
	   the row, the frame takes the legend's height and the editor fills it, so
	   the two columns end together instead of leaving a column of empty page
	   beside the last few entries. */
	max-height: min(860px, 88vh);
	border: 1px solid var(--border);
	border-radius: 6px;
	overflow: hidden;
	background: var(--bg-panel);
}

/* The editor fills a window; here it is given a height to fill instead. Tall
   enough for a small tree and the whole of its project file, which is the
   figure's job -- a reader scrolling the file to find the lit lines is being
   asked to do the thing the highlight was for. */
/* Tall enough to be worked in rather than looked at -- the figure is the
   centrepiece of the page, and a panel with a project file the reader has to
   scroll a two-line window through is a picture of a panel again.

   A ceiling rather than a height, because a small map should not be followed
   by four hundred pixels of empty tree. The children carry their own
   scrollers, so the grid sizes to whatever they need up to this. */
.docs-map-frame .map-editor {
	flex: 1;
	min-width: 0;
	min-height: 380px;
}

/* The project file keeps a scroller of its own rather than growing until the
   whole Inspector scrolls. Selecting a row then moves the file to the lines
   that row writes and leaves Name, Class and Path where the reader is looking. */
.docs-map-frame .map-preview {
	max-height: 320px;
}

/* Lines of the project file are spans so each can carry the node that wrote
   it. `<pre>` already keeps their whitespace; this only keeps them on their
   own rows. */
.map-preview-line {
	display: block;
	border-radius: 3px;
	transition: background-color 90ms ease, box-shadow 90ms ease;
}

/* On a filesystem map, what reaches the file, pushed to the right of it. */
.map-reach {
	float: right;
	padding-left: 24px;
	color: var(--fg-faint);
}

/* Both sides take the same highlight, because the point being made is that
   they are the same thing seen twice. `lit` is the row under the pointer;
   `selected` is the row the Inspector is showing, and is the editor's own
   class doing its own job. */
.docs-map .map-preview-line.lit,
.docs-map .map-row.lit {
	background: color-mix(in srgb, var(--accent) 14%, transparent);
	box-shadow: inset 2px 0 0 var(--accent);
}

.docs-map .map-row[data-control] {
	cursor: pointer;
}

/* A demonstrator, not a sandbox.

   The buttons are the parts that genuinely do nothing -- there is no project
   to add a folder to -- so they are dimmed to say so before anybody presses
   one. The fields are not: they are filled from whichever row is selected and
   change as you go, so dimming them would be calling live output dead. */
.docs-map-frame .map-editor button[disabled],
.docs-map-frame .map-editor .map-actions select[disabled] {
	opacity: 0.38;
	cursor: default;
}

.docs-map-frame .map-editor input[readonly],
.docs-map-frame .map-editor select[disabled] {
	cursor: default;
}

/* --- the legend, and pointing between it and the panel ------------------- */

/* The entries are separated by space rather than by rules.

   Each one is a name over a sentence, so the gap between two entries has to
   beat the gap between a name and its own sentence or the list reads as one
   run of alternating weights. Hence the tight `margin` inside an entry and the
   much larger `gap` between them. */
.docs-map-legend {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 10px;
	align-content: start;
}

.docs-map-legend li {
	padding: 4px 10px 5px;
	border-radius: 5px;
	border-left: 2px solid transparent;
	font-size: 12px;
	line-height: 1.5;
	transition: background-color 90ms ease, opacity 90ms ease;
}

.docs-map-legend .docs-map-part {
	display: block;
	margin-bottom: 1px;
	font-weight: 600;
	color: var(--fg);
}

.docs-map-legend .docs-map-what {
	display: block;
	color: var(--fg-muted);
	text-wrap: pretty;
}

.docs-map.linked .docs-map-legend li {
	cursor: help;
}

/* Pointing at one part dims everything that is not it, on both sides at once.

   The scrim is on the figure rather than on each part, so there is one rule
   deciding what "not the thing you are pointing at" looks like -- the same
   arrangement the Variables panel preview makes, where a section that is not
   the subject of the paragraph steps back rather than disappearing. */
.docs-map.pointing .docs-map-legend li,
.docs-map.pointing .docs-map-frame [data-part] {
	opacity: 0.32;
}

.docs-map.pointing .docs-map-legend li.shown,
.docs-map.pointing .docs-map-frame [data-part].shown {
	opacity: 1;
}

.docs-map-legend li.shown {
	background: color-mix(in srgb, var(--accent) 12%, transparent);
	border-left-color: var(--accent);
}

/* The ring is an overlay, not a shadow or an outline.

   Both of those are painted with the element's own box decoration, which put
   them underneath two different things. The tree fills its half of the frame
   and touches three of its edges, so an outer shadow was cut off by the
   frame's `overflow: hidden`; and the tree's bar is `position: sticky` with a
   z-index, so an inset one was painted over along the top. The Inspector's
   fields had the milder version of the same thing, the next field's background
   covering the millimetre of ring between them.

   A pseudo-element sits above all of it, inside the box so nothing can clip
   it, and takes no pointer events so it cannot change what is being hovered. */
.docs-map-frame [data-part] {
	position: relative;
}

.docs-map-frame [data-part].shown::after {
	content: "";
	position: absolute;
	inset: 0;
	border: 2px solid var(--accent);
	border-radius: 4px;
	pointer-events: none;
	z-index: 4;
}

.docs-map > figcaption {
	margin: 8px 0 0;
}

/* One under the other once there is no room for two: an Inspector squeezed
   beside a tree on a phone is two unreadable columns instead of one. */
@media (max-width: 860px) {
	.docs-map-frame .map-editor {
		grid-template-columns: minmax(0, 1fr);
		height: auto;
	}

	.docs-map-frame .map-side {
		border-left: none;
		border-top: 1px solid var(--border);
	}
}

/* --------------------------------------------------------------------------
   The introduction panel, on the Roswaal mark in every window.

   Sized to its content rather than to the window: it is a way in, not a
   workspace, and a dialog that filled a 4K screen would put its two buttons
   half a metre apart.
   ----------------------------------------------------------------------- */

.intro-backdrop {
	position: fixed;
	inset: 0;
	/* Under the dialogs, not over them.

	   `Dialog` sits at `LAYER.menu + 1`, and this was at 90 — so a notice
	   raised while the panel was open rendered behind it: invisible, modal,
	   and holding the focus. Opening a recent project that had been deleted
	   did exactly that, which is how it was found. Above everything on the
	   canvas, below anything that speaks. */
	z-index: 68;
	display: grid;
	place-items: center;
	padding: 24px;
	background: rgba(0, 0, 0, 0.45);
	backdrop-filter: blur(2px);
}

.intro-panel {
	display: flex;
	flex-direction: column;
	width: min(760px, 100%);
	max-height: min(640px, 100%);
	border: 1px solid var(--border-strong);
	border-radius: 10px;
	background: var(--bg-panel);
	box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);
	overflow: hidden;
}

.intro-head {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 12px 12px 12px 16px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-input);
}

/* The mark and the wordmark, which is the one place in the app they sit
   together at this size -- everywhere else the mark is chrome. */
.intro-head .logo {
	display: flex;
	align-items: center;
	gap: 9px;
	font-size: 15px;
	font-weight: 600;
	letter-spacing: 0.01em;
	color: var(--fg);
}

/* Which window you are in, said quietly. The panel is the same in all three,
   so this is the only thing on it that answers "where am I". */
.intro-where {
	display: flex;
	align-items: center;
	gap: 6px;
	padding-left: 10px;
	border-left: 1px solid var(--border);
	font-size: 12px;
	color: var(--fg-dim);
}

.intro-body {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
	padding: 4px 0 8px;
}

.intro-shelf {
	padding: 10px 0 4px;
}

.intro-shelf-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 0 16px 8px;
}

.intro-shelf-head h2 {
	margin: 0;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--fg-faint);
}

.intro-arrows {
	display: flex;
	gap: 4px;
	margin-left: auto;
}

/* One chevron glyph, turned. It points down as drawn. */
.turn-left,
.turn-right {
	display: inline-flex;
}

.turn-left svg {
	transform: rotate(90deg);
}

.turn-right svg {
	transform: rotate(-90deg);
}

/* The rail scrolls sideways and snaps, so a nudge never leaves half a card
   showing. Padding rather than margin on the ends, so the first card starts
   level with the heading above it and the last one can still reach the edge. */
.intro-rail {
	display: flex;
	gap: 10px;
	padding: 2px 16px 6px;
	overflow-x: auto;
	scroll-snap-type: x proximity;
	/* The gutter, said twice.

	   `padding` puts it there; `scroll-padding` keeps it there. A snap aligns
	   a card to the *scrollport* edge, which ignores padding -- so the moment
	   this rail was scrolled at all, the first card slid flush against the
	   panel's edge while the shelf below it, still at zero, kept its 16px. Two
	   rows of the same cards, indented differently, because one of them had
	   been touched. */
	scroll-padding-inline: 16px;
	/* Always thin, never toggled. Showing it on hover changed the scrollport's
	   height, so every card in the shelf shifted up a pixel or two as the
	   pointer arrived and back again as it left. A quiet scrollbar that is
	   always there costs less than a tidy one that moves the furniture. */
	scrollbar-width: thin;
	scrollbar-color: transparent transparent;
	transition: scrollbar-color 120ms ease;
}

.intro-shelf:hover .intro-rail {
	scrollbar-color: var(--border-strong) transparent;
}

.intro-card {
	flex: 0 0 auto;
	scroll-snap-align: start;
	display: flex;
	flex-direction: column;
	gap: 3px;
	width: 220px;
	padding: 11px 12px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--bg-input);
	text-align: left;
	cursor: pointer;
	transition: border-color 90ms ease, background-color 90ms ease;
}

.intro-card:hover {
	border-color: var(--accent);
	background: var(--bg-hover);
}

/* The project this window already has open. Marked rather than hidden: it is
   still the one you are most likely to mean. */
.intro-card.on {
	border-color: color-mix(in srgb, var(--accent) 60%, transparent);
}

.intro-card-name {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 13px;
	font-weight: 600;
	color: var(--fg);
}

/* The runtime chip on a demo card, drawn as the documentation draws it on a
   page title -- same shape, same two colours, because it is answering the same
   question in both places. Restated rather than shared: `.docs .badge` is
   scoped to the documentation, and widening that selector to reach a panel in
   the editor would make every badge in the app depend on a docs rule. */
.intro-card .badge {
	font-size: 9px;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--fg-faint);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 1px 4px;
	white-space: nowrap;
}

.intro-card .badge.runtime.roblox {
	color: var(--accent);
	border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.intro-card .badge.runtime.lune {
	color: var(--warning);
	border-color: color-mix(in srgb, var(--warning) 50%, transparent);
}

.intro-card-what {
	font-size: 11.5px;
	line-height: 1.4;
	color: var(--fg-muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.intro-card-open {
	margin-top: 2px;
	font-size: 10.5px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--fg-faint);
}

.intro-empty {
	margin: 0;
	padding: 28px 16px;
	text-align: center;
	font-size: 12.5px;
	color: var(--fg-muted);
}

.intro-foot {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 12px;
	border-top: 1px solid var(--border);
	background: var(--bg-input);
}

/* Wraps rather than scrolls: the project actions are host-dependent, so how
   many are in this row is not something the layout can be written around. */
@media (max-width: 620px) {
	.intro-foot {
		flex-wrap: wrap;
	}

	.intro-foot > span[style] {
		display: none;
	}
}

/* The mark in a window's header, once it became a button.

   Styled back to what it was: it is the same chip it has always been and the
   only change is that pressing it now does something. A button's own border,
   background and padding would have made the header look rearranged. */
.docs-page-head .logo.as-chip {
	border: 1px solid transparent;
	border-radius: 6px;
	margin-left: -6px;
	padding: 2px 6px;
	background: transparent;
	/* Family and size, not the `font` shorthand. `font: inherit` also resets
	   weight and letter-spacing, which `.docs-page-head .logo` above is the
	   one supplying -- so the wordmark came out at the header's normal weight
	   the moment it became a button. */
	font-family: inherit;
	font-size: inherit;
	color: inherit;
	cursor: pointer;
	transition: background-color 90ms ease, border-color 90ms ease;
}

.docs-page-head .logo.as-chip:hover {
	border-color: var(--border);
	background: var(--bg-hover);
}

.docs-page-head .logo.as-chip:active {
	background: color-mix(in srgb, var(--accent) 14%, var(--bg-hover));
}

.docs-page-head .logo.as-chip:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* A copy that did not happen, said where it was asked for rather than in a
   dialog over the top of the panel. */
.intro-trouble {
	margin: 0;
	padding: 4px 16px 10px;
	font-size: 12px;
	color: var(--danger);
}

/* A card mid-copy. Dimmed rather than removed, so the row does not reflow
   under the pointer that just pressed it. */
.intro-card:disabled {
	opacity: 0.55;
	cursor: default;
}

/* --------------------------------------------------------------------------
   A drawn graph with what it declares beside it.

   The panel goes on the left because it is read first: `fs` is declared and
   then used, and a reader following `fs.readFile` back to its module should
   travel the way the eye already goes.
   ----------------------------------------------------------------------- */
.docs-preview.graph.with-panel {
	display: grid;
	grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
	gap: 14px;
	align-items: start;
}

.docs-preview.graph.with-panel > figcaption {
	grid-column: 1 / -1;
}

/* The panel draws itself; this only stops it stretching to the graph's height,
   which would leave a tall empty box under three rows of modules. */
.graph-declares .docs-bar-frame {
	margin: 0;
}

/* One under the other once there is no room for two: a 240px panel beside a
   graph on a phone is two things too narrow to read instead of one. */
@media (max-width: 860px) {
	.docs-preview.graph.with-panel {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* A recent card is two controls: the project, and taking it off the list.

   The cross is the affordance the project menu had and the first version of
   this panel dropped — which left a root that had been deleted, or thrown away
   with the browser volume, sitting there failing every time it was pressed. */
.intro-card {
	position: relative;
}

.intro-card-open-it {
	display: flex;
	flex-direction: column;
	gap: 3px;
	width: 100%;
	padding: 0;
	border: 0;
	background: none;
	text-align: left;
	font: inherit;
	color: inherit;
	cursor: pointer;
}

.intro-forget {
	position: absolute;
	top: 6px;
	right: 6px;
	width: 20px;
	height: 20px;
	min-width: 0;
	padding: 0;
	opacity: 0;
	transition: opacity 90ms ease;
}

/* Shown on the card it belongs to, and always once the keyboard is on it —
   a control that only exists under a pointer is one a keyboard cannot reach. */
.intro-card:hover .intro-forget,
.intro-forget:focus-visible {
	opacity: 1;
}

/* A project that would not open. Marked rather than removed: taking it off the
   list is the reader's decision, and one they may not want if the drive it is
   on is simply not plugged in today. */
.intro-card.gone {
	border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

.intro-card-open.gone {
	color: var(--danger);
}

/* --------------------------------------------------------------------------
   A checkbox the page puts beside the thing it changes.

   The settings popover is for what a reader sets once and forgets; this is for
   a choice that only makes sense while looking at the page it belongs to.
   ----------------------------------------------------------------------- */
.docs-toggle {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	align-items: start;
	column-gap: 9px;
	row-gap: 2px;
	margin: 0 0 18px;
	padding: 10px 12px;
	border: 1px solid var(--border);
	border-radius: 6px;
	background: var(--bg-input);
	cursor: pointer;
}

.docs-toggle input {
	margin: 2px 0 0;
	cursor: pointer;
}

.docs-toggle-label {
	font-weight: 600;
	color: var(--fg);
}

.docs-toggle-hint {
	grid-column: 2;
	font-size: 12px;
	line-height: 1.5;
	color: var(--fg-muted);
}

/* What the answer hides.

   Only when it is explicitly off, which is the script having run and read the
   preference. With no JavaScript the attribute is never set and every fold is
   drawn — a reader who cannot toggle sees everything rather than a checkbox
   that does nothing and a history they cannot reach. */
:root[data-showprereleasenotes="off"] .docs-details[data-prerelease] {
	display: none;
}

/* --------------------------------------------------------------------------
   Phones, tablets and fingers.

   Last in the file on purpose: every rule here narrows one written further
   up, and a narrowing that loses on order does nothing. That is how the
   docs' outline stayed on screen below 1100px.
   ----------------------------------------------------------------------- */

/* A finger has no hover and no double-click, and iPadOS reads a quick second
   tap as "zoom the page". `manipulation` keeps pans and pinches and drops that
   one, so a double tap reaches `touch.ts` as the double-click it stands for.
   The callout is the iOS menu a long press raises over text, which would sit
   on top of the one Roswaal opens for the same press. */
.app,
.designer,
.docs-page {
	touch-action: manipulation;
	-webkit-touch-callout: none;
}

@media (pointer: coarse) {
	/* A long press is a right-click here, so a press on a tree row or a node
	   title must not start selecting its text first. Fields are left alone. */
	.app,
	.designer {
		-webkit-user-select: none;
		user-select: none;
	}

	.app :is(input, textarea, [contenteditable="true"], .cm-content),
	.designer :is(input, textarea, [contenteditable="true"], .cm-content) {
		-webkit-user-select: text;
		user-select: text;
	}

	/* Five pixels is a line for a mouse and a miss for a finger. The strip
	   drawn stays the same; only what catches the press grows. */
	.splitter.col {
		border-left-width: 9px;
		border-right-width: 9px;
		margin: 0 -9px;
	}
}

/* A finger held on empty canvas: drag for a marquee, lift for the menu. The
   ring says the hold has registered, since nothing else on screen changes. */
.canvas-hold {
	position: absolute;
	width: 44px;
	height: 44px;
	margin: -22px 0 0 -22px;
	border: 2px solid var(--select);
	border-radius: 50%;
	background: color-mix(in srgb, var(--select) 14%, transparent);
	pointer-events: none;
	animation: canvas-hold-in 140ms ease-out;
}

@keyframes canvas-hold-in {
	from { transform: scale(0.4); opacity: 0; }
}

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

/* Dragging by touch: see `touch.ts`. Held long enough, the item lifts to say
   a drag is armed; a label follows the finger once it moves. */
.touch-lifted {
	outline: 2px solid var(--select);
	outline-offset: -2px;
	border-radius: 5px;
	background: color-mix(in srgb, var(--select) 14%, transparent);
}

.touch-drag-ghost {
	position: fixed;
	z-index: 1000;
	pointer-events: none;
	transform: translate(-50%, calc(-100% - 18px));
	max-width: 240px;
	padding: 6px 10px;
	border: 1px solid var(--select);
	border-radius: 6px;
	background: var(--bg-panel);
	color: var(--fg);
	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* A drag image the source drew, such as a node from the node picker. Placed by
   the offset the source gave rather than centred over the finger. */
.touch-drag-image {
	position: fixed;
	z-index: 1000;
	pointer-events: none;
	opacity: 0.9;
	filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
}

.touch-drag-image svg {
	display: block;
}

@media (hover: none) and (pointer: coarse) {
	/* iOS's own drag would lift the same item at the same moment and take the
	   finger away from the one above. */
	[draggable="true"] {
		-webkit-user-drag: none;
	}

	/* A tapped button keeps `:hover` on iOS until something else is tapped,
	   so the last one pressed stayed painted as if a pointer were over it --
	   the Variables button, after its drawer was closed by tapping the graph.
	   A finger does not hover; the rest state is the only one. */
	button.tb:hover:not(:disabled):not(.on):not(.primary):not(.danger),
	select.tb:hover {
		background: var(--bg-input);
		border-color: var(--border);
	}

	button.tb.on:hover:not(:disabled) {
		background: color-mix(in srgb, var(--accent) 18%, var(--bg-input));
	}
}

/* ---------------------------------------------------- the editor, on a phone */

/* See `useCompact` in Workspace.tsx: below this the side docks leave the grid
   and slide over the graph, one at a time. */
.dock.drawer {
	position: absolute;
	top: 0;
	bottom: 0;
	z-index: 67;
	width: min(340px, 86vw);
	background: var(--bg-panel);
	box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
	overflow: auto;
	transition: transform 160ms ease-out, visibility 160ms;
}

.dock.drawer.left {
	left: 0;
	border-right: 1px solid var(--border-strong);
}

.dock.drawer.right {
	right: 0;
	border-left: 1px solid var(--border-strong);
}

.dock.drawer:not(.drawer-open) {
	visibility: hidden;
	box-shadow: none;
}

.dock.drawer.left:not(.drawer-open) { transform: translateX(-100%); }
.dock.drawer.right:not(.drawer-open) { transform: translateX(100%); }


.drawer-backdrop {
	position: relative;
	z-index: 66;
	background: rgba(0, 0, 0, 0.35);
}

.drawer-toggles {
	position: absolute;
	left: 10px;
	right: 10px;
	bottom: 10px;
	/* Above the drawers, so one tap moves from Project to Variables rather
	   than two. The drawers keep their last 56px clear for them. */
	z-index: 68;
	display: flex;
	justify-content: space-between;
	gap: 8px;
	pointer-events: none;
}

.drawer-toggles button.tb {
	pointer-events: auto;
	padding: 8px 12px;
	box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}

/* What is selected, above the drawer buttons: its own row, centred, so it
   never has to share a line with them on a phone. */
.drawer-toggles {
	flex-wrap: wrap;
	row-gap: 8px;
}

.touch-bar {
	flex-basis: 100%;
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 6px;
}

.touch-bar button.tb {
	pointer-events: auto;
	padding: 8px 12px;
	box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}

.touch-bar-group {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 6px;
}

/* One bar, as the graph's floating tools are one: the buttons sit flat on it
   and the bar carries the edge and the shadow. */
.touch-bar-group.unified {
	gap: 4px;
	padding: 6px 8px;
	border: 1px solid var(--border);
	border-radius: 8px;
	background: var(--bg-panel);
	box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
	pointer-events: auto;
}

.touch-bar-group.unified button.tb {
	border-color: transparent;
	background: transparent;
	box-shadow: none;
}

/* Where a tap leaves a button hovered, a flat bar keeps its buttons flat. */
@media (hover: none) {
	.touch-bar .touch-bar-group.unified button.tb:hover:not(:disabled):not(.on) {
		border-color: transparent;
		background: transparent;
	}
}

/* A 24px glyph in a 46px square: past the 44pt Apple asks a tap target to be.
   A size rather than padding, because `.tb.icon-only` fixes its buttons at 28px
   and padding inside that left the glyph two pixels from each edge. */
.touch-bar .touch-bar-group button.tb.icon-only {
	width: 46px;
	height: 46px;
	padding: 0;
	display: inline-grid;
	place-items: center;
	line-height: 0;
}

/* A group per side, so the buttons sit on the edge their drawers slide in
   from, whichever side has none. */
.drawer-group {
	display: flex;
	gap: 8px;
}

.dock.drawer {
	padding-bottom: 56px;
}

/* One panel to a drawer, and it has the whole height. */
.dock.drawer .panel {
	flex: 1;
	min-height: 0;
}

.dock.drawer .panel.panel-away {
	display: none;
}

.dock.drawer .panel-variables {
	flex: 1;
	max-height: none;
}

@media (prefers-reduced-motion: reduce) {
	.dock.drawer { transition: none; }
}

/* Node Design's node list, the same way: a drawer over the node editor on a
   phone or a tablet, opened from a bar that also names the node that is open. */
.pack-view.compact {
	position: relative;
	grid-template-columns: minmax(0, 1fr);
	grid-template-rows: auto minmax(0, 1fr);
}

/* Above the drawer and its scrim, so the button that opened it closes it. */
.pack-compact-bar {
	position: relative;
	z-index: 68;
	display: flex;
	align-items: center;
	gap: 10px;
	min-width: 0;
	padding: 6px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-panel);
}

/* The open node's switches, pushed to the end of the bar. */
.pack-compact-slot {
	margin-left: auto;
	display: flex;
	gap: 8px;
	flex: none;
}

.pack-compact-slot:empty {
	display: none;
}

.pack-compact-current {
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: var(--fg-muted);
}

/* Placed in the row under the bar, which makes that row its containing
   block: it and the scrim cover the editor and never the bar. */
.pack-view.compact .pack-nodes.drawer {
	grid-row: 2;
	grid-column: 1;
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	z-index: 67;
	width: min(320px, 86vw);
	box-sizing: border-box;
	border-right: 1px solid var(--border-strong);
	box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
	transition: transform 160ms ease-out, visibility 160ms;
}

.pack-view.compact .pack-nodes.drawer:not(.drawer-open) {
	visibility: hidden;
	box-shadow: none;
	transform: translateX(-100%);
}

.pack-scrim {
	grid-row: 2;
	grid-column: 1;
	position: absolute;
	inset: 0;
	z-index: 66;
	background: rgba(0, 0, 0, 0.35);
}

@media (prefers-reduced-motion: reduce) {
	.pack-view.compact .pack-nodes.drawer { transition: none; }
}

/* Node Design's node and its logic, one at a time on a phone or a tablet:
   see `split` in NodeEditor.tsx. */
.node-editor-views {
	display: flex;
	justify-content: center;
	padding: 6px 10px;
	border-bottom: 1px solid var(--border);
	background: var(--bg-panel);
}

/* With its title, hint and Luau/Nodes elsewhere, the logic's head holds at most
   a pure node's choice of output -- and is not drawn with nothing in it. */
.logic-head.logic-head-slim:not(:has(> *)) {
	display: none;
}

.node-editor.split .logic-splitter {
	display: none;
}

.node-editor.split.view-preview .node-logic,
.node-editor.split.view-logic .node-editor-stage {
	display: none;
}

.node-editor.split.view-logic .node-logic {
	flex: 1;
}

/* The graph over the Luau it compiles to, rather than beside it: side by side,
   an iPad in portrait gave each half too little width to read. */
.node-editor.split .logic-split {
	grid-template-columns: minmax(0, 1fr);
	grid-template-rows: minmax(0, 1fr) minmax(120px, 30%);
}

/* ----------------------------------------------------- dialogs, on a phone */

@media (max-width: 640px) {
	.docs-backdrop {
		padding: 8px;
	}

	/* The sections along the top rather than down the side: a 230px column of
	   five words left the page beside it a hundred pixels wide. */
	.settings .docs-body {
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: auto minmax(0, 1fr);
	}

	.settings .docs-nav {
		flex-direction: row;
		overflow-x: auto;
		border-right: 0;
		border-bottom: 1px solid var(--border);
		padding: 6px;
		gap: 4px;
	}

	.settings .settings-tab {
		flex: none;
		width: auto;
		white-space: nowrap;
	}

	.settings .settings-tab-sub {
		display: none;
	}

	.settings-page {
		padding: 14px 14px 32px;
	}

	.setting {
		grid-template-columns: minmax(0, 1fr);
		gap: 8px;
	}

	.setting-control {
		justify-content: flex-start;
	}
}

/* ------------------------------------------------------ the docs, narrow */

.docs-nav-check {
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

/* `.tb` in both, to outrank `.docs-page-head button.tb`, which makes every
   header button a flex box -- and made this one visible everywhere, with or
   without the script it needs. */
.docs-page-head .tb.docs-nav-toggle,
.docs-page-head .tb.docs-search-toggle {
	display: none;
}

.docs-page-head label.tb {
	cursor: pointer;
	background: var(--bg-input);
	border: 1px solid var(--border);
	border-radius: 5px;
	padding: 4px 9px;
	height: 29px;
	box-sizing: border-box;
	align-items: center;
}

/* The contents as a drawer: on anything narrow, and on a touch screen of any
   size. A column of links held open beside the page is a desktop's way of
   reading; on an iPad it is a third of the screen spent on a list you use
   once per page. Keep the query in step with `COMPACT_QUERY` in panels.ts,
   which asks the same question of the editor. */
@media (max-width: 760px), (hover: none) and (pointer: coarse) {
	.docs-page-head .tb.docs-nav-toggle,
	.docs-page-head .tb.docs-search-toggle:not([hidden]) {
		display: inline-flex;
	}

	/* The nav is out of the grid, so the reading column is the first track.
	   The outline keeps its own track wherever it is shown. */
	.docs-page .docs-body {
		grid-template-columns: minmax(0, 1fr);
		position: relative;
	}

	.docs-page .docs-nav {
		display: none;
	}

	.docs-page.nav-open .docs-nav,
	.docs-page:has(.docs-nav-check:checked) .docs-nav {
		display: flex;
		position: absolute;
		top: 0;
		bottom: 0;
		left: 0;
		width: min(360px, 100%);
		box-sizing: border-box;
		z-index: 5;
		background: var(--bg-panel);
		border-right: 1px solid var(--border-strong);
		box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
	}

	/* Tapping the page beside the drawer puts it away. A label for the same
	   checkbox, so the static site needs no script for it. */
	.docs-page.nav-open .docs-nav-scrim,
	.docs-page:has(.docs-nav-check:checked) .docs-nav-scrim {
		display: block;
		position: absolute;
		inset: 0;
		z-index: 4;
		background: rgba(0, 0, 0, 0.35);
		cursor: pointer;
	}

	.docs-page:has(.docs-nav-check:checked) .docs-nav-toggle,
	.docs-page.nav-open .docs-nav-toggle {
		background: color-mix(in srgb, var(--accent) 18%, var(--bg-input));
		border-color: var(--accent);
		color: var(--accent);
	}
}

@media (hover: none) and (pointer: coarse) and (min-width: 1101px) {
	.docs-page .docs-body { grid-template-columns: minmax(0, 1fr) 210px; }
}

@media (hover: none) and (pointer: coarse) and (min-width: 1500px) {
	.docs-page .docs-body { grid-template-columns: minmax(0, 1fr) 240px; }
}

.docs-nav-scrim {
	display: none;
}

@media (max-width: 760px) {
	.docs-page-head {
		flex-wrap: wrap;
		row-gap: 6px;
		padding: 8px 10px;
	}

	.docs-page .docs-content {
		padding: 18px 16px 60px;
	}
}

/* Arrow keys, Enter and Escape, on a screen with no keys. */
@media (hover: none) and (pointer: coarse) {
	.docs-palette-foot {
		display: none;
	}
}

/* iOS zooms the page in on any field under 16px when it takes focus, and the
   docs let a page zoom. Focusing the search did exactly that: the palette ran
   off the right-hand edge and the page stayed zoomed after it closed. */
@media (pointer: coarse) {
	.docs-static :is(input, select, textarea),
	.docs-palette-field input {
		font-size: 16px;
	}
}

/* Over everything on the page, whatever it has positioned. The app's copies
   set this inline; the static site's did not set it at all. */
.docs-palette-backdrop,
.docs-static .docs-backdrop {
	z-index: 100;
}

/* No grey flash where a tap lands on a picture that lights what was tapped:
   the lit state is the answer, and iOS drawing its own box over it first
   read as the picture flickering. */
.docs-bar.linked,
.docs-map.linked {
	-webkit-tap-highlight-color: transparent;
}

@media (max-width: 560px) {
	/* Two rows on purpose: the mark and the contents, then the ways out. Left
	   to wrap by itself, the header dropped whichever button ran out of room. */
	.docs-page-head .grow {
		flex-basis: 100%;
		height: 0;
	}

	.docs-page-head .logo .version {
		display: none;
	}
}

/* --------------------------------------------------------------------------
   Dropdowns, drawn by us everywhere.

   A <select> left to the browser is three different controls: Chrome on
   Windows draws something close to the app's buttons, and Safari on a Mac
   draws its own -- a double chevron, its own height, its padding ignored --
   so the tools over the graph came out a different size from the buttons
   beside them, and from the picture of that bar in the docs. This is the
   box `select.tb` already paints, with the triangle `docs-bar-select` draws,
   made the same way the node's own dropdowns make theirs.

   Last in the file, and specific enough, because every `background:`
   shorthand above -- `select.tb:hover` among them -- resets the image. The
   list a select opens is still the system's, which is right: it is the one
   part a browser does well on every platform.
   ----------------------------------------------------------------------- */
select:not(.literal):not([multiple]) {
	-webkit-appearance: none;
	appearance: none;
	padding-right: 24px;
	background-image:
		linear-gradient(45deg, transparent 50%, var(--fg-muted) 50%),
		linear-gradient(135deg, var(--fg-muted) 50%, transparent 50%);
	background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%;
	background-size: 4px 4px, 4px 4px;
	background-repeat: no-repeat;
	text-overflow: ellipsis;
}

/* --------------------------------------------------------------------------
   A window as a diagram, on The Interface. See `src/core/docs/layouts.ts`.

   Boxes in the places the real regions occupy, each numbered as the legend
   under it numbers them. Recognisable rather than faithful: the bars are drawn
   control by control on Toolbars, and a picture of every button at this size
   would be a picture of nothing in particular.
   ----------------------------------------------------------------------- */
.docs-layout-frame {
	display: grid;
	gap: 3px;
	padding: 6px;
	margin: 14px;
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	background: var(--bg-app);
	font-size: 11px;
	line-height: 1.3;
}

.docs-layout-frame.device-desktop {
	aspect-ratio: 16 / 10;
}

/* A tablet held sideways: its bezel, so the two tabs are two devices at a
   glance rather than two sizes of one box. */
.docs-layout-frame.device-tablet {
	aspect-ratio: 4 / 3;
	max-width: 620px;
	margin-inline: auto;
	border: 10px solid var(--border-strong);
	border-radius: 22px;
}

/* A phone held upright: taller than wide, and narrow enough to sit in the
   reading column beside its legend's first rows rather than fill it. */
.docs-layout-frame.device-phone {
	aspect-ratio: 9 / 19;
	font-size: 9.5px;
	max-width: 280px;
	margin-inline: auto;
	border: 9px solid var(--border-strong);
	border-radius: 28px;
}

.docs-layout-region {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	align-content: flex-start;
	gap: 4px 6px;
	min-width: 0;
	min-height: 0;
	padding: 5px 7px;
	overflow: hidden;
	border: 1px solid var(--border);
	border-radius: 5px;
	background: var(--bg-panel);
	color: var(--fg-muted);
}

.docs-layout-region.kind-bar {
	align-items: center;
	align-content: center;
}

/* The graph, drawn as the graph's own grid so it reads as the canvas. */
.docs-layout-region.kind-canvas {
	background-color: var(--bg-canvas);
	background-image:
		linear-gradient(to right, var(--grid-fine) 1px, transparent 1px),
		linear-gradient(to bottom, var(--grid-fine) 1px, transparent 1px);
	background-size: 14px 14px;
	align-content: flex-end;
}

.docs-layout-region.number-middle-end {
	align-content: center;
	justify-content: flex-end;
}

/* Over the graph: in the graph's cells, sized to what is on it, raised. */
.docs-layout-region.kind-float {
	flex-wrap: nowrap;
	z-index: 2;
	align-self: center;
	align-items: center;
	margin: 0 6px;
	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}

.docs-layout-region.kind-float.place-start { justify-self: start; }
.docs-layout-region.kind-float.place-center { justify-self: center; }
.docs-layout-region.kind-float.place-end { justify-self: end; }

/* A panel slid out over the graph: above it, with the shadow the real one has. */
.docs-layout-region.kind-drawer {
	z-index: 3;
	border-color: var(--border-strong);
	box-shadow: -8px 0 20px rgba(0, 0, 0, 0.3);
}

.docs-layout-num {
	display: inline-grid;
	place-items: center;
	flex: none;
	width: 17px;
	height: 17px;
	border-radius: 50%;
	background: var(--accent);
	color: var(--bg-panel);
	font-size: 10px;
	font-weight: 700;
	line-height: 1;
}

.docs-layout-icons,
.docs-layout-chips {
	display: inline-flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px;
}

.docs-layout-icons.docs-layout-end {
	margin-left: auto;
}

.docs-layout-chips > span,
.docs-layout-chip {
	padding: 1px 6px;
	border: 1px solid var(--border);
	border-radius: 4px;
	background: var(--bg-input);
	white-space: nowrap;
}

.docs-bar.docs-layout .docs-bar-picture {
	overflow: visible;
}

.docs-layout-frame [data-control] {
	cursor: pointer;
}

.docs-layout-frame [data-control].lit {
	box-shadow: 0 0 0 2px var(--accent);
}

.docs-layout-legend {
	list-style: none;
}

/* The number beside the name, as it sits in the picture; the when-it-is-there
   line keeps its own row under both. */
.docs-layout-legend .docs-bar-name {
	flex-direction: row;
	flex-wrap: wrap;
	align-items: center;
	column-gap: 7px;
}

.docs-layout-legend .docs-bar-where {
	flex-basis: 100%;
	padding-left: 24px;
}

/* A phone draws the diagram small: the numbers carry it, and the names are
   in the legend beneath. */
@media (max-width: 560px) {
	.docs-layout-frame .docs-layout-chips,
	.docs-layout-frame .docs-layout-icons {
		display: none;
	}

	.docs-layout-frame {
		margin: 8px;
	}
}

/* --------------------------------------------------------------------------
   Bars short of room: words become glyphs.

   The top bar and the graph's tools are one row each on a computer. On an iPad
   in portrait they wrapped into two, and the graph lost the height of both.
   So where they are short of room the buttons that carry a word -- Compile
   project, Straighten, Compile script -- show only their glyph, and the
   Compile caption goes. The word stays as the button's accessible name, and
   the title says it on a long press.
   ----------------------------------------------------------------------- */
.tb-icon-when-narrow {
	display: none;
}

/* The top bar measures the window: it always spans it. */
@media (max-width: 899px) {
	.toolbar .group-label,
	.toolbar .logo .version {
		display: none;
	}

	.toolbar .tb-collapsible .tb-icon-when-narrow {
		display: inline-block;
	}

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

/* The graph's tools measure the graph, which is narrower than the window
   whenever a panel is docked beside it. */
.centre {
	container: centre / inline-size;
}

@container centre (max-width: 819px) {
	.floating-tools .tb-collapsible .tb-icon-when-narrow {
		display: inline-block;
	}

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

/* The graph's settings behind a button each, on a phone: see `Popout` in
   Toolbar.tsx. The panel hangs under its button, over the graph. */
.tool-popout {
	position: relative;
}

.tool-popout-panel {
	position: absolute;
	top: calc(100% + 8px);
	left: 0;
	z-index: 20;
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 190px;
	padding: 8px;
	border: 1px solid var(--border-strong);
	border-radius: 8px;
	background: var(--bg-panel);
	box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
}

.tool-popout-end .tool-popout-panel {
	left: auto;
	right: 0;
}

.tool-popout-panel select.tb {
	width: 100%;
}

/* A menu of actions: the projects panel's Project button. Each is a row. */
.tool-popout-up .tool-popout-panel {
	min-width: 170px;
	gap: 2px;
	padding: 5px;
}

.tool-popout-up .tool-popout-panel > button.tb {
	justify-content: flex-start;
	width: 100%;
	border-color: transparent;
	background: none;
}

.tool-popout-up .tool-popout-panel > button.tb:hover {
	background: var(--bg-hover);
}

.tool-popout-up .tool-popout-panel > .tool-popout-rule {
	height: 1px;
	margin: 3px 4px;
	background: var(--border);
}

/* A phone's graph tools, drawn a little tighter so the three groups share one
   row: with two of them behind buttons they are about 400px wide, and a phone
   has 373 once the edges are taken. */
@media (max-width: 699px) {
	.floating-tools {
		left: 6px;
		right: 6px;
		gap: 5px;
	}

	.floating-tools .tool-group {
		gap: 4px;
		padding: 4px;
	}
}

/* The compile target, with Lune's warning after its name rather than the word
   "experimental" in it: drawn over the select, clear of its chevron, and
   passing taps through to it. */
.target-pick {
	position: relative;
	display: inline-flex;
}

.target-pick select.doc-target.lune {
	padding-right: 44px;
}

.target-pick .target-warn {
	position: absolute;
	right: 24px;
	top: 50%;
	transform: translateY(-50%);
	color: var(--warning);
	pointer-events: none;
}

.target-warn-inline {
	color: var(--warning);
}

/* Node Design's node tools, folded on a phone. The type chips wrap into a
   block rather than a line; the pin counts and the node's kind keep a row. */
.pin-chip-grid {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	max-width: 260px;
}

.pin-counts {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
}

/* Words a screen reader needs and the row has no room to show. */
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* A window's header on a phone: a button that carries a word beside its icon
   keeps only the icon, so the header holds one row -- Node Design's Settings
   had wrapped under the rest. */
@media (max-width: 699px) {
	.docs-page-head .tb-collapsible .tb-label {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}
}

/* Node Design's logic graph with its Luau put away: the graph has the room. */
.logic-split.luau-hidden,
.node-editor.split .logic-split.luau-hidden {
	grid-template-columns: minmax(0, 1fr);
	grid-template-rows: minmax(0, 1fr);
}

/* A phone's picture is small: its glyphs are too, so a row holds what the real
   row holds. */
.docs-layout-frame.device-phone .icon {
	width: 11px;
	height: 11px;
}

.docs-layout-frame.device-phone .docs-layout-region {
	gap: 3px 4px;
	padding: 4px 5px;
}

.docs-layout-frame.device-phone .docs-layout-num {
	width: 14px;
	height: 14px;
	font-size: 9px;
}

.docs-layout-frame.device-phone .docs-layout-region.kind-float {
	margin: 0 3px;
}

.docs-layout-frame.device-phone .docs-layout-chips > span,
.docs-layout-frame.device-phone .docs-layout-chip {
	padding: 0 4px;
}

.docs-layout-frame.device-phone .docs-layout-icons {
	gap: 3px;
}

/* A bar drawn at a tablet's or a phone's width: the real one's width, so it
   wraps where the real one does. Only these wrap; a computer's bar is drawn as
   one line whatever the page's width. */
.docs-bar-frame.device-tablet {
	max-width: 760px;
}

.docs-bar-frame.device-phone {
	max-width: 409px;
}

.docs-bar-frame.device-tablet .toolbar,
.docs-bar-frame.device-phone .toolbar,
.docs-bar-frame.device-tablet .docs-page-head,
.docs-bar-frame.device-phone .docs-page-head,
.docs-bar-frame.device-tablet .floating-tools,
.docs-bar-frame.device-phone .floating-tools {
	flex-wrap: wrap;
}

.docs-bar-frame .docs-bar-break {
	flex-basis: 100%;
	height: 0;
}


/* A walkthrough: one drawing of the screen, the steps under it lit in turn.
   See `src/app/docsWalk.ts`. Without its script the first drawing shows over
   the ordinary numbered list, so nothing here hides a step. */
.docs-walk {
	margin: 16px 0;
	border: 1px solid var(--border);
	border-radius: 8px;
	overflow: hidden;
	background: var(--bg-panel);
}

.docs-walk-window {
	overflow-x: auto;
	background: var(--bg-app);
}

.docs-walk-frame[hidden] {
	display: none;
}

/* A drawing keeps its real shape -- one row, words unbroken -- and the script
   scales it to fit the window instead. */
.docs-walk-frame {
	width: max-content;
}

.docs-walk-frame .tb {
	white-space: nowrap;
}

/* Two bars in one frame, as the Project menu sits over the panel's footer. */
.docs-walk-frame .docs-bar-frame + .docs-bar-frame {
	padding-top: 0;
}

.docs-walk-nav {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 12px;
	border-top: 1px solid var(--border);
	border-bottom: 1px solid var(--border);
}

.docs-walk:not(.walk-on) .docs-walk-nav {
	display: none;
}

.docs-walk-count {
	flex: 1;
	text-align: center;
	font-size: 12px;
	color: var(--fg-faint);
}

.docs-walk-steps {
	margin: 0;
	padding: 10px 14px 12px 36px;
}

.docs-walk-steps > li {
	padding: 6px 8px;
	border-radius: 6px;
}

.walk-on .docs-walk-steps > li {
	cursor: pointer;
	transition: background-color 120ms ease, color 120ms ease;
}

.walk-on .docs-walk-steps > li.walk-next {
	color: var(--fg-faint);
}

.walk-on .docs-walk-steps > li.walk-done {
	color: var(--fg-faint);
}

.walk-on .docs-walk-steps > li.walk-done::marker {
	color: var(--ok);
}

.walk-on .docs-walk-steps > li.walk-current {
	color: var(--fg);
	background: color-mix(in srgb, var(--accent) 12%, transparent);
	box-shadow: inset 2px 0 0 var(--accent);
}

/* The control to press, ringed, and pressable: it moves to the next step. */
.docs-walk-frame .walk-here {
	box-shadow: 0 0 0 2px var(--accent);
	animation: walk-here 1.6s ease-in-out infinite;
	cursor: pointer;
}

@keyframes walk-here {
	50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 35%, transparent); }
}

@media (prefers-reduced-motion: reduce) {
	.docs-walk-frame .walk-here {
		animation: none;
	}
}

/* An open menu, drawn where it sits rather than hung from a button. */
.docs-bar-frame.popmenu > .docs-menu-shot {
	position: static;
	display: inline-flex;
	gap: 2px;
	padding: 5px;
	min-width: 170px;
	box-shadow: none;
}

.docs-menu-shot > button.tb {
	justify-content: flex-start;
	width: 100%;
	border-color: transparent;
	background: none;
}

/* Home's arrow points back, as the projects panel turns it. */
.docs-bar-frame [data-control="home"] .icon {
	transform: rotate(90deg);
}

/* A text box, with its placeholder. */
.docs-bar-field {
	display: inline-flex;
	align-items: center;
	min-width: 220px;
	color: var(--fg-faint);
	cursor: text;
}

/* A drawn Inspector's Luau, as the real one shows it: code, in the mono face. */
.docs-bar-frame.inspector .docs-bar-code {
	font-family: var(--mono);
	font-size: 11px;
}

/* The code editor's hover: a signature, a sentence, and where to read more.
   The signature is a band of its own: code, in the code editor's face, bold,
   on a shade darker than the body, so it reads as the thing and the sentence
   under it as what is said about it. */
.luau-hover {
	max-width: 400px;
	border-radius: 6px;
	overflow: hidden;
	font-size: 12px;
	line-height: 1.45;
	color: var(--fg);
}

.luau-hover-head {
	padding: 7px 10px;
	background: color-mix(in srgb, var(--bg-canvas) 70%, var(--bg-panel));
	border-bottom: 1px solid var(--border);
}

.luau-hover-head:last-child {
	border-bottom: 0;
}

/* What kind of name it is, under the name: quieter than the code, which is
   the thing the hover is about. */
.luau-hover-role {
	display: block;
	margin-top: 2px;
	font-size: 11px;
	color: var(--fg-faint);
}

.luau-hover code {
	display: block;
	font-family: "Cascadia Mono", Consolas, ui-monospace, monospace;
	font-size: 13px;
	font-weight: 700;
	color: var(--fg);
	white-space: pre-wrap;
}

.luau-hover p {
	margin: 0;
	padding: 7px 10px 0;
	color: var(--fg-muted);
}

.luau-hover a {
	display: inline-block;
	margin: 6px 10px 8px;
	color: var(--accent);
}


/* The call being typed, above the cursor: its parameters, the one the cursor
   is on picked out. Code, like the hover's signature line. */
.luau-signature {
	padding: 5px 9px;
	border-radius: 6px;
}

.luau-signature code {
	font-family: "Cascadia Mono", Consolas, ui-monospace, monospace;
	font-size: 12px;
	color: var(--fg-muted);
	white-space: pre-wrap;
}

.luau-signature .param.active {
	color: var(--fg);
	font-weight: 700;
	text-decoration: underline;
	text-underline-offset: 3px;
}
