/* ── Header ─────────────────────────────────────────── */ /* Identity on the left, clock + settings on the right, one row at every width. The service pills used to occupy a second full-width row here; they now live in the Status section at the top of
, which is where someone looks for them. 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); } .brand { 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 — the Status section's only content, left-aligned with every other section's content. All six share the row equally and shrink together, so the strip is always exactly one row: never two, never a scroller. Every service stays visible at every width; what gives way as space tightens is detail *within* a pill, in this order: the stat drops below 1024px, then the name shortens below 560px. The dot and the pill itself never go, so no service is ever hidden. */ .svc-strip { display: flex; align-items: center; gap: 0.5rem; flex-wrap: nowrap; min-width: 0; } /* Full --tap height now they're content rather than header furniture. */ .svc-pill { flex: 1 1 0; /* equal share of the row, all shrink together */ min-width: 0; /* ...which flex items won't do without this */ display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0.35rem 0.9rem; min-height: var(--tap); 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; overflow: hidden; 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; min-width: 0; overflow: hidden; text-overflow: ellipsis; } .svc-stat { color: var(--text-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; } .svc-pill.down .svc-stat { color: var(--danger); } @media (max-width: 1024px) { /* Six stats stop fitting here. The dot already carries up/down, so the stat is the first thing that can go without losing any status. */ .svc-stat { display: none; } } @media (max-width: 620px) { /* Tighten the furniture so the names keep their characters longer. */ .svc-strip { gap: 0.3rem; } .svc-pill { padding: 0.35rem 0.5rem; gap: 0.3rem; } } @media (max-width: 560px) { /* Six full names in one row leaves ~23px each — two characters and an ellipsis, which identifies nothing. Swap in the short label instead. The full name stays in the DOM (font-size: 0 — invisible but still announced) and on the pill's title attribute. */ .svc-strip { gap: 0.25rem; } .svc-pill { padding: 0.35rem 0.35rem; gap: 0.25rem; } .svc-name { font-size: 0; } .svc-name::after { content: attr(data-short); font-size: var(--fs-xs); } } .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 { 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) { header { padding: 0.7rem 1rem; gap: 0.6rem; } .brand { flex: 1 1 auto; } .brand h1 { font-size: var(--fs-base); } .header-clock { display: none; } 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 */ }