Skip to content

The page-header byline renders in a sky blue nothing else uses and fails AA contrast #183

Description

@mmcky

Found in DrDrij's review of the v2.5.0 deploy (https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app, which is origin/main at 6d773b6). The author names in the page header render in Tailwind's text-sky-500 (#0ea5e9) — a blue nothing else on the page uses, and the only text on the page below the WCAG AA floor. It is a leftover from the original theme scaffold rather than a colour decision, and no rule in styles/ currently reaches it.

Everything below was measured on 2026-09-09 with Playwright/Chromium at 1440×900, against the reviewer's own deploy and against python-programming.quantecon.org/intro.html. Sphinx declarations are quoted from the stylesheet that page loads, _static/styles/quantecon-book-theme.css?digest=1c349b9a0045df0ce6f4a606c2cc9441cbf3bef6 (81,062 bytes, re-fetched today).

Property Theme (v2.5.0, the reviewer's deploy) Sphinx (python-programming.quantecon.org)
element <button> (Radix popover trigger) <a href> to the author's own site
colour, light rgb(14, 165, 233) = #0ea5e9 rgb(0, 114, 188) = #0072bc
contrast on white 2.77:1 — fails AA 4.5:1 5.08:1 — passes
font-size 14px 19.44px
font-weight 600 400
decoration at rest none none
decoration on hover underline (upstream hover:underline) underline
colour on hover unchanged, #0ea5e9 #004979
colour, dark #0ea5e9 (6.31:1 on #1c1917 — passes) #6cb6ff on #1a1a2e

#0ea5e9 appears on exactly two leaf elements in the whole page — the two author names. Every element's computed colour on the deploy was enumerated and nothing else uses it. It sits directly above the header's 5px rgb(0, 114, 188) rule (ProjectFrontmatter.tsx:30, col-body border-b-[5px] border-b-qeborder-blue, with 'qeborder-blue': 'rgb(0 114 188)' at tailwind.config.js:41), which is what makes the mismatch read as an error rather than a choice — two blues, five pixels apart, one of them #0072bc and one of them not.

Root cause

app/components/ProjectFrontmatter.tsx:67 passes className="text-[102%] font-[400] text-sky-500" to upstream's <Author>. @myst-theme/frontmatter@1.3.0/dist/Authors.js:11 renders <span className={classNames('myst-fm-author font-semibold text-sm', className)}> wrapping an AuthorPopover, whose trigger is <button className="myst-fm-author-popover … hover:underline">; Tailwind preflight gives button color: inherit, so the visible name takes sky-500 off the span. This is not a QuantEcon colour a later PR overlooked — git log -S "text-sky-500" -- app/components/ProjectFrontmatter.tsx returns exactly one commit, d1ac95aac, the original theme scaffold. text-sky-500 occurs exactly once across app/ and styles/, and the string myst-fm occurs in styles/ zero times.

PR #167 could not have reached it, and did not miss it. .link and .hover-link are applied only by the content renderers; the byline carries neither, and is not an anchor at all. #167's body says so verbatim: "Site chrome is untouched: .link/.hover-link are only applied by the content renderers". Being a <button> rather than an <a href> is also why the names have no :visited state and why .article :where(a) (styles/quantecon.css:195) does nothing for them.

text-[102%] and font-[400] on that same line are dead code. Both are (0,1,0) utilities that lose on emission order to upstream's text-sm font-semibold from Authors.js:11. Measured, the name renders 14px/600 — the line's stated intent has never reached a page. Fixing the colour and leaving those two utilities in place keeps the line lying about what it does.

Size and weight

Sphinx steps the byline by viewport and this theme deliberately steps nothing. From the deployed sheet: .qe-page__header-authors{font-size:1.08rem;margin:.5rem 0 0} with @media(max-width:768px){.qe-page__header-authors{font-size:.8rem}}, over a root that is 16px below 992px and 18px at and above it.

Viewport Sphinx root Sphinx byline Theme byline
≥ 992px 18px 19.44px 14px
769–991px 16px 17.28px 14px
≤ 768px 16px 12.8px 14px

The theme's flat type scale is a documented house rule with a good reason behind it (styles/quantecon.css:55-74: the root stays 16px so the reader's browser font-size preference still applies — WCAG 1.4.4 — and "Flat was chosen over a matching breakpoint so the size never shifts under the reader"). Adopting Sphinx's ladder would break that rule for one element, so the parity-faithful move that stays inside the house rule is a flat 19.44px at weight 400 — Sphinx's desktop value, pinned. If the full ladder is adopted instead, the @media (max-width: 768px) { font-size: 0.8rem } companion is mandatory and not optional: the theme's root is 16px at that width, the same as Sphinx's, so 0.8rem lands on 12.8px exactly, and without it the theme would show a 19.44px byline on a 393px phone where Sphinx shows 12.8px. Worth noting either way that Sphinx's 19.44px byline is larger than its own 18px body copy, which is unusual for secondary metadata.

The link decoration this shares a stylesheet with

The same header sits above content links whose resting decoration is inconsistent, measured on the in-body table of contents of the deploy's landing page:

Entry Class Resting decoration Colour
1 About These Lectures hover-link underline, dotted #0072bc
2 Getting Started hover-link underline, dotted #0072bc
3 An Introductory Example hover-link underline, dotted #0072bc
4 Functions link underline, solid #0072bc
5 Python Essentials link underline, solid #0072bc
6 OOP I: Objects and Methods link underline, solid #0072bc
7 Names and Namespaces hover-link underline, dotted #0072bc
8 OOP II: Building Classes link underline, solid #0072bc

Which entries go dotted is decided by whether the target page has a thumbnail: myst-to-react@1.3.0/dist/links/index.js:26 reads const skipPreview = !page || (!page.description && !page.thumbnail); — a falsy result gives .link, otherwise .hover-link plus a Radix hover preview card. All eight of those pages have an empty description; four have a non-empty thumbnail.

The decoration is upstream and survives this theme's cascade. @myst-theme/styles@1.3.0/typography.css:13-15 sets .link, .hover-link { @apply … underline decoration-[0.1em] … } and hover.css:41-43 adds .hover-link { @apply decoration-dotted }. Both are (0,1,0). The theme's .article :where(a) { text-decoration: none } (styles/quantecon.css:195-198) is also (0,1,0) but sits in @layer base (line 105), which Tailwind emits ahead of @layer components (lines 289/344/381/451) regardless of @import order in styles/app.css, so it loses. The CONTENT LINKS block at styles/quantecon.css:344-365 then sets colour and text-decoration-color only, never text-decoration-line or -style — exactly as its own comment at lines 326-327 records. text-decoration-style appears zero times in the entire 81KB Sphinx sheet, and its base rule is a{color:#0072bc;overflow-wrap:break-word;text-decoration:none;transition:all .15s linear}.

This is not a captured hover state. Hovering a dotted entry on the deploy turns it solid, rgb(0, 73, 121): .article :where(a):hover { text-decoration: underline } (styles/quantecon.css:200-202) is (0,2,0) and is a shorthand, so it resets text-decoration-style, while upstream's dotted rule is only (0,1,0). Dotted exists at rest and nowhere else. The contents drawer is ruled out too — its anchors carry no class at all (app/components/ContentsSidebar.tsx:27-31), so they get only styles/app.css:52-58 and already behave correctly.

What to change

File Line Change
app/components/ProjectFrontmatter.tsx 67 Drop text-sky-500 and the dead text-[102%] font-[400]
app/components/ProjectFrontmatter.tsx 62 Add a stable hook to the authors wrapper, className="qe-page__header-authors" — Sphinx's own class name for this element, and the sibling of the qe-page__header-translators class #174 introduces
styles/quantecon.css after 365 New PAGE HEADER block colouring and sizing the byline from the existing tokens
styles/quantecon.css 345-355 text-decoration-line / -style on .link, .hover-link and their :hover
styles/quantecon.css 326-327, 342-343 Update both stale comments in the same commit
tests/visual/fixture/features.md 5-6 Add url: to the two authors, so the byline renders through <Author> in the suite at all (see baselines below)

The byline rule wants .qe-page__header-authors .myst-fm-author at (0,2,0), which beats both text-sky-500 and upstream's text-sm font-semibold at (0,1,0) without relying on emission order. Both class names survive Tailwind's purge: qe-page__header-authors appears in app/components/ProjectFrontmatter.tsx, and myst-fm-author in node_modules/@myst-theme/frontmatter/dist/Authors.js, and both paths are in mystTheme.content (verified: the glob list includes ./app/**/*.{js,ts,jsx,tsx} and node_modules/@myst-theme/frontmatter/{src,dist}/**/*.{js,ts,jsx,tsx}). --qe-link-color and --qe-link-hover-color are already declared at styles/quantecon.css:85-86 and overridden under .dark at lines 101-102, so dark mode flips for free.

@layer components {
  /* Page-header byline. Upstream renders the name as a popover <button> inside
     `span.myst-fm-author`, so neither `.link` nor `.article :where(a)` reaches
     it and PR #167's content-link colour never applied. `1.215rem` is 19.44px
     off the 16px root, matching the Sphinx build's `1.08rem` over its 18px
     desktop root; `rem` per the UNITS convention above, since this is header
     chrome and not content. */
  .qe-page__header-authors .myst-fm-author {
    color: var(--qe-link-color, #0072bc);
    font-size: 1.215rem; /* 19.44px */
    font-weight: 400;
  }

  .qe-page__header-authors .myst-fm-author:hover {
    color: var(--qe-link-hover-color, #004979);
  }
}

Baselines

The draft of this item claimed the visual fixture declares no authors. It does: tests/visual/fixture/features.md:4-6 declares Thomas J. Sargent and John Stachurski, and the committed baseline tests/visual/__snapshots__/desktop-chrome-darwin/features.png shows them in the page header. What it does not declare is a url, and ProjectFrontmatter.tsx:66 only takes the <Author> branch if (a.url) — so today the fixture renders the names as bare text nodes at prose size and body colour, and text-sky-500 is emitted in no snapshot at all. Adding url: to those two entries is the one-line change that puts the defect under regression cover; it also flips the names to text-sm font-semibold, so it moves pixels on its own and belongs in the same commit as the fix rather than ahead of it.

Change Snapshots moved
Byline colour, size, weight, as the fixture stands today none — no fixture author has a url, so the <Author> branch is never taken
Adding url: to the two fixture authors features.png and history-open.png × 4 platform dirs = 8 PNGs
Link decoration intro.png, features.png, history-open.png, sidebar-open.png × 4 = 16 PNGs
Both together 16 PNGs — the byline's 8 are a subset

The decoration half is what moves the 16. intro.md:9 has an external link and a footnote reference and :14 a cross-reference; tests/visual/fixture/features.md:50 is See {eq}`euler`., which myst-to-react@1.3.0/dist/crossReference.js:123 renders through its local branch as _jsx("a", { href: …, onClick: scroll, className: classNames({ 'hover-link': !isButtonLike }, className) }), and isButtonLike at line 122 is (className ?? '').split(' ').includes('button') — false here, so it is a .hover-link anchor, dotted-underlined at rest under today's CSS. history-open.png screenshots /features too (tests/visual/theme.spec.ts:60) and sidebar-open navigates to / (theme.spec.ts:200), so both move. lists.md and notebook.ipynb contain no anchors, and footer.md's only link wraps an image — visible in the committed lists.png, where the CC-BY-SA badge carries no underline for a decoration rule to change.

Refresh darwin locally with --update-snapshots=all — these are sub-threshold changes that the 1% maxDiffPixelRatio budget at tests/visual/theme.spec.ts:34 would otherwise swallow (#113) — then /update-snapshots on the PR for linux.

Sequencing

PR #174 copies text-sky-500 onto its new translator links, so the colour change should fold into #174 rather than land after it. On that branch the string now appears twice in the same file: line 102 is the untouched <Author> call, and line 138 is <PeopleList people={translators} linkClassName="text-sky-500 hover:underline" />. Landing after #174 therefore means two edits instead of one, and means the theme ships a second credit line in a blue that fails AA. Its ProjectFrontmatter.tsx hunks are at old lines 4, 11, 38 and 84, so line 67 itself is not modified and a one-line edit would merge either way — the argument is about not shipping the second occurrence, not about a merge conflict. Worth noting that #174's translator names are real <a href> elements (PeopleList, line 23 of its version of the file); it is only the author half that is popover-only.

PR #171 does not conflict textually — its styles/quantecon.css hunks are at old lines 81, 98, 243 and 468, none of which overlaps the CONTENT LINKS block at 344-365. It conflicts on binaries: its 20-PNG rewrite (features, history-open, intro, notebook, sidebar-open × 4) is a superset of the 16 moved here, so the CSS work queues behind it and takes one combined --update-snapshots=all pass rather than its own. PR #175 touches app/root.tsx and tests/visual/fouc.spec.ts only — no overlap.

A record to correct

#147's row for #143 currently reads "Translator attribution block, per-page override, localisable label (the translators half only; authors are already at parity)". Authors are not at parity — wrong colour, wrong size, wrong weight, wrong element — and that sentence is part of why this was never scheduled. Correcting it belongs with this work.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is wrong or broken in a lecture or builddesign-review

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions