/* ============================================================================
   Edwin UI Kit v1 — ui.css
   One canonical implementation per component job. The inventory of 2026-08-17
   found 1,558 distinct classes doing about 20 jobs; this file is the 20.

   RULES THIS FILE KEEPS ON ITSELF
     1. Zero colour literals. Every value is var(--dw-*). A rule the exemplar
        breaks is a rule nobody follows, so `grep -E '#[0-9a-fA-F]{3}'` must
        come back empty on this file too.
     2. Flat. No box-shadow other than var(--dw-shadow) (which is `none` by
        default and exists so a skin can opt in). No lit gradients.
     3. Every transition reads a duration token, so --dw-motion-scale: 0
        silences the whole kit.

   NAMING
     .dw-<component> for the block, .dw-<component>-<part> for parts, and
     .is-<state> for variants. A page may EXTEND a kit class with layout and
     spacing; it may never re-colour one.
   ========================================================================= */

/* ---------------------------------------------------------------- ground */
/* THE PAGE GROUND, and until now the kit never painted it.
   --dw-ground was read by exactly one kit rule, .dw-app, which NOTHING in
   the tree wears — I first wrote "13 of 55 pages" here and a reviewer
   grepped it: the count is zero. The surface everyone actually sees came from
   modern-refresh.css:127, `background: var(--background-color)` — a LEGACY
   token — so the default page stayed #F4F7F9 blue-grey while the kit said
   #FAF9F6 warm off-white, and deleting the legacy bridge did not change it
   because the bridge was never in that path. Found by looking at a
   screenshot, after every measurement I took said the palette had moved.

   The shorthand is deliberate: it also resets background-image, which kills
   a radial gradient that block layered on top, keyed to rgba of the RETIRED
   accent #E8882E. Direction A is flat and forbids lit gradients, so that
   wash was against the system twice over.

   Specificity is unchanged (0,0,1), so a skin's own
   html[data-skin='x'] body rule still wins; this only beats the base layer
   because ui.css loads after it. */
body {
	background: var(--dw-ground);
	color: var(--dw-text);
}

.dw-app {
	background: var(--dw-ground);
	color: var(--dw-text);
	font-family: var(--dw-font);
	font-size: var(--dw-size-base);
	line-height: var(--dw-leading);
	-webkit-font-smoothing: antialiased;
}

/* The ambient layer. One element, fixed, inert, above the ground and below
   everything else. A skin retimes or removes it through the grain tokens. */
.dw-grain {
	position: fixed;
	inset: 0;
	z-index: 1;
	pointer-events: none;
	opacity: var(--dw-grain-opacity);
	mix-blend-mode: var(--dw-grain-blend);
	background-image: var(--dw-grain-image);
}

/* ------------------------------------------------------------------ type */
.dw-eyebrow {
	display: inline-flex;
	align-items: center;
	gap: var(--dw-space-3);
	margin: 0 0 var(--dw-space-4);
	font-size: var(--dw-size-eyebrow);
	font-weight: var(--dw-weight-strong);
	letter-spacing: var(--dw-track-eyebrow);
	text-transform: uppercase;
	color: var(--dw-accent-text);
}
.dw-eyebrow::before {
	content: "";
	display: block;
	flex: none;
	width: 26px;
	height: 1px;
	background: currentColor;
	opacity: .55;
}

.dw-h1, .dw-h2, .dw-h3 { font-family: var(--dw-font-display); }

.dw-h1 {
	margin: 0 0 var(--dw-space-2);
	font-size: var(--dw-size-h1);
	font-weight: var(--dw-weight-strong);
	line-height: var(--dw-leading-tight);
	letter-spacing: var(--dw-track-h1);
	color: var(--dw-text);
	text-wrap: balance;
}
.dw-h2 {
	margin: 0 0 var(--dw-space-2);
	font-size: var(--dw-size-h2);
	font-weight: var(--dw-weight-strong);
	line-height: 1.12;
	letter-spacing: var(--dw-track-h2);
	color: var(--dw-text);
	text-wrap: balance;
}
.dw-h3 {
	margin: 0 0 var(--dw-space-2);
	font-size: var(--dw-size-h3);
	font-weight: var(--dw-weight-strong);
	letter-spacing: -.012em;
	color: var(--dw-text);
}
.dw-lede {
	margin: 0;
	font-size: var(--dw-size-lede);
	line-height: 1.42;
	letter-spacing: var(--dw-track-lede);
	color: var(--dw-text);
	max-width: var(--dw-prose-max);
}
/* max-width is load-bearing, not typographic. Without it the title block in a
   .dw-pagehead claims the full row and the actions wrap to a second line —
   measured at titleW 1116 = headW, on two separate pages independently. 47
   pages use the pagetitle-sub this replaces. */
.dw-sub { margin: 0; color: var(--dw-muted); max-width: var(--dw-prose-max); }
.dw-prose { color: var(--dw-muted); max-width: var(--dw-prose-max); }
/* Text that arrived with line breaks and must keep them: a pasted brief, a
   stored note. The only alternative in the kit was .dw-code, which is a
   monospace block and therefore asserts the content is code when it is not. */
.dw-prose.is-pre { white-space: pre-wrap; }

/* RENDERED MARKDOWN. .dw-prose was a colour and a measure, which is enough
   for a paragraph and not for a brief: a page rendering markdown had to
   restate seven rules privately. These are the elements a renderer actually
   emits, and nothing else. */
.dw-prose h2, .dw-prose h3, .dw-prose h4, .dw-prose h5 {
	color: var(--dw-text);
	font-weight: var(--dw-weight-strong);
	letter-spacing: -.012em;
	margin: var(--dw-space-5) 0 var(--dw-space-2);
}
.dw-prose h2 { font-size: var(--dw-size-h3); }
.dw-prose h3 { font-size: var(--dw-size-base); }
.dw-prose h4, .dw-prose h5 { font-size: var(--dw-size-small); }
.dw-prose > :first-child { margin-top: 0; }
.dw-prose > :last-child { margin-bottom: 0; }
.dw-prose blockquote {
	margin: var(--dw-space-4) 0;
	padding-left: var(--dw-space-4);
	border-left: 2px solid var(--dw-line-strong);
	color: var(--dw-faint);
}
.dw-prose pre {
	padding: var(--dw-space-3);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	font-family: var(--dw-font-mono);
	font-size: var(--dw-size-micro);
	overflow-x: auto;
}

/* A word-level diff. The washes existed; the component did not. */
.dw-prose ins, .dw-ins { background: var(--dw-ok-wash); color: var(--dw-ok); text-decoration: none; }
.dw-prose del, .dw-del { background: var(--dw-bad-wash); color: var(--dw-bad); text-decoration: line-through; }

/* --------------------------------------------------------- page header --
   Formalises `pagetitle`, which the inventory found on 54 of 56 pages. It
   already exists in practice; this only gives it a contract. */
.dw-pagehead {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: var(--dw-space-5);
	flex-wrap: wrap;
	padding-bottom: var(--dw-space-5);
	margin-bottom: var(--dw-space-5);
	border-bottom: var(--dw-border) solid var(--dw-line);
}
.dw-pagehead-actions { display: flex; gap: var(--dw-space-2); flex-wrap: wrap; }

/* ---------------------------------------------------------------- toolbar
   Shipped in the July kit and used on zero pages, because nothing forced it.
   It is a slot in the list archetype now, not an option. */
.dw-toolbar {
	display: flex;
	align-items: center;
	gap: var(--dw-space-2);
	flex-wrap: wrap;
	margin-bottom: var(--dw-space-5);
}
.dw-toolbar-grow { flex: 1 1 auto; }

/* A control bar that is honestly more than one row. One page carries 29
   controls in 6 rows and 8 more have three or more selects. is-stacked only
   opens up the row gap and aligns on the baseline of the controls; the real
   work is done by the GROUP.

   THE GROUP IS THE POINT. A wrapping flex row breaks between any two children,
   so a bar of loose <label> and <select> elements strands "From" at the end of
   one line and its date picker at the start of the next. A group is a flex
   item that does not wrap inside itself, so the pair moves as one object. */
.dw-toolbar.is-stacked {
	align-items: flex-end;
	row-gap: var(--dw-space-3);
}
.dw-toolbar-group {
	display: flex;
	align-items: center;
	gap: var(--dw-space-2);
	flex: 0 1 auto;
}
.dw-toolbar-group > label {
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	color: var(--dw-faint);
	white-space: nowrap;
}

/* ----------------------------------------------------------------- button
   Two full button systems coexist today across 2,225 uses. This is the one. */
.dw-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--dw-space-2);
	padding: 8px 17px;
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-pill);
	background: transparent;
	color: var(--dw-text);
	font: inherit;
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	letter-spacing: -.005em;
	line-height: 1.2;
	cursor: pointer;
	box-shadow: var(--dw-shadow);
	transition: background var(--dw-dur-fast) var(--dw-ease),
		border-color var(--dw-dur-fast) var(--dw-ease),
		color var(--dw-dur-fast) var(--dw-ease);
}
.dw-btn:hover { background: var(--dw-surface-hover); }
.dw-btn:focus-visible {
	outline: 2px solid var(--dw-focus);
	outline-offset: 2px;
}
.dw-btn[disabled], .dw-btn.is-disabled {
	opacity: .45;
	cursor: not-allowed;
}

/* One solid fill. No gradient, no glow: that is what makes it survive on a
   skin whose accent is nothing like amber. */
.dw-btn.is-primary {
	background: var(--dw-accent);
	border-color: var(--dw-accent);
	color: var(--dw-accent-ink);
}
.dw-btn.is-primary:hover { background: var(--dw-accent); border-color: var(--dw-accent); opacity: .88; }
.dw-btn.is-ghost { border-color: transparent; color: var(--dw-muted); }
.dw-btn.is-ghost:hover { color: var(--dw-text); background: var(--dw-surface-hover); }
.dw-btn.is-danger { border-color: var(--dw-bad); color: var(--dw-bad); }
.dw-btn.is-danger:hover { background: var(--dw-bad-wash); }

/* The counterpart is-danger never had. An irreversible Approve/Deny pair had
   nowhere to put the affirmative, so pages either made Approve primary — which
   is the accent, and the accent means "the one action on this screen", not
   "the safe one" — or left it looking identical to Cancel. Outline, not fill:
   an Approve that shouts is an Approve that gets clicked by accident. */
.dw-btn.is-ok { border-color: var(--dw-ok); color: var(--dw-ok); }
.dw-btn.is-ok:hover { background: var(--dw-ok-wash); }

/* A PRESSED TOGGLE — a pin, a star, a filter that stays on. Distinct from
   .is-primary, which means "the one action on this screen". Pair it with
   aria-pressed; the outline alone tells a screen reader nothing. */
.dw-btn[aria-pressed="true"], .dw-btn.is-pressed {
	background: var(--dw-accent-wash);
	border-color: var(--dw-accent);
	color: var(--dw-accent-text);
}
.dw-btn.is-sm { padding: 5px 12px; font-size: var(--dw-size-micro); }

/* A BUTTON THAT READS AS TEXT. Not a link: it goes nowhere, it acts. And not a
   .dw-btn either, because a pill sitting where a value belongs is chrome the
   eye has to parse before it can read the row. The kit had no answer, so the
   first page to need one reached for `.dw-btn.is-ghost` and got 12px of
   padding that pushed the label out of the column its neighbours line up in.

   Inherits its size so it can sit inside a value row, a note or a table cell
   and match whatever is around it. */
.dw-textbtn {
	display: inline; padding: 0; border: 0; background: none;
	font: inherit; color: var(--dw-accent-text);
	text-align: inherit; cursor: pointer;
	text-decoration: underline; text-underline-offset: 2px;
	text-decoration-color: var(--dw-line-strong);
	transition: text-decoration-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-textbtn:hover { text-decoration-color: currentColor; }
.dw-textbtn:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }
.dw-textbtn.is-quiet { color: var(--dw-muted); }
.dw-textbtn.is-quiet:hover { color: var(--dw-text); }

/* A CONTROL THAT IS WORKING. 47 sites hand-roll save-innerHTML, swap in a
   spinner, then restore — which throws the label away, so the button collapses
   to spinner-width and the whole row reflows on every save, twice.

   This composes with .dw-spinner instead of replacing the content: leave the
   label alone, add a <span class="dw-spinner"> inside, add this class. The gap
   between them is already the button's own. pointer-events is the part that
   was actually load-bearing in those 47 hand-rolls — they were guarding
   against the double submit, not decorating. Pair it with aria-busy="true";
   opacity alone tells a screen reader nothing. */
.dw-btn.is-busy { pointer-events: none; opacity: .7; }
.dw-btn.is-busy .dw-spinner { flex: none; }

/* Icon-only. Found on the first real page: agent-list puts SEVEN of these on
   every card, and the tree has them as `.btn.btn-icon`. Square rather than
   pill so a row of them reads as a toolbar instead of a row of lozenges, and
   sized from the icon token so it tracks whatever a skin does to icons. */
.dw-btn.is-icon {
	padding: 0;
	width: 30px;
	height: 30px;
	border-radius: var(--dw-radius-sm);
	border-color: transparent;
	color: var(--dw-muted);
	font-size: var(--dw-icon-size);
}
.dw-btn.is-icon:hover { color: var(--dw-text); background: var(--dw-surface-hover); }
.dw-btn.is-icon.is-danger { color: var(--dw-bad); }

/* ------------------------------------------------------------------ chip */
.dw-chip {
	display: inline-flex;
	align-items: center;
	gap: var(--dw-space-1);
	padding: 6px 13px;
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius-pill);
	background: transparent;
	color: var(--dw-muted);
	font: inherit;
	font-size: var(--dw-size-micro);
	cursor: pointer;
	transition: color var(--dw-dur-fast) var(--dw-ease),
		background var(--dw-dur-fast) var(--dw-ease),
		border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-chip:hover { color: var(--dw-text); border-color: var(--dw-line-strong); }
/* RADIO-BACKED, so a single-choice chip group needs no JS to stay in sync.
   Bootstrap's .btn-check:checked + .btn does not reach .dw-chip, so three
   sites were mirroring the state by hand — which means three places to
   forget. The real input keeps arrow-key navigation and the group semantics
   that a div cannot have. */
.dw-chip-input { position: absolute; opacity: 0; pointer-events: none; }
.dw-chip-input:checked + .dw-chip,
.dw-chip.is-on {
	background: var(--dw-accent);
	border-color: var(--dw-accent);
	color: var(--dw-accent-ink);
	font-weight: var(--dw-weight-strong);
}
.dw-chip:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }

