/**
 * Shared shell for every admin panel (SiteNotes, SkillAdmin,
 * LoginAudit, AccountEditor) — sidebar nav, topbar, content wrapper,
 * and the small-box stat card / card-header patterns used across
 * them. Built entirely on the site's existing --c-* design tokens
 * (see assets/css/main.css) — no new colors introduced, no framework
 * dependency (hand-written, matching the rest of the site).
 */

.admin-shell {
	display: flex;
	min-height: 100vh;
	min-height: 100dvh;
	background: var(--c-bg-1);
}

/* Sidebar
   ------------------------------------------------------------------ */

.admin-sidebar {
	--admin-sidebar-w: 232px;
	flex: none;
	width: var(--admin-sidebar-w);
	background: var(--c-bg-0);
	border-right: 1px solid var(--c-border-2);
	display: flex;
	flex-direction: column;
	position: sticky;
	top: 0;
	height: 100vh;
	height: 100dvh;
	overflow-y: auto;
	/* overflow-y alone forces the X axis to "auto" too (CSS can't leave
	   one axis ambiguous once the other isn't visible) — .admin-nav-tip's
	   240px popout, positioned past the sidebar's right edge, then reads
	   as real horizontal overflow and shows a permanent scrollbar. Pin
	   it down explicitly; the tooltip was already being clipped by this
	   same forced overflow before this fix, so it's no worse off. */
	overflow-x: hidden;
	z-index: 30;
}

.admin-sidebar-logo {
	display: block;
	padding: 1.5em 1.5em 1em;
}

.admin-sidebar-logo img {
	height: 34px;
	display: block;
}

/* Sits between the logo and the panel list — same link styling as
   .admin-nav a below so it doesn't look like a second, competing nav
   system, just visually separated (margin + border) since it isn't one
   of this account's panels. Opens in a new tab so leaving to check the
   live site never risks an admin form's unsaved-changes state. */
.admin-view-site {
	display: flex;
	align-items: center;
	gap: 12px;
	margin: 0 0.9em 1em;
	padding: 0.65em 0.9em;
	border-radius: 8px;
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	text-decoration: none;
	font-size: 0.85em;
	font-weight: 700;
	transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease;
}

.admin-view-site svg {
	width: 15px;
	height: 15px;
	flex: none;
}

.admin-view-site:hover {
	background: var(--c-bg-2);
	color: var(--c-text-5);
	border-color: var(--c-accent-2);
}

.admin-nav {
	list-style: none;
	margin: 0.5em 0 0 0;
	padding: 0 0.9em;
	flex: 1;
}

.admin-nav li {
	margin: 0 0 4px 0;
}

.admin-nav a {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 0.65em 0.9em;
	border-radius: 8px;
	color: var(--c-text-2);
	text-decoration: none;
	font-size: 0.92em;
	font-weight: 700;
	transition: background-color 0.4s ease, color 0.4s ease;
}

.admin-nav a svg {
	width: 17px;
	height: 17px;
	flex: none;
}

.admin-nav a:hover {
	background: var(--c-bg-2);
	color: var(--c-text-5);
}

.admin-nav a.active {
	background: var(--c-accent-fill);
	color: var(--c-text-7);
}

/* Per-panel description tooltip — same rich-tooltip language as the
   Component Lab showcase's .tip-anchor/.tip (bounce-in scale, arrow,
   var(--c-bg-3)/var(--c-border-3) card), just anchored to the right
   of the sidebar link instead of above a pill button. */
.admin-nav-tip-anchor {
	position: relative;
}

.admin-nav-tip {
	/* Fixed (not absolute) so it escapes .admin-sidebar's overflow box
	   entirely regardless of overflow-x/-y — an absolutely-positioned
	   240px popout nested inside a scrollable sidebar has no way to
	   both scroll vertically and escape horizontally at the same time
	   (see admin-shell.js, which sets top/left in px on hover/focus
	   since fixed positioning needs real viewport coordinates, not a
	   percentage relative to the anchor). */
	position: fixed;
	top: 0;
	left: 0;
	transform: translateY(-50%) translateX(-6px) scale(0.92);
	transform-origin: left center;
	width: 240px;
	padding: 0.9em 1em;
	border-radius: 14px;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-4);
	font-size: 0.78em;
	line-height: 1.6;
	font-weight: 400;
	text-decoration: none;
	box-shadow: 0 10px 30px var(--c-shadow-heavy);
	opacity: 0;
	pointer-events: none;
	z-index: 50;
	transition: opacity 0.4s ease, transform 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.admin-nav-tip::after {
	content: '';
	position: absolute;
	top: 50%;
	right: 100%;
	transform: translateY(-50%);
	border: 6px solid transparent;
	border-right-color: var(--c-bg-3);
}

@media (hover: hover) {
	.admin-nav-tip-anchor:hover .admin-nav-tip,
	.admin-nav-tip-anchor:focus-visible .admin-nav-tip {
		opacity: 1;
		transform: translateY(-50%) translateX(0) scale(1);
		transition-delay: 0.3s;
	}
}

@media (prefers-reduced-motion: reduce) {
	.admin-nav-tip {
		transition: opacity 0.3s ease;
		transform: translateY(-50%);
	}
	@media (hover: hover) {
		.admin-nav-tip-anchor:hover .admin-nav-tip,
		.admin-nav-tip-anchor:focus-visible .admin-nav-tip {
			transform: translateY(-50%);
			transition-delay: 0s;
		}
	}
}

.admin-sidebar-clock {
	display: flex;
	align-items: center;
	gap: 0.5em;
	margin: 0.6em 1.4em 0;
	padding: 0.6em 0.9em;
	border-radius: 8px;
	background: var(--c-bg-2);
	border: 1px solid var(--c-border-2);
	color: var(--c-text-2);
	font-size: 0.78em;
	font-variant-numeric: tabular-nums;
}
.admin-sidebar-clock svg { flex: none; width: 15px; height: 15px; color: var(--c-text-1); }
.admin-sidebar-clock + .admin-sidebar-clock { margin-top: 0.4em; }

.admin-sidebar-foot {
	padding: 1em 1.4em 1.4em;
	border-top: 1px solid var(--c-border-2);
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 0.75em;
}

