Give every fastext destination a distinct mark; add skip-link, fix aria-label
All checks were successful
Deploy / deploy (push) Successful in 23s

Colours: "if the intent is to have them all different colors than
lets have them all different colors." The locked eight only give five
safe as full text (blue fails contrast, black's invisible on black),
so the other three get their own treatments instead of sharing plain
ink: Watch gets blue as a swatch-only accent (the button cap's colour,
not the label), Activity gets an ordered-dither swatch (the other
legal way to mark something without a ninth colour), and Settings
alone keeps plain ink deliberately, as the one page with no live
status of its own.

Accessibility (items 1 and 3 from the design review, item 2 skipped
per request — the header's download figure stays live and unmuted):
- Added a skip-link, off-screen until focused, jumping past the
  8-item top nav straight to each page's #main-content.
- Renamed the nav's aria-label from "Fastext" (meaningless without
  the visual teletext context) to "Page navigation".
This commit is contained in:
Barely Removable 2026-08-30 09:24:52 -07:00
parent 15936027de
commit 5dd49c505c
4 changed files with 79 additions and 28 deletions

View file

@ -105,18 +105,22 @@ the user's Unraid home server. Server-side project directory is still named
under the header on every page but the index — those pages carry no
other nav, so it needs no scroll to reach. The index overrides it
with `HOME_FASTEXT`, the original four-entry row, as its own closing
line, since its body already lists every destination once. The
original four (Index/Library/Discover/System) keep their `r`/`g`/
`y`/`c` keyboard shortcut and colour; Requests also gets a colour
(magenta, `--pending`) without a shortcut — a deliberate fit, not
an arbitrary fifth pick, since Requests already is "waiting on a
human" everywhere else magenta appears. That's the ceiling: the
locked eight colours leave exactly five that read as legible text
on black (blue fails contrast there, black is invisible), so Watch/
Activity/Settings share plain ink rather than a sixth colour that
doesn't exist in this palette. Whichever entry matches the current
page renders as a `.fastext-here` span instead of a link — a page
linking to itself did nothing, so it isn't offered as a click.
line, since its body already lists every destination once. Every
destination gets its own distinct mark — the locked eight colours
only give five that read as legible text on black, so getting to
eight took three treatments: five flat colours as text+swatch
(Index/Library/Discover/System keep their `r`/`g`/`y`/`c` shortcut;
Requests gets magenta/`--pending` with no shortcut — a deliberate
fit, since Requests already is "waiting on a human" everywhere else
magenta appears); Watch gets blue as a swatch-only accent (`.k-blue`
— blue fails contrast as body text, so only the small cell carries
it, not the row's own label); Activity gets an ordered-dither swatch
(`.k-dither`, same mechanism as `--dither-mid`) rather than a flat
colour. Settings alone keeps plain ink with no swatch treatment —
deliberately, as the one page with no live status of its own.
Whichever entry matches the current page renders as a
`.fastext-here` span instead of a link — a page linking to itself
did nothing, so it isn't offered as a click.
- **Nothing is clipped, hidden, or sticky.** All seven page numbers and
every fastext entry stay visible at every width, reflowing to another
row rather than scrolling, truncating, or folding into a menu. No

34
app.py
View file

@ -1303,24 +1303,34 @@ def service_urls():
}
# Every destination, for the top of every page but the index. Order
# matches nav_items. The locked eight teletext colours leave exactly
# five that read as text on black without inventing a ninth tone or
# borrowing a colour that already means something else here: the four
# with a real keyboard shortcut (r/g/y/c), plus magenta for Requests —
# not arbitrary, it's the same --pending magenta a request already
# shows once approved elsewhere, and Requests IS "waiting on a human".
# Blue fails contrast as text and black is invisible on black, so the
# three destinations left over (Watch/Activity/Settings) share plain
# ink rather than a sixth colour that doesn't exist in this palette.
# Every destination, for the top of every page but the index, each
# with its own distinct treatment. Order matches nav_items. The
# locked eight teletext colours only give five that read as full-
# saturation text on black (blue fails contrast there), so getting all
# eight distinct without inventing a ninth colour takes three
# treatments, not one:
# - Five flat colours, text and swatch both: the four with a real
# keyboard shortcut (r/g/y/c), plus magenta for Requests — not
# arbitrary, it's the same --pending magenta a request already
# shows once approved elsewhere, and Requests IS "waiting on a
# human".
# - Blue as a swatch-only accent (Watch): legible as a small solid
# cell — "the physical coloured button on the remote" — but not as
# the row's own text colour, where it fails contrast.
# - An ordered dither swatch (Activity): the one other legal way to
# get a distinct mark out of this palette per base.css's own first
# discipline, without smuggling in a ninth hue.
# Settings keeps plain ink deliberately — the one purely administrative
# destination with no live status of its own, so it's the one page
# that gets no colour at all rather than an arbitrary last one.
FULL_FASTEXT = [
("k-red", "r", "100", "Index", "/"),
("", "", "101", "Watch", "/watch"),
("k-blue", "", "101", "Watch", "/watch"),
("k-green", "g", "102", "Library", "/library"),
("k-yellow", "y", "103", "Discover", "/discover"),
("k-magenta", "", "104", "Requests", "/requests"),
("k-cyan", "c", "105", "System", "/system"),
("", "", "106", "Activity", "/activity"),
("k-dither", "", "106", "Activity", "/activity"),
("", "", "900", "Settings", "/settings"),
]

View file

@ -8,6 +8,26 @@
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
@ -512,8 +532,10 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
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. */
/* 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;
@ -538,6 +560,15 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.fastext .k-yellow { color: var(--voice); }
.fastext .k-cyan { color: var(--live); }
.fastext .k-magenta { color: var(--pending); }
/* Blue fails contrast as body text on black, so it marks the swatch
only — the button cap's colour, not the printed label — while the
row's own text stays legible ink. */
.fastext .k-blue::before { background: var(--field); }
.fastext .k-blue:hover, .fastext .k-blue:focus-visible { background: 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;

View file

@ -17,6 +17,12 @@
</head>
<body data-timezone="{{ timezone }}" data-url-base="{{ request.script_root }}"
data-pages='{{ nav_items | tojson }}'>
{#- Skip-link: every page but the index now opens with a full nav
block before any real content — a keyboard or screen-reader visitor
would otherwise tab through all eight destinations on every single
visit. Off-screen until focused, then it's the first thing tabbing
into the page reaches. -#}
<a class="skip-link" href="#main-content">Skip to content</a>
<!--
THESIS: Media Centre is a broadcast information service, not a dashboard. It
refuses the bordered-card grid every self-hosted dashboard ships, this one
@ -56,7 +62,7 @@ provenance.
index's own body already is the full nav. Everywhere else it leads,
right under the header, since it's the only nav those pages carry. -#}
{% set fastext_nav %}
<nav class="fastext{{ '' if page == 'home' else ' fastext-top' }}" aria-label="Fastext">
<nav class="fastext{{ '' if page == 'home' else ' fastext-top' }}" aria-label="Page navigation">
{% for cls, key, num, label, href in fastext %}
{% if num == here[1] %}
<span class="fastext-here" aria-current="page"><span>{{ num }} {{ label }}</span></span>
@ -71,7 +77,7 @@ provenance.
{% if page != 'home' %}{{ fastext_nav }}{% endif %}
<main class="tt-page">
<main class="tt-page" id="main-content">
{% block content %}{% endblock %}
</main>