/* An APPLIED filter, which is a different thing from an available one: it
   states a constraint currently narrowing the list, and it has to be
   removable. agent-list built this as `.btn-close-filter`; it belongs here so
   the other 23 list pages do not each invent it. */
.dw-chip.is-applied {
	background: var(--dw-accent-wash);
	border-color: transparent;
	color: var(--dw-accent-text);
	cursor: default;
	padding-right: 6px;
}
.dw-chip-x {
	appearance: none;
	border: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	line-height: 1;
	padding: 2px 5px;
	margin-left: 2px;
	border-radius: var(--dw-radius-sm);
	cursor: pointer;
	opacity: .7;
	transition: opacity var(--dw-dur-fast) var(--dw-ease),
		background var(--dw-dur-fast) var(--dw-ease);
}
.dw-chip-x:hover { opacity: 1; background: var(--dw-accent-wash); }
.dw-chip-x:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 1px; }

/* An applied chip whose target no longer exists — a policy hook naming a tool
   that was deleted. It must stay visible and stay stored (dropping it would be a
   policy quietly uninstalling itself on a page load), so it is coloured instead.
   Same X.is-bad convention as .dw-pill, .dw-stat-tag, .dw-icontile, .dw-avatar. */
.dw-chip.is-applied.is-bad {
	background: var(--dw-bad-wash);
	color: var(--dw-bad);
}

/* ------------------------------------------------------------------ card */
.dw-card {
	/* WITHOUT THIS, an absolutely positioned child (.dw-card-dismiss, a menu)
	   anchors to the nearest positioned ancestor, which on most pages is the
	   page itself — so a per-card dismiss button lands in the top-right corner
	   of the DOCUMENT, once, on top of whichever card happens to be first. It
	   looks like a z-index problem and is not one. */
	position: relative;
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	box-shadow: var(--dw-shadow);
	overflow: hidden;
}
/* The kit already spells danger as .is-danger on five components, so this is
   one rule rather than a .dw-danger block. */
.dw-card.is-danger { border-color: var(--dw-bad); }

/* overflow: hidden is right for a card with a media slot and wrong for a card
   with a menu in it: the dropdown is clipped at the card edge and reads as a
   rendering bug. A card that owns a menu opts out. Affects any card with a
   menu, on any page, which is why it is a state and not a page override. */
/* THE WHOLE CARD IS THE CONTROL. Four pages set cursor:pointer on a card or
   row themselves, which a page may not do to a kit class. The hover uses the
   surface step rather than a lift, because the system is flat and a lift was
   also what two of those pages were faking with a shadow. */
/* The card counterpart of .dw-row.is-quiet, same rationale: a disabled tool,
   a retired template. Opacity dims the pills and icons with it, which a
   colour change cannot. */
.dw-card.is-quiet { opacity: .62; }
.dw-card.is-clickable { cursor: pointer; }
.dw-card.is-clickable:hover { background: var(--dw-surface-hover); }
.dw-card.is-clickable:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }
.dw-card.is-open { overflow: visible; }
.dw-card-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--dw-space-3);
	padding: var(--dw-space-3) var(--dw-card-pad);
	border-bottom: var(--dw-border) solid var(--dw-line);
}
.dw-card-head b { font-size: var(--dw-size-small); font-weight: var(--dw-weight-strong); }
.dw-card-head span { font-size: var(--dw-size-micro); color: var(--dw-faint); }
.dw-card-body { padding: var(--dw-card-pad); }
.dw-card-foot {
	padding: var(--dw-space-3) var(--dw-card-pad);
	border-top: var(--dw-border) solid var(--dw-line);
}

/* A leading icon beside a STACKED title-over-meta pair. 6 uses. This is a card
   region in its own right and sits where a .dw-card-head would.

   Not .dw-card-head: that is align-items: baseline and space-between — a
   title-left, meta-right BAR. Baseline drops a 30px icon so its bottom edge
   meets the cap line of the heading, which parks it visibly low and is exactly
   why the six sites that needed this each overrode the head instead. Centre
   alignment is the entire point of the component. */
.dw-card-lead {
	display: flex;
	align-items: center;
	gap: var(--dw-space-3);
	padding: var(--dw-space-3) var(--dw-card-pad);
}
.dw-card-lead > :first-child { flex: none; }
/* The stack. min-width: 0 is what lets a long title ellipsise instead of
   pushing the meta out of the card — a flex item's min-width is auto. */
.dw-card-lead > :last-child { min-width: 0; }

/* A footer icon RAIL: 13 buttons across 2 sites. Not .dw-btngroup, which JOINS
   its buttons into one segmented control and is a different statement. Not
   .dw-toolbar, which carries a bottom margin that is wrong inside a card.
   The wrap rule is explicit because the measured case is seven icon buttons in
   a 280px grid column, where the alternative to wrapping is horizontal
   overflow inside a card that clips it. */
.dw-card-actions {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--dw-space-1);
	/* Pins the rail to the foot of a card in .dw-cardgrid, which makes its
	   cards flex columns — so rails line up across a row of uneven cards. */
	margin-top: auto;
	padding: var(--dw-space-3) var(--dw-card-pad);
	border-top: var(--dw-border) solid var(--dw-line);
}

/* A destructive control anchored in the card's corner: "remove this from the
   list", not "close a dialog". Absolute so it takes no part in the head's
   layout — a long title runs underneath it rather than shoving it out of the
   card. Works only because .dw-card sets position: relative above. */
.dw-card-dismiss {
	position: absolute;
	top: var(--dw-space-2);
	right: var(--dw-space-2);
	z-index: 1;
}

/* A fixed-height preview slot. Fixed, not intrinsic: a grid of cards whose
   images arrive at different times and different aspect ratios reflows on
   every load, and the height token is what stops that. */
.dw-card-media {
	position: relative;
	height: var(--dw-card-media-h);
	overflow: hidden;
	background: var(--dw-surface-2);
}
/* iframe is here because the slot's one real user is a live page preview. */
.dw-card-media > img,
.dw-card-media > video,
.dw-card-media > canvas,
.dw-card-media > iframe {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}
/* The bottom scrim, so a caption laid over the preview stays legible against
   pixels nobody chose. The ramp is a TOKEN because the gate rejects a gradient
   function in every file but tokens.css — and it is right to. See
   --dw-fade-bottom there for why a scrim is not the depth cue that rule exists
   to catch. (This comment says it the long way round on purpose: the gate
   reads lines, not syntax, so naming the function here would fail the file
   the rule is meant to protect.) */
.dw-card-media::after {
	content: "";
	position: absolute;
	inset: auto 0 0 0;
	height: 40%;
	background: var(--dw-fade-bottom);
	pointer-events: none;
}

/* ------------------------------------------------------------------ stat
   Replaces six inventions: ts-stat-card, sc-stat-card, mt-stat,
   mcp-status-card, cc-col, quota-card, usage-stat-card. */
.dw-stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
	gap: var(--dw-space-2);
}
.dw-stat {
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	padding: var(--dw-space-4) var(--dw-space-4) var(--dw-space-4);
	box-shadow: var(--dw-shadow);
}
/* One tile that needs the width of two. One measured use, one line. */
.dw-stat.is-wide { grid-column: span 2; }

/* The leading icon slot. 19 uses across 3 page groups.
   It leads on its OWN ROW above the label rather than to the left of the text,
   and that is a deliberate trade: an icon to the left needs the label, value
   and foot wrapped in a stack element, so every one of the 19 sites would have
   to move markup. This way a page adds one element and nothing else changes. */
/* Tones, so a stat icon does not need .dw-icontile stacked on it to say
   anything. Four sites did exactly that. */
.dw-stat-icon.is-ok { color: var(--dw-ok); }
.dw-stat-icon.is-warn { color: var(--dw-warn); }
.dw-stat-icon.is-bad { color: var(--dw-bad); }
.dw-stat-icon.is-info { color: var(--dw-info); }
.dw-stat-icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	margin-bottom: var(--dw-space-2);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-muted);
	font-size: var(--dw-icon-size);
}

.dw-stat-label {
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	color: var(--dw-faint);
	margin-bottom: var(--dw-space-2);
}

/* A QUALIFIER beside the label — "Filtered", "Run-rate", "Over Quota". 10 uses
   as two separate inventions. It says how to READ the number, which is not
   what a pill or a badge says.

   Not .dw-pill: a pill carries a verdict, and "Run-rate" is not a verdict on
   anything. Not .dw-badge: bordered and row-sized, so it outweighs the 10.5px
   label it is meant to annotate and turns the label line into two objects. The
   is-* states exist because a qualifier sometimes IS loaded ("Over Quota"), and
   when it is, it should say so where the eye already is. */
.dw-stat-tag {
	display: inline-flex;
	align-items: center;
	margin-left: var(--dw-space-2);
	padding: 1px 6px;
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-faint);
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	white-space: nowrap;
}
.dw-stat-tag.is-ok { color: var(--dw-ok); background: var(--dw-ok-wash); }
.dw-stat-tag.is-warn { color: var(--dw-warn); background: var(--dw-warn-wash); }
.dw-stat-tag.is-bad { color: var(--dw-bad); background: var(--dw-bad-wash); }
.dw-stat-value {
	font-size: var(--dw-size-stat);
	font-weight: var(--dw-weight-strong);
	letter-spacing: -.032em;
	line-height: 1;
	font-variant-numeric: tabular-nums;
	color: var(--dw-text);
}
.dw-stat-value.is-accent { color: var(--dw-accent-text); }
/* is-warn was missing and two migrations independently mapped their warning
   tiles onto is-accent instead. On light, --dw-accent-text and --dw-warn are
   both #8A5416, so the substitution is invisible TODAY and would have stayed
   invisible until a skin moved one of them. Six uses measured. */
.dw-stat-value.is-warn { color: var(--dw-warn); }
.dw-stat-value.is-ok { color: var(--dw-ok); }
.dw-stat-value.is-bad { color: var(--dw-bad); }
.dw-stat-foot { margin-top: var(--dw-space-1); font-size: var(--dw-size-micro); color: var(--dw-muted); }
.dw-stat-foot.is-ok { color: var(--dw-ok); }

/* ----------------------------------------------------------------- table
   A bare <thead> inherits correctly, so most of the 52 in the tree need no
   markup change at all. Tables are the one place that uses the stronger
   hairline: a row border has to be trackable across the full width. */
.dw-tablewrap { overflow-x: auto; }
.dw-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--dw-size-small);
}
.dw-table thead th {
	text-align: left;
	padding: var(--dw-cell-pad);
	font-size: var(--dw-size-eyebrow);
	font-weight: var(--dw-weight-strong);
	letter-spacing: var(--dw-track-th);
	text-transform: uppercase;
	color: var(--dw-faint);
	background: var(--dw-surface-2);
	border-bottom: var(--dw-border) solid var(--dw-line-strong);
	white-space: nowrap;
}
/* A TABLE THAT SCROLLS ITSELF KEEPS ITS HEADER. Composing .dw-tablewrap with
   .dw-scrollbody gives a bounded, vertically scrolling table — which is the
   answer to a long list that pagination would otherwise hide behind a click —
   but it also scrolls the column names out of sight, and a table you cannot
   read the headings of is worse than a long one. Scoped to the combination, so
   the 52 plain tables in the tree are untouched.

   The explicit background is load-bearing: a sticky cell is painted over by the
   rows sliding under it unless it has an opaque ground of its own. thead th
   already declares --dw-surface-2 above, and repeating it here is what keeps
   that true if the rule above ever moves. */
.dw-tablewrap.dw-scrollbody .dw-table thead th {
	position: sticky;
	top: 0;
	z-index: 1;
	background: var(--dw-surface-2);
}

.dw-table tbody td {
	padding: var(--dw-cell-pad);
	border-bottom: var(--dw-border) solid var(--dw-line-strong);
	color: var(--dw-muted);
	vertical-align: middle;
}
.dw-table tbody tr:last-child td { border-bottom: 0; }
.dw-table tbody td:first-child { color: var(--dw-text); font-weight: var(--dw-weight-medium); }
.dw-table tbody tr:hover td { background: var(--dw-surface-hover); }
.dw-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The table's answer to `.dw-cardgrid > .dw-empty`: 12 colspan placeholders in
   the tree, every one of them a <tr> that inherits the row hover, the row
   border and the first-cell emphasis. So "No results" lights up under the
   cursor and reads as a record you can click. Suppressing the hover and the
   border is the whole job; the rest is making it look like the zero state it
   is rather than like a row with one field filled in. */
.dw-table tbody tr.is-empty td {
	padding: var(--dw-space-6) var(--dw-space-4);
	border-bottom: 0;
	text-align: center;
	color: var(--dw-faint);
	font-weight: var(--dw-weight-body);
}
.dw-table tbody tr.is-empty:hover td { background: transparent; }

/* ------------------------------------------------------------------ pill
   The verdict on a number or a state. 284 uses today across 12 classes,
   251 of them a bare Bootstrap `badge`. This is the target for all of them
   and for the 25 status maps in page JS. */
.dw-pill {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 3px 10px;
	border-radius: var(--dw-radius-pill);
	font-size: var(--dw-size-eyebrow);
	font-weight: var(--dw-weight-strong);
	letter-spacing: .1em;
	text-transform: uppercase;
	white-space: nowrap;
	color: var(--dw-muted);
	background: var(--dw-accent-wash);
}
.dw-pill::before {
	content: "";
	width: 5px;
	height: 5px;
	border-radius: 50%;
	background: currentColor;
	flex: none;
}
.dw-pill.is-ok { color: var(--dw-ok); background: var(--dw-ok-wash); }
.dw-pill.is-warn { color: var(--dw-warn); background: var(--dw-warn-wash); }
.dw-pill.is-bad { color: var(--dw-bad); background: var(--dw-bad-wash); }
.dw-pill.is-run { color: var(--dw-accent-text); background: var(--dw-accent-wash); }
.dw-pill.is-off { color: var(--dw-faint); background: var(--dw-surface-2); }
.dw-pill.is-info { color: var(--dw-info); background: var(--dw-info-wash); }

/* TERMINAL BUT NOT FAILED — "cancelled", "closed", "archived". Distinct from
   is-off, which says never-started, and from is-ok, which is a verdict. It
   drops the status dot: a settled thing has no state left to report. */
.dw-pill.is-done { color: var(--dw-muted); background: var(--dw-surface-2); }
.dw-pill.is-done::before { display: none; }