.admin-sidebar-user {
	color: var(--c-text-3);
	font-size: 0.85em;
	font-weight: 700;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.admin-demo-badge {
	display: inline-block;
	margin-left: 0.4em;
	padding: 0.1em 0.5em;
	border-radius: 999px;
	background: rgba(217, 164, 6, 0.18);
	color: #d9a406;
	font-size: 0.68em;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.03em;
	vertical-align: middle;
}
/* #d9a406 is ~2.3:1 on a white background, well under AA — darken for light theme. */
html[data-theme="light"] .admin-demo-badge { color: #8a6304; }

.admin-sidebar-logout {
	color: var(--c-text-1);
	font-size: 0.82em;
	text-decoration: none;
	flex: none;
	transition: color 0.4s ease;
}

.admin-sidebar-logout:hover {
	color: var(--c-accent-1);
}

.admin-sidebar-toggle {
	display: none;
	position: fixed;
	top: 14px;
	left: 14px;
	z-index: 40;
	width: 40px;
	height: 40px;
	align-items: center;
	justify-content: center;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	border-radius: 8px;
	color: var(--c-text-4);
	cursor: pointer;
}

.admin-sidebar-toggle svg {
	width: 18px;
	height: 18px;
}

.admin-sidebar-backdrop {
	display: none;
	position: fixed;
	inset: 0;
	background: var(--c-shadow-heavy);
	z-index: 25;
}

/* Main column
   ------------------------------------------------------------------ */

.admin-main {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
}

.admin-topbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1em;
	padding: 1.1em 2em;
	background: var(--c-bg-0);
	border-bottom: 1px solid var(--c-border-2);
	position: sticky;
	top: 0;
	z-index: 20;
}

.admin-topbar h1 {
	font-size: 1.25em;
	margin: 0;
	color: var(--c-text-6);
}

.admin-topbar-actions {
	display: flex;
	align-items: center;
	gap: 14px;
}

.admin-lang-toggle {
	font-size: 0.85em;
	font-weight: 700;
	color: var(--c-text-3);
	border: 1px solid var(--c-border-3);
	border-radius: 999px;
	padding: 0.3em 0.9em;
	text-decoration: none;
	transition: border-color 0.3s ease, color 0.3s ease;
}

.admin-lang-toggle:hover,
.admin-lang-toggle:focus-visible {
	border-color: var(--c-accent-2);
	color: var(--c-accent-1);
}

/* Same amber "demo" color as .admin-demo-badge above, but as a full-width
   banner under the topbar so it's impossible to miss on any panel — the
   badge alone sits in the sidebar footer, easy to scroll past unnoticed. */
.admin-demo-banner {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 0.7em 2em;
	background: rgba(217, 164, 6, 0.12);
	border-bottom: 1px solid rgba(217, 164, 6, 0.35);
	color: #d9a406;
	font-size: 0.88em;
}

.admin-demo-banner svg {
	width: 18px;
	height: 18px;
	flex: none;
}

.admin-demo-banner strong {
	color: #d9a406;
}

html[data-theme="light"] .admin-demo-banner,
html[data-theme="light"] .admin-demo-banner strong { color: #8a6304; }

@media screen and (max-width: 900px) {
	.admin-demo-banner {
		padding: 0.7em 1.2em;
	}
}

/**
 * SiteNotes/LoginAudit still link assets/css/main.css, which has its
 * own unrelated #content { ... } rule for the public site's template
 * (background, border-top, padding) — an ID selector, so it would
 * otherwise beat a plain .admin-content class rule regardless of
 * source order. #content is also load-bearing here for JS (the
 * auto-refresh feature in sitenotes-init.js DOM-swaps it by ID), so
 * it can't just be dropped. Every rule below that needs to win uses
 * the compound #content.admin-content selector instead, which is
 * strictly higher specificity than main.css's bare #content.
 */

#content.admin-content {
	flex: 1;
	padding: 1.75em clamp(1.5em, 3vw, 3em) 3em;
	width: 100%;
	background: none;
	border-top: none;
}

/* SiteNotes/LoginAudit still build their markup on the public site's
   HTML5UP .container/.row/.col-* grid (a fixed 1200px .container by
   default) — override it to actually fill the admin content area.
   The .row/.col-* percentage-based flex system underneath still works
   at any width, so this is a safe override, not a structural rewrite. */
#content.admin-content .container {
	width: 100%;
	max-width: none;
	margin: 0;
}

/* Cards/tables that want a comfortable reading width on very wide
   monitors instead of stretching edge-to-edge opt into this. */
.admin-content-narrow {
	max-width: 1400px;
}

/* SkillAdmin/AE's category/account lists are single-column,
   drag-to-reorder editors — the reorder math (getDragAfterElement in
   skilladmin.js/ae.js) compares Y position only, so a multi-column
   grid would make drops land in the wrong place. Rather than stretch
   single-column cards edge-to-edge on an ultrawide monitor (a huge
   card with a handful of rows and acres of empty padding), the
   editing column caps at a comfortable width and centers — the same
   call real editing UIs (Notion, Linear's settings) make even inside
   an otherwise full-width shell. */
.admin-editor-col {
	max-width: 900px;
	margin: 0 auto;
}

/* AE has no drag-and-drop (unlike SkillAdmin's categories/skills), so
   its account cards are free to reflow into a real grid on wide
   screens instead of a single stacked column. */
.ae-account-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(380px, 100%), 1fr));
	gap: 1.25em;
	align-items: start;
}

.ae-account-grid .ae-account {
	margin: 0;
}

/* Cards — every top-level panel section (stat groups, tables, forms)
   gets the same bordered-box treatment: flat at rest, matching the
   site's "shadow means interaction" rule (no resting shadow).
   ------------------------------------------------------------------ */

#content.admin-content section {
	background: var(--c-bg-0);
	border: 1px solid var(--c-border-2);
	border-radius: 8px;
	padding: 1.4em 1.6em;
	margin: 0 0 1.5em 0;
	box-shadow: none;
}

#content.admin-content section > header:not(.dash-header) {
	display: flex;
	align-items: center;
	gap: 0.6em;
	margin: -1.4em -1.6em 1.25em;
	padding: 1em 1.6em;
	border-bottom: 1px solid var(--c-border-2);
}

#content.admin-content section > header:not(.dash-header) h2,
#content.admin-content section > header:not(.dash-header) h3 {
	margin: 0;
	font-size: 1em;
	color: var(--c-text-5);
}

#content.admin-content section > header:not(.dash-header)::before {
	content: '';
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--c-accent-1);
	flex: none;
}

/* Collapsible section headers, applied generically by
   assets/js/admin-collapsible.js to every card's header. Same visual
   language as Traffic Analyzer's older bespoke .cl-collapsible-header/
   .cl-chevron (ta.php), just reusable across every panel now. */
.admin-collapsible-header { cursor: pointer; user-select: none; }
.admin-collapsible-header:hover h2, .admin-collapsible-header:hover h3 { color: var(--c-accent-1); }
.admin-collapsible-header:hover .admin-collapsible-chevron { color: var(--c-accent-1); }
.admin-collapsible-chevron { flex: none; width: 18px; height: 18px; margin-left: auto; color: var(--c-accent-1); transition: transform 0.4s ease; }
.admin-collapsible-chevron svg { width: 100%; height: 100%; }
.admin-collapsible-header[aria-expanded="false"] .admin-collapsible-chevron { transform: rotate(-90deg); }
.admin-collapsible-header[aria-expanded="false"] { margin-bottom: -1.25em; border-bottom: none; }
/* Smooth expand/collapse via the CSS grid-template-rows 1fr/0fr trick
   (see assets/js/admin-collapsible.js, which moves every non-header
   child into this one wrapper): animating a grid row's own track size
   works even though the wrapper's content height isn't known ahead of
   time, unlike animating height/max-height directly. The inner element
   is what actually clips during the transition — a grid item's own
   overflow still participates in intrinsic sizing otherwise. */
.admin-collapsible-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.5s ease; }
.admin-collapsible-body.is-collapsed { grid-template-rows: 0fr; }
.admin-collapsible-body-inner { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) {
	.admin-collapsible-body { transition: none; }
}

/* Nested cards are always wrong — a section inside another section
   (e.g. LoginAudit's per-tab result panel, itself inside the outer
   page section) loses the boxed treatment; only the outermost one
   reads as a card. */
#content.admin-content section section {
	background: none;
	border: none;
	border-radius: 0;
	padding: 0;
	margin: 0;
}

#content.admin-content section section > header:not(.dash-header) {
	margin: 0 0 1.25em;
	padding: 0 0 1em;
}

/* main.css's #content h2/h3 (public-site template rules) also beat
   any single-class selector by ID specificity, including SkillAdmin's
   and AccountEditor's own dialog headings — restore their intended
   size/color here at matching specificity. */
#content.admin-content .sa-modal h3,
#content.admin-content .ae-modal h3 {
	color: var(--c-text-5);
	font-size: 1.1em;
}

/* Adaptive card row — used for groups of small, related cards (e.g.
   SiteNotes' Language/Browser/Device/Resolutions). auto-fit means the
   number of cards per row grows with the viewport instead of being
   pinned to a fixed fraction (3 cards stretched thin on an ultrawide
   monitor is the thing this replaces). */
#content.admin-content .dash-card-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
	gap: 1.5em;
	margin: 0 0 1.5em 0;
	align-items: start;
}

