/* ====================================================================
   Beacon Consultants - customer web app
   Built from the V1-68 demo's design tokens (.workflow/scratch/demo/DESIGN-TOKENS.md).
   Every value below that the demo also uses is a variable here, so the rest of the
   app is written in tokens rather than in hex codes.
   ==================================================================== */

:root {
	/* Brand and status */
	--color-primary: #EF233C;
	--color-primary-light: #FFF3F4;
	--color-ink: #171725;
	--color-info: #0058FF;
	--color-info-hover: #0047CC;
	/* #0077C2, not the demo's #00A0FF - white initials on #00A0FF are ~2.6:1, below AA's 4.5:1.
	   Demo colours are for decoration; anything that carries text meets WCAG AA (see the tabs
	   and form-hint rules below for the same reasoning applied elsewhere). */
	--color-avatar: #0077C2;
	--color-success: #5FBF8B;
	--color-success-bg: #E8F5E9;
	--color-danger: #F0142F;
	--color-danger-bg: #FFEBEE;

	/* Surfaces */
	--color-bg-app: #FAFAFA;
	--color-bg-card: #FFFFFF;
	--color-bg-section: #F3F3F3;
	--color-bg-muted: #F0F0F0;

	/* Text */
	--color-text-primary: #131523;
	--color-text-secondary: #666666;
	--color-text-muted: #999999;
	--color-text-header: #5A607F;
	--color-text-inverse: #FFFFFF;

	/* Borders */
	--color-border: #E5E5E5;
	--color-border-dark: #CCCCCC;
	--color-border-light: #F0F0F0;

	/* Spacing */
	--spacing-xs: 4px;
	--spacing-sm: 6px;
	--spacing-md: 8px;
	--spacing-lg: 12px;
	--spacing-xl: 16px;
	--spacing-2xl: 20px;
	--spacing-3xl: 24px;
	--spacing-4xl: 30px;
	--spacing-5xl: 40px;
	--spacing-6xl: 48px;

	/* Radii */
	--radius-sm: 4px;
	--radius-md: 6px;
	--radius-lg: 8px;
	--radius-xl: 10px;
	--radius-pill: 16px;
	--radius-round: 50%;

	/* Type */
	--font-family: 'Lexend Deca', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
	/* The export preview table and its column pills only - both stand for a CSV file's own text. */
	--font-mono: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
	--font-size-2xs: 10px;
	--font-size-xs: 11px;
	--font-size-sm: 12px;
	--font-size-base: 13px;
	--font-size-md: 14px;
	--font-size-lg: 15px;
	--font-size-xl: 16px;
	--font-size-2xl: 18px;
	--font-size-3xl: 20px;
	--font-size-4xl: 24px;
	--font-size-5xl: 28px;

	/* Icons, shadows, motion */
	--icon-md: 16px;
	--icon-lg: 18px;
	--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
	--shadow-base: 0 2px 8px rgba(0, 0, 0, 0.1);
	--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
	--transition-fast: 0.2s;

	/* Layout */
	/* Matches the demo's .main-content max-width exactly (dashboard-styles.css), so the content
	   column and app bar both stop widening at the same point the demo does. */
	--content-max-width: 1600px;
	--app-bar-height: 56px;
}

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

html,
body {
	margin: 0;
	padding: 0;
}

body {
	background: var(--color-bg-app);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-md);
	font-weight: 400;
	line-height: 1.5;
	-webkit-font-smoothing: antialiased;
}

a {
	color: var(--color-info);
	text-decoration: none;
}

a:hover {
	text-decoration: underline;
}

.material-symbols-outlined {
	font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
}

/* top/left: 0 pins this at the viewport/nearest-positioned-ancestor origin instead of its static
   position - without them, an absolutely positioned element with no offsets keeps the x/y it would
   have had in normal flow, so one sitting inside a wide, horizontally-scrolling table cell (e.g. the
   "Actions" column heading) stretched the whole document's scrollWidth out to that cell's un-clipped
   position and made the page itself scroll sideways, even though the table's own overflow was
   already contained by its .card. */
.visually-hidden {
	position: absolute;
	top: 0;
	left: 0;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ====================================================================
   TOP BAR
   ==================================================================== */
.app-bar {
	position: sticky;
	top: 0;
	z-index: 1000;
	background: var(--color-bg-card);
	box-shadow: var(--shadow-sm);
}

.app-bar-inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-3xl);
	min-height: var(--app-bar-height);
	padding: var(--spacing-md) var(--spacing-5xl);
}

.app-bar-left {
	display: flex;
	align-items: center;
	gap: var(--spacing-3xl);
	min-width: 0;
}

.app-bar-right {
	display: flex;
	align-items: center;
	/* 20px between the credits pill and the avatar, as the demo's own bar has it. */
	gap: var(--spacing-2xl);
	flex: 0 0 auto;
}

/* The real Beacon mark, same rendered height as the demo (dashboard-styles.css .beacon-logo:
   height: var(--spacing-3xl), width left to the SVG's own aspect ratio). */
.beacon-logo {
	height: var(--spacing-3xl);
}

/* Tablet portrait and below - swap the wide wordmark for the compact mark, same technique and
   breakpoint as the demo (dashboard-styles.css ~line 8974). Separate rule from the customer
   .app-bar/.auth-brand context so the admin bar's dark background gets the white-on-transparent
   mark instead.

   Cache busting (phase 13, ledger item P1): these two url()s are plain paths, not run through
   View::AssetUrl(), so they carry no content-hash query string of their own. A new app.css URL
   does NOT re-fetch them - a sub-resource keeps its own cached URL - which is why /img/ is
   served with a one-day Cache-Control rather than this file's immutable year (see
   rootfs/etc/apache2/conf-available/zz-consultants-hardening.conf): a changed mark is at worst
   a day late, never stuck for a year. */
@media (max-width: 768px) {
	.app-bar .beacon-logo,
	.auth-brand .beacon-logo {
		content: url('/img/beacon-mark.svg');
		height: 32px;
		width: auto;
	}

	.admin-bar .beacon-logo {
		content: url('/img/beacon-mark-white.svg');
		height: 32px;
		width: auto;
	}
}

.company-name {
	padding-left: var(--spacing-xl);
	border-left: 1px solid var(--color-border);
	color: var(--color-text-primary);
	font-size: var(--font-size-lg);
	font-weight: 600;
	line-height: 1.4;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.credit-chip {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs);
	padding: var(--spacing-md) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-size: var(--font-size-md);
	font-weight: 600;
	/* The shell's body sets line-height: 1.5 for readable paragraphs elsewhere; a pill sized to
	   its own text needs the font's own natural line-height instead, or the inherited 1.5 adds
	   several extra pixels of height on top of the padding below. */
	line-height: normal;
	white-space: nowrap;
	transition: all var(--transition-fast);
}

.credit-chip:hover {
	border-color: var(--color-border-dark);
	background: var(--color-bg-app);
	text-decoration: none;
}

/* The balance is a <strong> for its semantic weight, not for the browser's default 'bolder'
   rendering - inside this 600-weight pill, 'bolder' resolves to 900. The demo's own version is a
   plain span at 600, so this pins the same value explicitly. */
.credit-chip strong {
	font-weight: 600;
}

.credit-word {
	color: var(--color-text-secondary);
	font-weight: 400;
}

/* ====================================================================
   AVATAR MENU
   ==================================================================== */
.user-menu {
	position: relative;
}

.user-avatar {
	display: flex;
	align-items: center;
	justify-content: center;
	width: var(--spacing-5xl);
	height: var(--spacing-5xl);
	padding: 0;
	border: none;
	border-radius: var(--radius-round);
	background: var(--color-avatar);
	color: var(--color-text-inverse);
	font-family: var(--font-family);
	font-size: var(--font-size-md);
	font-weight: 600;
	cursor: pointer;
}

.user-avatar:focus-visible {
	outline: 2px solid var(--color-info);
	outline-offset: 2px;
}

.user-menu-dropdown {
	position: absolute;
	top: calc(100% + var(--spacing-md));
	right: 0;
	z-index: 1000;
	min-width: 200px;
	overflow: hidden;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-lg);
	background: var(--color-bg-card);
	box-shadow: var(--shadow-lg);
}

.user-menu-dropdown[hidden] {
	display: none;
}

.user-menu-form {
	margin: 0;
}

.user-menu-item {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
	width: 100%;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: none;
	background: transparent;
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-base);
	font-weight: 500;
	text-align: left;
	cursor: pointer;
	transition: background var(--transition-fast);
}

.user-menu-item:hover {
	background: var(--color-bg-app);
	text-decoration: none;
}

.user-menu-item .material-symbols-outlined {
	color: var(--color-text-secondary);
	font-size: var(--icon-md);
}

/* #C4102A, not --color-danger: the token is 4.34:1 on the white panel, and "Log out" is 13px
   normal text, so it misses AA's 4.5:1. Same override and same reason as .admin-badge-stale and
   .property-remove further down. Found in phase 12's admin sweep (ledger item 12) when this menu
   moved onto the admin bar too. */
.user-menu-item-danger {
	color: #C4102A;
}

.user-menu-item-danger .material-symbols-outlined {
	color: #C4102A;
}

/* ====================================================================
   CONTENT COLUMN
   ==================================================================== */
.main-content {
	max-width: var(--content-max-width);
	margin: 0 auto;
	padding: 25px var(--spacing-5xl) var(--spacing-5xl) var(--spacing-5xl);
}

/* The demo's own .page-description sits 15px under the title and leaves 39px before whatever
   comes next (a section heading or a card) - .page-subtitle below carries the first figure, this
   one the second. Ours were 6px and 25px, which crowded the title into its description and then
   the description into the page below it. Phase 12, ledger item 16. */
.page-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-3xl);
	margin-bottom: 39px;
}

/* A list page's header (h1, description, primary action) as a grid rather than a title-block
   div plus a button: the demo's own mobile layout puts the button between the two lines of text,
   which only a visual reorder (grid-template-areas here) can do without changing what a screen
   reader meets first - the DOM stays h1, then description, then action (the right reading order
   at any width), and only the visual position of the button moves. */
.page-header-list {
	display: grid;
	grid-template-columns: 1fr auto;
	grid-template-areas:
		"title  button"
		"desc   button";
	align-items: center;
	/* .page-header's own gap would otherwise open a 24px ROW gap between the title and
	   description rows on top of .page-subtitle's 15px margin - only the column gap is wanted. */
	gap: 0 var(--spacing-3xl);
}

.page-header-list h1 {
	grid-area: title;
}

.page-header-list .page-subtitle {
	grid-area: desc;
}

.page-header-list .btn {
	grid-area: button;
	/* Level with the h1, as the demo has it, not centred on the whole two-line block. */
	align-self: start;
	justify-self: end;
}

.page-header h1 {
	margin: 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-5xl);
	font-weight: 600;
	/* The demo sets no line-height on a heading, so its h1 is 35px tall at 28px. Ours inherited
	   the body's 1.5 and stood 42px, which pushed every page's first card 7px down and made the
	   two-line report titles on the detail pages sit further apart than the demo's. */
	line-height: 1.25;
}