/* Named dwKitPulse, not dwPulse. modern-refresh.css:2625 already defines
   @keyframes dwPulse with a different midpoint (.55 against .35) and keyframe
   names are a single global namespace where the last definition simply wins.
   Unnamespaced, this component's animation was decided by stylesheet load
   order rather than by this file. */
.dw-pill.is-run::before { animation: dwKitPulse calc(1.6s * var(--dw-motion-scale)) var(--dw-ease) infinite; }
@keyframes dwKitPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Severity is a scale, not a state: it always means the same things. sev-info
   and sev-none complete it — the platform's own SEVERITIES list includes info,
   and a clean scan renders "0 findings", which sev-low would mis-state as a
   finding of low severity. */
.dw-pill.sev-critical { color: var(--dw-bad); background: var(--dw-bad-wash); }
.dw-pill.sev-high { color: var(--dw-warn); background: var(--dw-warn-wash); }
.dw-pill.sev-medium { color: var(--dw-accent-text); background: var(--dw-accent-wash); }
.dw-pill.sev-low { color: var(--dw-muted); background: var(--dw-surface-2); }
.dw-pill.sev-info { color: var(--dw-info); background: var(--dw-info-wash); }
.dw-pill.sev-none { color: var(--dw-ok); background: var(--dw-ok-wash); }

/* A badge labels a KIND. A pill carries a verdict. Do not swap them. */
.dw-badge {
	display: inline-flex;
	align-items: center;
	padding: 2px 9px;
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-sm);
	font-size: var(--dw-size-eyebrow);
	letter-spacing: .06em;
	color: var(--dw-muted);
	background: transparent;
}

/* ------------------------------------------------------------------ spec
   The key/value row. Detail pages, config panels and metadata blocks all
   re-implement this; there is one now. */
.dw-specs { margin: 0; padding: 0; border-top: var(--dw-border) solid var(--dw-line); }
.dw-spec {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--dw-space-5);
	padding: var(--dw-space-3) 0;
	border-bottom: var(--dw-border) solid var(--dw-line);
}
.dw-spec dt {
	margin: 0;
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	color: var(--dw-faint);
	white-space: nowrap;
}
.dw-spec dd { margin: 0; font-size: var(--dw-size-base); color: var(--dw-text); text-align: right; }

/* ------------------------------------------------------------------ tabs */
/* The strip scrolls sideways when the tabs outrun it — and ONLY sideways.
   `overflow-x: auto` on its own makes overflow-y compute to `auto` rather
   than stay `visible`, and .dw-tab is pulled up 1px (below) so its underline
   lands on the strip's border. A negative margin does not shrink the
   scrollable overflow area, so the strip measures 1px taller than its box and
   the browser painted a full-height vertical scrollbar at its right edge for
   that one dead pixel: arrows, a thumb, and nothing to scroll. */
.dw-tabs {
	display: flex;
	gap: var(--dw-space-5);
	border-bottom: var(--dw-border) solid var(--dw-line);
	margin-bottom: var(--dw-space-5);
	overflow-x: auto;
	overflow-y: hidden;
}
.dw-tab {
	appearance: none;
	border: 0;
	background: transparent;
	padding: var(--dw-space-3) 0;
	font: inherit;
	font-size: var(--dw-size-nav);
	color: var(--dw-muted);
	cursor: pointer;
	white-space: nowrap;
	border-bottom: 2px solid transparent;
	margin-bottom: -1px;
	transition: color var(--dw-dur-fast) var(--dw-ease),
		border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-tab:hover { color: var(--dw-text); }
/* One page disables five of six tabs while a create flow runs, and had to
   HIDE them instead — a tab that reads clickable and is dead is worse than
   a tab that is not there, but neither is what was wanted. */
.dw-tab[disabled], .dw-tab.is-disabled {
	opacity: .45;
	cursor: not-allowed;
	pointer-events: none;
}
/* Seven tabs carry a count. A .dw-badge is row-sized and bordered, which is
   wrong inside a tab label; this is the same idea as .dw-stat-tag. */
.dw-tab-count {
	margin-left: var(--dw-space-1);
	font-size: var(--dw-size-eyebrow);
	color: var(--dw-faint);
	font-variant-numeric: tabular-nums;
}
.dw-tab.is-on .dw-tab-count { color: var(--dw-accent-text); }
.dw-tab.is-on { color: var(--dw-text); border-bottom-color: var(--dw-accent); }
.dw-tab:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }

/* ------------------------------------------------------------------ form */
.dw-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--dw-space-4); }
/* NOT a direct-child selector. .dw-field is a flex column, so a label-left
   settings row cannot nest its label inside it — 13 cells on the first real
   settings page fell back to a bare <label> at body weight, giving the label
   and its value identical typography. Both selectors, one rule. */
/* DIRECT CHILD OF EITHER, and both halves were learned the hard way. The
   original `.dw-field > label` was too tight: a settings row lays its label
   beside the control rather than inside .dw-field, and 13 cells fell back to
   an unstyled <label>. Loosening it to a descendant selector then went too far
   the other way and caught the label belonging to a .dw-switch NESTED in a
   field, rendering it as 10.5px letterspaced caps. Naming both parents as
   direct children serves both without over-reaching. */
.dw-field > label,
.dw-settinggroup > label {
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	color: var(--dw-faint);
}
.dw-input, .dw-select, .dw-textarea {
	width: 100%;
	padding: 9px 12px;
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface);
	color: var(--dw-text);
	font: inherit;
	font-size: var(--dw-size-base);
	transition: border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-input::placeholder, .dw-textarea::placeholder { color: var(--dw-faint); }
/* A DISABLED OR READONLY FIELD MUST LOOK IT. The kit sets an explicit colour
   and background on .dw-input, which beats the browser default, so a disabled
   input rendered pixel-identical to an editable one — only the mouse cursor
   differed, which is nothing at all for a keyboard user. mcp-setup.html alone
   has 10 readonly fields: token reveal, OAuth URLs, client IDs. */
.dw-input:disabled, .dw-select:disabled, .dw-textarea:disabled,
.dw-input[readonly], .dw-textarea[readonly] {
	background: var(--dw-surface-2);
	color: var(--dw-muted);
	cursor: not-allowed;
}
.dw-input:focus, .dw-select:focus, .dw-textarea:focus {
	outline: 2px solid var(--dw-focus);
	outline-offset: 1px;
	border-color: var(--dw-focus);
}
/* Help text and the error share one slot so the layout does not jump when
   validation fires. */
.dw-field-note { font-size: var(--dw-size-micro); color: var(--dw-faint); min-height: 1.2em; }
.dw-field.is-invalid .dw-input,
.dw-field.is-invalid .dw-select { border-color: var(--dw-bad); }
.dw-field.is-invalid .dw-field-note { color: var(--dw-bad); }

/* ----------------------------------------------------------------- alert */
.dw-alert {
	display: flex;
	gap: var(--dw-space-3);
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-text);
	font-size: var(--dw-size-small);
}
.dw-alert.is-ok { border-color: var(--dw-ok); background: var(--dw-ok-wash); }
.dw-alert.is-warn { border-color: var(--dw-warn); background: var(--dw-warn-wash); }
.dw-alert.is-bad { border-color: var(--dw-bad); background: var(--dw-bad-wash); }
.dw-alert.is-info { border-color: var(--dw-info); background: var(--dw-info-wash); }

/* ONE LINE, NO BOX. Seven of eight list pages own a message region, and the
   two shapes going into them are a dismissible banner and a single tinted
   sentence. Forcing the sentence into a bordered padded box makes it heavier
   than the thing it is reporting on, so two migrations reached for a bare
   <span> instead. The tone still comes from the same .is-* states. */
.dw-alert.is-inline {
	padding: 0;
	border: 0;
	background: transparent;
	color: var(--dw-muted);
}
.dw-alert.is-inline.is-ok { color: var(--dw-ok); }
.dw-alert.is-inline.is-warn { color: var(--dw-warn); }
.dw-alert.is-inline.is-bad { color: var(--dw-bad); }
.dw-alert.is-inline.is-info { color: var(--dw-info); }

/* ------------------------------------------------------------- accordion
   THE KIT HAD NO DISCLOSURE COMPONENT AT ALL, against 28 accordion-items
   across 5 pages plus 3 hand-built ones. That is not drift, it is a gap.

   Driven by .is-open on the block rather than by <details>: the tree's 31
   existing instances are all div-based with JS toggling, and a component that
   only worked as <details> would have converted none of them. The head must
   still be a <button> with aria-expanded — this styles it, it does not make it
   accessible on its own. */
.dw-accordion {
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	box-shadow: var(--dw-shadow);
	overflow: hidden;
}
.dw-accordion + .dw-accordion { margin-top: var(--dw-space-2); }
.dw-accordion-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--dw-space-3);
	width: 100%;
	padding: var(--dw-space-3) var(--dw-card-pad);
	border: 0;
	background: transparent;
	color: var(--dw-text);
	font: inherit;
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	text-align: left;
	cursor: pointer;
	transition: background var(--dw-dur-fast) var(--dw-ease);
}
.dw-accordion-head:hover { background: var(--dw-surface-hover); }
/* Inset, because the head is flush with the block's own border and a ring
   drawn outside would be clipped by its overflow: hidden. */
.dw-accordion-head:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: -2px; }
/* The marker belongs to the head, so a page cannot render an accordion that
   forgets to turn its chevron — which is what three of the hand-built ones do.
   Drawn from two borders rather than an icon font: it must rotate, and it must
   exist whether or not the icon set loaded. */
.dw-accordion-head::after {
	content: "";
	flex: none;
	width: 7px;
	height: 7px;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	opacity: .55;
	transform: rotate(45deg);
	transition: transform var(--dw-dur-fast) var(--dw-ease);
}
.dw-accordion.is-open > .dw-accordion-head::after { transform: rotate(225deg); }
.dw-accordion-body {
	padding: 0 var(--dw-card-pad) var(--dw-space-4);
	font-size: var(--dw-size-small);
	color: var(--dw-muted);
}
.dw-accordion:not(.is-open) > .dw-accordion-body { display: none; }

/* ----------------------------------------------------------------- empty
   Nothing is wrong; there is nothing to show. Distinct from an error, which
   is why eight pages invented their own when only `.empty-state` existed. */
.dw-empty { padding: var(--dw-space-6) var(--dw-space-4); text-align: center; }
/* 5 uses, one rule. Sized off the icon token rather than a fixed px so a skin
   that scales its icons scales this too. */
/* Scoped to .dw-empty to outrank `.dw-icon, .bi`, which sits later in this
   file at equal specificity and therefore won: the icon measured 16px
   instead of 2.4x. Every icon on the platform is a `bi`, all 998 of them,
   so the unscoped form was dead in every case that could occur. */
.dw-empty .dw-empty-icon {
	display: block;
	margin-bottom: var(--dw-space-3);
	font-size: calc(var(--dw-icon-size) * 2.4);
	color: var(--dw-faint);
	opacity: .7;
}
.dw-empty-title {
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
	margin-bottom: var(--dw-space-1);
}
.dw-empty-text { margin: 0 auto var(--dw-space-4); font-size: var(--dw-size-micro); color: var(--dw-faint); max-width: 44ch; }

/* --------------------------------------------------------------- spinner */
/* A CONTROL THAT IS WORKING, which is not what a skeleton says. A skeleton is
   a placeholder for content that has not arrived; this goes inside a button
   that has been pressed. Three separate surveys asked for it independently:
   160 spinner-border uses across 35 of the platform's 55 page modules, and 47
   sites that hand-roll save-innerHTML then spinner then restore.
   Sized in em so it inherits the type scale of whatever it sits in. */
.dw-spinner {
	display: inline-block;
	width: 1em;
	height: 1em;
	vertical-align: -.125em;
	border: 2px solid currentColor;
	border-right-color: transparent;
	border-radius: 50%;
	animation: dwKitSpin calc(.7s * var(--dw-motion-scale)) linear infinite;
}
.dw-spinner.is-sm { width: .8em; height: .8em; border-width: 1.5px; }
@keyframes dwKitSpin { to { transform: rotate(360deg); } }

/* -------------------------------------------------------------- skeleton */
.dw-skeleton {
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	animation: dwKitShimmer calc(1.4s * var(--dw-motion-scale)) var(--dw-ease) infinite;
}
.dw-skeleton-text { height: 1em; margin-bottom: 6px; }
/* Sized to the shapes they stand in for, so the page does not jump when the
   real content lands: is-number takes the stat value's own type size, is-row
   the height of a table row. 16 uses, three one-liners. */
.dw-skeleton.is-number { height: var(--dw-size-stat); width: 4ch; }
.dw-skeleton.is-row { height: 34px; }
.dw-skeleton-group { display: flex; flex-direction: column; gap: var(--dw-space-2); }
/* Namespaced to dwKit* like the other two. dwShimmer had no collision TODAY,
   but that is what dwPulse looked like before modern-refresh.css:2625 quietly
   took it over — keyframe names are one global namespace and the last
   definition wins, whoever wrote it. */
@keyframes dwKitShimmer { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* ------------------------------------------------------------------ code
   Thirteen independent code-block styles exist and no base rule for <pre>
   exists at all. This is both. */
.dw-code {
	margin: 0;
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-text);
	font-family: var(--dw-font-mono);
	font-size: var(--dw-size-micro);
	line-height: 1.6;
	overflow-x: auto;
	/* A <pre> already computes to pre, but .dw-code is put on <div> and <code>
	   too, where it does not — and a log with its indentation reflowed is a
	   different log. Stated so the class means the same thing on any element. */
	white-space: pre;
	/* So it can BE the working surface of a pane rather than only sitting in
	   one. Inert outside a flex parent. */
	flex: 1 1 auto;
}
/* Log and terminal panes stay dark on every skin by design, and are
   tokenized so a skin may still retheme them. */
.dw-code.is-terminal { background: var(--dw-terminal-bg); color: var(--dw-terminal-fg); border-color: transparent; }
/* The caret and the row rule of a live terminal are --dw-terminal-cursor and
   --dw-terminal-line. They are tokens without a kit rule on purpose: the two
   terminal surfaces draw their own caret element and their own row divider,
   and the kit has no business deciding whether a caret blinks. What it does
   decide is that both colours come from the terminal group, so a skin retints
   the whole surface at once instead of two thirds of it. */

