arr-summary/static/css/dashboard.css
Barely Removable 08c71ba68d
All checks were successful
Deploy / deploy (push) Successful in 23s
Give Storage its own row and rebuild the disk list as a table
Ten disks stacked at --row-h each turned one third of the "Right now"
strip into a ~950px tower. The row is a grid, so Now playing and
Downloads stretched to match and held roughly 1500px of empty card
between them.

The information needed width, not a shorter list. "Right now" drops to
two columns and Storage moves to a full-width row beneath it, where the
same ten disks are a six-column table — disk, used, size, free, usage
bar, temp — at ~270px total. Nothing is hidden and nothing scrolls
except the table itself, which gets its own overflow container below
720px rather than wrapping the sizes into mush.

Also: the array total moves to a bar above the table and the summary
into the card header, so the hero figure isn't repeating what every row
already says. The header summary is a sentence rather than a badge, so
it needs .card-meta-wrap to opt out of the global nowrap — without it
that one string pushed the whole page sideways on a phone.

Local-fallback (no per-disk data) and error states keep the previous
hero-figure treatment. CLAUDE.md now carries the dead-space rule this
came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:52:34 -07:00

729 lines
23 KiB
CSS

/* ── Header ─────────────────────────────────────────── */
/* Two deliberate rows at every width: identity + controls on top,
service pills on their own full-width row beneath. Letting the pills
share row one made the header wrap into ragged rows and orphan the
clock as soon as the six of them stopped fitting. Scrolls away with
the page — a status bar isn't worth permanent screen real estate. */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem var(--gap);
flex-wrap: wrap;
}
.brand {
order: 1;
display: flex;
align-items: center;
gap: 0.65rem;
min-width: 0;
flex: 0 0 auto;
}
.brand svg { flex-shrink: 0; display: block; }
.brand h1 {
margin: 0;
font-size: var(--fs-lg);
font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
}
/* Service status pills — own row, wrapping among themselves so every
service stays visible at any width. Never scroll or clip: a hidden
pill is a service whose status you can't see. */
.svc-strip {
order: 3;
display: flex;
align-items: center;
gap: 0.4rem;
flex: 0 0 100%;
flex-wrap: wrap;
justify-content: center;
min-width: 0;
}
.svc-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.35rem 0.7rem;
min-height: 34px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 100px;
font-size: var(--fs-xs);
color: var(--text-2);
text-decoration: none;
white-space: nowrap;
transition: border-color 0.2s, color 0.2s;
}
.svc-pill:hover { border-color: var(--border-lt); color: var(--text); }
.svc-pill.down { color: var(--danger); border-color: var(--danger-border); }
.svc-name { font-weight: 600; }
.svc-stat { color: var(--text-3); }
.svc-pill.down .svc-stat { color: var(--danger); }
.status-light {
width: 7px; height: 7px; border-radius: 50%;
flex-shrink: 0; background: var(--text-3);
}
.status-light.up { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-dim); }
.status-light.down { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-dim); }
.status-light.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-dim); }
.header-right {
order: 2;
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: auto;
flex: 0 0 auto;
}
.header-clock {
font-variant-numeric: tabular-nums;
font-size: var(--fs-sm);
color: var(--text-2);
white-space: nowrap;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-width: var(--tap);
min-height: var(--tap);
padding: 0 0.85rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-2);
font-size: var(--fs-sm);
font-family: inherit;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.icon-btn:hover { color: var(--text); border-color: var(--border-lt); background: var(--surface); }
/* ── Layout ─────────────────────────────────────────── */
main {
padding: var(--gap-lg);
max-width: 1360px;
margin: 0 auto;
}
.section {
margin-bottom: 2rem;
}
.section > h2 {
margin: 0 0 0.85rem;
font-size: var(--fs-xs);
font-weight: 600;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-3);
}
.row {
display: grid;
gap: var(--gap);
}
.row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.row.cols-2 { grid-template-columns: repeat(2, 1fr); }
/* A section with two stacked rows (Watch insights) needs the same gap
between them as the grid uses between columns. */
.row + .row { margin-top: var(--gap); }
/* ── Card ───────────────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
flex-direction: column;
min-width: 0;
}
.card-header {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.85rem 1.1rem;
border-bottom: 1px solid var(--border);
}
.card-header h3 {
margin: 0;
font-size: var(--fs-sm);
font-weight: 600;
color: var(--text);
}
.card-header h3 a { color: inherit; text-decoration: none; }
.card-header h3 a:hover { color: var(--accent); }
.card-meta {
margin-left: auto;
font-size: var(--fs-xs);
color: var(--text-3);
white-space: nowrap;
}
.card-body { padding: 1.1rem; flex: 1; }
.card-body > :first-child { margin-top: 0; }
/* Requests can grow tall when expanded; the Indexers card beside it should
keep its natural height and sit at the top rather than stretching into a
mostly-empty box with its content floating in the middle. */
.row.align-start { align-items: start; }
/* ── "Right now" hero strip ─────────────────────────── */
.hero .card { border-color: var(--border-lt); }
.hero-figure {
font-size: var(--fs-2xl);
font-weight: 600;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.hero-sub {
font-size: var(--fs-sm);
color: var(--text-3);
margin-top: 0.3rem;
}
/* Now-playing list */
.stream {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.55rem 0;
border-top: 1px solid var(--border);
}
.stream:first-child { border-top: none; padding-top: 0; }
.stream-icon { font-size: var(--fs-lg); flex-shrink: 0; }
.stream-info { min-width: 0; flex: 1; }
.stream-title {
font-size: var(--fs-base); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stream-user { font-size: var(--fs-sm); color: var(--text-3); }
.live-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--ok); flex-shrink: 0;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) {
.live-dot { animation: none; }
* { transition-duration: 0.01ms !important; }
}
/* ── Meter (storage / progress) ─────────────────────── */
.meter {
height: 7px;
background: var(--surface-2);
border-radius: 4px;
overflow: hidden;
margin-top: 0.65rem;
}
.meter-fill {
height: 100%;
border-radius: 4px;
background: var(--ok);
transition: width 0.5s ease;
}
.meter-fill.warn { background: var(--warn); }
.meter-fill.danger { background: var(--danger); }
.meter-legend {
display: flex;
justify-content: space-between;
font-size: var(--fs-xs);
color: var(--text-3);
margin-top: 0.45rem;
}
/* ── Stats (semantic definition list) ───────────────── */
dl.stats {
display: grid;
gap: 0.6rem;
margin: 0;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}
/* Only the Downloads card stacks stats below the hero figure/sub block —
give that pairing breathing room without affecting stats blocks that
open a card (Sonarr/Radarr put dl.stats right after the search bar). */
.card-body > [aria-live="polite"] + dl.stats { margin-top: 0.9rem; }
dl.stats > div {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0.65rem 0.75rem;
min-width: 0;
}
dl.stats dt {
font-size: var(--fs-xs);
color: var(--text-3);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
dl.stats dd {
margin: 0.2rem 0 0;
font-size: var(--fs-lg);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
dd.is-warn { color: var(--warn); }
dd.is-danger { color: var(--danger); }
dd.is-ok { color: var(--ok); }
dd.is-accent { color: var(--accent); }
/* ── Buttons ────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--tap);
padding: 0 1rem;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: var(--accent);
color: var(--bg);
font-family: inherit;
font-size: var(--fs-sm);
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.15s, background 0.15s;
}
.btn:hover:not(:disabled) { opacity: 0.88; }
.btn:disabled { opacity: 0.5; cursor: default; }
.btn-quiet {
background: var(--surface-2);
border-color: var(--border);
color: var(--text-2);
}
.btn-quiet:hover:not(:disabled) { color: var(--text); border-color: var(--border-lt); }
/* Compact add button — still meets 44px on touch (see media query) */
.btn-add {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 0 0.7rem;
border: 1px solid var(--border-lt);
border-radius: var(--radius-sm);
background: var(--surface-2);
color: var(--text);
font-family: inherit;
font-size: var(--fs-xs);
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-add:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
/* Armed = awaiting the confirming second tap */
.btn-add.armed {
background: var(--warn);
border-color: var(--warn);
color: var(--bg);
}
.btn-add.done {
background: var(--ok-dim);
border-color: transparent;
color: var(--ok);
cursor: default;
}
.btn-add:disabled { cursor: default; }
/* ── Search ─────────────────────────────────────────── */
.search-bar { display: flex; gap: 0.5rem; margin-bottom: 0.85rem; }
.search-bar input {
flex: 1;
min-width: 0;
min-height: var(--tap);
padding: 0 0.85rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: inherit;
font-size: var(--fs-sm);
}
.search-bar input::placeholder { color: var(--text-3); }
.search-bar input:focus { border-color: var(--accent); outline: none; }
.search-result {
display: none;
align-items: center;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
margin-bottom: 0.85rem;
background: var(--accent-dim);
border: 1px solid rgba(122,162,247,0.3);
border-radius: var(--radius-sm);
}
.search-result.visible { display: flex; }
.search-result-info { min-width: 0; flex: 1; }
.search-result-title {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-result-meta { font-size: var(--fs-xs); color: var(--text-3); }
.search-msg {
font-size: var(--fs-sm);
color: var(--text-2);
padding: 0.5rem 0;
}
.search-msg.error { color: var(--danger); }
/* ── Discover (RT) list ─────────────────────────────── */
.rt-list { display: flex; flex-direction: column; gap: 0.5rem; }
.rt-item {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
transition: border-color 0.2s, opacity 0.3s, transform 0.3s;
}
.rt-item:hover { border-color: var(--border-lt); }
.rt-item.removing { opacity: 0; transform: scale(0.97); pointer-events: none; }
.rt-item.is-hidden { display: none; }
.rt-rank {
font-size: var(--fs-xs);
color: var(--text-3);
font-variant-numeric: tabular-nums;
width: 1.2rem;
text-align: right;
flex-shrink: 0;
}
.rt-poster {
width: 38px; height: 56px;
object-fit: cover;
border-radius: 4px;
background: var(--surface);
flex-shrink: 0;
}
.rt-info { min-width: 0; flex: 1; }
.rt-title {
font-size: var(--fs-sm);
font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rt-year { color: var(--text-3); font-weight: 400; }
.rt-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: var(--fs-xs);
margin-top: 0.15rem;
}
.score.certified { color: var(--ok); }
.score.critics { color: var(--warn); }
.score.audience { color: var(--text-3); }
/* Description is inline + clamped — readable on touch, no hover required */
.rt-desc {
font-size: var(--fs-xs);
color: var(--text-3);
margin-top: 0.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rt-desc:empty { display: none; }
.show-more {
margin-top: 0.6rem;
width: 100%;
min-height: 38px;
}
/* Sits below the request pills, which use --row-h — match it so the
button reads as one more row rather than a shorter tacked-on control. */
.show-more.show-more-row { min-height: var(--row-h); }
/* ── Indexers ───────────────────────────────────────── */
/* One indexer per line, built to the same rhythm as the Ombi request rows
beside it — same padding, gap, radius and two-line body, so the two cards
read as one pair rather than two unrelated layouts. */
.indexer-grid { display: flex; flex-direction: column; gap: 0.5rem; }
.indexer-item {
display: flex;
align-items: center;
gap: 0.7rem;
min-height: var(--row-h);
padding: 0.6rem 0.7rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
min-width: 0;
}
.indexer-item.down { border-color: var(--danger-border); }
.indexer-item .status-light { width: 9px; height: 9px; flex-shrink: 0; }
.indexer-info { min-width: 0; flex: 1; }
.indexer-name {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.indexer-sub { font-size: var(--fs-xs); color: var(--text-3); }
.indexer-sub .proto { text-transform: capitalize; }
.indexer-sub:empty::before { content: "Indexer"; }
/* Share of grabs, scaled to the busiest indexer (see dashboard.html).
The margin is trimmed so a bar-carrying row still fits inside --row-h
and stays flush with the request rows beside it. */
.share-bar {
margin-top: 0.3rem;
height: 4px;
border-radius: 100px;
background: var(--surface);
overflow: hidden;
}
.share-bar span {
display: block;
height: 100%;
border-radius: 100px;
background: var(--accent);
}
.indexer-item.down .share-bar span { background: var(--text-3); }
.share-pct {
font-size: var(--fs-sm);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text-2);
flex-shrink: 0;
}
/* ── Requests (Ombi) ────────────────────────────────── */
/* Request rows are the same object as an indexer row — same geometry and
the same dot/title/sub/badge anatomy — so the two cards read across as
one pair. --row-h keeps them aligned line-for-line; the indexer rows
reach it naturally once their share bar is in. */
.req-list { display: flex; flex-direction: column; gap: 0.5rem; }
.req-item.is-hidden { display: none; }
.req-item {
display: flex;
align-items: center;
gap: 0.7rem;
min-height: var(--row-h);
padding: 0.6rem 0.7rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.req-item .status-light { width: 9px; height: 9px; flex-shrink: 0; }
.req-type { text-transform: capitalize; }
.badge {
font-size: var(--fs-xs);
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 100px;
white-space: nowrap;
flex-shrink: 0;
}
.badge.movie { background: var(--accent-dim); color: var(--accent); }
.badge.tv { background: var(--ok-dim); color: var(--ok); }
.badge.pending { background: var(--warn-dim); color: var(--warn); }
.badge.available, .badge.approved { background: var(--ok-dim); color: var(--ok); }
.badge.denied { background: var(--danger-dim); color: var(--danger); }
.badge.idle { background: var(--surface); color: var(--text-3); }
.badge.downloading { background: var(--accent-dim); color: var(--accent); }
.badge.online { background: var(--ok-dim); color: var(--ok); }
.badge.offline { background: var(--danger-dim); color: var(--danger); }
.badge.paused { background: var(--warn-dim); color: var(--warn); }
.req-info { min-width: 0; flex: 1; }
.req-title {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.req-sub { font-size: var(--fs-xs); color: var(--text-3); }
/* ── Storage — per-disk table ──────────────────────────── */
/* Storage owns a full-width row of its own, so the per-disk breakdown is a
real table rather than a stack of cards: columns line up, digits are
tabular, and a new metric is a new column instead of a redesign. Stacking
these as rows in a third-of-a-page column is what hollowed out the two
cards beside it — see the dead-space rule in CLAUDE.md. */
/* Storage's header meta is a sentence, not a badge. .card-meta is nowrap so
short status words never break; this one has to wrap or it pushes the whole
page sideways on a phone. */
.card-meta.card-meta-wrap { white-space: normal; text-align: right; }
.array-meter.meter { margin-top: 0; margin-bottom: 1.1rem; }
.table-scroll { overflow-x: auto; }
.disk-table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}
.disk-table th, .disk-table td { padding: 0.45rem 0.6rem; text-align: left; }
.disk-table th:first-child, .disk-table td:first-child { padding-left: 0; }
.disk-table th:last-child, .disk-table td:last-child { padding-right: 0; }
.disk-table thead th {
font-size: var(--fs-xs);
font-weight: 500;
color: var(--text-3);
white-space: nowrap;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.disk-table tbody th {
font-size: var(--fs-sm);
font-weight: 600;
color: var(--text);
white-space: nowrap;
}
.disk-table tbody td {
font-size: var(--fs-sm);
color: var(--text-2);
border-bottom: 1px solid var(--border);
}
.disk-table tbody th { border-bottom: 1px solid var(--border); }
.disk-table tbody tr:last-child th,
.disk-table tbody tr:last-child td { border-bottom: none; }
.disk-table .num { text-align: right; white-space: nowrap; }
/* The bar and its percentage share one cell, so the bar flexes with the
table while the number stays a fixed, right-aligned column of digits. */
.disk-table .usage { width: 32%; min-width: 140px; }
.usage-cell { display: flex; align-items: center; gap: 0.6rem; }
.usage-cell .meter { margin-top: 0; flex: 1; height: 5px; }
.usage-pct {
font-size: var(--fs-xs);
color: var(--text-3);
width: 2.6em;
text-align: right;
flex-shrink: 0;
}
.disk-null { color: var(--text-3); }
.disk-table tr.is-parity th { color: var(--text-2); }
@media (max-width: 720px) {
/* Below this the six columns stop fitting; the table scrolls in its own
container rather than squeezing the sizes into unreadable wraps. */
.table-scroll .disk-table { min-width: 540px; }
}
.badge.temp-ok { background: var(--ok-dim); color: var(--ok); }
.badge.temp-warn { background: var(--warn-dim); color: var(--warn); }
.badge.temp-danger { background: var(--danger-dim); color: var(--danger); }
/* ── Insights top lists ─────────────────────────────── */
.top-list { display: flex; flex-direction: column; gap: 0.4rem; }
.top-item {
display: flex;
align-items: center;
gap: 0.65rem;
min-height: 3.25rem;
padding: 0.5rem 0.65rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.top-rank {
font-size: var(--fs-xs);
color: var(--text-3);
font-variant-numeric: tabular-nums;
width: 1.1rem;
flex-shrink: 0;
}
.top-info { min-width: 0; flex: 1; }
.top-name {
font-size: var(--fs-sm);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.top-sub { font-size: var(--fs-xs); color: var(--text-3); }
.top-plays {
font-size: var(--fs-xs);
color: var(--text-2);
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.empty { color: var(--text-3); font-size: var(--fs-sm); }
/* Storage card's usage note trails the meter with no room to spare below it. */
.meter-legend + .empty { margin-bottom: 0; }
/* ── Errors ─────────────────────────────────────────── */
.error {
background: var(--surface-2);
border: 1px solid var(--border);
border-left: 3px solid var(--danger);
border-radius: var(--radius-sm);
padding: 0.8rem 0.9rem;
color: var(--text-2);
font-size: var(--fs-sm);
}
.error-detail { margin-top: 0.5rem; }
.error-detail summary {
cursor: pointer;
font-size: var(--fs-xs);
color: var(--text-3);
list-style: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0;
}
.error-detail summary::-webkit-details-marker { display: none; }
.error-detail summary::before { content: "▸"; }
.error-detail[open] summary::before { content: "▾"; }
.error-detail summary:hover { color: var(--text-2); }
.error-detail pre {
margin: 0.4rem 0 0;
padding: 0.6rem 0.7rem;
background: rgba(0,0,0,0.3);
border-radius: 6px;
font-family: var(--mono);
font-size: var(--fs-xs);
line-height: 1.5;
color: var(--text-3);
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}
footer {
padding: 1.5rem;
text-align: center;
font-size: var(--fs-xs);
color: var(--text-3);
border-top: 1px solid var(--border);
}
/* Build identity — dim, so it reads as metadata rather than content. */
footer .version {
display: block;
margin-top: 0.3rem;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.visually-hidden {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* ── Responsive ─────────────────────────────────────── */
@media (max-width: 1100px) {
.row.cols-3 { grid-template-columns: 1fr 1fr; }
.row.cols-3 > :first-child { grid-column: 1 / -1; }
}
@media (max-width: 820px) {
.row.cols-2, .row.cols-3 { grid-template-columns: 1fr; }
.row.cols-3 > :first-child { grid-column: auto; }
}
@media (max-width: 700px) {
/* Row order and the full-width pill row are set in the base rules. */
header { padding: 0.7rem 1rem; gap: 0.6rem; }
.brand { flex: 1 1 auto; }
.brand h1 { font-size: var(--fs-base); }
.header-clock { display: none; }
.svc-strip { justify-content: flex-start; }
main { padding: 1rem; }
.card-body { padding: 0.9rem; }
.card-header { padding: 0.75rem 0.9rem; }
.search-bar { flex-direction: column; }
.search-bar .btn { width: 100%; }
/* Full touch targets once there's no mouse */
.btn-add { min-height: var(--tap); padding: 0 0.9rem; }
.search-bar input { font-size: 16px; } /* prevent iOS zoom on focus */
}