/*
 * Agent Control — single plain-CSS stylesheet (T-005, no framework).
 *
 * Visual language: MoroSystems Brand Manual 2025 (V02). The whole design system is the
 * `:root` block below — four tiers of custom properties. The rule for anything added later:
 *
 *   write component CSS against TIER 2/3 role tokens only.
 *
 * Never reach for a tier-1 `--moro-*` value (or a raw hex) in a component rule. That is what
 * keeps the brand one edit wide: re-theming means editing tier 2, not grepping the file. It is
 * also why this redesign touched almost no HTML — every class kept its name and its markup.
 */

:root {
	/* ---- Tier 1: the brand palette, verbatim from the manual (p. 10-11), plus its tint ramp.
	   Source of truth for the tokens below; not for component rules. --------------------- */
	--moro-yellow: #ffdc3c;
	--moro-yellow-80: #ffe363;
	--moro-yellow-60: #ffea8a;
	--moro-yellow-40: #fff1b1;
	--moro-yellow-20: #fff8d8;

	--moro-indigo: #252746;
	--moro-indigo-80: #51526b;
	--moro-indigo-60: #7c7d90;
	--moro-indigo-40: #a8a9b5;
	--moro-indigo-20: #d3d4da;

	--moro-purple: #8250be;
	--moro-purple-80: #9b73cb;
	--moro-purple-60: #b496d8;
	--moro-purple-40: #cdb9e5;
	--moro-purple-20: #e6dcf2;

	--moro-white: #ffffff;

	/* ---- Tier 2: roles. This is the layer component CSS is allowed to name. ------------- */
	--bg: #f1f1f5;
	--surface: var(--moro-white);
	--surface-sunken: #f7f7fa;
	--text: var(--moro-indigo);
	--muted: var(--moro-indigo-60);
	--border: var(--moro-indigo-20);

	/* Interactive = purple. The manual reserves it for CTAs and links (p. 9, p. 22). */
	--accent: var(--moro-purple);
	--accent-strong: #6a3f9d;
	--accent-contrast: var(--moro-white);

	/* Yellow is a fill, never a text colour: #ffdc3c on white is ~1.5:1. Use it for bars,
	   rails, active chrome and the primary data series (manual p. 20) — never for glyphs. */
	--highlight: var(--moro-yellow);
	--highlight-soft: var(--moro-yellow-20);
	--on-highlight: var(--moro-indigo);

	/* Header chrome: the manual's recurring "white sheet on an indigo page" (p. 18-21, 31). */
	--chrome-bg: var(--moro-indigo);
	--chrome-text: var(--moro-white);
	--chrome-muted: var(--moro-indigo-40);
	--chrome-link: var(--moro-indigo-20);
	--chrome-hover-bg: var(--moro-indigo-80);

	/* ---- Tier 3: status. Neutral is indigo, attention is yellow, "your turn" is purple.
	   Green and red sit outside the brand palette on purpose — the manual defines no
	   failure colour, and a board that cannot say "this broke" is not worth looking at. */
	--info-bg: #e9e9ef;
	--info-fg: #3c3e5e;
	--info-border: #d5d6de;
	--warn-bg: var(--moro-yellow-40);
	--warn-fg: #6b5100;
	--warn-border: var(--moro-yellow-60);
	--error-bg: #fbe4e4;
	--error-fg: #9b1c1c;
	--error-border: #efb9b9;
	--ok-bg: #e2f2e5;
	--ok-fg: #1c6b33;
	--ok-border: #bfe0c8;
	--turn-bg: var(--moro-purple-20);
	--turn-fg: #56308a;
	--turn-border: var(--moro-purple-40);

	/* ---- Tier 4: shape and type. The identity is built from rectangles, so corners are
	   nearly square; pills are kept only where a shape reads as a counter or a track. --- */
	--radius: 4px;
	--radius-sm: 2px;
	--radius-pill: 999px;
	--font-body: "Inter Tight", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
	/*
	 * ⚠️ One family, two roles. The display face used to be Boldonse and is not any more: on a dense
	 * operator console its heavy condensed caps shouted at every page headline, and the brand's own
	 * workhorse says the same thing at weight 700. Do not reintroduce it — a test asserts the
	 * stylesheet no longer names it.
	 */
	--font-display: "Inter Tight", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
	--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/*
 * Self-hosted so the app has no runtime dependency on a third-party CDN — it is deployed on an
 * internal host, and a Google Fonts <link> would both leak visitor IPs and break offline. Inter
 * Tight is SIL Open Font License 1.1 — the licence text ships beside it in fonts/OFL-InterTight.txt
 * — which permits this redistribution.
 * Latin + latin-ext only: the UI is English but operator and ticket text is often Czech.
 */
@font-face {
	font-family: "Inter Tight";
	font-style: normal;
	font-weight: 400 700;
	/*
	 * ⚠️ `optional`, not `swap`, and it is a layout-stability decision rather than a loading one.
	 * `swap` renders the fallback and then RE-LAYS-OUT the page when the real face arrives — which, with
	 * Spring Security's `no-store` on every response, happened on every single navigation and made the
	 * whole header shift sideways (see StaticAssetCacheConfig for the measurement). `optional` tells the
	 * browser to use the fallback for that page view and never to reflow for a late font. The cost is a
	 * cold first visit rendered in `system-ui`; the caching in that same class means there is at most one
	 * of those. ⚠️ It also means "the font looks wrong" is the expected symptom with DevTools cache
	 * disabled — that is the flag, not a regression.
	 */
	font-display: optional;
	src: url("/fonts/inter-tight-latin.woff2") format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
		U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Inter Tight";
	font-style: normal;
	font-weight: 400 700;
	/*
	 * ⚠️ `optional`, not `swap`, and it is a layout-stability decision rather than a loading one.
	 * `swap` renders the fallback and then RE-LAYS-OUT the page when the real face arrives — which, with
	 * Spring Security's `no-store` on every response, happened on every single navigation and made the
	 * whole header shift sideways (see StaticAssetCacheConfig for the measurement). `optional` tells the
	 * browser to use the fallback for that page view and never to reflow for a late font. The cost is a
	 * cold first visit rendered in `system-ui`; the caching in that same class means there is at most one
	 * of those. ⚠️ It also means "the font looks wrong" is the expected symptom with DevTools cache
	 * disabled — that is the flag, not a regression.
	 */
	font-display: optional;
	src: url("/fonts/inter-tight-latin-ext.woff2") format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
		U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
		U+2113, U+2C60-2C7F, U+A720-A7FF;
}

* { box-sizing: border-box; }

/* Form controls do not inherit the page font by default — Inter Tight has to be handed to them. */
button, input, select, textarea { font-family: inherit; }

body {
	margin: 0;
	min-height: 100vh;
	display: flex;
	flex-direction: column;
	font-family: var(--font-body);
	font-size: 15px;
	line-height: 1.5;
	color: var(--text);
	background: var(--bg);
}

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

/* One visible focus ring for every interactive element, in the interaction colour. */
:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

/*
 * Header / nav.
 *
 * The manual's dominant composition is a light sheet laid on a deep-indigo page, so the chrome
 * is indigo and the working area stays light. Text inside the bar therefore uses --chrome-*,
 * not --text; those are the only rules in this file that may.
 */
.site-header {
	display: flex;
	align-items: center;
	gap: 1.25rem;
	padding: 0.55rem 1.25rem;
	background: var(--chrome-bg);
	color: var(--chrome-text);
	border-bottom: 2px solid var(--highlight);
}

/*
 * The lockup, in the manual's most common form: the yellow mark beside the wordmark, both on
 * indigo (p. 6). The mark is the app's own "AC" rather than the MoroSystems "M" - the manual's
 * module is the company's logotype, not the product's. The mark and the name are one unit, so
 * they share a tight gap of their own rather than the header's; the negative margin cancels most
 * of .site-header's 1.25rem to leave 0.5rem between them.
 */
.site-header .app-mark {
	fill: var(--highlight);
	flex: none;
	/*
	 * ⚠️ ~~`margin-right: -0.75rem`~~ — that cancelled `.site-header`'s 1.25rem gap down to 0.5rem back
	 * when the mark and the wordmark were two separate header children. They are now one link
	 * (`.app-home`), so the header gap no longer falls between them and the negative margin would pull
	 * the wordmark onto the mark. The 0.5rem it was aiming for is the lockup's own gap below.
	 */
	margin-right: 0;
}

/*
 * The lockup is a link home — the convention every reader arrives with, and `/` is the board.
 *
 * Underline on hover rather than a permanent link colour: in the interaction purple this would read as
 * a call to action in the chrome, and the manual reserves the wordmark's own treatment (p. 6).
 */
.app-home {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	text-decoration: none;
	color: inherit;
	flex: none;
}

.app-home:hover .app-name { text-decoration: underline; }

/* A keyboard visitor has to be able to see where they are, and the chrome is dark. */
.app-home:focus-visible {
	outline: 2px solid var(--highlight);
	outline-offset: 2px;
	border-radius: var(--radius-sm);
}

/* SemiBold, a step below the h1's Bold: the wordmark is chrome, not the page's headline. */
.app-name {
	font-size: 0.95rem;
	font-weight: 600;
	line-height: 1.35;
	letter-spacing: 0.02em;
	color: var(--chrome-text);
}

.site-nav { display: flex; gap: 0.25rem; }

/*
 * ⚠️ **Every entry carries the active weight, always.** `600` used to be part of `.active` alone, and
 * bold text is wider than the same text at 400 — so each navigation re-measured the nav and every
 * entry after the active one slid sideways. Measured at 1–2px per item: small, constant, and exactly
 * the kind of movement that reads as the page being unstable. The weight is now a property of the nav
 * (it matches `.app-name` beside it), and `.active` differs only in colour, which costs no width.
 */
.site-nav a {
	padding: 0.3rem 0.75rem;
	border-radius: var(--radius-sm);
	text-decoration: none;
	font-weight: 600;
	color: var(--chrome-link);
}

.site-nav a:hover { background: var(--chrome-hover-bg); color: var(--chrome-text); }

/* Active nav is the lockup again in miniature: the yellow block, indigo text. */
.site-nav a.active {
	background: var(--highlight);
	color: var(--on-highlight);
}

.auth-slot { margin-left: auto; font-size: 0.9rem; display: flex; align-items: center; gap: 0.6rem; }

.auth-slot .current-user, .auth-slot .account-link { color: var(--chrome-muted); }
/* T-175: the signed-in name became a link to /account/password. Underline only on hover, so the header
   still reads as a label rather than as navigation. */
a.current-user, a.account-link { text-decoration: none; }
a.current-user:hover, a.current-user:focus-visible,
a.account-link:hover, a.account-link:focus-visible { text-decoration: underline; }

.logout-form { display: inline; margin: 0; padding: 0; }

/* Links sitting on indigo take the yellow; --accent purple has too little contrast there. */
.logout-link {
	border: none;
	background: none;
	padding: 0;
	font: inherit;
	color: var(--highlight);
	cursor: pointer;
	text-decoration: underline;
}

.login-link { color: var(--highlight); }

@media (max-width: 480px) {
	.site-header { flex-wrap: wrap; gap: 0.4rem 0.75rem; padding: 0.55rem 1rem; }
	.site-nav { order: 3; flex: 1 1 100%; flex-wrap: wrap; }
	.site-nav a { padding: 0.3rem 0.5rem; }
	.auth-slot { flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
}

/* Login page */
.login-form { display: flex; flex-direction: column; gap: 0.5rem; max-width: 20rem; }

.login-form input {
	padding: 0.4rem 0.6rem;
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	background: var(--surface);
	color: var(--text);
	font: inherit;
}

/* The one unambiguous primary action on the page — the manual's filled-purple CTA (p. 22). */
.login-form button, .btn-primary {
	margin-top: 0.4rem;
	padding: 0.45rem 1rem;
	border: 1px solid var(--accent);
	border-radius: var(--radius-sm);
	background: var(--accent);
	color: var(--accent-contrast);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
}

.login-form button:hover, .btn-primary:hover {
	background: var(--accent-strong);
	border-color: var(--accent-strong);
}

.login-error { color: var(--error-fg); }

.login-info { color: var(--muted); }

/* Banners */
.banners { padding: 0.5rem 1.25rem 0; }

.banner {
	display: flex;
	gap: 0.6rem;
	align-items: baseline;
	padding: 0.5rem 0.9rem;
	margin-bottom: 0.5rem;
	border-radius: var(--radius);
	border: 1px solid transparent;
}

.banner-severity { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.05em; }

.banner-info  { background: var(--info-bg);  color: var(--info-fg);  border-color: var(--info-border); }
.banner-warn  { background: var(--warn-bg);  color: var(--warn-fg);  border-color: var(--warn-border); }
.banner-error { background: var(--error-bg); color: var(--error-fg); border-color: var(--error-border); }

/* Content */
.content { flex: 1; padding: 1.25rem; }

/*
 * The display role is Inter Tight Bold, tightened. It used to be a separate condensed display face,
 * which read as a poster headline on what is actually a dense operator console — the page title
 * outshouted the state badge under it, which is the thing an operator is here to read. Weight and
 * tracking carry the hierarchy instead, which is the brand's stated use of the workhorse (p. 17-18).
 *
 * Rule for later work: if you reach for var(--font-display), the thing you are styling has to be
 * the one <h1> on the page.
 */
h1 {
	font-family: var(--font-display);
	font-weight: 700;
	line-height: 1.25;
	letter-spacing: -0.01em;
	font-size: 1.35rem;
	margin: 0 0 1rem;
}

h2 { font-weight: 600; }

/* Empty state */
.empty-state {
	max-width: 32rem;
	margin: 4rem auto;
	text-align: center;
	color: var(--muted);
}

.empty-icon { font-size: 2.2rem; margin-bottom: 0.5rem; }

.empty-message { font-size: 1.05rem; font-weight: 600; color: var(--text); margin: 0 0 0.35rem; }

.empty-hint { margin: 0; font-size: 0.9rem; }

/* Error pages */
.error-page { max-width: 34rem; margin: 4rem auto; text-align: center; }

/* Board (T-015) */
.filter-bar { display: flex; gap: 0.6rem; margin-bottom: 1rem; }

.filter-bar input, .filter-bar select {
	padding: 0.4rem 0.6rem;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font: inherit;
	background: var(--surface);
	color: var(--text);
}

.filter-bar input { flex: 1; max-width: 22rem; }

.filter-note {
	margin: -0.4rem 0 0.8rem;
	padding: 0.3rem 0.6rem;
	border-left: 3px solid var(--text);
	font-size: 0.85rem;
	color: var(--text);
}

@media (max-width: 480px) {
	.filter-bar { flex-wrap: wrap; }
	.filter-bar input, .filter-bar select { flex: 1 1 100%; max-width: none; min-width: 0; }
}

/*
 * ================================================================================================
 * A filter that is SET has to look set.
 *
 * The filter persists across reloads now (board.js, localStorage), which turned a small ambiguity
 * into a real one: a board narrowed to one project looks exactly like a quiet Monday, and before the
 * filter survived a reload at least closing the tab cleared it. So it is marked twice — the control
 * itself, here, and a sentence with counts above the columns (`.board-filter-active`), because the
 * control answers *"is something filtered"* and only the sentence answers *"how much am I not seeing"*.
 *
 * ⚠️ **Neither yellow nor purple**, deliberately, and this is the same rule the project accents
 * follow: in this stylesheet yellow means *a machine is working* and purple means *your turn*, and a
 * filter is neither — a board mark that borrowed either would be read as a fact about the work. The
 * emphasis is weight and a neutral indigo edge instead of a hue.
 *
 * `:not(:placeholder-shown)` and `:has(option:checked…)` keep this CSS-only: no script has to run for
 * a typed filter to look typed, which matters because the *persisted* filter is exactly the case
 * where nobody typed it in this page view.
 * ============================================================================================== */
.filter-bar input:not(:placeholder-shown),
.filter-bar select:has(option:checked:not([value=""])) {
	border-color: var(--text);
	background: var(--surface-sunken);
	font-weight: 600;
	box-shadow: inset 0 0 0 1px var(--text);
}

.board-filter-active {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.3rem 0.6rem;
	margin: 0 0 0.6rem;
	padding: 0.4rem 0.6rem;
	border: 1px solid var(--info-border);
	border-left: 3px solid var(--info-fg);
	border-radius: var(--radius);
	background: var(--info-bg);
	color: var(--info-fg);
	font-size: 0.85rem;
}

.board-filter-active-mark { font-size: 0.7rem; }

.board-filter-counts { color: var(--muted); }

/* Last, and pushed to the far end: it is the way out of the state the row is describing. */
.board-filter-clear { margin-left: auto; font-weight: 600; color: var(--info-fg); }

/* #104: the line under the columns saying how many settled tickets moved to History. Quiet, like a footnote. */
.board-archived { margin: 0.8rem 0 0; color: var(--muted); font-size: 0.85rem; }
.board-archived a { font-weight: 600; }
.board-archived-note { margin-left: 0.4rem; }

/*
 * #118: the server-drawn, unfiltered columns while board.js re-applies a remembered filter. Only
 * board.js adds the class, so a scriptless board is drawn in full; `visibility` rather than `display`
 * so the filter bar and the approval panel above keep their place.
 */
#board-columns.board-filter-pending { visibility: hidden; }

/*
 * T-081: the phase vocabulary went from 8 columns to 12, four of which are drawn only when
 * occupied (see BoardPhase.alwaysVisible). A fixed `repeat(7, ...)` would have wrapped the rest
 * onto a second grid row, which reads as a second board rather than as a scroll — so the track
 * list follows the column count and the container scrolls sideways, which is what a kanban does.
 *
 * ⚠️ **Flex, not grid, and the reason is `.board-column-empty`.** Grid track sizing belongs to the
 * container: `grid-auto-columns` sizes every implicit track alike, so an empty column could not be
 * narrower than a full one however it was classed. On a fleet with eight always-visible columns that
 * cost every real card ~230px of a 2000px screen — titles wrapped to six lines beside two columns
 * showing the number 0. Flex keeps the sideways scroll and lets the child state its own width.
 *
 * ~~`display: grid; grid-auto-flow: column; grid-auto-columns: minmax(13rem, 1fr)`~~
 */
.board-columns {
	display: flex;
	align-items: flex-start;
	gap: 0.75rem;
	overflow-x: auto;
	padding-bottom: 0.3rem;
}

.board-column {
	background: var(--surface);
	border: 1px solid var(--border);
	border-top: 3px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem;
	/*
	 * Grow into spare width; shrink only as far as a card stays readable, and let the container
	 * scroll after that. 15rem is what keeps the eight always-visible columns on one screen at
	 * 1760px of content — measured, not guessed: at 16rem they overflowed by 68px, which reads as a
	 * board that has been cut off rather than one that scrolls.
	 */
	flex: 1 1 15rem;
	min-width: 13rem;
}

/*
 * An always-visible column with nothing in it. It still has to *say* it is empty — "nothing is
 * Running" is a fact a board must be able to state, and an absent column cannot state it — so it
 * keeps its label and its zero and gives up its share of the width. The label wraps, which is why
 * `min-width` goes as well as `flex-basis`.
 */
.board-column-empty {
	flex: 0 0 auto;
	width: 7rem;
	min-width: 0;
	background: var(--surface-sunken);
}

/*
 * ⚠️ The label has to WRAP inside the strip, not overflow it. A flex item does not shrink below its
 * content's minimum width by default, so `Dispatching` in a 6.5rem box printed straight over the next
 * column's heading — visible only once the board was looked at, which is why it is pinned here.
 */
/*
 * ⚠️ The strip's label stacks over its count instead of sitting beside it, and it does NOT
 * break mid-word: `DISP / ATCH / ING` is worse than a slightly wider column. So the heading becomes
 * a block (label on its own line, count under it) and the width is set so the longest phase name
 * fits whole at this size.
 */
.board-column-empty .board-column-title {
	display: block;
	opacity: 0.7;
	font-size: 0.75rem;
	margin-bottom: 0;
}

.board-column-empty .board-column-count { display: inline-block; margin-top: 0.3rem; }

/* The label yields before the count pill does (`flex: none` above) — but only by ellipsis. */
.board-column-title > span:first-child {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}
.board-column-empty .board-column-cards { display: none; }

.board-column-title {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.4rem;
	font-size: 0.9rem;
	margin: 0 0 0.6rem;
	color: var(--muted);
	text-transform: uppercase;
	letter-spacing: 0.03em;
}

.board-column-count {
	background: var(--bg);
	border-radius: var(--radius-pill);
	padding: 0.05rem 0.5rem;
	font-size: 0.8rem;
	flex: none;
}

/*
 * The columns are not peers, and the board was drawing them as eleven identical grey boxes: the one
 * phase that means *a human is next* (BG-70) looked exactly like `Cancelled`. The rail on top sorts
 * them into the three things an operator actually distinguishes, using the tier-3 status roles the
 * badges already use — so the colour of a column and the colour of the badges inside it agree.
 *
 *   purple  — your turn: a person is the blocking resource
 *   yellow  — a machine has it: wait
 *   green / grey — settled
 */
.board-column-awaiting-merge, .board-column-awaiting-approval, .board-column-needs-info {
	border-top-color: var(--turn-border);
}

.board-column-awaiting-merge .board-column-title,
.board-column-awaiting-approval .board-column-title,
.board-column-needs-info .board-column-title { color: var(--turn-fg); }

.board-column-queued, .board-column-dispatching, .board-column-running, .board-column-mr-open {
	border-top-color: var(--highlight);
}

.board-column-done { border-top-color: var(--ok-border); }
.board-column-failed, .board-column-rejected { border-top-color: var(--error-border); }
.board-column-parked { border-top-color: var(--info-border); }

.board-column-failed .board-column-title { color: var(--error-fg); }

/* A settled column is chrome, not news — it must not compete with the two above it. */
.board-column-done, .board-column-cancelled {
	background: var(--surface-sunken);
}

.board-column-cards { display: flex; flex-direction: column; gap: 0.5rem; }

.item-card {
	background: var(--bg);
	border: 1px solid var(--border);
	/* The project's identity colour, per card. Left edge only — see the project-accent block below. */
	border-left: 3px solid var(--project-accent, var(--border));
	border-radius: var(--radius);
	padding: 0.55rem 0.65rem;
	font-size: 0.85rem;
}

/*
 * ================================================================================================
 * Project identity — six accents, assigned by name (`projectAccentOf`).
 *
 * ⚠️ **Position keeps this from lying; the hues keep it from shouting.** Every status colour in this
 * file is a *fill* (a badge, a chip, a column rail on TOP), while a project accent only ever appears
 * as the card's LEFT edge and an 8px dot beside the project's own name — so a project cannot occupy a
 * status's slot. On top of that the ramp deliberately contains **no yellow, no green and no red**: the
 * first draft gave `agent-control` an olive that read as this palette's `--highlight` (which means "a
 * machine is working") and a customer project a rust that read as failure. Six cool-to-neutral hues
 * are distinguishable enough at 8px and cannot be mistaken for a state.
 *
 * They are tier-1-style raw values on purpose: an identity ramp of their own, not roles, and nothing
 * else in the stylesheet may name them.
 * ============================================================================================== */
.item-card-project-1 { --project-accent: #3f6ea2; } /* blue */
.item-card-project-2 { --project-accent: #1f7a72; } /* teal */
.item-card-project-3 { --project-accent: #8b3f6b; } /* plum */
.item-card-project-4 { --project-accent: #6b4fa0; } /* violet */
.item-card-project-5 { --project-accent: #55606b; } /* slate */
.item-card-project-6 { --project-accent: #7a5230; } /* brown */

.item-project-dot {
	flex: none;
	width: 0.5rem;
	height: 0.5rem;
	border-radius: var(--radius-pill);
	background: var(--project-accent, var(--muted));
	/* Baseline-aligned row, so the dot needs nudging onto the text's optical centre. */
	transform: translateY(-0.05rem);
}

/*
 * ================================================================================================
 * "What has moved since I last looked?"
 *
 * A board sorted by priority inside columns cannot answer that, and it is the question somebody who
 * leaves this page open all day asks most often. So a ticket whose newest write is inside
 * `RECENTLY_MOVED_MINUTES` marks itself twice: a yellow top edge on the card, findable by peripheral
 * vision across eight columns, and the Moved row's value picked out in the same yellow, which is the
 * one that says *how* recently.
 *
 * ⚠️ Yellow because it is this palette's "a machine is working" colour and that is usually what a
 * fresh change means — never purple, which is reserved for *your turn*. The mark decays on its own:
 * the board re-polls every five seconds, so it disappears without anybody reloading.
 * ============================================================================================== */
.item-card-moved { box-shadow: inset 0 2px 0 0 var(--highlight); }

/*
 * ================================================================================================
 * Dormant cards — an old settled ticket is a title, until it is asked to be more.
 *
 * BG-17 keeps a terminal request on the board for its project's archive age (7 days by default since
 * #104, BG-268), which is right: somebody arriving on Monday has to be able to see what Friday finished.
 * What was wrong was the WEIGHT — a `Done` card still carried a spend row, a moved row, a caption and a
 * run line per module, and three columns of those push the ticket somebody is actually waiting on off the
 * screen. So past its project's collapse age (`Project.boardCondenseDays`, 3 days by default) the card
 * keeps its title, its project rail and one control, and gives the room back.
 *
 * ⚠️ **Gated on `.board-js`.** The expansion is a script (board.js), so the collapse must not exist
 * without it — collapsing from the stylesheet alone would hide a week of finished work behind a button
 * that does nothing. Same gate hides the button itself when the script never ran, so the no-script
 * board is precisely the board this rule found.
 *
 * ⚠️ **`> *`, and that is on purpose**: it means a row added to the card later is collapsed by default
 * rather than being the one thing that keeps sticking out of a collapsed card. The two exceptions are
 * named, and the title stays a link to the ticket page — collapsed is quieter, never a dead end.
 * ============================================================================================== */
html:not(.board-js) .item-card-expand { display: none; }

.board-js .item-card-dormant:not(.item-card-expanded) { padding: 0.35rem 0.5rem; }

.board-js .item-card-dormant:not(.item-card-expanded) > *:not(.item-title):not(.item-card-expand) {
	display: none;
}

/* One line, not three: a dormant title is being counted rather than read. */
.board-js .item-card-dormant:not(.item-card-expanded) .item-title {
	-webkit-line-clamp: 1;
	font-weight: 500;
	color: var(--muted);
	margin-bottom: 0.1rem;
}

.item-card-expand {
	display: flex;
	align-items: center;
	gap: 0.25rem;
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	font-size: 0.7rem;
	letter-spacing: 0.02em;
	color: var(--muted);
	cursor: pointer;
}

.item-card-expand:hover { color: var(--text); text-decoration: underline; }

/* The one moving part: the mark turns to say which way the card will go. */
.item-card-expand-mark { display: inline-block; transition: transform 0.12s ease-out; }

.item-card-expanded .item-card-expand-mark { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
	.item-card-expand-mark { transition: none; }
}

.item-moved-fresh {
	background: var(--highlight-soft);
	color: var(--warn-fg);
	font-weight: 600;
	border-radius: var(--radius-sm);
	padding: 0 0.25rem;
}

/*
 * ================================================================================================
 * The card's facts, as a two-column label/value grid.
 *
 * ⚠️ The point is the LABEL. This was a flowing line of grey values — `attempt 4  2 modules  37.4M
 * tokens · ~$37.74` — in which two adjacent numbers read as one fact, and a spend figure read as the
 * cost of the run rather than of the whole ticket. A narrow card can afford 4.5rem of label; it
 * cannot afford a reader who has to guess.
 *
 * The label column is fixed so values line up down the card, and every row is optional: a plain
 * ticket draws one, a retried split one draws four, and neither is a different shape of card.
 * ============================================================================================== */
.item-facts {
	display: grid;
	grid-template-columns: 4.5rem minmax(0, 1fr);
	gap: 0.1rem 0.5rem;
	margin: 0 0 0.4rem;
	font-size: 0.78rem;
}

.item-facts dt {
	color: var(--muted);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	font-size: 0.68rem;
	/* Small caps against a 0.78rem value: the two baselines only agree with a nudge. */
	padding-top: 0.08rem;
}

.item-facts dd {
	margin: 0;
	min-width: 0;
	color: var(--text);
	overflow-wrap: anywhere;
}

.item-card-top {
	display: flex;
	align-items: baseline;
	gap: 0.45rem;
	color: var(--muted);
	font-size: 0.75rem;
	margin-bottom: 0.2rem;
}

/* The project name yields first: it is the one item here that can be arbitrarily long. */
.item-project { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item-card-top .item-age { margin-left: auto; flex: none; }
.item-card-top .item-ticket-link { flex: none; font-size: 0.75rem; }
/* Priority sits with the identity, and the age still owns the right edge (`margin-left: auto`). */
.item-card-top .badge-priority { flex: none; }
/*
 * T-179/BG-175: the "no ticket behind this row" badge. Deliberately quiet - it states an unusual
 * fact, it is not a status, and a board where a whole column shouts is a board nobody reads.
 */
.item-card-top .badge-source { flex: none; }
.badge-source { background: var(--info-bg); color: var(--info-fg); }
.item-source-link { text-decoration: none; }
.item-source-link:hover { text-decoration: underline; }
.item-findings-blocking { color: var(--error-fg); font-weight: 600; }

/*
 * ⚠️ Clamped to three lines. A board column is narrow by construction and a long ticket title was
 * pushing everything under it off the screen; the whole title stays reachable through the `title`
 * attribute the template sets. `-webkit-line-clamp` is the only clamp with real support — it is
 * unprefixed nowhere yet, and the fallback (an unclamped title) is merely the old behaviour.
 */
.item-title {
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;
	font-weight: 600;
	line-height: 1.35;
	text-decoration: none;
	color: var(--text);
	margin-bottom: 0.3rem;
}

.item-title:hover { text-decoration: underline; }

.item-meta { display: flex; flex-wrap: wrap; gap: 0.15rem 0.5rem; color: var(--muted); margin-bottom: 0.3rem; }

.item-badges { display: flex; gap: 0.35rem; margin-bottom: 0.3rem; }

.badge {
	font-size: 0.7rem;
	font-weight: 700;
	letter-spacing: 0.03em;
	border-radius: var(--radius-sm);
	padding: 0.05rem 0.4rem;
}

.badge-stuck { background: var(--error-bg); color: var(--error-fg); }

/*
 * T-100/BG-123: a dispatch that ran on cached materials. Warn, deliberately not error — the run is not
 * broken and nothing failed; what it needs is to be readable as an explanation for output that
 * disappoints. Sharing .badge-stuck's red would put it in the same class as a wedged agent.
 */
.badge-materials-stale { background: var(--warn-bg); color: var(--warn-fg); }

/*
 * T-132/D-11: the branch moved after the merge gate passed, so "ready for a human to merge" is about a
 * commit that is no longer the head. Warn rather than error for the same reason as above — nothing failed
 * and nothing is blocked; the operator may well decide the new commit needs no second look. Red would read
 * as "do not merge", which is a decision this app does not get to take.
 */
.badge-head-unreviewed { background: var(--warn-bg); color: var(--warn-fg); }

/* BG-302: a run held at the merge gate for a person's decision reads differently from "ready to merge". */
.badge.badge-state.decision-needed, .badge.badge-run-state.decision-needed { background: var(--warn-bg); color: var(--warn-fg); }

.badge-priority.priority-critical { background: var(--error-bg); color: var(--error-fg); }
.badge-priority.priority-high { background: var(--warn-bg); color: var(--warn-fg); }
.badge-priority.priority-low { background: var(--info-bg); color: var(--info-fg); }

.badge-ci { background: var(--info-bg); color: var(--info-fg); }
.badge-ci.ci-success { background: var(--ok-bg); color: var(--ok-fg); }
.badge-ci.ci-failed { background: var(--error-bg); color: var(--error-fg); }
.badge-ci.ci-running, .badge-ci.ci-pending { background: var(--warn-bg); color: var(--warn-fg); }

.item-links { display: flex; gap: 0.6rem; font-size: 0.8rem; }

/* Item detail (T-016) */
.item-detail-header {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 1rem 1.25rem;
	margin-bottom: 1rem;
}

.item-detail-top { display: flex; align-items: center; gap: 0.6rem; color: var(--muted); font-size: 0.85rem; }

.item-detail-header h1 { font-size: 1.15rem; margin: 0.4rem 0 0.7rem; }

/*
 * The headline links to the ticket, but it is a title first: in the interaction purple at display
 * size it read as a giant CTA and outshouted the state badge under it. Underline on hover keeps
 * the affordance.
 */
.item-detail-header h1 a { text-decoration: none; color: var(--text); }
.item-detail-header h1 a:hover { text-decoration: underline; }

.badge-state { background: var(--info-bg); color: var(--info-fg); }
.badge-state.state-done { background: var(--ok-bg); color: var(--ok-fg); }
/* T-065: the merge gate is waiting on a person — read as "your turn", not as progress. */
.badge-state.state-awaiting-merge { background: var(--turn-bg); color: var(--turn-fg); }
.badge-state.state-failed, .badge-state.state-cancelled { background: var(--error-bg); color: var(--error-fg); }
.badge-state.state-running, .badge-state.state-dispatching, .badge-state.state-queued, .badge-state.state-seen {
	background: var(--warn-bg); color: var(--warn-fg);
}
/*
 * T-081. `parked` is not an error colour on purpose: both park reasons are a request waiting for a
 * person or for an unbuilt pipeline, and colouring them red would say "broken" where the message
 * says "waiting". The run badge repeats the same palette one level down so a nested run's state and
 * a request's phase read alike; it is a separate class only so a page can carry exactly one
 * `.badge-state`, which is the ticket's headline.
 */
.badge-state.state-parked, .badge-state.state-needs-info, .badge-state.state-awaiting-approval {
	background: var(--info-bg); color: var(--info-fg);
}
.badge-run-state {
	background: var(--info-bg); color: var(--info-fg);
}
.badge-run-state.state-done { background: var(--ok-bg); color: var(--ok-fg); }
.badge-run-state.state-awaiting-merge { background: var(--turn-bg); color: var(--turn-fg); }
.badge-run-state.state-failed, .badge-run-state.state-cancelled, .badge-run-state.state-rejected {
	background: var(--error-bg); color: var(--error-fg);
}
.badge-run-state.state-running, .badge-run-state.state-dispatching, .badge-run-state.state-queued {
	background: var(--warn-bg); color: var(--warn-fg);
}

.item-detail-meta { display: flex; gap: 1.5rem; margin: 0 0 0.7rem; flex-wrap: wrap; }
.item-detail-meta > div { display: flex; flex-direction: column; }
.item-detail-meta dt { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; }
.item-detail-meta dd { margin: 0; }
/* T-097: a run dispatched before it carried its capability grant. A sentence, deliberately quieter
   than a value, so "not recorded" never reads as "recorded as nothing". */
.item-detail-meta dd.unrecorded { color: var(--muted); font-style: italic; }

/*
 * T-176: the run page's disclosures.
 *
 * ⚠️ Two rules, and they are the whole design:
 *
 *   1. A collapsed section states its own payload on the summary line (`.panel-disclosure-hint`) -
 *      the count, the spend, the progress. A disclosure that says only "Review findings" has hidden
 *      information rather than tidied it, and the reader has to open all of them to find the one.
 *   2. It is `<details>`, so opening it is the browser's job. There is no script on this page and no
 *      stored open/closed state (BG-111); the default comes from the run's own state in the template.
 *
 * The native marker is dropped only because a flex summary loses it in every engine anyway; `::before`
 * puts it back, so the affordance survives. `list-style: none` and the `-webkit-` rule are the two
 * spellings that between them cover the browsers on the LAN.
 */
.panel-disclosure { margin-bottom: 1rem; }
.panel-disclosure > summary {
	display: flex;
	align-items: baseline;
	gap: 0.6rem;
	flex-wrap: wrap;
	cursor: pointer;
	list-style: none;
	padding: 0.1rem 0;
}
.panel-disclosure > summary::-webkit-details-marker { display: none; }
.panel-disclosure > summary::before {
	content: '\25b8';
	color: var(--muted);
	font-size: 0.8rem;
}
.panel-disclosure[open] > summary::before { content: '\25be'; }
/* Beats `.findings-section h2` / `.item-timeline-section h2`, whose bottom margin would push the
   summary's own row apart. Spacing below the summary belongs to the open state only. */
.panel-disclosure > summary h2 { font-size: 1rem; margin: 0; }
.panel-disclosure[open] > summary { margin-bottom: 0.5rem; }
.panel-disclosure-hint { color: var(--muted); font-size: 0.85rem; font-weight: 400; }

/* The run header's second half (T-176): the rows that read the same on every run. */
.run-meta-more { margin: 0 0 0.7rem; }
.run-meta-more > summary { cursor: pointer; color: var(--muted); font-size: 0.8rem; }
.run-meta-more[open] > summary { margin-bottom: 0.4rem; }

/* Sibling attempts (BG-2) */
.sibling-attempts { margin-bottom: 1rem; }
.sibling-attempts ul { margin: 0; padding-left: 1.2rem; }

/*
	The live session view (T-088, E4).

	Two things here are load-bearing rather than decorative:

	1. `.run-stream-scroll` is a `column-reverse` flex scroll container with exactly ONE child
	   (`.run-stream-log`). That is the same no-JavaScript trick `.run-output-text` has used since
	   T-061, and it is what gives the task's scroll requirement for free: the box opens at the
	   bottom and keeps following as htmx appends rows, and it stops following the moment the reader
	   scrolls up, because from then on the scroll offset is theirs. There is no JS build here
	   (T-005, BG-111), so a behaviour that needs script is a behaviour we do without.
	2. A tool call's input is clamped by `max-height` as well as by the parser's character bound. A
	   `Write` call's input is a whole file; two independent ceilings is what keeps one row from
	   pushing the rest of the session off the page.
*/
.run-session-section { margin-bottom: 1rem; }
.run-session-section h2 { font-size: 1rem; margin: 0 0 0.5rem; }

/*
 * T-177: the cap notice and the full-session page.
 *
 * ⚠️ The notice sits ABOVE the scroll box rather than inside it. `.run-stream-scroll` is a
 * `column-reverse` container - anything placed inside it lands at the visual bottom and scrolls away
 * with the rows, which is the one place a "there is more above" sentence must never be.
 */
.run-stream-capped {
	margin: 0 0 0.4rem;
	font-size: 0.85rem;
	color: var(--muted);
	display: flex;
	gap: 0.5rem;
	flex-wrap: wrap;
}

/* The full-session page has nothing under it competing for the screen, which is the point of it. */
.full-session-scroll { max-height: 75vh; }
.run-session-back { margin: 0.3rem 0 0; font-size: 0.85rem; }
.full-session-picker {
	display: flex;
	gap: 0.6rem;
	flex-wrap: wrap;
	align-items: baseline;
	margin: 0 0 0.6rem;
	font-size: 0.85rem;
}
.full-session-picker-label {
	color: var(--muted);
	text-transform: uppercase;
	font-size: 0.75rem;
	letter-spacing: 0.03em;
}
.full-session-current { font-weight: 600; }

.run-stream-scroll {
	display: flex;
	flex-direction: column-reverse;
	max-height: 34rem;
	overflow-y: auto;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.2rem 0.7rem;
}

.run-stream-log { min-width: 0; }

.run-stream-event {
	display: grid;
	grid-template-columns: 7rem minmax(0, 1fr);
	gap: 0.6rem;
	padding: 0.35rem 0.3rem;
	border-bottom: 1px solid var(--border);
	font-size: 0.8rem;
}

.run-stream-kind {
	font-size: 0.68rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--muted);
	text-align: right;
	padding-top: 0.1rem;
	word-break: break-word;
}

.run-stream-body {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.25rem 0.55rem;
	min-width: 0;
}

.run-stream-text {
	flex: 1 1 100%;
	margin: 0;
	min-width: 0;
	font-family: inherit;
	white-space: pre-wrap;
	word-break: break-word;
}

.run-stream-meta { font-size: 0.72rem; color: var(--muted); }
.run-stream-flag { font-size: 0.72rem; font-style: italic; color: var(--muted); }

.run-stream-tool-name { font-weight: 700; color: var(--accent); }

.run-stream-tool-input {
	flex: 1 1 12rem;
	min-width: 0;
	max-height: 4.5rem;
	overflow: auto;
	font-family: var(--font-mono);
	font-size: 0.75rem;
	background: var(--surface-sunken);
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	padding: 0.05rem 0.35rem;
	white-space: pre-wrap;
	word-break: break-all;
}

/* <details>, so "expand the tool result" costs no JavaScript. */
.run-stream-details { flex: 1 1 100%; min-width: 0; }
.run-stream-details > summary { cursor: pointer; font-size: 0.75rem; color: var(--muted); }
.run-stream-details .run-stream-text {
	margin-top: 0.3rem;
	max-height: 20rem;
	overflow: auto;
	font-family: var(--font-mono);
	font-size: 0.75rem;
	background: var(--surface-sunken);
	border: 1px solid var(--border);
	border-radius: var(--radius-sm);
	padding: 0.4rem 0.5rem;
}

.run-stream-unparsed .run-stream-text { font-family: var(--font-mono); font-size: 0.75rem; }

.run-stream-outcome { font-weight: 700; }

/* Errors and lines we could not parse are distinct and never hidden. */
.run-stream-api_error, .run-stream-unparsed, .run-stream-denied { background: var(--error-bg); }
.run-stream-api_error .run-stream-kind, .run-stream-unparsed .run-stream-kind, .run-stream-denied .run-stream-kind {
	color: var(--error-fg);
	font-weight: 700;
}
.run-stream-note { background: var(--info-bg); }
.run-stream-denied-note {
	margin: 0 0 0.5rem;
	padding: 0.5rem 0.75rem;
	background: var(--error-bg);
	color: var(--error-fg);
	font-weight: 700;
}
.run-stream-result { background: var(--highlight-soft); }

/*
	The tail is the only part of the fragment htmx replaces rather than keeps, which is why the
	live/finished statement lives here and nowhere else: a badge among the rows would be appended
	once and then go on claiming "live" after the run ended.
*/
.run-stream-tail {
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	padding: 0.45rem 0.3rem;
	font-size: 0.75rem;
	color: var(--muted);
}

.run-stream-live {
	font-weight: 700;
	letter-spacing: 0.03em;
	text-transform: uppercase;
	color: var(--info-fg);
	background: var(--info-bg);
	border-radius: var(--radius-sm);
	padding: 0.05rem 0.35rem;
}

/* T-061's transcript, one click away (BG-113). */
.run-transcript { margin-top: 0.6rem; }
.run-transcript > summary {
	cursor: pointer;
	font-size: 0.8rem;
	font-weight: 600;
	color: var(--muted);
	margin-bottom: 0.4rem;
}

/* Agent output (T-061) */
.run-output-section { margin-bottom: 1rem; }
.run-output-section h2 { font-size: 1rem; margin: 0 0 0.5rem; }

.run-output-meta {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: 0.6rem;
	margin-bottom: 0.35rem;
	font-size: 0.75rem;
	color: var(--muted);
}

.run-output-live {
	font-weight: 700;
	letter-spacing: 0.03em;
	text-transform: uppercase;
	color: var(--info-fg);
	background: var(--info-bg);
	border-radius: var(--radius-sm);
	padding: 0.05rem 0.35rem;
}

.run-output-flag { font-style: italic; }

/*
	Newest output is at the bottom, so the box has to open there rather than at the oldest
	line of the tail. column-reverse on the scroll container anchors the initial scroll
	position at the end without a line of JavaScript (there is no JS build here, T-005) and
	without reversing the text itself - the <pre> is a single flex item.
*/
.run-output-text {
	font-family: var(--font-mono);
	display: flex;
	flex-direction: column-reverse;
	margin: 0;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.7rem;
	white-space: pre-wrap;
	word-break: break-word;
	max-height: 28rem;
	overflow-y: auto;
	font-size: 0.8rem;
}

/*
	md-viewer: a rendered markdown document (`fragments/markdown.html`) and the <details> holding its
	source. Deliberately quiet - a plan is read for its content, not admired, and the type sizes match
	the rest of the run page rather than a document viewer's.

	The source block keeps whatever class its caller passed (`run-plan-text`, `run-split-scope`, …), so
	everything below this block still styles it.
*/
.markdown-doc { margin: 0.3rem 0 0; }

.markdown-rendered {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.8rem;
	font-size: 0.85rem;
	line-height: 1.5;
	max-height: 40rem;
	overflow-y: auto;
	overflow-wrap: break-word;
}

.markdown-rendered > :first-child { margin-top: 0; }
.markdown-rendered > :last-child { margin-bottom: 0; }
.markdown-rendered h1, .markdown-rendered h2, .markdown-rendered h3,
.markdown-rendered h4, .markdown-rendered h5, .markdown-rendered h6 {
	font-size: 0.9rem;
	margin: 0.8rem 0 0.35rem;
	line-height: 1.3;
}
.markdown-rendered h1 { font-size: 1rem; }
.markdown-rendered p { margin: 0 0 0.5rem; }
.markdown-rendered ul, .markdown-rendered ol { margin: 0 0 0.5rem; padding-left: 1.2rem; }
.markdown-rendered li { margin-bottom: 0.15rem; }
.markdown-rendered code {
	font-family: var(--font-mono);
	font-size: 0.78rem;
	background: var(--bg);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0 0.2rem;
}
.markdown-rendered pre {
	font-family: var(--font-mono);
	font-size: 0.78rem;
	background: var(--bg);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.5rem 0.6rem;
	margin: 0 0 0.5rem;
	overflow-x: auto;
}
.markdown-rendered pre code { background: none; border: 0; padding: 0; }
.markdown-rendered blockquote {
	margin: 0 0 0.5rem;
	padding-left: 0.6rem;
	border-left: 3px solid var(--border);
	color: var(--muted);
}
/* A table wide enough to need it scrolls inside the document, never widening the page. */
.markdown-rendered table { border-collapse: collapse; margin: 0 0 0.5rem; font-size: 0.8rem; }
.markdown-rendered th, .markdown-rendered td {
	border: 1px solid var(--border);
	padding: 0.25rem 0.45rem;
	text-align: left;
}
.markdown-rendered hr { border: 0; border-top: 1px solid var(--border); margin: 0.7rem 0; }

.markdown-raw { margin-top: 0.35rem; }
.markdown-raw > summary { cursor: pointer; font-size: 0.78rem; color: var(--muted); }

/*
	The plan (T-103). Rendered for reading (above) with its source markdown one <details> away, and the
	escaping that makes that safe belongs to `web/MarkdownRenderer`, not to the template.
*/
.run-plan-section { margin-bottom: 1rem; }
.run-plan-section h2 { font-size: 1rem; margin: 0 0 0.5rem; }
.run-plan-section h3 { font-size: 0.85rem; margin: 0.6rem 0 0.3rem; }
.run-plan-attribution { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--muted); }
.badge-plan-edited { background: var(--warn-bg); color: var(--warn-fg); }

.run-plan-text {
	font-family: var(--font-mono);
	margin: 0;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.7rem;
	white-space: pre-wrap;
	word-break: break-word;
	max-height: 32rem;
	overflow-y: auto;
	font-size: 0.8rem;
}

.run-plan-proposed > summary { cursor: pointer; font-size: 0.85rem; margin-bottom: 0.3rem; }

/*
	The plan overview (T-189, BG-192/BG-193). What a human reads first at the gate, so it is the block
	with the emphasis and the plan document below it is the disclosure. No monospace: unlike every other
	agent-authored surface on this page it is not a document, it is five captions, and it carries no
	renderer at all - `th:text` and nothing else, which is what keeps the one th:utext in this
	application at one.
*/
.plan-overview {
	margin: 0 0 0.8rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.7rem;
}
.plan-overview-heading { font-size: 0.85rem; margin: 0 0 0.3rem; }
.plan-overview-attribution,
.plan-overview-edited,
.plan-overview-counts,
.plan-overview-absent { margin: 0 0 0.4rem; font-size: 0.8rem; color: var(--muted); }
.plan-overview-counts:last-child { margin-bottom: 0; }
/*
	T-191/BG-196: the plan's own summary, and the only line on this panel set larger than the fields -
	it is what the panel is read for, and the fields are what it is read with.
*/
.plan-overview-summary { margin: 0 0 0.5rem; font-size: 0.95rem; line-height: 1.35; }
.plan-overview-fields { margin: 0; font-size: 0.85rem; }
.plan-overview-fields > div { margin-bottom: 0.4rem; }
.plan-overview-fields > div:last-child { margin-bottom: 0; }
.plan-overview-fields dt { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }
.plan-overview-fields dd { margin: 0.1rem 0 0; word-break: break-word; }
.plan-overview-steps, .plan-overview-touches, .plan-overview-risks { margin: 0; padding-left: 1.1rem; }
/*
	T-200/BG-207: the blockers. The only block on this panel with the error palette, because it is the
	only one a person acts on - and the caption under it is load-bearing for the same reason the risk
	badge's is: a red box beside an *Approve* button reads as a refusal, and this refuses nothing.
*/
.plan-overview-blockers {
	margin: 0 0 0.6rem;
	padding: 0.4rem 0.6rem;
	border: 1px solid var(--error-border);
	border-left-width: 3px;
	border-radius: var(--radius);
	background: var(--error-bg);
	font-size: 0.85rem;
}
.plan-overview-blockers-heading { font-size: 0.8rem; margin: 0 0 0.25rem; color: var(--error-fg); }
.plan-overview-blockers ul { margin: 0; padding-left: 1.1rem; }
.plan-overview-blockers li { word-break: break-word; }
.plan-overview-blockers-caption { margin: 0.35rem 0 0; font-size: 0.78rem; color: var(--muted); }
/*
 * T-190/BG-194: the risk badge. Colour is the affordance, the caption beside it is the meaning — a
 * badge alone next to an *Approve* button reads as a verdict, and this is an opinion.
 */
.plan-overview-risk { display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; margin: 0 0 0.5rem; }
.plan-risk-badge {
	font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
	padding: 0.08rem 0.4rem; border-radius: 0.2rem; border: 1px solid currentColor;
}
.plan-risk-low { color: var(--ok-fg); background: var(--ok-bg); border-color: var(--ok-border); }
.plan-risk-medium { color: var(--warn-fg); background: var(--warn-bg); border-color: var(--warn-border); }
.plan-risk-high { color: var(--error-fg); background: var(--error-bg); border-color: var(--error-border); }
.plan-risk-caption { font-size: 0.78rem; color: var(--muted); }

/*
 * T-203/BG-219: the plan's guess at its own cost, at the approval gate. Same caption treatment as the
 * risk badge and the blockers above, and for the same reason - the caption carries "it decides
 * nothing: no run is held or refused on it", and at the number's own size and colour that sentence
 * reads as part of the figure, i.e. as the budget this panel is careful not to imply.
 */
.plan-overview-estimate { font-variant-numeric: tabular-nums; }
.plan-overview-estimate-caption {
	display: block;
	margin-top: 0.15rem;
	font-size: 0.78rem;
	color: var(--muted);
}

/*
 * #102/BG-267: the estimate placed among the module's first-pass costs, on a log scale. The tones reuse
 * the ok/warn/error tokens every other badge uses; the word beside the bar is always drawn, so the
 * colour is never the only signal. Positions come from Kotlin via th:style - no script.
 */
.plan-gauge { margin-top: 0.4rem; max-width: 18rem; }
.plan-gauge-track {
	position: relative;
	height: 0.45rem;
	border-radius: var(--radius-pill);
	background: var(--border);
}
.plan-gauge-median {
	position: absolute;
	top: -0.2rem;
	width: 2px;
	height: 0.85rem;
	margin-left: -1px;
	background: var(--muted);
}
.plan-gauge-marker {
	position: absolute;
	top: 50%;
	width: 0.75rem;
	height: 0.75rem;
	margin: -0.375rem 0 0 -0.375rem;
	border-radius: 50%;
	border: 2px solid var(--bg);
}
.plan-gauge-marker.plan-gauge-ok { background: var(--ok-fg); }
.plan-gauge-marker.plan-gauge-warn { background: var(--warn-border); }
.plan-gauge-marker.plan-gauge-error { background: var(--error-fg); }
.plan-gauge-labels {
	display: flex;
	justify-content: space-between;
	font-size: 0.72rem;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
}
.plan-gauge-word {
	display: inline-block;
	margin-top: 0.15rem;
	padding: 0 0.35rem;
	border-radius: var(--radius-pill);
	font-size: 0.8rem;
}
.plan-gauge-word.plan-gauge-ok { color: var(--ok-fg); background: var(--ok-bg); }
.plan-gauge-word.plan-gauge-warn { color: var(--warn-fg); background: var(--warn-bg); }
.plan-gauge-word.plan-gauge-error { color: var(--error-fg); background: var(--error-bg); }
.plan-gauge-caption { display: block; margin-top: 0.15rem; font-size: 0.78rem; color: var(--muted); }
/* #125: the price beside the estimate and the phase breakdown under the gauge. */
.plan-overview-price, .plan-estimate-price { cursor: help; border-bottom: 1px dotted var(--muted); font-variant-numeric: tabular-nums; }
.plan-phases { margin-top: 0.5rem; max-width: 28rem; }
.plan-phases-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; font-variant-numeric: tabular-nums; }
.plan-phases-table th, .plan-phases-table td { padding: 0.1rem 0.4rem; text-align: right; }
.plan-phases-table th:first-child, .plan-phases-table td:first-child { text-align: left; }
.plan-phases-total td { border-top: 1px solid var(--border); font-weight: 600; }
.plan-phases-no-price { margin: 0.2rem 0 0; font-size: 0.8rem; }
.plan-phases-caption { display: block; margin-top: 0.15rem; font-size: 0.78rem; color: var(--muted); }

/*
	The triage split (T-163, BG-143). Same treatment as the plan above and for the same reason: every
	agent-authored string is escaped text inside a labelled block, because this is the page where a
	human decides to create N runs off it. A superseded proposal is dimmed rather than hidden - it is
	the answer to "what did it propose the first time".
*/
.run-split { margin-bottom: 0.8rem; }
.run-split-superseded { opacity: 0.7; }
.run-split-note, .run-split-untrusted { margin: 0 0 0.4rem; font-size: 0.8rem; color: var(--muted); }
.run-split-summary, .run-split-scope, .run-split-reasoning {
	font-family: var(--font-mono);
	margin: 0.3rem 0 0;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.5rem 0.6rem;
	white-space: pre-wrap;
	word-break: break-word;
	font-size: 0.78rem;
}

.run-needs-info { margin-bottom: 0.8rem; }
.run-needs-info-question, .run-needs-info-answer {
	font-family: var(--font-mono);
	margin: 0.3rem 0 0.4rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.5rem 0.6rem;
	white-space: pre-wrap;
	word-break: break-word;
	font-size: 0.78rem;
}

.run-split-proposals { margin: 0; padding-left: 1.1rem; }
.run-split-proposal { margin-bottom: 0.6rem; }
.run-split-module { font-weight: 600; font-size: 0.85rem; }
.run-split-claimed, .run-split-spawned { font-size: 0.78rem; color: var(--muted); margin-left: 0.4rem; }

.badge-split-outcome { background: var(--info-bg); color: var(--info-fg); }
.badge-split-outcome.split-approved { background: var(--ok-bg); color: var(--ok-fg); }
.badge-split-outcome.split-rejected { background: var(--error-bg); color: var(--error-fg); }
.badge-split-outcome.split-superseded { background: var(--surface); color: var(--muted); }

/*
	The approval gate (T-105). The textarea is the plan itself, editable in place: approve and
	edit-and-approve are one action, so there is one control rather than an edit mode beside a button.
*/
.plan-approval { margin: 0 0 0.8rem; }
.plan-approval-form { display: flex; flex-direction: column; gap: 0.4rem; }
.plan-approval-label { font-size: 0.85rem; color: var(--muted); }

.plan-approval-text {
	font-family: var(--font-mono);
	font-size: 0.8rem;
	width: 100%;
	background: var(--surface);
	color: var(--text);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.7rem;
	resize: vertical;
}

/*
	The one place a `.btn` earns the filled treatment outside login: this is the decision the page
	exists for, and it is the gesture that spends an agent run.
*/
.plan-approval-form .btn-approve {
	align-self: flex-start;
	border-color: var(--accent);
	background: var(--accent);
	color: var(--accent-contrast);
}
.plan-approval-form .btn-approve:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--accent-contrast); }
.plan-approval-outcome { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--muted); }

/*
	#100: the proposal rendered beside the textarea while the gate is open - the agent's text, not a
	preview of the edit. `minmax(0, 1fr)` and `min-width: 0` keep a long code line or URL from widening the
	grid past the page. The rendered column scrolls on its own, capped near the 20-row textarea's height:
	a plan is pages long, and an uncapped column would push the approve button down by all of it. Stacked
	below 60rem of viewport, like the run page's other breakpoint.
*/
.plan-approval-columns {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	gap: 0.8rem;
	align-items: start;
}
.plan-approval-edit { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.plan-approval-rendered { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.plan-approval-rendered .markdown-doc { margin: 0; }
.plan-approval-rendered .markdown-rendered { max-height: 26rem; }
.plan-approval-rendered-text {
	font-family: var(--font-mono);
	font-size: 0.8rem;
	white-space: pre-wrap;
	overflow-wrap: break-word;
	max-height: 26rem;
	overflow-y: auto;
}
@media (max-width: 60rem) {
	.plan-approval-columns { grid-template-columns: minmax(0, 1fr); }
}

/*
	Rejection (T-106). Beside the approve form rather than behind a toggle: both decisions are on the
	table while the gate is open, and the one that ends the ticket's plan is the one behind a checkbox.
*/
.plan-rejection-form { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.8rem; }
.plan-rejection-text {
	font-family: var(--font-mono);
	font-size: 0.8rem;
	width: 100%;
	background: var(--surface);
	color: var(--text);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.7rem;
	resize: vertical;
}
.plan-rejection-outright { font-size: 0.85rem; color: var(--muted); display: flex; align-items: center; gap: 0.35rem; }
.plan-rejection-form .btn-reject { align-self: flex-start; }
.plan-rejection-history { margin: 0.6rem 0 0.5rem; }
.plan-rejection-comment {
	font-family: var(--font-mono);
	font-size: 0.8rem;
	white-space: pre-wrap;
	margin: 0;
	padding: 0.5rem 0.6rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

/* The approval queue on the board (T-107): "your turn" toned, like the merge gate's badge. */
.awaiting-approval {
	margin: 0 0 0.9rem;
	padding: 0.6rem 0.8rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-left: 3px solid var(--accent);
	border-radius: var(--radius);
}
.awaiting-approval-title { font-size: 0.95rem; margin: 0 0 0.5rem; display: flex; align-items: center; gap: 0.4rem; }
.awaiting-approval-count {
	font-family: var(--font-mono);
	font-size: 0.75rem;
	padding: 0.05rem 0.4rem;
	border-radius: 999px;
	background: var(--accent);
	color: var(--accent-contrast);
}
.awaiting-approval-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.awaiting-approval-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; font-size: 0.85rem; }
.awaiting-approval-link { font-family: var(--font-mono); font-size: 0.8rem; }
.awaiting-approval-meta { color: var(--muted); font-size: 0.8rem; }
.awaiting-approval-rejections { color: var(--warn-fg); font-size: 0.75rem; }
.awaiting-approval-note { margin: 0.5rem 0 0; font-size: 0.8rem; color: var(--muted); }
/* T-165: one ticket, its plans nested under it — the fan-out reads as one decision, not N tickets. */
.awaiting-approval-group { font-size: 0.85rem; }
.awaiting-approval-plans {
	list-style: none;
	margin: 0.15rem 0 0;
	padding: 0 0 0 0.8rem;
	border-left: 2px solid var(--border);
	display: flex;
	flex-direction: column;
	gap: 0.2rem;
}

/*
 * T-165/BG-145: the plan gates one triage split left open, on the request page.
 * Toned like the board's approval queue, because it is the same "your turn" and the same reading.
 */
.split-plan-approval {
	margin: 0 0 0.9rem;
	padding: 0.6rem 0.8rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-left: 3px solid var(--accent);
	border-radius: var(--radius);
}
.split-plan-approval-title { font-size: 0.95rem; margin: 0 0 0.4rem; display: flex; align-items: center; gap: 0.4rem; }
.split-plan-approval-count {
	font-family: var(--font-mono);
	font-size: 0.75rem;
	padding: 0.05rem 0.4rem;
	border-radius: 999px;
	background: var(--accent);
	color: var(--accent-contrast);
}
.split-plan-approval-note, .split-plan-approval-untrusted, .split-plan-approval-outcome {
	margin: 0 0 0.4rem;
	font-size: 0.8rem;
	color: var(--muted);
}
.split-plan-approval-list { list-style: none; margin: 0; padding: 0; }
.split-plan-approval-row { margin: 0 0 0.8rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.split-plan-approval-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.split-plan-approval-module { font-weight: 600; font-size: 0.85rem; }
.split-plan-approval-link { font-family: var(--font-mono); font-size: 0.8rem; }
.split-plan-approval-rejections { color: var(--warn-fg); font-size: 0.75rem; }
.split-plan-approval-scope, .split-plan-approval-plan-text {
	margin: 0.2rem 0 0.4rem;
	padding: 0.4rem 0.6rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font-family: var(--font-mono);
	font-size: 0.78rem;
	white-space: pre-wrap;
	word-break: break-word;
}
.split-plan-approval-plan > summary { font-size: 0.8rem; color: var(--muted); cursor: pointer; }
.split-plan-approval-one { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.split-plan-approval-elsewhere { font-size: 0.78rem; }
.split-plan-approval-all {
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
	margin-top: 0.6rem;
	padding-top: 0.6rem;
	border-top: 1px solid var(--border);
}
.split-plan-approval-label { font-size: 0.85rem; color: var(--muted); }
.split-plan-approval-all .btn-approve { align-self: flex-start; }

/* Review findings (T-069) */
.findings-section { margin-bottom: 1rem; }
.findings-section h2 { font-size: 1rem; margin: 0 0 0.5rem; }

.findings-verdict { margin: 0 0 0.4rem; font-size: 0.9rem; }

.findings-caveats {
	margin: 0 0 0.6rem;
	padding: 0.4rem 0.7rem 0.4rem 1.6rem;
	background: var(--warn-bg);
	color: var(--warn-fg);
	border-radius: var(--radius);
	font-size: 0.8rem;
}

.findings-group { margin-bottom: 0.8rem; }
.findings-group h3 { font-size: 0.85rem; margin: 0 0 0.35rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }

.findings-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }

.finding {
	background: var(--surface);
	border: 1px solid var(--border);
	border-left: 3px solid var(--border);
	border-radius: var(--radius);
	padding: 0.5rem 0.7rem;
}

.findings-group-blocking .finding { border-left-color: var(--error-fg); }
.finding-resolved, .finding-dismissed { opacity: 0.6; }

.finding-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.45rem; }
.finding-location { font-family: var(--font-mono); font-size: 0.78rem; color: var(--muted); }
/* T-169: the same location, now clickable when the app can build an honest link to the code. */
.finding-location-link { color: var(--muted); text-decoration: underline dotted; }
.finding-location-link:hover, .finding-location-link:focus { color: var(--text); text-decoration: underline; }
.badge-severity { background: var(--bg); color: var(--muted); }
.badge-finding-status { background: var(--info-bg); color: var(--info-fg); }
/*
 * T-185 / BG-184: the reviewed repository's own rule id. Monospace because it is somebody else's
 * identifier rather than our prose, and muted because it is a lookup key, not a severity.
 */
.badge-rule { background: var(--bg); color: var(--muted); font-family: var(--font-mono); }

.finding-title { margin: 0.3rem 0 0; font-size: 0.88rem; }

/*
 * T-171. The title is the scan line and the detail collapses behind it - native <details>, no JS build
 * (BG-111). Blocking rows are rendered open by the template, so the marker is the only affordance a
 * collapsed advisory needs.
 */
.finding-disclosure > summary { cursor: pointer; }
.finding-disclosure > summary::marker { color: var(--muted); }

.finding-detail {
	margin: 0.35rem 0 0;
	padding: 0.4rem 0.55rem;
	background: var(--bg);
	border-radius: var(--radius-sm);
	white-space: pre-wrap;
	word-break: break-word;
	font-size: 0.78rem;
	max-height: 14rem;
	overflow-y: auto;
}

.finding-foot { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; gap: 0.6rem; margin-top: 0.35rem; }
.finding-origin { font-size: 0.75rem; color: var(--muted); }

/*
 * T-129. The hint reads as ordinary quiet prose beside the button; the warning does not, because it is
 * the one case where the click has a consequence beyond the finding it names. Warning tone rather than
 * error tone on purpose — nothing is wrong, and the merge is still the operator's to make.
 */
.finding-dismiss-hint { flex: 1 1 16rem; font-size: 0.75rem; color: var(--muted); }

.finding-gate-warning {
	margin: 0.45rem 0 0;
	padding: 0.4rem 0.55rem;
	border-left: 3px solid var(--warn-border);
	background: var(--warn-bg);
	color: var(--warn-fg);
	border-radius: var(--radius-sm);
	font-size: 0.78rem;
}

/* E27/BG-303: a finding's proposed approaches and the picker beneath them. */
.finding-approaches { margin: 0.45rem 0 0; }
.finding-approach-choices { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.4rem 0.6rem; margin: 0; }
.finding-approach-choices legend { font-size: 0.78rem; color: var(--muted); }
.finding-approach { margin: 0.3rem 0; }
.finding-approach-label { font-size: 0.85rem; font-weight: 600; }
.approach-text, .finding-chosen-text { white-space: pre-wrap; font-size: 0.8rem; margin: 0.2rem 0; }
.approach-tradeoff { font-size: 0.75rem; color: var(--muted); margin: 0; }
.finding-decision-needed {
	margin: 0 0 0.35rem;
	padding: 0.4rem 0.55rem;
	border-left: 3px solid var(--warn-border);
	background: var(--warn-bg);
	color: var(--warn-fg);
	border-radius: var(--radius-sm);
	font-size: 0.78rem;
}
.finding-chosen { margin-top: 0.35rem; font-size: 0.78rem; }
.finding-approach-input { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.45rem; }
.finding-approach-input label { font-size: 0.8rem; }
.finding-approach-text { width: 100%; font: inherit; font-size: 0.85rem; padding: 0.4rem; }

/* T-170: the operator's own description of what is wrong, above the actions block. */
.findings-instruction { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.8rem; }
.findings-instruction label { font-size: 0.8rem; }
.findings-instruction-input { width: 100%; font: inherit; font-size: 0.85rem; padding: 0.4rem; }
.findings-instruction-hint { font-size: 0.75rem; color: var(--muted); }
.findings-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; margin-top: 0.6rem; }
.findings-actions-hint { font-size: 0.75rem; color: var(--muted); }

/*
 * Timeline.
 *
 * One table, five columns, one accent rail down the left of each row carrying the tone. The tone is
 * set once per row as a pair of custom properties and consumed twice — by the rail and by the event
 * chip — so adding a tone is one rule, not three, and a row can never have a rail of one colour and
 * a chip of another.
 */
.item-timeline-section h2, .sibling-attempts h2 { font-size: 1rem; margin: 0 0 0.5rem; }

/* Radius + overflow live on the wrapper: a border-collapse table cannot clip its own corners. */
.timeline-table-wrap {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow-x: auto;
}

.timeline-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }

.timeline-table th, .timeline-table td { text-align: left; padding: 0.4rem 0.7rem; vertical-align: baseline; }

/* Not sticky: the wrapper scrolls sideways, not down, so a sticky header would never trigger. */
.timeline-table thead th {
	background: var(--surface-sunken);
	font-size: 0.7rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--muted);
	border-bottom: 1px solid var(--border);
}

/* The date, once per day rather than once per row — the reason this is a table and not a list. */
.timeline-day th {
	padding-top: 0.7rem;
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.02em;
	color: var(--text);
	background: var(--surface-sunken);
	border-top: 1px solid var(--border);
	border-bottom: 1px solid var(--border);
}

.timeline-entry + .timeline-entry td { border-top: 1px solid var(--border); }

.timeline-at { color: var(--muted); font-size: 0.78rem; white-space: nowrap; font-variant-numeric: tabular-nums; }

.timeline-since {
	color: var(--muted);
	font-size: 0.78rem;
	white-space: nowrap;
	font-variant-numeric: tabular-nums;
}
.timeline-col-since { width: 1%; }

.timeline-headline { width: 100%; }

/*
 * Colour coding. Neutral is the default so an unrecognised tone degrades to a plain row rather than
 * to an invisible one, and MUTED rows (notes, bookkeeping) deliberately keep it: if every row is
 * coloured, none of them is.
 */
.timeline-entry { --tone-line: var(--border); --tone-bg: var(--info-bg); --tone-fg: var(--muted); }
.timeline-entry.tone-ok { --tone-line: var(--ok-border); --tone-bg: var(--ok-bg); --tone-fg: var(--ok-fg); }
.timeline-entry.tone-warn { --tone-line: var(--warn-border); --tone-bg: var(--warn-bg); --tone-fg: var(--warn-fg); }
/* BG-70's "your turn" purple, the same one .badge-state.state-awaiting-merge uses. */
.timeline-entry.tone-turn { --tone-line: var(--turn-border); --tone-bg: var(--turn-bg); --tone-fg: var(--turn-fg); }
.timeline-entry.tone-error { --tone-line: var(--error-border); --tone-bg: var(--error-bg); --tone-fg: var(--error-fg); }
.timeline-entry.tone-info { --tone-line: var(--info-border); --tone-bg: var(--info-bg); --tone-fg: var(--info-fg); }

/* The rail. inset box-shadow rather than a border, which border-collapse would merge away. */
.timeline-entry > td:first-child { box-shadow: inset 3px 0 0 var(--tone-line); }
.timeline-entry.tone-error > td { background: color-mix(in srgb, var(--error-bg) 35%, transparent); }

/*
 * The type, as a chip in the row's tone. Colour is never the only carrier — the chip states the type
 * in words, so the row is as readable in monochrome or to a colour-blind reader as it is here.
 */
.timeline-type {
	display: inline-block;
	padding: 0.05rem 0.4rem;
	border-radius: var(--radius-sm);
	background: var(--tone-bg);
	color: var(--tone-fg);
	font-size: 0.7rem;
	font-weight: 600;
	letter-spacing: 0.03em;
	white-space: nowrap;
}

/* Footer */
.site-footer {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	padding: 0.5rem 1.25rem;
	border-top: 1px solid var(--border);
	background: var(--surface);
	color: var(--muted);
	font-size: 0.85rem;
}

.time-refresh {
	border: 1px solid var(--border);
	background: var(--surface);
	color: var(--muted);
	border-radius: var(--radius-sm);
	cursor: pointer;
	padding: 0 0.35rem;
}

.time-refresh:hover { color: var(--text); }

/* Operator controls (T-017) */
.flash {
	border-radius: var(--radius);
	padding: 0.3rem 0.6rem;
	margin-bottom: 0.4rem;
	font-size: 0.8rem;
	border: 1px solid transparent;
}
.flash-success { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-border); }
.flash-conflict { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-border); }