/* 12 uses. WITHOUT IT A LOG SCROLLS THE PAGE INSTEAD OF ITSELF, which on a
   tailing log means the page walks away from the reader.
   Two contexts, one rule: inside the editor's height chain the flex pair takes
   exactly the space left over, and everywhere else there is no definite parent
   height to take a share of, so the vh cap is what actually does the stopping.
   Same reasoning as .dw-modal's 86vh. */
/* WRAPS INSTEAD OF SCROLLING SIDEWAYS. .dw-code is white-space: pre and beats
   a page's own pre-wrap on source order, so a 400-character log line ran off
   the edge. One page had already worked around it with an inline style. */
.dw-code.is-wrap { white-space: pre-wrap; overflow-wrap: anywhere; }
.dw-code.is-scroll {
	overflow-y: auto;
	flex: 1 1 auto;
	min-height: 0;
	max-height: 60vh;
}

/* INLINE monospace: a uid, a path, a header name inside a sentence. 22 uses.
   Not .dw-code — that is a padded, bordered, filled BLOCK, and putting it
   around three words mid-paragraph breaks the line box. The step down in size
   is because a mono face at the same nominal size has a larger x-height and
   visibly outgrows the prose it sits inside. */
.dw-mono {
	font-family: var(--dw-font-mono);
	font-size: .92em;
	letter-spacing: 0;
	/* Inherit is deliberate. On a <code> element Bootstrap's reboot sets
	   color: var(--bs-code-color), which is a fixed pink no skin can reach, so
	   without this the component silently escapes the token system on exactly
	   the element it is most likely to be used on. */
	color: inherit;
}

/* A code <textarea>. One page has 9 read-only ones, and they are textareas on
   purpose: the content has to be selectable and is set through .value, which a
   <pre> cannot offer and a form control should not be styled as. So this is
   neither .dw-code (a block, not a control) nor .dw-textarea (prose metrics,
   soft wrap, and it would reflow the code). */
.dw-codeedit {
	width: 100%;
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-text);
	font-family: var(--dw-font-mono);
	font-size: var(--dw-size-micro);
	line-height: 1.6;
	/* Code does not soft-wrap: a wrapped line silently changes what the reader
	   thinks the file says. It scrolls sideways instead. */
	white-space: pre;
	overflow-wrap: normal;
	overflow-x: auto;
	tab-size: 2;
	resize: vertical;
}
.dw-codeedit:focus {
	outline: 2px solid var(--dw-focus);
	outline-offset: 1px;
	border-color: var(--dw-focus);
}

/* ------------------------------------------------------------ pagination
   Always states the range. "1-50 of 214" answers how much is NOT shown. */
.dw-pagination {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--dw-space-3);
	padding-top: var(--dw-space-3);
	font-size: var(--dw-size-micro);
	color: var(--dw-faint);
}
.dw-pagination nav { display: flex; gap: var(--dw-space-1); }

/* ------------------------------------------------------------- progress */
.dw-progress { height: 4px; border-radius: var(--dw-radius-pill); background: var(--dw-surface-2); overflow: hidden; }
.dw-progress-bar { height: 100%; background: var(--dw-accent); transition: width var(--dw-dur) var(--dw-ease); }
/* A bar reporting a VERDICT, not just a quantity: over quota, near limit.
   Two pages had this as bg-success/warning/danger, which no skin reaches. */
.dw-progress-bar.is-ok { background: var(--dw-ok); }
.dw-progress-bar.is-warn { background: var(--dw-warn); }
.dw-progress-bar.is-bad { background: var(--dw-bad); }

/* ------------------------------------------------------------- chart --
   Chart.js needs a definite height from an ancestor or it collapses, and
   with maintainAspectRatio:false it needs one that does not depend on the
   canvas. Twelve canvases were each carrying an inline height because the
   kit offered nowhere to put it. */
.dw-chartbox {
	position: relative;
	height: var(--dw-chart-h);
}
.dw-chartbox.is-sm { height: calc(var(--dw-chart-h) * .66); }
.dw-chartbox.is-lg { height: calc(var(--dw-chart-h) * 1.5); }
.dw-chartbox > canvas { display: block; width: 100%; height: 100%; }

/* The key. A chart drawn from --dw-cat-* needs somewhere to say which hue is
   which, and every page with a chart was building one by hand. */
.dw-legend { display: flex; flex-wrap: wrap; gap: var(--dw-space-3); margin-top: var(--dw-space-3); }
.dw-legend-item { display: inline-flex; align-items: center; gap: var(--dw-space-2); font-size: var(--dw-size-micro); color: var(--dw-muted); }
.dw-legend-swatch { width: 10px; height: 10px; border-radius: 2px; flex: none; background: currentColor; }

/* ------------------------------------------------------ scrolling body --
   A panel that scrolls ITSELF, available anywhere. This is the seventh
   instance of the problem --dw-editor-chrome solved for one archetype: a
   live feed had `height: calc(100vh - 240px)` because the kit offered no
   way to say "this box scrolls and the page does not". .dw-pane-body only
   works inside .dw-editorgrid's height chain, and .dw-code.is-scroll is a
   monospace code block, so a migration that used neither shipped a feed
   that scrolls the whole page and an auto-scroll aimed at the window.
   max-height rather than height: a short list should not leave a hole. */
.dw-scrollbody {
	max-height: var(--dw-scroll-h);
	overflow-y: auto;
	min-height: 0;
}
.dw-scrollbody.is-sm { max-height: calc(var(--dw-scroll-h) * .5); }
.dw-scrollbody.is-lg { max-height: calc(var(--dw-scroll-h) * 1.6); }

/* ----------------------------------------------------------- close button
   101 instances across the tree. The X is Bootstrap's black SVG data URI and
   cannot be coloured, only filtered. One token fixes every one of them. */
/* Bootstrap's close is a background IMAGE, so the only way to retint it is a
   filter. That stays, for markup still using .btn-close. */
.btn-close:not(.btn-close-white) { filter: var(--dw-btn-close-filter); }
/* The accordion chevron has exactly the same problem and the same only fix:
   it is a black SVG data URI, so it can be filtered but not coloured. On any
   dark skin it was black on near-black. 28 accordion items across 5 pages. */
.accordion-button::after { filter: var(--dw-btn-close-filter); }

/* .dw-close IS A REAL COMPONENT NOW. It was documented in kit.json as "the
   dismiss affordance" while its only rule was the filter above — so used on
   its own, as the contract invited, it drew literally nothing. A promise with
   an empty box behind it. It renders its own glyph in currentColor, which is
   also what makes it reachable by a skin, unlike the filtered image. */
.dw-close {
	appearance: none;
	border: 0;
	padding: var(--dw-space-1);
	background: transparent;
	color: var(--dw-muted);
	font-size: var(--dw-size-base);
	line-height: 1;
	cursor: pointer;
}
.dw-close::before { content: "\00d7"; display: block; }
.dw-close:hover { color: var(--dw-text); }
.dw-close:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }

/* An alert had no dismiss slot, so the first page to need one reached for a
   .dw-btn.is-icon and a .dw-toolbar-grow spacer. One rule instead. */
.dw-alert > .dw-close { margin-left: auto; flex: none; }

/* ------------------------------------------------------------------ logo
   Pages reference the slot, never a file. Changing the mark becomes one
   token instead of 144 hardcoded paths. */
/* IT DRAWS THE MARK. It previously set only a size, while kit.json told page
   authors to "reference the slot, never a file" — so following the contract
   produced an empty box. The token is the slot; this is what reads it.
   Sized by height with contain, so a customer mark of any aspect ratio fits
   the same header without being distorted to fit a box. */
.dw-logomark {
	display: block;
	/* Always on a dark plate. See --dw-logo-plate for why. */
	background-color: var(--dw-logo-plate);
	border-radius: var(--dw-logo-plate-radius);
	padding: 3px;
	box-sizing: border-box;
	background-image: var(--dw-logomark);
	background-repeat: no-repeat;
	background-position: center;
	background-size: contain;
	aspect-ratio: 1;
	height: var(--dw-logomark-h);
	min-height: var(--dw-logomark-min);
	width: auto;
	flex: none;
}
/* The mark keeps the fills it was drawn with. They live in the SVG rather
   than in tokens, so recolouring it is a job for a future skin hook. */

/* ============================================================================
   PAGE ARCHETYPES
   Components alone do not make pages behave alike; the inventory is the proof.
   The 56 pages already SHARE their chrome through partials/_header.html and
   partials/_sidebar.html, so the drift is not in the chrome. It is in the
   content area, where every page invented its own arrangement.

   So an archetype here is a CONTENT-AREA contract, not a JS shell. A page
   declares which of the five it is, and the archetype owns the slot order and
   the rhythm between slots. That is checkable by grep, needs no change to how
   pages render, and is the reason this can be rolled out one page at a time.
   ========================================================================= */
/* THE TYPE FLOOR LIVES HERE, not only on .dw-app. .dw-app is the whole-shell
   wrapper and no page in the tree sets it, so --dw-size-base never reached
   inherited text and a migrated page silently kept the base layer's larger
   font — measured as a body paragraph outsizing the .dw-row-title above it.
   Every migrated page has .dw-page by definition, which makes it the honest
   place for the scale to start. */
/* THE MEASURE, and it only became load-bearing when the sidebar went.
   .dw-page never had a max-width. It did not need one: #main carried a
   264px left margin for the sidebar, so the content column was bounded by
   accident. Remove the sidebar and the accident goes with it. Measured,
   content widened from 956px to 1240px at 1280, and from 1596px to 1880px
   at 1920, which is a full-bleed line length nobody chose.

   --dw-content-max and --dw-gutter have been declared in tokens.css from
   the start and NOTHING has ever read either one. That is the eighth
   construct in this project promising something no code delivered. The
   token is read here now — --dw-content-max only. --dw-gutter STILL has no
   reader anywhere, so half of that instance is open, and saying otherwise
   here would be a ninth. The 1240px the layout used to land on is
   exactly the value it already held, so this restores the measure rather
   than inventing one.

   margin-inline: auto is what centres the column once #main starts at the
   viewport edge. Both archetype overrides still win on specificity:
   .dw-page.is-editor sets max-width: none because an editor is meant to
   fill the viewport, and .dw-page.is-narrow sets 780px. */
.dw-page {
	font-family: var(--dw-font);
	font-size: var(--dw-size-base);
	line-height: var(--dw-leading);
	display: flex;
	flex-direction: column;
	gap: var(--dw-space-5);
	width: 100%;
	max-width: var(--dw-content-max);
	margin-inline: auto;
}
.dw-page > .dw-pagehead { margin-bottom: 0; }

/* INSIDE THE COMPANION FRAME THE PANEL IS THE MEASURE, so the page must
   not impose a second one. main.js sets body.embed-mode-companion on a
   page loaded with ?embed=companion; there the frame is already a
   deliberate width chosen by the split, and --dw-content-max on top of it
   just parks the content in the middle with dead gutters either side.
   Measured at a 2560 viewport: a 2118px panel drawing 1240px of content.
   The narrow reading measure still applies to a page opened on its own. */
body.embed-mode-companion .dw-page,
body.embed-mode .dw-page {
	max-width: none;
}

/* LIST, 24 pages. header, toolbar, applied filters, body, empty, pagination.
   The BODY has two shapes and the archetype has to carry both. The plan
   assumed a table; the first real page (agent-list) turned out to be a card
   grid, and forcing it into a table would have been the archetype bending the
   page rather than the page fitting the archetype. */
/* The archetype owns vertical rhythm, so a child may not also carry a margin.
   .dw-toolbar shipped with margin-bottom and the page adds row-gap, measured
   as a 44px double gap on the first real list page. Neutralise BOTH, or every
   one of the 24 list pages inherits the same doubled space. */
/* THE ARCHETYPE OWNS VERTICAL RHYTHM, so no direct child may also carry a
   margin. .dw-toolbar shipped with margin-bottom while .dw-page adds row-gap,
   measured as a 44px double gap on the first real list page and hit again
   independently on the first dashboard. Neutralising per-archetype missed it
   twice, so it is neutralised once, here, for every archetype. */
.dw-page > .dw-pagehead,
.dw-page > .dw-toolbar,
.dw-page > .dw-applied,
.dw-page > .dw-card,
.dw-page > .dw-stats,
.dw-page > .dw-cardgrid,
.dw-page > .dw-panelgrid,
.dw-page > .dw-alert { margin-bottom: 0; }

/* THE ARCHETYPE'S RHYTHM, AVAILABLE AT ANY DEPTH. .dw-page gaps only its
   DIRECT children, and most of this platform renders its body from a JS
   module into one container two or three levels down — so the spacing the
   archetype promises simply does not reach the content on a JS-heavy page.
   Two migrations restated it privately before this existed. Same gap, same
   neutralisers, one name. */
.dw-stack {
	display: flex;
	flex-direction: column;
	gap: var(--dw-space-5);
}
.dw-stack > .dw-pagehead,
.dw-stack > .dw-toolbar,
.dw-stack > .dw-applied,
.dw-stack > .dw-card,
.dw-stack > .dw-stats,
.dw-stack > .dw-cardgrid,
.dw-stack > .dw-panelgrid,
.dw-stack > .dw-alert { margin-bottom: 0; }

/* The applied filters belong to the toolbar above them, not to the body below:
   they are one control cluster, and a full page gap between them reads as two
   unrelated regions. This is the list archetype's own rule. */
.dw-page.is-list > .dw-applied { margin-top: calc(-1 * var(--dw-space-3)); }

/* Card-grid variant. auto-fill rather than auto-fit so a single remaining
   result keeps its column width instead of stretching across the row. */
.dw-cardgrid {
	display: grid;
	gap: var(--dw-space-4);
	grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.dw-cardgrid > .dw-card { display: flex; flex-direction: column; }
/* The empty state spans the whole grid rather than sitting in column one. */
.dw-cardgrid > .dw-empty { grid-column: 1 / -1; }
/* A thumbnail wall wants more columns than a card wall: the 280px track gives
   3-up where an asset grid wants 5-up, which is why one page kept Bootstrap. */
.dw-cardgrid.is-tight { grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }
/* Full-width inside any grid. Previously only .dw-stat.is-wide could say it,
   so a details panel stacked instead of spanning. */
.dw-cardgrid > .is-full, .dw-panelgrid > .is-full, .dw-stats > .is-full { grid-column: 1 / -1; }

/* ROWS — the list archetype's THIRD body shape, and the same lesson as the
   second: the plan assumed a table, the first page turned out to be a card
   grid, and four renderers turned out to be neither. ~38 rows across two page
   groups, each row a title, a meta line, an input and two or more buttons.

   Not .dw-cardgrid: minmax(280px, 1fr) tiles them into columns and every one
   of these needs the full width for its controls. Not .dw-table: the cells
   hold number inputs and buttons, not values — a table promises that a column
   means one thing all the way down, and these break that on row one.

   Deliberately NOT hoverable. A row is not itself clickable; its buttons are.
   Lighting the row on hover makes the same false promise that the colspan
   empty state was making before .dw-table tbody tr.is-empty above. */
/* --------------------------------------------------------- icon tile ---
   A SMALL SQUARE CARRYING A TONE. The platform header bell renders one on
   all 56 pages and every notification row carries another, in four tones.

   .dw-avatar cannot substitute, and that was measured rather than assumed:
   it sets `background` at (0,1,0) and the kit loads after notifications.css,
   so applying it silently kills the tint it was meant to preserve. A page
   may not re-colour a kit class, which left no legal page-level fix — which
   is exactly the situation where the kit has to grow instead.

   Square, not round: a round tile reads as a person. This is a category. */
.dw-icontile {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 32px;
	height: 32px;
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-muted);
	font-size: var(--dw-size-base);
}
.dw-icontile.is-sm { width: 24px; height: 24px; font-size: var(--dw-size-small); }
.dw-icontile.is-lg { width: 44px; height: 44px; font-size: var(--dw-size-lede); }
.dw-icontile.is-ok { background: var(--dw-ok-wash); color: var(--dw-ok); }
.dw-icontile.is-warn { background: var(--dw-warn-wash); color: var(--dw-warn); }
.dw-icontile.is-bad { background: var(--dw-bad-wash); color: var(--dw-bad); }
.dw-icontile.is-info { background: var(--dw-info-wash); color: var(--dw-info); }
.dw-icontile.is-accent { background: var(--dw-accent-wash); color: var(--dw-accent-text); }

/* .dw-card-lead is documented as an icon beside a STACKED title-over-meta
   pair, and shipped with neither half — so six sites borrowed .dw-row-meta,
   a part belonging to a different component. A component that names a
   structure has to provide it. */
.dw-lead-main { min-width: 0; }
.dw-lead-title {
	font-size: var(--dw-size-base);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.dw-lead-meta { font-size: var(--dw-size-micro); color: var(--dw-faint); }

.dw-rows { display: flex; flex-direction: column; }
.dw-row {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--dw-space-3);
	padding: var(--dw-space-3) var(--dw-card-pad);
	border-bottom: var(--dw-border) solid var(--dw-line);
}
.dw-row:last-child { border-bottom: 0; }

/* ----------------------------------------------------------- message ---
   A ROLE-ATTRIBUTED MESSAGE IN A SEQUENCE. Held back deliberately until two
   pages had tried to compose it from .dw-row and both reported the same four
   things missing — role as a visual rather than a written word, a body that
   is the subject rather than muted prose, a way to separate machinery from
   conversation, and a session divider. Five vocabularies and 383 uses exist
   across the platform today.

   It is three declarations more than what both pages hand-rolled, and
   everything else composes: the avatar is .dw-avatar, the verdict is
   .dw-pill, a tool payload is .dw-accordion over .dw-code.is-scroll. */
.dw-msg {
	display: flex;
	gap: var(--dw-space-3);
	padding: var(--dw-space-3) 0;
	align-items: flex-start;
}
.dw-msg-body {
	flex: 1 1 auto;
	min-width: 0;
	color: var(--dw-text);
}

/* Role as a VISUAL. A written "Assistant" was the only attribution either
   page could manage; these are the tone, and the alignment is separate so a
   transcript can stay left-aligned while a chat mirrors the user. */
.dw-msg.is-user > .dw-avatar { background: var(--dw-accent-wash); color: var(--dw-accent-text); }
.dw-msg.is-agent > .dw-avatar { background: var(--dw-info-wash); color: var(--dw-info); }
.dw-msg.is-system, .dw-msg.is-tool { color: var(--dw-muted); font-size: var(--dw-size-small); }
.dw-msg.is-system > .dw-avatar, .dw-msg.is-tool > .dw-avatar { background: var(--dw-surface-2); color: var(--dw-faint); }

/* Mirrored, for a chat. A transcript simply does not set it. */
.dw-msg.is-end { flex-direction: row-reverse; text-align: end; }

/* A session boundary. Both pages faked this with a centred pill. */
.dw-msg-sep {
	display: flex;
	align-items: center;
	gap: var(--dw-space-3);
	margin: var(--dw-space-4) 0;
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-eyebrow);
	text-transform: uppercase;
	color: var(--dw-faint);
}
.dw-msg-sep::before, .dw-msg-sep::after {
	content: "";
	flex: 1 1 auto;
	height: 1px;
	background: var(--dw-line);
}

/* READ / RESOLVED / HANDLED. A notification feed and a work inbox are the
   same shape with the same need: say which rows still want attention
   without hiding the rest. Two pages were doing it independently, one with
   a pill reading "Unread" and one with an ad-hoc opacity. Opacity is the
   right instrument, because it dims the row AND its pills and icons, which
   a colour change cannot — it just belongs on a named state. */
/* SELECTED — the current record in a master list. Four independent names for
   this existed (sk-card-active, tpl-row-active, is-latest, a bare .active),
   and the first cluster to migrate had to borrow .dw-menu-item.is-on, whose
   own comment says six files needed to say this and none of them could.
   A master list is not a dropdown, so it gets the state properly.
   A wash plus an accent edge, never a saturated fill: the selected row still
   has to be readable, and it is usually the row you are reading. */
.dw-row.is-selected, .dw-card.is-selected {
	background: var(--dw-accent-wash);
	border-left: 2px solid var(--dw-accent);
}
.dw-row.is-selected { padding-left: calc(var(--dw-card-pad) - 2px); }

.dw-row.is-quiet { opacity: .62; }

/* The counterpart: still asking. A leading accent edge rather than a fill,
   so twenty unread rows do not read as twenty warnings. */
/* A LEADING EDGE, not an inset box-shadow. The first attempt used
   `box-shadow: inset 2px 0 0` and the gate rejected it, correctly: the flat
   rule bans box-shadow outright rather than trying to distinguish an edge from
   an elevation cue, and widening it would reopen exactly the loophole it
   closes. A border with compensating padding says the same thing and keeps
   every row's content on the same left edge. */
.dw-row.is-unread {
	border-left: 2px solid var(--dw-accent);
	padding-left: calc(var(--dw-card-pad) - 2px);
}
/* Takes the slack and truncates; 220px is the point below which a title and
   its meta stop being readable side by side with a control. */
.dw-row-main { flex: 1 1 220px; min-width: 0; }
.dw-row-title {
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
}
.dw-row-meta { font-size: var(--dw-size-micro); color: var(--dw-faint); }
.dw-row-actions {
	display: flex;
	align-items: center;
	gap: var(--dw-space-2);
	flex: none;
	margin-left: auto;
}

/* The applied-filter row. Present only when something is filtering, so it
   collapses to nothing rather than leaving a gap. */
.dw-applied {
	display: flex;
	flex-wrap: wrap;
	gap: var(--dw-space-2);
	align-items: center;
}
.dw-applied:empty { display: none; }

/* DETAIL, 11 pages. header, status strip, tabs, specs, actions. */
.dw-page.is-detail .dw-detail-grid {
	display: grid;
	gap: var(--dw-space-5);
	grid-template-columns: minmax(0, 2fr) minmax(240px, 1fr);
}
@media (max-width: 860px) {
	.dw-page.is-detail .dw-detail-grid { grid-template-columns: minmax(0, 1fr); }
}
.dw-statusstrip {
	display: flex; flex-wrap: wrap; align-items: center; gap: var(--dw-space-4);
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
}

/* DASHBOARD, 7 pages. header, KPI row, panel grid, feed.
   The grid itself moved OUT of this selector and became a component below. It
   had to: `.dw-page.is-dashboard .dw-panelgrid` scores higher than
   `.dw-panelgrid.is-split`, so on a dashboard the split variant would have
   been silently overruled by the auto-fit default and nobody would have known
   why. The archetype keeps only what is its own — neutralising a stray margin
   so its own gap sets the rhythm, exactly as is-list does for its card. */
.dw-page.is-dashboard > .dw-panelgrid { margin: 0; }

/* The panel grid. Auto-fit is right for the 5 dashboards that hold equal
   panels; the 7/5 split is for the one page with a subject and a sidebar. Both
   minmax(0, …) so a wide child — a table, a code block — cannot blow the
   column out past its track, which is the auto-fit failure mode. */
.dw-panelgrid {
	display: grid;
	gap: var(--dw-space-4);
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.dw-panelgrid.is-split { grid-template-columns: minmax(0, 7fr) minmax(280px, 5fr); }
@media (max-width: 860px) {
	.dw-panelgrid.is-split { grid-template-columns: minmax(0, 1fr); }
}

/* MASTER-DETAIL. Four independent implementations of this already exist, which
   is the usual sign. A list column of ~320px that scrolls on its own beside a
   detail pane that scrolls with the page.

   The list is the FIRST CHILD rather than a named part: the component is one
   class in the spec and giving the column its own class would mean every one
   of the four sites renames an element it already has.

   Not .dw-editorgrid: that is a two-column working surface inside one bordered
   box, and it does not scroll independently. */
.dw-splitpane {
	display: grid;
	gap: var(--dw-space-5);
	grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
	align-items: start;
}
.dw-splitpane > :first-child {
	position: sticky;
	top: calc(var(--dw-header-h) + var(--dw-space-3));
	max-height: calc(100vh - var(--dw-header-h) - var(--dw-space-6));
	overflow-y: auto;
	min-height: 0;
}
@media (max-width: 900px) {
	.dw-splitpane { grid-template-columns: minmax(0, 1fr); }
	/* Sticky in a single-column layout is not a master-detail affordance, it
	   is a list that refuses to scroll away. It comes off. */
	.dw-splitpane > :first-child { position: static; max-height: none; overflow: visible; }
}

/* SETTINGS, 8 pages. header, section list, field groups, save bar.

   THE LABEL COLUMN IS A FIXED MEASURE, not a fraction of the row, and the
   fraction is what was wrong with it. `minmax(200px, 1fr) minmax(0, 2fr)`
   handed a third of the row to the labels: measured 255px on the profile form
   at 1440, for words like "Job" and "Email", pushing every control a quarter of
   the way across a screen the eye then has to travel back from. A fraction
   scales the labels with the viewport, which is the one thing a label does not
   need — its width is set by the longest word in it, and that does not change
   when the window does.

   28ch fits the longest label these pages actually carry ("Organization
   credentials can be used…" wraps, and is meant to) while giving the controls
   the room. It still collapses to one column below 760px. */
.dw-page.is-settings .dw-settinggroup {
	display: grid; gap: var(--dw-space-4);
	grid-template-columns: minmax(0, 28ch) minmax(0, 1fr);
	padding: var(--dw-space-5) 0;
	border-bottom: var(--dw-border) solid var(--dw-line);
}
@media (max-width: 760px) {
	.dw-page.is-settings .dw-settinggroup { grid-template-columns: minmax(0, 1fr); }
}
.dw-savebar {
	position: sticky; bottom: 0; z-index: var(--dw-z-sticky);
	display: flex; justify-content: flex-end; gap: var(--dw-space-2);
	padding: var(--dw-space-3) var(--dw-space-4);
	border-top: var(--dw-border) solid var(--dw-line-strong);
	background: var(--dw-surface);
	transition: opacity var(--dw-dur-fast) var(--dw-ease);
}

/* A save bar with nothing to save is worse than no save bar, so it recedes
   rather than disappearing: vanishing would reflow the page under the reader
   on every keystroke.
   Trap found on the first settings page — sticky silently stops working under
   an overflow:hidden ancestor, and .dw-card is one, so wrap it in
   .dw-card.is-open. */
/* OPT IN TO DIMMING, do not opt out of it. The first version dimmed any bar
   without .is-dirty, so every page that does not track a dirty state — the
   reference page included — rendered its save bar at .62 forever. A component
   whose DEFAULT is the degraded state looks broken until someone finds the
   opt-out. */
.dw-savebar.is-clean { opacity: .62; }

/* NARROW, a MODIFIER rather than an archetype. Any archetype can take it.
   There was a wizard archetype here until the data said otherwise: the
   stepper markup was almost entirely agent-wizard, which has since been
   deleted, leaving mcp-setup as the only user. One page is a page, not a type.
   So the stepper stays a component (.dw-steps) and the narrow column that
   suited it becomes reusable. */
.dw-page.is-narrow { max-width: 780px; width: 100%; }

/* EDITOR, 5 pages. Full bleed, a tree or list, and a working surface.
   This was the weakest part of the kit: the survey found .dw-page.is-editor and
   .dw-editorgrid with ZERO uses, because neither fitted a real page.

   THE HEIGHT CHAIN, which is the thing that was actually missing. .dw-page set
   no height at all, so five files carry six different ad-hoc formulas — 62vh,
   calc(100vh - 190px), calc(90vh - 25px) and three more. Every one of them is
   a guess at the same number, and every one is wrong on some window. It is one
   formula, in tokens, so a skin that changes the header height keeps working. */
/* HEIGHT AND MIN-HEIGHT BOTH, and that is not belt-and-braces. min-height
   alone cannot cap anything: a pane holding 9,081 rows grew the grid to
   2,735px and scrolled the DOCUMENT 2,152px while neither pane body
   scrolled. Setting height alone does not work either, because min-height
   clamps upward and wins. Measured on the first real editor page.

   --dw-editor-chrome is what sits above and below the content box: 64px
   header margin, 20+20 padding, 63px footer = 167px, measured with
   getBoundingClientRect rather than guessed. The archetype previously
   subtracted --dw-space-6 and left 71px of residual page scroll.
   It is a token so a page with different chrome can correct it without
   inventing a seventh vh formula — six already existed across five files,
   which is why this archetype exists at all. */
.dw-page.is-editor {
	max-width: none;
	height: calc(100vh - var(--dw-editor-chrome));
	min-height: calc(100vh - var(--dw-editor-chrome));
}

/* Below the breakpoint the pane layout is a single column, and a pinned
   height then squeezes the whole editor into 121px on a phone. Release it. */
@media (max-width: 1000px) {
	.dw-page.is-editor { height: auto; min-height: 0; }
}
/* AND A FLOOR. The release above is on WIDTH only, so a short-but-wide
   viewport kept the pin: at 1280x720 the file tree measured 32px tall. A
   laptop in a video call is exactly that shape. */
@media (max-height: 806px) and (min-width: 1001px) {
	.dw-page.is-editor { height: auto; min-height: 640px; }
}
/* The head reports its own size and never gets squeezed. */
.dw-page.is-editor > .dw-pagehead { flex: none; }
/* THE FLEXBOX OVERFLOW TRAP, and the reason those six formulas exist. A flex
   item's min-height computes to auto, which is its CONTENT height — so a pane
   holding a 5,000-line log grows the column to 5,000 lines instead of
   scrolling inside it, and the page scrolls instead. Authors then reach for a
   fixed vh to cap it. min-height: 0 is the actual fix, and it has to be on the
   scrolling child, not on the container. */
/* A SLOT, NOT ONE COMPONENT. This named .dw-editorgrid alone, and it is the
   only selector that can consume the height — so a page whose grid sits two
   levels down inside a tab got nothing: measured 1,043px of DOCUMENT scroll
   with no pane body scrolling at all. Three pages hit it. Any of these may
   carry the chain. */
.dw-page.is-editor > .dw-editorgrid,
.dw-page.is-editor > .dw-editorbody,
.dw-page.is-editor > .dw-pane,
.dw-page.is-editor > .dw-splitpane { flex: 1 1 auto; min-height: 0; }
.dw-editorbody { display: flex; flex-direction: column; min-height: 0; }

/* TWO columns, not three. The third was an inspector, and not one surveyed
   page is a tree | surface | inspector: they are all a list beside a surface.
   A speculative column that no page fills is a column every page has to
   remember to leave empty. */
.dw-editorgrid {
	display: grid;
	/* The gap IS the divider — one hairline of the background showing through,
	   so there is no border to keep in sync between adjacent panes. */
	gap: var(--dw-border);
	background: var(--dw-line);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	grid-template-columns: minmax(200px, 320px) minmax(0, 1fr);
	overflow: hidden;
}
@media (max-width: 1000px) {
	.dw-editorgrid { grid-template-columns: minmax(0, 1fr); }
}
/* min-height: 0 belongs beside min-width: 0 and was missing. With a child
   that is not a .dw-pane, the cell grew to 2,765px inside a 583px grid and
   overflow:hidden ATE 2,182px — no scrollbar, no page scroll, content simply
   gone. A grid item defaults to min-height:auto, which refuses to shrink. */
.dw-editorgrid > * {
	background: var(--dw-surface);
	padding: var(--dw-space-4);
	min-width: 0;
	min-height: 0;
}

/* A pane whose BODY SCROLLS and whose head carries actions.
   Not .dw-card: a card is overflow: hidden with a title/subtitle head, so its
   content is clipped rather than scrolled and its head has nowhere to put a
   toolbar. A pane is the editor's unit; a card is the list's. */
/* position: relative IS LOAD-BEARING, for the same reason .dw-card carries it.
   #flow-lines-bg is a fixed canvas at z-index 0. A positioned element paints
   above it; an unpositioned one paints its background in the block-background
   layer, UNDERNEATH. .dw-card was positioned and stayed clean, .dw-pane was
   not, so on the editor archetype the ambient ribbons ran straight across the
   schema list and the ticket rows. Found in a screenshot, not by any check. */
.dw-pane {
	position: relative;
	display: flex;
	flex-direction: column;
	min-height: 0;
	background: var(--dw-surface);
	/* The head and the body bring their own padding, so the pane opts out of
	   the grid cell's. Same specificity as `.dw-editorgrid > *` above, so this
	   rule must stay AFTER it in this file. */
	padding: 0;
}
.dw-pane-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--dw-space-3);
	flex: none;
	padding: var(--dw-space-3) var(--dw-space-4);
	border-bottom: var(--dw-border) solid var(--dw-line);
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
}
.dw-pane-meta {
	font-size: var(--dw-size-micro);
	font-weight: var(--dw-weight-body);
	color: var(--dw-faint);
	white-space: nowrap;
}
.dw-pane-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
	padding: var(--dw-space-4);
}

