Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
trigger and hint placement; search behaviour, keyboard navigation and
platform detection are unchanged
([#165](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/165)).
- Lecture headings now follow the Sphinx builds' typography. `h1`–`h3` are set
in PT Serif with `h4`/`h5` kept in the sans face, sized on the Sphinx scale
(h1 `2em`, h2 `1.7em`, h3 `1.4em`, h4 `1.2em` of the content size, so the
scale tracks the font-scale controls), with every level at line-height 1.15
and `h4`/`h5` at weight 900 to match the Sphinx build's measured values.
All of it is mirrored into the critical CSS so the first WebKit paint on
static-build navigations is already serif at the final sizes rather than
painting the title small and swapping
([#166](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/166)).

### Fixed
- Every toolbar control was nesting a second `<button>` inside its real button
Expand All @@ -44,6 +53,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
longer leaves its tooltip open over the trigger
([#165](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/165)).

### Dependencies
- `@fontsource/pt-serif` 5.3.0 self-hosts the PT Serif heading face (400/700,
upright and italic), routed through the same Remix/esbuild pipeline as Source
Sans 3, so no Google Fonts request
([#166](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/166)).


## [2.4.0] - 2026-09-04

> Headline: lecture content now matches the typography of the existing Sphinx
Expand Down
24 changes: 24 additions & 0 deletions app/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import katexCss from 'katex/dist/katex.min.css';
// `declare module "*.css"` and fail `npm run compile`.
import sourceSans3Css from '@fontsource-variable/source-sans-3/index.css';
import sourceSans3ItalicCss from '@fontsource-variable/source-sans-3/wght-italic.css';
import ptSerif400Css from '@fontsource/pt-serif/400.css';
import ptSerif400ItalicCss from '@fontsource/pt-serif/400-italic.css';
import ptSerif700Css from '@fontsource/pt-serif/700.css';
import ptSerif700ItalicCss from '@fontsource/pt-serif/700-italic.css';

/**
* Self-hosted KaTeX stylesheet.
Expand Down Expand Up @@ -68,3 +72,23 @@ export const SourceSans3CSS: HtmlLinkDescriptor[] = [
{ rel: 'stylesheet', href: sourceSans3Css },
{ rel: 'stylesheet', href: sourceSans3ItalicCss },
];

/**
* Self-hosted PT Serif, the heading face of the Sphinx lecture builds
* (python-programming.quantecon.org sets `h1,h2,h3` in `"PT Serif",serif`).
* `styles/quantecon.css` applies it to `.article` headings here.
*
* Self-hosted for the same reasons as Source Sans 3 above, and routed through
* the same Remix/esbuild pipeline. The static (non-variable) package: PT Serif
* only ships 400 and 700. Four stylesheets because the package splits every
* weight/style pair; each is a handful of `@font-face` rules and the browser
* only downloads the faces a page actually uses. The Sphinx build asks for
* weight 900 on headings, which resolves to the 700 face -- the same face
* these files provide.
*/
export const PTSerifCSS: HtmlLinkDescriptor[] = [
{ rel: 'stylesheet', href: ptSerif400Css },
{ rel: 'stylesheet', href: ptSerif400ItalicCss },
{ rel: 'stylesheet', href: ptSerif700Css },
{ rel: 'stylesheet', href: ptSerif700ItalicCss },
];
21 changes: 20 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { LinksFunction, V2_MetaFunction, LoaderFunction } from '@remix-run/node';
import tailwind from '~/styles/app.css';
import thebeCoreCss from 'thebe-core/dist/lib/thebe-core.css';
import { SourceSans3CSS } from '~/links';
import { PTSerifCSS, SourceSans3CSS } from '~/links';
import { getConfig } from '~/backend/loaders.server';
import type { SiteLoader } from '@myst-theme/common';
import {
Expand Down Expand Up @@ -73,6 +73,15 @@ export const meta: V2_MetaFunction<typeof loader> = ({ data }) => {
* it is declared below rather than in styles/app.css so it
* is available at this first paint too. Being `local()`-only
* it costs no request.
* - heading face: styles/quantecon.css -> `.article h1/h2/h3` ("PT Serif",
* self-hosted via app/links.ts like the sans above, so it is
* likewise absent at first paint). Without this rule the
* headings paint in the sans stack and swap to serif when
* the Tailwind bundle lands; the generic `serif` fallback
* here is far closer to the final shape. The sizes come
* from the same block. They matter beyond reflow: the UA
* default for an h1 inside <article> is 1.5em, not 2em,
* so without them the h1 would paint visibly small.
* - content size: styles/quantecon.css -> `.article` font-size (1.125rem).
* Without it the article paints at the UA default 16px and
* jumps to 18px when the Tailwind bundle lands, reflowing
Expand Down Expand Up @@ -100,6 +109,13 @@ const CRITICAL_CSS = `
@font-face{font-family:"Source Sans 3 Fallback";src:local("Helvetica"),local("Arial"),local("Liberation Sans"),local("Arimo");size-adjust:92.25%;ascent-override:111%;descent-override:43.36%;line-gap-override:0%}
:where(html){font-family:"Source Sans 3 Variable","Source Sans 3","Source Sans 3 Fallback",sans-serif}
:where(.article){font-size:1.125rem}
:where(.article h1,.article h2,.article h3){font-family:"PT Serif",serif}
:where(.article h1){font-size:2em}
:where(.article h2){font-size:1.7em}
:where(.article h3){font-size:1.4em}
:where(.article h4){font-size:1.2em}
:where(.article h1,.article h2,.article h3,.article h4,.article h5){line-height:1.15}
:where(.article h4,.article h5){font-weight:900}
:where(body){margin:0;background-color:#fff}
:where(.dark body){background-color:#1c1917}
:where([hidden],.hidden){display:none}
Expand All @@ -121,6 +137,9 @@ export const links: LinksFunction = () => {
// renders — a 404, or the missing-site response thrown below — and the body
// font has to be right on those pages too.
...SourceSans3CSS,
// Self-hosted PT Serif for the article headings (see app/links.ts). Also
// on the root route: the ErrorBoundary pages render an <h1> too.
...PTSerifCSS,
{ rel: 'stylesheet', href: tailwind },
{ rel: 'stylesheet', href: thebeCoreCss },
{ rel: 'stylesheet', href: '/myst-theme.css' },
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"@fontsource-variable/source-sans-3": "^5.3.0",
"@fontsource/pt-serif": "^5.3.0",
Comment thread
mmcky marked this conversation as resolved.
"@heroicons/react": "^2.2.0",
"@myst-theme/common": "^1.3.0",
"@myst-theme/icons": "^1.3.0",
Expand Down
71 changes: 71 additions & 0 deletions styles/quantecon.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,77 @@
}

@layer base {
/* Heading face. The Sphinx build sets `h1,h2,h3,h4,h5` in `"PT Serif",serif`
and then puts h4/h5 back to Source Sans Pro at weight 900 -- so serif is
h1-h3 only, and that split is reproduced here. The face is self-hosted via
app/links.ts (@fontsource/pt-serif) and mirrored in CRITICAL_CSS
(app/root.tsx) so the first WebKit paint is serif too.

Family only here -- sizes are matched in the block below. The typography
plugin's heading weights (800/700/600) resolve against PT Serif's 400+700
faces to the same 700 the Sphinx build renders -- it asks for 900, which
also resolves to 700.

The plugin's own heading rules are `.article :where(h1)` etc at (0,1,0);
`.article h1` at (0,1,1) wins outright, and no utility on the rendered
headings (`pt-12 m-0`) sets a family. */
.article h1,
.article h2,
.article h3 {
font-family: 'PT Serif', serif;
}

/* Heading scale, matching the Sphinx build's content headings: h1 2em of
the body size, then h2 1.7rem / h3 1.4rem / h4 1.2rem / h5 1rem of its
root -- which is 18px at desktop widths, the same 18px this theme sets on
`.article`. So each value carries over as the same number in `em`, and
`em` (not `rem`) keeps the whole scale tracking the content size, per
UNITS above. h5 has no size rule: the plugin leaves it at 1em, already
the match. The plugin's own scale was h1 2.25em / h2 1.5em / h3 1.25em /
h4 1em. Mirrored in CRITICAL_CSS (app/root.tsx) so the first WebKit paint
lays out at the final sizes.

The plugin's heading *margins* are left alone, deliberately: they are
declared in `em`, so they resolve against these new sizes and keep their
proportions. */
.article h1 {
font-size: 2em; /* 36px at 18px content */
}

.article h2 {
font-size: 1.7em; /* 30.6px */
}

.article h3 {
font-size: 1.4em; /* 25.2px */
}

.article h4 {
font-size: 1.2em; /* 21.6px */
}

/* Heading line-height. Measured computed on the live Sphinx build (every
level renders line-height = 1.15 x its font-size: 41.4px at h1's 36px
down to 20.7px at h5's 18px), replacing the plugin's per-level values
(1.11 / 1.33 / 1.6 / 1.5, and prose's 1.75 on the unstyled h5). */
.article h1,
.article h2,
.article h3,
.article h4,
.article h5 {
line-height: 1.15;
}

/* h4/h5 weight. The Sphinx build sets both at 900 in Source Sans. Unlike
h1-h3 -- where any weight from 600 up resolves to PT Serif's single 700
face, so the plugin's values already render identically -- the sans here
is a variable font with a real weight axis, and the plugin's 600 (h4) and
unstyled 400 (h5) draw visibly lighter than the Sphinx build's 900. */
.article h4,
.article h5 {
font-weight: 900;
}

/* Paragraph rhythm. The plugin ships `.article :where(p)` at 1.25em (22.5px).
The Sphinx build leaves `p` at `margin: revert`, i.e. the UA default of
`1em 0` measured against its own text size, so `1em` keeps that proportion.
Expand Down
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/launch-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/history-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/launch-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/sidebar-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/history-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-darwin/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/history-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/sidebar-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading