arr-summary/static/css/teletext.css
Barely Removable d672c3f02d
All checks were successful
Deploy / deploy (push) Successful in 21s
Show SABnzbd's queue in the header on every page
Adds a live download figure next to the header clock, present on
every page (not just Activity/System). It's populated by the same
/api/sabnzbd poll those pages already use, extended to run
unconditionally and to fill in a new #head-dl span — never a
server-rendered fetch, so an unreachable SABnzbd can't add latency to
pages that don't otherwise touch it. Quiet ink at "0 IN QUEUE" when
idle, live cyan with a speed reading once something's moving.
2026-08-30 08:54:41 -07:00

707 lines
24 KiB
CSS

/* ═══════════════════════════════════════════════════════
Page 100 — the service.
Every rule here answers to one of the five disciplines in
base.css. The two that get broken by reflex, and so get
guarded hardest: NO ENCLOSURE (there is not a single
border-radius or box-drawing border in this file) and ONE
RULER (every bar is --bar-cells wide, no exceptions).
═══════════════════════════════════════════════════════ */
/* ── Header row ─────────────────────────────────────────
Teletext's top line, unchanged for forty years: the page
number you are on, who is transmitting, and the clock. It
is a row of the page, not furniture bolted above it, so it
scrolls away like everything else. */
.tt-head {
max-width: min(var(--page-cols), 100%);
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0 1ch;
padding: var(--row) var(--gutter) 0;
margin: 0 auto;
}
.tt-head-num {
font-family: var(--font-dh);
color: var(--voice);
letter-spacing: 0.08em;
}
.tt-head-name {
color: var(--ink);
letter-spacing: 0.22em;
text-transform: uppercase;
}
.tt-head-clock {
color: var(--live);
white-space: nowrap;
min-width: 0;
}
/* The page-number entry indicator. Hidden until the visitor starts
typing digits, then fills in place — the signature interaction. */
.tt-entry {
color: var(--voice);
letter-spacing: 0.3em;
}
.tt-entry[hidden] { display: none; }
/* SABnzbd's queue, live on every page — filled in by service.js, empty
(and so invisible) until its first poll returns. Quiet ink when
idle; the live colour only while something is actually moving. */
.tt-head-dl {
margin-left: auto;
color: var(--ink-dim);
white-space: nowrap;
min-width: 0;
}
.tt-head-dl.live { color: var(--live); }
/* ── Page ───────────────────────────────────────────────
One measure for every page in the service. Nothing is ever
centred except the page itself. */
.tt-page {
max-width: min(var(--page-cols), 100%);
margin: 0 auto;
padding: 0 var(--gutter) calc(var(--row) * 2);
}
/* The page masthead: double height in the service's own colour. */
.tt-masthead { color: var(--voice); margin-bottom: calc(var(--row) * 0.75); }
/* A rows block that follows another block rather than opening one. */
.tt-rows-after { margin-top: var(--row); }
/* Double-height. The only size other than body text, and the
only place bloom is allowed. */
.tt-dh {
font-family: var(--font-dh);
font-size: var(--fs2);
line-height: 1.15;
letter-spacing: 0.02em;
margin: 0;
}
/* ── Field row (discipline 2: grouping without enclosure) ──
A section is announced by a full-bleed band of colour with
the title reversed out of it. This is what replaces every
card header and every 1px border in the old design. */
.tt-field {
display: flex;
align-items: baseline;
gap: 1ch;
margin: calc(var(--row) * 1.5) 0 var(--row);
padding: 0 1ch;
background: var(--field);
color: var(--voice);
letter-spacing: 0.22em;
text-transform: uppercase;
min-height: calc(var(--row) * 1.6);
}
.tt-field-meta {
margin-left: auto;
color: var(--tt-white);
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* A page carrying critical state gets a heavier field than a quiet
one (discipline 5) — the band goes red and doubles in height. */
.tt-field.crit {
background: var(--alert);
/* Black, not white: white-on-red measures 4.0:1 here, under the
4.5:1 floor for 15px uppercase text. Black-on-red clears 5.25:1
and is the correct teletext reversal — every other coloured band
(.warn, a hover row) already reverses to black. */
color: var(--tt-black);
}
.tt-field.crit .tt-field-meta { color: var(--tt-black); }
.tt-field.warn { background: var(--voice); color: var(--tt-black); }
.tt-field.warn .tt-field-meta { color: var(--tt-black); }
/* ── Mosaic separator ───────────────────────────────────
Teletext's block graphics doing the job a rule would do in a
world that allowed rules. It is content in the grid, not a
border on a box. */
.tt-sep {
height: 0.4em;
margin: var(--row) 0;
background-image: repeating-linear-gradient(
90deg,
var(--ink-dimmer) 0 0.6em,
transparent 0.6em 0.9em
);
}
/* ── Index / list rows ──────────────────────────────────
The index page IS the status page: every destination row
carries its own live figure in its own colour, so reading the
menu and reading the state are the same act. */
.tt-rows { display: flex; flex-direction: column; }
.tt-row {
display: flex;
align-items: center;
gap: 1.5ch;
min-height: var(--tap);
padding: 0 1ch;
color: var(--ink);
text-decoration: none;
}
a.tt-row:hover, a.tt-row:focus-visible {
background: var(--voice);
color: var(--tt-black);
outline: none;
}
a.tt-row:hover .tt-row-num,
a.tt-row:hover .tt-row-val,
a.tt-row:hover .tt-row-sub,
a.tt-row:focus-visible .tt-row-num,
a.tt-row:focus-visible .tt-row-val,
a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.tt-row-num {
color: var(--voice);
flex: 0 0 auto;
letter-spacing: 0.08em;
}
.tt-row-name {
flex: 0 0 auto;
letter-spacing: 0.16em;
text-transform: uppercase;
min-width: 11ch;
}
.tt-row-body { flex: 1; min-width: 0; }
.tt-row-title {
overflow-wrap: anywhere;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt-row-sub { color: var(--ink-dim); }
.tt-row-val {
flex: 0 0 auto;
margin-left: auto;
color: var(--live);
white-space: nowrap;
}
.tt-row-val.ok { color: var(--ok); }
.tt-row-val.alert { color: var(--alert); }
.tt-row-val.warn { color: var(--voice); }
.tt-row-val.pending { color: var(--pending); }
.tt-row-val.off { color: var(--ink-dim); }
/* ── The one ruler (discipline 3) ───────────────────────
Every bar in the service is exactly --bar-cells cells wide and
fills in whole cells. Two percentages anywhere in the app are
therefore directly comparable by eye. Never restyle this per
card; that is precisely what it exists to prevent. */
.tt-bar {
display: inline-block;
width: min(calc(var(--bar-cells) * var(--bar-cell-w)), 100%);
height: 0.8em;
flex: 0 0 auto;
vertical-align: -0.05em;
background-image: repeating-linear-gradient(
90deg,
var(--ink-dimmer) 0 calc(var(--bar-cell-w) - 2px),
transparent calc(var(--bar-cell-w) - 2px) var(--bar-cell-w)
);
}
.tt-bar > i {
display: block;
height: 100%;
background-image: repeating-linear-gradient(
90deg,
currentColor 0 calc(var(--bar-cell-w) - 2px),
transparent calc(var(--bar-cell-w) - 2px) var(--bar-cell-w)
);
}
.tt-bar.ok { color: var(--ok); }
.tt-bar.warn { color: var(--voice); }
.tt-bar.alert { color: var(--alert); }
/* ── Trend (discipline 4) ───────────────────────────────
Only rendered where the backend actually carries a comparison
(current speed against the 7-day average, a temperature
against its threshold). Where there is no history there is no
arrow — an invented trend is worse than no trend. */
.tt-trend { flex: 0 0 auto; letter-spacing: 0.05em; }
.tt-trend.up { color: var(--ok); }
.tt-trend.down { color: var(--voice); }
.tt-trend.flat { color: var(--ink-dim); }
/* ── Figures ────────────────────────────────────────────
A labelled number, set at double height because on this page
the number is the content. */
.tt-figs {
display: flex;
flex-wrap: wrap;
gap: 0 4ch;
margin: var(--row) 0;
padding: 0 1ch;
}
.tt-fig { min-width: 12ch; }
.tt-fig-v { color: var(--live); }
.tt-fig-v.ok { color: var(--ok); }
.tt-fig-v.alert { color: var(--alert); }
.tt-fig-v.warn { color: var(--voice); }
.tt-fig-v.pending { color: var(--pending); }
.tt-fig-l {
color: var(--ink-dim);
letter-spacing: 0.16em;
text-transform: uppercase;
}
/* ── Now showing ────────────────────────────────────────
The lower two-thirds of the index. Double height, cyan, with
the progress as a run of mosaic cells. */
.tt-now { margin: var(--row) 0 0; padding: 0 1ch; }
.tt-now-title { color: var(--live); margin-bottom: calc(var(--row) * 0.4); }
.tt-now-meta { color: var(--ink); }
.tt-now-line {
display: flex;
align-items: center;
gap: 1.5ch;
margin-top: calc(var(--row) * 0.5);
color: var(--live);
}
.tt-now-pct { color: var(--live); flex: 0 0 auto; }
/* ── Table ──────────────────────────────────────────────
No rules, no zebra, no borders. Columns are held by alignment
and the monospace grid; the head row is a colour field. */
.tt-table-scroll { overflow-x: auto; }
.tt-table {
width: 100%;
border-collapse: collapse;
white-space: nowrap;
}
.tt-table th, .tt-table td {
padding: 0.35em 1ch 0.35em 0;
text-align: left;
font-weight: 400;
}
.tt-table thead th {
color: var(--voice);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.tt-table tbody th { color: var(--ink); }
.tt-table td { color: var(--ink-dim); }
.tt-table .num { text-align: right; padding-right: 2ch; }
.tt-table .val { color: var(--live); }
.tt-table tbody tr:hover th,
.tt-table tbody tr:hover td { background: var(--field); color: var(--tt-white); }
/* ── Disk rows (System, phone reflow of the table above) ──
Below 560px a six-column table stops fitting; scrolling it
horizontally hides the bar and the temperature, the two
readings that matter most here. This is the other rendering
of the same disk data, shown only below that width — see the
media query at the bottom of this file for the swap. */
.tt-disk-rows { display: none; flex-direction: column; }
.tt-disk-row { padding: 0.4em 1ch; }
.tt-disk-row-top {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4em 1ch;
}
.tt-disk-name {
flex: 0 0 auto;
min-width: 5ch;
color: var(--ink);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.tt-disk-pct {
flex: 0 0 auto;
color: var(--live);
font-variant-numeric: tabular-nums;
}
.tt-disk-row-sub {
margin-top: 0.15em;
color: var(--ink-dim);
}
.tt-disk-row:hover { background: var(--field); }
.tt-disk-row:hover .tt-disk-name,
.tt-disk-row:hover .tt-disk-pct,
.tt-disk-row:hover .tt-disk-row-sub { color: var(--tt-white); }
/* ── Tiles (library / discover) ─────────────────────────
Artwork is the one thing in this world that is not made of
characters, so it is treated as an inserted transmission: a
flat rectangle on the grid with its label in a colour field
beneath. No rounding, no shadow, no border. */
.tt-tiles {
display: grid;
gap: var(--row) 2ch;
grid-template-columns: repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr));
padding: 0 1ch;
}
/* P100's Just Added: a fixed row, not a viewport-dependent auto-fill.
Paired with a fixed item count (see home()), this guarantees a full
row every time rather than leaving a lone tile beside empty tracks —
and it keeps this block compact, so NOW SHOWING above it can hold the
lower two-thirds of the first viewport the direction contract names. */
.tt-tiles-row {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 560px) {
.tt-tiles-row { grid-template-columns: repeat(1, 1fr); }
}
.tt-tile { display: flex; flex-direction: column; min-width: 0; }
.tt-tile-art {
display: block;
width: 100%;
aspect-ratio: 2 / 3;
object-fit: cover;
/* Covers the box while a real poster is still loading, and IS the
fallback for a poster that never arrives — dithered, per
discipline 1, never a flat mid-grey panel. */
background-color: var(--tt-black);
background-image: var(--dither-mid);
}
.tt-tile-t {
margin-top: 0.4em;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt-tile-s { color: var(--ink-dim); }
.tt-tile-badge { color: var(--voice); }
.tt-tile-p { margin-top: 0.35em; }
/* ── Controls ───────────────────────────────────────────
A teletext control is a reversed row of cells. That is the
entire button vocabulary: no fill, no radius, no border. */
.tt-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--tap);
padding: 0 1.5ch;
background: var(--voice);
color: var(--tt-black);
border: none;
font-family: inherit;
font-size: inherit;
letter-spacing: 0.16em;
text-transform: uppercase;
cursor: pointer;
}
.tt-btn:hover:not(:disabled) { background: var(--live); }
.tt-btn:disabled {
background-color: var(--tt-black);
background-image: var(--dither-mid);
color: var(--ink-dim);
cursor: default;
}
.tt-btn.armed { background: var(--alert); color: var(--tt-black); }
.tt-btn.done { background: transparent; color: var(--ok); cursor: default; }
.tt-btn-quiet {
background: transparent;
color: var(--live);
padding: 0 1ch;
}
.tt-btn-quiet:hover:not(:disabled) { background: var(--live); color: var(--tt-black); }
.tt-btn-sm { min-height: calc(var(--row) * 1.6); width: 100%; }
.tt-input {
min-height: var(--tap);
padding: 0 1ch;
background: var(--tt-black);
color: var(--voice);
border: none;
/* The one place a line is allowed: an underscore run, which is
how a teletext form showed a field. It is a character row, not
an enclosure. */
background-image: repeating-linear-gradient(
90deg,
var(--ink-dimmer) 0 0.6em,
transparent 0.6em 0.9em
);
background-size: 100% 2px;
background-repeat: no-repeat;
background-position: 0 100%;
font-family: inherit;
font-size: 16px; /* stops iOS zooming the page on focus */
outline: none;
}
.tt-input::placeholder { color: var(--ink-dim); }
.tt-input:focus {
background-image: repeating-linear-gradient(
90deg,
var(--voice) 0 0.6em,
transparent 0.6em 0.9em
);
background-size: 100% 2px;
background-repeat: no-repeat;
background-position: 0 100%;
}
/* ── Degraded service ───────────────────────────────────
Teletext's own failure state was a page that simply did not
arrive, announced in red on the row where it should have been.
No panel, no left-border accent, no icon. */
.tt-err { padding: 0 1ch; color: var(--alert); }
.tt-err-cause { color: var(--alert); }
.tt-err details { margin-top: 0.3em; }
.tt-err summary {
color: var(--ink-dim);
cursor: pointer;
list-style: none;
letter-spacing: 0.16em;
text-transform: uppercase;
min-height: calc(var(--row) * 1.6);
display: flex;
align-items: center;
}
.tt-err summary::-webkit-details-marker { display: none; }
.tt-err summary::before { content: "\25B8\00a0"; }
.tt-err details[open] summary::before { content: "\25BE\00a0"; }
.tt-err summary:hover { color: var(--ink); }
.tt-err pre {
margin: 0.3em 0 0;
padding: 0.5em 1ch;
background-color: var(--tt-black);
background-image: var(--dither-mid);
color: var(--ink);
font-family: inherit;
font-size: inherit;
white-space: pre-wrap;
word-break: break-word;
}
.tt-empty { padding: 0 1ch; color: var(--ink-dim); }
/* ── Fastext ────────────────────────────────────────────
The four coloured keys. Real navigation, not decoration:
click one, or press its initial. Deliberately NOT fixed to
the viewport — it is a row of the page, where it always
was, not furniture bolted over it. On the index it closes
the page, under Now Showing/Just Added, because the index's
own body is already the full nav. Everywhere else — where
these four keys are the only nav on the page — it opens the
page, right under the header, so it doesn't need a scroll
to reach. */
.fastext {
display: grid;
grid-template-columns: repeat(4, 1fr);
max-width: min(var(--page-cols), 100%);
margin: calc(var(--row) * 2) auto 0;
padding: 0 var(--gutter);
gap: 1ch;
}
.fastext-top {
margin: var(--row) auto calc(var(--row) * 1.5);
}
.fastext a {
display: flex;
align-items: center;
min-width: 0;
gap: 1ch;
min-height: var(--tap);
padding: 0 1ch;
color: var(--ink);
letter-spacing: 0.16em;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The key's colour is carried by a solid cell at its head — the
physical coloured button on the remote. */
.fastext a > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.fastext a::before {
content: "";
flex: 0 0 auto;
width: 1.2ch;
height: 1em;
background: currentColor;
}
.fastext .k-red { color: var(--alert); }
.fastext .k-green { color: var(--ok); }
.fastext .k-yellow { color: var(--voice); }
.fastext .k-cyan { color: var(--live); }
.fastext a:hover, .fastext a:focus-visible {
background: currentColor;
outline: none;
}
.fastext a:hover span, .fastext a:focus-visible span { color: var(--tt-black); }
/* ── Footer ───────────────────────────────────────────── */
.tt-foot {
max-width: min(var(--page-cols), 100%);
margin: var(--row) auto 0;
padding: 0 var(--gutter) var(--row);
display: flex;
gap: 2ch;
color: var(--ink-dim);
}
.tt-foot .v { margin-left: auto; }
/* ── Motion ─────────────────────────────────────────────
The one authored moment: a teletext page did not appear, it
was transmitted, painting in a few rows at a time. Sections
arrive in sequence on load. Nothing else in this world moves.
Content is visible by default; the animation only delays an
already-rendered row's opacity. */
@keyframes tt-in {
from { opacity: 0; }
to { opacity: 1; }
}
.tt-page > * {
animation: tt-in 0.18s steps(2, end) both;
}
.tt-page > :nth-child(1) { animation-delay: 0.00s; }
.tt-page > :nth-child(2) { animation-delay: 0.05s; }
.tt-page > :nth-child(3) { animation-delay: 0.10s; }
.tt-page > :nth-child(4) { animation-delay: 0.15s; }
.tt-page > :nth-child(5) { animation-delay: 0.20s; }
.tt-page > :nth-child(6) { animation-delay: 0.25s; }
.tt-page > :nth-child(7) { animation-delay: 0.30s; }
.tt-page > :nth-child(n+8) { animation-delay: 0.35s; }
@media (prefers-reduced-motion: reduce) {
.tt-page > * { animation: none; }
* { transition-duration: 0.01ms !important; }
}
/* ── Responsive ─────────────────────────────────────────
The grid is character-based, so it survives narrowing on its
own. Two real moves: the double-height row stops being double
on a phone (it would eat the viewport), and fastext stacks to
two columns rather than shrinking below a tap target. */
@media (max-width: 760px) {
/* --bar-cells stays 20 at every width, on purpose: the bar() macro in
_macros.html hardcodes the same 20 so it can quantise a percentage to
whole cells server-side. Two constants that have to agree is exactly
the kind of drift discipline 3 (One Ruler) exists to prevent — an
earlier version of this file set 14 here, which desynced the CSS
width from the markup's own cell math and could land a fill mid-cell
on a phone. --bar-cell-w is in ch, so the ruler still shrinks with
the font; it just does it as 20 smaller cells, never a different
cell count. */
:root {
--gutter: 1rem;
}
.tt-dh { font-size: 1.375rem; }
.fastext { grid-template-columns: repeat(2, 1fr); gap: 0; }
.tt-row-name { min-width: 9ch; }
.tt-figs { gap: 0 2.5ch; }
}
@media (max-width: 560px) {
/* Below this a row cannot hold number, name, description and value on
one line. The description drops to its own line under them rather
than the row growing wider than the screen. */
.tt-row {
gap: 0 1ch;
flex-wrap: wrap;
padding-top: 0.35em;
padding-bottom: 0.35em;
}
.tt-row-name { min-width: 0; }
.tt-row-body {
flex: 1 0 100%;
order: 3;
}
.tt-row-title { white-space: normal; }
.tt-row-val { margin-left: auto; white-space: normal; }
.tt-head-name { letter-spacing: 0.12em; }
.tt-head-dl { margin-left: 0; flex-basis: 100%; }
.tt-head-clock { margin-left: 0; flex-basis: 100%; }
/* The System disk table swaps for its row reflow here — see the
"Disk rows" block above. */
.tt-table-scroll { display: none; }
.tt-disk-rows { display: flex; }
}
/* ── Search ─────────────────────────────────────────────
A field and a reversed key. The underscore run under the
input is the only line in this world, and it is a character
row rather than a box around anything. */
.tt-search {
display: flex;
gap: 1ch;
align-items: stretch;
margin: var(--row) 0;
padding: 0 1ch;
}
.tt-search .tt-input { flex: 1; min-width: 0; }
@media (max-width: 560px) {
.tt-search { flex-direction: column; }
}
/* ── Forms ──────────────────────────────────────────────
A label above its field, the field marked by an underscore
run. No box, no group border — the colour field header above
each block is what says where one section ends. */
.tt-form {
display: flex;
flex-direction: column;
gap: var(--row);
align-items: flex-start;
margin: var(--row) 0 calc(var(--row) * 1.5);
padding: 0 1ch;
max-width: 52ch;
}
.tt-field-row { width: 100%; }
.tt-label {
display: block;
color: var(--ink-dim);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.tt-form .tt-input { width: 100%; }
.tt-select {
appearance: none;
-webkit-appearance: none;
color: var(--voice);
cursor: pointer;
}
.tt-select option { background: var(--tt-black); color: var(--voice); }
.tt-hint { margin: 0.2em 0 0; color: var(--ink-dim); }
.tt-inline-err { color: var(--alert); min-height: 1em; }
.tt-keys {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(30ch, 100%), 1fr));
gap: 0 3ch;
}
.tt-keys .tt-form { max-width: none; }
/* ── Toast ──────────────────────────────────────────────
Teletext announced itself in the header row, so a confirmation
lands as a reversed strip rather than a floating card. */
.tt-toast {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.6em var(--gutter);
background: var(--ok);
color: var(--tt-black);
letter-spacing: 0.16em;
text-transform: uppercase;
opacity: 0;
transform: translateY(100%);
transition: opacity 0.15s, transform 0.15s;
pointer-events: none;
}
.tt-toast.show { opacity: 1; transform: translateY(0); }
.tt-toast.error {
background: var(--alert);
color: var(--tt-black); /* same contrast fix as .tt-field.crit */
}
/* ── Sign-in ────────────────────────────────────────────
The one page with no navigation, so it centres its single
block rather than opening the full page measure. */
.tt-login .tt-page { max-width: 52ch; margin: 0 auto; }
.tt-login .tt-head { max-width: 52ch; }
.tt-login .tt-foot { max-width: 52ch; }