feat: make the dashboard readable at a glance

The dashboard showed the anonymize date under "Expired", a dash in the
Expires column for 54 of 60 rows, raw status codes such as
branch_not_found, and a chip reading "REMOVED" that actually meant
"removed items are hidden". Muted text sat at 3.5:1 contrast, keyboard
focus was invisible, numbers were left-aligned, and a legacy record
without an identifier rendered as an empty link to /pr/undefined/.

This rewrites the dashboard template and controller and tightens the
theme tokens:

- Status sub-line is labelled ("anonymized on Sep 6, 2026"); Expires
  shows Never, the date, or "Expired on <date>".
- Conference folded into the name cell as a tag; Views right-aligned;
  every column header is a keyboard-sortable button with aria-sort.
- Sort/Status buttons show their state; chips read "Hiding Removed";
  result count and Clear filters; empty state with a way out.
- Status filter covers Ready, In progress, Error, Expired, Removed.
  Stuck downloads say so; codes map to sentences; statuses to labels.
- Lists load in parallel and merge once behind a skeleton; broken
  records are flagged instead of linking nowhere.
- Actions menu: View, View page, Edit, Force update, divider, Remove,
  all as buttons rather than href="#" anchors.
- Theme: warm dark canvas, muted text >= 4.5:1 in both modes, semantic
  status tokens, 11/13/14/16px type scale, mono only for identifiers,
  6px/10px/pill radii, :focus-visible ring, quota bars in ink until a
  limit is near, unlimited shown as such.
- Mobile: no 9px text, filter row wraps in two lines, meta on one line.