/* A FILE TREE. ~20 uses. Not .dw-menu-item: that is a flat row with no indent,
   no selection state and no chevron, and a tree is exactly those three things.

   INDENT IS AN INPUT, NOT A TOKEN. Depth is per-node data, so the node reads
   --dw-tree-depth, which the page sets inline: style="--dw-tree-depth: 2".
   It is not defined in tokens.css because it is not a theme value; the
   fallback of 0 means an unset node renders flush rather than broken. */
.dw-tree { margin: 0; padding: 0; list-style: none; font-size: var(--dw-size-small); }
.dw-tree-node {
	display: flex;
	align-items: center;
	gap: var(--dw-space-2);
	width: 100%;
	padding: 5px var(--dw-space-2);
	padding-left: calc(var(--dw-space-2) + var(--dw-space-4) * var(--dw-tree-depth, 0));
	border: 0;
	border-radius: var(--dw-radius-sm);
	background: transparent;
	color: var(--dw-muted);
	font: inherit;
	font-size: var(--dw-size-small);
	text-align: left;
	cursor: pointer;
	transition: background var(--dw-dur-fast) var(--dw-ease),
		color var(--dw-dur-fast) var(--dw-ease);
}
.dw-tree-node:hover { background: var(--dw-surface-hover); color: var(--dw-text); }
.dw-tree-node:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: -2px; }
.dw-tree-chev {
	flex: none;
	width: var(--dw-icon-size);
	color: var(--dw-faint);
	transition: transform var(--dw-dur-fast) var(--dw-ease);
}
.dw-tree-icon { flex: none; color: var(--dw-faint); font-size: var(--dw-icon-size); }
.dw-tree-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dw-tree-meta { flex: none; font-size: var(--dw-size-eyebrow); color: var(--dw-faint); }
/* A directory reads as structure, a file as content. */
.dw-tree-node.is-dir { color: var(--dw-text); font-weight: var(--dw-weight-medium); }
.dw-tree-node.is-sel { background: var(--dw-accent-wash); color: var(--dw-accent-text); }
/* The drop target during a drag. An inset outline rather than a border, so the
   row does not change size and shove the rest of the tree down by 2px at the
   exact moment the pointer is trying to hit it. */
/* The tree itself is a drop target, not only a row: a file dropped into
   empty space below the last node still belongs somewhere. */
.dw-tree.is-drop { outline: 2px dashed var(--dw-accent); outline-offset: -2px; }
.dw-tree-node.is-drop { outline: 2px solid var(--dw-accent); outline-offset: -2px; }

/* ----------------------------------------------------------------- modal
   810 uses across the tree, 88 separate dialogs. Built in the main document:
   a srcless iframe as a render surface is refused by the sandbox. */
/* The width belongs HERE, on the standalone kit dialog, not on .dw-modal.
   .dw-modal is documented as SKINNING a Bootstrap modal by going on
   .modal-content — where Bootstrap's .modal-dialog already owns the width,
   so a max-width of 560px silently shrank every .modal-lg it touched.
   A veil only exists for a kit-owned dialog, which makes it the right scope. */
.dw-modal-veil > .dw-modal { max-width: 560px; }
/* .is-lg had exactly the defect .dw-modal was fixed for one commit earlier:
   as a skin on Bootstrap .modal-content it capped a .modal-xl at 860px and
   cost a terminal ~280px. Scope it to the kit-owned dialog too. */
.dw-modal-veil > 

.dw-modal-veil {
	position: fixed; inset: 0; z-index: var(--dw-z-veil);
	display: flex; align-items: center; justify-content: center;
	padding: var(--dw-space-4);
	background: var(--dw-veil);
}
.dw-modal {
	width: 100%; max-height: 86vh;
	display: flex; flex-direction: column;
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	box-shadow: var(--dw-shadow);
	overflow: hidden;
}
.dw-modal.is-lg { max-width: 860px; }
.dw-modal-head {
	display: flex; align-items: center; justify-content: space-between;
	gap: var(--dw-space-3);
	padding: var(--dw-space-4);
	border-bottom: var(--dw-border) solid var(--dw-line);
}
.dw-modal-body { padding: var(--dw-space-4); overflow-y: auto; }
.dw-modal-foot {
	display: flex; justify-content: flex-end; gap: var(--dw-space-2);
	padding: var(--dw-space-4);
	border-top: var(--dw-border) solid var(--dw-line);
}

/* --------------------------------------------------------------- stepper */
.dw-steps { display: flex; flex-wrap: wrap; gap: var(--dw-space-2); margin-bottom: var(--dw-space-5); }
.dw-step {
	flex: 1 1 140px;
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface);
	border-top: 2px solid var(--dw-line-strong);
}
.dw-step-n {
	font-size: var(--dw-size-eyebrow); letter-spacing: var(--dw-track-label);
	text-transform: uppercase; color: var(--dw-faint);
}
.dw-step-label { font-size: var(--dw-size-small); color: var(--dw-muted); margin-top: 2px; }
.dw-step.is-done { border-top-color: var(--dw-ok); }
.dw-step.is-done .dw-step-n { color: var(--dw-ok); }
.dw-step.is-on { border-top-color: var(--dw-accent); }
.dw-step.is-on .dw-step-n { color: var(--dw-accent-text); }
.dw-step.is-on .dw-step-label { color: var(--dw-text); font-weight: var(--dw-weight-medium); }

/* ----------------------------------------------------------------- toast
   The one component that never drifted, because there is exactly one
   renderer and no page-level markup. Tokenized so a skin can reposition and
   restyle it without touching that renderer. */
/* z-index 60 put the toast UNDER the platform header, which is fixed at
   z-index 997 and 64px tall — elementFromPoint at the toast centre returned
   the header. It was invisible on all 56 pages. The default now clears the
   header by reading its own layout token. */
.dw-toasts {
	position: fixed; z-index: var(--dw-z-toast);
	top: var(--dw-toast-top); right: var(--dw-toast-side);
	display: flex; flex-direction: column; gap: var(--dw-space-2);
	pointer-events: none;
}
.dw-toast {
	min-width: 240px; max-width: 380px;
	display: flex; align-items: flex-start; gap: var(--dw-space-3);
	padding: var(--dw-space-3) var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line-strong);
	border-left: var(--dw-toast-rail) solid var(--dw-muted);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface);
	color: var(--dw-text);
	font-size: var(--dw-size-small);
	box-shadow: var(--dw-shadow);
	pointer-events: auto;
}
.dw-toast.is-ok { border-left-color: var(--dw-ok); }
.dw-toast.is-warn { border-left-color: var(--dw-warn); }
.dw-toast.is-bad { border-left-color: var(--dw-bad); }

/* ------------------------------------------------------- checks, switches
   264 uses of form-check / form-switch today, all Bootstrap defaults that no
   skin can reach. */
/* A COLOUR WELL. Sized as layout, never re-coloured — the whole point of the
   control is that its fill is the user's data, so a kit that tints it would
   be lying about the value. Five uses on one page. */
.dw-input[type="color"] {
	width: 44px;
	height: 32px;
	padding: 2px;
	cursor: pointer;
}

/* A RANGE. Only reachable through vendor pseudo-elements, which is why two
   pages kept it private; the track and thumb are tokenised here so a skin
   reaches them, and the progress fill a page was drawing with a lit gradient
   is deliberately not reproduced — the system is flat. */
.dw-range {
	width: 100%;
	height: 18px;
	appearance: none;
	background: transparent;
	cursor: pointer;
}
.dw-range::-webkit-slider-runnable-track {
	height: 4px;
	border-radius: var(--dw-radius-pill);
	background: var(--dw-surface-2);
}
.dw-range::-moz-range-track {
	height: 4px;
	border-radius: var(--dw-radius-pill);
	background: var(--dw-surface-2);
}
.dw-range::-webkit-slider-thumb {
	appearance: none;
	margin-top: -5px;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	background: var(--dw-accent);
}
.dw-range::-moz-range-thumb {
	width: 14px;
	height: 14px;
	border: 0;
	border-radius: 50%;
	background: var(--dw-accent);
}
.dw-range:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 2px; }

.dw-check { display: flex; align-items: center; gap: var(--dw-space-2); font-size: var(--dw-size-base); }
.dw-check input {
	width: 15px; height: 15px; flex: none; margin: 0;
	accent-color: var(--dw-accent);
}
/* THE PRIORITY FIX IN THIS PASS. Read this before changing anything here.

   The kit shipped .dw-switch as a <div> plus a JS-set .is-on. All 46 real
   switches on the platform are <input type="checkbox" role="switch"> with a
   <label for=>. Rolling the div version out would have REMOVED KEYBOARD ACCESS
   FROM 46 CONTROLS: a div is not in the tab order, does not toggle on Space,
   reports no checked state to a screen reader, and submits nothing with a
   form. It would have looked like a successful migration on every screenshot.

   So the track is driven by :checked on a real input that is still in the DOM
   and still focusable — only visually hidden. .is-on is kept alongside it, at
   the same specificity, for the cases that are genuinely JS-driven state
   rather than a form control.

   MARKUP CONTRACT — the input must be the track's immediately preceding
   sibling, because :checked reaches the track through `+`:
     <label class="dw-switch">
       <input type="checkbox" role="switch">
       <span class="dw-switch-track"></span>
       <span>Label text</span>
     </label> */
/* position: relative IS THE LOAD-BEARING LINE. The real checkbox inside is
   position: absolute, so without a positioned parent it resolves against the
   nearest positioned ancestor instead — inside a .dw-modal that is the
   dialog. Measured: focusing the switch by keyboard scrolled the DIALOG to
   594 while .dw-modal-body stayed at 0, dragging .dw-modal-head to top:-565
   and out of the overflow:hidden box. The dialog appeared to lose its own
   header. Keyboard-only, invisible on load, and it would have reached every
   one of the 46 switches on the platform that sits inside a dialog. */
.dw-switch {
	position: relative;
	display: inline-flex;
	align-items: center;
	gap: var(--dw-space-3);
	cursor: pointer;
}
/* Hidden from the eye, not from the tab order or the accessibility tree.
   display: none and visibility: hidden would both take the focus away again,
   which is the bug this rule exists to avoid. Same technique as
   .dw-visually-hidden; restated rather than composed so the markup needs one
   class, not two. */