#content.admin-content .dash-card-grid section {
	margin: 0;
}

.table-scroll { overflow-x: auto; }

/* Reusable "auto-fit list" for a plain label/value list (browser
   names, country codes, screen resolutions, etc.) — reflows into more
   columns as its container gets wider instead of staying pinned to one
   tall single column regardless of how much horizontal room is
   available. Same auto-fit idiom as .dash-card-grid above, just for
   individual rows within one section instead of whole cards; used
   wherever a panel has this kind of list, not just one place. */
.admin-stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); gap: 0 1.5em; }
.admin-stat-row { display: flex; align-items: center; justify-content: space-between; gap: 1em; padding: 0.5em 0.3em; border-bottom: 1px solid var(--c-border-2); }
.admin-stat-row:last-child { border-bottom: none; }
.admin-stat-label { display: flex; align-items: center; gap: 0.4em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin-stat-value { flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--c-text-1); }
/* Same zebra tint every data table on these panels already gets (see
   the table tbody rule above) — applied here too since this grid
   replaced plain <table> rows for these lists. */
.admin-stat-row:nth-child(even) { background-color: rgba(255, 255, 255, 0.055); }
html[data-theme="light"] .admin-stat-row:nth-child(even) { background-color: rgba(0, 0, 0, 0.055); }

/* Small muted description paragraph under a section/card heading —
   same role TA's page-local .cl-desc already played; genericized here
   since Honeypot Log needs the identical treatment for its new
   sections and there was nothing shared to reach for yet. */
.admin-desc { font-size: 0.85em; color: var(--c-text-2); margin: 0 0 1.1em; line-height: 1.6; }

/* Was a repeated inline style="" in hbr_classification_badge() — moved
   here so style-src can drop 'unsafe-inline' (includes/nocache.php's
   hbr_apply_csp_header()). */
.badge-muted-text { color: var(--c-text-1); }

/* Demo accounts must never be able to read a real, collected IP address —
   the underlying value is itself replaced with a fake one server-side (see
   hbr_ip_badge_demo_safe() etc.), this is just the visual "hidden" look on
   top of that. Shared across every admin panel that shows IPs. */
.admin-ip-blurred { filter: blur(2.2px); -webkit-user-select: none; user-select: none; cursor: default; }

/* Generic horizontal bar list for a magnitude-by-label breakdown (e.g.
   Traffic Analyzer's classification breakdown, Honeypot Log's decoy-type
   breakdown) — single accent hue only, no legend needed since every row
   already carries its own text label; bar length is the only encoding. */