Adds scripts/dev-mock.js to serve the dashboard with fixture data and
test/dashboard-ui.test.js covering the filters, template invariants and
token contrast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
tdurieux
2026-09-08 20:20:17 +02:00
co-authored by Claude Fable 5.1
parent 9f00641529
commit 8e9b5b9a45
8 changed files with 1157 additions and 325 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+332 -67
View File
@@ -3,10 +3,11 @@
Serif display (Instrument Serif), Inter body, JetBrains Mono. */
.dark-mode {
/* Warm charcoal — softened from pure black */
--canvas-bg-color: #1f1f1f;
/* Warm ink canvas — same temperature as the light paper palette so
cards (#25231F) and borders (#33302B) sit on a matching base. */
--canvas-bg-color: #1B1916;
--sidebar-bg-color: #1A1918;
--main-bg-color: #1f1f1f;
--main-bg-color: #1B1916;
--header-bg-color: #1A1918;
--hover-bg-color: rgba(255, 255, 255, 0.05);
@@ -29,9 +30,20 @@
--accent: #A7B2FF;
--accent-soft: rgba(167, 178, 255, 0.12);
--ink-muted: #8A857C;
/* --ink-muted is used for small text: 6.5:1 on the canvas.
--ink-faint is for non-text (icons, rules, dots), never for copy. */
--ink-muted: #A39D92;
--ink-faint: #8A857C;
--ink-soft: #B5AFA2;
/* Semantic status colours (text/dot/border on canvas ≥ 4.5:1) */
--status-ready: #5FB27A;
--status-progress: #E0A94A;
--status-error: #F08A82;
--status-inactive: #8A857C;
--status-error-soft: rgba(240, 138, 130, 0.08);
--status-error-soft-hover: rgba(240, 138, 130, 0.14);
--admin-nav-bg: #1A1918;
--admin-nav-active: rgba(167, 178, 255, 0.12);
--admin-stat-bg: #25231F;
@@ -70,12 +82,27 @@ body {
--accent: #3B4AD6;
--accent-soft: #EEF0FF;
--ink-muted: #8A857C;
/* --ink-muted is used for small text: 5.1:1 on the paper canvas.
--ink-faint is for non-text (icons, rules, dots), never for copy. */
--ink-muted: #6F6A61;
--ink-faint: #8A857C;
--ink-soft: #4A4641;
--paper-bg-alt: #F3F1EC;
--paper-card: #FFFFFF;
--highlight: #FFF3C4;
/* Semantic status colours (text/dot/border on canvas ≥ 4.5:1) */
--status-ready: #2F7A44;
--status-progress: #946512;
--status-error: #B42318;
--status-inactive: #8A857C;
--status-error-soft: rgba(180, 35, 24, 0.04);
--status-error-soft-hover: rgba(180, 35, 24, 0.08);
/* Two-step radius system: controls and surfaces. Chips/badges are pills. */
--radius-control: 6px;
--radius-surface: 10px;
--font-serif: 'Instrument Serif', 'Iowan Old Style', Georgia, serif;
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -106,19 +133,31 @@ body {
}
}
/* Keyboard focus: a visible 2px accent ring only when focus came from the
keyboard (:focus-visible); pointer clicks get no ring. The ring is ≥ 3:1
against both canvases (indigo 6.4:1 light, periwinkle 8.7:1 dark). */
.btn:focus,
.btn:active {
outline: none !important;
box-shadow: none;
}
textarea,
select,
input,
button {
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.btn:focus-visible,
.form-control:focus-visible,
.dropdown-item:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
box-shadow: none;
}
input:hover,
input:active {
box-shadow: none !important;
@@ -1898,9 +1937,13 @@ code {
opacity: 0.7;
cursor: pointer;
padding: 0;
margin-left: 2px;
/* 24px tap target; negative margin keeps the chip visually compact */
min-width: 24px;
min-height: 24px;
margin: -6px -6px -6px 0;
font-size: 1rem;
line-height: 1;
border-radius: 999px;
}
.filter-chip-close:hover {
@@ -2025,8 +2068,8 @@ code {
.type-badge {
font-family: var(--font-mono);
letter-spacing: 0.06em;
padding: 2px 7px;
border-radius: 3px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--border-color);
background: var(--paper-bg-alt, transparent);
}
@@ -3898,7 +3941,7 @@ code {
.paper-crumbs {
font-family: var(--font-mono);
font-size: 11.5px;
font-size: 11px;
color: var(--ink-muted);
letter-spacing: 0.02em;
}
@@ -4161,6 +4204,12 @@ textarea::selection {
height: 38px;
background: var(--paper-card);
}
.paper-page .dashboard-filter-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.paper-page .dashboard-filter-row .dropdown-toggle,
.paper-page .dashboard-filter-row .btn-group .btn {
background: var(--paper-card);
@@ -4170,12 +4219,30 @@ textarea::selection {
padding: 0 14px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
border-radius: var(--radius-control, 6px);
display: inline-flex;
align-items: center;
gap: 6px;
line-height: 1;
}
/* "Sort · Name ↓" style toggles: the label is muted, the value is ink so the
current state reads at a glance. */
.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label {
color: var(--ink-muted);
font-weight: 500;
}
.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-label::after {
content: "·";
margin-left: 6px;
color: var(--ink-faint);
}
.paper-page .dashboard-filter-row .dropdown-toggle .filter-btn-value {
font-weight: 500;
}
.paper-page .dashboard-filter-row .dropdown-toggle .fas {
font-size: 11px;
color: var(--ink-muted);
}
.paper-page .dashboard-filter-row .dropdown-toggle:hover,
.paper-page .dashboard-filter-row .btn-group .btn:hover {
background: var(--hover-bg-color);
@@ -4222,20 +4289,42 @@ textarea::selection {
}
.paper-page .quota-value {
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-soft);
font-size: 13px;
font-variant-numeric: tabular-nums;
color: var(--color);
}
.paper-page .quota-progress {
.paper-page .quota-value .quota-unlimited-tag {
font-family: var(--font-sans);
font-size: 12px;
color: var(--ink-muted);
margin-left: 4px;
}
/* Quota bars: ink by default, colour only when a limit is nearly reached.
Unlimited quotas render an empty track (no fill), never a full green bar. */
.paper-page .quota-track {
height: 6px;
background: var(--paper-bg-alt);
border: 1px solid var(--border-color);
border-radius: 999px;
margin-bottom: 0;
overflow: hidden;
width: 100%;
min-width: 0;
}
.paper-page .quota-progress .progress-bar {
.paper-page .quota-track .quota-fill {
height: 100%;
min-width: 2px;
border-radius: 999px;
background: var(--color);
transition: width 0.3s ease;
}
.paper-page .quota-track.quota-warn .quota-fill { background: var(--status-progress); }
.paper-page .quota-track.quota-danger .quota-fill { background: var(--status-error); }
.paper-page .quota-track.quota-unlimited {
background: repeating-linear-gradient(
-45deg,
var(--paper-bg-alt) 0 4px,
var(--border-soft, var(--border-color)) 4px 6px
);
}
/* Dashboard repo/PR list refinements */
@@ -4381,12 +4470,22 @@ textarea::selection {
grid-template-columns: 28px minmax(280px, 2.4fr) 90px 120px 100px 52px !important;
gap: 12px !important;
}
/* Dashboard: conference folded into the name cell, numbers right-aligned. */
.paper-table.paper-table-dashboard .paper-table-head,
.paper-table.paper-table-dashboard .paper-table-row {
grid-template-columns: minmax(320px, 3fr) 190px 90px 170px 52px;
gap: 20px;
/* Two-line rows land at ~60px, close to Carbon's "large" density. */
padding: 8px 4px;
}
.paper-table.paper-table-dashboard .cell-anon .anon-text { gap: 2px; }
.paper-table.paper-table-dashboard .cell-anon .repo-name { padding: 0; }
.paper-table .paper-table-head {
background: transparent;
border-bottom: 1px solid var(--border-color);
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: 0.14em;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-muted);
padding-top: 8px;
@@ -4397,13 +4496,25 @@ textarea::selection {
user-select: none;
display: inline-flex;
align-items: center;
gap: 4px;
gap: 6px;
/* Sortable headers are real <button>s for keyboard users. */
background: none;
border: none;
padding: 2px 0;
margin: 0;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
color: inherit;
border-radius: 4px;
}
.paper-table .paper-table-head .sortable:hover { color: var(--color); }
.paper-table .paper-table-head .sortable.active { color: var(--color); }
.paper-table .paper-table-head .sortable i { font-size: 10px; }
/* Quantitative columns are right-aligned so digits line up. */
.paper-table .paper-table-head .num,
.paper-table .paper-table-row .num { text-align: left; }
.paper-table .paper-table-row .num { text-align: right; }
.paper-table .paper-table-head .num .sortable { flex-direction: row-reverse; }
.paper-table .paper-table-row {
border-top: 1px solid var(--border-soft, var(--border-color));
font-size: 14px;
@@ -4412,6 +4523,8 @@ textarea::selection {
.paper-table .paper-table-row:hover {
background: var(--hover-bg-color);
}
/* Rows that open a view on click get a pointer so hover means something. */
.paper-table .paper-table-row.row-clickable { cursor: pointer; }
/* Dim inactive rows through their cells rather than the row itself. Applying
opacity to the row creates a stacking context that traps the actions
dropdown menu (z-index) beneath the rows rendered after it, making its
@@ -4419,20 +4532,16 @@ textarea::selection {
interactive. */
.paper-table .paper-table-row.repo-inactive > *:not(.cell-actions) { opacity: 0.55; }
.paper-table .paper-table-row.repo-error {
background: rgba(180, 35, 24, 0.04);
border-left: 2px solid #B42318;
background: var(--status-error-soft);
border-left: 2px solid var(--status-error);
padding-left: 6px;
}
.paper-table .paper-table-row.repo-error:hover { background: rgba(180, 35, 24, 0.08); }
.dark-mode .paper-table .paper-table-row.repo-error { background: rgba(240, 138, 130, 0.05); border-left-color: #F08A82; }
.dark-mode .paper-table .paper-table-row.repo-error:hover { background: rgba(240, 138, 130, 0.1); }
.paper-table .paper-table-row.repo-error:hover { background: var(--status-error-soft-hover); }
.paper-table .cell-status .status-sub-error,
.cell-status .status-sub.status-sub-error {
color: #B42318;
font-family: var(--font-mono);
color: var(--status-error);
}
.dark-mode .paper-table .cell-status .status-sub-error,
.dark-mode .cell-status .status-sub.status-sub-error { color: #F08A82; }
.paper-table .cell-status .status-sub-warn { color: var(--status-progress); }
.paper-table .cell-anon {
display: flex;
align-items: center;
@@ -4444,8 +4553,16 @@ textarea::selection {
min-width: 52px;
justify-content: center;
}
.paper-table .cell-anon .anon-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.paper-table .cell-anon .anon-text { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.paper-table .cell-anon .anon-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.paper-table .cell-anon .anon-title .type-badge { margin-left: 0; }
.paper-table .cell-anon .repo-name {
/* Identifiers stay mono; everything human-readable below is sans. */
font-family: var(--font-mono);
font-size: 14px;
color: var(--color);
@@ -4455,45 +4572,174 @@ textarea::selection {
white-space: nowrap;
display: block;
max-width: 100%;
padding: 2px 0;
}
.paper-table .cell-anon .repo-name-static { color: var(--ink-muted); font-style: italic; }
.paper-table .cell-anon .anon-sub {
font-size: 12px;
font-family: var(--font-sans);
font-size: 13px;
color: var(--ink-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.paper-table .cell-anon .anon-sub .anon-source {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The conference tag keeps a readable share of the line; the source
truncates first since it is repeated in the name and in the tooltip. */
.paper-table .cell-anon .anon-sub .conf-tag {
flex: 0 1 auto;
min-width: 72px;
max-width: 50%;
}
.paper-table .cell-anon .anon-sub a {
color: var(--ink-muted);
border-bottom: 1px dotted var(--border-color);
/* Taller hit area without changing the line box */
padding: 4px 0;
margin: -4px 0;
}
.paper-table .cell-anon .anon-sub a { color: var(--ink-muted); border-bottom: 1px dotted var(--border-color); }
.paper-table .cell-anon .anon-sub a:hover { color: var(--color); }
.paper-table .cell-anon .anon-sub .commit-hash { font-family: var(--font-mono); font-size: 12px; }
/* Conference shown as a small tag inside the name cell. `.cell-conf` keeps
the clipping rules so a long conference string never bleeds. */
.paper-table .cell-conf {
font-family: var(--font-mono);
font-size: 13px;
color: var(--color);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-muted);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.paper-table .cell-conf span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.paper-table .cell-status { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; font-size: 14px; color: var(--color); }
.paper-table .cell-status { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; font-size: 14px; color: var(--color); min-width: 0; }
.paper-table .cell-status .status-line { display: inline-flex; align-items: center; gap: 8px; }
.paper-table .cell-status .status-sub { flex-basis: 100%; font-size: 11px; line-height: 1.2; color: var(--ink-muted); }
.paper-table .cell-status .status-sub {
flex-basis: 100%;
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.3;
color: var(--ink-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.paper-table .cell-views { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--color); }
.paper-table .cell-expires { font-size: 13px; color: var(--ink-soft); }
.paper-table .empty-dash { color: var(--ink-muted); opacity: 0.5; }
.paper-table .cell-expires { font-family: var(--font-sans); font-size: 13px; color: var(--ink-soft); }
.paper-table .cell-expires .expires-never { color: var(--ink-muted); }
.paper-table .cell-expires .expires-past { color: var(--ink-muted); }
.paper-table .empty-dash { color: var(--ink-faint); }
.paper-table .cell-actions { display: flex; justify-content: flex-end; }
/* Status dots use the semantic tokens so they stay legible in both themes.
Old status-* class names are kept as aliases for other templates. */
.status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
background: var(--ink-muted);
flex-shrink: 0;
background: var(--status-inactive);
}
.status-dot.status-ready { background: #2F7A44; }
.status-dot.status-error { background: #B42318; }
.status-dot.status-preparing { background: #C48A2E; }
.status-dot.status-removed { background: #9A8F7B; }
.status-dot.status-ready { background: var(--status-ready); }
.status-dot.status-error { background: var(--status-error); }
.status-dot.status-progress,
.status-dot.status-preparing { background: var(--status-progress); }
.status-dot.status-expired,
.status-dot.status-removed { background: var(--status-inactive); }
/* ---- Dashboard extras ---------------------------------------------- */
/* Room for the fixed Ko-fi pill so it never covers the last row's actions. */
.dashboard-page { padding-bottom: 96px; }
.dashboard-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
font-size: 13px;
color: var(--ink-muted);
}
.dashboard-meta .dashboard-count { font-variant-numeric: tabular-nums; }
.btn-link-inline {
background: none;
border: none;
padding: 2px 4px;
margin: -2px 0;
font: inherit;
color: var(--link-color);
cursor: pointer;
border-radius: 4px;
}
.btn-link-inline:hover { color: var(--link-hover-color); text-decoration: underline; }
.btn.btn-outline-ink {
background: transparent;
color: var(--color);
border: 1px solid var(--border-color);
font-weight: 500;
border-radius: var(--radius-control, 6px);
padding: 8px 14px;
}
.btn.btn-outline-ink:hover { background: var(--hover-bg-color); color: var(--color); border-color: var(--color); }
.paper-page .dropdown-menu .dropdown-item-danger,
.paper-page .dropdown-menu .dropdown-item-danger i { color: var(--status-error); }
.paper-page .dropdown-menu .dropdown-item-danger:hover,
.paper-page .dropdown-menu .dropdown-item-danger:focus { background: var(--status-error-soft-hover); color: var(--status-error); }
.paper-page .dropdown-menu button.dropdown-item { width: 100%; text-align: left; background: none; border: none; }
.paper-page .dropdown-menu .dropdown-divider { border-top-color: var(--border-soft, var(--border-color)); margin: 4px 0; }
/* Loading skeleton */
.paper-table .paper-table-skeleton { pointer-events: none; }
.skeleton {
display: block;
background: var(--paper-bg-alt);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.skeleton::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, var(--hover-bg-color), transparent);
animation: skeleton-shimmer 1.4s infinite;
}
.skeleton-line { height: 12px; }
.skeleton-line + .skeleton-line { margin-top: 8px; }
.skeleton-line-sm { height: 10px; }
.skeleton-badge { width: 52px; height: 22px; border-radius: 999px; flex-shrink: 0; }
@keyframes skeleton-shimmer { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { animation: none; } }
/* Empty state with a way out */
.paper-table .paper-table-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 56px 20px;
color: var(--ink-muted);
text-align: center;
border-top: 1px solid var(--border-soft, var(--border-color));
}
.paper-table .paper-table-empty i { font-size: 1.6rem; color: var(--ink-faint); }
.paper-table .paper-table-empty-actions { display: flex; gap: 8px; margin-top: 8px; }
.bulk-bar {
display: flex;
@@ -5297,13 +5543,18 @@ textarea::selection {
.paper-table .cell-conf,
.paper-table .cell-expires {
order: 3;
font-size: 12px;
font-size: 13px;
color: var(--ink-muted);
display: inline-flex;
align-items: center;
gap: 6px;
text-align: left;
}
/* Status, date, views and expiry sit on one meta line with a common
baseline; the sub-text is inline instead of wrapping under the word. */
.paper-table .cell-status { flex-wrap: nowrap; gap: 6px; min-width: 0; }
.paper-table .cell-status .status-sub { flex-basis: auto; font-size: 13px; line-height: inherit; }
.paper-table .cell-status .status-sub::before { content: "·"; margin-right: 6px; color: var(--ink-faint); }
.paper-table .cell-conf {
max-width: 100%;
overflow: hidden;
@@ -5311,6 +5562,9 @@ textarea::selection {
white-space: nowrap;
}
.paper-table .cell-views.num { text-align: left; }
.paper-table .paper-table-skeleton .cell-status,
.paper-table .paper-table-skeleton .cell-views,
.paper-table .paper-table-skeleton .cell-expires { flex: 1 1 30%; }
.paper-table .cell-views::before {
content: '\f06e';
font-family: 'Font Awesome 5 Free';
@@ -5332,15 +5586,19 @@ textarea::selection {
font-size: 10px;
color: var(--ink-muted);
}
/* The dashboard renders its own icon inside the conference tag. */
.paper-table-dashboard .cell-conf::before { content: none; }
.paper-table .cell-conf:has(.empty-dash),
.paper-table .cell-expires:has(.empty-dash) { display: none; }
.paper-table .cell-views::before,
.paper-table .cell-expires::before { color: var(--ink-faint); }
}
/* Paper-themed dropdown menus (Sort / Status / Actions) */
.paper-page .dropdown-menu {
background: var(--paper-card);
border: 1px solid var(--border-color);
border-radius: 8px;
border-radius: var(--radius-surface, 10px);
box-shadow: 0 12px 28px rgba(26, 24, 21, 0.08);
padding: 6px;
min-width: 220px;
@@ -5448,25 +5706,33 @@ textarea::selection {
.paper-page .paper-page-lede { font-size: 13px; line-height: 1.4; margin-bottom: 0; }
.paper-page .btn-ink { padding: 6px 12px; font-size: 13px; }
/* Quotas stack in one column on phones so labels and values keep the
same 11px / 13px sizes as desktop instead of shrinking to 9px. */
.paper-page .quota-row {
grid-template-columns: repeat(3, 1fr);
gap: 10px 12px;
grid-template-columns: 1fr;
gap: 12px;
padding: 0;
margin: 12px 0 0;
margin: 16px 0 0;
}
.paper-page .dashboard-filter-row {
padding: 8px 0;
margin-top: 10px;
padding: 10px 0;
margin-top: 12px;
gap: 8px;
}
.paper-page .quota-header {
flex-direction: column;
align-items: flex-start;
gap: 2px;
flex-direction: row;
align-items: baseline;
gap: 8px;
}
.paper-page .quota-label { font-size: 10px; letter-spacing: 0.1em; }
.paper-page .quota-value { font-size: 11px; }
.paper-page .quota-value { white-space: nowrap; }
.paper-page .dashboard-filter-row { gap: 8px; }
/* Filter controls: type group fills the row, Sort and Status share one. */
.paper-page .dashboard-filter-controls { width: 100%; }
.paper-page .dashboard-filter-controls .btn-group { flex: 1 1 100%; display: flex; }
.paper-page .dashboard-filter-controls .btn-group .btn { flex: 1; justify-content: center; }
.paper-page .dashboard-filter-controls .dropdown { flex: 1 1 0; min-width: 0; }
.paper-page .dashboard-filter-controls .dropdown .dropdown-toggle { width: 100%; justify-content: space-between; }
.paper-page .dashboard-filter-controls .dropdown .filter-btn-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
@media (max-width: 480px) {
@@ -5476,9 +5742,6 @@ textarea::selection {
-webkit-box-orient: vertical;
overflow: hidden;
}
.paper-page .quota-row { gap: 8px; }
.paper-page .quota-label { font-size: 9px; letter-spacing: 0.06em; }
.paper-page .quota-value { font-size: 10px; white-space: nowrap; }
}
/* Empty state */
@@ -5510,10 +5773,12 @@ textarea::selection {
font-size: 10px;
line-height: 10px;
}
.paper-page .progress .progress-bar.bg-success { background: #2F7A44; }
.paper-page .progress .progress-bar.bg-warning { background: #C48A2E; color: #fff; }
.paper-page .progress .progress-bar.bg-danger { background: #B42318; }
.paper-page .progress .progress-bar.bg-dark { background: var(--ink-muted); }
/* Bootstrap's .bg-* utilities are !important, so the paper palette must be
too, otherwise the bright Bootstrap green/yellow/red leak through. */
.paper-page .progress .progress-bar.bg-success { background: var(--status-ready) !important; }
.paper-page .progress .progress-bar.bg-warning { background: var(--status-progress) !important; color: #fff; }
.paper-page .progress .progress-bar.bg-danger { background: var(--status-error) !important; }
.paper-page .progress .progress-bar.bg-dark { background: var(--ink-faint) !important; }
.paper-page > h5,
.paper-page form > h5,