.item-card-actions, .item-actions, .queue-controls-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 0.4rem;
	margin-top: 0.4rem;
}

.item-card-actions form, .item-actions form, .queue-controls-actions form {
	display: inline-flex;
	gap: 0.3rem;
	margin: 0;
}

.btn {
	border: 1px solid var(--border);
	background: var(--surface);
	color: var(--text);
	border-radius: var(--radius-sm);
	padding: 0.15rem 0.5rem;
	font-size: 0.78rem;
	cursor: pointer;
}
/*
 * The manual's secondary CTA: no fill, a rule, and the interaction colour arriving on hover
 * (p. 22). Filled purple is reserved for `.btn-primary` / the login submit, so that a row of
 * eight operator actions does not read as eight equally urgent invitations.
 */
.btn:hover { border-color: var(--accent); color: var(--accent); }

.btn-cancel, .btn-stop { color: var(--error-fg); }
.btn-retry, .btn-resume { color: var(--ok-fg); }

/*
 * T-091's `Run again` is deliberately NOT `.btn-retry` green. Green here means "recovering" — it is
 * the colour of the button that gets a broken thing working. Re-running a delivered ticket recovers
 * nothing; it starts a fresh piece of work, so it takes the default secondary treatment and reads as
 * ordinary rather than as remediation.
 */