.dw-switch input {
	position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
	overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.dw-switch-track {
	width: 34px; height: 19px; flex: none;
	border-radius: var(--dw-radius-pill);
	background: var(--dw-surface-2);
	border: var(--dw-border) solid var(--dw-line-strong);
	position: relative;
	transition: background var(--dw-dur-fast) var(--dw-ease),
		border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-switch-track::after {
	content: ""; position: absolute; top: 2px; left: 2px;
	width: 13px; height: 13px; border-radius: 50%;
	background: var(--dw-muted);
	transition: transform var(--dw-dur-fast) var(--dw-ease),
		background var(--dw-dur-fast) var(--dw-ease);
}
.dw-switch input:checked + .dw-switch-track,
.dw-switch.is-on .dw-switch-track { background: var(--dw-accent); border-color: var(--dw-accent); }
.dw-switch input:checked + .dw-switch-track::after,
.dw-switch.is-on .dw-switch-track::after { transform: translateX(15px); background: var(--dw-accent-ink); }
/* The focus ring has to be drawn on the TRACK: the element that receives focus
   is the hidden one, so without this the switch is operable by keyboard and
   invisible while focused — the same defect one step further along. */
.dw-switch input:focus-visible + .dw-switch-track {
	outline: 2px solid var(--dw-focus);
	outline-offset: 2px;
}
.dw-switch input[disabled] + .dw-switch-track { opacity: .45; }

/* An input with an INTERACTIVE addon: the password-visibility toggle, 4 uses.
   Not .dw-search: its icon is position: absolute, pointer-events: none and
   leading-only, so it can hold a glyph and cannot hold a button. */
.dw-inputgroup { display: flex; align-items: stretch; }
.dw-inputgroup .dw-input {
	border-top-right-radius: 0;
	border-bottom-right-radius: 0;
}
.dw-inputgroup .dw-btn {
	flex: none;
	/* -1px collapses the doubled hairline where the two boxes meet, the same
	   way .dw-btngroup does it. */
	margin-left: -1px;
	border-radius: 0 var(--dw-radius-sm) var(--dw-radius-sm) 0;
	border-color: var(--dw-line-strong);
	color: var(--dw-muted);
}
.dw-inputgroup .dw-btn:hover { color: var(--dw-text); }
/* The addon must not sit on top of the input's focus ring, which is drawn
   outside the box and would otherwise be half-covered by the button. */
.dw-inputgroup .dw-input:focus { position: relative; z-index: 1; }

/* ------------------------------------------------------------ search box */
.dw-search { position: relative; flex: 1 1 200px; min-width: 160px; }
.dw-search .dw-input { padding-left: 32px; }
.dw-search .dw-icon {
	position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
	color: var(--dw-faint); pointer-events: none;
}

/* ---------------------------------------------------------- dropdown menu */
.dw-menu {
	min-width: 180px; padding: var(--dw-space-1);
	border: var(--dw-border) solid var(--dw-line-strong);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface);
	box-shadow: var(--dw-shadow);
}
.dw-menu-item {
	/* So a nested .dw-menu.is-flyout anchors to THIS ROW rather than to the
	   page. No visual effect on its own. */
	position: relative;
	display: flex; align-items: center; gap: var(--dw-space-2); width: 100%;
	padding: 7px 11px; border: 0; border-radius: var(--dw-radius-sm);
	background: transparent; color: var(--dw-text); font: inherit;
	font-size: var(--dw-size-small); text-align: left; cursor: pointer;
	transition: background var(--dw-dur-fast) var(--dw-ease);
}
.dw-menu-item:hover { background: var(--dw-surface-hover); }
.dw-menu-item.is-danger { color: var(--dw-bad); }
/* The CHOSEN item in a filter menu, which a hover style cannot express: hover
   says "this is where the pointer is", this says "this is what the list is
   showing you". 6 files tree-wide had to say it and none of them could. */
.dw-menu-item.is-on {
	background: var(--dw-accent-wash);
	color: var(--dw-accent-text);
	font-weight: var(--dw-weight-strong);
}
.dw-menu-sep { height: 1px; margin: var(--dw-space-1) 0; background: var(--dw-line); }

/* A second-level menu: it opens BESIDE its parent row, not below the trigger,
   which is what a filter submenu needs — the first level stays on screen so
   you can see which branch you are in. Host it inside the .dw-menu-item it
   belongs to; that row is its containing block. Use a <div> for that row, not
   a <button>: a menu nested inside a button is invalid, and the flyout's own
   buttons would be unreachable. */
.dw-menu.is-flyout {
	position: absolute;
	top: calc(-1 * var(--dw-space-1));
	left: 100%;
	z-index: 2;
}

/* ------------------------------------------------------------ breadcrumb */
.dw-crumbs {
	display: flex; flex-wrap: wrap; align-items: center; gap: var(--dw-space-2);
	margin: 0 0 var(--dw-space-3); padding: 0; list-style: none;
	font-size: var(--dw-size-micro); color: var(--dw-faint);
}
.dw-crumbs li { display: flex; align-items: center; gap: var(--dw-space-2); }
.dw-crumbs li + li::before { content: "/"; color: var(--dw-faint); opacity: .6; }
.dw-crumbs a { color: var(--dw-muted); text-decoration: none; }
.dw-crumbs a:hover { color: var(--dw-text); }

/* ---------------------------------------------------------------- avatar
   SIZE IS A TOKEN, not a width, and that is the whole point of this rewrite.
   The 120px identity portrait on users-profile.html was an #id rule in a base
   layer: (1,0,0), so no kit class and no skin could reach it, and it carried a
   hardcoded #2b384e that measured 1.3:1 on the abyss ground. A component that
   only comes in the sizes the kit thought of is a component every page
   eventually forks. Set --dw-avatar-size for anything the states below miss.
   It is declared in tokens.css rather than here, so a skin retunes every
   avatar at once from :root -- a default declared on .dw-avatar itself would
   sit on the element and beat any :root rule a skin could write.

   The initials size DERIVES from it, so a custom size stays in proportion with
   no second declaration. max() keeps the 28px default at exactly the 11px it
   already was; .is-lg keeps its 12px explicitly, because 40px * .36 would have
   grown it and this rewrite is not allowed to move the 56 pages already using
   these two. */
.dw-avatar {
	width: var(--dw-avatar-size); height: var(--dw-avatar-size); flex: none;
	border-radius: 50%; object-fit: cover; overflow: hidden;
	border: var(--dw-border) solid var(--dw-line-strong);
	background: var(--dw-surface-2);
	display: inline-flex; align-items: center; justify-content: center;
	font-size: max(var(--dw-size-micro), calc(var(--dw-avatar-size) * .36));
	font-weight: var(--dw-weight-strong);
	color: var(--dw-muted);
	-webkit-user-select: none; user-select: none;
}
.dw-avatar.is-lg { --dw-avatar-size: 40px; font-size: var(--dw-size-small); }
.dw-avatar.is-xl { --dw-avatar-size: 64px; }
.dw-avatar.is-2xl { --dw-avatar-size: 112px; }

/* The element may BE the image or CONTAIN one. Containing one is what lets a
   single box hold initials until a photo exists and then hold the photo, with
   no second element and no layout shift at the swap -- which is what the page
   this was built for did with an icon and an <img> stacked, each toggled by a
   class the other did not know about. */
.dw-avatar > img {
	width: 100%; height: 100%;
	object-fit: cover; border-radius: inherit; display: block;
}

/* TONES, for the initials state. Not decoration: a list of forty people whose
   avatars are all the same grey circle is forty identical rows. Each tone is a
   wash plus the ink already proven readable on it, so it survives every skin,
   unlike a colour picked here. WHICH tone a person gets is decided by a table
   in utils/dw-avatar.js -- this file only says what each one looks like. */
.dw-avatar.is-accent { background: var(--dw-accent-wash); color: var(--dw-accent-text); }
.dw-avatar.is-info { background: var(--dw-info-wash); color: var(--dw-info); }
.dw-avatar.is-ok { background: var(--dw-ok-wash); color: var(--dw-ok); }
.dw-avatar.is-warn { background: var(--dw-warn-wash); color: var(--dw-warn); }
.dw-avatar.is-bad { background: var(--dw-bad-wash); color: var(--dw-bad); }
.dw-avatar.is-brand { background: var(--dw-surface-2); color: var(--dw-brand-text); }

/* ---------------------------------------------------------- avatar field
   The control that EDITS a .dw-avatar. Hover is a HINT here, never the
   affordance: the previous version of this control put its only Change and
   Remove buttons inside a `:hover { opacity: 1 }` overlay, which is nothing at
   all on a touch screen and nothing at all to a keyboard. The portrait is a
   real <button>, the scrim tells you so, and the same two actions also sit
   below it as ordinary buttons that are always there. */
.dw-avatarfield {
	display: inline-flex; flex-direction: column; align-items: center;
	gap: var(--dw-space-3);
}
.dw-avatarfield-btn {
	position: relative; display: inline-flex;
	padding: 0; border: 0; background: none; border-radius: 50%;
	font: inherit; color: inherit; cursor: pointer;
}
.dw-avatarfield-btn:focus-visible { outline: 2px solid var(--dw-focus); outline-offset: 3px; }
.dw-avatarfield-hint {
	position: absolute; inset: 0; border-radius: 50%;
	display: flex; flex-direction: column; align-items: center; justify-content: center;
	gap: var(--dw-space-1);
	background: var(--dw-scrim); color: var(--dw-scrim-ink);
	font-size: var(--dw-size-eyebrow); font-weight: var(--dw-weight-strong);
	text-transform: uppercase; letter-spacing: var(--dw-track-label);
	opacity: 0; transition: opacity var(--dw-dur-fast) var(--dw-ease);
	pointer-events: none;
}
.dw-avatarfield-btn:hover .dw-avatarfield-hint,
.dw-avatarfield-btn:focus-visible .dw-avatarfield-hint,
.dw-avatarfield.is-dragover .dw-avatarfield-hint,
.dw-avatarfield.is-busy .dw-avatarfield-hint { opacity: 1; }

/* The scrim is 68% opaque, which is right over a PHOTO -- you want to see what
   you are replacing -- and wrong over initials, where two large letters read
   straight through it and collide with the word on top. So the avatar's own
   ink goes transparent for the duration. The tone ground stays, so the circle
   still belongs to the person; only the glyph that would fight the label goes. */
.dw-avatarfield-btn:hover .dw-avatar,
.dw-avatarfield-btn:focus-visible .dw-avatar,
.dw-avatarfield.is-dragover .dw-avatar,
.dw-avatarfield.is-busy .dw-avatar { color: transparent; }
.dw-avatarfield.is-dragover .dw-avatar { border-color: var(--dw-accent); }
.dw-avatarfield.is-busy .dw-avatarfield-btn { pointer-events: none; }
.dw-avatarfield-actions {
	display: flex; flex-wrap: wrap; justify-content: center; gap: var(--dw-space-2);
}
.dw-avatarfield-note {
	margin: 0;
	font-size: var(--dw-size-micro); color: var(--dw-faint); text-align: center;
	max-width: 34ch;
}

/* ----------------------------------------------------------- button group */
.dw-btngroup { display: inline-flex; }
.dw-btngroup .dw-btn { border-radius: 0; margin-left: -1px; }
.dw-btngroup .dw-btn:first-child {
	border-top-left-radius: var(--dw-radius-pill);
	border-bottom-left-radius: var(--dw-radius-pill);
	margin-left: 0;
}
.dw-btngroup .dw-btn:last-child {
	border-top-right-radius: var(--dw-radius-pill);
	border-bottom-right-radius: var(--dw-radius-pill);
}

/* ------------------------------------------------------------------ icon
   998 uses of Bootstrap Icons `bi` in the tree. The icon SET stays; this only
   gives it a consistent size and optical alignment, which it does not have
   today. Icons inherit colour, so they follow the skin for free. */
.dw-icon, .bi {
	display: inline-block; line-height: 1;
	font-size: var(--dw-icon-size);
	vertical-align: -.09em;
}

/* -------------------------------------------------------------- dropzone
   A file drop TARGET, across 3 upload surfaces. Not .dw-empty: an empty state
   reports that nothing is here, a drop zone asks for something. They look
   similar and mean opposite things, which is why two of the three built theirs
   by copying an empty state and then fighting it. */
.dw-dropzone {
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--dw-space-2);
	padding: var(--dw-space-6) var(--dw-space-4);
	border: 2px dashed var(--dw-line-strong);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	color: var(--dw-muted);
	font-size: var(--dw-size-small);
	text-align: center;
	cursor: pointer;
	transition: border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-dropzone.is-over { border-color: var(--dw-accent); }
/* The over-state lays --dw-veil OVER the content rather than recolouring the
   box. A drop zone is often stacked on the preview of the file about to be
   replaced, and tinting the background loses exactly the thing the user is
   checking. The existing veil token already means "the layer behind is not the
   subject right now", which is precisely this. */
.dw-dropzone.is-over::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: inherit;
	background: var(--dw-veil);
	pointer-events: none;
}

/* ------------------------------------------------------------ asset tile
   A media grid tile. Two implementations exist and one is already drifting
   into inline style= attributes, which is the last stop before a component
   stops being reachable by any skin at all. */
.dw-assettile {
	display: flex;
	flex-direction: column;
	gap: var(--dw-space-2);
	padding: var(--dw-space-2);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface);
	cursor: pointer;
	/* SELECTION IS AN OUTLINE, DELIBERATELY. A border or a scale on the
	   selected tile changes its box, which reflows the whole grid as the user
	   arrows through it — the selection appears to run away from the cursor.
	   An outline is painted outside the box and moves nothing. Declared
	   transparent up front so only the colour transitions. */
	outline: 2px solid transparent;
	outline-offset: -1px;
	transition: outline-color var(--dw-dur-fast) var(--dw-ease),
		border-color var(--dw-dur-fast) var(--dw-ease);
}
.dw-assettile:hover { border-color: var(--dw-line-strong); }
.dw-assettile.is-on { outline-color: var(--dw-accent); }
.dw-assettile:focus-visible { outline-color: var(--dw-focus); }
/* 1:1 and cover, so a grid of portrait, landscape and square uploads is still
   a grid. Also the MIME-icon fallback box: when there is no thumbnail the page
   drops an icon in here and it is already centred. */
