All checks were successful
Deploy / deploy (push) Successful in 24s
Requested directly: "can we make activity orange". The locked eight palette had already run out at six legible text colours (Watch's blue was the last one, and it's already a contrast exception); a dithered swatch alone read as unfinished, and dithering the word itself rendered as illegible static — tried and reverted this session. --tt-orange (#ff8000) is defined in base.css right after the locked eight, clearly marked as a deliberate, acknowledged one-off exception to "eight colours, no ninth" rather than a secretly-smuggled-in tint. Same flat text+swatch treatment as every other fastext key. Updated CLAUDE.md's discipline 1 and DESIGN.md's Locked Eight Rule to name the exception explicitly, so it reads as a documented decision next time someone audits the palette, not a bug.
349 lines
28 KiB
Markdown
349 lines
28 KiB
Markdown
---
|
||
name: Media Centre
|
||
description: A broadcast information service for a self-hosted media stack — teletext's locked eight colours on pure black, no borders, no boxes, no radius.
|
||
colors:
|
||
tt-black: "#000000"
|
||
tt-red: "#ff0000"
|
||
tt-green: "#00ff00"
|
||
tt-yellow: "#ffff00"
|
||
tt-blue: "#0000ff"
|
||
tt-magenta: "#ff00ff"
|
||
tt-cyan: "#00ffff"
|
||
tt-white: "#ffffff"
|
||
ink-dim: "#949494"
|
||
ink-dimmer: "#4d4d4d"
|
||
typography:
|
||
double-height:
|
||
fontFamily: "DotGothic16, 'Chivo Mono', ui-monospace, Menlo, monospace"
|
||
fontSize: "1.875rem"
|
||
fontWeight: 400
|
||
lineHeight: 1.15
|
||
letterSpacing: "0.02em"
|
||
double-height-mobile:
|
||
fontFamily: "DotGothic16, 'Chivo Mono', ui-monospace, Menlo, monospace"
|
||
fontSize: "1.375rem"
|
||
fontWeight: 400
|
||
lineHeight: 1.15
|
||
letterSpacing: "0.02em"
|
||
body:
|
||
fontFamily: "'Chivo Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
||
fontSize: "0.9375rem"
|
||
fontWeight: 400
|
||
lineHeight: 1.45
|
||
letterSpacing: "normal"
|
||
fontFeature: "tabular-nums"
|
||
label:
|
||
fontFamily: "'Chivo Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
||
fontSize: "0.9375rem"
|
||
fontWeight: 400
|
||
lineHeight: 1.45
|
||
letterSpacing: "0.16em"
|
||
textTransform: "uppercase"
|
||
field-title:
|
||
fontFamily: "'Chivo Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
||
fontSize: "0.9375rem"
|
||
fontWeight: 400
|
||
lineHeight: 1.45
|
||
letterSpacing: "0.22em"
|
||
textTransform: "uppercase"
|
||
input:
|
||
fontFamily: "'Chivo Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
||
fontSize: "16px"
|
||
fontWeight: 400
|
||
lineHeight: 1.45
|
||
letterSpacing: "normal"
|
||
rounded:
|
||
none: "0"
|
||
spacing:
|
||
cell: "1ch"
|
||
row: "1.359rem"
|
||
gutter: "1.5rem"
|
||
gutter-mobile: "1rem"
|
||
tap: "44px"
|
||
measure: "92ch"
|
||
components:
|
||
button-primary:
|
||
backgroundColor: "{colors.tt-yellow}"
|
||
textColor: "{colors.tt-black}"
|
||
typography: "{typography.label}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0 1.5ch"
|
||
height: "{spacing.tap}"
|
||
button-primary-hover:
|
||
backgroundColor: "{colors.tt-cyan}"
|
||
textColor: "{colors.tt-black}"
|
||
button-primary-armed:
|
||
backgroundColor: "{colors.tt-red}"
|
||
textColor: "{colors.tt-white}"
|
||
button-primary-done:
|
||
backgroundColor: "transparent"
|
||
textColor: "{colors.tt-green}"
|
||
button-primary-disabled:
|
||
backgroundColor: "{colors.ink-dimmer}"
|
||
textColor: "{colors.ink-dim}"
|
||
button-quiet:
|
||
backgroundColor: "transparent"
|
||
textColor: "{colors.tt-cyan}"
|
||
typography: "{typography.label}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0 1ch"
|
||
height: "{spacing.tap}"
|
||
button-quiet-hover:
|
||
backgroundColor: "{colors.tt-cyan}"
|
||
textColor: "{colors.tt-black}"
|
||
input-field:
|
||
backgroundColor: "{colors.tt-black}"
|
||
textColor: "{colors.tt-yellow}"
|
||
typography: "{typography.input}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0 1ch"
|
||
height: "{spacing.tap}"
|
||
field-band:
|
||
backgroundColor: "{colors.tt-blue}"
|
||
textColor: "{colors.tt-yellow}"
|
||
typography: "{typography.field-title}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0 1ch"
|
||
height: "2.175rem"
|
||
field-band-warn:
|
||
backgroundColor: "{colors.tt-yellow}"
|
||
textColor: "{colors.tt-black}"
|
||
field-band-crit:
|
||
backgroundColor: "{colors.tt-red}"
|
||
textColor: "{colors.tt-white}"
|
||
index-row:
|
||
backgroundColor: "transparent"
|
||
textColor: "{colors.tt-white}"
|
||
typography: "{typography.body}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0 1ch"
|
||
height: "{spacing.tap}"
|
||
index-row-hover:
|
||
backgroundColor: "{colors.tt-yellow}"
|
||
textColor: "{colors.tt-black}"
|
||
toast:
|
||
backgroundColor: "{colors.tt-green}"
|
||
textColor: "{colors.tt-black}"
|
||
typography: "{typography.label}"
|
||
rounded: "{rounded.none}"
|
||
padding: "0.6em 1.5rem"
|
||
toast-error:
|
||
backgroundColor: "{colors.tt-red}"
|
||
textColor: "{colors.tt-white}"
|
||
---
|
||
|
||
# Design System: Media Centre
|
||
|
||
## Overview
|
||
|
||
**Creative North Star: "Page 100"**
|
||
|
||
Media Centre is a broadcast information service, not a dashboard. It refuses the bordered-card grid that every self-hosted *arr dashboard ships — including this one, until this redesign. The world is teletext: Ceefax and the American cable guide channel, fused. Eight colours at full saturation on pure black, one blocky monospace at exactly two heights, a character-cell grid, and grouping carried entirely by colour fields and row weight. The visitor types or clicks a page number, reads the machine's state off colour alone, and leaves knowing whether anything needs them.
|
||
|
||
The density is high and unapologetic. A teletext page was 40 columns; this one gets 92 (`--page-cols: 92ch`) because the screen allows it, but the character grid is the substrate everywhere — padding is measured in `ch`, bars are measured in cells, and nothing floats free of the row rhythm (`--row`, one character row). There is no chrome. The header is a row of the page, not furniture bolted above it, so it scrolls away with everything else. The fastext keys sit at the top of every page but the index — which closes with them instead, since its own body already is the full nav — never fixed to the viewport.
|
||
|
||
The confirmed anti-reference is the world this replaced and every dashboard that shares its reflexes: the rounded, bordered, drop-shadowed card grid with a neutral dark-grey palette and a soft accent. Nothing here is enclosed. There is not one `border-radius` and not one `box-shadow` in the codebase — the single `text-shadow` is a phosphor bloom, and it is spent deliberately. Emphasis is bought with colour and with doubled height, never with a weight ramp, never with a third type size, never with a container.
|
||
|
||
**Key Characteristics:**
|
||
- Eight locked colours at full saturation on pure black; a ninth tone is a dither, never a new colour
|
||
- Zero enclosure: no border, no box, no radius, no shadow, anywhere
|
||
- Exactly two type sizes (15px and 30px), one weight, two blocky monospace faces
|
||
- Every bar in the service shares one 20-cell ruler, so any two percentages compare by eye
|
||
- Grouping by full-bleed colour band, indentation and row weight
|
||
- The page number is the navigation: type three digits anywhere and the service jumps
|
||
- Pages differ in stock — a page holding critical state is set heavier than a quiet one
|
||
|
||
## Colors
|
||
|
||
Teletext had exactly eight colours, at full saturation, and the product's semantics were already the same semantics: green runs, yellow works, red is off air. The palette is locked; softening a value is a regression, because the emissive-on-black scene is what full saturation is for.
|
||
|
||
### Primary
|
||
|
||
- **Service Yellow** (`#ffff00`, `--voice`): the service speaking in its own voice. Mastheads, page numbers, the row number on every index line, section-band titles, the caret and focus reversal. It is also the "warn" tier on any threshold-coloured value. The loudest colour on the page, and the one the visitor's eye is trained to follow to a heading.
|
||
- **Transmission Cyan** (`#00ffff`, `--live`): live values and now-showing. The header clock, active figures, links, the currently-playing title at double height. Cyan means *this number is moving*.
|
||
|
||
### Secondary
|
||
|
||
- **Signal Green** (`#00ff00`, `--ok`): running, healthy, available. Service ON AIR, array usage under 75%, progress bars on a healthy stream, the confirmation toast.
|
||
- **Off-Air Red** (`#ff0000`, `--alert`): off air, failed, danger. Degraded-service copy, OFF AIR rows, usage at or above 90%, disk temperature at or above 55°C, the armed state of a destructive button.
|
||
- **Request Magenta** (`#ff00ff`, `--pending`): waiting on a human. Ombi's pending count and requested events, and nothing else. Its rarity is the point — magenta on the page means someone is owed a decision.
|
||
|
||
### Tertiary
|
||
|
||
- **Field Blue** (`#0000ff`, `--field`): the only colour in the system used as a background rather than as ink. It draws the full-bleed section band and the table-row hover. It never sets text.
|
||
|
||
### Neutral
|
||
|
||
- **Transmission White** (`#ffffff`, `--ink`): body text, row titles, table row headers. The default reading colour.
|
||
- **Pure Black** (`#000000`): the ground, everywhere, unconditionally — `html`, `body`, `theme-color`, scrollbar tracks, input fills. There is no second surface colour, because there are no surfaces.
|
||
- **Dim Ink** (`#949494`, `--ink-dim`, white at 58%): the *only* dimmed value permitted to carry text. At 58% it clears 4.5:1 on black. Secondary row copy, labels, timestamps, the flat trend arrow.
|
||
- **Dither Grey** (`#4d4d4d`, `--ink-dimmer`, white at 30%): graphics only — bar troughs, the mosaic separator, the empty-artwork field, the disabled-button fill, scrollbar thumbs. At 30% it fails text contrast outright, and text set in it has already had to be fixed once.
|
||
|
||
### Named Rules
|
||
|
||
**The Locked Eight Rule.** Eight colours exist. Adding a ninth hue, or desaturating one of the eight to make it sit more politely, is a regression rather than a variation. If a new state needs a colour, it takes one of the eight and inherits that colour's meaning. **One acknowledged exception:** `--tt-orange` in `base.css`, added for the fastext nav's eighth destination once the locked eight ran out of colours legible as text on black — a one-time, explained break, not a precedent for a second.
|
||
|
||
**The Dither Rule.** A mid-tone is an ordered dither of two of the eight at cell scale — a checkerboard or a repeating gradient stepped at `--dither-size: 2px` or at `--bar-cell-w` — exactly how a 16-colour screen faked a gradient. It is never a ninth colour smuggled into the palette. The two derived greys are the sanctioned exception, and they derive from white by `color-mix`, not by hand.
|
||
|
||
**The One Meaning Rule.** Each colour means one thing across all nine pages. Cyan is never decorative; magenta never appears outside a pending request; red never marks emphasis, only failure. A reader who learns the palette on P100 can read P105 without looking at a label.
|
||
|
||
**The Text Contrast Rule.** `--ink-dim` is the floor for anything a person reads. `--ink-dimmer` draws graphics and never sets type.
|
||
|
||
## Typography
|
||
|
||
**Display Font:** DotGothic16 (with Chivo Mono, `ui-monospace`, Menlo fallback)
|
||
**Body Font:** Chivo Mono (with `ui-monospace`, SFMono-Regular, Menlo, Consolas fallback)
|
||
|
||
**Character:** Two blocky monospaces doing one job between them. Chivo Mono sets the entire service at one size and one weight — even table headers are explicitly `font-weight: 400` — with `tabular-nums` on so columns of figures line up on the character grid without a table cell holding them. DotGothic16 carries every double-height row: a pixel-derived face whose letterforms read as broadcast rather than as terminal, and the reason a masthead feels transmitted rather than typeset. Anti-aliasing is left on but `text-rendering: optimizeSpeed` keeps the letterforms crisp rather than blending them toward tones the palette does not contain.
|
||
|
||
### Hierarchy
|
||
|
||
- **Double height** (DotGothic16, 400, `1.875rem` / 30px, line-height 1.15, `0.02em`): the only size other than body text. Page mastheads, the now-showing title, and the big figures on System, Library and Requests where the number *is* the content. Steps down to `1.375rem` below 760px, because a true 30px double-height row eats a phone viewport.
|
||
- **Body** (Chivo Mono, 400, `0.9375rem` / 15px, line-height 1.45, tabular figures): everything else. Row titles, table cells, meta lines, error copy, hints. There is no third size and no second weight.
|
||
- **Label** (Chivo Mono, 400, 15px, `0.16em`, uppercase): row names, figure captions, buttons, fastext keys, table headers, disclosure summaries. Letterspacing and case do the work a smaller size or a lighter weight would do in a scale-based system.
|
||
- **Field title** (Chivo Mono, 400, 15px, `0.22em`, uppercase): the section band's own text, and the header row's service name. The widest tracking in the system, reserved for the two places that announce a region.
|
||
- **Input** (Chivo Mono, 400, `16px` literal): the one hardcoded pixel size in the codebase, and it is deliberate — anything under 16px makes iOS Safari zoom the page on focus, which would break the character grid mid-interaction.
|
||
|
||
### Named Rules
|
||
|
||
**The Two Heights Rule.** Emphasis comes from colour and from doubling the height. It never comes from a weight ramp and never from a third size. Chivo Mono 600 is loaded but unused; reaching for it is the first sign the hierarchy is being solved the wrong way.
|
||
|
||
**The Bloom Rule.** Phosphor bloom (`text-shadow: 0 0 0.4em currentColor@55%`) is opt-in via `.bloom` and is spent only where the world is speaking loudly — page mastheads and the now-showing title. Bloom under a dense table of figures costs more legibility than it buys character, so the double-height figures on System and Library carry none.
|
||
|
||
**The Uppercase-Is-A-Role Rule.** Uppercase plus tracking marks a label, a key or a band title. Content — a show name, a username, a raw error — is never uppercased for effect.
|
||
|
||
## Layout
|
||
|
||
One measure for the whole service: `min(92ch, 100%)`, centred, with a `1.5rem` gutter that narrows to `1rem` below 760px. The page itself is the only thing ever centred; everything inside it is flush left against the character grid.
|
||
|
||
The vertical rhythm is the character row: `--row` is `--fs × --lh` (≈21.75px), and every margin in the system is a multiple or simple fraction of it (`--row`, `calc(--row * 0.75)`, `calc(--row * 1.5)`, `calc(--row * 2)`). Horizontal rhythm is measured in character cells: block padding is `1ch`, gaps between row parts are `1ch`–`1.5ch`, figure columns are `4ch` apart, tile columns `2ch`. Interactive rows and controls are held to `--tap: 44px` minimum height.
|
||
|
||
The page is a single column of stacked regions, in the order: masthead → colour-field band → content block → band → block. The only grids in the system are the tile grid (`repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr))`), the fastext key strip (4 columns, 2 on a phone), and the settings key panel (`minmax(min(30ch, 100%), 1fr)`). There is no card grid, and no multi-column region whose columns have to be equalised — the failure mode that dominated the previous design cannot occur here.
|
||
|
||
Responsive behaviour is minimal by construction, because a character-cell grid narrows on its own. Three real moves: at 760px the double-height row drops to `1.375rem`, the fastext strip goes to two columns rather than shrinking a key below a tap target, and the shared bar ruler narrows from 20 cells to 14. At 560px a list row wraps — its description drops to a full-width third line under the number, name and value, rather than the row growing wider than the screen — and the header clock takes its own line.
|
||
|
||
### Named Rules
|
||
|
||
**The One Ruler Rule.** Every bar, meter and percentage in the service shares one scale, `--bar-cells`, and is drawn by the `bar()` macro and nowhere else. 60% on the array and 60% on an indexer are the same physical width and compare directly. Restyling a bar per context is precisely what the ruler exists to prevent.
|
||
|
||
**The Page Number Is The Navigation Rule.** Every page owns a three-digit number (100 Index, 101 Watch, 102 Library, 103 Discover, 104 Requests, 105 System, 106 Activity, 900 Settings, 000 Sign In). That number is the header's first element, the row label on the index, the browser title prefix, and what a visitor types to jump. There is no separate nav bar, and adding one would be a regression.
|
||
|
||
**The Index Is The Status Page Rule.** On P100 every destination row carries its own live figure in its own status colour, so choosing where to go and reading how things are is one act. A navigation row without a reading is an incomplete row.
|
||
|
||
## Elevation & Depth
|
||
|
||
**There are no shadows.** The codebase contains zero `box-shadow` declarations and zero `border-radius` declarations — this is checkable with a single grep, and it is the audit test for the whole system. Nothing is lifted, nothing is layered, nothing casts.
|
||
|
||
Depth is conveyed three ways, all of them native to a phosphor screen rather than to paper. First, **emission**: full-saturation colour on pure black reads as lit, and brightness alone separates a live cyan figure from dim-grey secondary copy. Second, **reversal**: a region comes forward by inverting — a coloured band with black or yellow text reversed out of it is the only "raised" gesture the system has, and it is how section headers, hover, focus, buttons and the toast all announce themselves. Third, **bloom**: the single `text-shadow` in the system is a phosphor halo at 55% of the element's own colour, applied only to mastheads and the now-showing title, so the loudest rows glow slightly the way a CRT let bright phosphor spill.
|
||
|
||
### Named Rules
|
||
|
||
**The No Enclosure Rule.** Nothing is ever surrounded by a border or a box. Grouping is a coloured field row, indentation and weight. This is the exact reflex the previous design was built on and the main thing this world refuses — an added card, panel, outline, divider rule or hairline is a regression, not a refinement.
|
||
|
||
**The Reversed Cell Rule.** Focus and hover are a reversed cell, the way a teletext menu row selected itself: `outline: none`, background set to the state colour, text to black. A focus ring is an enclosure and is therefore banned; the reversal is what replaces it, and it must be present on every interactive element so keyboard focus stays visible.
|
||
|
||
**The Two Sanctioned Lines Rule.** Exactly two horizontal lines exist, and both are characters in the grid rather than borders on a box: the mosaic separator (`.tt-sep`, a repeating block-graphic run in dither grey) and the underscore run beneath an input, which is how a teletext form showed a field and which turns yellow on focus. Neither may be repurposed as a divider between arbitrary regions.
|
||
|
||
## Shapes
|
||
|
||
The corner language is zero radius, everywhere, with no scale — `rounded.none: "0"` is the entire ramp, and there is no step above it to reach for. Every rectangle in the system is a hard-cornered cell: buttons, inputs, colour bands, poster tiles, bar segments, the fastext key swatch, the toast.
|
||
|
||
Form comes from the character grid rather than from silhouette. A poster tile is a flat `2/3` rectangle with `object-fit: cover`, sitting directly on the black with its label below — no frame, no rounding, no shadow. When artwork fails to load, the tile shows a 45° dithered field rather than a fake poster or a placeholder icon. The bar is a run of discrete mosaic cells, not a continuous track: both the trough and the fill are repeating gradients stepped at `--bar-cell-w` with a 2px gap, so a bar always reads as *n* whole blocks lit out of 20. The fastext key carries its colour as a solid `1.2ch × 1em` cell at its head — the physical coloured button on the remote.
|
||
|
||
Iconography is drawn from characters, never from an icon set: `▲ ▼ ▬` for trend, `▸ ▾` for a disclosure marker. There are no glyph icons and no SVG furniture apart from the favicon.
|
||
|
||
## Components
|
||
|
||
### Buttons
|
||
|
||
- **Shape:** hard-cornered cell (`0` radius), `border: none`, minimum `44px` tall, `0 1.5ch` padding, uppercase at `0.16em`.
|
||
- **Primary:** a reversed row of cells — solid Service Yellow ground, black text. That is the entire fill vocabulary; there is no outlined or filled-vs-tonal distinction.
|
||
- **Hover:** ground swaps to Transmission Cyan, text stays black. **Focus:** the global reversed-cell treatment (yellow ground, black text, no ring).
|
||
- **Armed:** a destructive action arms on first press — ground goes Off-Air Red, white text, label becomes `CONFIRM?`. **Sending:** label becomes `SENDING`, disabled. **Done:** ground drops to transparent, text Signal Green, label `ADDED`. **Disabled:** dither-grey ground, dim-ink text.
|
||
- **Quiet:** transparent ground, cyan text, `0 1ch`; inverts to a cyan ground with black text on hover. Used for the secondary save actions on Settings.
|
||
- **Small:** `.tt-btn-sm` drops the height to `calc(--row * 1.6)` and goes full-width inside a tile.
|
||
|
||
### Inputs / Fields
|
||
|
||
- **Style:** black ground, Service Yellow text, no border and no box. The field is marked by a 2px underscore run pinned to the bottom edge — a repeating block-graphic gradient in dither grey. `44px` minimum height, `16px` type (the iOS anti-zoom literal).
|
||
- **Focus:** the underscore run turns Service Yellow. Nothing else changes; there is no ring, no glow, no border colour shift.
|
||
- **Placeholder:** dim ink. **Caret:** Service Yellow. **Select:** the same field with `appearance: none` and black-on-yellow options.
|
||
- **Label:** sits above the field in dim ink, uppercase at `0.16em`. **Hint:** dim ink directly under. **Inline error:** Off-Air Red with a reserved `min-height: 1em` so the layout does not jump when it appears.
|
||
|
||
### Colour Field Band (section header)
|
||
|
||
- **Style:** a full-bleed band of Field Blue with the section title reversed out in Service Yellow, uppercase at `0.22em`, `calc(--row * 1.6)` tall, `0 1ch` padding. Optional right-aligned meta in white at `0.06em`.
|
||
- **This is what replaces every card header and every 1px border in the previous design.** A region begins at a band and ends where the next one starts.
|
||
- **Stock variants:** `.warn` turns the band Service Yellow with black text; `.crit` turns it Off-Air Red with white text. This is how a page carrying critical state is set heavier than a quiet one.
|
||
|
||
### Index / List Row
|
||
|
||
- **Style:** a flush-left row at `44px` minimum height with four optional parts in fixed order — page number (yellow, `0.08em`), name (uppercase, `0.16em`, `11ch` minimum so names column up), body (title plus dim-ink subtitle, ellipsised), and a right-flushed value.
|
||
- **Value tones:** cyan by default, with `.ok` green, `.alert` red, `.warn` yellow, `.pending` magenta, `.off` dim ink.
|
||
- **Hover / focus (links only):** the whole row reverses to a Service Yellow ground with every child forced to black — number, value and subtitle included.
|
||
- **Mobile:** below 560px the body drops to its own full-width line beneath the number, name and value.
|
||
|
||
### Tables
|
||
|
||
- **Style:** no rules, no zebra, no borders, no cell backgrounds. Columns are held by alignment and the monospace grid alone. `0.35em 1ch` cell padding; numeric columns right-aligned with `2ch` trailing space.
|
||
- **Head:** Service Yellow, uppercase, `0.16em`, `font-weight: 400` like everything else. **Row header:** white. **Cells:** dim ink, with `.val` cells in cyan.
|
||
- **Hover:** the row reverses to a Field Blue ground with white text. **Overflow:** the table scrolls inside `.tt-table-scroll` rather than the page scrolling sideways.
|
||
|
||
### Tiles
|
||
|
||
- **Style:** a flat `2/3` poster on the black grid, `object-fit: cover`, dither-grey ground while loading. Title below in white (single line, ellipsised), subtitle in dim ink with an optional yellow badge (the score, the age) leading it. Optional full-width small button underneath.
|
||
- **Empty state:** a 45° dithered field, never a placeholder illustration or icon.
|
||
- **Grid:** `repeat(auto-fill, minmax(min(12.5ch, 100%), 1fr))` at `--row` × `2ch` gaps.
|
||
|
||
### Navigation (Fastext)
|
||
|
||
- **Style:** four coloured keys as the last row of the page — red, green, yellow, cyan — each a `44px` row with a solid `1.2ch` colour cell at its head and its destination in white, uppercase at `0.16em`. Two columns below 760px.
|
||
- **Deliberately not fixed to the viewport.** It is the last row of the page, where it always was.
|
||
- **Hover / focus:** the key fills with its own colour and the label goes black.
|
||
- **Behaviour:** clicking a key or pressing its initial (`r`, `g`, `y`, `c`) navigates. Each page may name its own four destinations, so the keys always point somewhere useful from where you are.
|
||
|
||
### Degraded Service
|
||
|
||
- **Style:** the failure state is a page that did not arrive, announced in Off-Air Red on the row where the content should have been. No panel, no left-border accent, no icon, no illustration.
|
||
- A friendly human cause in red, with the raw exception behind a `▸ TECHNICAL DETAIL` disclosure (dim-ink summary, `44px`-class tap height, native marker suppressed). The raw block sits on a dither-grey ground in white at body size and wraps rather than scrolling.
|
||
- Where the failure is structural to a whole region, the region's colour band goes `.crit` above it.
|
||
|
||
### Toast
|
||
|
||
- **Style:** a full-width reversed strip pinned to the bottom edge — Signal Green ground, black uppercase text at `0.16em`; Off-Air Red with white text on failure. Slides up 100% and fades in over 150ms. Not a floating card, and not rounded.
|
||
|
||
### Signature: The Page-Number Jump
|
||
|
||
The service's defining interaction. Typing any three digits anywhere on any page jumps to that page, exactly as a teletext remote did — no field to focus, no command palette to open. The digits paint into the header as you type them (`P1__` → `P10_` → `P100`) so the interaction shows its own state instead of happening invisibly. An unknown number sits on `P123 ?` for 1.2s and clears; a half-typed number times out after 2s; `Escape` clears immediately. It stands down entirely while the visitor is in an input, textarea, select or contenteditable, and while a modifier key is held.
|
||
|
||
### Signature: The One Ruler
|
||
|
||
Every percentage in the service is drawn by one macro at one width — 20 cells (14 on a phone), filled in whole cells, rounded so a bar never ends mid-character. Colour is chosen by threshold unless a tone is passed explicitly: green under 75%, yellow from 75%, red from 90%. Because array usage, disk usage, stream progress and indexer grab-share all render on the same ruler, any two of them can be compared by eye across pages.
|
||
|
||
## Do's and Don'ts
|
||
|
||
### Do:
|
||
|
||
- **Do** take every colour from the locked eight (`#000000 #ff0000 #00ff00 #ffff00 #0000ff #ff00ff #00ffff #ffffff`) at full saturation, and keep each one's single meaning: yellow speaks, cyan is live, green is healthy, red is off air, magenta wants a human, blue is the only background.
|
||
- **Do** group with a full-bleed colour band, indentation and row weight. A `.tt-field` opens a region; the next one closes it.
|
||
- **Do** draw every bar, meter and percentage through the `bar()` macro so the whole service shares one ruler.
|
||
- **Do** set a page carrying critical state heavier than a quiet one — `.tt-field.warn` when something wants attention, `.tt-field.crit` when a region is down. Uniform texture across the pages is a failure.
|
||
- **Do** reverse the cell for hover and focus (state colour ground, black text), and make sure every interactive element has it.
|
||
- **Do** make a tone that isn't one of the eight by dithering at cell scale, using `--ink-dim` for anything readable and `--ink-dimmer` for graphics only.
|
||
- **Do** give a new page a three-digit number, register it in `nav_items`, and put that number in the header, the browser title and its row on P100.
|
||
- **Do** keep spacing on the grid: `1ch` horizontal, multiples of `--row` vertical, `44px` minimum on anything tappable.
|
||
- **Do** keep inputs at the `16px` literal, so iOS does not zoom the page on focus.
|
||
- **Do** draw a trend arrow only where the backend genuinely holds a comparison. Today that is one place — SABnzbd's current speed against its stored 7-day average — because no history exists for disk usage or temperature. An invented trend is worse than no trend; extend the rule by adding history, not by adding arrows.
|
||
|
||
### Don't:
|
||
|
||
- **Don't** introduce a ninth colour, a tint, or a desaturated version of one of the eight.
|
||
- **Don't** enclose anything. No card, no panel, no `border`, no `border-radius`, no `outline` ring, no hairline divider, no `box-shadow`. The audit test is a grep: `border-radius` and `box-shadow` must return zero hits across `static/` and `templates/`.
|
||
- **Don't** add a third type size or a second font weight. If something needs emphasis, colour it or double its height.
|
||
- **Don't** put bloom on body copy, on tables of figures, or on anything that is not a masthead or the now-showing title.
|
||
- **Don't** restyle a bar for one context, or give a card its own scale — that breaks the one ruler and with it every cross-page comparison.
|
||
- **Don't** set text in `--ink-dimmer`; it fails contrast on black and has had to be corrected once already.
|
||
- **Don't** fix the fastext strip, the header or anything else to the viewport. Everything scrolls, because everything is a row of the page.
|
||
- **Don't** solve overflow with an inner scroll area, a truncated list, or content hidden behind a toggle. Give the content the page's full measure, or lay it out in more columns. The one sanctioned scroller is `.tt-table-scroll`, and only to keep a wide table from scrolling the whole page sideways.
|
||
- **Don't** reach for a glyph icon set. Trend and disclosure marks are characters (`▲ ▼ ▬ ▸ ▾`); artwork is the only non-character content in the world.
|
||
- **Don't** animate anything beyond the page-transmission stagger and the toast. A teletext page painted in a few rows at a time and then held still.
|