.btn-rerun { color: var(--muted); }

/* A destructive or recovering action keeps its own colour on hover rather than turning purple. */
.btn-cancel:hover, .btn-stop:hover, .btn-danger:hover { border-color: var(--error-fg); color: var(--error-fg); }
.btn-retry:hover, .btn-resume:hover { border-color: var(--ok-fg); color: var(--ok-fg); }

/*
 * ⚠️ One row, and it is a density fix rather than a taste one. A paused project used to be stated
 * three times within 90 vertical pixels of the top of the board: the fleet-wide INFO banner (which
 * earns its place — it is what tells you on /stats), this bar's badge, and its own Resume button.
 * The banner stays, because it is the only one of the three that appears on every page; the bar
 * stops spending a second row on the same sentence.
 */
.queue-controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.4rem 0.8rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.5rem 0.8rem;
	margin-bottom: 1rem;
	/*
	 * ⚠️ Height reserved for the first paint. This bar arrives by htmx (`hx-trigger="load"`), so the
	 * board renders a placeholder first and swaps — and without a floor, that swap moved the approval
	 * panel, the filters and every column down by the difference. Measured at 39.8px for the real bar
	 * against ~22px for a bare line of text; 2.5rem is that height, so the two boxes agree and nothing
	 * below them moves. See board.html for why this is a reserved box rather than a server-rendered bar.
	 */
	min-height: 2.5rem;
}

