/* ═══════════════════════════════════════════════════════ 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; }