All checks were successful
Deploy / deploy (push) Successful in 55s
Every TV request now needs a decision, so P104 offers the two verdicts
inline on the rows actually waiting rather than sending the visitor to
Ombi's own UI.
Ombi is now the one service the app writes to. Three details the new
ombi_write() helper and /api/ombi/decide route have to get right:
approve is a POST but deny is a PUT; a TV decision acts on the child
request id, not the show, so get_ombi_summary() carries child["id"] for
TV rows and m["id"] for movies; and Ombi answers 200 with
{"result": false, ...} on a logical refusal, so the body is checked
rather than the status code. A successful decision clears _ombi_cache —
the 4-hour TTL would otherwise keep calling the row pending for the
rest of the afternoon.
The buttons reuse the existing two-tap arming and share its armed slot,
so arming one disarms the other: an approval starts a download and a
denial is somebody's answer, neither wants a stray tap. They reverse
into green and red rather than the shared red of .tt-btn.armed, so the
confirming press states which verdict it is about to send. After a
decision the row's status flips in place and the Pending figure and its
"N of M" both settle; at zero the page drops its warn weight, since
nothing waits on a human any more (discipline 5). Below 560px the
verdicts take a full line of their own rather than shrinking below the
tap target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNc3gd3GRPudFwgPXNEm8A
781 lines
27 KiB
CSS
781 lines
27 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).
|
|
═══════════════════════════════════════════════════════ */
|
|
|
|
/* ── Skip-link ──────────────────────────────────────────
|
|
Off-screen until it receives focus, then it's a plain field row
|
|
like any other — a solid colour ground, no border — sitting above
|
|
everything else so tabbing past it reaches the header next. */
|
|
.skip-link {
|
|
position: absolute;
|
|
top: -999px;
|
|
left: 0;
|
|
z-index: 1;
|
|
padding: 0.5em 1ch;
|
|
background: var(--field);
|
|
color: var(--ink);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.16em;
|
|
}
|
|
.skip-link:focus {
|
|
top: 0;
|
|
outline: none;
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
/* Marks a row that leaves the app (System's Transmission links to each
|
|
service's own UI) — everywhere else a .tt-row stays inside Media
|
|
Centre, so leaving needs its own quiet signal. */
|
|
.tt-row-ext { color: var(--ink-dim); }
|
|
.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;
|
|
}
|
|
/* The verdicts sit after the status they resolve, so the row reads
|
|
state-then-decision. Only pending rows carry one. */
|
|
.tt-row-acts {
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
gap: 1ch;
|
|
}
|
|
.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%; }
|
|
|
|
/* The two verdicts on a pending request (P104). Quiet until armed, then
|
|
each reverses into its own meaning rather than the shared red of
|
|
.tt-btn.armed — green is the go-ahead, red is the refusal — so the
|
|
confirming press says which verdict it is about to send and not merely
|
|
that something is live. Both come after .tt-btn.armed to win on
|
|
source order at equal specificity. */
|
|
.tt-btn-yes, .tt-btn-no {
|
|
background: transparent;
|
|
padding: 0 1ch;
|
|
letter-spacing: 0.12em;
|
|
}
|
|
.tt-btn-yes { color: var(--ok); }
|
|
.tt-btn-no { color: var(--alert); }
|
|
.tt-btn-yes:hover:not(:disabled),
|
|
.tt-btn-yes.armed { background: var(--ok); color: var(--tt-black); }
|
|
.tt-btn-no:hover:not(:disabled),
|
|
.tt-btn-no.armed { background: var(--alert); color: var(--tt-black); }
|
|
|
|
.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, .fastext-here {
|
|
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 identity is carried by a cell at its head — the physical
|
|
coloured button on the remote. */
|
|
.fastext a > span, .fastext-here > span {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.fastext a::before, .fastext-here::before {
|
|
content: "";
|
|
flex: 0 0 auto;
|
|
width: 1.2ch;
|
|
height: 1em;
|
|
background: currentColor;
|
|
}
|
|
/* The page you're already on: present for orientation, same shape as
|
|
its neighbours, but not a link — clicking your own destination did
|
|
nothing useful, so now it isn't offered as a click at all. */
|
|
.fastext-here {
|
|
color: var(--ink-dim);
|
|
cursor: default;
|
|
}
|
|
.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 .k-magenta { color: var(--pending); }
|
|
/* Pure blue on black is ~2.4:1 contrast — well under the 4.5:1 every
|
|
other body text in this app clears — but the word carries the
|
|
colour like every other key here, at the visitor's request. */
|
|
.fastext .k-blue { color: var(--field); }
|
|
/* --tt-orange is a genuine ninth colour, not one of the locked eight —
|
|
see its definition in base.css for why the palette ran out here. */
|
|
.fastext .k-orange { color: var(--tt-orange); }
|
|
.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; }
|
|
/* Name and status hold line one, the description takes line two, and
|
|
the verdicts take a full line of their own — at this width they
|
|
cannot share a line with the status without either one shrinking
|
|
below the tap target. */
|
|
.tt-row-acts {
|
|
flex: 1 0 100%;
|
|
order: 4;
|
|
justify-content: flex-end;
|
|
margin-top: 0.35em;
|
|
}
|
|
.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; }
|