Accessibility and narrow screens: focus rings, a skip link, named landmarks and aria-current, scrolling tab bars and tables, and WCAG AA text contrast in both themes checked by a test 756fe44a0f4d
closes #12
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-28 13:38 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
756fe44a0f4d5efcfbfa0c7747ec85228435519137a9abf8becf5af29096cd72- Depends on
- 19bb31ff1f5c 1ac84c62ba53 25ab8c73514a 3f4b0ea6eabf 42a0ba4bac4e 42fa95540468 5c6eb1feaa71 6ae41081ccd3 8d2caf122517 93be9ce4653e a160b98fb922 abb98fa9964e c40c2d3edb51 d23464c0ac60 de505df5cf8d
- Depended on by
- 166f6e9bf4f1 16ee5d19d220 d7d182467382 17ca6cddc534 c9aacc0e7dc1 757195ad7a5d c8df33a6829f a024e800af4e 36e5a9c15a84 554ccacfc123 66fd9d6b1452 e9bfea8e26db 83d5052bf76c e1ec9fbf538e 883c77eaf2c9
- In bookmarks
- api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
- In tags
- v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 31 atoms, 1 tree operation, 7 files
Browse the tree at this change
added crates/hub/tests/contrast.rs +75 −0
| 1 | + | //! Text contrast (hub spec §6.13): every text colour of each theme meets |
|
| 2 | + | //! WCAG AA (4.5:1) on every surface it is drawn on, and text never uses |
|
| 3 | + | //! `--dim`, which is for rules and decoration. |
|
| 4 | + | ||
| 5 | + | use std::collections::HashMap; |
|
| 6 | + | ||
| 7 | + | fn css() -> String { |
|
| 8 | + | std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../templates/style.css")).unwrap() |
|
| 9 | + | } |
|
| 10 | + | ||
| 11 | + | /// `--name:#rrggbb` pairs of the block that starts with `selector`. |
|
| 12 | + | fn theme(css: &str, selector: &str) -> HashMap<String, String> { |
|
| 13 | + | let start = css.find(selector).unwrap_or_else(|| panic!("{selector} missing")); |
|
| 14 | + | let body = &css[start..start + css[start..].find('}').unwrap()]; |
|
| 15 | + | let mut out = HashMap::new(); |
|
| 16 | + | for part in body.split("--").skip(1) { |
|
| 17 | + | let Some((name, rest)) = part.split_once(':') else { continue }; |
|
| 18 | + | let value = rest.trim_start(); |
|
| 19 | + | let valid = !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'); |
|
| 20 | + | if valid && value.len() >= 7 && value.starts_with('#') && value[1..7].chars().all(|c| c.is_ascii_hexdigit()) { |
|
| 21 | + | out.insert(name.to_string(), value[..7].to_lowercase()); |
|
| 22 | + | } |
|
| 23 | + | } |
|
| 24 | + | out |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | fn luminance(hex: &str) -> f64 { |
|
| 28 | + | let c = |i: usize| { |
|
| 29 | + | let x = u8::from_str_radix(&hex[i..i + 2], 16).unwrap() as f64 / 255.0; |
|
| 30 | + | if x <= 0.03928 { |
|
| 31 | + | x / 12.92 |
|
| 32 | + | } else { |
|
| 33 | + | ((x + 0.055) / 1.055).powf(2.4) |
|
| 34 | + | } |
|
| 35 | + | }; |
|
| 36 | + | 0.2126 * c(1) + 0.7152 * c(3) + 0.0722 * c(5) |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | fn ratio(a: &str, b: &str) -> f64 { |
|
| 40 | + | let (x, y) = (luminance(a), luminance(b)); |
|
| 41 | + | (x.max(y) + 0.05) / (x.min(y) + 0.05) |
|
| 42 | + | } |
|
| 43 | + | ||
| 44 | + | #[test] |
|
| 45 | + | fn text_meets_aa_in_both_themes() { |
|
| 46 | + | let css = css(); |
|
| 47 | + | let mut failures = Vec::new(); |
|
| 48 | + | for (name, sel) in [("dark", ":root {"), ("light", ":root[data-theme=\"light\"] {")] { |
|
| 49 | + | let t = theme(&css, sel); |
|
| 50 | + | let mut text: Vec<&str> = vec!["ink", "mute", "acc", "ok", "bad", "warn"]; |
|
| 51 | + | text.extend(t.keys().filter(|k| k.starts_with("sy-")).map(String::as_str)); |
|
| 52 | + | for fg in text { |
|
| 53 | + | for bg in ["bg", "panel", "soft"] { |
|
| 54 | + | let r = ratio(&t[fg], &t[bg]); |
|
| 55 | + | if r < 4.5 { |
|
| 56 | + | failures.push(format!("{name}: --{fg} on --{bg} is {r:.2}:1")); |
|
| 57 | + | } |
|
| 58 | + | } |
|
| 59 | + | } |
|
| 60 | + | for (fg, bg) in [("band-ink", "band"), ("add-ink", "add"), ("del-ink", "del"), ("now-ink", "now")] { |
|
| 61 | + | let r = ratio(&t[fg], &t[bg]); |
|
| 62 | + | if r < 4.5 { |
|
| 63 | + | failures.push(format!("{name}: --{fg} on --{bg} is {r:.2}:1")); |
|
| 64 | + | } |
|
| 65 | + | } |
|
| 66 | + | } |
|
| 67 | + | assert!(failures.is_empty(), "below WCAG AA:\n{}", failures.join("\n")); |
|
| 68 | + | } |
|
| 69 | + | ||
| 70 | + | #[test] |
|
| 71 | + | fn text_does_not_use_dim() { |
|
| 72 | + | let css = css(); |
|
| 73 | + | let offenders: Vec<&str> = css.lines().filter(|l| l.contains("color: var(--dim)") && !l.contains("border")).collect(); |
|
| 74 | + | assert!(offenders.is_empty(), "--dim is too faint for text:\n{}", offenders.join("\n")); |
|
| 75 | + | } |
modified crates/hub/tests/browse.rs +5 −1
| … | 128 unchanged lines |
||
| 129 | 129 | assert!(o.contains("href=\"/andy/demo/file/main/docs/guide.md\""), "README link rebased: {o}"); |
|
| 130 | 130 | assert!(o.contains(&format!("oxy clone {}/andy/demo", h.base))); |
|
| 131 | 131 | assert!(o.contains("2 changes")); |
|
| 132 | + | // Landmarks, skip link, current tab (hub spec §6.13). |
|
| 133 | + | assert!(o.contains("<a class=\"skip\" href=\"#main\">") && o.contains("<main id=\"main\"")); |
|
| 134 | + | assert!(o.contains("aria-label=\"Site\"") && o.contains("aria-label=\"Repository\"") && o.contains("aria-label=\"Account\"")); |
|
| 135 | + | assert!(o.contains("class=\"on\" aria-current=\"page\">Code</a>"), "the current tab is announced"); |
|
| 132 | 136 | assert!(o.contains("v0.1")); |
|
| 133 | 137 | assert!(o.contains("feature")); |
|
| 134 | 138 | ||
| … | 15 unchanged lines |
||
| 150 | 154 | // Sidebar, permalink, blame shading and "blame before this change" (hub spec §6.6). |
|
| 151 | 155 | assert!(f.contains("class=\"file-side\""), "sidebar"); |
|
| 152 | 156 | assert!(f.contains("href=\"/andy/demo/tree/main/docs\""), "sibling directory listed"); |
|
| 153 | - | let here = f.split("aria-current=\"page\">").nth(1).unwrap_or(""); |
|
| 157 | + | let here = f.split("class=\"here\" aria-current=\"page\">").nth(1).unwrap_or(""); |
|
| 154 | 158 | assert!(here.trim_start().starts_with("<a href=\"/andy/demo/file/main/src/main.rs\""), "the file marked"); |
|
| 155 | 159 | assert!(f.contains(&format!("data-permalink=\"/andy/demo/file/{c2}/src/main.rs\"")), "permalink at the head"); |
|
| 156 | 160 | assert!(b.contains(&format!("data-permalink=\"/andy/demo/file/{c2}/src/main.rs?blame=1\""))); |
|
| … | 120 unchanged lines |
modified docs/spec.md +1 −1
| … | 315 unchanged lines |
||
| 316 | 316 | ||
| 317 | 317 | ### 6.13 Keyboard and accessibility |
|
| 318 | 318 | ||
| 319 | - | `/` focuses search, `g c` / `g p` / `g i` jump to Code, Proposals, Issues, `y` on a tree or file page switches to its permalink, `?` shows the sheet. Every interactive element is reachable by keyboard; colour is never the only carrier of meaning (glyphs accompany every status colour); contrast meets WCAG AA in both themes. |
|
| 319 | + | `/` focuses search, `g c` / `g p` / `g i` jump to Code, Proposals, Issues, `y` on a tree or file page switches to its permalink, `?` shows the sheet. Every interactive element is reachable by keyboard and shows a visible focus ring; a "Skip to content" link comes first; the header, site band, repository and settings tabs are named navigation landmarks, and the current tab carries `aria-current`. Colour is never the only carrier of meaning (glyphs accompany every status colour). Text meets WCAG AA (4.5:1) on every surface in both themes, checked by a test over the theme variables; `--dim` is for rules and decoration, never text. Under 760 pixels the tab bars and wide tables scroll sideways instead of breaking the layout, sectioned pages stack their section list above the content, and the header search hides (the `/` key and the search page remain). |
|
| 320 | 320 | ||
| 321 | 321 | ## 7. Proposals and review |
|
| 322 | 322 | ||
| … | 245 unchanged lines |
modified templates/base.html +5 −4
| … | 8 unchanged lines |
||
| 9 | 9 | <script>/* Theme before first paint: the theme cookie (dark, light or auto; dark when unset), auto following the system. */(function(){var d=document.documentElement,m=document.cookie.match(/(?:^|; )theme=(light|dark|auto)/),p=m?m[1]:"dark",q=window.matchMedia&&matchMedia("(prefers-color-scheme: light)");function a(){d.dataset.theme=p==="auto"?(q&&q.matches?"light":"dark"):p;}d.dataset.themePref=p;a();if(q&&q.addEventListener)q.addEventListener("change",function(){if(d.dataset.themePref==="auto")d.dataset.theme=q.matches?"light":"dark";});})();</script> |
|
| 10 | 10 | </head> |
|
| 11 | 11 | <body{% block bodyattrs %}{% endblock %}> |
|
| 12 | + | <a class="skip" href="#main">Skip to content</a> |
|
| 12 | 13 | <header> |
|
| 13 | 14 | <a class="brand" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true"><defs><linearGradient id="oxyGradH" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0FF"/><stop offset="100%" stop-color="#0072FF"/></linearGradient></defs><path d="M -11 -6 L 11 -6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><path d="M -11 6 L 11 6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/><circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/></svg><span class="px">OXYGEN<em>HUB</em></span></a> |
|
| 14 | 15 | <form class="find" action="/search" method="get" role="search"><input type="search" name="q" placeholder="Search…" aria-label="Search" autocomplete="off"></form> |
|
| 15 | - | <nav> |
|
| 16 | + | <nav aria-label="Account"> |
|
| 16 | 17 | <button type="button" class="theme-toggle" data-theme-toggle aria-label="Theme" title="Theme"><span aria-hidden="true">◐</span></button> |
|
| 17 | 18 | {% if let Some(u) = shell.user %} |
|
| 18 | 19 | <a class="go" href="/new" title="New repository">+ NEW</a> |
|
| … | 6 unchanged lines |
||
| 25 | 26 | {% endif %} |
|
| 26 | 27 | </nav> |
|
| 27 | 28 | </header> |
|
| 28 | - | <div class="band"> |
|
| 29 | + | <nav class="band" aria-label="Site"> |
|
| 29 | 30 | <a href="/">{% if shell.user.is_some() %}Dashboard{% else %}Repositories{% endif %}</a> |
|
| 30 | 31 | <a href="/explore">Explore</a> |
|
| 31 | 32 | <a href="/help">Help</a> |
|
| … | 1 unchanged lines |
||
| 33 | 34 | <span class="grow"></span> |
|
| 34 | 35 | {% if shell.is_admin %}<a class="admin" href="/admin">Admin</a>{% endif %} |
|
| 35 | 36 | <a class="dim" href="/{{ shell.version }}" title="OxygenHub {{ shell.version }}" tabindex="-1">v{{ shell.version }}</a> |
|
| 36 | - | </div> |
|
| 37 | + | </nav> |
|
| 37 | 38 | {% block tabs %}{% endblock %} |
|
| 38 | - | <main{% block mainclass %}{% endblock %}> |
|
| 39 | + | <main id="main" tabindex="-1"{% block mainclass %}{% endblock %}> |
|
| 39 | 40 | {% block content %}{% endblock %} |
|
| 40 | 41 | </main> |
|
| 41 | 42 | <footer> |
|
| … | 19 unchanged lines |
modified templates/repo_tabs.html +14 −14
| 1 | 1 | {% macro tabs(repo, tab) %} |
|
| 2 | - | <div class="tabs"> |
|
| 2 | + | <nav class="tabs" aria-label="Repository"> |
|
| 3 | 3 | <div class="tabs-in"> |
|
| 4 | 4 | <span class="crumb"><a href="/{{ repo.owner }}">{{ repo.owner }}</a> / <a href="{{ repo.url }}"><strong>{{ repo.name }}</strong></a>{% if repo.visibility != "public" %} <span class="tag">{{ repo.visibility }}</span>{% endif %}</span> |
|
| 5 | - | <a href="{{ repo.url }}"{% if tab == "code" %} class="on"{% endif %}>Code</a> |
|
| 6 | - | <a href="{{ repo.url }}/changes/{{ repo.default_bookmark }}"{% if tab == "changes" %} class="on"{% endif %}>Changes</a> |
|
| 7 | - | <a href="{{ repo.url }}/proposals"{% if tab == "proposals" %} class="on"{% endif %}>Proposals{% if repo.open_proposals > 0 %} <span class="count-chip">{{ repo.open_proposals }}</span>{% endif %}</a> |
|
| 8 | - | <a href="{{ repo.url }}/issues"{% if tab == "issues" %} class="on"{% endif %}>Issues{% if repo.open_issues > 0 %} <span class="count-chip">{{ repo.open_issues }}</span>{% endif %}</a> |
|
| 9 | - | <a href="{{ repo.url }}/flow"{% if tab == "flow" %} class="on"{% endif %}>Flow</a> |
|
| 10 | - | <a href="{{ repo.url }}/bookmarks"{% if tab == "bookmarks" %} class="on"{% endif %}>Bookmarks</a> |
|
| 11 | - | <a href="{{ repo.url }}/releases"{% if tab == "releases" %} class="on"{% endif %}>Releases</a> |
|
| 12 | - | <a href="{{ repo.url }}/tags"{% if tab == "tags" %} class="on"{% endif %}>Tags</a> |
|
| 13 | - | <a href="{{ repo.url }}/conflicts"{% if tab == "conflicts" %} class="on"{% endif %}>Conflicts</a> |
|
| 14 | - | <a href="{{ repo.url }}/trust"{% if tab == "trust" %} class="on"{% endif %}>Trust</a> |
|
| 15 | - | <a href="{{ repo.url }}/pushes"{% if tab == "pushes" %} class="on"{% endif %}>Pushes</a> |
|
| 16 | - | {% if repo.can_maintain() %}<a href="{{ repo.url }}/settings"{% if tab == "settings" %} class="on"{% endif %}>Settings</a>{% endif %} |
|
| 5 | + | <a href="{{ repo.url }}"{% if tab == "code" %} class="on" aria-current="page"{% endif %}>Code</a> |
|
| 6 | + | <a href="{{ repo.url }}/changes/{{ repo.default_bookmark }}"{% if tab == "changes" %} class="on" aria-current="page"{% endif %}>Changes</a> |
|
| 7 | + | <a href="{{ repo.url }}/proposals"{% if tab == "proposals" %} class="on" aria-current="page"{% endif %}>Proposals{% if repo.open_proposals > 0 %} <span class="count-chip">{{ repo.open_proposals }}</span>{% endif %}</a> |
|
| 8 | + | <a href="{{ repo.url }}/issues"{% if tab == "issues" %} class="on" aria-current="page"{% endif %}>Issues{% if repo.open_issues > 0 %} <span class="count-chip">{{ repo.open_issues }}</span>{% endif %}</a> |
|
| 9 | + | <a href="{{ repo.url }}/flow"{% if tab == "flow" %} class="on" aria-current="page"{% endif %}>Flow</a> |
|
| 10 | + | <a href="{{ repo.url }}/bookmarks"{% if tab == "bookmarks" %} class="on" aria-current="page"{% endif %}>Bookmarks</a> |
|
| 11 | + | <a href="{{ repo.url }}/releases"{% if tab == "releases" %} class="on" aria-current="page"{% endif %}>Releases</a> |
|
| 12 | + | <a href="{{ repo.url }}/tags"{% if tab == "tags" %} class="on" aria-current="page"{% endif %}>Tags</a> |
|
| 13 | + | <a href="{{ repo.url }}/conflicts"{% if tab == "conflicts" %} class="on" aria-current="page"{% endif %}>Conflicts</a> |
|
| 14 | + | <a href="{{ repo.url }}/trust"{% if tab == "trust" %} class="on" aria-current="page"{% endif %}>Trust</a> |
|
| 15 | + | <a href="{{ repo.url }}/pushes"{% if tab == "pushes" %} class="on" aria-current="page"{% endif %}>Pushes</a> |
|
| 16 | + | {% if repo.can_maintain() %}<a href="{{ repo.url }}/settings"{% if tab == "settings" %} class="on" aria-current="page"{% endif %}>Settings</a>{% endif %} |
|
| 17 | 17 | </div> |
|
| 18 | - | </div> |
|
| 18 | + | </nav> |
|
| 19 | 19 | {% endmacro %} |
|
| 20 | 20 | ||
| 21 | 21 | {% macro sig(s) %}<span class="sig {{ s.class() }}" title="{{ s.title() }}">{{ s.glyph() }}</span>{% endmacro %} |
|
| … | 53 unchanged lines |
modified templates/settings_tabs.html +6 −6
| 1 | 1 | {% macro tabs(on) %} |
|
| 2 | - | <div class="tabs"><div class="tabs-in"> |
|
| 2 | + | <nav class="tabs" aria-label="Settings"><div class="tabs-in"> |
|
| 3 | 3 | <span class="crumb"><strong>Settings</strong></span> |
|
| 4 | - | <a href="/settings"{% if on == "profile" %} class="on"{% endif %}>Profile</a> |
|
| 5 | - | <a href="/settings/security"{% if on == "security" %} class="on"{% endif %}>Security</a> |
|
| 6 | - | <a href="/settings/keys"{% if on == "keys" %} class="on"{% endif %}>Keys</a> |
|
| 7 | - | <a href="/settings/tokens"{% if on == "tokens" %} class="on"{% endif %}>Tokens</a> |
|
| 4 | + | <a href="/settings"{% if on == "profile" %} class="on" aria-current="page"{% endif %}>Profile</a> |
|
| 5 | + | <a href="/settings/security"{% if on == "security" %} class="on" aria-current="page"{% endif %}>Security</a> |
|
| 6 | + | <a href="/settings/keys"{% if on == "keys" %} class="on" aria-current="page"{% endif %}>Keys</a> |
|
| 7 | + | <a href="/settings/tokens"{% if on == "tokens" %} class="on" aria-current="page"{% endif %}>Tokens</a> |
|
| 8 | 8 | <a href="/orgs/new">New organisation</a> |
|
| 9 | - | </div></div> |
|
| 9 | + | </div></nav> |
|
| 10 | 10 | {% endmacro %} |
modified templates/style.css +28 −11
| … | 16 unchanged lines |
||
| 17 | 17 | --add:#0d3a3a; --del:#3f1a2e; --add-ink:#8ff0d8; --del-ink:#ffa5b8; |
|
| 18 | 18 | --m1:#00f0ff; --m2:#d6dcea; --m3:#5aa8ff; --m4:#0072ff; --m5:#8592ad; |
|
| 19 | 19 | /* syntax */ |
|
| 20 | - | --sy-kw:#7fb4ff; --sy-str:#8ff0d8; --sy-num:#9fd3ff; --sy-cmt:#5b6785; --sy-fn:#00e0f0; --sy-ty:#c6a0ff; --sy-var:#e6ecf7; --sy-const:#b48cff; |
|
| 20 | + | --sy-kw:#7fb4ff; --sy-str:#8ff0d8; --sy-num:#9fd3ff; --sy-cmt:#7a86a4; --sy-fn:#00e0f0; --sy-ty:#c6a0ff; --sy-var:#e6ecf7; --sy-const:#b48cff; |
|
| 21 | 21 | color-scheme: dark; |
|
| 22 | 22 | } |
|
| 23 | 23 | :root[data-theme="light"] { |
|
| 24 | - | --bg:#f4f7fc; --panel:#ffffff; --soft:#e9eef8; --ink:#1b2333; --mute:#66718a; --line:#d6dded; --dim:#b3bccf; |
|
| 25 | - | --acc:#0072ff; --acc-2:#00b8d4; --band:#0b3f9e; --band-ink:#ffffff; --band-acc:#9ff3ff; --band-line:rgba(255,255,255,.25); --now:#0072ff; --now-ink:#ffffff; |
|
| 26 | - | --ok:#1f8f5f; --bad:#c62d47; --warn:#6b3fd1; |
|
| 24 | + | --bg:#f4f7fc; --panel:#ffffff; --soft:#e9eef8; --ink:#1b2333; --mute:#5f6a81; --line:#d6dded; --dim:#b3bccf; |
|
| 25 | + | --acc:#0064e0; --acc-2:#00b8d4; --band:#0b3f9e; --band-ink:#ffffff; --band-acc:#9ff3ff; --band-line:rgba(255,255,255,.25); --now:#0064e0; --now-ink:#ffffff; |
|
| 26 | + | --ok:#1a7850; --bad:#c62d47; --warn:#6b3fd1; |
|
| 27 | 27 | --add:#e1f5ee; --del:#fbe3e8; --add-ink:#176b48; --del-ink:#a1243c; |
|
| 28 | 28 | --m1:#0090a8; --m2:#7a8290; --m3:#2f6fcf; --m4:#0055c0; --m5:#8a8f9c; |
|
| 29 | - | --sy-kw:#0b4fc9; --sy-str:#1f8f5f; --sy-num:#1e63b8; --sy-cmt:#8a94a8; --sy-fn:#0083a8; --sy-ty:#7a3fd1; --sy-var:#1b2333; --sy-const:#6b3fd1; |
|
| 29 | + | --sy-kw:#0b4fc9; --sy-str:#1a7850; --sy-num:#1e63b8; --sy-cmt:#606b81; --sy-fn:#007394; --sy-ty:#7a3fd1; --sy-var:#1b2333; --sy-const:#6b3fd1; |
|
| 30 | 30 | color-scheme: light; |
|
| 31 | 31 | } |
|
| 32 | 32 | ||
| … | 11 unchanged lines |
||
| 44 | 44 | p { margin: 0 0 10px; } |
|
| 45 | 45 | .small { font-size: 12px; } |
|
| 46 | 46 | .mute { color: var(--mute); } |
|
| 47 | - | .dim { color: var(--dim); } |
|
| 47 | + | .dim { color: var(--mute); } |
|
| 48 | 48 | .center { text-align: center; } |
|
| 49 | 49 | .right { text-align: right; } |
|
| 50 | 50 | .empty { color: var(--mute); padding: 22px; text-align: center; border: 1px dashed var(--line); } |
|
| … | 61 unchanged lines |
||
| 112 | 112 | .lbl-tag { color: var(--acc); border-color: var(--acc); } |
|
| 113 | 113 | .sig { display: inline-block; width: 1.2em; text-align: center; font-weight: 600; } |
|
| 114 | 114 | .sig-valid { color: var(--ok); } |
|
| 115 | - | .sig-unsigned { color: var(--dim); } |
|
| 115 | + | .sig-unsigned { color: var(--mute); } |
|
| 116 | 116 | .sig-invalid { color: var(--bad); } |
|
| 117 | 117 | .sig-revoked { color: var(--warn); } |
|
| 118 | 118 | ||
| … | 61 unchanged lines |
||
| 180 | 180 | .modes a.on { border-color: var(--acc); color: var(--acc); } |
|
| 181 | 181 | table.code { width: 100%; } |
|
| 182 | 182 | table.code td { padding: 0 8px; border: 0; background: transparent; line-height: 1.5; font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 13px; } |
|
| 183 | - | table.code td.n { width: 1%; min-width: 3em; text-align: right; color: var(--dim); user-select: none; cursor: pointer; padding-right: 12px; } |
|
| 183 | + | table.code td.n { width: 1%; min-width: 3em; text-align: right; color: var(--mute); user-select: none; cursor: pointer; padding-right: 12px; } |
|
| 184 | 184 | table.code td.n:hover { color: var(--acc); } |
|
| 185 | 185 | table.code td.t pre { margin: 0; white-space: pre; } |
|
| 186 | 186 | table.code tr.hl td { background: color-mix(in srgb, var(--acc) 18%, transparent); } |
|
| … | 114 unchanged lines |
||
| 301 | 301 | td.thread { background: var(--soft); padding: 8px 12px; } |
|
| 302 | 302 | td.thread .comment { margin: 4px 0; font-family: "Barlow", sans-serif; font-size: 14px; } |
|
| 303 | 303 | details.linecomment { display: inline; float: right; } |
|
| 304 | - | details.linecomment summary { list-style: none; cursor: pointer; color: var(--dim); font-size: 12px; padding: 0 4px; } |
|
| 304 | + | details.linecomment summary { list-style: none; cursor: pointer; color: var(--mute); font-size: 12px; padding: 0 4px; } |
|
| 305 | 305 | details.linecomment summary:hover { color: var(--acc); } |
|
| 306 | 306 | details.linecomment form { font-family: "Barlow", sans-serif; background: var(--panel); border: 1px solid var(--acc); padding: 8px; margin: 4px 0; } |
|
| 307 | 307 | details.linecomment textarea { width: 100%; font: inherit; background: var(--bg); color: var(--ink); border: 1px solid var(--line); } |
|
| … | 85 unchanged lines |
||
| 393 | 393 | .file-side li a { display: block; padding: 2px 12px 2px calc(12px + var(--d, 0) * 14px); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
|
| 394 | 394 | .file-side li a:hover { background: var(--soft); text-decoration: none; } |
|
| 395 | 395 | .file-side li.here a { background: color-mix(in srgb, var(--acc) 16%, transparent); font-weight: 600; box-shadow: inset 3px 0 0 var(--acc); } |
|
| 396 | - | .file-side .glyph { color: var(--dim); font-size: 10px; } |
|
| 396 | + | .file-side .glyph { color: var(--mute); font-size: 10px; } |
|
| 397 | 397 | .file-main { min-width: 0; overflow-x: auto; } |
|
| 398 | 398 | .linebar { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--acc) 8%, var(--panel)); } |
|
| 399 | 399 | .linebar[hidden] { display: none; } |
|
| … | 3 unchanged lines |
||
| 403 | 403 | table.code td.blame.age-3 { --age: 4; } table.code td.blame.age-4 { --age: 5; } table.code td.blame.age-5 { --age: 6; } |
|
| 404 | 404 | table.code td.blame.age-6 { --age: 7; } table.code td.blame.age-7 { --age: 8; } table.code td.blame.age-8 { --age: 9; } |
|
| 405 | 405 | table.code td.blame.age-9 { --age: 10; } |
|
| 406 | - | table.code td.blame .before { color: var(--dim); margin-left: 4px; } |
|
| 406 | + | table.code td.blame .before { color: var(--mute); margin-left: 4px; } |
|
| 407 | 407 | table.code td.blame .before:hover { color: var(--acc); text-decoration: none; } |
|
| 408 | 408 | @media (max-width: 900px) { |
|
| 409 | 409 | .file-layout { grid-template-columns: minmax(0, 1fr); } |
|
| … | 63 unchanged lines |
||
| 473 | 473 | .picture-row { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; } |
|
| 474 | 474 | .picture-row img.avatar.xl { width: 96px; height: 96px; } |
|
| 475 | 475 | .picture-row > div { flex: 1; min-width: 220px; } |
|
| 476 | + | ||
| 477 | + | /* Accessibility and narrow screens (hub spec §6.13). */ |
|
| 478 | + | :focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; } |
|
| 479 | + | .band a:focus-visible, .tabs a:focus-visible { outline-offset: -2px; } |
|
| 480 | + | .skip { position: absolute; left: 12px; top: -48px; z-index: 100; padding: 8px 14px; background: var(--acc); color: var(--bg); font-weight: 600; } |
|
| 481 | + | .skip:focus { top: 8px; } |
|
| 482 | + | main:focus { outline: none; } |
|
| 483 | + | @media (max-width: 760px) { |
|
| 484 | + | .tabs-in { overflow-x: auto; white-space: nowrap; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } |
|
| 485 | + | .tabs-in > * { flex-shrink: 0; } |
|
| 486 | + | .band { overflow-x: auto; white-space: nowrap; } |
|
| 487 | + | main table.list, main table.flow-runs { display: block; overflow-x: auto; } |
|
| 488 | + | .sectioned { grid-template-columns: 1fr; } |
|
| 489 | + | .section-nav { position: static; display: flex; flex-wrap: wrap; gap: 4px; } |
|
| 490 | + | header .find { display: none; } |
|
| 491 | + | } |
|
| 492 | + | @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; transition: none !important; } } |