diff --git a/layouts/partials/headers.html b/layouts/partials/headers.html
index 6587e2fd..9bcedbb2 100644
--- a/layouts/partials/headers.html
+++ b/layouts/partials/headers.html
@@ -35,6 +35,7 @@
+
{{ `
diff --git a/static/css/custom.css b/static/css/custom.css
index 9b5b449f..29ccffeb 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -534,6 +534,175 @@ h3.text-uppercase {
padding: 12px 10px;
}
+/* Dense data tables */
+.data-table {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-md);
+ box-shadow: 0 8px 28px rgba(23, 38, 51, 0.08);
+ margin: 24px 0 36px;
+ overflow: hidden;
+}
+
+.data-table__toolbar {
+ align-items: center;
+ background: #f8fafb;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ gap: 18px;
+ justify-content: space-between;
+ padding: 12px 16px;
+}
+
+.data-table__search {
+ flex: 0 1 340px;
+ margin: 0;
+ position: relative;
+}
+
+.data-table__search i {
+ color: #71838d;
+ left: 13px;
+ pointer-events: none;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.data-table__input {
+ background: #fff;
+ border: 1px solid #cbd8de;
+ border-radius: var(--radius-sm);
+ color: var(--ink);
+ font-size: 14px;
+ height: 40px;
+ padding: 8px 13px 8px 38px;
+ width: 100%;
+}
+
+.data-table__input:focus {
+ border-color: var(--primary-accent);
+ box-shadow: 0 0 0 3px var(--form-shadow);
+ outline: 0;
+}
+
+.data-table__count {
+ color: var(--ink-muted);
+ flex: none;
+ font-size: 13px;
+ font-weight: 650;
+}
+
+.data-table__scroll {
+ max-width: 100%;
+ overflow: auto;
+ overscroll-behavior: contain;
+ scrollbar-color: #aebfc6 #edf2f4;
+ scrollbar-width: thin;
+}
+
+.data-table--large .data-table__scroll {
+ max-height: min(68vh, 720px);
+}
+
+#content .data-table__table {
+ border-collapse: separate;
+ border-spacing: 0;
+ margin: 0;
+ min-width: 760px;
+ width: 100%;
+}
+
+#content .data-table__table th,
+#content .data-table__table td {
+ background-clip: padding-box;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ padding: 13px 16px;
+ vertical-align: middle;
+}
+
+#content .data-table__table thead th {
+ background: #eaf5f7;
+ border-bottom: 2px solid var(--primary-accent);
+ color: #173b47;
+ font-size: 14px;
+ font-weight: 750;
+ position: sticky;
+ top: 0;
+ white-space: normal;
+ z-index: 2;
+}
+
+#content .data-table__table tbody tr:nth-child(even) td {
+ background-color: #f8fafb;
+}
+
+#content .data-table__table tbody tr:hover td {
+ background-color: #eaf7f9;
+}
+
+#content .data-table__table th:first-child,
+#content .data-table__table td:first-child {
+ box-shadow: 1px 0 0 var(--line);
+ left: 0;
+ min-width: 210px;
+ position: sticky;
+ text-align: left;
+ z-index: 1;
+}
+
+#content .data-table__table thead th:first-child {
+ background: #deeff2;
+ z-index: 3;
+}
+
+#content .data-table__table tbody td:not(:first-child) {
+ min-width: 118px;
+ text-align: center;
+}
+
+#content .data-table__table tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+#content .data-table__table tr[hidden] {
+ display: none;
+}
+
+.data-table__scroll:focus-visible {
+ box-shadow: inset 0 0 0 3px var(--form-shadow);
+ outline: 0;
+}
+
+@media (max-width: 767px) {
+ .data-table {
+ border-radius: var(--radius-sm);
+ margin-left: -8px;
+ margin-right: -8px;
+ }
+
+ .data-table__toolbar {
+ align-items: stretch;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .data-table__search {
+ flex-basis: auto;
+ }
+
+ #content .data-table__table th,
+ #content .data-table__table td {
+ padding: 11px 13px;
+ }
+
+ #content .data-table__table th:first-child,
+ #content .data-table__table td:first-child {
+ min-width: 180px;
+ }
+}
+
#content blockquote {
background: var(--surface-soft);
border-left: 4px solid var(--primary-accent);
diff --git a/static/js/table-enhancements.js b/static/js/table-enhancements.js
new file mode 100644
index 00000000..b036efd9
--- /dev/null
+++ b/static/js/table-enhancements.js
@@ -0,0 +1,87 @@
+(function () {
+ "use strict";
+
+ function getTableLabel(table, index) {
+ var heading = table.previousElementSibling;
+
+ while (heading && !/^H[2-6]$/.test(heading.tagName)) {
+ heading = heading.previousElementSibling;
+ }
+
+ return heading ? heading.textContent.trim() : "Data table " + (index + 1);
+ }
+
+ function enhanceTable(table, index) {
+ if (table.closest(".data-table") || table.matches(".table-plain")) {
+ return;
+ }
+
+ var rows = Array.prototype.slice.call(table.tBodies).reduce(function (allRows, body) {
+ return allRows.concat(Array.prototype.slice.call(body.rows));
+ }, []);
+ var label = getTableLabel(table, index);
+ var wrapper = document.createElement("div");
+ var scroller = document.createElement("div");
+
+ wrapper.className = "data-table" + (rows.length >= 15 ? " data-table--large" : "");
+ scroller.className = "data-table__scroll";
+ scroller.tabIndex = 0;
+ scroller.setAttribute("role", "region");
+ scroller.setAttribute("aria-label", label + ", scrollable table");
+
+ table.parentNode.insertBefore(wrapper, table);
+ wrapper.appendChild(scroller);
+ scroller.appendChild(table);
+ table.classList.add("data-table__table");
+
+ if (rows.length < 15) {
+ return;
+ }
+
+ var toolbar = document.createElement("div");
+ var searchGroup = document.createElement("label");
+ var searchIcon = document.createElement("i");
+ var search = document.createElement("input");
+ var count = document.createElement("span");
+ var updateCount = function (visible) {
+ count.textContent = visible + " of " + rows.length + " entries";
+ };
+
+ toolbar.className = "data-table__toolbar";
+ searchGroup.className = "data-table__search";
+ searchIcon.className = "fas fa-search";
+ searchIcon.setAttribute("aria-hidden", "true");
+ search.type = "search";
+ search.className = "data-table__input";
+ search.placeholder = "Filter tableā¦";
+ search.setAttribute("aria-label", "Filter " + label);
+ count.className = "data-table__count";
+ count.setAttribute("aria-live", "polite");
+
+ search.addEventListener("input", function () {
+ var query = search.value.trim().toLocaleLowerCase();
+ var visible = 0;
+
+ rows.forEach(function (row) {
+ var matches = !query || row.textContent.toLocaleLowerCase().indexOf(query) !== -1;
+ row.hidden = !matches;
+ if (matches) {
+ visible += 1;
+ }
+ });
+
+ updateCount(visible);
+ });
+
+ searchGroup.appendChild(searchIcon);
+ searchGroup.appendChild(search);
+ toolbar.appendChild(searchGroup);
+ toolbar.appendChild(count);
+ wrapper.insertBefore(toolbar, scroller);
+ updateCount(rows.length);
+ }
+
+ document.addEventListener("DOMContentLoaded", function () {
+ Array.prototype.forEach.call(document.querySelectorAll("#content table"), enhanceTable);
+ });
+}());