/* Every card in the demo is a white box with a shadow and NO border, at 24px padding:
   .settings-card, .co-search-card, .co-wallet-card and .data-table-container are all
   `box-shadow: var(--shadow-base)` (dashboard-styles.css:6593, consultants-styles.css:1081, :893,
   dashboard-styles.css:3703). Ours drew a 1px #E5E5E5 border, no shadow, at 30px padding, which
   reads as a flatter and boxier page at every width. Phase 12, ledger item 16. */
.card {
	padding: var(--spacing-3xl);
	border-radius: var(--radius-lg);
	background: var(--color-bg-card);
	box-shadow: var(--shadow-base);
	/* Unconditional, not just under the 720px breakpoint below: a wide table (7+ admin columns)
	   can outgrow a card anywhere from about 375px up to roughly 900px, and a card with room to
	   spare never shows a scrollbar anyway - this is what keeps a table's own horizontal scroll
	   inside its card instead of the whole page scrolling sideways at, say, 768px. */
	overflow-x: auto;
}

/* A card whose content is a table. The demo's .data-table-container (dashboard-styles.css:3703)
   has no padding at all: the shaded header band runs edge to edge and the card's own radius clips
   it. Ours put the same table inside a 30px-padded card, which inset the band on every list page
   and left a white margin above the header and below the last row. Used together with .card, so
   the .card + .card spacing rules still see it. Phase 12, ledger item 16. */
.table-card {
	padding: 0;
}

/* The two things that can sit above a table inside that card still need the card's own padding
   back - and, being flush to the card's edges, they draw the demo's full-width divider for free.
   The `.card.table-card` prefix is what beats the equally specific full-bleed rule further down
   (a card with padding has to pull its heading band out with negative margins; this one does not
   have to, because the card has no padding to cancel). */
.card.table-card > .card-title:first-child,
.card.table-card > .card-header-row:first-child {
	margin: 0;
	padding: var(--spacing-3xl);
}

/* The empty state replaces the table in the same card (an unfiltered list with no rows). */
.table-card > .empty-state {
	padding: var(--spacing-3xl);
}

.card-message {
	margin: 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
}

/* A link sitting inside a paragraph of ordinary text needs more than colour to mark it as a
   link (axe's link-in-text-block rule) - the close-account card's support address is the case
   this exists for. */
.card-message a {
	text-decoration: underline;
}

/* No bottom margin is right where .card-message is the last thing in its card (the credits page's
   Paddle note) and wrong where a form follows it: on the admin company detail page's Free re-run
   card the "Report" label sat flush against the sentence above it, which the customer credits
   page's own eyebrow-then-controls cards never do. Phase 12, ledger item 12. */
.card-message + form,
.card-message + .form-hint {
	margin-top: var(--spacing-3xl);
}

/* The Edit user page's email line: a plain sentence above the form, not a card-message (which
   has no bottom margin and would sit flush against the Name label below it). */
.card-subtext {
	margin: 0 0 var(--spacing-3xl) 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

/* Forms sit in a column roughly this wide inside their card - the demo never lets an input or a
   select stretch the full width of the card. */
.form-column {
	max-width: 600px;
}

/* ====================================================================
   BUTTONS
   ==================================================================== */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--spacing-sm);
	padding: var(--spacing-md) var(--spacing-2xl);
	border: 1px solid transparent;
	border-radius: var(--radius-md);
	font-family: var(--font-family);
	font-size: var(--font-size-md);
	font-weight: 500;
	/* `normal`, as the demo's own .co-btn: the body's inherited 1.5 made the content box 21px
	   against the demo's 18px, which stood every button 39px tall next to the demo's 36px. */
	line-height: normal;
	white-space: nowrap;
	cursor: pointer;
	transition: all var(--transition-fast);
}

/* Ledger item E3, phase 14: "Copy prompt for AI" (the first .btn to carry a leading icon) stood
   42px against every icon-less button's 36px - Material Symbols' own glyph metrics make
   line-height: normal (the rule just above) compute a much taller line box for the icon than for
   the button's own text, and a flex item's line box sets its height like any other. Pinning the
   icon's own line-height to 1 stops it contributing more height than its font-size, so the row's
   height goes back to being set by the text, exactly as it is on every other .btn. */
.btn .material-symbols-outlined {
	font-size: var(--icon-md);
	line-height: 1;
}

.btn-primary {
	border-color: var(--color-ink);
	background: var(--color-ink);
	color: var(--color-text-inverse);
}

.btn-primary:hover {
	border-color: #2B2D42;
	background: #2B2D42;
	text-decoration: none;
}

.btn-primary:focus-visible {
	outline: 2px solid var(--color-info);
	outline-offset: 2px;
}

.btn-block {
	width: 100%;
}

/* ====================================================================
   FORMS
   ==================================================================== */
.form-group {
	margin-bottom: var(--spacing-3xl);
}

.form-group label {
	display: block;
	margin-bottom: var(--spacing-md);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	font-weight: 500;
	/* `normal`, as the demo's - the body's 1.5 made every label 19.5px tall against the demo's 16
	   and opened the gap between a label and its field by 3.5px on every form in the app. */
	line-height: 1.3;
}

/* The height is stated rather than left to each control's own intrinsic one: an input worked out
   at 42px and a select at 44px from identical padding and font size, which is 2px of drift in
   every filter bar that has both - and, because .filter-grid aligns its columns on their bottom
   edge, 2px of drift in the labels above them too. The demo has one field height (42px) for both,
   so this says so in both places rather than relying on two different intrinsic line boxes
   agreeing. */
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group input[type="date"],
.form-group input[type="number"] {
	width: 100%;
	height: 42px;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-base);
	transition: border-color var(--transition-fast);
}

.form-group input:hover {
	border-color: var(--color-border-dark);
}

.form-group input:focus {
	outline: none;
	border-color: var(--color-info);
	box-shadow: 0 0 0 2px rgba(0, 88, 255, 0.1);
}

/* Secondary rather than muted: #999 on white is 2.85:1, which fails AA outright, and this line
   carries a rule the visitor has to be able to read (the minimum password length). */
.form-hint {
	margin: var(--spacing-sm) 0 0 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
}

.form-errors {
	margin: 0 0 var(--spacing-3xl) 0;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-danger);
	border-radius: var(--radius-md);
	background: var(--color-danger-bg);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
}

.form-errors ul {
	margin: 0;
	padding-left: var(--spacing-2xl);
}

.form-errors li + li {
	margin-top: var(--spacing-xs);
}

/* ====================================================================
   FLASH MESSAGES
   ==================================================================== */
.flash {
	margin: 0 0 var(--spacing-3xl) 0;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid transparent;
	border-radius: var(--radius-md);
	font-size: var(--font-size-base);
}

.flash-success {
	border-color: var(--color-success);
	background: var(--color-success-bg);
	color: var(--color-text-primary);
}

.flash-error {
	border-color: var(--color-danger);
	background: var(--color-danger-bg);
	color: var(--color-text-primary);
}

/* Every flash is .main-content's own direct child, rendered before $content by layout.php /
   admin-layout.php, and .main-content's own padding already lines it up with whatever it precedes
   - EXCEPT the New/Edit report page, whose $content narrows itself to .report-form-page's own
   800px centred column (below), leaving the flash a visibly wider, left-shifted banner above the
   h1/card it introduces. :has() picks out exactly that one case - a flash immediately before a
   .report-form-page - so it matches that column instead; every other page's flash, already flush
   with its own h1 at .main-content's edge, is untouched. */
.flash:has(+ .report-form-page) {
	max-width: 800px;
	margin-right: auto;
	margin-left: auto;
}

/* The "Viewing as admin: <company>" strip (ledger item 3, phase 10) on report-form.php and
   report-detail.php when AdminPages renders them for a company's report rather than
   ReportPages rendering them for the logged-in customer. Same tint --color-info already gets at
   10% opacity elsewhere on this page (the search input's own focus ring), rather than a new
   --color-info-bg variable for one banner. */
.admin-viewing-strip {
	margin: 0 0 var(--spacing-3xl) 0;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-info);
	border-radius: var(--radius-md);
	background: rgba(0, 88, 255, 0.06);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
}

.admin-viewing-strip a {
	color: var(--color-info);
	font-weight: 500;
}

.admin-viewing-strip a:hover {
	color: var(--color-info-hover);
	text-decoration: underline;
}

/* ====================================================================
   SIGNED-OUT PAGES
   ==================================================================== */
.auth-page {
	min-height: 100vh;
}

.auth-shell {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	min-height: 100vh;
	padding: var(--spacing-5xl) var(--spacing-3xl);
}

.auth-brand {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
	margin-bottom: var(--spacing-4xl);
}

.auth-brand-area {
	padding-left: var(--spacing-lg);
	border-left: 1px solid var(--color-border-dark);
	color: var(--color-text-secondary);
	font-size: var(--font-size-lg);
	font-weight: 500;
	line-height: 1.4;
}

/* No border: the demo's .settings-card is a shadowed box with none (see .card above), and
   .auth-card is its own rule rather than sharing .card, so it needs the same fix said fixed there.
   Phase 12, ledger item 16. */
.auth-card {
	width: 100%;
	max-width: 460px;
	padding: var(--spacing-5xl);
	border-radius: var(--radius-lg);
	background: var(--color-bg-card);
	box-shadow: var(--shadow-base);
}

.auth-title {
	margin: 0 0 var(--spacing-sm) 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-3xl);
	font-weight: 600;
	text-align: center;
}

.auth-sub {
	margin: 0 0 var(--spacing-4xl) 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
	line-height: 1.5;
	text-align: center;
}

.auth-form {
	margin: 0;
}

.auth-footer {
	margin: var(--spacing-3xl) 0 0 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	text-align: center;
}

.auth-footer a {
	color: var(--color-text-secondary);
	text-decoration: underline;
}

.auth-footer a:hover {
	color: var(--color-text-primary);
}

/* ====================================================================
   PAGE HEADER SUBTITLE, STACKED CARDS
   ==================================================================== */
.page-subtitle {
	/* The demo's own .page-description margin-bottom, matched here as margin-top since ours sits
	   under the h1 rather than the h1 sitting above it - see the .page-header comment above.
	   Phase 12, ledger item 16. */
	margin: 15px 0 0 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
}

/* The line under a report or run title is the demo's .co-title-meta, which is a size smaller than
   a page description and sits tight under the heading (consultants-styles.css). */
.page-byline {
	margin: var(--spacing-xs) 0 0 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	line-height: 1.3;
}

.card + .card {
	margin-top: var(--spacing-3xl);
}

/* The sibling margin above is for cards stacked down a page. Inside a grid or flex
   row the container's gap spaces them, and the margin would push every card but
   the first down a row's worth - which is exactly what the credits page and the
   admin dashboard did (first card visibly higher than its neighbours). */
:is(.admin-stat-grid, .admin-action-grid) > .card + .card {
	margin-top: 0;
}

