@import url('/assets/css/vendor/manrope.min.css');
html, body {font-family:'Manrope',sans-serif; height:100%; margin:0; padding:0; scroll-behavior:smooth;}
body {display:flex; flex-direction:column; overflow:hidden; scrollbar-width:thin;}


/* || VARIABLES */
:root {
	--bs-border-radius: 8px;
	--bs-border-radius-sm: 8px;
	--bs-border-radius-lg: 8px;
	--bs-border-radius-xl: 8px;
	--bs-border-radius-xxl: 8px;
	--bs-dropdown-item-border-radius: 5px;
}
/* … */


/* || BOOTSTRAP OVERRIDES */
.card {border-radius:12px !important; border-width:2px !important;}
.card-header {padding:16px !important;}
.card-footer {border-radius:0 0 12px 12px !important;}
.card-flex {flex:1 1 auto; display:flex; flex-direction:column; min-height:0;}
.modal.fade .modal-dialog {transform:scale(0.95); transition:transform 0.25s ease, opacity 0.15s ease;}
.modal.fade.show .modal-dialog {transform:scale(1);}
.modal.modal-static .modal-dialog {transform:scale(1.02) !important;}
/* … */


/* || DATATABLES OVERRIDES */
.dt-container div {background-color:transparent !important;}
.dt-container {overflow:visible !important;}
div[id$="_wrapper"] .dt-layout-table {margin-top:0 !important;}
/* … */


/* || NAVIGATION */
#nav-progress {
	position: fixed;
	top: 0;
	left: 0;
	height: 3px;
	width: 0%;
	z-index: 9999;
	background: var(--bs-primary);
	transition: width 200ms ease, opacity 300ms ease;
	opacity: 0;
	pointer-events: none;
}
html.full-page-transition-pending #main-content,
html.full-page-transition-pending body > .container.py-5,
html.full-page-transition-pending body > .container {opacity:0;}
html.full-page-transition-needs-sidebar-fade #sidebar {opacity:0;}
/* … */


/* || BACKGROUND */
body {
	background-color: #080808 !important;
	background-image:
		linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
		linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px);
	background-size: 50px 50px;
	background-attachment: fixed;
}
body::before {
	content: '';
	position: fixed;
	bottom: -10%;
	left: 50%;
	transform: translateX(-50%);
	width: 120%;
	height: 70%;
	background: radial-gradient(ellipse at bottom center,
		#0a2a4030 0%,
		#0d6efd08 40%,
		transparent 70%
	);
	pointer-events: none;
	z-index: -1;
}
body::after {
	content: '';
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background:
		radial-gradient(ellipse at 15% 20%, rgba(13, 110, 253, 0.03) 0%, transparent 50%),
		radial-gradient(ellipse at 85% 80%, rgba(80, 30, 140, 0.03) 0%, transparent 50%),
		radial-gradient(circle at 50% 50%, rgba(8, 8, 8, 0) 0%, rgba(8, 8, 8, 0.5) 55%, rgba(8, 8, 8, 0.92) 100%);
	pointer-events: none;
	z-index: -1;
}
/* … */


/* || APEX CHARTS OVERRIDES */
.apexcharts-tooltip {color:var(--bs-dark);}
.apexcharts-text {fill:var(--bs-light);}
/* … */


/* || SCROLLBAR OVERRIDES */
::-webkit-scrollbar {width:5px; height:1px;}
::-webkit-scrollbar-track {background:transparent;}
::-webkit-scrollbar-thumb {background-color:rgb(255, 255, 255, 0.3); border-radius:4px;}
::-webkit-scrollbar-thumb:hover {background-color:rgba(255, 255, 255, 0.6);}
::-webkit-scrollbar-corner {background:transparent;}
/* … */


/* || TABLE COLUMNS */
.dtcol-id{width:64px}
.dtcol-date{width:120px}
.dtcol-datetime{width:200px}
.dtcol-filesize{width:120px}
/* … */


/* || CUSTOM OVERRIDES */
.glass {
	--bs-body-bg: rgba(200, 200, 200, 0.15) !important;
	background-color: rgba(200, 200, 200, 0.05) !important;
	backdrop-filter: blur(3px) !important;
	color: white !important;
	--bs-table-striped-color: rgb(210, 210, 210) !important;
	--bs-table-color: rgb(200, 200, 200) !important;
	--bs-alert-bg: rgba(200, 200, 200, 0.15) !important;
	transition: background 0.3s ease !important;
}
input[type="search"] {user-select:none;}