/* The same box, before htmx has answered. Quiet, because it is scaffolding rather than information. */
.queue-controls-loading { color: var(--muted); }
.queue-controls-status { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
/* The flash is a whole-width statement inside a row-shaped container. */
.queue-controls > .flash { flex-basis: 100%; }
.queue-controls .queue-controls-actions { margin-top: 0; }
.badge-paused { background: var(--warn-bg); color: var(--warn-fg); }
.badge-running { background: var(--ok-bg); color: var(--ok-fg); }
.queue-controls-project-form select { margin-right: 0.3rem; }

/* History (T-019) */
.history-rows-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }

.pagination { display: flex; align-items: center; gap: 0.75rem; }

.pagination-summary { color: var(--muted); font-size: 0.85rem; }

.pagination .btn:disabled { opacity: 0.5; cursor: default; }

/* Stats (T-018; two-level with stated units, T-082) */
.stats-week { margin-bottom: 2rem; }

.stats-week-title { font-size: 1rem; margin: 0 0 0.5rem; }
.stats-week-title a { text-decoration: none; }
.stats-week-title a:hover { text-decoration: underline; }

.stats-lede { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.25rem; max-width: 60rem; }

.stats-table-title { font-size: 0.9rem; margin: 0.9rem 0 0.35rem; font-weight: 600; }
.stats-unit-note { color: var(--muted); font-weight: 400; font-size: 0.82rem; }