.admin-bar-list { display: flex; flex-direction: column; gap: 0.7em; }
.admin-bar-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1em; margin-bottom: 0.3em; }
.admin-bar-label { font-size: 0.85em; color: var(--c-text-4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin-bar-value { flex: none; font-size: 0.85em; font-weight: 700; color: var(--c-text-6); font-variant-numeric: tabular-nums; }
.admin-bar-track { height: 8px; border-radius: 999px; background: var(--c-bg-2); overflow: hidden; }
/* transform:scaleX(), not width — animating width is a layout property
   (triggers reflow); scaleX is compositor-only. width:100% is the fill's
   own resting size, scaled down by the inline transform per row. */
.admin-bar-fill { width: 100%; height: 100%; border-radius: 999px; background: var(--c-accent-fill); transform-origin: left; transition: transform 0.6s ease; }

/* Alternating row backgrounds ("zebra striping") for every data table
   across every admin panel — a run of same-color rows gets hard to
   scan once there's more than a handful, especially with the tall
   multi-line rows Traffic Analyzer's Rules/Activity tables use. A
   subtle overlay tint rather than a fixed color, so it auto-adapts
   between the dark and light themes without needing two separate
   color tokens per panel. Low specificity on purpose — any panel's
   own hover/selection styles (e.g. sitenotes.php's .visit-summary-row
   :hover) already win over this without any extra !important. */
#content table:not(.visit-table) tbody tr:nth-child(even):not(:hover) td { background-color: rgba(255, 255, 255, 0.055); }
html[data-theme="light"] #content table:not(.visit-table) tbody tr:nth-child(even):not(:hover) td { background-color: rgba(0, 0, 0, 0.055); }

/* sitenotes.php's visit-table pairs every visible row with a hidden
   detail row right after it, so plain nth-child(even) either always
   lands on the hidden row or (once the header row's own position
   shifts the count) tints every visible row uniformly instead of
   alternating — excluded above, striped here instead by counting only
   every other *visible* row (step of 4, skipping each pair's hidden
   partner). */
#content table.visit-table tr.visit-summary-row:nth-child(4n+2):not(:hover) td { background-color: rgba(255, 255, 255, 0.055); }
html[data-theme="light"] #content table.visit-table tr.visit-summary-row:nth-child(4n+2):not(:hover) td { background-color: rgba(0, 0, 0, 0.055); }

/* Row hover must read as its own distinct state, not just "a slightly
   different shade of the same gray" as either zebra stripe — a plain
   white/black-tint hover (matching the stripe's own tint formula)
   would land close enough in value to the striped rows that hovering
   a striped row barely changed anything visually. Uses the accent
   color instead of gray, at low opacity, so it's a genuinely different
   hue from both stripe states in either theme. Sitewide, every plain
   and .visit-table row alike. */
/* Excludes .visit-detail-row/.admin-table-detail-row — the expanded
   detail panel beneath a summary row is one wide always-there <tr>,
   not a hoverable data row; tinting it made the whole open panel flash
   burgundy under the cursor while reading it, which reads as a stray
   effect rather than a hover state. */
#content table tr:not(.visit-detail-row):not(.admin-table-detail-row):hover td { background-color: rgba(143, 36, 56, 0.22); }
html[data-theme="light"] #content table tr:not(.visit-detail-row):not(.admin-table-detail-row):hover td { background-color: rgba(109, 15, 34, 0.14); }

/* Sortable/resizable columns for every data table, applied generically
   by assets/js/admin-table.js — one shared script for every panel
   rather than per-panel markup/CSS. Traffic Analyzer's Rules table has
   its own older, bespoke version of this same idea (assets/js/ta.js,
   .cl-resizable-table/.cl-sort-indicator/.cl-col-resize) which the
   script deliberately skips re-enhancing, which is why these rules
   use different class names than that one. */
.admin-table-fixed { table-layout: fixed; }
.admin-sortable-th { position: relative; user-select: none; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.admin-sortable-th:hover { color: var(--c-accent-1); }
.admin-table-fixed td { overflow: hidden; text-overflow: ellipsis; }
.admin-sort-indicator { display: inline-block; width: 0.9em; margin-left: 0.2em; color: var(--c-accent-1); font-size: 0.85em; }
.admin-col-resize { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 1; background: var(--c-accent-2); opacity: 0; transition: opacity 0.15s ease; }
/* Faint hint the moment the header is hovered anywhere, full-strength
   once the cursor is actually over the 8px handle itself (or mid-drag) —
   makes the handle discoverable without hunting for the narrow strip. */
.admin-sortable-th:hover .admin-col-resize { opacity: 0.22; }
.admin-col-resize:hover, .admin-col-resize.resizing { opacity: 0.5; }
/* Drag-and-drop column reordering — the header cell itself is the drag
   handle (see assets/js/admin-table.js), so the whole <th> gets the
   feedback states rather than a dedicated grip icon. */
.admin-col-dragging { opacity: 0.4; }
.admin-col-drop-target { outline: 2px dashed var(--c-accent-2); outline-offset: -2px; }
.admin-table-toolbar { display: flex; justify-content: flex-end; gap: 0.5em; margin-bottom: 0.75em; }
.admin-table-reset {
	background: none; border: none; color: var(--c-text-1); font-family: inherit; font-size: 0.78em;
	cursor: pointer; padding: 0.4em 0.6em; text-decoration: underline; text-underline-offset: 2px;
}
.admin-table-reset:hover { color: var(--c-accent-1); }
/* "Save as default for all users" is the one action here that affects
   everyone, not just the account clicking it — a touch of accent color
   keeps it visually distinct from the two personal-only actions
   (Reset, Save my layout) sitting right next to it. */
.admin-table-save-default { color: var(--c-accent-1); }
.admin-table-save-default:hover { color: var(--c-accent-2); }

/* Save-confirmation toast for admin-table.js's Save actions — a plain
   fixed-position element rather than each panel's own local
   showToast(), since this script runs on panels with no dedicated JS
   of their own (loginaudit.php has none). */
.admin-table-toast {
	position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(12px);
	background: var(--c-bg-3); border: 1px solid var(--c-border-3); border-radius: 8px;
	padding: 0.7em 1.1em; font-size: 0.85em; color: var(--c-text-5);
	box-shadow: 0 10px 28px var(--c-shadow-heavy); z-index: 1200;
	opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease;
}
.admin-table-toast-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
.admin-table-toast-error { border-color: var(--c-accent-2); color: var(--c-accent-1); }

/* Column hide/show (right-click a header -> Remove column / Add
   column…) and the shared right-click menu + "Add column" dialog it
   opens — see assets/js/admin-table.js. A "hidden" column stays fully
   in the DOM (server always renders every column), just display:none
   on both its <th> and every matching <td>, so re-adding it later
   needs no re-fetch. */
.admin-col-hidden { display: none !important; }
/* Brief flash on a data cell after "Copy" from its right-click menu —
   the only feedback that a silent clipboard write actually happened. */
.admin-cell-copied { background: var(--c-accent-2) !important; color: var(--c-text-7) !important; }
.admin-ctx-menu {
	position: fixed;
	z-index: 1000;
	display: flex;
	flex-direction: column;
	min-width: 190px;
	padding: 4px;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	border-radius: 8px;
	box-shadow: 0 10px 28px var(--c-shadow-heavy);
}
.admin-ctx-menu button {
	display: block;
	width: 100%;
	text-align: left;
	background: none;
	border: 0;
	padding: 0.5em 0.8em;
	border-radius: 6px;
	color: var(--c-text-5);
	font-family: inherit;
	font-size: 0.85em;
	cursor: pointer;
}
.admin-ctx-menu button:hover:not(:disabled) { background: var(--c-bg-2); color: var(--c-accent-1); }
.admin-ctx-menu button:disabled { opacity: 0.4; cursor: not-allowed; }
dialog.admin-col-dialog {
	border: 1px solid var(--c-border-3);
	border-radius: 14px;
	padding: 1.4em;
	background: var(--c-bg-2);
	color: var(--c-text-5);
	max-width: 420px;
	width: 90vw;
}
dialog.admin-col-dialog::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
dialog.admin-col-dialog h3 { margin: 0 0 0.8em; font-size: 1.05em; }
.admin-col-dialog-list { display: flex; flex-direction: column; gap: 6px; max-height: 40vh; overflow-y: auto; margin-bottom: 1em; }
.admin-col-dialog-list button {
	text-align: left;
	padding: 0.55em 0.8em;
	border-radius: 8px;
	border: 1px solid var(--c-border-3);
	background: var(--c-bg-3);
	color: var(--c-text-5);
	font-family: inherit;
	font-size: 0.88em;
	cursor: pointer;
}
.admin-col-dialog-list button:hover { border-color: var(--c-accent-2); color: var(--c-accent-1); }
.admin-col-dialog-empty { color: var(--c-text-1); font-size: 0.88em; margin-bottom: 1em; }
.admin-col-dialog-actions { display: flex; justify-content: flex-end; }
.admin-col-dialog-actions button {
	padding: 0.5em 1.1em;
	border-radius: 999px;
	border: 1px solid var(--c-border-3);
	background: none;
	color: var(--c-text-3);
	font-family: inherit;
	font-size: 0.85em;
	cursor: pointer;
}
.admin-col-dialog-actions button:hover { border-color: var(--c-accent-2); color: var(--c-accent-1); }

/* Below the mobile breakpoint, every enhanced datagrid collapses from
   a scrolling grid into a stack of one card per row — a wide table
   forced into a narrow viewport otherwise squeezes every column down
   to 1-2 unreadable characters (table-layout: fixed shares the
   container's width evenly instead of letting the table overflow into
   its .table-scroll wrapper). Labels come from data-label — set on
   each cell by admin-table.js's applyMobileLabels() for every
   generically-enhanced table, and hardcoded in ta.js's renderRules()
   for Traffic Analyzer's Rules table (.cl-resizable-table), which
   predates and skips the shared script entirely — see its own comment
   there. Sorting/resizing/column drag-reorder stay desktop-only —
   none of those gestures make sense once there's no header row or
   column grid left on screen. */
@media screen and (max-width: 736px) {
	#content table.admin-table-enhanced thead,
	/* :not(.admin-table-detail-row): :has(th) matches ANY descendant, not
	   just direct children — without this exclusion it also catches an
	   expand-for-detail row whose nested session-pages-table has its own
	   real <th> cells, clipping the entire expanded panel (position, width
	   1px, height 1px, clip:rect(0,0,0,0)) to invisible on mobile. Real bug,
	   found via mobile QA 2026-08-14: tapping to expand rendered nothing. */
	#content table.admin-table-enhanced tr:has(th):not(.admin-table-detail-row),
	#content table.cl-resizable-table thead {
		position: absolute;
		/* !important on width/height: without it, this loses to the plain
		   `tr { width: 100% !important }` block-conversion rule below despite
		   this selector's higher specificity — !important always beats
		   specificity. That let the real header row's <th> cells render at
		   full natural width (100% resolving against the viewport, since
		   position:absolute here has no positioned ancestor), which doesn't
		   paint (still clipped) but still counts toward the page's scrollable
		   width, so the whole page gained a phantom horizontal scrollbar on
		   mobile. Found via mobile QA 2026-08-14 alongside the detail-row bug
		   above — same underlying mechanism, different row. */
		width: 1px !important; height: 1px !important;
		overflow: hidden;
		clip: rect(0, 0, 0, 0);
		white-space: nowrap;
	}
	#content table.admin-table-enhanced,
	#content table.admin-table-enhanced tbody,
	#content table.admin-table-enhanced tr,
	#content table.admin-table-enhanced td,
	#content table.cl-resizable-table,
	#content table.cl-resizable-table tbody,
	#content table.cl-resizable-table tr,
	#content table.cl-resizable-table td {
		display: block;
		width: 100% !important;
	}
	#content table.admin-table-enhanced tr:not(.admin-table-detail-row),
	#content table.cl-resizable-table tr {
		border: 1px solid var(--c-border-3);
		border-radius: 8px;
		margin: 0 0 0.7em;
		padding: 0.4em 0.9em;
		background: var(--c-bg-2);
	}
	/* Stacked block layout (label line, then whatever content follows),
	   not a label/value flex row — several cells here hold more than one
	   child element (Traffic Analyzer's Rules "Name" cell has a name,
	   an auto-description and an optional note as three separate divs).
	   A flex row treats each of those as its own competing flex item and
	   crushes them all sideways into the same unreadable columns this
	   whole fix exists to get rid of; stacking avoids that regardless of
	   how many children a cell holds. */
	#content table.admin-table-enhanced td,
	#content table.cl-resizable-table td {
		display: block;
		padding: 0.35em 0;
		border-bottom: 1px solid var(--c-border-2);
		text-align: left;
		overflow: visible;
		text-overflow: clip;
		white-space: normal;
		word-break: break-word;
	}
	#content table.admin-table-enhanced td:last-child,
	#content table.cl-resizable-table td:last-child { border-bottom: none; }
	#content table.admin-table-enhanced td[data-label]::before,
	#content table.cl-resizable-table td[data-label]::before {
		content: attr(data-label);
		display: block;
		color: var(--c-text-3);
		font-size: 0.78em;
		margin-bottom: 0.2em;
	}
	/* Detail rows (sitenotes.php's expand-for-session-pages panel) keep
	   their own full-width nested content as-is, just without the
	   flex/label treatment meant for plain data cells. */
	#content table.admin-table-enhanced tr.admin-table-detail-row td {
		display: block;
		text-align: left;
		border-bottom: none;
	}
	/* Converting <tr> to display:block on mobile leaves some browsers
	   still applying an internal-table-row sizing quirk that clips the
	   row to a 1px box with overflow:hidden regardless of its block-level
	   content's actual height — even though no rule here sets either
	   property. Forcing both explicitly is the only reliable fix found;
	   this row's own expand/collapse is already handled by the nested
	   .visit-detail-collapse grid-rows animation, so the row itself
	   never needs to clip anything on its own. */
	#content table.admin-table-enhanced tr.admin-table-detail-row {
		height: auto !important;
		overflow: visible !important;
	}
	#content table.admin-table-enhanced .session-pages-table { display: block; overflow-x: auto; }
	#content table.admin-table-enhanced .session-pages-table tr { display: table-row; }
	#content table.admin-table-enhanced .session-pages-table td,
	#content table.admin-table-enhanced .session-pages-table th { display: table-cell; }
}