/* The other half of that rule, and the one the admin side was missing (phase 12, ledger item 12).
   .card + .card only ever spaces two CARDS: a grid of cards following a card is a <div>, not a
   .card, so it matched nothing and the admin company detail page's Adjust credits / Free re-run
   pair sat flush against the bottom edge of the Ledger card above it. The customer side never
   showed it because .credits-summary is the first thing on its page. Written out per grid rather
   than as a bare `.card + div` so it says which layouts it is for. */
.card + .admin-stat-grid,
.card + .admin-action-grid,
.table-card + .admin-action-grid,
.card + .form-errors,
.table-card + .form-errors {
	margin-top: var(--spacing-3xl);
}

.card-header-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-xl);
	margin-bottom: var(--spacing-3xl);
	padding-bottom: var(--spacing-lg);
	border-bottom: 1px solid var(--color-border);
}

.card-title {
	margin: 0 0 var(--spacing-3xl) 0;
	padding-bottom: var(--spacing-lg);
	border-bottom: 1px solid var(--color-border);
	color: var(--color-text-primary);
	font-size: var(--font-size-2xl);
	font-weight: 600;
}

/* The row already carries the divider under both the title and the Invite button together, so
   the title's own copy of it would draw the rule twice. */
.card-header-row .card-title {
	margin-bottom: 0;
	padding-bottom: 0;
	border-bottom: none;
}

/* The demo's card heading is a band, not an inset line: .settings-card is `overflow: hidden` with
   a .card-header of its own 24px padding and a bottom border, so the rule reaches both edges of
   the card (dashboard-styles.css:6593-6607). Ours drew it inside the card's padding, stopping
   24px short at each end. Negative margins give the same band without restructuring every card
   template; the card's `overflow-x: auto` clips them to the radius and adds no scrollbar (the
   left overflow is exactly the padding it cancels). Phase 12, ledger item 16. */
.card > .card-title:first-child,
.card > .card-header-row:first-child {
	margin: calc(-1 * var(--spacing-3xl)) calc(-1 * var(--spacing-3xl)) var(--spacing-3xl) calc(-1 * var(--spacing-3xl));
	padding: var(--spacing-3xl);
}

.form-static-label {
	display: block;
	margin-bottom: var(--spacing-md);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	font-weight: 500;
}

.form-static-value {
	margin: 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

/* 42px, the same as .form-group's inputs above and for the reason given there.
   The chevron is the demo's own .co-select (consultants-styles.css:1354-1369): appearance is
   turned off and the same inline SVG sits 12px from the right edge, with 36px of padding kept
   clear for it. Ours used the browser's native arrow, which is a different shape and sits in a
   different place - visible on every filter bar and on the New report form's Stay length.
   Phase 12, ledger item 16. */
.form-group select {
	width: 100%;
	height: 42px;
	padding: var(--spacing-lg) 36px var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background-color: var(--color-bg-card);
	background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23666666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
	background-repeat: no-repeat;
	background-position: right var(--spacing-lg) center;
	appearance: none;
	-webkit-appearance: none;
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-base);
	cursor: pointer;
	transition: border-color var(--transition-fast);
}

.form-group select:hover {
	border-color: var(--color-border-dark);
}

.form-group select:focus {
	outline: none;
	border-color: var(--color-info);
	box-shadow: 0 0 0 2px rgba(0, 88, 255, 0.1);
}

.btn-secondary {
	border-color: var(--color-border);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
}

.btn-secondary:hover {
	border-color: var(--color-border-dark);
	background: var(--color-bg-app);
	text-decoration: none;
}

/* ====================================================================
   TABS (demo's underline style: plain text, active tab in the brand red
   with a red underline sitting on the shared border - see 09-company.jpg)
   ==================================================================== */
.tabs {
	display: flex;
	gap: var(--spacing-3xl);
	margin-bottom: var(--spacing-3xl);
	border-bottom: 1px solid var(--color-border);
}

.tab {
	margin-bottom: -1px;
	padding: var(--spacing-lg) 0;
	border: none;
	border-bottom: 2px solid transparent;
	background: transparent;
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
	font-weight: 500;
	line-height: 1;
}

.tab:hover {
	color: var(--color-text-primary);
	text-decoration: none;
	border-bottom-color: var(--color-border-dark);
}

/* The underline carries the brand red; the text stays dark rather than red-on-white, which is
   only ~3.9:1 - below AA's 4.5:1 for text. Decoration and text follow different rules here. */
.tab-active,
.tab-active:hover {
	border-bottom-color: var(--color-primary);
	color: var(--color-text-primary);
	font-weight: 600;
}

/* ====================================================================
   TABLES
   ==================================================================== */
.table {
	width: 100%;
	border-collapse: collapse;
}

/* Cell metrics are the demo's .data-table exactly (dashboard-styles.css:3719 and :3757): a header
   row of 18px/20px and body rows of 10px/20px, so a row is 34px of text in a 50px header. Ours
   used 12px/16px for both, which made the header shallower and every row taller than the demo's.
   Phase 12, ledger item 16. */
.table th,
.table td {
	text-align: left;
}

.table th {
	padding: 18px var(--spacing-2xl);
	border-bottom: 1px solid var(--color-border);
	/* The demo leaves headings and cells at `normal` (~1.32 for Lexend Deca); ours inherited the
	   body's 1.5, which made every row about 3.5px taller than the demo's and a long list visibly
	   looser than the same list in the demo. */
	line-height: 1.3;
	background: var(--color-bg-section);
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
	/* 0.5px, as .data-table th - 0.02em computes to 0.22px at 11px and read as no tracking. */
	letter-spacing: 0.5px;
	white-space: nowrap;
}

.table td {
	padding: 10px var(--spacing-2xl);
	font-size: var(--font-size-md);
	line-height: 1.3;
}

/* 0.5px, one rule for the eyebrow/micro-label role instead of three copies of it: the demo's own
   .co-stat-label is 0.5px, the same figure .table th above already carries. .report-meta-label,
   .card-eyebrow and .admin-stat-label each declared the tighter 0.02em (0.22px at 11px) on their
   own rule; that declaration is removed from each and given here once. */
.report-meta-label,
.card-eyebrow,
.admin-stat-label {
	letter-spacing: 0.5px;
}

/* The demo's tables are a light grid, and the two rules are not the same weight: column rules are
   --color-border (#E5E5E5, `border-right` on every cell but the last), row rules the lighter
   --color-border-light (#F0F0F0, on the row) - ours had the two colours the other way round, so
   the grid read as rows-first where the demo reads as columns-first. */
.table th + th,
.table td + td {
	border-left: 1px solid var(--color-border);
}

.table tbody tr {
	border-bottom: 1px solid var(--color-border-light);
}

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

/* The demo runs two table styles: its reports table is .data-table, which .table above already
   matches; its Users table (both the customer Company > Users tab and the admin company detail
   page - the same class on both) is the roomier one, 12px/16px padding against a 57px row. A
   modifier on `.table` rather than changing the cell metrics generally, which would move every
   other table (reports, credits, admin logs) off the figures they were just matched to. */
.table-users th,
.table-users td {
	padding: var(--spacing-lg) var(--spacing-xl);
}

/* The reports list's own column widths, as .co-table-reports (consultants-styles.css:1798-1830).
   Without them the table is laid out from its content, so the same list looks different from one
   company to the next - here the report-name column came out 237px against the demo's 313px
   because a wider Status cell took the room. The two count columns keep tabular figures for the
   same reason the demo gives them. Phase 12, ledger item 16. */
.reports-table th:nth-child(1), .reports-table td:nth-child(1) { width: 23%; }
.reports-table th:nth-child(2), .reports-table td:nth-child(2) { width: 8%; }
.reports-table th:nth-child(3), .reports-table td:nth-child(3) { width: 9%; font-variant-numeric: tabular-nums; }
.reports-table th:nth-child(4), .reports-table td:nth-child(4) { width: 9%; }
.reports-table th:nth-child(5), .reports-table td:nth-child(5) { width: 12%; }
.reports-table th:nth-child(6), .reports-table td:nth-child(6) { width: 12%; font-variant-numeric: tabular-nums; }
.reports-table th:nth-child(7), .reports-table td:nth-child(7) { width: 11%; }
.reports-table th:nth-child(8), .reports-table td:nth-child(8) { width: 10%; }
.reports-table th:nth-child(9), .reports-table td:nth-child(9) { width: 6%; }

/* "Capturing 1,234 of 2,190" under an In progress chip (phase 14, ledger item Q2), and the note
   that appears when a report changed elsewhere could not be shown in place. Both are the same
   quiet secondary line .export-progress already is on the report page - the same size, the same
   colour - so a customer meets one voice for "this is still happening", not two. */
.reports-table-progress {
	display: block;
	margin-top: var(--spacing-xs);
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
	font-variant-numeric: tabular-nums;
}

.table-refresh-note {
	margin: 0 0 var(--spacing-lg);
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
}

/* Money columns: the demo right-aligns the credits and balance columns of the credit activity
   table, header and values together, and leaves every other column left. */
.table th.table-number,
.table td.table-number {
	text-align: right;
}

.table-actions-heading {
	text-align: right;
}

/* `.table td.table-actions` so it outranks `.table td` above, which otherwise wins the text-align
   and leaves the action links sitting on the left of a right-hand column. */
.table td.table-actions,
.table-actions {
	display: table-cell;
	text-align: right;
	white-space: nowrap;
}

/* --color-text-muted (#999) is 2.85:1 on white - below AA. Secondary is the same grey family at
   5.7:1, same reasoning as form-hint above. */
.table-actions-you {
	color: var(--color-text-secondary);
}

.table-name-cell {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
}

.table-avatar {
	display: flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	width: 32px;
	height: 32px;
	border-radius: var(--radius-round);
	background: var(--color-avatar);
	color: var(--color-text-inverse);
	font-size: var(--font-size-sm);
	font-weight: 600;
}

.action-link {
	display: inline-block;
	margin-left: var(--spacing-xl);
	border: none;
	background: transparent;
	padding: 0;
	color: var(--color-info);
	font-family: var(--font-family);
	font-size: var(--font-size-base);
	font-weight: 500;
	cursor: pointer;
}

.action-link:first-child {
	margin-left: 0;
}

.action-link:hover {
	color: var(--color-info-hover);
	text-decoration: underline;
}

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

/* An .action-link that is a plain reference inside an ordinary body cell - the credit ledger's
   report name, an admin company/report/run link - is not an action, and the demo's own cell text
   for these is unstyled dark text with no underline until hover: no blue, no button look. Scoped
   to anchors so it never touches the .action-link-button elements below, and to `:not(.table-
   actions)` so the ghost-button treatment further down keeps the actions column separate.
   Phase 12, ledger item 16. */
.table td:not(.table-actions) a.action-link {
	color: var(--color-text-primary);
	text-decoration: none;
}

.table td:not(.table-actions) a.action-link:hover,
.table td:not(.table-actions) a.action-link:focus {
	text-decoration: underline;
}