/* T-082: the unit line under each column heading, and beside each duration's sample. */
.stats-unit {
	display: block;
	color: var(--muted);
	font-weight: 400;
	font-size: 0.72rem;
	text-transform: none;
	letter-spacing: 0;
}

.stats-caveat { color: var(--muted); font-size: 0.8rem; margin: 0.4rem 0 0; max-width: 60rem; }

/* Ten labelled columns do not fit a narrow window; the page must never scroll sideways itself. */
.stats-table-scroll { overflow-x: auto; }

.stats-table {
	width: 100%;
	border-collapse: collapse;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
	font-size: 0.85rem;
}

.stats-table th, .stats-table td {
	padding: 0.4rem 0.6rem;
	text-align: left;
	border-bottom: 1px solid var(--border);
}

.stats-table thead { background: var(--surface-sunken); }

.stats-table th { color: var(--info-fg); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.02em; }

.stats-table tbody tr:last-child td { border-bottom: none; }

.stats-total-row { background: var(--highlight-soft); font-weight: 600; }

.stats-rate-cell { min-width: 6rem; }

.stats-bar {
	height: 0.35rem;
	border-radius: var(--radius-pill);
	background: var(--border);
	margin-top: 0.2rem;
	overflow: hidden;
}

.stats-bar-fill { height: 100%; background: var(--highlight); }