/* Small-box stat cards
   ------------------------------------------------------------------ */

.stat-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
	gap: 16px;
	margin: 0 0 1.5em 0;
}

.stat-tile {
	position: relative;
	background: var(--c-bg-2);
	border: 1px solid var(--c-border-2);
	border-radius: 8px;
	padding: 1.1em 1.3em;
	overflow: hidden;
	transition: border-color 0.4s ease, transform 0.4s ease;
}

.stat-tile:hover {
	border-color: var(--c-border-4);
	transform: translateY(-2px);
}

.stat-tile .stat-icon {
	position: absolute;
	top: 0.9em;
	right: 0.95em;
	width: 30px;
	height: 30px;
	color: var(--c-accent-1);
	opacity: 0.5;
}

.stat-tile .stat-icon svg {
	width: 100%;
	height: 100%;
}

.stat-tile .label {
	color: var(--c-text-1);
	font-size: 0.85em;
	padding-right: 2.2em;
}

.stat-tile .value {
	color: var(--c-text-5);
	font-size: 2em;
	font-weight: 700;
	margin-top: 0.2em;
}

.stat-tile .value.stat-value-sm {
	font-size: 1.15em;
	font-weight: 700;
	line-height: 1.4;
}

.stat-tile .stat-sub {
	color: var(--c-text-1);
	font-size: 0.8em;
	margin-top: 0.25em;
}

@media (prefers-reduced-motion: reduce) {
	.stat-tile { transition: none; }
	.stat-tile:hover { transform: none; }
}

/* Cross-panel unification — every admin panel grew its own toast,
   modal, and badge classes independently (.sa-toast, .ae-toast,
   .sessions-toast, .ta-toast, .set-toast; dialog.sa-modal/.ae-modal/
   .sessions-detail-modal; hardcoded rgba(91,141,193,...) "admin" blue
   badges in sitenotes.php and sessions.php). Rather than rename all of
   those (and the JS that references them by name), this file targets
   every existing class name with one shared rule set — same visual
   result, zero markup/JS changes required anywhere.
   ------------------------------------------------------------------ */

