Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 070fd3d7ba | |||
| 0b4ded3fe9 | |||
| afc17ca34a | |||
| c962998a2b | |||
| 815a072ef0 |
+20
-12
@@ -218,10 +218,7 @@
|
||||
-->
|
||||
|
||||
<main class="deck" id="movie" hidden aria-label="movie detail">
|
||||
<header class="releases-head">
|
||||
<button type="button" class="control" id="movie-back">back</button>
|
||||
<p class="deck-status readout" id="movie-status" role="status" hidden></p>
|
||||
</header>
|
||||
<p class="deck-status readout" id="movie-status" role="status" hidden></p>
|
||||
|
||||
<section class="module movie-hero" id="movie-hero" aria-label="title metadata">
|
||||
<div class="movie-body">
|
||||
@@ -266,12 +263,19 @@
|
||||
<button type="button" class="control" id="movie-sweep">search indexers</button>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet"
|
||||
class="control control-quiet control-icon"
|
||||
id="movie-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="remove-panel"
|
||||
aria-label="remove from the library"
|
||||
>
|
||||
remove
|
||||
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M2.8 4.3h10.4" />
|
||||
<path d="M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2" />
|
||||
<path d="M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5" />
|
||||
<path d="M6.6 7v4.2" />
|
||||
<path d="M9.4 7v4.2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="remove-panel" id="remove-panel" hidden></div>
|
||||
@@ -470,10 +474,7 @@
|
||||
-->
|
||||
|
||||
<main class="deck releases" id="series" hidden aria-label="series detail">
|
||||
<header class="releases-head">
|
||||
<button type="button" class="control" id="series-back">back</button>
|
||||
<p class="deck-status readout" id="series-status" role="status" hidden></p>
|
||||
</header>
|
||||
<p class="deck-status readout" id="series-status" role="status" hidden></p>
|
||||
|
||||
<section class="module movie-hero" id="series-hero" aria-label="title metadata">
|
||||
<div class="movie-body">
|
||||
@@ -506,12 +507,19 @@
|
||||
<span class="movie-controls-space"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="control control-quiet"
|
||||
class="control control-quiet control-icon"
|
||||
id="series-remove"
|
||||
aria-expanded="false"
|
||||
aria-controls="series-remove-panel"
|
||||
aria-label="remove from the library"
|
||||
>
|
||||
remove
|
||||
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M2.8 4.3h10.4" />
|
||||
<path d="M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2" />
|
||||
<path d="M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5" />
|
||||
<path d="M6.6 7v4.2" />
|
||||
<path d="M9.4 7v4.2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="remove-panel" id="series-remove-panel" hidden></div>
|
||||
|
||||
+208
-59
@@ -103,7 +103,7 @@ import {
|
||||
type TvTarget,
|
||||
waiveAndGrabTv,
|
||||
} from "./series";
|
||||
import { armedDelete, settingsMain } from "./settings";
|
||||
import { settingsMain } from "./settings";
|
||||
import "./style.css";
|
||||
|
||||
const POLL_MS = 15_000;
|
||||
@@ -393,14 +393,14 @@ function main() {
|
||||
must<HTMLElement>("#library"),
|
||||
{ kind: "library" },
|
||||
);
|
||||
// no origin click to restore focus to on a deep link — the series
|
||||
// view's back control is the closest stand-in
|
||||
// no origin click to restore focus to on a deep link — the library
|
||||
// rail button is the closest stand-in
|
||||
tvDeck.open({
|
||||
title,
|
||||
sub,
|
||||
seriesId,
|
||||
target,
|
||||
origin: must<HTMLButtonElement>("#series-back"),
|
||||
origin: must<HTMLElement>("#nav-library"),
|
||||
returnTo: must<HTMLElement>("#series"),
|
||||
parentRoute: { kind: "series", seriesId },
|
||||
});
|
||||
@@ -1263,6 +1263,118 @@ function wireCollapsedToggle(bucket: CollapsedBucketDom) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- icon-only controls (#230) ----------------------------------------- */
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
const ICON_PATHS = {
|
||||
trash: [
|
||||
"M2.8 4.3h10.4",
|
||||
"M5.7 4.3V3.1c0-.5.4-.9.9-.9h2.8c.5 0 .9.4.9.9v1.2",
|
||||
"M4.2 4.3l.5 8.5c0 .8.7 1.4 1.5 1.4h3.6c.8 0 1.5-.6 1.5-1.4l.5-8.5",
|
||||
"M6.6 7v4.2",
|
||||
"M9.4 7v4.2",
|
||||
],
|
||||
search: ["M7.2 2.6a4.4 4.4 0 1 1 0 8.8 4.4 4.4 0 0 1 0-8.8Z", "M10.4 10.4l3.2 3.2"],
|
||||
disk: [
|
||||
"M2.5 9.8l1.6-5c.2-.7.8-1.2 1.5-1.2h4.8c.7 0 1.3.5 1.5 1.2l1.6 5",
|
||||
"M2.5 9.8h11v2.5c0 .7-.5 1.2-1.2 1.2H3.7c-.7 0-1.2-.5-1.2-1.2z",
|
||||
"M10.9 11.6h.01",
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** Stroke glyph on the shared 16-grid; the control's aria-label carries the meaning. */
|
||||
function icon(name: keyof typeof ICON_PATHS): SVGSVGElement {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 16 16");
|
||||
svg.setAttribute("class", "icon");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
for (const d of ICON_PATHS[name]) {
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", d);
|
||||
svg.append(path);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
/** §9.6 provider marks, drawn and shipped inline — never a remote image. */
|
||||
function providerMark(name: "tmdb" | "tvdb" | "imdb" | "rt"): SVGSVGElement {
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("class", "icon icon-mark");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
if (name === "rt") {
|
||||
svg.setAttribute("viewBox", "0 0 20 20");
|
||||
const paths = [
|
||||
"M10 6.6c4.1 0 6.8 2.1 6.8 5.2 0 3.3-3 5.7-6.8 5.7s-6.8-2.4-6.8-5.7c0-3.1 2.7-5.2 6.8-5.2Z",
|
||||
"M10 6.6C8.6 7.1 7 6.8 5.9 5.8",
|
||||
"M10 6.6c1.4.5 3 .2 4.1-.8",
|
||||
"M10 6.6c-.3-1.2 0-2.3.9-3.2",
|
||||
];
|
||||
for (const d of paths) {
|
||||
const path = document.createElementNS(SVG_NS, "path");
|
||||
path.setAttribute("d", d);
|
||||
svg.append(path);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
svg.setAttribute("viewBox", "0 0 44 20");
|
||||
const box = document.createElementNS(SVG_NS, "rect");
|
||||
box.setAttribute("x", "1");
|
||||
box.setAttribute("y", "1.5");
|
||||
box.setAttribute("width", "42");
|
||||
box.setAttribute("height", "17");
|
||||
box.setAttribute("rx", "4");
|
||||
const letters = document.createElementNS(SVG_NS, "text");
|
||||
letters.setAttribute("x", "22");
|
||||
letters.setAttribute("y", "13.2");
|
||||
letters.setAttribute("text-anchor", "middle");
|
||||
letters.setAttribute("font-size", "9");
|
||||
letters.textContent = { tmdb: "TMDB", tvdb: "TVDB", imdb: "IMDb" }[name];
|
||||
svg.append(box, letters);
|
||||
return svg;
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings arm-then-confirm, icon-only: a trash glyph resting, the
|
||||
* visible word "confirm" while armed, so an accidental hit never destroys.
|
||||
*/
|
||||
function armedDeleteIcon(label: string, execute: () => void): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "control control-quiet control-icon";
|
||||
button.setAttribute("aria-label", label);
|
||||
button.title = label;
|
||||
button.replaceChildren(icon("trash"));
|
||||
let armed = false;
|
||||
let resetTimer: number | undefined;
|
||||
const disarm = () => {
|
||||
armed = false;
|
||||
window.clearTimeout(resetTimer);
|
||||
delete button.dataset.armed;
|
||||
button.setAttribute("aria-label", label);
|
||||
button.replaceChildren(icon("trash"));
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
button.disabled = true;
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
armed = true;
|
||||
button.dataset.armed = "true";
|
||||
button.textContent = "confirm";
|
||||
button.setAttribute("aria-label", `confirm — ${label}`);
|
||||
resetTimer = window.setTimeout(disarm, 4000);
|
||||
});
|
||||
button.addEventListener("blur", () => {
|
||||
if (armed) {
|
||||
disarm();
|
||||
}
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
/* ---- §9.6 painters shared by both detail pages ------------------------- */
|
||||
|
||||
/** The drawn star before a TMDB rating — no glyph standing in for an icon. */
|
||||
@@ -1281,20 +1393,21 @@ function starIcon(): SVGSVGElement {
|
||||
}
|
||||
|
||||
/** External links read as quiet controls; they leave the app entirely. */
|
||||
function externalLink(label: string, href: string): HTMLAnchorElement {
|
||||
function externalLink(mark: SVGSVGElement, label: string, href: string): HTMLAnchorElement {
|
||||
const link = document.createElement("a");
|
||||
link.className = "control control-quiet";
|
||||
link.className = "control control-quiet control-icon";
|
||||
link.href = href;
|
||||
link.target = "_blank";
|
||||
link.rel = "noreferrer";
|
||||
link.textContent = label;
|
||||
link.setAttribute("aria-label", label);
|
||||
link.title = label;
|
||||
link.append(mark);
|
||||
return link;
|
||||
}
|
||||
|
||||
function movieMain(views: HideableView[]): MovieView {
|
||||
const view = must<HTMLElement>("#movie");
|
||||
const deckEl = must<HTMLElement>("#deck");
|
||||
const back = must<HTMLButtonElement>("#movie-back");
|
||||
const statusEl = must<HTMLElement>("#movie-status");
|
||||
const hero = must<HTMLElement>("#movie-hero");
|
||||
const poster = must<HTMLImageElement>("#movie-poster");
|
||||
@@ -1414,6 +1527,8 @@ function movieMain(views: HideableView[]): MovieView {
|
||||
}
|
||||
titleEl.textContent = movie.title;
|
||||
yearEl.textContent = movie.year === null ? "" : String(movie.year);
|
||||
remove.setAttribute("aria-label", `remove ${movie.title} from the library`);
|
||||
remove.title = `remove ${movie.title} from the library`;
|
||||
chipsEl.replaceChildren();
|
||||
const root = roots.find((candidate) => candidate.id === movie.root_id);
|
||||
chipsEl.append(chip(root ? root.audience : `root ${movie.root_id}`));
|
||||
@@ -1606,13 +1721,26 @@ function movieMain(views: HideableView[]): MovieView {
|
||||
trailer.textContent = "trailer";
|
||||
actionsEl.append(trailer);
|
||||
}
|
||||
actionsEl.append(externalLink("tmdb", tmdbMovieLink(detail.tmdb_id)));
|
||||
const linkTitle = current?.title ?? "this movie";
|
||||
actionsEl.append(
|
||||
externalLink(
|
||||
providerMark("tmdb"),
|
||||
`open ${linkTitle} on TMDB`,
|
||||
tmdbMovieLink(detail.tmdb_id),
|
||||
),
|
||||
);
|
||||
if (detail.imdb_id !== null) {
|
||||
actionsEl.append(externalLink("imdb", imdbLink(detail.imdb_id)));
|
||||
actionsEl.append(
|
||||
externalLink(providerMark("imdb"), `open ${linkTitle} on IMDb`, imdbLink(detail.imdb_id)),
|
||||
);
|
||||
}
|
||||
if (current !== null) {
|
||||
actionsEl.append(
|
||||
externalLink("rotten tomatoes", rottenTomatoesSearch(current.title, current.year)),
|
||||
externalLink(
|
||||
providerMark("rt"),
|
||||
`search Rotten Tomatoes for ${current.title}`,
|
||||
rottenTomatoesSearch(current.title, current.year),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1906,7 +2034,8 @@ function movieMain(views: HideableView[]): MovieView {
|
||||
filesSection.hidden = true;
|
||||
diskRows.replaceChildren();
|
||||
sweep.disabled = false;
|
||||
back.focus();
|
||||
titleEl.setAttribute("tabindex", "-1");
|
||||
titleEl.focus();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -1929,8 +2058,6 @@ function movieMain(views: HideableView[]): MovieView {
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
back.addEventListener("click", close);
|
||||
|
||||
// capture + stopImmediatePropagation: one Escape steps back one layer —
|
||||
// the library and search decks also listen for Escape on this window
|
||||
window.addEventListener(
|
||||
@@ -3125,7 +3252,6 @@ const SERIES_REFRESH_WAIT_MS = 30_000;
|
||||
function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
const view = must<HTMLElement>("#series");
|
||||
const deckEl = must<HTMLElement>("#deck");
|
||||
const back = must<HTMLButtonElement>("#series-back");
|
||||
const hero = must<HTMLElement>("#series-hero");
|
||||
const poster = must<HTMLImageElement>("#series-poster");
|
||||
const titleEl = must<HTMLElement>("#series-title");
|
||||
@@ -3230,6 +3356,8 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
}
|
||||
titleEl.textContent = current.title;
|
||||
yearEl.textContent = current.year === null ? "" : String(current.year);
|
||||
remove.setAttribute("aria-label", `remove ${current.title} from the library`);
|
||||
remove.title = `remove ${current.title} from the library`;
|
||||
chipsEl.replaceChildren();
|
||||
const root = roots.find((candidate) => candidate.id === current.root_id);
|
||||
chipsEl.append(chip(root ? root.audience : `root ${current.root_id}`));
|
||||
@@ -3332,26 +3460,41 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
trailer.textContent = "trailer";
|
||||
actionsEl.append(trailer);
|
||||
}
|
||||
actionsEl.append(externalLink("tmdb", tmdbSeriesLink(detail.tmdb_id)));
|
||||
const linkTitle = series?.title ?? "this series";
|
||||
actionsEl.append(
|
||||
externalLink(
|
||||
providerMark("tmdb"),
|
||||
`open ${linkTitle} on TMDB`,
|
||||
tmdbSeriesLink(detail.tmdb_id),
|
||||
),
|
||||
);
|
||||
// a series has no imdb_id in this app; TVDB is its second id (§9.6)
|
||||
if (detail.tvdb_id !== null) {
|
||||
actionsEl.append(externalLink("tvdb", tvdbLink(detail.tvdb_id)));
|
||||
actionsEl.append(
|
||||
externalLink(providerMark("tvdb"), `open ${linkTitle} on TVDB`, tvdbLink(detail.tvdb_id)),
|
||||
);
|
||||
}
|
||||
if (series !== null) {
|
||||
actionsEl.append(
|
||||
externalLink("rotten tomatoes", rottenTomatoesSearch(series.title, series.year)),
|
||||
externalLink(
|
||||
providerMark("rt"),
|
||||
`search Rotten Tomatoes for ${series.title}`,
|
||||
rottenTomatoesSearch(series.title, series.year),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function seasonCountsChip(season: ApiSeason): HTMLSpanElement {
|
||||
const counts = seasonCounts(season);
|
||||
return countsChip(
|
||||
`${counts.available}/${counts.wanted} on disk`,
|
||||
const span = countsChip(
|
||||
`${counts.available}/${counts.wanted}`,
|
||||
counts.available,
|
||||
counts.wanted,
|
||||
`${counts.available} of ${counts.wanted} wanted episodes on disk`,
|
||||
);
|
||||
span.prepend(icon("disk"));
|
||||
return span;
|
||||
}
|
||||
|
||||
function seasonRow(season: ApiSeason): HTMLLIElement {
|
||||
@@ -3418,8 +3561,14 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
|
||||
const deckBtn = document.createElement("button");
|
||||
deckBtn.type = "button";
|
||||
deckBtn.className = "control";
|
||||
deckBtn.textContent = "deck";
|
||||
deckBtn.className = "control control-icon";
|
||||
const deckLabel =
|
||||
season.number === 0
|
||||
? "open the specials release deck"
|
||||
: `open the season ${PAD_TWO(season.number)} release deck`;
|
||||
deckBtn.setAttribute("aria-label", deckLabel);
|
||||
deckBtn.title = deckLabel;
|
||||
deckBtn.append(icon("search"));
|
||||
deckBtn.addEventListener("click", () => {
|
||||
const currentId = seriesId;
|
||||
const currentTitle = series?.title ?? "";
|
||||
@@ -3452,26 +3601,25 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
// #174: files go, episodes stop being wanted, the season stays listed.
|
||||
// Only offered with files on disk — intent alone is the tracked toggle.
|
||||
if (season.episodes.some((episode) => filesByEpisode.has(episode.id))) {
|
||||
const clear = armedDelete("remove files", () => {
|
||||
const currentId = seriesId;
|
||||
if (currentId === null) {
|
||||
return;
|
||||
}
|
||||
void removeSeasonFiles(currentId, season.number).then((outcome) => {
|
||||
if (outcome.kind === "error") {
|
||||
clear.disabled = false;
|
||||
setStatus(`remove failed — ${outcome.detail}`, "fault");
|
||||
const clear = armedDeleteIcon(
|
||||
`remove ${season.number === 0 ? "specials" : `season ${PAD_TWO(season.number)}`} files from disk and stop wanting its episodes — the season stays listed`,
|
||||
() => {
|
||||
const currentId = seriesId;
|
||||
if (currentId === null) {
|
||||
return;
|
||||
}
|
||||
// the control itself disappears with the files; the tracked
|
||||
// toggle is the season's control that survives the repaint
|
||||
focusKey = `track-${season.number}`;
|
||||
void load();
|
||||
});
|
||||
});
|
||||
clear.setAttribute(
|
||||
"aria-label",
|
||||
`remove ${season.number === 0 ? "specials" : `season ${PAD_TWO(season.number)}`} files from disk and stop wanting its episodes — the season stays listed`,
|
||||
void removeSeasonFiles(currentId, season.number).then((outcome) => {
|
||||
if (outcome.kind === "error") {
|
||||
clear.disabled = false;
|
||||
setStatus(`remove failed — ${outcome.detail}`, "fault");
|
||||
return;
|
||||
}
|
||||
// the control itself disappears with the files; the tracked
|
||||
// toggle is the season's control that survives the repaint
|
||||
focusKey = `track-${season.number}`;
|
||||
void load();
|
||||
});
|
||||
},
|
||||
);
|
||||
line.append(clear);
|
||||
}
|
||||
@@ -3539,21 +3687,20 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
if (aired && onDisk) {
|
||||
// #174: the file goes and the episode stops being wanted; the row
|
||||
// stays listed. Same arm-then-confirm as a settings row.
|
||||
const clear = armedDelete("remove file", () => {
|
||||
void removeEpisodeFiles(episode.id).then((outcome) => {
|
||||
if (outcome.kind === "error") {
|
||||
clear.disabled = false;
|
||||
setStatus(`remove failed — ${outcome.detail}`, "fault");
|
||||
return;
|
||||
}
|
||||
// once missing, the row's want control is what remains to focus
|
||||
focusKey = `want-${episode.id}`;
|
||||
void load();
|
||||
});
|
||||
});
|
||||
clear.setAttribute(
|
||||
"aria-label",
|
||||
const clear = armedDeleteIcon(
|
||||
`remove the ${episodeTag(seasonNumber, episode.number)} file from disk and stop wanting the episode — it stays listed`,
|
||||
() => {
|
||||
void removeEpisodeFiles(episode.id).then((outcome) => {
|
||||
if (outcome.kind === "error") {
|
||||
clear.disabled = false;
|
||||
setStatus(`remove failed — ${outcome.detail}`, "fault");
|
||||
return;
|
||||
}
|
||||
// once missing, the row's want control is what remains to focus
|
||||
focusKey = `want-${episode.id}`;
|
||||
void load();
|
||||
});
|
||||
},
|
||||
);
|
||||
actions.append(clear);
|
||||
} else if (!aired) {
|
||||
@@ -3584,8 +3731,11 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
|
||||
const deckBtn = document.createElement("button");
|
||||
deckBtn.type = "button";
|
||||
deckBtn.className = "control";
|
||||
deckBtn.textContent = "deck";
|
||||
deckBtn.className = "control control-icon";
|
||||
const deckLabel = `open the ${episodeTag(seasonNumber, episode.number)} release deck`;
|
||||
deckBtn.setAttribute("aria-label", deckLabel);
|
||||
deckBtn.title = deckLabel;
|
||||
deckBtn.append(icon("search"));
|
||||
deckBtn.addEventListener("click", () => {
|
||||
const currentId = seriesId;
|
||||
const currentTitle = series?.title ?? "";
|
||||
@@ -3793,7 +3943,8 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
view.hidden = false;
|
||||
clearRemove();
|
||||
clearRichDetail();
|
||||
back.focus();
|
||||
titleEl.setAttribute("tabindex", "-1");
|
||||
titleEl.focus();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -3814,8 +3965,6 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
||||
origin?.focus();
|
||||
}
|
||||
|
||||
back.addEventListener("click", close);
|
||||
|
||||
// capture, like every other layer: the tv deck's listener is registered
|
||||
// first, so one Esc steps back one layer
|
||||
window.addEventListener(
|
||||
|
||||
@@ -470,6 +470,62 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ---- icon-only controls (#230) ---------------------------------------- */
|
||||
|
||||
/* One block, shared: title pages, decks and readouts here, the settings
|
||||
rows via #231. The glyph carries the action, the aria-label carries the
|
||||
meaning, and the geometry stays on the --control-h rail with the chips. */
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
flex: none;
|
||||
height: 0.875rem;
|
||||
width: auto;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
min-width: var(--control-h);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
/* provider wordmark badges (§9.6): the mark is drawn and shipped inline,
|
||||
monochrome on currentColor — never a remote image */
|
||||
.icon-mark {
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.icon-mark rect {
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
/* the mark's letters live in viewBox units, so the size rides the SVG
|
||||
scale as an attribute; only voice and weight come from the tokens */
|
||||
.icon-mark text {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
font-family: var(--font-readout);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* a readout chip's glyph sits beside its figure, sized to the readout */
|
||||
.chip .icon {
|
||||
height: 0.75rem;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
/* rows widen every .control's padding; an icon control stays near-square */
|
||||
.season-line .control-icon,
|
||||
.ep-actions .control-icon,
|
||||
.movie-controls .control-icon {
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
/* ---- unified search (§9.2) ------------------------------------------- */
|
||||
|
||||
.rail-search {
|
||||
@@ -528,6 +584,12 @@ body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* movie and series: the hero banner meets the rail — no top padding (#229) */
|
||||
.deck#movie,
|
||||
.deck#series {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.deck-status {
|
||||
margin: 0 0 var(--space-4);
|
||||
color: var(--ink-muted);
|
||||
|
||||
Reference in New Issue
Block a user