.dw-assettile-thumb {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	aspect-ratio: 1 / 1;
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	color: var(--dw-faint);
	font-size: calc(var(--dw-icon-size) * 1.8);
	object-fit: cover;
	overflow: hidden;
}
.dw-assettile-name {
	font-size: var(--dw-size-micro);
	color: var(--dw-text);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.dw-assettile-meta { font-size: var(--dw-size-eyebrow); color: var(--dw-faint); }

/* A CONNECTION ROW WAS SPECIFIED HERE AND DELIBERATELY NOT BUILT.
   Three implementations of "connected, until when, and the control that
   changes it" exist today, so the need is real — but the component was going
   to be .dw-row with a border, and the two facts it carries are already a
   .dw-pill and a .dw-btn. Compose it: .dw-row, verdict pill in the meta slot,
   action in the actions slot.
   Recorded rather than silently dropped, because "three implementations exist"
   is a strong argument and someone will reach this conclusion again. The
   answer is that three implementations of a ROW want one row, not a fourth
   name for the same shape. */

/* --------------------------------------------------------------- finding
   A scan finding. Written twice already, in two JS files, with different field
   sets. Not .dw-alert: that is one message, one colour and no parts. Not
   .dw-card: a finding appears 5 to 50 times in a single modal, and 50 bordered
   surfaces is a wall rather than a list.

   The severity rail reuses the sev-* scale the pill already defines, so a skin
   recolours severity once and both follow. --dw-toast-rail is reused for its
   width for the same reason: a skin that decides tone rails are not part of
   its personality sets it to 0 and every rail on the platform goes. */
.dw-finding {
	padding: var(--dw-space-3) var(--dw-space-4);
	border-left: var(--dw-toast-rail) solid var(--dw-line-strong);
	border-bottom: var(--dw-border) solid var(--dw-line);
	background: var(--dw-surface);
}
.dw-finding:last-child { border-bottom: 0; }
.dw-finding.sev-critical { border-left-color: var(--dw-bad); }
.dw-finding.sev-high { border-left-color: var(--dw-warn); }
.dw-finding.sev-medium { border-left-color: var(--dw-accent); }
.dw-finding.sev-low { border-left-color: var(--dw-line-strong); }
.dw-finding.sev-info { border-left-color: var(--dw-info); }
.dw-finding.sev-none { border-left-color: var(--dw-ok); }
.dw-finding-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--dw-space-3);
}
.dw-finding-title {
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
}
.dw-finding-meta {
	flex: none;
	font-size: var(--dw-size-eyebrow);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
	color: var(--dw-faint);
	white-space: nowrap;
}
.dw-finding-desc { margin: var(--dw-space-1) 0 0; font-size: var(--dw-size-small); color: var(--dw-muted); }
/* A file path or a selector. break-all rather than ellipsis: a truncated
   location is not actionable, and this is the field the reader retypes. */
.dw-finding-loc {
	margin-top: var(--dw-space-1);
	font-family: var(--dw-font-mono);
	font-size: var(--dw-size-micro);
	color: var(--dw-faint);
	word-break: break-all;
}
/* The remedy is the part anyone acts on, so it is set apart instead of being a
   third paragraph in the same grey as the description. */
.dw-finding-fix {
	margin-top: var(--dw-space-2);
	padding: var(--dw-space-2) var(--dw-space-3);
	border-radius: var(--dw-radius-sm);
	background: var(--dw-surface-2);
	font-size: var(--dw-size-micro);
	color: var(--dw-muted);
}

/* ----------------------------------------------------------- option card
   A radio-backed picker card, 7 on one page. Same real-input treatment as
   .dw-switch and for the same reason: a div picker is not in the tab order and
   tells a screen reader nothing about the options being ONE choice. The radio
   stays visible here — unlike the switch, where the track is the affordance,
   a picker card's radio is what says "pick exactly one". */
.dw-optioncard {
	display: flex;
	align-items: flex-start;
	gap: var(--dw-space-3);
	padding: var(--dw-space-4);
	border: var(--dw-border) solid var(--dw-line);
	border-radius: var(--dw-radius);
	background: var(--dw-surface);
	cursor: pointer;
	transition: border-color var(--dw-dur-fast) var(--dw-ease),
		background var(--dw-dur-fast) var(--dw-ease);
}
.dw-optioncard input {
	width: 15px; height: 15px; flex: none;
	margin: 2px 0 0;
	accent-color: var(--dw-accent);
}
.dw-optioncard:hover { border-color: var(--dw-line-strong); }
/* :has() carries the real control's state; .is-on is there for the JS-driven
   case, the same pairing as .dw-switch. */
.dw-optioncard:has(input:checked),
.dw-optioncard.is-on { border-color: var(--dw-accent); background: var(--dw-accent-wash); }
.dw-optioncard:focus-within { outline: 2px solid var(--dw-focus); outline-offset: 2px; }
/* The stack, so title, badge and description do not become three flex items
   in a row beside the radio. */
.dw-optioncard-main { flex: 1 1 auto; min-width: 0; }
.dw-optioncard-title {
	display: flex;
	align-items: center;
	gap: var(--dw-space-2);
	font-size: var(--dw-size-small);
	font-weight: var(--dw-weight-strong);
	color: var(--dw-text);
}
.dw-optioncard-desc { margin: var(--dw-space-1) 0 0; font-size: var(--dw-size-micro); color: var(--dw-muted); }

/* --------------------------------------------------------- visitor shell
   THE ELEVENTH PAGE GROUP, and the one every wave skipped. Eleven surfaces
   sit outside the application chrome - login, first-boot setup, the six
   legal documents and the three error pages - and none of them was migrated,
   so they still wore the RETIRED brand: an #E8882E -> #CE1090 gradient
   button under a magenta glow, on the pre-kit #F4F7F9 blue-grey ground,
   while the application behind them had already moved to a flat #E5A043 on
   #FAF9F6. A person met the old brand at login, the new one for the whole
   session, and the old one again on a 404.

   They are ONE job, not eleven: a page with no header, no sidebar and no
   session, that has to introduce itself before it can do anything. So this
   is one component. Inventing a shell per page is precisely how the platform
   arrived at 19 stat tiles and 17 empty states, one reasonable local
   decision at a time.

   WIDTH IS A TOKEN, NOT A LIST OF MODIFIERS. --dw-visitor-max is the single
   knob; .is-doc only re-sets it. A page that needs a third width overrides
   the token inline-free via its own rule, rather than growing a class here. */
.dw-visitor {
	--dw-visitor-max: 440px;
	min-height: 100vh;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--dw-space-5);
	padding: var(--dw-space-7) var(--dw-space-4);
	box-sizing: border-box;
}
/* dvh where it exists. On a phone 100vh is the UNCOLLAPSED viewport, so a
   centred login sits partly behind the browser chrome until you scroll. */
@supports (min-height: 100dvh) {
	.dw-visitor { min-height: 100dvh; }
}

/* Vertically centred, for the short surfaces: auth, setup, errors.
   A long document must NOT take this. Centring content taller than the
   viewport pushes its own masthead off the top of the screen. */
.dw-visitor.is-centered { justify-content: center; }

/* A document is read rather than filled in, so it gets a measure. */
.dw-visitor.is-doc { --dw-visitor-max: 860px; }

/* THE MEASURE BELONGS TO THE TEXT, NOT TO THE BLOCK.
   .dw-prose caps its whole container at --dw-prose-max (68ch), which is
   right for paragraphs and wrong for everything else in the same flow.
   Measured on the privacy policy: its five-column processing table was
   confined to 588px - "name, email, username, password hash, role" wrapped
   onto five lines - while the card around it sat 270px wider and empty.

   So inside a document the cap moves onto the text elements and the wide
   blocks take the card. Scoped to .is-doc rather than changed on .dw-prose
   itself, because 55 migrated pages already rely on the block form. */
.dw-visitor.is-doc .dw-prose { max-width: none; }
.dw-visitor.is-doc .dw-prose > p,
.dw-visitor.is-doc .dw-prose > ul,
.dw-visitor.is-doc .dw-prose > ol,
.dw-visitor.is-doc .dw-prose > blockquote,
.dw-visitor.is-doc .dw-prose > h2,
.dw-visitor.is-doc .dw-prose > h3,
.dw-visitor.is-doc .dw-prose > h4,
.dw-visitor.is-doc .dw-prose > h5 { max-width: var(--dw-prose-max); }

.dw-visitor > * { width: 100%; max-width: var(--dw-visitor-max); }

/* LINKS INSIDE THE SHELL READ THE TOKEN.
   The kit styles no bare <a> at all, so every link on the platform still
   falls through to light-theme.css:42, `a { color: var(--accent-color) }` -
   and --accent-color is pinned to rgb(232,136,46), the RETIRED amber, with
   :hover on rgb(206,16,144), the RETIRED magenta. Measured after the
   migration: 12 of 12 links on the privacy policy and 6 of 7 on login were
   still #E8882E, on pages whose whole point was that they no longer are.

   This is PRE-EXISTING and PLATFORM-WIDE - background-tasks.html, migrated in
   wave 3, measures identically - and the honest fix is one line on bare `a`
   in the kit, which is base-layer-collapse work touching all 69 pages. Scoped
   here so the pages this wave actually delivered are right, and so the wider
   fix stays a decision someone takes on purpose rather than a side effect.

   --dw-accent-text, not --dw-accent: plain amber measures 2.11:1 on the
   ground and is a fill, never text. */
.dw-visitor a:not(.dw-btn):not(.dw-visitor-mark) {
	color: var(--dw-accent-text);
}
.dw-visitor a:not(.dw-btn):not(.dw-visitor-mark):hover {
	color: var(--dw-text);
}
/* The current document in a doc nav is a <span>, not a link, so it needs the
   quiet treatment stated rather than inherited. */
.dw-visitor-nav [aria-current='page'] {
	color: var(--dw-muted);
	font-weight: var(--dw-weight-strong);
}

.dw-visitor-mark {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--dw-space-3);
	text-decoration: none;
	/* The masthead is a LINK, so without this it fell through to
	   light-theme.css:42 and computed as the retired #E8882E - 2.5:1 on the
	   ground. Nothing rendered in it today, because the only text inside sits
	   in .dw-visitor-name which sets its own colour, so this was one added
	   text node away from being visible. State it rather than rely on that. */
	color: var(--dw-text);
}

/* THE NAME IS LIVE TEXT, not an image and not a painted gradient. The
   retired treatment filled it with the amber -> magenta gradient, which
   reads only on a light ground and left the wordmark nearly invisible on
   every dark skin. As text it inherits --dw-text and follows all 28.
   The mark beside it is .dw-logomark, which already resolves the two-mark
   problem through var(--dw-logomark): outline on dark skins, solid on the
   rest. Hardcoding an <img src> here is what pinned one mark on all of
   them. */
.dw-visitor-name {
	font-family: var(--dw-font-display);
	font-size: var(--dw-size-h3);
	font-weight: var(--dw-weight-strong);
	letter-spacing: var(--dw-track-h2);
	color: var(--dw-text);
}

/* The status numeral on an error page. It is a PART rather than a page-local
   font-size because three pages need the same one, and --dw-accent-text is
   the only amber that is a text colour: plain --dw-accent measures 2.11:1 on
   the ground and would fail AA at any size that is not decorative. */
.dw-visitor-code {
	margin: 0;
	font-family: var(--dw-font-display);
	font-size: clamp(64px, 12vw, 132px);
	font-weight: var(--dw-weight-strong);
	line-height: 1;
	letter-spacing: var(--dw-track-h1);
	font-variant-numeric: tabular-nums;
	color: var(--dw-accent-text);
}

/* The labelled rule between the password form and the federated sign-in
   buttons. A part rather than a page-local rule because it is the one shape
   in this group that Bootstrap has no equivalent for - everything else here
   (centring, spacing, full-width) is a layout utility the kit deliberately
   leaves to Bootstrap. */
.dw-visitor-sep {
	display: flex;
	align-items: center;
	gap: var(--dw-space-3);
	margin: var(--dw-space-4) 0;
	color: var(--dw-faint);
	font-size: var(--dw-size-micro);
	letter-spacing: var(--dw-track-label);
	text-transform: uppercase;
}
.dw-visitor-sep::before,
.dw-visitor-sep::after {
	content: "";
	flex: 1;
	height: var(--dw-border);
	background: var(--dw-line);
}

/* TABLE HEADERS IN A PUBLIC DOCUMENT ARE CONTENT, NOT CHROME.
   The kit paints .dw-table thead th in --dw-faint, which tokens.css itself
   describes as living in the small gap "between quieter than muted and below
   4.5:1" - and on a skin whose muted is already near the floor, faint follows
   it down. Measured on the privacy policy's processing table: 4.14:1 on dwc
   and 4.26:1 on abyss, at 10.5px uppercase, which is where AA matters most.

   Before this migration those headers were Bootstrap's, inheriting the body
   colour, so this is a regression these pages introduced even though the rule
   is shared. Scoped to .dw-visitor rather than changed on .dw-table, because
   how quiet a header should be across the other ~24 list pages is the kit
   owner's call, not a side effect of migrating the legal documents. */
.dw-visitor .dw-table thead th {
	color: var(--dw-muted);
}

.dw-visitor-nav {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--dw-space-2) var(--dw-space-4);
	margin: 0;
	padding: 0;
	list-style: none;
	font-size: var(--dw-size-small);
}

.dw-visitor-foot {
	color: var(--dw-faint);
	font-size: var(--dw-size-micro);
	text-align: center;
}

/* --------------------------------------------------------------- utility
   `text-muted` has 730 uses. Rather than rewrite 730 sites, point Bootstrap's
   utility at the token. This is the migration-alias trick: the sweep becomes
   a deletion rather than a rewrite. */
.text-muted, .dw-muted { color: var(--dw-muted); }
.dw-faint-text { color: var(--dw-faint); }
.dw-num { font-variant-numeric: tabular-nums; }
.dw-visually-hidden {
	position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
	overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* Line clamp. Six stylesheets already carry a private copy of exactly this.
   The -webkit- pair is not legacy cruft: it is the only implementation that
   ships everywhere, and `line-clamp` is declared beside it so this file does
   not have to be revisited when it is not. */
.dw-clamp-2, .dw-clamp-3 {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	overflow: hidden;
}
.dw-clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }
.dw-clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; }

/* Durations already collapse through --dw-motion-scale, which the tokens file
   sets to 0 under this same query — so this block is for the ANIMATIONS, which
   a scale of 0 makes instant rather than absent. Every animated component in
   the kit belongs here; there are five. */
@media (prefers-reduced-motion: reduce) {
	.dw-pill.is-run::before, .dw-skeleton, .dw-spinner,
	.dw-skeleton.is-number, .dw-skeleton.is-row { animation: none; }
	/* Not animations, but movement: the chevron and the switch knob travel.
	   With the duration token at 0 they already jump rather than slide; this
	   states it so a skin that overrides the token does not reintroduce it. */
	.dw-accordion-head::after, .dw-switch-track::after { transition: none; }
}