/* Inside a table's actions column the demo does not use a blue link at all: .co-action-btn is a
   quiet grey ghost button - 13px/500 #666666, 6px/8px padding, 6px radius, and the row's
   background on hover (consultants-styles.css:272-293). Ours drew #0058FF links there, which put
   the loudest colour on the page in the least important column of every list. Phase 12, ledger
   item 16. */
.table-actions .action-link {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs);
	margin-left: var(--spacing-xs);
	padding: var(--spacing-sm) var(--spacing-md);
	border-radius: var(--radius-md);
	color: var(--color-text-secondary);
	transition: background var(--transition-fast), color var(--transition-fast);
}

.table-actions .action-link:hover {
	background: var(--color-bg-section);
	color: var(--color-text-primary);
	text-decoration: none;
}

/* The negative margin puts the last action's text-not-padding on the cell's right edge, so a
   column of them lines up with the header above and with the cells that hold plain text. */
.table-actions .action-link:last-child {
	margin-right: calc(-1 * var(--spacing-md));
}

/* The one destructive action in a table's actions column: Delete on /admin/admins (phase 15,
   ledger item G3). #C4102A, not --color-danger, for the reason .admin-badge-stale gives - 6.08:1
   on the card's white, where the token itself is 4.33:1 and misses AA. */
.table-actions .action-link-danger {
	color: #C4102A;
}

.table-actions .action-link-danger:hover,
.table-actions .action-link-danger:focus {
	background: var(--color-danger-bg);
	color: #C4102A;
}

/* ====================================================================
   STATUS CHIPS
   ==================================================================== */
.chip {
	display: inline-flex;
	align-items: center;
	padding: var(--spacing-xs) var(--spacing-lg);
	border-radius: var(--radius-pill);
	font-size: var(--font-size-sm);
	font-weight: 500;
	white-space: nowrap;
}

/* Chip text is darkened past the demo's own token for each colour - #5FBF8B and #F57C00 on
   their pastel backgrounds are 2.0:1 and 2.5:1, both well under AA's 4.5:1. The backgrounds
   stay exactly as the demo has them; only the text darkens, which is what a chip actually needs
   to meet AA for the word inside it. */
.chip-active {
	background: var(--color-success-bg);
	color: #1B5E20;
}

.chip-invited {
	background: #FFF8E1;
	color: #8A5300;
}

.chip-disabled {
	background: var(--color-bg-muted);
	color: var(--color-text-secondary);
}

/* The demo's role badges are 11px/600 rather than the 12px/500 a status badge carries
   (.role-badge, dashboard-styles.css:6859), and it gives every role its own colour. Ours painted
   Owner and Admin the same red, so the two most consequential roles were indistinguishable in the
   Users table. Phase 12, ledger item 16. */
.chip-owner,
.chip-admin,
.chip-member {
	font-size: var(--font-size-xs);
	font-weight: 600;
}

/* .role-badge.role-owner exactly: #7B1FA2 on #F3E5F5 is 6.77:1, comfortably AA. */
.chip-owner {
	background: #F3E5F5;
	color: #7B1FA2;
}

/* The demo's .role-admin pair (#D32F2F on #FFEBEE) is 4.36:1, under AA at this size, so the red
   stays the one already proven here: #C41E3A on --color-primary-light is 5.39:1. Same hue as the
   demo's, one step darker - the house rule that AA beats demo fidelity for anything carrying
   text. --color-primary itself is only 3.9:1 on the same background. */
.chip-admin {
	background: var(--color-primary-light);
	color: #C41E3A;
}

.chip-member {
	background: var(--color-bg-muted);
	color: var(--color-text-secondary);
}

/* ====================================================================
   NARROW SCREENS
   ==================================================================== */
@media (max-width: 720px) {
	.app-bar-inner {
		gap: var(--spacing-lg);
		padding: var(--spacing-md) var(--spacing-xl);
	}

	.app-bar-left {
		gap: var(--spacing-lg);
	}

	.company-name {
		padding-left: var(--spacing-lg);
		font-size: var(--font-size-md);
	}

	/* The credits pill is NOT shrunk here. It was (6px 12px, a 32px pill), and it is the one
	   thing on a phone header that has to stay tappable and legible - the demo keeps it at 36px
	   with its 8px/16px padding at every width, and the bar has room for it once the company
	   name truncates. */

	.main-content {
		padding: var(--spacing-2xl) var(--spacing-xl) var(--spacing-4xl) var(--spacing-xl);
	}

	.page-header h1 {
		font-size: var(--font-size-4xl);
	}

	/* A long report/run name wraps to 2-3 lines at this width; align-items: center then floats the
	   action button somewhere in the middle of that block instead of level with anything. Stacking
	   is the same treatment .report-detail-header already gives its own, more specific header. */
	.page-header {
		flex-direction: column;
		align-items: stretch;
	}

	/* The demo's own stacking at this width: h1, then the full-width primary button, then the
	   description - see the .page-header-list comment above for why this is a CSS reorder rather
	   than a template change. */
	.page-header-list {
		grid-template-columns: 1fr;
		grid-template-areas:
			"title"
			"button"
			"desc";
	}

	.page-header-list .btn {
		justify-self: stretch;
		/* The demo's 12px between the h1 and the stacked button (the description's own 15px
		   margin-top provides the gap below it). */
		margin-top: 12px;
	}

	.auth-card {
		padding: var(--spacing-4xl) var(--spacing-3xl);
	}

	.table {
		min-width: 560px;
	}
}

/* ====================================================================
   REPORTS LIST (ledger item 3)
   ==================================================================== */
/* The demo's own section heading is a plain h2: `font-size: 20px; margin: 24px 0 16px` with the
   note beside it (dashboard-styles.css:172-177, consultants-styles.css:237). Ours was an 18px
   heading with 12px under it and nothing above, so "Your reports" and "Credit activity" sat
   tighter to the block above them and read a size smaller than the demo's. */
.section-heading-row {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--spacing-xl);
	margin: var(--spacing-3xl) 0 var(--spacing-xl) 0;
}

.section-heading-row .card-title {
	font-size: var(--font-size-3xl);
	line-height: 1.25;
}

/* A section heading that follows a card (admin run detail: "By property" card, then
   "Failed requests") needs its own breathing room, or the heading sits on the card. */
.card + .section-heading-row,
.table-card + .section-heading-row {
	margin-top: var(--spacing-5xl);
}

/* .card-title's own divider (drawn for it sitting alone atop a card, e.g. company-about.php)
   would otherwise show as a stray underline the width of the text - .section-heading-row is a
   flex row outside any card, which shrinks the heading to its content width. */
.section-heading-row .card-title {
	margin-bottom: 0;
	padding-bottom: 0;
	border-bottom: none;
}

/* The right-hand end of a .section-heading-row when it carries more than one thing - the admin run
   detail page's "N requests." note beside its Retry button (phase 12, ledger item 0). Without it
   the row's own space-between would strand the note in the middle of the heading. */
.section-heading-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-xl);
}

.section-heading-note {
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	/* .co-reports-note is `white-space: nowrap`; ours wrapped "Your 4 reports, most recent first."
	   onto a second line as soon as the heading beside it grew. */
	white-space: nowrap;
}

/* .co-search-card: 20px/24px of padding and 16px under it, not the 24px/24px a plain card has. */
.filter-card {
	padding: var(--spacing-2xl) var(--spacing-3xl);
	margin-bottom: var(--spacing-xl);
}

/* .card + .card's own 24px would be added to the 16px above, opening a 40px gap between a filter
   bar and the table it filters where the demo has 16px. */
.filter-card + .card {
	margin-top: 0;
}

.filter-grid {
	display: grid;
	grid-template-columns: repeat(6, 1fr) auto;
	gap: var(--spacing-xl);
	align-items: end;
}

/* The reports bar's own column weights, as .co-search-grid-reports (consultants-styles.css:1781):
   the name search is the widest field and Currency the narrowest. Six equal tracks truncated the
   search placeholder ("Search by report nam…") while leaving the Currency select half empty. */
.reports-filter-grid {
	grid-template-columns: 1.6fr 0.6fr 1.1fr 1.1fr 0.9fr 1.1fr auto;
}

/* The admin log's own column weights (phase 12b, visual-verifier fix to ledger item L1) - the same
   kind of per-bar override .reports-filter-grid above is, for the two things this bar has that no
   other one does:

     - its Admin select's options are whole email addresses, and a native <select> clips with no
       ellipsis at all, so an equal 1fr track showed "visual-verifier-tmp@" and simply stopped -
       the bar no longer said which admin it was filtered to. 2fr fits the longest address from
       about 1200px up, and the select carries the selected address as its title for the widths
       below that, since there is no width at which a 255px address fits a 375px phone.
     - it is the only bar with TWO date inputs. A native date input's min-content is 156px, and
       `1fr` is `minmax(auto, 1fr)`, so between about 901 and 1350px those two tracks outgrew their
       fr share and squeezed the three fields beside them to 93px each. minmax() states that 156px
       floor explicitly instead of letting it fight the other tracks for the space.

   minmax(0, ...) on the first three is what stops a long option or placeholder pushing its own
   track past its share. Six tracks, not .filter-grid's seven: five fields and the buttons, with no
   empty track to leave. Declared here, above the narrow-screen block, so that block's own
   .filter-grid rules still collapse this bar to two columns at 900 and one at 720 - the ordering
   trap .credits-filter-grid had to re-declare its way out of, avoided rather than repeated. */
.admin-log-filter-grid {
	grid-template-columns: minmax(0, 2fr) minmax(0, 1.4fr) minmax(0, 1fr) minmax(156px, 1fr) minmax(156px, 1fr) auto;
}

.filter-grid .form-group {
	margin-bottom: 0;
}

/* The demo's own filter-bar buttons (.co-search-buttons): 12px between Clear and Search, and both
   44px tall against the 42px fields beside them - the pair is one shape on every bar, which is
   what /credits already looked like and /rate-capture, /admin/companies and /admin/events did not.
   The grid-column is for /admin/events above all: its two-field form leaves four of .filter-grid's
   six 1fr tracks empty, so without being placed in the last track the buttons sat where the third
   field would be, with most of the card empty to their right. Naming the last track changes
   nothing for the bars whose fields already fill every other one. Below 900px the grid has no such
   track and the buttons take a row of their own - see the narrow-screens block further down. */
.filter-search-button {
	display: flex;
	gap: var(--spacing-lg);
}

.filter-grid .filter-search-button {
	grid-column: -2 / -1;
}

.filter-search-button .btn {
	width: 100%;
	height: 44px;
}

.empty-state {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--spacing-xl);
	padding: var(--spacing-3xl) 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

.empty-state p {
	margin: 0;
}

/* Report status chips - separate from the Users tab's chip-active/chip-invited/chip-disabled
   even where the colour matches, since the two chip families describe different things. */
/* .co-badge-draft's own background is --color-bg-section, a shade lighter than the muted grey
   this used, which is what makes the demo's "Not run" read as a label rather than a button. */
.chip-not-run {
	background: var(--color-bg-section);
	color: var(--color-text-secondary);
}

