arr-summary/static/css/teletext.css
Barely Removable 9fba4f210e
All checks were successful
Deploy / deploy (push) Successful in 22s
Colour Watch's fastext label, not just its swatch
Swatch-only blue read as unfinished next to the other five fully-
coloured entries. The word now carries the colour like every other
key, same currentColor mechanism as the rest — accepting blue's
~2.4:1 contrast on black as a known, deliberate exception to the
4.5:1 bar everything else here clears.
2026-08-30 09:27:39 -07:00

749 lines
26 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;
}
.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, .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. Solid for most; k-blue and k-dither
override its background below, since a flat swatch can't carry
every one of the eight destinations without repeating a colour. */
.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); }
/* The one other legal way to get a distinct mark out of eight colours
without a ninth: an ordered dither, same mechanism as --dither-mid,
in place of a flat swatch. */
.fastext .k-dither::before { background: var(--dither-mid); }
.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; }