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>
1963 lines
40 KiB
CSS
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;
|
|
}
|