/* T-082: module rows sit under the project rows, indented by CSS rather than by their own text. */
.stats-module-row td:first-child { color: var(--muted); padding-left: 1.2rem; }
.stats-module-row td:first-child::before { content: "\21b3\00a0"; }

/* T-082: the runs-per-request distribution. A mean hides the seven-run tail; this does not. */
.stats-histogram { margin: 0.3rem 0 0.4rem; max-width: 34rem; }

.stats-histogram-row {
	display: grid;
	grid-template-columns: 5.5rem 1fr 7rem;
	align-items: center;
	gap: 0.5rem;
	font-size: 0.8rem;
	padding: 0.12rem 0;
}

.stats-histogram-key { color: var(--muted); }

.stats-histogram-bar {
	height: 0.5rem;
	border-radius: var(--radius-pill);
	background: var(--border);
	overflow: hidden;
}

.stats-histogram-fill { display: block; height: 100%; background: var(--highlight); }

.stats-histogram-tail .stats-histogram-key,
.stats-histogram-tail .stats-histogram-value { color: var(--warn-fg); font-weight: 600; }

.stats-histogram-summary { color: var(--muted); font-size: 0.8rem; margin: 0.2rem 0 0; }

/* --- operator configuration screens (T-079) ----------------------------------------- */

.admin-lede { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }

.admin-breadcrumb { font-size: 0.85rem; color: var(--muted); margin-bottom: 0.4rem; }

.admin-muted { color: var(--muted); font-size: 0.85rem; }

.admin-section {
	margin: 1.25rem 0;
	padding: 0.9rem 1rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

.admin-section h2 { font-size: 1rem; margin: 0 0 0.6rem; }
.admin-section h3 { font-size: 0.9rem; margin: 1rem 0 0.5rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; }

.admin-table {
	width: 100%;
	border-collapse: collapse;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
	font-size: 0.88rem;
	margin-bottom: 0.8rem;
}

.admin-table th, .admin-table td {
	padding: 0.4rem 0.6rem;
	text-align: left;
	border-bottom: 1px solid var(--border);
}

.admin-table thead { background: var(--surface-sunken); }

.admin-table th { color: var(--info-fg); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.02em; }
.admin-table tbody tr:last-child td { border-bottom: none; }

.admin-form { display: flex; flex-direction: column; gap: 0.55rem; max-width: 44rem; }
.admin-form-inline { flex-direction: row; align-items: flex-end; flex-wrap: wrap; gap: 0.6rem; }
.admin-form-danger { flex-direction: row; align-items: center; gap: 0.6rem; margin-top: 0.9rem; }

.admin-form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.85rem; }
.admin-form label > span { color: var(--muted); }
.admin-form input[type="text"], .admin-form input[type="number"], .admin-form select, .admin-form textarea {
	padding: 0.35rem 0.5rem;
	border: 1px solid var(--border);
	border-radius: var(--radius);
	background: var(--surface);
	font: inherit;
	font-size: 0.88rem;
}

.admin-form textarea { font-family: var(--font-mono); font-size: 0.82rem; }

.admin-checkbox { flex-direction: row !important; align-items: center; gap: 0.4rem; }
.admin-checkbox > span { color: var(--text) !important; }

/* T-168: explanatory text under an admin form field, and its cautioning variant. */
.field-hint {
	margin: -0.35rem 0 0.2rem;
	font-size: 0.85rem;
	line-height: 1.4;
	color: var(--muted);
	max-width: 60ch;
}

.field-hint-warning { color: var(--warn-fg); }

.admin-fieldset {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.7rem 0.9rem;
	display: flex;
	flex-direction: column;
	gap: 0.55rem;
	margin: 0.3rem 0;
}

.admin-fieldset legend { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; padding: 0 0.3rem; }

.admin-actions { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.4rem; }

.admin-violations {
	margin: 0 0 0.7rem;
	padding: 0.5rem 0.6rem 0.5rem 1.6rem;
	background: var(--error-bg);
	color: var(--error-fg);
	border: 1px solid var(--error-border);
	border-radius: var(--radius);
	font-size: 0.85rem;
}


/*
 * T-180: the one render in which a cleartext secret is on screen — the issued review token.
 * Deliberately loud: the "copy it now, it will not be shown again" sentence is the whole point, and a
 * muted note under a table would be read after the operator had already navigated away.
 */
.admin-callout {
	margin: 0 0 0.9rem;
	padding: 0.7rem 0.8rem;
	background: var(--info-bg);
	color: var(--info-fg);
	border: 1px solid var(--info-border);
	border-radius: var(--radius);
	font-size: 0.85rem;
}

.admin-callout p { margin: 0 0 0.4rem; }
.admin-callout p:last-child { margin-bottom: 0; }
.admin-callout code { font-size: 0.9rem; word-break: break-all; user-select: all; }

.inline-form { display: inline; margin: 0; padding: 0; }

.module-list { list-style: none; margin: 0 0 0.8rem; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }

.module-card {
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.6rem 0.8rem;
	background: var(--bg);
}

.module-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; }
.module-name { font-weight: 600; }