.chip-in-progress {
	background: var(--color-bg-section);
	color: var(--color-text-primary);
}

/* --color-success on --color-success-bg is only ~2:1, well below WCAG AA - fine for a decorative
   dot but not for the text this chip carries. #1E7A44 on the same background is ~4.76:1. A new
   token rather than changing --color-success itself, which other chips still use decoratively. */
.chip-completed {
	background: var(--color-success-bg);
	color: #1E7A44;
}

.chip-dot {
	display: inline-block;
	width: 6px;
	height: 6px;
	margin-right: var(--spacing-xs);
	border-radius: var(--radius-round);
	background: var(--color-ink);
}

/* ====================================================================
   NEW / EDIT REPORT FORM (ledger item 3)
   ==================================================================== */
.report-form-page {
	max-width: 800px;
	margin: 0 auto;
}

.form-divider {
	margin: 0 0 var(--spacing-3xl) 0;
	border: none;
	border-top: 1px solid var(--color-border);
}

/* The demo's own .co-suggest-head is 600, not the 500 every other .form-group label carries -
   this one line introduces the URL field rather than labelling it, so it is scoped here rather
   than changing .form-group label for every form in the app. `.form-group label` outranks a bare
   class on specificity, so this has to be qualified the same way to win. */
.form-group .property-url-label {
	font-weight: 600;
}

.property-add-row {
	display: flex;
	gap: var(--spacing-lg);
}

.property-add-row input[type="text"] {
	flex: 1 1 auto;
}

/* 11px, not the app's usual 12px .form-hint: the demo's own hint under this field is a size
   smaller. Scoped to this one hint - the password-length hint elsewhere in .form-hint was not
   one of the ten findings. */
.property-url-hint {
	font-size: var(--font-size-xs);
}

/* #C4102A, not --color-danger: this line is 12px text on the white card, and #F0142F is 4.33:1
   there - below the 4.5:1 AA floor. The demo's own field error is #F0142F, and the project's AA
   rule wins over matching it, exactly as .credits-amount-negative and .admin-badge-stale already
   decide the same question the same way. */
.form-error-inline {
	margin: var(--spacing-sm) 0 0 0;
	color: #C4102A;
	font-size: var(--font-size-sm);
}

.form-error-inline[hidden] {
	display: none;
}

.property-list {
	margin-bottom: var(--spacing-xl);
}

.property-row {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
	padding: var(--spacing-lg) 0;
	border-bottom: 1px solid var(--color-border);
}

.property-name-input {
	flex: 0 0 200px;
	padding: var(--spacing-sm) var(--spacing-md);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-sm);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-sm);
}

.property-name-input:focus {
	outline: none;
	border-color: var(--color-info);
	box-shadow: 0 0 0 2px rgba(0, 88, 255, 0.1);
}

.property-url {
	flex: 1 1 auto;
	overflow: hidden;
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
	white-space: nowrap;
	text-overflow: ellipsis;
}

.property-remove {
	flex: 0 0 auto;
	margin-left: 0;
	/* #C4102A, not --color-danger: 5.5:1 on white, so the text passes AA */
	color: #C4102A;
}

.property-remove:hover {
	color: #9E0D22;
}

/* Cancel sits on the left, the count note and the buttons together on the
   right - the demo's own .co-wizard-actions order (phase 12, item 6). */
.form-footer {
	display: flex;
	align-items: center;
	gap: var(--spacing-xl);
	padding-top: var(--spacing-xl);
	border-top: 1px solid var(--color-border);
}

.form-footer-note {
	margin-left: auto;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

.quiet-link {
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	text-decoration: underline;
}

.quiet-link:hover {
	color: var(--color-text-primary);
}

/* Clear on a filter bar is the demo's .co-quiet-link - grey, underlined, no border and no box
   (consultants-styles.css:1262). It has to be a submit here rather than the demo's <span>, since
   clearing means posting the empty search back to the session, so the button element is stripped
   back to the same appearance. Phase 12, ledger item 16. */
button.quiet-link {
	border: none;
	background: transparent;
	padding: 0;
	font-family: var(--font-family);
	font-weight: 400;
	cursor: pointer;
}

.form-footer-buttons {
	display: flex;
	gap: var(--spacing-lg);
}

/* The demo fades a disabled button's own fill rather than repainting it grey - .co-btn-primary
   disabled stays --color-ink at opacity .4, .co-btn-secondary disabled stays white at .5. Ours
   repainted every disabled button to the same #F0F0F0/#999, which read as a different button
   (and its border-color/background/color rule was at odds with .pagination-link-disabled's own
   "#666 not the muted #999" AA reasoning further down). */
.btn-primary:disabled,
.btn-primary:disabled:hover {
	opacity: 0.4;
	cursor: not-allowed;
}

.btn-secondary:disabled,
.btn-secondary:disabled:hover {
	opacity: 0.5;
	cursor: not-allowed;
}

/* ====================================================================
   CURRENCY COMBOBOX (ledger item 3 - searchable currency picker)
   ==================================================================== */
.combobox {
	position: relative;
}

.combobox-input {
	width: 100%;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-base);
}

.combobox-input:focus {
	outline: none;
	border-color: var(--color-info);
	box-shadow: 0 0 0 2px rgba(0, 88, 255, 0.1);
}

.combobox-list {
	position: absolute;
	top: calc(100% + var(--spacing-xs));
	left: 0;
	right: 0;
	z-index: 1000;
	max-height: 260px;
	margin: 0;
	padding: var(--spacing-sm) 0;
	overflow-y: auto;
	list-style: none;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-card);
	box-shadow: var(--shadow-lg);
}

.combobox-list[hidden] {
	display: none;
}

.combobox-option {
	padding: var(--spacing-lg) var(--spacing-xl);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	cursor: pointer;
}

.combobox-option:hover,
.combobox-option-active {
	background: var(--color-bg-app);
}

/* ====================================================================
   REPORT DETAIL (ledger item 4)
   ==================================================================== */
.report-detail-header {
	align-items: flex-start;
}

.report-title-row {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
}

.page-header-actions {
	display: flex;
	align-items: center;
	gap: var(--spacing-lg);
	flex: 0 0 auto;
}

.report-detail-section {
	margin-top: var(--spacing-4xl);
}

/* Pulses the same dark dot the reports list already draws on an In progress chip - a still dot
   reads as decoration, a pulsing one reads as "this is happening right now". */
.chip-in-progress .chip-dot {
	animation: chip-pulse 1.4s ease-in-out infinite;
}

@keyframes chip-pulse {
	0%, 100% { opacity: 1; }
	50% { opacity: 0.35; }
}

.report-meta {
	display: grid;
	grid-template-columns: minmax(160px, auto) repeat(3, 1fr);
	column-gap: var(--spacing-6xl);
	row-gap: var(--spacing-2xl);
}

.report-meta-captured {
	grid-row: span 2;
	padding-right: var(--spacing-6xl);
	border-right: 1px solid var(--color-border);
}

.report-meta-label {
	display: block;
	margin-bottom: var(--spacing-sm);
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
}

.report-meta-value {
	margin: 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-md);
	font-weight: 400;
}

/* Only CAPTURED is large and bold in the demo; the other six values are regular weight. */
.report-meta-captured .report-meta-value {
	font-size: var(--font-size-3xl);
	font-weight: 600;
}

.report-meta-sub {
	margin: var(--spacing-xs) 0 0 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
}

.report-meta-sub-capturing {
	display: flex;
	align-items: center;
	gap: var(--spacing-sm);
}

/* A small rotating ring - the demo's "capturing" indicator, next to the report's status dot and
   above the Export card while a run is In progress. */
.spinner {
	display: inline-block;
	flex: 0 0 auto;
	width: 14px;
	height: 14px;
	border: 2px solid var(--color-border);
	border-bottom-color: var(--color-primary);
	border-radius: var(--radius-round);
	animation: spinner-rotate 0.8s linear infinite;
}

@keyframes spinner-rotate {
	to { transform: rotate(360deg); }
}

/* "N pages could not be captured", above the export panel of a completed run that has failed
   pages, with its free Retry button (phase 12, ledger item 0). The same tinted-strip shape as
   .admin-viewing-strip, in the danger palette; the text is --color-text-primary on
   --color-danger-bg (14.9:1) rather than the red itself, so it passes AA at body size the way
   .flash-error already does. */
.report-failed-notice {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-lg);
	margin: 0 0 var(--spacing-3xl) 0;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-danger);
	border-radius: var(--radius-md);
	background: var(--color-danger-bg);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
}

.report-failed-notice p {
	margin: 0;
}

/* "N past dates skipped" (ledger item P3, phase 13): not a failure - the check-in date had
   already passed when the worker got to it, so booking.com was never asked - so this uses the
   neutral --color-info tint .admin-viewing-strip already gets, not the danger palette
   .report-failed-notice does. */
.report-past-notice {
	margin: 0 0 var(--spacing-3xl) 0;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-info);
	border-radius: var(--radius-md);
	background: rgba(0, 88, 255, 0.06);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
}

.report-past-notice p {
	margin: 0;
}

.export-status-card {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--spacing-lg);
	padding: var(--spacing-5xl) var(--spacing-4xl);
	text-align: center;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

.export-status-card p {
	margin: 0;
}

.export-progress {
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
}

.property-link {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs);
	color: var(--color-text-secondary);
}

.property-link .material-symbols-outlined {
	font-size: var(--icon-md);
}

/* ====================================================================
   MODAL (phase 12, ledger item 15 - the demo's .co-modal, as a native
   <dialog> so the focus trap, Escape and the return of focus are the
   browser's own. The dialog itself is the overlay: transparent, padded,
   and closing on a click that lands on it rather than on the card, so
   .modal-card carries every visible pixel.)
   ==================================================================== */
.modal {
	width: 100%;
	/* 480 = the demo's 440px card plus this overlay's own 20px padding on each side. Capping the
	   overlay at 440 instead left a 400px card - the padding came out of the card. */
	max-width: 480px;
	margin: auto;
	padding: var(--spacing-2xl);
	border: none;
	background: transparent;
}

.modal::backdrop {
	background: rgba(19, 21, 35, 0.5);
}

/* A dialog that has never been shown is display:none by the browser's own rule; saying it again
   here is what keeps the Invite user modal's form (ledger item 10) out of reach in a browser with
   no <dialog> support at all, where an unknown element would otherwise render inline in the page. */
.modal:not([open]) {
	display: none;
}

.modal-card {
	padding: var(--spacing-3xl);
	border-radius: var(--radius-lg);
	background: var(--color-bg-card);
	box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}

.modal-title {
	margin: 0 0 var(--spacing-lg) 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-2xl);
	font-weight: 600;
}

.modal-body {
	margin: 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
	line-height: 1.5;
}

.modal-summary {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--spacing-xl);
	margin: var(--spacing-2xl) 0;
	padding: var(--spacing-lg) 0;
	border-top: 1px solid var(--color-border);
	color: var(--color-text-primary);
	font-size: var(--font-size-md);
	font-weight: 600;
}

