Files
arr/web/src/style.css
Miguel PalhasandClaude Fable 5 7c30928907 feat(web): series, season and episode removal
Series detail exposes DELETE /api/series/{id} through the movie
removePanel, generalised over endpoints and file rollup instead of
copied. Season headers and on-disk episode rows get the settings
armed-delete control wired to #174's file endpoints, worded for what
they do: files go, wanted clears, the row stays listed.

Closes #175

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 17:09:46 +01:00

1963 lines
40 KiB
CSS

/* arr — master-control board.
Tokens mirror .impeccable/design.json; components consume tokens, never
literals. Committed dark: a control room is lit by its lamps. */
@font-face {
font-family: "Rajdhani";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/rajdhani-600-latin.woff2") format("woff2");
}
@font-face {
font-family: "Rajdhani";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/rajdhani-700-latin.woff2") format("woff2");
}
:root {
/* ground and panels */
--ground: oklch(0.14 0.01 250);
--panel: oklch(0.18 0.012 250);
--panel-raised: oklch(0.21 0.014 250);
--line: oklch(0.3 0.015 250);
--line-strong: oklch(0.42 0.02 250);
--highlight: oklch(1 0 0 / 4%);
/* ink */
--ink: oklch(0.93 0.008 250);
--ink-muted: oklch(0.72 0.015 250);
--ink-faint: oklch(0.58 0.015 250);
/* signal lamps */
--signal-ok: oklch(0.8 0.17 150);
--signal-warn: oklch(0.82 0.14 80);
--signal-fault: oklch(0.68 0.21 25);
--signal-idle: oklch(0.45 0.02 250);
/* the one interactive hue */
--accent: oklch(0.8 0.12 220);
--accent-bright: oklch(0.88 0.12 220);
/* dead lamp and pressed-control lowlight */
--lamp-off: oklch(0.3 0.012 250);
--lowlight: oklch(0 0 0 / 30%);
/* release verdicts (§9.3) — rejected is quiet slate, never fault red */
--verdict-eligible: oklch(0.8 0.17 150);
--verdict-waived: oklch(0.82 0.14 80);
--verdict-rejected: oklch(0.68 0.02 250);
/* score heat scale (issue #111) — one list-relative gradient from lowest
red to highest green; JS picks the stop, colour stays in tokens */
--score-0: oklch(0.68 0.19 25);
--score-1: oklch(0.7 0.185 46);
--score-2: oklch(0.72 0.175 67);
--score-3: oklch(0.75 0.165 88);
--score-4: oklch(0.77 0.16 108);
--score-5: oklch(0.79 0.165 129);
--score-6: oklch(0.8 0.17 150);
/* library state ramp (§4.2): green satisfied, amber gap, violet active,
neutral nothing-happening. Satisfaction reuses --signal-ok and a gap
--signal-warn, so the only hue this family still owns is the active one,
which doubles as the downloading colour. */
--status-airing: oklch(0.78 0.14 305);
/* type */
--font-label: system-ui, "Segoe UI", sans-serif;
--font-readout:
ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--font-display: "Rajdhani", "Avenir Next Condensed", "Arial Narrow", sans-serif;
/* type scale */
--text-2xs: 0.6875rem;
--text-xs: 0.75rem;
--text-sm: 0.8125rem;
--text-base: 1rem;
--text-md: 1.0625rem;
--text-lg: 1.3125rem;
--text-xl: 1.625rem;
/* rhythm */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--radius: 4px;
--control-h: 2rem;
--ease-out: cubic-bezier(0.25, 1, 0.5, 1);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
/* a scrollbar appearing must not reflow the rail's wrap */
html {
scrollbar-gutter: stable;
}
body {
min-height: 100dvh;
display: flex;
flex-direction: column;
background: var(--ground);
color: var(--ink);
font-family: var(--font-label);
font-size: var(--text-base);
line-height: 1.5;
}
.readout {
font-family: var(--font-readout);
font-size: var(--text-sm);
}
.dim {
color: var(--ink-muted);
}
/* ---- rails ---------------------------------------------------------- */
.rail {
padding: var(--space-3) var(--space-6);
border-bottom: 1px solid var(--line);
background: var(--panel);
box-shadow: inset 0 1px var(--highlight);
}
/* contents share the deck's column; the panel itself stays full-bleed */
.rail-inner {
display: flex;
align-items: center;
gap: var(--space-6);
width: 100%;
max-width: 68rem;
margin-inline: auto;
}
.rail-id {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* §9.7: the wordmark is a link home — quiet at rest, cyan on approach
like every other interactive control */
.wordmark {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
letter-spacing: 0.1em;
line-height: 1;
color: var(--ink);
text-decoration: none;
}
.wordmark:hover,
.wordmark:focus-visible {
color: var(--accent-bright);
}
.rail-meta {
display: flex;
align-items: center;
gap: var(--space-4);
/* the verdict line used to absorb the slack; without it the readout
needs to claim the right edge itself */
margin-left: auto;
}
/* ---- lamps ---------------------------------------------------------- */
.lamp {
--lamp: var(--signal-idle);
width: 0.625rem;
height: 0.625rem;
flex: none;
border-radius: 50%;
background: var(--lamp);
/* the panel's own device: a lit indicator, not a depth shadow */
box-shadow:
0 0 6px var(--lamp),
0 0 14px oklch(from var(--lamp) l c h / 35%);
transition: background 300ms var(--ease-out);
}
.lamp-big {
width: 0.875rem;
height: 0.875rem;
}
.lamp[data-state="ok"] {
--lamp: var(--signal-ok);
}
.lamp[data-state="unconfigured"] {
--lamp: var(--signal-warn);
}
.lamp[data-state="degraded"] {
--lamp: var(--signal-warn);
}
.lamp[data-state="unreachable"] {
--lamp: var(--signal-fault);
}
.lamp[data-state="probing"] {
--lamp: var(--signal-idle);
animation: lamp-probe 1.2s var(--ease-out) infinite alternate;
}
/* dead air: dark, no glow, no motion */
.lamp[data-state="off"] {
--lamp: var(--lamp-off);
box-shadow: none;
}
@keyframes lamp-probe {
from {
opacity: 0.45;
}
to {
opacity: 1;
}
}
/* power-up: lamps flicker on left to right, once, on first verdict */
@keyframes lamp-strike {
0% {
opacity: 0.2;
}
40% {
opacity: 1;
}
55% {
opacity: 0.35;
}
100% {
opacity: 1;
}
}
.chain.powered .lamp {
animation: lamp-strike 420ms var(--ease-out) both;
animation-delay: calc(var(--strike, 0) * 120ms);
}
.chain.powered .lamp[data-state="off"] {
animation: none;
}
@media (prefers-reduced-motion: reduce) {
.lamp[data-state="probing"],
.chain.powered .lamp {
animation: none;
}
}
/* ---- signal chain (§9.7: a settings section, no route of its own) ----- */
.chain {
width: 100%;
display: grid;
grid-template-columns: 1fr 2.5rem 1.2fr 2.5rem 1fr;
grid-template-areas:
"tmdb t1 master out sink"
"prow t2 master out sink";
align-items: center;
row-gap: var(--space-6);
}
.area-tmdb {
grid-area: tmdb;
}
.area-prowlarr {
grid-area: prow;
}
.area-t1 {
grid-area: t1;
}
.area-t2 {
grid-area: t2;
}
.area-master {
grid-area: master;
}
.area-out {
grid-area: out;
}
.area-transmission {
grid-area: sink;
}
/* ---- modules --------------------------------------------------------- */
.module {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--highlight);
padding: var(--space-4) var(--space-4) var(--space-3);
}
.module-master {
background: var(--panel-raised);
border-color: var(--line-strong);
padding: var(--space-6) var(--space-4) var(--space-4);
}
.module-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
}
.module-name {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
line-height: 1;
}
.module-master .module-name {
font-size: var(--text-lg);
}
.module-status {
margin-left: auto;
padding-left: var(--space-3);
color: var(--ink-muted);
}
.module-status[data-tone="ok"] {
color: var(--signal-ok);
}
.module-status[data-tone="warn"] {
color: var(--signal-warn);
}
.module-status[data-tone="fault"] {
color: var(--signal-fault);
}
.module-role {
margin: var(--space-2) 0 0;
font-size: var(--text-2xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-muted);
}
.module-detail {
margin: var(--space-1) 0 0;
font-size: var(--text-xs);
color: var(--ink-muted);
min-height: 1.2em;
}
/* ---- bus traces ------------------------------------------------------ */
.bus {
display: grid;
align-content: center;
justify-items: center;
}
.bus-trace {
--trace: var(--line-strong);
width: 100%;
height: 0.5rem;
display: block;
transition: filter 300ms var(--ease-out);
}
.bus-trace line {
stroke: var(--trace);
stroke-width: 1;
}
.bus-arrow {
fill: var(--trace);
}
.bus-trace[data-tone="ok"] {
--trace: var(--signal-ok);
}
.bus-trace[data-tone="warn"] {
--trace: var(--signal-warn);
}
.bus-trace[data-tone="fault"] {
--trace: var(--signal-fault);
}
/* ---- controls -------------------------------------------------------- */
.control {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--control-h);
padding: 0 var(--space-4);
font-family: var(--font-readout);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
background: var(--panel-raised);
border: 1px solid var(--line-strong);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--highlight);
cursor: pointer;
transition:
border-color 150ms var(--ease-out),
color 150ms var(--ease-out);
}
.control:hover {
border-color: var(--accent);
color: var(--accent-bright);
}
.control:active {
box-shadow: inset 0 1px var(--lowlight);
}
.control:disabled {
color: var(--ink-faint);
border-color: var(--line);
cursor: default;
}
/* the engaged view control stays lit while its surface is up */
.control[aria-pressed="true"] {
border-color: var(--accent);
color: var(--accent-bright);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---- unified search (§9.2) ------------------------------------------- */
.rail-search {
position: relative;
flex: 1;
max-width: 30rem;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
min-height: var(--control-h);
padding: 0 var(--space-8) 0 var(--space-3);
color: var(--ink);
background: var(--ground);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--lowlight);
caret-color: var(--accent);
transition: border-color 150ms var(--ease-out);
}
.search-input::placeholder {
color: var(--ink-faint);
opacity: 1;
}
.search-input:hover {
border-color: var(--line-strong);
}
.search-input:focus {
border-color: var(--accent);
outline: none;
}
.search-hint {
position: absolute;
right: var(--space-3);
padding: 0 var(--space-1);
font-size: var(--text-2xs);
color: var(--ink-faint);
border: 1px solid var(--line);
border-radius: var(--radius);
pointer-events: none;
}
/* ---- search deck ------------------------------------------------------ */
.deck {
width: 100%;
max-width: 68rem;
margin: 0 auto;
padding: var(--space-8) var(--space-6) var(--space-12);
flex: 1;
}
.deck-status {
margin: 0 0 var(--space-4);
color: var(--ink-muted);
}
.deck-status[data-tone="fault"] {
color: var(--signal-fault);
}
/* a sweep in flight borrows the rail's probing-lamp idiom, inline */
.deck-status .lamp {
display: inline-block;
margin-right: var(--space-2);
vertical-align: baseline;
}
.deck-group {
margin: 0 0 var(--space-8);
}
.deck-head {
display: flex;
align-items: baseline;
gap: var(--space-3);
padding: 0 var(--space-1) var(--space-2);
border-bottom: 1px solid var(--line);
}
/* the engraved micro-label is the heading itself, not a kicker */
.deck-label {
margin: 0;
font-size: var(--text-2xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-muted);
}
.deck-count {
font-size: var(--text-xs);
color: var(--ink-faint);
}
.deck-rows {
margin: 0;
padding: 0;
list-style: none;
}
.row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--space-1) var(--space-3);
width: 100%;
padding: var(--space-2) var(--space-1);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
text-align: left;
}
.row-id {
display: inline-flex;
align-items: baseline;
gap: var(--space-2);
min-width: 0;
}
/* titles are names: the display face, per the Readout Rule in design.json */
.row-title {
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink);
}
.row-year {
font-size: var(--text-xs);
}
.row-chips {
display: inline-flex;
align-items: center;
gap: var(--space-1);
margin-left: auto;
}
.chip {
display: inline-flex;
align-items: center;
gap: var(--space-1);
min-height: var(--control-h);
padding: 0 var(--space-2);
background: var(--panel-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
font-family: var(--font-readout);
font-size: var(--text-xs);
color: var(--ink-muted);
}
.chip[data-verdict="eligible"] {
color: var(--verdict-eligible);
border-color: oklch(from var(--verdict-eligible) l c h / 45%);
}
/* the dashed border is the visible mark of a relaxed rule */
.chip[data-verdict="waived"] {
color: var(--verdict-waived);
border-style: dashed;
border-color: oklch(from var(--verdict-waived) l c h / 55%);
}
/* rejected is the policy working, never fault red */
.chip[data-verdict="rejected"] {
color: var(--verdict-rejected);
}
/* media state ramp (§4.2): green on disk, violet downloading, amber wanted
and still missing. Unwanted-missing and parked are nothing-happening and
stay neutral; `parked` exists so a vanished grab never reads as a gap. */
.chip[data-movie-state="available"] {
color: var(--signal-ok);
border-color: oklch(from var(--signal-ok) l c h / 45%);
}
.chip[data-movie-state="downloading"] {
color: var(--status-airing);
border-color: oklch(from var(--status-airing) l c h / 45%);
}
.chip[data-movie-state="missing"][data-wanted="true"] {
color: var(--signal-warn);
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
/* library status ramp (§4.2): violet while activity is live, green once
everything wanted is on disk, amber for a grabbable gap; waiting is
between seasons with nothing to do, so it stays neutral */
.chip[data-status="airing"] {
color: var(--status-airing);
border-color: oklch(from var(--status-airing) l c h / 45%);
}
.chip[data-status="incomplete"] {
color: var(--signal-warn);
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
.chip[data-status="complete"],
.chip[data-status="ended"] {
color: var(--signal-ok);
border-color: oklch(from var(--signal-ok) l c h / 45%);
}
/* season/series counts chip (§4.2): the same ramp read off N/M itself */
.chip[data-counts="complete"] {
color: var(--signal-ok);
border-color: oklch(from var(--signal-ok) l c h / 45%);
}
.chip[data-counts="partial"] {
color: var(--signal-warn);
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
/* a TMDB row is the affordance: the whole row opens the add flow */
.row-tmdb {
font: inherit;
background: none;
border: 0;
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
cursor: pointer;
transition: background 150ms var(--ease-out);
}
.row-tmdb:hover:not(:disabled):not([aria-disabled="true"]),
.row-tmdb:focus-visible:not([aria-disabled="true"]) {
background: var(--panel);
}
.row-tmdb:disabled,
.row-tmdb[aria-disabled="true"] {
cursor: default;
}
.row-add {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
}
.row-overview {
flex-basis: 100%;
overflow: hidden;
font-size: var(--text-xs);
color: var(--ink-faint);
white-space: nowrap;
text-overflow: ellipsis;
}
/* an episode hit's title sits beside its SxxEyy chip: a name, not a readout */
.row-sub {
font-size: var(--text-sm);
color: var(--ink-muted);
}
/* ---- rich search rows (#148): poster, rating chip, trailer chip -------- */
.row-rich {
align-items: flex-start;
}
.row-main {
display: flex;
flex: 1;
align-items: baseline;
flex-wrap: wrap;
gap: var(--space-1) var(--space-3);
min-width: 0;
}
/* w92 is the right order of magnitude for a row; the blank is the same
box, so the column edge holds while results stream in */
.row-poster,
.row-poster-blank {
width: 2.75rem;
aspect-ratio: 2 / 3;
flex-shrink: 0;
align-self: center;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.row-poster {
display: block;
background: var(--panel-raised);
object-fit: cover;
}
.chip[role="button"] {
cursor: pointer;
transition:
color 150ms var(--ease-out),
border-color 150ms var(--ease-out);
}
.chip[role="button"]:hover,
.chip[role="button"]:focus-visible {
color: var(--accent);
border-color: oklch(from var(--accent) l c h / 45%);
}
.row-trailer[data-state="pending"] {
color: var(--ink-faint);
}
/* no video on TMDB is an ordinary outcome: quiet and dashed, not fault red */
.row-trailer[data-state="none"] {
border-style: dashed;
color: var(--ink-faint);
cursor: default;
}
.row-trailer[data-state="blocked"] {
color: var(--signal-warn);
cursor: default;
}
/* ---- add flow (inline, root and policy pre-filled) -------------------- */
.add-panel {
margin: 0 0 var(--space-2);
padding: var(--space-3) var(--space-3) var(--space-3);
background: var(--panel);
border: 1px solid var(--line);
border-top: 0;
border-radius: 0 0 var(--radius) var(--radius);
box-shadow: inset 0 1px var(--highlight);
}
.add-roots {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: var(--space-2);
}
.root-option {
display: grid;
gap: var(--space-1);
padding: var(--space-2) var(--space-3);
font: inherit;
text-align: left;
color: var(--ink);
background: var(--panel-raised);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--highlight);
cursor: pointer;
transition: border-color 150ms var(--ease-out);
}
.root-option:hover {
border-color: var(--line-strong);
}
.root-option[aria-pressed="true"] {
border-color: var(--accent);
}
.root-audience {
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
line-height: 1;
}
.root-policy,
.root-path {
font-size: var(--text-xs);
}
.add-actions {
display: flex;
align-items: center;
gap: var(--space-3);
margin: var(--space-3) 0 0;
}
/* the one series-level add-time question (§4.1): the choice and what it does */
.add-autotrack {
display: grid;
justify-content: start;
gap: var(--space-1);
margin: var(--space-3) 0 0;
}
.add-note {
margin: 0;
font-size: var(--text-xs);
color: var(--ink-muted);
}
.add-note[data-tone="fault"] {
color: var(--signal-fault);
}
.add-done {
margin: 0;
color: var(--verdict-eligible);
}
/* ---- release deck (§9.3) ---------------------------------------------- */
.releases-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
margin: 0 0 var(--space-6);
}
.releases-id {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.releases-title {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.releases-year {
font-size: var(--text-xs);
}
/* ---- removal confirmation (§7.4, issue 104) --------------------------- */
/* remove is not the deck's errand: it reads as a quiet control until the
operator reaches for it, and never competes with the sweep */
.control-quiet {
color: var(--ink-muted);
border-color: var(--line);
}
.control-quiet:hover,
.control-quiet[aria-expanded="true"] {
color: var(--accent-bright);
border-color: var(--accent);
}
.remove-panel {
margin: 0 0 var(--space-6);
}
.remove-body {
display: grid;
gap: var(--space-2);
padding: var(--space-3);
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--highlight);
}
/* armed is a state, not a toggle: it is amber whenever files are on disk to
delete */
.remove-body[data-armed="true"] {
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
.remove-evidence,
.remove-path,
.remove-note {
margin: 0;
font-size: var(--text-xs);
}
/* the folder is evidence: it wraps rather than truncating, because a path
the operator cannot read whole is not a confirmation */
.remove-path {
color: var(--ink-muted);
overflow-wrap: anywhere;
white-space: pre-line;
}
.remove-note {
color: var(--ink-muted);
}
.remove-note[data-tone="warn"] {
color: var(--signal-warn);
}
.remove-note[data-tone="fault"] {
color: var(--signal-fault);
}
.remove-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin: var(--space-1) 0 0;
}
.remove-body[data-armed="true"] .remove-actions .control:first-child {
color: var(--signal-warn);
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
.remove-body[data-armed="true"] .remove-actions .control:first-child:hover:not(:disabled) {
border-color: var(--signal-warn);
}
/* the settings arm-then-confirm, shared with season and episode rows: an
armed control turns caution amber until it disarms or fires */
.control[data-armed="true"] {
color: var(--signal-warn);
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
.control[data-armed="true"]:hover:not(:disabled) {
border-color: var(--signal-warn);
}
.bucket-head {
align-items: center;
}
.bucket-toggle {
margin-left: auto;
min-height: var(--control-h);
padding: 0 var(--space-2);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
background: none;
border: 0;
cursor: pointer;
transition: color 150ms var(--ease-out);
}
.bucket-toggle:hover {
color: var(--accent-bright);
}
/* fixed-width mono columns: chips and header labels share rem widths, so
the pseudo-table survives font-size differences and flex wrapping alike */
.cw {
display: inline-block;
flex: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* single nowrap line: line-height equal to the box height centres it,
and keeps ellipsis + text-align working, which flex would break */
line-height: var(--control-h);
}
.cw-score {
width: 4.5rem;
text-align: right;
}
.cw-res {
width: 4rem;
}
.cw-src {
width: 4.5rem;
}
.cw-hdr {
width: 6rem;
}
.cw-aud {
width: 7.5rem;
}
.cw-size {
width: 4.5rem;
text-align: right;
}
.cw-seed {
width: 3.5rem;
text-align: right;
}
/* §9.3: headers never wrap — the narrow-deck block below sheds columns
instead */
.colhead {
display: flex;
gap: var(--space-1);
padding: var(--space-2) var(--space-1) 0;
}
.colhead .cw {
padding: 0 var(--space-2);
border: 1px solid transparent;
font-family: var(--font-readout);
font-size: var(--text-2xs);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ink-faint);
}
.rel {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
padding: var(--space-2) var(--space-1);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
/* the line is plain content now — grab is its own control */
.rel-line {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
}
/* §9.3: the release name is secondary — evidence, not the primary key */
.rel-name {
flex-basis: 100%;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: var(--text-xs);
color: var(--ink-faint);
}
.rel-none {
padding: var(--space-2) var(--space-1);
font-size: var(--text-xs);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
.rel-note {
font-size: var(--text-xs);
color: var(--ink-muted);
}
.rel-note[data-tone="fault"] {
color: var(--signal-fault);
}
/* score heat: data-score is the list-relative stop, 0 lowest … 6 highest */
.rel-line .cw-score[data-score="0"] {
color: var(--score-0);
border-color: oklch(from var(--score-0) l c h / 45%);
}
.rel-line .cw-score[data-score="1"] {
color: var(--score-1);
border-color: oklch(from var(--score-1) l c h / 45%);
}
.rel-line .cw-score[data-score="2"] {
color: var(--score-2);
border-color: oklch(from var(--score-2) l c h / 45%);
}
.rel-line .cw-score[data-score="3"] {
color: var(--score-3);
border-color: oklch(from var(--score-3) l c h / 45%);
}
.rel-line .cw-score[data-score="4"] {
color: var(--score-4);
border-color: oklch(from var(--score-4) l c h / 45%);
}
.rel-line .cw-score[data-score="5"] {
color: var(--score-5);
border-color: oklch(from var(--score-5) l c h / 45%);
}
.rel-line .cw-score[data-score="6"] {
color: var(--score-6);
border-color: oklch(from var(--score-6) l c h / 45%);
}
/* ---- library view (§4.2) ---------------------------------------------- */
.rail-nav {
flex: none;
}
/* the library's master line: total readout left, the one §4.2 toggle right */
.library-bar {
margin: 0 0 var(--space-6);
}
/* the toggle's own padding would inset its text past the chip column's
right rag; with the shape toggle right of it, SATISFIED sits flush
against its neighbour instead of the deck edge */
.library-bar .bucket-toggle {
padding: 0;
margin-left: var(--space-2);
}
/* ---- library poster grid (§9.6, issue 151) ------------------------------ */
/* the shape toggle: a joined pair, engaged side lit like any view control */
.view-toggle {
margin-left: auto;
display: inline-flex;
}
.view-btn {
min-height: var(--control-h);
padding: 0 var(--space-2);
font-family: var(--font-readout);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-faint);
background: none;
border: 1px solid var(--line);
cursor: pointer;
transition:
border-color 150ms var(--ease-out),
color 150ms var(--ease-out);
}
.view-btn + .view-btn {
border-left-width: 0;
}
.view-btn:hover {
color: var(--accent-bright);
}
.view-btn[aria-pressed="true"] {
color: var(--accent-bright);
border-color: var(--accent);
}
/* auto-fill keeps every tile the same width down to a phone, where three
narrow-but-legible columns beat two oversized ones */
.deck-rows.grid-rows {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
gap: var(--space-4);
}
.card {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
height: 100%;
padding: 0;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
text-align: left;
cursor: pointer;
transition:
border-color 150ms var(--ease-out),
transform 150ms var(--ease-out);
}
.card:hover {
border-color: var(--accent);
}
.card:active {
transform: translateY(1px);
}
/* the poster is framed evidence, like every other image on a panel */
.card-art {
display: block;
aspect-ratio: 2 / 3;
}
.card-poster {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
background: var(--panel-raised);
}
/* same ratio as a poster, carrying the title: the grid never has a hole */
.card-blank {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: var(--space-3);
background: var(--panel-raised);
box-shadow: inset 0 1px var(--highlight);
}
.card-blank-title {
font-family: var(--font-display);
font-size: var(--text-base);
font-weight: 600;
letter-spacing: 0.02em;
text-align: center;
color: var(--ink-faint);
}
.card-body {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-2);
}
.card-id {
display: flex;
align-items: baseline;
gap: var(--space-1);
min-width: 0;
}
.card-name {
flex: 1;
/* two clamped lines: a long title stays readable where a single
ellipsised line at tile width would not be */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
font-family: var(--font-display);
font-size: var(--text-sm);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink);
}
.card-year {
flex: none;
font-size: var(--text-2xs);
}
.card-chips {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
}
/* ---- series detail (§4.1 + §4.2, issue 129) --------------------------- */
/* one season: a collapsible group with its rule, counts and actions */
.season {
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
.season-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-1);
}
/* the disclosure is a drawn chevron — no glyph standing in for an icon */
.season-disclose {
flex: none;
width: 1.75rem;
height: 1.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: 1px solid var(--line);
border-radius: var(--radius);
cursor: pointer;
transition: border-color 150ms var(--ease-out);
}
.season-disclose:hover,
.season-disclose:focus-visible {
border-color: var(--accent);
}
.season-disclose::before {
content: "";
width: 0.4rem;
height: 0.4rem;
border-right: 2px solid var(--ink-muted);
border-bottom: 2px solid var(--ink-muted);
transform: rotate(-45deg) translate(-5%, -5%);
transition: transform 200ms var(--ease-out);
}
.season-disclose[aria-expanded="true"]::before {
transform: rotate(45deg) translate(-5%, -5%);
}
.season-name {
font-family: var(--font-display);
font-size: var(--text-base);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink);
}
/* the tracked toggle reads as the rule it is; pressed = on */
.season-line .control,
.ep-actions .control {
padding: 0 var(--space-3);
}
.season-space {
flex: 1;
}
.episodes {
margin: 0;
padding: 0 0 var(--space-2) var(--space-8);
list-style: none;
}
.episode {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-2);
padding: var(--space-2) var(--space-1);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
.ep-tag {
flex: none;
width: 2.75rem;
color: var(--ink-faint);
}
.ep-title {
min-width: 0;
font-size: var(--text-sm);
color: var(--ink);
}
.ep-air {
flex: none;
width: 6.5rem;
color: var(--ink-faint);
}
.ep-chips {
margin-left: auto;
}
.ep-actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: var(--space-3);
}
.ep-unaired {
font-size: var(--text-xs);
}
/* issue 122: gone upstream while its file remained — a conflict, amber dashed */
.chip[data-flag="vanished"] {
color: var(--signal-warn);
border-style: dashed;
border-color: oklch(from var(--signal-warn) l c h / 55%);
}
/* ---- attention queues (§5.2 + §5.7) ------------------------------------ */
/* the badge is a signal, not a control: amber attention on the cyan word */
.nav-count {
margin-left: var(--space-2);
font-size: var(--text-xs);
color: var(--signal-warn);
}
.queue-note {
margin: var(--space-2) var(--space-1) 0;
font-size: var(--text-xs);
color: var(--ink-faint);
}
/* like .rel: the line expands to fill, the action is its own control */
.queue-item {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1) var(--space-3);
padding: var(--space-2) var(--space-1);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
.queue-item .queue-line {
flex: 1;
min-width: 0;
width: auto;
padding: 0;
border-bottom: 0;
}
.queue-empty {
padding: var(--space-2) var(--space-1);
font-size: var(--text-xs);
border-bottom: 1px solid oklch(from var(--line) l c h / 45%);
}
.queue-feedback {
font-size: var(--text-xs);
color: var(--ink-muted);
}
.queue-feedback[data-tone="fault"] {
color: var(--signal-fault);
}
/* ---- manual intake ---------------------------------------------------- */
.intake .module-detail {
overflow-wrap: anywhere;
}
/* ---- title detail pages (§9.6, issues #149 and #150) ------------------- */
/* The hero is one machined module carrying what the title IS — shared by
the movie and series pages, which is why its classes read `movie-*`.
The backdrop hotlinks from image.tmdb.org (§9.6) and sinks into the
console charcoal under a scrim, so the readouts keep their contrast — a
photo never sits behind text bare. */
.movie-hero {
position: relative;
overflow: hidden;
margin: 0 0 var(--space-8);
}
/* the backdrop layer: painted only when detail supplied one */
.movie-hero::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--backdrop);
background-size: cover;
background-position: center 20%;
opacity: 0;
transition: opacity 400ms var(--ease-out);
}
.movie-hero.has-backdrop::before {
opacity: 1;
}
/* the scrim: ground at the bottom, heavy everywhere the text sits */
.movie-hero::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(
to top,
oklch(from var(--ground) l c h / 92%) 0%,
oklch(from var(--ground) l c h / 72%) 45%,
oklch(from var(--ground) l c h / 40%) 100%
),
oklch(from var(--ground) l c h / 35%);
}
.movie-body {
position: relative;
z-index: 1;
display: flex;
gap: var(--space-6);
align-items: flex-start;
}
/* the poster is framed evidence, like every other image on a panel */
.movie-poster {
flex: none;
width: 10.5rem;
aspect-ratio: 2 / 3;
object-fit: cover;
border: 1px solid var(--line-strong);
border-radius: var(--radius);
box-shadow: 0 2px 12px oklch(from var(--lowlight) l c h / 75%);
}
.movie-info {
min-width: 0;
display: grid;
gap: var(--space-2);
padding: var(--space-6) 0;
}
.movie-idline {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
}
/* the page's voice: bigger than a deck title, same display face */
.movie-title {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.05;
}
.movie-year {
font-size: var(--text-sm);
}
.movie-chips {
margin-left: 0;
}
/* rating and meta are readouts on one line each; the star is drawn */
.movie-rating {
display: inline-flex;
align-items: center;
gap: var(--space-1);
margin: 0;
color: var(--ink);
}
/* the star is amber (§4.2): rating reads at a glance, the number stays ink */
.star {
width: 0.75rem;
height: 0.75rem;
fill: var(--signal-warn);
}
/* a row chip is smaller than the detail page's rating readout */
.row-rating .star {
width: 0.625rem;
height: 0.625rem;
}
.movie-meta {
margin: 0;
}
.movie-tagline {
margin: var(--space-1) 0 0;
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink-muted);
}
.movie-overview {
max-width: 65ch;
margin: var(--space-1) 0 0;
color: var(--ink);
}
.movie-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-2);
}
.movie-actions .add-note[data-tone="fault"] {
margin: 0;
color: var(--signal-fault);
}
/* external links wear the control skin; anchors must drop the underline */
.movie-actions a.control {
text-decoration: none;
}
/* library controls follow the hero on their own row: state left, errands right */
.movie-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-1);
}
.movie-controls .control {
padding: 0 var(--space-3);
}
/* wanted and blocked read as rules, pressed = on — the tracked-toggle idiom */
.movie-controls .control[aria-pressed="true"] {
color: var(--accent-bright);
border-color: var(--accent);
}
.movie-root-field {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.movie-root {
width: auto;
min-width: 11rem;
}
.movie-controls-space {
flex: 1;
}
/* on-disk rows share the deck's rel grammar: name leads, chips trail */
.disk-name {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--ink);
}
.rel[data-tone="fault"] {
color: var(--signal-fault);
}
/* ---- small screens --------------------------------------------------- */
@media (max-width: 46rem) {
.rail-inner {
flex-wrap: wrap;
gap: var(--space-3) var(--space-4);
}
/* the library bar wraps to two lines at phone widths: identity and count
on the first, shape and satisfied toggles on the second, each label
whole instead of squeezed into a mid-word break */
.library-bar {
flex-wrap: wrap;
row-gap: var(--space-2);
}
.library-bar .bucket-toggle,
.view-btn {
white-space: nowrap;
}
.rail-search {
order: 4;
flex-basis: 100%;
max-width: none;
}
/* deterministic first row: identity, views, then meta pushed right —
never re-wrapped by content or scrollbar changes */
.rail-nav {
order: 2;
}
.rail-meta {
order: 2;
margin-left: auto;
}
.deck {
padding: var(--space-6) var(--space-4) var(--space-8);
}
.row-chips {
margin-left: 0;
flex-basis: 100%;
}
/* an episode row wraps: identity first, chips full-width, actions right */
.ep-chips {
margin-left: 0;
flex-basis: 100%;
}
.ep-actions {
margin-left: auto;
}
.episodes {
padding-left: var(--space-4);
}
.releases-id {
order: -1;
flex-basis: 100%;
}
/* the line shrinks rather than wrapping, so the grab control never
leaves the row — the release name absorbs the squeeze via ellipsis */
.rel-grab {
margin-left: auto;
}
/* same wrap on a queue entry: the one click drops below its line */
.queue-item .queue-line {
flex-basis: 100%;
}
.queue-allow {
margin-left: auto;
}
/* the hero stacks: poster above the info block, both full width */
.movie-body {
flex-direction: column;
align-items: stretch;
gap: var(--space-3);
}
.movie-poster {
width: 8.5rem;
}
.movie-info {
padding-bottom: var(--space-4);
}
/* state controls wrap to their own line, errands stay right-aligned */
.movie-controls-space {
display: none;
}
.movie-root {
min-width: 0;
flex: 1;
}
.chain {
grid-template-columns: 1fr;
grid-template-areas:
"tmdb"
"prow"
"t1"
"master"
"out"
"sink";
row-gap: var(--space-4);
}
/* one junctioned trace on the stacked chain; the second is redundant */
.area-t2 {
display: none;
}
.bus {
height: 2.25rem;
justify-content: center;
}
.bus svg {
transform: rotate(90deg);
transform-origin: center;
width: 2.25rem;
height: 0.5rem;
}
}
/* ---- narrow deck (§9.3 at phone widths) ------------------------------- */
/* the full seven-column line is 36rem; under 40rem it cannot fit, so hdr
and audio leave the line and the five surviving columns tighten —
aligned, never wrapped, never scrolled */
@media (max-width: 40rem) {
.colhead .cw,
.rel-line .chip {
padding: 0 var(--space-1);
}
.cw-hdr,
.cw-aud {
display: none;
}
.cw-score {
width: 3rem;
}
.cw-res {
width: 3rem;
}
.cw-src {
width: 3.5rem;
}
.cw-size {
width: 3rem;
}
.cw-seed {
width: 2.5rem;
}
}
/* ---- settings (issue #116) -------------------------------------------- */
/* rows follow the queues pattern: line, then an action pair */
.settings-row-actions {
display: flex;
gap: var(--space-2);
margin: var(--space-1) 0 var(--space-2);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: var(--space-3);
align-items: end;
}
.field {
display: grid;
gap: var(--space-1);
}
.field-label {
font-size: var(--text-2xs);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.form-input {
min-height: 2.25rem;
width: 100%;
padding: 0 var(--space-2);
color: var(--ink);
font-family: var(--font-readout);
font-size: var(--text-xs);
background: var(--ground);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: inset 0 1px var(--lowlight);
caret-color: var(--accent);
transition: border-color 150ms var(--ease-out);
}
.form-input:hover {
border-color: var(--line-strong);
}
.form-input:focus {
border-color: var(--accent);
outline: none;
}
.form-input:disabled {
color: var(--ink-faint);
border-color: var(--line);
}
select.form-input {
appearance: none;
}
.form-input[data-invalid] {
border-color: var(--signal-fault);
}
/* one resolution's size band: label, floor, target, penalty on a line */
.band-line {
display: flex;
align-items: center;
gap: var(--space-2);
}
.band-line .field-label {
width: 3.5rem;
}
.band-num {
width: 4.5rem;
}