/* The open list of a <select> is painted by the browser from the options' own
   colours. Inside a .glass panel the control inherits that panel's pale
   --bs-body-bg, and the list opens white on a dark page — so the options carry
   their own colours rather than inheriting one meant for a translucent surface. */
select option,
select optgroup {background-color:#212529 !important; color:#fff !important;}
/* … */




/* || LOADING PLACEHOLDERS */
/* A shimmer stand-in for a value that has not arrived yet. Give it a width so
   the layout does not jump when the real text lands on top of it. */
.skeleton {
	display:inline-block;
	min-width:3rem;
	width:8rem;
	height:1em;
	vertical-align:middle;
	border-radius:6px;
	background-color:rgba(255,255,255,0.07);
	background-image:linear-gradient(90deg,
		rgba(255,255,255,0.00) 0%,
		rgba(255,255,255,0.12) 50%,
		rgba(255,255,255,0.00) 100%);
	background-size:200% 100%;
	background-repeat:no-repeat;
	animation:skeleton-shimmer 1.5s ease-in-out infinite;
}
.skeleton-block {display:block;}
.skeleton-circle {border-radius:50%; width:1em;}
/* Inside a table cell the width is a percentage of the column, so the floor has
   to be low enough not to blow out narrow columns. */
.skeleton-cell {min-width:1.25rem;}
/* Fills an image frame — logo box, avatar, thumbnail — so the placeholder is the
   shape of the thing that is arriving. */
.skeleton-image {width:100%; height:100%; min-width:0; border-radius:inherit;}
/* Placeholder rows should not react to the row hover/pointer styling that real
   rows carry — there is nothing to click yet. */
.skeleton-row td {cursor:default;}

@keyframes skeleton-shimmer {
	0%   {background-position:-100% 0;}
	100% {background-position: 200% 0;}
}

/* A travelling gradient is exactly the kind of thing motion sensitivity reacts
   to — keep the placeholder, drop the movement. */
/* Bootstrap already slows its own spinners under this query. */
@media (prefers-reduced-motion: reduce) {
	.skeleton {animation:none; background-image:none;}
}

/* || SUGGESTIONS (form-modal "suggest" field) */
/* The panel floats over the form instead of sitting in it. `fixed` rather than
   `absolute` on purpose: the modal body is a scroll container, and an absolute
   panel would be clipped by that overflow. Fixed escapes it — but only from
   <body>, because Bootstrap leaves an identity transform on .modal-dialog and a
   transformed ancestor becomes the containing block for a fixed child. So JS
   moves the panel to <body> while it is open and places it against the input's
   viewport rect. z-index clears Bootstrap's modal (1055). */
.fm-suggest {position:relative;}
.fm-suggest-panel {
	position:fixed;
	z-index:1060;
	display:flex;
	flex-direction:column;
	overflow:hidden;
	border:1px solid var(--bs-border-color);
	border-radius:8px;
	/* Opaque, not translucent: this sits on top of form fields, and the text
	   behind it would otherwise show through the rows. */
	background-color:#212529;
	box-shadow:0 10px 28px rgba(0,0,0,0.55);
}
/* JS sets the height in whole rows, so nothing is ever cut mid-row. */
.fm-suggest-list {overflow-y:auto; overscroll-behavior:contain;}
.fm-suggest-option {
	display:flex;
	align-items:baseline;
	gap:0.5rem;
	text-align:left;
	background-color:transparent;
	border-width:0 0 1px 0;
	padding:0.5rem 0.75rem;
}
.fm-suggest-list .list-group-item:last-child {border-bottom-width:0;}
/* The name must stay readable in full; the address is what gives way. Both are
   held to one line so every row is the same height — which is what lets the
   panel end on a row boundary. */
.fm-suggest-main {flex:0 1 auto; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.fm-suggest-sub {
	flex:1 1 auto;
	min-width:0;
	white-space:nowrap;
	overflow:hidden;
	text-overflow:ellipsis;
	font-size:0.875em;
	color:var(--bs-secondary-color);
}
/* Keyboard and pointer highlight are the same thing here — `.active` is set by
   the arrow keys, and hover has to match it or the two disagree on screen. */
.fm-suggest-option:hover,
.fm-suggest-option.active {background-color:rgba(255,255,255,0.09); color:inherit;}
.fm-suggest-option.active {box-shadow:inset 2px 0 0 var(--bs-primary);}
/* Required credit for the data provider — see places.js. */
.fm-suggest-credit {
	flex:0 0 auto;
	padding:0.35rem 0.75rem;
	border-top:1px solid var(--bs-border-color);
	font-size:0.75rem;
	text-align:right;
}
.fm-suggest-credit a {color:var(--bs-secondary-color); text-decoration:none;}
.fm-suggest-credit a:hover {text-decoration:underline;}
/* … */


/* || IMAGE CROP (form-modal "crop" field) */
/* The window is square, so the 1:1 ratio is a property of the layout rather than
   something the user can drag out of shape. */
.fm-crop-viewport {
	position:relative;
	width:100%;
	max-width:18rem;
	aspect-ratio:1 / 1;
	margin-inline:auto;
	overflow:hidden;
	border-radius:12px;
	border:1px solid var(--bs-border-color);
	background-color:rgba(255,255,255,0.04);
	cursor:grab;
	touch-action:none;      /* panning is ours, not the browser's */
	user-select:none;
}
.fm-crop-viewport:active {cursor:grabbing;}
.fm-crop-viewport:focus-visible {outline:2px solid var(--bs-primary); outline-offset:2px;}
.fm-crop-viewport img {
	position:absolute;
	max-width:none;         /* undo the global img fluid rule, or panning fights it */
	user-select:none;
	-webkit-user-drag:none;
}

/* || ATTACHMENT THUMBNAILS (form-modal "files" field, grid presentation) */
/* One row of square tiles. The column count is set inline by JS, which has to know
   it anyway to work out which tile becomes the "N More" one. */
.fm-tiles {display:grid; gap:8px;}
.fm-tile {
	position:relative;
	aspect-ratio:1 / 1;
	overflow:hidden;
	border-radius:10px;
	border:1px solid var(--bs-border-color);
	background-color:rgba(255,255,255,0.04);
}
.fm-tile img {
	position:absolute;
	inset:0;
	width:100%;
	height:100%;
	object-fit:cover;      /* square tiles, whatever shape the photo is */
	display:block;
	/* Held back until the bytes are there, so the skeleton underneath is what shows
	   rather than a half-painted picture. */
	opacity:0;
	transition:opacity 0.2s ease;
	user-select:none;
	-webkit-user-drag:none;
}
.fm-tile-ready img {opacity:1;}
/* Done shimmering — and gone, rather than merely covered, since a PNG with
   transparency would let it carry on showing through. */
.fm-tile-ready .skeleton {display:none;}
/* The whole tile is the button. It sits over the picture rather than around it so
   the focus ring traces the tile's own corners. */
.fm-tile-open {
	position:absolute;
	inset:0;
	display:flex;
	flex-direction:column;
	align-items:center;
	justify-content:center;
	gap:0.15rem;
	padding:0;
	border:0;
	border-radius:inherit;
	background-color:transparent;
	color:#fff;
	cursor:pointer;
}
.fm-tile-open:disabled {cursor:default;}
.fm-tile-open:focus-visible {outline:2px solid var(--bs-primary); outline-offset:-2px;}
.fm-tile-open:hover:not(:disabled) {background-color:rgba(0,0,0,0.25);}
/* A document in a picture field: nothing to show but what kind of thing it is. */
.fm-tile-doc {
	display:flex;
	align-items:center;
	justify-content:center;
	height:100%;
	font-size:1.75rem;
	color:var(--bs-secondary-color);
}
/* Corner controls sit above the tile's own button, or the click that removes a
   picture would open it instead. */
.fm-tile-toggle {
	position:absolute;
	top:4px;
	right:4px;
	z-index:2;
	display:flex;
	align-items:center;
	justify-content:center;
	width:22px;
	height:22px;
	padding:0;
	border:0;
	border-radius:50%;
	background-color:rgba(0,0,0,0.6);
	color:#fff;
	font-size:0.8rem;
	line-height:1;
	opacity:0;
	transition:opacity 0.12s ease, background-color 0.12s ease;
}
/* Hidden until wanted on a pointer, but a touch screen has no hover to reveal it
   with and a keyboard has to be able to reach it. */
.fm-tile:hover .fm-tile-toggle,
.fm-tile-toggle:focus-visible,
.fm-tile-removed .fm-tile-toggle {opacity:1;}
@media (hover: none) {
	.fm-tile-toggle {opacity:1;}
}
.fm-tile-toggle:hover {background-color:var(--bs-danger);}
.fm-tile-removed .fm-tile-toggle:hover {background-color:var(--bs-info);}
.fm-tile-toggle:focus-visible {outline:2px solid #fff; outline-offset:1px;}
/* Struck off, but still there to be put back — the list presentation says the same
   thing with a line through the name. Specific enough to win against the rule that
   reveals a loaded picture, which would otherwise put it back to full strength. */
.fm-tile-ready.fm-tile-removed img {opacity:0.25; filter:grayscale(1);}
.fm-tile-removed {border-style:dashed;}
.fm-tile-flag {
	position:absolute;
	bottom:4px;
	left:4px;
	padding:0.05rem 0.3rem;
	border-radius:5px;
	background-color:var(--bs-success);
	color:#fff;
	font-size:0.65rem;
	line-height:1.4;
}
/* The overflow tile. The picture behind it is dimmed hard enough that the count
   reads cleanly over anything, however bright the photo. */
.fm-tile-ready.fm-tile-more img {opacity:0.45; filter:brightness(0.5);}
.fm-tile-more .fm-tile-open {background-color:rgba(0,0,0,0.35);}
.fm-tile-more .fm-tile-open:hover {background-color:rgba(0,0,0,0.15);}
.fm-tile-more-count {font-size:0.8rem; font-weight:600;}
.fm-tile-more-icon {font-size:1.1rem;}

/* Upload progress, drawn over each picture as it goes up. Over, not instead of: a
   bar that cannot say which photo it belongs to is not worth the room it takes, and
   the whole point of showing this per file is being able to see which is which. */
.fm-tile-progress {
	position:absolute;
	inset:0;
	z-index:3;
	display:flex;
	align-items:center;
	justify-content:center;
	background-color:rgba(0,0,0,0.55);
	/* Invisible to the pointer, so the tile's own button underneath still works —
	   looking at a photo while it uploads costs nothing. */
	pointer-events:none;
	transition:background-color 0.2s ease;
}
/* Stored: out of the way, since the picture is the news now and not the ring. */
.fm-tile-progress[data-fm-state="done"] {background-color:rgba(0,0,0,0.15);}
/* A file already on its way cannot be called back, so the button that would try
   to goes away for as long as it is going. */
.fm-tile-uploading .fm-tile-toggle {display:none;}

/* Registered so it can be transitioned: an unregistered custom property is just a
   token to the engine, and stepping between two conic gradients is not something it
   would know how to do. Progress events arrive in whatever bursts the network gives
   them, and this is what turns those into something that moves evenly. */
@property --fm-progress {
	syntax:'<number>';
	inherits:false;
	initial-value:0;
}
/* The ring: a full disc of two colours with the middle covered over. */
.fm-tile-ring {
	--fm-progress:0;
	display:flex;
	align-items:center;
	justify-content:center;
	width:46px;
	height:46px;
	border-radius:50%;
	background:conic-gradient(
		var(--bs-primary) calc(var(--fm-progress) * 1%),
		rgba(255,255,255,0.25) 0
	);
	transition:--fm-progress 0.2s linear, background-color 0.2s ease;
}
.fm-tile-mark {
	display:flex;
	align-items:center;
	justify-content:center;
	width:36px;
	height:36px;
	border-radius:50%;
	background-color:rgba(0,0,0,0.82);
	color:#fff;
	font-size:0.7rem;
	font-weight:600;
	line-height:1;
	/* Digits of one width, or the number jitters as it counts up. */
	font-variant-numeric:tabular-nums;
}
.fm-tile-mark .bi {font-size:1rem;}
.fm-tile-mark .spinner-border {width:1rem; height:1rem; border-width:2px;}
/* Finished, one way or the other: the ring is whole, and its colour carries which. */
.fm-tile-progress[data-fm-state="done"] .fm-tile-ring {background:var(--bs-success);}
.fm-tile-progress[data-fm-state="done"] .fm-tile-mark {color:var(--bs-success);}
.fm-tile-progress[data-fm-state="failed"] .fm-tile-ring {background:var(--bs-danger);}
.fm-tile-progress[data-fm-state="failed"] .fm-tile-mark {color:var(--bs-danger);}
.fm-tile-progress[data-fm-state="queued"] .fm-tile-mark {color:rgba(255,255,255,0.55);}

/* The overflow tile gets a bar along the bottom instead of a ring: the count it
   already carries sits exactly where the ring would go, and the count is the more
   useful of the two. It covers every file behind the tile at once. */
.fm-tile-bar {
	position:absolute;
	right:8px;
	bottom:8px;
	left:8px;
	z-index:3;
	height:4px;
	overflow:hidden;
	border-radius:2px;
	background-color:rgba(255,255,255,0.25);
	pointer-events:none;
}

/* The same bar for the list presentation, where documents are named rather than
   shown and there is no tile to draw over. */
.fm-file-bar {
	display:inline-block;
	width:5rem;
	height:4px;
	overflow:hidden;
	border-radius:2px;
	background-color:rgba(255,255,255,0.2);
	vertical-align:middle;
}
.fm-tile-bar > span,
.fm-file-bar > span {
	display:block;
	width:0;
	height:100%;
	background-color:var(--bs-primary);
	transition:width 0.2s linear;
}
.fm-tile-bar[data-fm-state="done"] > span,
.fm-file-bar[data-fm-state="done"] > span {background-color:var(--bs-success);}
.fm-tile-bar[data-fm-state="failed"] > span,
.fm-file-bar[data-fm-state="failed"] > span {background-color:var(--bs-danger);}


/* || IMAGE GALLERY (gallery.js) */
/* Fixed to the viewport, and mounted inside the open dialog so Bootstrap counts it
   as part of the modal for focus purposes — see openGallery. .modal-dialog carries
   a transform and would capture `fixed`; the .modal element does not. */
.gal-overlay {
	position:fixed;
	inset:0;
	z-index:1065;
	display:flex;
	flex-direction:column;
	background-color:rgba(0,0,0,0.92);
	opacity:0;
	transition:opacity 0.26s ease;
	overscroll-behavior:contain;
	touch-action:pan-y;
}
.gal-overlay.gal-open {opacity:1;}
.gal-overlay:focus {outline:none;}
.gal-bar {
	flex:0 0 auto;
	display:flex;
	align-items:center;
	gap:0.75rem;
	padding:0.75rem 1rem;
	color:rgba(255,255,255,0.85);
	font-size:0.875rem;
}
.gal-count {flex:0 0 auto; font-variant-numeric:tabular-nums;}
/* The name gives way rather than the counter — one is a fixed few characters, the
   other is a filename of any length at all. */
.gal-name {
	flex:1 1 auto;
	min-width:0;
	white-space:nowrap;
	overflow:hidden;
	text-overflow:ellipsis;
	color:rgba(255,255,255,0.55);
}
.gal-icon-btn {
	display:flex;
	align-items:center;
	justify-content:center;
	padding:0;
	border:0;
	border-radius:50%;
	background-color:rgba(255,255,255,0.08);
	color:#fff;
	transition:background-color 0.12s ease;
}
.gal-icon-btn:hover {background-color:rgba(255,255,255,0.2);}
.gal-icon-btn:focus-visible {outline:2px solid #fff; outline-offset:2px;}
.gal-close, .gal-remove {flex:0 0 auto; width:34px; height:34px;}
.gal-remove:hover {background-color:var(--bs-danger);}
.gal-stage {
	flex:1 1 auto;
	display:flex;
	align-items:center;
	gap:0.5rem;
	padding:0 0.5rem;
	min-height:0;               /* or the frame refuses to shrink and the strip is pushed off */
}
.gal-nav {width:44px; height:44px; flex:0 0 auto; font-size:1.1rem;}
.gal-frame {
	position:relative;
	flex:1 1 auto;
	height:100%;
	min-width:0;
	display:flex;
	align-items:center;
	justify-content:center;
}
/* `inset` with auto margins rather than leaning on the flex container to place it —
   the static position of an absolute child of a flexbox is not somewhere to be
   clever. */
.gal-layer {
	position:absolute;
	inset:0;
	margin:auto;
	max-width:100%;
	max-height:100%;
	object-fit:contain;
	border-radius:6px;
	opacity:0;
	transition:opacity 0.26s ease, transform 0.26s ease;
	user-select:none;
	-webkit-user-drag:none;
}
/* Enters offset towards the side it came from, then settles into place. Compounded
   with .gal-layer so the settled state cannot lose to the base rule on source order. */
.gal-layer.gal-from-right {transform:translateX(24px);}
.gal-layer.gal-from-left {transform:translateX(-24px);}
.gal-layer.gal-settled {opacity:1; transform:translateX(0);}
.gal-spinner {
	position:absolute;
	width:2rem;
	height:2rem;
	opacity:0;
	pointer-events:none;
	transition:opacity 0.15s ease;
}
.gal-spinner-on {opacity:0.7;}
/* Struck off, the same treatment its tile gets — and compounded with .gal-settled,
   which would otherwise put it back to full strength on source order alone. */
.gal-frame-removed .gal-layer.gal-settled {opacity:0.3; filter:grayscale(1);}
/* Says what the dimming means. "Removed" while the picture is plainly still on
   screen needs the explaining. */
.gal-flag {
	position:absolute;
	bottom:0.5rem;
	padding:0.15rem 0.6rem;
	border-radius:999px;
	background-color:var(--bs-danger);
	color:#fff;
	font-size:0.75rem;
	line-height:1.5;
	opacity:0;
	transition:opacity 0.15s ease;
	pointer-events:none;
}
.gal-frame-removed .gal-flag {opacity:1;}
.gal-strip {
	flex:0 0 auto;
	display:flex;
	gap:6px;
	padding:0.75rem 1rem;
	overflow-x:auto;
	overscroll-behavior-x:contain;
	scrollbar-width:thin;
}
.gal-thumb {
	position:relative;           /* the picture sits over the skeleton, not after it */
	flex:0 0 auto;
	width:56px;
	height:56px;
	padding:0;
	overflow:hidden;
	border:2px solid transparent;
	border-radius:8px;
	background-color:rgba(255,255,255,0.06);
	opacity:0.5;
	transition:opacity 0.12s ease, border-color 0.12s ease;
}
.gal-thumb img {
	position:absolute;
	inset:0;
	width:100%;
	height:100%;
	object-fit:cover;
	display:block;
	/* Held back until the bytes are there, so the shimmer underneath is what shows
	   rather than a half-painted thumbnail. */
	opacity:0;
	transition:opacity 0.2s ease;
}
/* Block, or the inline-block placeholder sits on a baseline and leaves the
   thumbnail's own background showing as a strip beneath it. */
.gal-thumb .skeleton {display:block;}
.gal-thumb-ready img {opacity:1;}
/* Gone rather than merely covered — a PNG with transparency would let it carry
   on shimmering through the picture. */
.gal-thumb-ready .skeleton {display:none;}
.gal-thumb:hover {opacity:0.85;}
.gal-thumb-on {opacity:1; border-color:var(--bs-primary);}
/* Marked for removal, so the strip shows which without having to walk them. */
.gal-thumb-off img {filter:grayscale(1); opacity:0.4;}
.gal-thumb-off {border-color:var(--bs-danger);}
.gal-thumb:focus-visible {outline:2px solid #fff; outline-offset:2px;}
/* Narrow screens: the side arrows cost width the picture needs more, and a swipe
   does the same job. */
@media (max-width: 575.98px) {
	.gal-nav {display:none;}
	.gal-thumb {width:44px; height:44px;}
}
/* … */


/* A clickable image frame — the logo box on the organisation page. */
.image-drop {cursor:pointer; transition:border-color 0.15s ease, background-color 0.15s ease;}
.image-drop:hover, .image-drop:focus-visible {border-color:var(--bs-primary) !important;}
.image-drop:focus-visible {outline:2px solid var(--bs-primary); outline-offset:2px;}
/* … */


/* A row whose record is still being fetched. Further clicks are already ignored
   in JS; this is what says so. */
.clickable-row.row-busy {cursor:progress; opacity:0.55;}
.clickable-row.row-busy td {pointer-events:none;}


/* Centred spinner for a panel or table body that is still filling. */
.loading-pane {
	display:flex;
	align-items:center;
	justify-content:center;
	gap:0.6rem;
	padding:1.75rem 1rem;
	color:var(--bs-secondary-color);
}
/* … */