.module-facts { display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem; margin: 0.3rem 0; }
.module-facts > div { display: flex; flex-direction: column; }
.module-facts dt { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; }
.module-facts dd { margin: 0; font-size: 0.85rem; }

.module-resolved { margin: 0.4rem 0; font-size: 0.85rem; }

.verify-proposal { margin: 0.8rem 0; }
.verify-proposal-rationale { white-space: pre-line; font-size: 0.85rem; margin: 0.3rem 0; }
.module-resolved summary { cursor: pointer; color: var(--muted); }

.module-actions { display: flex; gap: 0.4rem; margin-top: 0.4rem; }

.btn-small { font-size: 0.78rem; padding: 0.2rem 0.5rem; }
.btn-danger { background: var(--error-bg); color: var(--error-fg); border-color: var(--error-border); }

/* --- per-connection health (T-080 / TA-53) ------------------------------------------
   Degraded is VISIBLE without holding the whole system: this badge is a fact about one
   connection row, never a claim about the app. UNKNOWN ("no call yet") is not degraded. */

.conn-health-ok { background: var(--ok-bg); color: var(--ok-fg); }
.conn-health-bad { background: var(--error-bg); color: var(--error-fg); }
.conn-health-unknown { background: var(--surface); color: var(--muted); }

/* --- the two-level board and its pages (T-081) ------------------------------------------------ */

/* The park reason on a board row: a short headline, the full sentence in the title attribute. */
.item-parked {
	margin: 0 0 0.3rem;
	font-size: 0.78rem;
	color: var(--warn-fg);
	background: var(--warn-bg);
	border-radius: var(--radius-sm);
	padding: 0.15rem 0.4rem;
}

.request-parked-banner { margin-bottom: 0.6rem; }

/*
 * A ticket's runs, nested inside its one row — "a split ticket is one row, not N".
 *
 * ⚠️ **Each row is a grid with the same two-column template, and that is the fix rather than the
 * styling.** As a wrapping flex row, a module line spilled its extras onto the next visual line —
 * where `attempt 4` sat directly above the *following* module's badges and read as part of that row.
 * Now the module name and its state badge are columns 1 and 2 on line one (so every badge in the card
 * lines up down the right edge), and everything secondary is one full-width line below it.
 */
.item-runs {
	list-style: none;
	margin: 0 0 0.35rem;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.3rem;
}

/* One word of caption, because `Backend  Done` inside an AWAITING MERGE column reads as a
   contradiction until a reader knows these rows are per module. */
.item-runs-caption {
	margin: 0 0 0.2rem;
	font-size: 0.68rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--muted);
}

.item-run {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: baseline;
	gap: 0.15rem 0.4rem;
	font-size: 0.78rem;
}

.item-run-link {
	color: var(--text);
	text-decoration: none;
	font-weight: 600;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.item-run-link:hover { text-decoration: underline; }

/* CI, the MR link, attempt, sessions, the live chip — the row's second line, never its first. */
.item-run-detail {
	grid-column: 1 / -1;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.3rem;
	color: var(--muted);
}

/*
 * A run with nothing secondary to say draws no line at all. `:empty` would not do it — Thymeleaf
 * leaves whitespace between the conditional spans, and whitespace is not "empty" — so the test is
 * "has no element children". (A flex container drops whitespace-only children anyway, so this is
 * belt-and-braces against the row's own grid gap.)
 */
.item-run-detail:not(:has(> *)) { display: none; }

.item-run-attempt, .item-run-count, .item-retries { color: var(--muted); }

/* ~~`.item-superseded`~~ — what the board leaves out is now the Retries row of `.item-facts`, so the
   card states it in the same labelled place as every other fact instead of in a footnote. */

/* The runs section of the request page. */
.request-runs-section { margin-bottom: 1rem; }
.request-runs-section h2 { font-size: 1rem; margin: 0 0 0.5rem; }
.run-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.run-row {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 0.55rem 0.65rem;
}
.run-row-head { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; }
.run-row-link { font-weight: 600; color: var(--text); text-decoration: none; }
.run-row-link:hover { text-decoration: underline; }
.run-row-pipeline, .run-row-attempt { font-size: 0.78rem; color: var(--muted); }
.run-row-note, .run-state-note { margin: 0.3rem 0 0; font-size: 0.8rem; color: var(--muted); }

/*
 * T-178: the current run, and the ones it superseded.
 *
 * ⚠️ The emphasis is the whole feature. The run carrying the merge gate and the *fix selected*
 * control used to be the LAST row of a list that grows with every retry — this gives it the accent
 * rail and the weight, and puts the history behind a disclosure. Nothing here changes what either
 * list renders: both use `fragments/run-line`, and the difference is placement and emphasis only.
 */
.run-list-current { margin-bottom: 0.8rem; }
.run-row-current {
	border-left: 3px solid var(--accent);
	padding: 0.7rem 0.8rem;
}
.run-row-current .run-row-link { font-size: 1.05rem; }

/* The history reads as history: same markup, quieter, and one click away. */
.earlier-runs .run-row-earlier { background: transparent; }
.earlier-runs .run-row-earlier .run-row-link { font-weight: 500; }

/* T-136: how a run ended. Reads as prose rather than as a badge, because it is an answer and not a
   classification - and one shade darker than .run-state-note, which describes the state rather than
   the run. .run-outcome-outlived marks the case where the agent outlived the row's own transition. */
.run-outcome { margin: 0.45rem 0 0; font-size: 0.88rem; line-height: 1.5; color: var(--text); max-width: 70ch; }
.item-run-outcome { margin: 0.3rem 0 0; font-size: 0.78rem; line-height: 1.45; color: var(--muted); flex-basis: 100%; }
.run-outcome-outlived { border-left: 3px solid var(--warn, #b8860b); padding-left: 0.6rem; }
.run-row-findings, .run-row-rounds { margin: 0.25rem 0 0; font-size: 0.78rem; color: var(--muted); }
.findings-progress { margin: 0 0 0.4rem; font-size: 0.85rem; color: var(--muted); }

/*
 * BG-107's priority control. The source line renders for everyone; the form is operator-only, which
 * is why they are separate elements rather than one disabled widget — the current value and who set
 * it are exactly what a reader needs to see, form or no form.
 */
.priority-control {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin-bottom: 1rem;
	padding: 0.45rem 0.6rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font-size: 0.85rem;
}
.priority-source { color: var(--muted); font-size: 0.78rem; }
.priority-form { display: flex; align-items: center; gap: 0.4rem; margin-left: auto; }
.priority-label { color: var(--muted); font-size: 0.78rem; }

/*
 * T-120's base-branch control (BG-128…BG-131). Same box as the priority control above, deliberately:
 * they are the same gesture on the same row of facts. The locked line replaces the form rather than
 * greying it out — BG-131's reason is a sentence worth reading, not a tooltip.
 */
.base-ref-control {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin-bottom: 1rem;
	padding: 0.45rem 0.6rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font-size: 0.85rem;
}
.base-ref-source, .base-ref-unrouted, .base-ref-unknown { color: var(--muted); font-size: 0.78rem; }
.base-ref-form { display: flex; align-items: center; gap: 0.4rem; margin-left: auto; }
.base-ref-clear-form { display: flex; align-items: center; }
.base-ref-label { color: var(--muted); font-size: 0.78rem; }
.base-ref-form input[type="text"] { min-width: 14rem; }
.base-ref-locked { margin: 0 0 0 auto; color: var(--muted); font-size: 0.78rem; max-width: 46rem; }

/* TA-49: which level a timeline row belongs to. `ticket` is the null-run-id case. */
.timeline-run, .timeline-scope {
	font-size: 0.75rem;
	color: var(--muted);
	white-space: nowrap;
	font-variant-numeric: tabular-nums;
}
.timeline-run { text-decoration: none; }
.timeline-run:hover { text-decoration: underline; }

/* ---- Admin configuration tree and layout (T-123 / #84) ----------------------------------
 *
 * The tree is a column beside the page, and below 60rem it becomes a scrollable block above it.
 * ⚠️ That media query is the FIRST in this file (the app is desktop-first, deliberately), and it is
 * scoped to `.admin-layout` — widening it to the board, run and stats pages is a different ticket.
 * No <details> disclosure: its open state cannot be driven by a media query without JS, and the
 * harness may run either browser. A block that scrolls behaves the same in both.
 */

.admin-layout {
	display: grid;
	grid-template-columns: 15rem minmax(0, 1fr);
	gap: 1.25rem;
	align-items: start;
}

/* The grid child holding the page itself: `minmax(0, 1fr)` above is what keeps a wide <pre> or a
   long unbroken URL inside it from stretching the column. */
.admin-pane { min-width: 0; }

.admin-nav {
	position: sticky;
	top: 1.5rem;
	padding: 1rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	font-size: 0.9rem;
	max-height: 80vh;
	overflow-y: auto;
}

.admin-nav .nav-section { margin-bottom: 1.5rem; }
.admin-nav .nav-section:last-child { margin-bottom: 0; }

.admin-nav .nav-label {
	display: block;
	color: var(--muted);
	font-size: 0.75rem;
	font-weight: 500;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	margin-bottom: 0.5rem;
	padding: 0 0.5rem;
}

/* The section heading that is also a link (Projects → the list page). */
.admin-nav .nav-label-link {
	display: block;
	font-size: 0.75rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.05em;
	margin-bottom: 0.5rem;
	color: var(--muted);
}

.admin-nav .nav-list {
	list-style: none;
	margin: 0;
	padding: 0;
}

.admin-nav .nav-list > li { margin-bottom: 0.25rem; }

.admin-nav a {
	display: block;
	padding: 0.5rem;
	border-radius: 0.25rem;
	text-decoration: none;
	color: var(--text);
	transition: background-color 0.15s;
}

.admin-nav a:hover {
	background: var(--surface-sunken);
}

/* Exactly one of these exists per render — see fragments/admin-nav.html. */
.admin-nav a[aria-current="page"] {
	background: var(--accent-strong);
	color: var(--accent-contrast);
	font-weight: 500;
}

/* The project whose areas are expanded below. Not `aria-current`: that marks the page, and the page
   is one of the leaves under it. */
.admin-nav a.nav-current-project {
	font-weight: 600;
	background: var(--surface-sunken);
}

.admin-nav .nav-group {
	display: block;
	padding: 0.5rem 0.5rem 0.5rem 0.5rem;
	color: var(--muted);
	font-weight: 500;
	font-size: 0.85rem;
}

.admin-nav .nav-sublist {
	list-style: none;
	margin: 0;
	padding-left: 1rem;
	margin-top: 0.25rem;
}

.admin-nav .nav-sublist > li { margin-bottom: 0.25rem; }

/* A module card's name is a link to the module's own page since T-123 — it keeps the card's weight. */
a.module-name { color: var(--text); text-decoration: none; }
a.module-name:hover { text-decoration: underline; }

@media (max-width: 60rem) {
	.admin-layout {
		grid-template-columns: 1fr;
	}

	.admin-nav {
		position: static;
		max-height: 12rem;
		overflow-y: auto;
		margin-bottom: 1.25rem;
	}
}

/* ---- T-130: the intake contract, /intake and the board's link to it. -----------------------
   Tier 2/3 role tokens only, like every rule above it. This page is read by people who do not
   log in, so it leans on plain text weight rather than on badges an operator would recognise. */

.board-intake-link { margin: -0.4rem 0 0.9rem; font-size: 0.85rem; color: var(--muted); }
.board-intake-link a { color: var(--accent); }

.intake-lede { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.25rem; max-width: 46rem; }

.intake-project {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 1rem 1.1rem;
	margin-bottom: 1rem;
}

.intake-project-name { font-size: 1.05rem; margin: 0 0 0.3rem; display: flex; align-items: center; gap: 0.5rem; }
.intake-project-description { color: var(--muted); font-size: 0.9rem; margin: 0 0 0.8rem; }
.intake-badge-paused { background: var(--warn-bg); color: var(--warn-fg); }

.intake-step {
	font-size: 0.78rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--muted);
	margin: 1rem 0 0.4rem;
}

.intake-tracker { border-left: 3px solid var(--highlight); padding-left: 0.8rem; margin-bottom: 0.8rem; }
.intake-tracker-kind { font-weight: 600; margin: 0 0 0.35rem; display: flex; align-items: center; gap: 0.5rem; }
.intake-gate, .intake-routing, .intake-app-labels, .intake-expect { margin: 0 0 0.4rem; }

.intake-code {
	font-family: var(--font-mono);
	font-size: 0.85em;
	background: var(--surface-sunken);
	border: 1px solid var(--border);
	border-radius: 3px;
	padding: 0.05rem 0.3rem;
}

.intake-routing-example { background: var(--highlight-soft); }

.intake-note { color: var(--muted); font-size: 0.85rem; margin: 0 0 0.4rem; }
.intake-note-warn { color: var(--warn-fg); }
.intake-muted { color: var(--muted); font-size: 0.85rem; }

.intake-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-bottom: 0.5rem; }
.intake-table th, .intake-table td {
	text-align: left;
	padding: 0.4rem 0.6rem;
	border-bottom: 1px solid var(--border);
	vertical-align: top;
}
.intake-table thead { background: var(--surface-sunken); }
.intake-table th {
	color: var(--info-fg);
	font-weight: 600;
	font-size: 0.78rem;
	text-transform: uppercase;
	letter-spacing: 0.02em;
}
.intake-table tbody tr:last-child td { border-bottom: none; }
.intake-module-name { font-weight: 600; }
.intake-module-disabled { color: var(--muted); }
.intake-table td .intake-code + .intake-code { margin-left: 0.25rem; }

@media (max-width: 480px) {
	/* Not display: block — that strips the table's semantics for some screen readers. Letting a cell
	 * break anywhere lowers its minimum width, so the table fits the viewport as a table. */
	.intake-table th, .intake-table td { overflow-wrap: anywhere; padding: 0.35rem 0.4rem; }
}

.intake-troubleshooting {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 6px;
	padding: 1rem 1.1rem;
}
.intake-troubleshooting h2 { font-size: 1rem; margin: 0 0 0.6rem; }
.intake-faq { margin: 0; }
.intake-faq dt { font-weight: 600; margin-top: 0.7rem; }
.intake-faq dd { margin: 0.2rem 0 0; color: var(--muted); }

/* ================================================================================================
 * T-137 — agent activity and spend.
 *
 * Two problems this section is the visual half of:
 *
 * 1. `MR open` was read as "the merge request is finished, go and review it" while a review agent
 *    and up to two fix-round agents were still pushing commits to that branch. So "an agent is
 *    working right now" has to be visible at a glance, on the board, without opening anything.
 * 2. A run can launch the agent several times and the page showed one of them. The session list
 *    gives each invocation a row of its own.
 *
 * ⚠️ The motion is CSS and nothing else (BG-111: no JS build, no vendored library), and it is
 * disabled under `prefers-reduced-motion` — a status board somebody leaves open all day must not
 * animate at a person who has asked it not to.
 * ============================================================================================== */

.activity-dot {
	display: inline-block;
	width: 0.5rem;
	height: 0.5rem;
	border-radius: var(--radius-pill);
	background: currentColor;
	flex: none;
	animation: activity-pulse 1.6s ease-in-out infinite;
}

@keyframes activity-pulse {
	0%, 100% { opacity: 1; transform: scale(1); }
	50% { opacity: 0.35; transform: scale(0.7); }
}

@media (prefers-reduced-motion: reduce) {
	.activity-dot { animation: none; }
}

/*
 * The card's live line. Yellow rather than purple: purple is "your turn" in this palette (--turn-*)
 * and this is the exact opposite claim — a machine has it, wait.
 */
.item-activity {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	margin: 0.2rem 0 0.3rem;
	padding: 0.2rem 0.45rem;
	border-radius: var(--radius-sm);
	background: var(--warn-bg);
	color: var(--warn-fg);
	font-size: 0.78rem;
	font-weight: 600;
}

.item-activity .activity-age { font-weight: 400; opacity: 0.8; }

/*
 * The blind-spot line: MR_OPEN with no session open yet, which is the dispatcher waiting for CI
 * before it launches the review agent. Quieter than the live line — it is a "not yet", not an event.
 */
.item-not-ready {
	margin: 0.2rem 0 0.3rem;
	font-size: 0.78rem;
	color: var(--muted);
	font-style: italic;
}

.badge-activity {
	display: inline-flex;
	align-items: center;
	gap: 0.3rem;
	background: var(--warn-bg);
	color: var(--warn-fg);
}

.item-usage, .run-row-usage, .run-row-sessions, .item-run-sessions,
.item-run-split, .run-row-split {
	color: var(--muted);
	font-size: 0.78rem;
}

/* A total that is a floor rather than an answer reads as provisional rather than as wrong. */
.usage-partial::after { content: " +"; opacity: 0.7; }

/*
 * An MR link on a run a machine is still working. NOT disabled — a person may still want to look —
 * but it must not read as the invitation that "MR" alone read as.
 */
.item-link-provisional { opacity: 0.75; font-style: italic; }

/* ---- the run page's session list ---- */

.run-sessions-section { margin: 1.2rem 0; }
.run-sessions-intro { color: var(--muted); font-size: 0.85rem; margin: 0 0 0.6rem; }

.run-usage-total { display: flex; gap: 1.5rem; flex-wrap: wrap; margin: 0 0 0.5rem; }
.run-usage-total > div { display: flex; flex-direction: column; }
.run-usage-total dt {
	font-size: 0.75rem;
	color: var(--muted);
	text-transform: uppercase;
	letter-spacing: 0.03em;
}
.run-usage-total dd { margin: 0; font-variant-numeric: tabular-nums; }
.run-usage-total dd.unrecorded { color: var(--muted); font-style: italic; }

.run-usage-partial { color: var(--muted); font-size: 0.8rem; font-style: italic; margin: 0 0 0.8rem; }

/*
 * T-203/BG-219: `fragments/plan-estimate`, the one rendering the run page and the request page share.
 * The comparison leads and the run/round count trails it muted. ⚠️ The "not recorded yet" span is a
 * bare `<span class="unrecorded">`, so neither `dd.unrecorded` nor `.run-session-meta .unrecorded`
 * reaches it - without the rule below it renders as ordinary body text claiming an absence.
 *
 * `.plan-estimate-estimated` and `.plan-estimate-actual` are deliberately unstyled: they are the two
 * figures being compared, so body text is what they should be, and a rule that only restated the
 * inherited value would be a hook for a later change nobody asked for.
 */
.plan-estimate-line { font-variant-numeric: tabular-nums; }
.plan-estimate-ratio { font-weight: 600; }
.plan-estimate-rounds { color: var(--muted); }
.plan-estimate-line .unrecorded { color: var(--muted); font-style: italic; }

.run-plan-estimate { margin: 0 0 0.8rem; font-size: 0.85rem; }
.run-plan-estimate-label {
	color: var(--muted);
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.03em;
}

.run-session-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }

.run-session-row {
	border: 1px solid var(--info-border);
	border-left: 3px solid var(--info-border);
	border-radius: var(--radius);
	padding: 0.5rem 0.7rem;
	background: var(--surface, transparent);
}

/*
 * The one being rendered below gets the accent edge, so "which of these am I reading" is answered.
 * A live row gets the attention edge and NOT a yellow fill: the running badge inside it is already
 * `--warn-bg`, and a yellow badge on a yellow row is a badge nobody can see.
 */
.run-session-row.is-selected { border-left-color: var(--accent); }
.run-session-row.is-live { border-left-color: var(--warn-border); border-color: var(--warn-border); }

.run-session-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.run-session-ordinal { color: var(--muted); font-size: 0.8rem; }
.run-session-current { color: var(--muted); font-size: 0.8rem; font-style: italic; }
.run-session-select { font-size: 0.8rem; }

.run-session-meta {
	display: flex;
	gap: 0.9rem;
	flex-wrap: wrap;
	margin-top: 0.25rem;
	color: var(--muted);
	font-size: 0.78rem;
	font-variant-numeric: tabular-nums;
}
.run-session-meta .unrecorded { font-style: italic; }

.run-session-id { display: block; margin-top: 0.2rem; font-size: 0.7rem; color: var(--muted); }

/* The run shape, which is the thing a reader scans this list for: plan / implement / review. */
.badge-shape { background: var(--info-bg); color: var(--info-fg); }
.badge-shape.shape-plan { background: var(--turn-bg); color: var(--turn-fg); }
.badge-shape.shape-review { background: var(--ok-bg); color: var(--ok-fg); }

/*
 * The run page's live banner. It sits above the state note because a running process outranks every
 * classification on the page — including the one the note is explaining.
 */
.banner-working {
	background: var(--warn-bg);
	color: var(--warn-fg);
	border-color: var(--warn-border);
	display: flex;
	align-items: center;
	gap: 0.5rem;
	font-weight: 600;
}

/* ===================================================================================
 * The stage checklist (redesign 1c) — one module's pipelines, the current one expanded.
 *
 * Emphasis is carried by the row's own background and border, never by hiding the others: a
 * checklist whose settled rows were collapsed away would answer "where is the work" and stop
 * answering "how did it get here", which is the half a reader arriving at a failure needs.
 * =================================================================================== */
.stage-checklists { display: flex; flex-direction: column; gap: 0.8rem; margin-bottom: 1rem; }

.stage-checklist {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 4px;
	overflow: hidden;
}

.stage-checklist-module {
	margin: 0;
	padding: 0.6rem 1rem;
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--muted);
	border-bottom: 1px solid var(--border);
}

.stage-rows { list-style: none; margin: 0; padding: 0; }

.stage-row {
	display: grid;
	grid-template-columns: 1.6rem minmax(0, 1fr);
	gap: 0.75rem;
	padding: 0.7rem 1rem;
	align-items: start;
	border-bottom: 1px solid var(--surface-sunken);
}

.stage-row:last-child { border-bottom: none; }

/* The one row worth reading first. The status colours below override the surface. */
.stage-row-current { background: var(--info-bg); padding: 0.95rem 1rem; }
.stage-row-current.stage-blocked, .stage-row-current.stage-failed { background: var(--error-bg); }
.stage-row-current.stage-waiting { background: var(--turn-bg); }
.stage-row-current.stage-done { background: var(--ok-bg); }
.stage-row-current .stage-label { font-weight: 700; }

/* Muted, not hidden: a pipeline that never ran is a fact about the queue, not an absence. */
.stage-not-reached .stage-label, .stage-not-reached .stage-status,
.stage-skipped .stage-label, .stage-skipped .stage-status { color: var(--muted); }

.stage-marker {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.35rem;
	height: 1.35rem;
	border-radius: 999px;
	font-size: 0.8rem;
	font-weight: 700;
	background: var(--info-bg);
	color: var(--info-fg);
	border: 1px solid var(--info-border);
}

.marker-done { background: var(--ok-bg); color: var(--ok-fg); border-color: var(--ok-border); }
.marker-blocked, .marker-failed { background: var(--error-fg); color: var(--surface); border-color: var(--error-fg); }
.marker-waiting { background: var(--turn-bg); color: var(--turn-fg); border-color: var(--turn-border); }
.marker-not-reached, .marker-skipped { background: var(--surface-sunken); color: var(--muted); border-color: var(--border); }

.stage-body { min-width: 0; }

.stage-head {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: 0.5rem;
	font-size: 0.85rem;
}

.stage-label { font-size: 0.95rem; font-weight: 600; color: var(--text); }

.stage-status { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.7rem; font-weight: 700; color: var(--muted); }
.stage-blocked .stage-status, .stage-failed .stage-status { color: var(--error-fg); }
.stage-waiting .stage-status { color: var(--turn-fg); }
.stage-done .stage-status { color: var(--ok-fg); }

.stage-state, .stage-retries { color: var(--muted); }

.stage-run-link { font-size: 0.85rem; }

.stage-blocked-reason {
	margin-top: 0.45rem;
	padding: 0.5rem 0.6rem;
	background: var(--surface);
	border: 1px solid var(--error-border);
	border-radius: 2px;
	color: var(--error-fg);
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: 0.8rem;
	overflow-wrap: anywhere;
}

.stage-facts {
	display: flex;
	flex-wrap: wrap;
	gap: 0.9rem;
	margin-top: 0.4rem;
	font-size: 0.8rem;
	color: var(--muted);
}

/* ===================================================================================
 * The work row: stages left, facts and recent activity right (redesign 1c).
 *
 * ⚠️ One column below 1000px, checklist FIRST — the side pane is context for the stages, and a
 * phone that led with ten facts would have moved the current step below the fold again, which is
 * the whole thing this layout exists to stop.
 * =================================================================================== */
.request-work {
	display: grid;
	grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
	gap: 1rem;
	align-items: start;
	margin-bottom: 1rem;
}

.request-work-main { min-width: 0; }
.request-work-main .stage-checklists { margin-bottom: 0; }

@media (max-width: 1000px) {
	.request-work { grid-template-columns: minmax(0, 1fr); }
}

.request-facts {
	display: flex;
	flex-direction: column;
	gap: 1rem;
	min-width: 0;
}

.request-facts > section {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 4px;
	padding: 0.9rem 1rem 1rem;
}

.request-facts h2 {
	margin: 0 0 0.7rem;
	font-size: 0.75rem;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--muted);
}

/* The excerpt is newest-first and rendered once; the polled timeline below is oldest-first. */
.recent-activity-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	font-size: 0.85rem;
}

.recent-activity-list li {
	display: grid;
	grid-template-columns: 4.6rem minmax(0, 1fr);
	gap: 0.1rem 0.6rem;
}

.recent-activity-time {
	color: var(--muted);
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: 0.78rem;
	font-variant-numeric: tabular-nums;
}

.recent-activity-headline { color: var(--text); overflow-wrap: anywhere; }
/* Only the two tones that mean *look here*. `warn` is BG-70's in-flight, which is every other row
   on a live ticket, and colouring it would make the excerpt one colour. */
.recent-activity-list li.tone-error .recent-activity-headline { color: var(--error-fg); font-weight: 600; }
.recent-activity-list li.tone-turn .recent-activity-headline { color: var(--turn-fg); font-weight: 600; }

.recent-activity-run { grid-column: 2; color: var(--muted); font-size: 0.78rem; }

.recent-activity-more { display: inline-block; margin-top: 0.8rem; font-size: 0.85rem; }

/* The facts moved out of the header, where they were a flex row under the title. Two columns in a
   narrow pane, one when the pane itself is the page. */
.request-facts-grid .item-detail-meta {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0.6rem 1rem;
	margin: 0;
}

@media (max-width: 560px) {
	.request-facts-grid .item-detail-meta { grid-template-columns: minmax(0, 1fr); }
}

.request-facts-grid .run-usage-partial { margin: 0.8rem 0 0; font-size: 0.8rem; }

/* T-203/BG-219: one per module, and the comparison is a sentence - it gets the full width. */
.request-plan-estimate { grid-column: 1 / -1; }
.request-plan-estimate .plan-estimate-line { font-size: 0.85rem; }

/* The audit trail (/admin/requests/{id}/audit). */
.request-audit section { margin-bottom: 1.5rem; }
.request-audit .audit-run { border-top: 1px solid var(--border); padding-top: 0.75rem; margin-top: 0.75rem; }
.request-audit .audit-text { white-space: pre-wrap; word-break: break-word; margin: 0; font-size: 0.82rem; }
.request-audit p.unrecorded { color: var(--muted); font-style: italic; }