/* Grey, but not lighter: the whole total row is 600 in the demo (.co-summary-row.total), label
   included, so this sets the colour and inherits the weight from .modal-summary above. */
.modal-summary-label {
	color: var(--color-text-secondary);
}

.modal-warning {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-xl);
	margin: var(--spacing-2xl) 0;
	padding: var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-section);
	color: var(--color-text-primary);
	font-size: var(--font-size-md);
	line-height: 1.5;
}

/* The shortfall top-up's two lines under the warning (phase 16, plan section 6): the note Paddle's
   own checkout.completed writes ("Payment received - adding your credits...") and the error a
   checkout that could not be opened leaves. Both sit where the warning's own bottom margin has
   already put the space, so neither carries a top margin of its own. */
.modal-note {
	margin: calc(-1 * var(--spacing-lg)) 0 var(--spacing-2xl) 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
	line-height: 1.5;
}

.modal-note[hidden] {
	display: none;
}

/* .form-error-inline sets the colour, the size and the [hidden] rule; this only closes the gap the
   warning above left, the same way .modal-note does. */
.modal-error {
	margin: calc(-1 * var(--spacing-lg)) 0 var(--spacing-2xl) 0;
}

.modal-warning[hidden] {
	display: none;
}

.modal-actions {
	display: flex;
	justify-content: flex-end;
	gap: var(--spacing-lg);
	margin-top: var(--spacing-3xl);
}

/* .form-errors carries a bottom margin for the form under it but no top one, since everywhere else
   it is the first thing in its card. In the Invite user modal it follows the body sentence and was
   sitting flush against it. */
.modal-body + .form-errors {
	margin-top: var(--spacing-2xl);
}

/* The Invite user modal's body is a form where every other modal's is a sentence (ledger item 10),
   so it needs the gap under .modal-body that a following .modal-summary would otherwise have
   carried. The last field's own bottom margin is dropped: .modal-actions already spaces itself. */
.modal-form {
	margin-top: var(--spacing-2xl);
}

.modal-form .form-group:last-of-type {
	margin-bottom: 0;
}

/* ====================================================================
   NARROW SCREENS (ledger item 3 additions)
   ==================================================================== */
@media (max-width: 900px) {
	.filter-grid {
		grid-template-columns: repeat(2, 1fr);
	}

	/* No auto track to sit in once the bar has collapsed, so the pair takes a row of its own
	   rather than half of one. This is also what .credits-filter-actions' own "span 2" / "span 1"
	   rules below already say for the credits bar, in the two column counts that bar uses. */
	.filter-grid .filter-search-button {
		grid-column: 1 / -1;
	}
}

@media (max-width: 720px) {
	.filter-grid {
		grid-template-columns: 1fr;
	}

	/* The note is nowrap so it never breaks mid-sentence beside its heading, but at 375 there is
	   no room for both on one line - the wrapping row above puts it on its own line instead of
	   squeezing "Your reports" into two. */
	.section-heading-note {
		white-space: normal;
	}

	.report-form-card {
		max-width: none;
	}

	.property-row {
		flex-wrap: wrap;
	}

	.property-name-input {
		flex: 1 1 100%;
	}

	.property-url {
		flex: 1 1 100%;
		white-space: normal;
		word-break: break-all;
	}

	.form-footer {
		flex-direction: column;
		align-items: flex-start;
	}

	.form-footer-note {
		margin-left: 0;
	}

	/* "Save and run (5 credits)" is too wide to share a row with Save report on a narrow
	   screen, and a button label never wraps - so the pair wraps instead, each button then
	   taking the whole width (phase 12, ledger item 6). */
	.form-footer-buttons {
		width: 100%;
		flex-wrap: wrap;
	}

	.form-footer-buttons .btn {
		flex: 1 1 auto;
	}
}

/* ====================================================================
   NARROW SCREENS (ledger item 4 additions)
   ==================================================================== */
@media (max-width: 720px) {
	.report-detail-header {
		flex-direction: column;
		align-items: stretch;
	}

	.page-header-actions {
		justify-content: flex-start;
	}

	/* One fact per row, as the demo's own facts card at this width - not the two-up grid this had
	   fallen back to. */
	.report-meta {
		grid-template-columns: 1fr;
	}

	.report-meta-captured {
		grid-row: auto;
		grid-column: 1 / -1;
		padding-right: 0;
		padding-bottom: var(--spacing-2xl);
		border-right: none;
		border-bottom: 1px solid var(--color-border);
	}
}

/* ====================================================================
   EXPORT THE DATA (ledger item 5; one split card, phase 12 items 2+4)
   ==================================================================== */
/* One card split by a vertical rule, not two cards side by side - the demo's own .co-export
   (consultants-styles.css 1960-2137). The groups column has a fixed sensible width and the
   preview takes whatever is left; minmax(0, 1fr) rather than 1fr is what lets the preview's own
   horizontal scroll work at all, since a bare 1fr track is min-content-wide and a wide table
   would push the card past the page instead of scrolling inside it. */
.export-panel {
	display: grid;
	grid-template-columns: minmax(300px, 380px) minmax(0, 1fr);
	border-radius: var(--radius-lg);
	background: var(--color-bg-card);
	/* The same surface every other card now carries - this one has its own rule rather than .card
	   because it is a grid, and it was left behind with a border and no shadow. */
	box-shadow: var(--shadow-base);
	/* Both halves run to the card's own edge (the preview's shaded header row, the rule between
	   the columns), so the radius has to clip them. */
	overflow: hidden;
}

.export-panel-groups {
	padding: var(--spacing-2xl) var(--spacing-3xl);
	border-right: 1px solid var(--color-border-light);
}

.export-groups-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-xl);
	margin-bottom: var(--spacing-md);
	padding-bottom: var(--spacing-lg);
	border-bottom: 1px solid var(--color-border-light);
}

/* 0.5px, the demo's own .co-stat-label - the eyebrow/micro-label role .report-meta-label,
   .card-eyebrow and .admin-stat-label share the same value with, further up. */
.export-groups-title,
.export-preview-title {
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.5px;
}

.export-select-all {
	display: flex;
	align-items: center;
	gap: var(--spacing-md);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	cursor: pointer;
}

.export-group {
	padding-top: 0;
}

/* The demo separates the groups with 2px and nothing else (.co-export-col + .co-export-col,
   consultants-styles.css:2035) - the hover fill on .export-group-toggle already tells one group
   from the next. Ours drew a 1px rule between every pair, which ruled the left column into six
   boxes the demo does not have. Phase 12, ledger item 16. */
.export-group + .export-group {
	margin-top: 2px;
}

/* The checkbox sits at the top of its group's text block (name, description, column pills)
   rather than centred against all three, and the whole block is the label - so a click anywhere
   in the row toggles the group. */
.export-group-toggle {
	display: flex;
	align-items: flex-start;
	gap: var(--spacing-lg);
	margin: 0 calc(-1 * var(--spacing-lg));
	padding: var(--spacing-lg);
	border-radius: var(--radius-md);
	cursor: pointer;
	transition: background var(--transition-fast);
}

.export-group-toggle:hover {
	background: var(--color-bg-section);
}

.export-select-all input,
.export-group-toggle input {
	width: 15px;
	height: 15px;
	flex-shrink: 0;
	margin: 0;
	accent-color: var(--color-ink);
	cursor: pointer;
}

.export-group-toggle input {
	margin-top: 3px;
}

.export-group-text {
	display: flex;
	flex-direction: column;
	gap: 3px;
	min-width: 0;
}

.export-group-name {
	color: var(--color-text-primary);
	font-size: var(--font-size-md);
	font-weight: 500;
}

.export-group-description {
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
	line-height: 1.4;
}

/* The file columns this group adds, in the file's own names - so they read as column headings
   rather than as UI chips: monospace, and the pill shape the demo gives them. */
.export-column-chip-list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--spacing-xs);
	margin-top: 5px;
}

.export-column-chip {
	padding: 1px 7px;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	background: var(--color-bg-card);
	color: var(--color-text-secondary);
	font-family: var(--font-mono);
	font-size: var(--font-size-xs);
}

/* Rendered inside <noscript> only - see templates/report-detail.php. */
.export-update-button {
	margin-top: var(--spacing-xl);
}

/* The right-hand half: a header row, the preview filling the space between, and the actions row
   pinned under it - so Export CSV sits at the bottom right of the card however tall the preview
   is. min-width: 0 for the same reason the grid track is minmax(0, 1fr). */
.export-panel-file {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.export-preview-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-xl);
	padding: var(--spacing-2xl) var(--spacing-3xl) var(--spacing-lg);
}

.export-preview-meta {
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	white-space: nowrap;
}

/* The only thing on this page that scrolls sideways: a file with every column group ticked is
   eighteen columns wide, and it scrolls here rather than widening the card or the page. */
.export-preview-scroll {
	flex: 1;
	overflow: auto;
	border-top: 1px solid var(--color-border);
	border-bottom: 1px solid var(--color-border-light);
}

/* A monospace preview reads more like the CSV it will become than the app's own UI font would -
   header row included, since those are the file's own column names rather than UI labels, which
   is also why they are not upper-cased the way every other table heading in this app is.
   width: max-content keeps every column at its content width and lets the container above do the
   scrolling; min-width: 100% stops a six-column selection leaving the card half empty. */
.export-preview-table {
	width: max-content;
	min-width: 100%;
	font-family: var(--font-mono);
	font-size: var(--font-size-sm);
}

/* One size for the whole table, header included: these are a file's own column names and values,
   not a UI label above UI text, so a header row set larger or smaller than its body would read as
   a mistake. The padding is the demo's own .data-table (18px/10px against a 20px gutter), which
   is what gives the file a 34px row rather than the app's roomier 44px one. */
.export-preview-table th,
.export-preview-table td {
	padding-left: var(--spacing-2xl);
	padding-right: var(--spacing-2xl);
	font-size: var(--font-size-sm);
	/* The shell's body line-height of 1.5 is for reading paragraphs; a spreadsheet row is sized
	   by its padding and the font's own natural height, which is what makes this row 34px rather
	   than 39 - the demo's own figure, and the same reasoning as .credit-chip further up. */
	line-height: normal;
	white-space: nowrap;
}

.export-preview-table th {
	padding-top: 18px;
	padding-bottom: 18px;
	text-transform: none;
	letter-spacing: 0;
}

/* The live demo's own export table DOES rule under a row - 1px --color-border-light, the same
   figure .table tbody tr uses. Phase 12 set this to 0 against a fetched CSS mirror that turned
   out to disagree with the live page on this one rule; phase 13's live probe corrected it. */
.export-preview-table tbody tr {
	border-bottom: 1px solid var(--color-border-light);
}

.export-preview-table td {
	padding-top: 10px;
	padding-bottom: 10px;
}

/* The column rules of the file grid, a shade darker than .table's own - this table is read as a
   spreadsheet, where the columns are the point. */
.export-preview-table th + th,
.export-preview-table td + td {
	border-left-color: var(--color-border);
}

