Replaces the neutral dark card-based theme with "Page 100": teletext's
locked eight colours at full saturation on pure black, numbered pages
reachable by typing the number (or the r/g/y/c fastext keys), and no
borders, boxes, or radius anywhere — grouping is carried by full-bleed
colour fields and row weight instead.
Chosen via the impeccable skill's replacement-world flow: seven visual
candidates derived from the audience's own world, a mandatory roll
assigned Ceefax/US-cable-guide teletext, six catalog challengers weighed
against it, and the user locked the assigned direction on the decision
page (seed aab4510e).
- New templates for all nine pages (home/watch/library/discover/
requests/system/activity/login/settings) behind a shared _shell.html
carrying the direction contract and _macros.html for the row/bar/tile
vocabulary; deletes the old single-page dashboard.html and its
dedicated stylesheets/script.
- static/css/base.css + teletext.css: the locked palette, a two-height
type scale (Chivo Mono / DotGothic16), and the five disciplines the
system is held to (no in-between tone, no enclosure, one shared bar
ruler, trend only where real history exists, pages weighted by state).
- static/js/service.js: header clock, page-number jump, fastext keys,
two-tap arm/confirm add, guarded live polling — replaces dashboard.js.
- app.py: nav_items now carry page numbers; fastext defaults added to
the page-chrome context processor; version bumped to 2.0.0.
- DESIGN.md + .impeccable/design.json regenerated from the built world
by the documenter pass; PRODUCT.md and CLAUDE.md updated to describe
the new system in place of the retired one.
- Fixed a live bug found along the way: settings.html called
url_for('dashboard'), an endpoint removed earlier in the branch, which
would have 500'd the Settings page.
- Fixed a One-Ruler violation the finish review's recapture request
surfaced: --bar-cells disagreed with the bar() macro's hardcoded cell
count below the 760px breakpoint, so a fill could land mid-cell on a
phone. Removed the mobile override; the ruler is now 20 cells at every
width, matching the macro unconditionally.
Passed two inspection rounds (desktop + true-390px mobile, via an
iframe harness since macOS Chrome clamps --window-size) and a finish
review; QA screenshots and interview logs are gitignored as scratch,
not shipped in the repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
625 lines
21 KiB
CSS
625 lines
21 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 {
|
|
margin-left: auto;
|
|
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; }
|
|
|
|
/* ── 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); color: var(--tt-white); }
|
|
.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); }
|
|
|
|
/* ── 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;
|
|
}
|
|
.tt-tile { display: flex; flex-direction: column; min-width: 0; }
|
|
.tt-tile-art {
|
|
display: block;
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
object-fit: cover;
|
|
background: var(--ink-dimmer);
|
|
}
|
|
/* No artwork came back. A dithered field, not a fake poster. */
|
|
.tt-tile-art-empty {
|
|
background-image: repeating-linear-gradient(
|
|
45deg,
|
|
var(--ink-dimmer) 0 1px,
|
|
var(--tt-black) 1px calc(var(--dither-size) * 2)
|
|
);
|
|
}
|
|
.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: var(--ink-dimmer); color: var(--ink-dim); cursor: default; }
|
|
.tt-btn.armed { background: var(--alert); color: var(--tt-white); }
|
|
.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: var(--ink-dimmer);
|
|
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 along the bottom of every teletext
|
|
page. Real navigation, not decoration: click one, or press
|
|
its initial. Deliberately NOT fixed to the viewport — it is
|
|
the last row of the page, where it always was. */
|
|
.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 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-clock { margin-left: 0; flex-basis: 100%; }
|
|
}
|
|
|
|
/* ── 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-white); }
|
|
|
|
/* ── 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; }
|