.sa-toast, .ae-toast, .sessions-toast, .ta-toast, .set-toast, .ome-toast {
	position: fixed;
	bottom: 24px;
	right: 24px;
	max-width: calc(100vw - 32px);
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-5);
	padding: 0.75em 1.1em;
	border-radius: 8px;
	font-size: 0.9em;
	box-shadow: 0 8px 24px var(--c-shadow-heavy);
	opacity: 0;
	transform: translateY(8px);
	transition: opacity 0.5s ease, transform 0.5s ease;
	pointer-events: none;
	z-index: 50;
}
.sa-toast.visible, .ae-toast.visible, .sessions-toast.visible, .ta-toast.visible, .set-toast.visible, .ome-toast.visible { opacity: 1; transform: translateY(0); }
.sa-toast.error, .ae-toast.error, .sessions-toast.error, .ta-toast.error, .set-toast.error, .ome-toast.error { border-color: var(--c-accent-2); color: var(--c-accent-1); }
.sa-toast.demo, .ae-toast.demo, .sessions-toast.demo, .ta-toast.demo, .set-toast.demo, .ome-toast.demo { border-color: #d9a406; color: #d9a406; }

/* Same cross-panel unification for the "Read-only" topbar badge — two
   genuinely different sizes existed (inline-with-a-toggle-row context
   vs sitting alone in the topbar), kept as two shared rules rather
   than forced into one. */
.cl-readonly-badge, .ome-readonly-badge, .sa-readonly-badge, .me-readonly-badge {
	font-size: 0.6em; font-weight: 700; vertical-align: middle; color: var(--c-text-3);
	border: 1px solid var(--c-border-3); border-radius: 999px; padding: 0.3em 0.7em;
	text-transform: uppercase; letter-spacing: 0.04em; margin-left: 0.5em;
}
.ae-readonly-badge, .sessions-readonly-badge {
	font-size: 0.75em; font-weight: 700; color: var(--c-text-3);
	border: 1px solid var(--c-border-3); border-radius: 999px; padding: 0.3em 0.7em;
	text-transform: uppercase; letter-spacing: 0.04em;
}

/* .ome-btn and .set-btn share the same shape (padding/radius/font/
   transition/hover/disabled/primary) — merged the parts that are
   genuinely identical. Background is NOT one of them, despite looking
   like the same "which var(--c-bg-N)" drift this file's other
   consolidations fixed: .ome-btn always renders on .ome-section
   (var(--c-bg-3)), so var(--c-bg-2) contrasts correctly there. But
   .set-btn's real container is dialog.set-modal (var(--c-bg-2), see
   below) — it needs var(--c-bg-3) to contrast against THAT, not
   against the page's .set-card (which is a different, unrelated
   element .set-btn never actually sits inside). Ruled out as a bug by
   checking actual computed background on live buttons in each panel's
   real container before "fixing" it — first attempt wrongly forced
   both to var(--c-bg-2), which made .set-btn invisible against its
   modal. Base shared rule uses var(--c-bg-2) for the common case;
   settings.php overrides .set-btn back to var(--c-bg-3) locally. */
.ome-btn, .set-btn {
	display: inline-flex; align-items: center; gap: 0.45em;
	border: 1px solid var(--c-border-3); background: var(--c-bg-2); color: var(--c-text-5); font-family: inherit;
	font-size: 0.88em; font-weight: 700; padding: 0.6em 1.1em; border-radius: 999px; cursor: pointer;
	transition: border-color 0.4s ease, box-shadow 0.4s ease, transform 0.3s ease;
}
.ome-btn:hover:not(:disabled), .set-btn:hover:not(:disabled) { border-color: var(--c-accent-2); box-shadow: 0 4px 14px var(--c-shadow-soft); transform: translateY(-1px); }
.ome-btn:disabled, .set-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.ome-btn-primary, .set-btn-primary { background: var(--c-accent-2); border-color: var(--c-accent-2); color: var(--c-text-7); }
.ome-btn-primary:hover:not(:disabled), .set-btn-primary:hover:not(:disabled) { box-shadow: 0 6px 18px rgba(165, 48, 77, 0.4); }

/* Same cross-panel unification for the dark text-input skin —
   main.css's site-wide appearance:none strips native input chrome
   regardless of theme, so every admin panel independently re-applies
   the same fix. ae.php/skilladmin.php/menueditor.php had byte-
   identical text/password rules (skilladmin/menueditor also style
   number inputs and textareas the same way). Selects share the same
   background/border/appearance fix too, but ae.php uses tighter
   padding/font-size than the other two (a plausible intentional
   density choice for its compact filter select, not confirmed as
   drift) — left as a local per-file override rather than guessed at. */
body.ae input[type="text"], body.ae input[type="password"],
body.skilladmin input[type="text"], body.skilladmin input[type="password"], body.skilladmin input[type="number"], body.skilladmin textarea,
body.menueditor input[type="text"] {
	background: var(--c-bg-1);
	color: var(--c-text-5);
	border: 1px solid var(--c-border-3);
	border-radius: 8px;
	padding: 0.6em 0.8em;
	font-size: 0.95em;
	font-family: inherit;
}
body.ae input[type="text"]:focus, body.ae input[type="password"]:focus,
body.skilladmin input[type="text"]:focus, body.skilladmin input[type="password"]:focus, body.skilladmin input[type="number"]:focus, body.skilladmin textarea:focus,
body.menueditor input[type="text"]:focus {
	outline: none;
	border-color: var(--c-accent-2);
	box-shadow: 0 0 0 2px rgba(165, 48, 77, 0.35);
}
body.ae select, body.skilladmin select, body.menueditor select {
	background: var(--c-bg-1);
	color: var(--c-text-5);
	border: 1px solid var(--c-border-3);
	border-radius: 8px;
	padding: 0.6em 0.8em;
	font-size: 0.95em;
	font-family: inherit;
	-webkit-appearance: menulist;
	-moz-appearance: menulist;
	appearance: menulist;
}
body.ae select:focus, body.skilladmin select:focus, body.menueditor select:focus {
	outline: none;
	border-color: var(--c-accent-2);
	box-shadow: 0 0 0 2px rgba(165, 48, 77, 0.35);
}

dialog.sa-modal, dialog.ae-modal, dialog.sessions-detail-modal, dialog.ta-modal, dialog.set-modal {
	border: 1px solid var(--c-border-3);
	border-radius: 14px;
	padding: 1.6em;
	background: var(--c-bg-2);
	color: var(--c-text-5);
	max-width: 480px;
	width: 92vw;
	max-height: 88vh;
	overflow-y: auto;
	overflow-x: hidden;
	/* Explicit (not just inherited from html's global rule in main.css) —
	   a <dialog> renders in the top layer, and some engines don't reliably
	   cascade scrollbar-color/width into that context from an ancestor. */
	scrollbar-color: var(--c-accent-2) var(--c-bg-2);
	scrollbar-width: thin;
}
dialog.sa-modal::-webkit-scrollbar, dialog.ae-modal::-webkit-scrollbar, dialog.sessions-detail-modal::-webkit-scrollbar, dialog.ta-modal::-webkit-scrollbar, dialog.set-modal::-webkit-scrollbar {
	width: 12px;
}
dialog.sa-modal::-webkit-scrollbar-track, dialog.ae-modal::-webkit-scrollbar-track, dialog.sessions-detail-modal::-webkit-scrollbar-track, dialog.ta-modal::-webkit-scrollbar-track, dialog.set-modal::-webkit-scrollbar-track {
	background: var(--c-bg-2);
}
dialog.sa-modal::-webkit-scrollbar-thumb, dialog.ae-modal::-webkit-scrollbar-thumb, dialog.sessions-detail-modal::-webkit-scrollbar-thumb, dialog.ta-modal::-webkit-scrollbar-thumb, dialog.set-modal::-webkit-scrollbar-thumb {
	background-color: var(--c-accent-2);
	border-radius: 999px;
	border: 3px solid var(--c-bg-2);
}
dialog.sa-modal::-webkit-scrollbar-thumb:hover, dialog.ae-modal::-webkit-scrollbar-thumb:hover, dialog.sessions-detail-modal::-webkit-scrollbar-thumb:hover, dialog.ta-modal::-webkit-scrollbar-thumb:hover, dialog.set-modal::-webkit-scrollbar-thumb:hover {
	background-color: var(--c-accent-1);
}
/* Deliberately theme-invariant black, unlike surface/interaction shadows
   elsewhere (--c-shadow-heavy) — a modal backdrop dims whatever's behind
   it regardless of theme, so it doesn't mirror the light-theme ramp the
   way card/button shadows do. */
dialog.sa-modal::backdrop, dialog.ae-modal::backdrop, dialog.sessions-detail-modal::backdrop, dialog.ta-modal::backdrop, dialog.set-modal::backdrop {
	background: rgba(0, 0, 0, 0.6);
	backdrop-filter: blur(2px);
}
.sa-modal h3, .ae-modal h3, .sessions-detail-modal h3, .ta-modal h3, .set-modal h3 { margin: 0 0 1em; font-size: 1.05em; }

/* Cancel/close pill button — identical across SkillAdmin, AccountEditor
   and Sessions' detail modal; also doubles as Sessions' terminate-row
   pill since it's the same base shape with a danger-color modifier. */
.sa-modal-cancel, .ae-modal-cancel, .sessions-detail-close, .sessions-terminate-btn {
	background: none;
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	border-radius: 999px;
	padding: 0.5em 1.1em;
	font-size: 0.85em;
	font-family: inherit;
	cursor: pointer;
	transition: border-color 0.4s ease, color 0.4s ease;
}
.sa-modal-cancel:hover, .sa-modal-cancel:focus-visible,
.ae-modal-cancel:hover, .ae-modal-cancel:focus-visible,
.sessions-detail-close:hover, .sessions-detail-close:focus-visible {
	border-color: var(--c-accent-2);
	color: var(--c-accent-1);
}
.sessions-terminate-btn:hover, .sessions-terminate-btn:focus-visible {
	border-color: var(--c-error);
	color: var(--c-error);
}

/* "This is an internal/admin thing" badge — SiteNotes' visit-log rows
   and Sessions' current-page column both flag admin-panel activity
   with the same neutral pill; centralized here instead of two copies of
   the same rule. Neutral rather than a third accent hue — this is a
   low-stakes informational tag, not a safety/state signal like the
   demo-mode indicator below, so it doesn't earn its own color. */
.admin-page-badge, .sessions-admin-page-badge {
	display: inline-block;
	margin-left: 0.5em;
	padding: 0.15em 0.55em;
	border-radius: 999px;
	background: var(--c-bg-2);
	color: var(--c-text-2);
	font-size: 0.7em;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.03em;
	vertical-align: middle;
}

/* Card hover-lift — applied site-wide to every panel's cards so the
   whole app reads as one thing. Flat at rest (the site's "shadow means
   interaction" rule), a border-color shift + lift + glow on hover,
   same treatment TextAdmin/Settings introduced, extended here to every
   #content.admin-content section without touching each panel's own
   markup. */
#content.admin-content section,
.sa-category, .ae-account, .cvadmin-section {
	transition: border-color 0.4s ease, box-shadow 0.5s ease, transform 0.4s ease;
}
@media (hover: hover) {
	#content.admin-content .dash-card-grid section:hover,
	#content.admin-content .stat-tile:hover,
	.sa-category:hover,
	.ae-account:hover,
	.cvadmin-section:hover {
		border-color: var(--c-accent-2);
		box-shadow: 0 10px 28px var(--c-shadow-heavy), 0 0 0 1px rgba(165, 48, 77, 0.15);
	}
}
@media (prefers-reduced-motion: reduce) {
	#content.admin-content section, .sa-category, .ae-account { transition: none; }
}

/* Responsive
   ------------------------------------------------------------------ */

@media screen and (max-width: 980px) {
	.admin-sidebar-toggle {
		display: inline-flex;
	}
	.admin-sidebar {
		position: fixed;
		left: 0;
		top: 0;
		transform: translateX(-100%);
		transition: transform 0.5s ease;
		box-shadow: 0 0 0 transparent;
	}
	.admin-sidebar.open {
		transform: translateX(0);
		box-shadow: 4px 0 24px var(--c-shadow-heavy);
	}
	.admin-sidebar-backdrop.open {
		display: block;
	}
	.admin-topbar {
		padding-left: 4.2em;
	}
	#content.admin-content {
		padding: 1.4em 1.2em 2.5em;
	}
	#content.admin-content section {
		padding: 1.1em 1.2em;
	}
	#content.admin-content section > header:not(.dash-header) {
		margin: -1.1em -1.2em 1em;
		padding: 0.85em 1.2em;
	}
	@media (prefers-reduced-motion: reduce) {
		.admin-sidebar { transition: none; }
	}
}