/* Ledger item E3, phase 14 widened this row to three items (More prompts, Copy prompt for AI,
   Export CSV) - gap dropped from --spacing-xl (16px) to --spacing-lg (12px), the figure the
   ledger brief measured for this row, so three items right-align without crowding the card's own
   edge. */
.export-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-end;
	gap: var(--spacing-lg);
	padding: var(--spacing-lg) var(--spacing-3xl);
}

/* ====================================================================
   NARROW SCREENS (ledger item 5 additions)
   ==================================================================== */
/* 1100px is the demo's own breakpoint for this panel (consultants-styles.css @media max-width:
   1100px), and the reason is the preview: below it the split leaves an eighteen-column monospace
   table about 300-600px of track, which is unreadable however well it scrolls. Stacked, it gets
   the full width. */
@media (max-width: 1100px) {
	/* One column: the groups stack above the preview, and the rule that separated them turns from
	   the side of a column into a divider between two rows. */
	.export-panel {
		grid-template-columns: minmax(0, 1fr);
	}

	.export-panel-groups {
		border-right: none;
		border-bottom: 1px solid var(--color-border-light);
	}
}

/* ====================================================================
   PROMPTS FOR AI PAGE (ledger item E3, phase 14)
   ==================================================================== */
.prompt-card + .prompt-card {
	margin-top: var(--spacing-3xl);
}

/* The system prompt reads as the CSV it describes - monospace, same as .export-preview-table -
   but it is prose, not a grid, so it wraps rather than scrolling sideways: pre-wrap keeps the
   text's own line breaks (the blank lines between sections) while still wrapping a long line.
   max-height + overflow-y is what keeps a long prompt from stretching the card past the fold;
   the card's own border-top gives the scrolled region a visible edge to scroll inside. */
.prompt-card-pre {
	max-height: 420px;
	overflow-y: auto;
	margin: 0;
	padding: var(--spacing-lg);
	border: 1px solid var(--color-border-light);
	border-radius: var(--radius-md);
	background: var(--color-bg-app);
	color: var(--color-text-primary);
	font-family: var(--font-mono);
	font-size: var(--font-size-sm);
	white-space: pre-wrap;
	word-break: break-word;
}

/* A ready-made prompt is one flowing instruction, not a file preview - the app's own body font
   reads better here than the system prompt's monospace does. */
.prompt-card-text {
	margin: 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	line-height: 1.5;
}

.prompt-back-link {
	margin-top: var(--spacing-3xl);
}

/* ====================================================================
   CREDITS PAGE (ledger item 1, phase 3)
   ==================================================================== */
/* The demo's credits page opens with ONE card, not two: .co-wallet-card holds the balance and the
   Add credits controls side by side, split by a vertical rule (consultants-styles.css:893-925).
   Ours drew two separate cards of unequal height with the packs stacked full-width above a
   full-width Add button, which made the top of the page twice as tall as the demo's and the two
   halves read as unrelated. Phase 12, ledger item 16. */
.credits-card {
	margin-bottom: var(--spacing-4xl);
}

.credits-card-body {
	display: flex;
	gap: var(--spacing-4xl);
	align-items: stretch;
}

.credits-balance {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-md);
	padding-right: var(--spacing-4xl);
	border-right: 1px solid var(--color-border-light);
	white-space: nowrap;
}

.credits-add {
	flex: 1;
	display: flex;
	flex-direction: column;
	gap: var(--spacing-md);
}

/* .co-chip-row, widened by ledger item J4 to carry the custom-amount pill too: the four packs,
   the custom input and the Add button are all one row, wrapping together at the same gap. This
   row has exactly two flex items - .credit-pack-list (the four pills) and .credit-custom-add-
   group (the input plus the button) - so wrapping only ever happens between those two groups,
   never inside either one: the pills stay together, and the input and button either share the
   main row or share a second row, but the button is never left alone on one (visual-verifier
   finding, ledger item J4: 902-963px orphaned the button). */
.credit-pack-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-md);
}

.credit-buy-button {
	min-width: 212px;
}

/* .credit-pack-list keeps its own role/aria-label for the four pills as a group, but lays out as
   nothing (display: contents) so its buttons become flex items of .credit-pack-row directly - one
   flex row, one gap, rather than a pill group nested inside a row with a gap of its own. This
   still leaves the four pills as one unbreakable run: they are far narrower than the input+button
   group beside them, so wrapping happens at that group's boundary before it ever reaches here. */
.credit-pack-list {
	display: contents;
}

/* The input and the button that share one custom-amount purchase: grouped into one flex item of
   .credit-pack-row so the row wraps them as a unit, never splitting the button onto its own line
   (ledger item J4 fix). */
.credit-custom-add-group {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: var(--spacing-md);
}

/* Below ~414px the input + button pair (330px) no longer fits the card column, so the pair
   may wrap and the button takes the full width rather than spilling past the card edge. */
@media (max-width: 420px) {
	.credit-custom-add-group {
		flex-wrap: wrap;
	}

	.credit-custom-add-group .btn {
		flex: 1 1 100%;
	}
}

.card-eyebrow {
	display: block;
	margin-bottom: var(--spacing-lg);
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
}

.balance-value {
	margin: 0;
	color: var(--color-text-primary);
	/* The demo shows the balance as the page's one big number. */
	font-size: 40px;
	font-weight: 600;
	line-height: 1.1;
}

.balance-unit {
	color: var(--color-text-secondary);
	font-size: var(--font-size-md);
	font-weight: 400;
}

/* .co-chip: a pill the width of its own label, not a quarter of the card. */
.credit-pack-button {
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	background: var(--color-bg-card);
	padding: var(--spacing-md) var(--spacing-2xl);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-md);
	font-weight: 500;
	cursor: pointer;
	transition: border-color var(--transition-fast), background var(--transition-fast);
}

.credit-pack-button:hover {
	border-color: var(--color-border-dark);
}

.credit-pack-button:disabled {
	border-color: var(--color-border);
	background: var(--color-bg-muted);
	color: var(--color-text-muted);
	cursor: not-allowed;
}

/* .co-chip.active is the dark ink the primary button already uses - white on #171725 is 15.9:1,
   and it is the same "selected" language as the button beside it. The red this carried before was
   chosen only because --color-primary was too pale to pass AA; the demo's own answer is better. */
.credit-pack-button-active {
	border-color: var(--color-ink);
	background: var(--color-ink);
	color: var(--color-text-inverse);
}

/* The custom-amount pill (ledger item J4): same height, radius and border as the four pack pills
   beside it - .credit-pack-button's own padding/font-size/line-height, just a fixed ~110px wide
   rather than sized to a label, and text-align: center so a typed number sits where the pill's
   own placeholder does. A typed value is font-weight 500, matching the pill labels beside it
   (visual-verifier finding, ledger item J4). appearance: textfield plus the two -webkit- spin-
   button rules below drop the number input's up/down spinner, which otherwise crowds a pill this
   narrow - inputmode="numeric" on the element still gets mobile visitors the numeric keypad. */
.credit-custom-input {
	width: 110px;
	padding: var(--spacing-md) var(--spacing-2xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-family: var(--font-family);
	font-size: var(--font-size-md);
	font-weight: 500;
	line-height: normal;
	text-align: center;
	appearance: textfield;
	transition: border-color var(--transition-fast), background var(--transition-fast);
}

.credit-custom-input::-webkit-inner-spin-button,
.credit-custom-input::-webkit-outer-spin-button {
	appearance: none;
	margin: 0;
}

/* #666 on white is 5.74:1, comfortably AA (visual-verifier finding, ledger item J4) - declared
   because the browser default placeholder colour is neither AA nor consistent between browsers. */
.credit-custom-input::placeholder {
	color: var(--color-text-secondary);
}

.credit-custom-input:hover {
	border-color: var(--color-border-dark);
}

.credit-custom-input:focus {
	outline: none;
	border-color: var(--color-info);
	box-shadow: 0 0 0 2px rgba(0, 88, 255, 0.1);
}

.credit-custom-input:disabled {
	border-color: var(--color-border);
	background: var(--color-bg-muted);
	color: var(--color-text-muted);
	cursor: not-allowed;
}

/* Billing off (the only state that disables this input) greys the placeholder to match the
   greyed pack pills beside it - .credit-pack-button:disabled uses the same --color-text-muted. */
.credit-custom-input:disabled::placeholder {
	color: var(--color-text-muted);
}

.credits-add .card-message {
	margin-top: var(--spacing-lg);
}

/* The demo's "Go to Paddle" link is quiet - #666, 13px, no underline at rest (underline on
   hover) - not the app's usual link blue. #666 on white is 5.7:1, comfortably AA. Scoped to this
   one link rather than `.card-message a` generally: the account page's support-email link in the
   same class stays the ordinary blue underlined link, which is not one of the ten findings. */
.credits-add .card-message a {
	display: inline-flex;
	align-items: center;
	gap: var(--spacing-xs);
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	text-decoration: none;
}

.credits-add .card-message a:hover {
	text-decoration: underline;
}

.credits-add .card-message .material-symbols-outlined {
	font-size: var(--font-size-md);
}

.credits-filter-grid {
	grid-template-columns: repeat(4, 1fr) auto;
}

.credits-filter-actions {
	display: flex;
	gap: var(--spacing-lg);
}

/* A band between the rows, not a second header: it keeps the header's type but takes back the
   18px .table th padding above it, so it stays the shallow strip the demo's month band is. */
.credits-month-row th {
	padding: var(--spacing-md) var(--spacing-2xl);
	background: var(--color-bg-section);
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.5px;
	text-align: left;
}

/* Both colours are chosen for AA on plain white, not on a tinted background - #C4102A already
   proven at 5.5:1 (see .property-remove above); #1E7A44 already proven at ~5.35:1 on white
   (chip-completed's own comment computes it against its pastel background, which is lighter still
   than white, so the ratio here can only be higher). */
.credits-amount-negative {
	color: #C4102A;
}

.credits-amount-positive {
	color: #1E7A44;
}

@media (max-width: 900px) {
	/* The vertical rule becomes a horizontal one once the two halves stack. */
	.credits-card-body {
		flex-direction: column;
		gap: var(--spacing-3xl);
	}

	.credits-balance {
		padding-right: 0;
		padding-bottom: var(--spacing-3xl);
		border-right: none;
		border-bottom: 1px solid var(--color-border-light);
	}

	.credits-filter-grid {
		grid-template-columns: repeat(2, 1fr);
	}

	.credits-filter-actions {
		grid-column: span 2;
	}
}

/* .filter-grid's own 720px rule (above, "NARROW SCREENS (ledger item 3 additions)") collapses to
   one column, but .credits-filter-grid's 900px override two rules up is a later, equally specific
   selector on the same element - source order alone would keep it at two columns all the way down
   to 375px, which is what actually overflowed the card once the Clear button widened the row
   (phase 12, verifier fix to ledger items 15+6+5). Naming the same breakpoint again here, after
   that override, is what makes it win back. */
@media (max-width: 720px) {
	.credits-filter-grid {
		grid-template-columns: 1fr;
	}

	.credits-filter-actions {
		grid-column: span 1;
	}
}

@media (max-width: 480px) {
	/* At 768/900 the Export and Search buttons comfortably share the row this spans; below about
	   480px the two full-width buttons (.filter-search-button .btn) no longer both fit on one
	   line, so the second button was scroll-clipped inside the card instead of wrapping. */
	.credits-filter-actions {
		flex-direction: column;
	}
}

/* ====================================================================
   ADMIN (ledger item 1, phase 4)
   A dark top bar and a text nav, deliberately unlike the customer app's light .app-bar, so an
   admin screenshot is never mistaken for one of the customer's own pages.
   ==================================================================== */
.admin-bar {
	background: var(--color-ink);
}

.admin-bar-inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--spacing-3xl);
	padding: var(--spacing-lg) var(--spacing-5xl);
}

