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.
Found in DrDrij's review of the v2.5.0 deploy (
https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app, which isorigin/mainat 6d773b6). The author names in the page header render in Tailwind'stext-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 instyles/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).python-programming.quantecon.org)<button>(Radix popover trigger)<a href>to the author's own sitergb(14, 165, 233)=#0ea5e9rgb(0, 114, 188)=#0072bchover:underline)#0ea5e9#004979#0ea5e9(6.31:1 on#1c1917— passes)#6cb6ffon#1a1a2e#0ea5e9appears 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 5pxrgb(0, 114, 188)rule (ProjectFrontmatter.tsx:30,col-body border-b-[5px] border-b-qeborder-blue, with'qeborder-blue': 'rgb(0 114 188)'attailwind.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#0072bcand one of them not.Root cause
app/components/ProjectFrontmatter.tsx:67passesclassName="text-[102%] font-[400] text-sky-500"to upstream's<Author>.@myst-theme/frontmatter@1.3.0/dist/Authors.js:11renders<span className={classNames('myst-fm-author font-semibold text-sm', className)}>wrapping anAuthorPopover, whose trigger is<button className="myst-fm-author-popover … hover:underline">; Tailwind preflight givesbuttoncolor: 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.tsxreturns exactly one commit,d1ac95aac, the original theme scaffold.text-sky-500occurs exactly once acrossapp/andstyles/, and the stringmyst-fmoccurs instyles/zero times.PR #167 could not have reached it, and did not miss it.
.linkand.hover-linkare 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-linkare only applied by the content renderers". Being a<button>rather than an<a href>is also why the names have no:visitedstate and why.article :where(a)(styles/quantecon.css:195) does nothing for them.text-[102%]andfont-[400]on that same line are dead code. Both are(0,1,0)utilities that lose on emission order to upstream'stext-sm font-semiboldfromAuthors.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.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, so0.8remlands 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:
hover-link#0072bchover-link#0072bchover-link#0072bclink#0072bclink#0072bclink#0072bchover-link#0072bclink#0072bcWhich entries go dotted is decided by whether the target page has a thumbnail:
myst-to-react@1.3.0/dist/links/index.js:26readsconst skipPreview = !page || (!page.description && !page.thumbnail);— a falsy result gives.link, otherwise.hover-linkplus a Radix hover preview card. All eight of those pages have an emptydescription; 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-15sets.link, .hover-link { @apply … underline decoration-[0.1em] … }andhover.css:41-43adds.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@importorder instyles/app.css, so it loses. The CONTENT LINKS block atstyles/quantecon.css:344-365then sets colour andtext-decoration-coloronly, nevertext-decoration-lineor-style— exactly as its own comment at lines 326-327 records.text-decoration-styleappears zero times in the entire 81KB Sphinx sheet, and its base rule isa{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 resetstext-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 onlystyles/app.css:52-58and already behave correctly.What to change
app/components/ProjectFrontmatter.tsxtext-sky-500and the deadtext-[102%] font-[400]app/components/ProjectFrontmatter.tsxclassName="qe-page__header-authors"— Sphinx's own class name for this element, and the sibling of theqe-page__header-translatorsclass #174 introducesstyles/quantecon.cssstyles/quantecon.csstext-decoration-line/-styleon.link, .hover-linkand their:hoverstyles/quantecon.csstests/visual/fixture/features.mdurl: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-authorat(0,2,0), which beats bothtext-sky-500and upstream'stext-sm font-semiboldat(0,1,0)without relying on emission order. Both class names survive Tailwind's purge:qe-page__header-authorsappears inapp/components/ProjectFrontmatter.tsx, andmyst-fm-authorinnode_modules/@myst-theme/frontmatter/dist/Authors.js, and both paths are inmystTheme.content(verified: the glob list includes./app/**/*.{js,ts,jsx,tsx}andnode_modules/@myst-theme/frontmatter/{src,dist}/**/*.{js,ts,jsx,tsx}).--qe-link-colorand--qe-link-hover-colorare already declared atstyles/quantecon.css:85-86and overridden under.darkat lines 101-102, so dark mode flips for free.Baselines
The draft of this item claimed the visual fixture declares no authors. It does:
tests/visual/fixture/features.md:4-6declares Thomas J. Sargent and John Stachurski, and the committed baselinetests/visual/__snapshots__/desktop-chrome-darwin/features.pngshows them in the page header. What it does not declare is aurl, andProjectFrontmatter.tsx:66only takes the<Author>branchif (a.url)— so today the fixture renders the names as bare text nodes at prose size and body colour, andtext-sky-500is emitted in no snapshot at all. Addingurl:to those two entries is the one-line change that puts the defect under regression cover; it also flips the names totext-sm font-semibold, so it moves pixels on its own and belongs in the same commit as the fix rather than ahead of it.url, so the<Author>branch is never takenurl:to the two fixture authorsfeatures.pngandhistory-open.png× 4 platform dirs = 8 PNGsintro.png,features.png,history-open.png,sidebar-open.png× 4 = 16 PNGsThe decoration half is what moves the 16.
intro.md:9has an external link and a footnote reference and:14a cross-reference;tests/visual/fixture/features.md:50isSee {eq}`euler`., whichmyst-to-react@1.3.0/dist/crossReference.js:123renders through its local branch as_jsx("a", { href: …, onClick: scroll, className: classNames({ 'hover-link': !isButtonLike }, className) }), andisButtonLikeat line 122 is(className ?? '').split(' ').includes('button')— false here, so it is a.hover-linkanchor, dotted-underlined at rest under today's CSS.history-open.pngscreenshots/featurestoo (tests/visual/theme.spec.ts:60) andsidebar-opennavigates to/(theme.spec.ts:200), so both move.lists.mdandnotebook.ipynbcontain no anchors, andfooter.md's only link wraps an image — visible in the committedlists.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%maxDiffPixelRatiobudget attests/visual/theme.spec.ts:34would otherwise swallow (#113) — then/update-snapshotson the PR for linux.Sequencing
PR #174 copies
text-sky-500onto 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. ItsProjectFrontmatter.tsxhunks 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.csshunks 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=allpass rather than its own. PR #175 touchesapp/root.tsxandtests/visual/fouc.spec.tsonly — 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.