/* ==========================================================================
   Shared visit-log/audit-panel module — sitenotes.php + loginaudit.php
   ==========================================================================
   Both pages independently built an expandable-row visitor/attempt log
   (range tabs, date-range form, per-page/pager controls, a base table
   style, and a summary-row/detail-row expand pattern) and each kept its
   own byte-for-byte copy of the CSS in its own <style> block. Consolidated
   here 2026-08-04 after that duplication caused two real bugs (a stale
   .session-pages-table margin override in sitenotes.php, and loginaudit.php's
   visit-detail-row padding rule using a bare class selector that lost to
   #content td's higher-specificity ID rule and never actually applied).
   Two class-name pairs differ page to page but share an identical rule body
   (.visits-expand-all-btn/-icon vs .audit-expand-all-btn/-icon) — merged via
   comma selector rather than renamed, since the JS on each page still
   references its own page's class name and renaming would be a three-way
   JS/CSS/HTML change for zero visual benefit. */
#content table { width: 100%; border-collapse: collapse; }
#content th, #content td { padding: 0.5em 0.75em; text-align: left; font-size: 0.9em; }
#content th { color: var(--c-text-1); border: 1px solid var(--c-border-3); }
#content td { color: var(--c-text-4); border: 1px solid var(--c-border-2); }
#content.content-loading { opacity: 0.55; pointer-events: none; transition: opacity 0.3s ease; }
.captcha-img { max-width: 220px; display: block; margin: 0.5em 0 1.25em 0; border-radius: 8px; }
.website-field { position: absolute; left: -9999px; }
.cf-turnstile { margin: 1em 0 1.5em 0; }
.range-tabs {
	display: flex;
	gap: 10px;
	flex-wrap: wrap;
	background: var(--c-bg-2);
	border-radius: 8px;
	padding: 14px 20px;
	margin: 0 0 2em 0;
}
#content .range-tabs a {
	padding: 0.5em 1.2em;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	text-decoration: none;
	border-radius: 999px;
	font-size: 0.9em;
	transition: background 0.3s, border-color 0.3s, color 0.3s;
}
#content .range-tabs a:hover { background: var(--c-bg-2); border-color: var(--c-border-4); color: var(--c-text-6); }
#content .range-tabs a.active { background: var(--c-accent-fill); border-color: var(--c-accent-fill); color: var(--c-text-7); }
.date-range-form {
	display: flex;
	gap: 16px;
	align-items: flex-end;
	flex-wrap: wrap;
	background: var(--c-bg-2);
	border-radius: 8px;
	padding: 14px 20px;
	margin: 0 0 2em 0;
}
#content .date-range-form label { display: flex; flex-direction: column; gap: 0.3em; font-size: 0.85em; color: var(--c-text-1); margin: 0; }
.date-range-form .clear-dates { color: var(--c-text-3); font-size: 0.9em; }
#content .date-range-form input[type="date"], #content .date-range-form input[type="text"] {
	width: auto;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	border-radius: 2px;
	color: var(--c-text-6);
	padding: 0.4em 0.6em;
	font-size: 0.95em;
	color-scheme: dark;
}
html[data-theme="light"] #content .date-range-form input[type="date"] { color-scheme: light; }
.date-range-form input.invalid { border-color: var(--c-error); }
#content .date-range-form button.button-large,
#content .per-page-row button.button-large {
	margin-top: 0;
	padding: 0.5em 1.2em;
	font-size: 0.9em;
}
.per-page-row { display: flex; align-items: center; gap: 16px; margin: 0 0 1em 0; }
#content .per-page-row label { display: flex; align-items: center; gap: 0.6em; font-size: 0.85em; color: var(--c-text-1); margin: 0; }
#content .per-page-row input {
	width: 5em;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	border-radius: 2px;
	color: var(--c-text-6);
	padding: 0.4em 0.6em;
	font-size: 0.95em;
}
.per-page-row input.invalid { border-color: var(--c-error); }
.pager { display: flex; align-items: center; gap: 12px; margin: 0.75em 0; flex-wrap: wrap; }
.pager a, .pager button, .pager .pager-current, .pager .pager-disabled {
	padding: 0.3em 0.7em;
	border-radius: 999px;
	font-size: 0.9em;
	text-decoration: none;
	border: none;
	font-family: inherit;
	cursor: pointer;
}
/* AJAX-driven pagers (e.g. Traffic Analyzer's Recent visits/Activity
   log, rendered client-side) use real <button> elements instead of
   server-rendered <a href> links — same look either way, just a
   different navigation mechanism underneath. */
.pager a, .pager button:not(.pager-current):not(.pager-disabled) { background: var(--c-bg-3); color: var(--c-text-3); }
.pager .pager-current { background: var(--c-accent-fill); color: var(--c-text-7); cursor: default; }
.pager .pager-disabled { color: var(--c-text-1); cursor: not-allowed; background: transparent; }
.pager button:disabled { opacity: 1; }
.pager-pages { display: flex; gap: 4px; flex-wrap: wrap; }
.country-badge { cursor: default; display: inline-flex; align-items: center; gap: 6px; }
.country-flag { display: inline-block; border-radius: 2px; vertical-align: middle; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1); }
.visit-summary-row { cursor: pointer; }
/* Row-hover color now lives in the sitewide #content table tr:hover td
   rule above (opaque td backgrounds paint over this tr-level one
   regardless, so this was already dead weight, not just redundant). */
.visit-summary-row:focus-visible { outline: 2px solid var(--c-accent-1); outline-offset: -2px; }
.visit-toggle-cell { width: 3em; white-space: nowrap; }
.visit-count-badge {
	display: inline-block;
	min-width: 1.4em;
	margin-left: 0.4em;
	padding: 0.05em 0.4em;
	border-radius: 999px;
	background: var(--c-bg-3);
	color: var(--c-text-1);
	font-size: 0.75em;
	text-align: center;
}
.owner-badge {
	display: inline-block;
	width: 16px;
	height: 16px;
	margin-left: 0.5em;
	border-radius: 3px;
	vertical-align: middle;
}
.owner-toggle {
	display: flex;
	gap: 10px;
	flex-wrap: wrap;
	background: var(--c-bg-2);
	border-radius: 8px;
	padding: 14px 20px;
	margin: 0 0 2em 0;
}
#content .owner-toggle a {
	padding: 0.5em 1.2em;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	text-decoration: none;
	border-radius: 999px;
	font-size: 0.9em;
	transition: background 0.3s, border-color 0.3s, color 0.3s;
}
#content .owner-toggle a:hover { background: var(--c-bg-2); border-color: var(--c-border-4); color: var(--c-text-6); }
#content .owner-toggle a.active { background: var(--c-accent-fill); border-color: var(--c-accent-fill); color: var(--c-text-7); }
.multi-page-toggle {
	display: flex;
	gap: 10px;
	flex-wrap: wrap;
	background: var(--c-bg-2);
	border-radius: 8px;
	padding: 14px 20px;
	margin: 0 0 2em 0;
}
#content .multi-page-toggle a {
	padding: 0.5em 1.2em;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	text-decoration: none;
	border-radius: 999px;
	font-size: 0.9em;
	transition: background 0.3s, border-color 0.3s, color 0.3s;
}
#content .multi-page-toggle a:hover { background: var(--c-bg-2); border-color: var(--c-border-4); color: var(--c-text-6); }
#content .multi-page-toggle a.active { background: var(--c-accent-fill); border-color: var(--c-accent-fill); color: var(--c-text-7); }
/* A bare colored arrow floating alone in its cell read as unfinished,
   especially once the column narrowed down to its real content width
   (see the data-col-default-width fix above) — nothing else in the
   cell to give it visual weight. Housed in a proper circular button
   instead: idle state is a quiet outlined disc, hover adds the site's
   standard accent glow (matching the burgundy hover-glow language used
   elsewhere — logo, buttons), and expanded state flips it to a solid
   filled accent disc with a white chevron, so "this row is open" reads
   at a glance without needing to look at the arrow's rotation. */
.visit-toggle-icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	border-radius: 50%;
	background: var(--c-bg-3);
	border: 1px solid var(--c-border-3);
	color: var(--c-accent-1);
	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
	transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
}
.visit-toggle-icon svg { width: 55%; height: 55%; }
.visit-summary-row:hover .visit-toggle-icon {
	border-color: var(--c-accent-2);
	box-shadow: 0 0 10px rgba(143, 36, 56, 0.55);
	transform: scale(1.1);
}
.visit-summary-row[aria-expanded="true"] .visit-toggle-icon {
	background: var(--c-accent-fill);
	border-color: var(--c-accent-fill);
	color: var(--c-text-7);
	box-shadow: 0 0 12px rgba(109, 15, 34, 0.7);
	transform: rotate(90deg);
}
.visit-summary-row[aria-expanded="true"]:hover .visit-toggle-icon { transform: rotate(90deg) scale(1.1); }
@media (prefers-reduced-motion: reduce) {
	.visit-toggle-icon { transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease; }
	.visit-summary-row[aria-expanded="true"] .visit-toggle-icon,
	.visit-summary-row:hover .visit-toggle-icon,
	.visit-summary-row[aria-expanded="true"]:hover .visit-toggle-icon { transform: none; }
}
/* Row is always rendered (no [hidden]) so it can animate open/closed via
   the grid-template-rows 0fr/1fr trick below — a <tr> can't itself animate
   height cleanly, so the grid lives on the detail row's inner content div
   instead. Both pages' outer table needs the .visit-table class for this
   selector to actually win over the plain #content td rule above (same ID
   tier, needs the extra class) — loginaudit.php was missing this scoping
   entirely until 2026-08-04 and its detail-row padding silently never
   applied. */
/* Rows that only matched the search text via the nested page/URL/
   referrer detail grid (not any session-level field) — both the
   summary row (so it's findable in the collapsed list) and, inside
   the auto-expanded detail grid, the specific page-view row that
   matched. */
#content table tr.search-match:not(:hover) td { background-color: var(--c-accent-fill); }
#content .visit-table tr.visit-detail-row td { padding: 0; }
.visit-detail-row.is-expanded td { border-bottom: 1px solid var(--c-border-2); background: var(--c-bg-2); }
.visit-detail-collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.5s ease; }
.visit-detail-row.is-expanded .visit-detail-collapse { grid-template-rows: 1fr; }
/* Padding belongs on this innermost div, not the grid item
   (.visit-detail-collapse-inner) it sits inside — padding is part of an
   element's own box regardless of overflow:hidden, so if it lived directly
   on the clipped grid item the row could never actually reach 0px, only
   shrink down to "padding-bottom" and stop. */
.visit-detail-collapse-inner { overflow: hidden; min-height: 0; }
.visit-detail-collapse-content { padding: 0 0.75em 1em 0.75em; }
@media (prefers-reduced-motion: reduce) {
	.visit-detail-collapse { transition: none; }
}
/* Bulk expand/collapse-all for a visit-log table — real SVG chevron
   (rotates 180deg via data-state, since this button has no single
   expanded/collapsed state of its own, just a direction), right-aligned,
   direction recomputed fresh on each click. sitenotes.php uses
   .visits-expand-all-btn/-icon, loginaudit.php uses .audit-expand-all-btn/
   -icon — same rule body, different name per page's own JS hook. */
.visits-expand-all-btn, .audit-expand-all-btn {
	display: flex;
	align-items: center;
	gap: 0.4em;
	justify-content: center;
	min-height: 32px;
	margin: 0.6em 0 1em auto;
	width: fit-content;
	background: none;
	border: 1px solid var(--c-border-3);
	color: var(--c-text-3);
	border-radius: 999px;
	padding: 0.35em 0.9em;
	font-size: 0.78em;
	font-weight: 700;
	cursor: pointer;
	transition: border-color 0.4s ease, color 0.4s ease;
}
.visits-expand-all-btn:hover, .visits-expand-all-btn:focus-visible,
.audit-expand-all-btn:hover, .audit-expand-all-btn:focus-visible { border-color: var(--c-accent-2); color: var(--c-accent-1); }
.visits-expand-all-icon, .audit-expand-all-icon { width: 16px; height: 16px; flex: none; transition: transform 0.3s ease; }
.visits-expand-all-btn[data-state="expanded"] .visits-expand-all-icon,
.audit-expand-all-btn[data-state="expanded"] .audit-expand-all-icon { transform: rotate(180deg); }
.visit-detail-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	gap: 0.75em 1.5em;
	margin: 0.75em 0 0 2.25em;
}
.visit-detail-grid dt { color: var(--c-text-1); font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.03em; margin: 0; }
.visit-detail-grid dd { color: var(--c-text-4); font-size: 0.9em; margin: 0.15em 0 0 0; word-break: break-all; }
.visit-detail-grid-full { grid-column: 1 / -1; }
.visit-detail-mono { font-family: ui-monospace, monospace; font-size: 0.82em; }
/* margin-top was 1em on sitenotes.php, 0.5em on loginaudit.php before this
   consolidation — split the difference rather than silently pick one. */
#content table.session-pages-table { width: calc(100% - 2.25em); margin: 0.75em 0 0.5em 2.25em; background: var(--c-bg-2); border-radius: 4px; }
.session-pages-table th, .session-pages-table td { padding: 0.4em 0.65em; font-size: 0.85em; }
.session-pages-table th { color: var(--c-text-1); border: 1px solid var(--c-border-3); }
.session-pages-table td { color: var(--c-text-4); border: 1px solid var(--c-border-1); }
#content form label { display: block; color: var(--c-text-3); font-size: 0.9em; margin: 1em 0 0.35em 0; }
#content form input[type="text"], #content form input[type="password"] {
	width: 100%;
	padding: 0.5em;
	font-size: 1.05em;
	background: var(--c-bg-2);
	border: 1px solid var(--c-border-3);
	color: var(--c-text-5);
}
#content form button { margin-top: 1.5em; }
#content .form-error { color: var(--c-accent-1); margin-top: 1em; }
@media screen and (max-width: 736px) {
	.range-tabs, .owner-toggle, .multi-page-toggle, .date-range-form { padding: 12px 14px; gap: 8px; }
	.stat-grid { grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr)); gap: 10px; }
	.visit-detail-grid { margin-left: 1em; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
	#content table.session-pages-table { width: calc(100% - 1em); margin-left: 1em; }
	.visit-detail-collapse-content { padding: 0 0.5em 1em 0.5em; }
}
@media (pointer: coarse) {
	#content .range-tabs a, #content .owner-toggle a { padding: 0.65em 1.2em; }
	.pager a, .pager button, .pager .pager-current, .pager .pager-disabled { padding: 0.55em 0.85em; }
	.visit-toggle-cell { width: 2.75em; }
}