.admin-bar-title {
	display: flex;
	align-items: center;
	gap: var(--spacing-3xl);
	color: var(--color-text-inverse);
	font-size: var(--font-size-lg);
	font-weight: 600;
}

/* Same vertical-rule-plus-padding treatment as the customer bar's .company-name, in a border
   colour that reads on the dark bar (the logout button below uses the same rgba white). */
.admin-bar-label {
	padding-left: var(--spacing-xl);
	border-left: 1px solid rgba(255, 255, 255, 0.3);
}

/* The right-hand end of the admin bar (phase 12, ledger item 13): the avatar menu, and the
   <noscript> Log out beside it that only a browser with scripting off ever renders. Without this
   wrapper .admin-bar-inner's own space-between would strand the two at opposite ends of the bar in
   exactly that browser. */
.admin-bar-actions {
	display: flex;
	align-items: center;
	gap: var(--spacing-xl);
}

/* The customer bar's .user-avatar in the admin bar's colours: a white outline circle on the dark
   bar rather than the blue disc, which would read as a second brand colour beside the logo. The
   focus ring is white here for the same reason - --color-info is 2.1:1 on --color-ink. */
.admin-user-avatar {
	border: 1px solid rgba(255, 255, 255, 0.3);
	background: transparent;
	color: var(--color-text-inverse);
}

.admin-user-avatar:hover {
	background: rgba(255, 255, 255, 0.1);
}

.admin-user-avatar:focus-visible {
	outline: 2px solid var(--color-text-inverse);
	outline-offset: 2px;
}

.admin-bar-logout {
	margin: 0;
}

.admin-bar-logout-button {
	padding: var(--spacing-sm) var(--spacing-lg);
	border: 1px solid rgba(255, 255, 255, 0.3);
	border-radius: var(--radius-pill);
	background: transparent;
	color: var(--color-text-inverse);
	font-family: var(--font-family);
	font-size: var(--font-size-sm);
	font-weight: 500;
	cursor: pointer;
}

.admin-bar-logout-button:hover {
	background: rgba(255, 255, 255, 0.1);
}

.admin-nav {
	background: var(--color-bg-card);
	border-bottom: 1px solid var(--color-border);
}

.admin-nav-inner {
	display: flex;
	gap: var(--spacing-3xl);
	padding: 0 var(--spacing-5xl);
}

.admin-nav-link {
	padding: var(--spacing-lg) 0;
	border-bottom: 2px solid transparent;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
	font-weight: 500;
}

.admin-nav-link:hover {
	color: var(--color-text-primary);
	text-decoration: none;
}

.admin-nav-link-active {
	border-bottom-color: var(--color-primary);
	color: var(--color-text-primary);
}

.admin-stat-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
	gap: var(--spacing-3xl);
	margin-bottom: var(--spacing-3xl);
}

.admin-stat-card {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-sm);
}

.admin-stat-card-wide {
	grid-column: span 2;
}

.admin-stat-label {
	color: var(--color-text-header);
	font-size: var(--font-size-xs);
	font-weight: 600;
	text-transform: uppercase;
}

.admin-stat-value {
	margin: 0;
	color: var(--color-text-primary);
	font-size: var(--font-size-3xl);
	font-weight: 600;
}

.admin-stat-sub {
	margin: 0;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

/* #C4102A, not --color-danger: 5.32:1 on --color-danger-bg, so the badge text passes AA (the
   same override .property-remove already makes against white, for the same reason). */
.admin-badge-stale {
	display: inline-flex;
	align-items: center;
	padding: var(--spacing-xs) var(--spacing-lg);
	border-radius: var(--radius-pill);
	background: var(--color-danger-bg);
	color: #C4102A;
	font-size: var(--font-size-sm);
	font-weight: 600;
}

/* The Default badge beside the default admin's name on /admin/admins (phase 15, ledger item G3) -
   the same shape and the same palette as the stale badge above, and for the same reason: it marks
   the one row on that page that is meant to stop existing. Smaller padding, because it sits inline
   in a table cell rather than alone in a stat card. */
.admin-badge-default {
	display: inline-flex;
	align-items: center;
	padding: var(--spacing-xs) var(--spacing-md);
	border-radius: var(--radius-pill);
	background: var(--color-danger-bg);
	color: #C4102A;
	font-size: var(--font-size-sm);
	font-weight: 600;
}

.admin-count-list {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-sm);
	margin: 0;
	padding: 0;
	list-style: none;
}

.admin-count-list li {
	display: flex;
	align-items: center;
	justify-content: space-between;
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

.admin-count-list li strong {
	color: var(--color-text-primary);
	font-weight: 600;
}

@media (max-width: 720px) {
	.admin-bar-inner,
	.admin-nav-inner {
		padding-left: var(--spacing-xl);
		padding-right: var(--spacing-xl);
	}

	/* The reduced side padding above still isn't enough on its own - "Dashboard", "Companies",
	   "Runs" and "Events" at the base 24px gap add up to more than a 375px viewport holds, so the
	   whole page scrolled sideways by the overrun. The nav items can't shrink (each is one
	   un-wrappable word), so the gap is what has to give. Phase 12's ledger item 13 took
	   "Password" out of this nav, which bought back about 90px but not the whole overrun. */
	.admin-nav-inner {
		gap: var(--spacing-md);
	}

	.admin-stat-card-wide {
		grid-column: span 1;
	}
}

/* ====================================================================
   ADMIN COMPANIES, RUNS & EVENTS (ledger item 2, phase 4)
   ==================================================================== */

/* The companies search box: one text input and a button, unlike the six-column filter bars
   elsewhere - its own template rather than stretching .filter-grid's column count to fit. */
.admin-search-grid {
	grid-template-columns: 1fr auto;
}

/* The same trap .credits-filter-grid documents further up: .filter-grid's own 720px rule collapses
   to one column, but this override is a later, equally specific selector on the same element, so
   source order alone kept the search box in a 97px first column beside its buttons at 375px, with
   the label wrapped over three lines. Naming the breakpoint again here, after the override, is
   what makes it win back. */
@media (max-width: 720px) {
	.admin-search-grid {
		grid-template-columns: 1fr;
	}
}

/* Adjust credits / Free re-run, side by side - same two-column shape as .credits-summary, kept
   separate since the two grids describe different pages and may need to diverge later. */
.admin-action-grid {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--spacing-3xl);
	margin-bottom: var(--spacing-3xl);
}

/* The company user page's support actions (phase 12, ledger item 14), each its own POST form, laid
   out as one row that wraps rather than as a stack of full-width forms. */
.admin-user-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-lg);
}

.admin-user-actions form {
	margin: 0;
}

/* The generated password-reset link, shown once. Deliberately a readonly input rather than a line
   of text: the URL is ~90 characters, and a field can be clicked into and copied whole on a phone,
   where selecting wrapped text cannot. Wider than .form-column's 600px for the same reason. */
.admin-reset-link {
	max-width: 760px;
	margin-bottom: var(--spacing-3xl);
	padding: var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-section);
}

.admin-reset-link label {
	display: block;
	margin-bottom: var(--spacing-md);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	font-weight: 500;
}

.admin-reset-link input[type="text"] {
	width: 100%;
	height: 42px;
	padding: var(--spacing-lg) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-bg-card);
	color: var(--color-text-primary);
	font-family: var(--font-mono);
	font-size: var(--font-size-sm);
}

/* The Events log's Detail column: Paddle's own rejection/error text can run long, so it is
   clipped to one line with the full text on the title attribute rather than breaking the table's
   row height. */
.table-cell-truncate {
	max-width: 320px;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}

/* The Previous / Next pager under a paged table (Companies, Events). */
.pagination {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--spacing-xl);
	margin-top: var(--spacing-3xl);
}

.pagination-link {
	padding: var(--spacing-sm) var(--spacing-xl);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	color: var(--color-text-primary);
	font-size: var(--font-size-base);
	font-weight: 500;
}

.pagination-link:hover {
	border-color: var(--color-border-dark);
	text-decoration: none;
}

.pagination-link-disabled {
	/* #666 not the muted #999: the disabled label is still text and must pass AA */
	color: var(--color-text-secondary);
}

.pagination-status {
	color: var(--color-text-secondary);
	font-size: var(--font-size-base);
}

/* ====================================================================
   PRESENCE (phase 14, ledger item Q4)
   ==================================================================== */
/* #1E7A44, not --color-success (#5FBF8B): the demo's pale green is 1.8:1 on white, and a dot that
   MEANS something - this person has a tab open - has to meet the 3:1 WCAG asks of a non-text
   indicator. This one is 5.3:1, which also lets .presence-note use it as text. The word "Online"
   is in the markup beside it, visually hidden, because a colour on its own says nothing to a
   screen reader. */
.presence-dot {
	display: inline-block;
	/* The name cell is a flex row, and without this the dot is the item that gives way when a long
	   name meets a narrow phone - it flattens into an oval rather than staying a dot. */
	flex-shrink: 0;
	width: 8px;
	height: 8px;
	border-radius: var(--radius-round);
	background: #1E7A44;
}

/* The [hidden] attribute is how both of these start life and how they go away again; without this
   the display rules above and below would out-specify the browser's own [hidden] rule and they
   would never hide at all. */
.presence-dot[hidden],
.presence-note[hidden] {
	display: none;
}

.presence-note {
	color: #1E7A44;
	font-size: var(--font-size-sm);
	font-weight: 500;
}

.presence-list {
	display: flex;
	flex-direction: column;
	gap: var(--spacing-sm);
	margin: var(--spacing-sm) 0 0 0;
	padding: 0;
	list-style: none;
}

/* Wraps rather than truncates: on a phone the page path and the time move onto their own line
   instead of one of them disappearing. */
.presence-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--spacing-md);
	font-size: var(--font-size-base);
}

.presence-name {
	color: var(--color-text-primary);
	font-weight: 500;
}

.presence-page {
	color: var(--color-text-secondary);
	word-break: break-word;
}

.presence-since {
	margin-left: auto;
	color: var(--color-text-secondary);
	font-size: var(--font-size-sm);
}

@media (max-width: 720px) {
	.admin-action-grid {
		grid-template-columns: 1fr;
	}

	.table-cell-truncate {
		max-width: 160px;
	}
}
