feat(type): self-host Manrope and DM Mono so they actually render
`styles.css` line 1 carried a malformed rule for the life of the site:
@font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?...')}
`src:` in an @font-face must point at a font binary. That URL returns a CSS
stylesheet, so no browser could ever load a face from it. Every
`font-family:Manrope,Arial,sans-serif` fell through to Arial, and 'DM Mono' was
never declared as a family at all, so it fell through to generic monospace. The
intended typography has never once been seen.
Task 02 spotted this and was told to default to deleting the dead rule and
declaring the stacks that actually render. It recorded that decision, deferred
the deletion to "future component tasks", and nothing picked it up. The human
has now chosen the other branch: the real fonts.
Self-hosted rather than linked from fonts.googleapis.com because
scripts/audit-ui.mjs rejects any external <link>/<script>, and because the site
is presented in workshop rooms with unreliable networks. Latin and latin-ext
subsets only — the site is EN and PT-BR, so the cyrillic, greek and vietnamese
subsets Google also serves are dropped. Manrope ships as one variable file
covering 400-800. 89 KB total across six faces, all SIL OFL.
One public/fonts/fonts.css serves both trees, with relative url()s that each
consumer resolves against that file's own location: BaseLayout.astro links it
for Astro pages, the legacy root styles.css @imports it.
This changes how every page renders. That is the point, and it is the one
sanctioned visual change in the migration — screenshots taken before today show
Arial and are no longer a valid baseline. The three governing documents that
said "do not add a webfont" are updated so the next design-system-keeper does
not undo this.
Adds .stylelintignore, mirroring .prettierignore's legacy list for the same
reason: staging the minified styles.css to change one declaration produced ~180
declaration-block-single-line-max-declarations errors and blocked the commit.
public/fonts/fonts.css is deliberately excluded from that ignore list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,13 @@ palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
|
|||||||
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||||
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
||||||
visibly different — screenshot both and get a human decision.
|
visibly different — screenshot both and get a human decision.
|
||||||
2. **The fonts have never rendered.** The `@font-face` in `styles.css:1` points
|
2. ~~**The fonts have never rendered.**~~ **Settled 2026-09-05 — do not
|
||||||
`src:` at a Google Fonts _stylesheet_, so Manrope and DM Mono have always
|
reopen.** The malformed `@font-face` was escalated and the human chose the
|
||||||
fallen back to Arial and generic monospace. Self-hosting them is a redesign,
|
real fonts. Manrope and DM Mono are self-hosted in `public/fonts/`, wired
|
||||||
not a refactor. Default: delete the dead rule, declare the stacks that
|
through `public/fonts/fonts.css`, which `BaseLayout.astro` links and the
|
||||||
actually render. Escalate if someone wants the real fonts.
|
legacy root `styles.css` `@import`s. **Do not delete these faces and do not
|
||||||
|
replace the stacks with `Arial`/`ui-monospace`** — that instruction is
|
||||||
|
obsolete. You may add `--font-sans` / `--font-mono` tokens pointing at them.
|
||||||
|
|
||||||
## You own
|
## You own
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ The same semantic names carry different values depending on which stylesheet
|
|||||||
loaded them:
|
loaded them:
|
||||||
|
|
||||||
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
||||||
| --- | --- | --- | --- |
|
| ---------- | -------------------------------- | ------------------------------------------ | ----------------------- |
|
||||||
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
||||||
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
||||||
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
||||||
@@ -34,35 +34,36 @@ exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
|
|||||||
Do not "just pick one" silently in the middle of another task. This is its own
|
Do not "just pick one" silently in the middle of another task. This is its own
|
||||||
reviewed change with visual diffs attached.
|
reviewed change with visual diffs attached.
|
||||||
|
|
||||||
## The typography you see is not the typography that was written
|
## The typography — fixed 2026-09-05
|
||||||
|
|
||||||
`styles.css` line 1:
|
`styles.css` line 1 used to read:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
@font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap')}
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
src: url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap');
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`src:` points at a **CSS stylesheet**, not a font file. No browser can load a
|
`src:` in an `@font-face` must point at a font binary. That URL returns a CSS
|
||||||
font from that, so:
|
stylesheet, so no browser could load a face from it. For the whole life of the
|
||||||
|
site, every `font-family:Manrope,Arial,sans-serif` rendered as **Arial** and
|
||||||
|
every `font:… 'DM Mono',monospace` rendered as the **generic monospace** face —
|
||||||
|
`'DM Mono'` was never declared as a family at all.
|
||||||
|
|
||||||
- every `font-family:Manrope,Arial,sans-serif` renders as **Arial**
|
**This was escalated and the human chose the real fonts.** Manrope and DM Mono
|
||||||
- every `font:… 'DM Mono',monospace` renders as the **generic monospace** face
|
are now self-hosted in `public/fonts/`, latin and latin-ext subsets only, under
|
||||||
- there are no `@font-face` blocks anywhere else and zero font files in the repo
|
the SIL Open Font License. One `fonts.css` serves both trees: Astro links it
|
||||||
- `scripts/audit-ui.mjs` only rejects external `<link>`/`<script>` tags, so this
|
from `BaseLayout.astro`, the legacy root `styles.css` `@import`s it. Self-hosted
|
||||||
slipped through the "dependency-free" audit
|
rather than linked from Google because `scripts/audit-ui.mjs` rejects any
|
||||||
|
external `<link>`/`<script>`, and because the site is presented in workshop
|
||||||
|
rooms with unreliable networks.
|
||||||
|
|
||||||
**This is a trap for the refactor.** Self-hosting Manrope and DM Mono in Astro
|
**This changed how every page renders**, deliberately. It is the one sanctioned
|
||||||
is the obvious "fix" — and it would change how every page looks, violating
|
visual change in the migration. Screenshots taken before 2026-09-05 show Arial
|
||||||
"maintain the same styles". Treat it as an explicit product decision:
|
and are no longer a valid baseline.
|
||||||
|
|
||||||
- **Keep current rendering**: delete the dead `@font-face`, replace the font
|
`Georgia, serif` for emphasis (`h1 em`, `.hero em`) is untouched and still real.
|
||||||
stacks with what actually renders today (`Arial, sans-serif` /
|
|
||||||
`ui-monospace, monospace`). Zero visual change. Honest CSS.
|
|
||||||
- **Adopt the intended fonts**: self-host the woff2 files in `public/fonts/`,
|
|
||||||
add real `@font-face` with `font-display:swap`. Better-looking, but it is a
|
|
||||||
redesign and needs sign-off plus fresh screenshots.
|
|
||||||
|
|
||||||
Default to the first unless a human says otherwise.
|
|
||||||
|
|
||||||
## Type scale
|
## Type scale
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ real — it is a system font, so it does render. Keep it.
|
|||||||
Sizes are all `clamp()`, roughly:
|
Sizes are all `clamp()`, roughly:
|
||||||
|
|
||||||
| Role | Value |
|
| Role | Value |
|
||||||
| --- | --- |
|
| --------------- | --------------------------------------------------------- |
|
||||||
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
||||||
| Section `h2` | `clamp(36px,5vw,65px)` |
|
| Section `h2` | `clamp(36px,5vw,65px)` |
|
||||||
| Sub-head | `clamp(24px,3vw,38px)` |
|
| Sub-head | `clamp(24px,3vw,38px)` |
|
||||||
@@ -87,11 +88,12 @@ should be unchanged within a pixel or two at common viewports.
|
|||||||
## Breakpoints
|
## Breakpoints
|
||||||
|
|
||||||
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
|
Sixteen distinct max-widths are in use: 420, 520, 530, 560, 600, 620, 720, 800,
|
||||||
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and `min-width:2200px`.
|
850, 880, 900, 1000, 1050, 1100 — plus `min-width:1600px` and
|
||||||
|
`min-width:2200px`.
|
||||||
|
|
||||||
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
|
Collapse to a named set (suggested: 560 / 800 / 1100 / 1600 / 2200) and prove
|
||||||
equivalence with screenshots at the *old* breakpoint values, since that is
|
equivalence with screenshots at the _old_ breakpoint values, since that is where
|
||||||
where regressions will hide.
|
regressions will hide.
|
||||||
|
|
||||||
`@media(prefers-reduced-motion:reduce)` is already respected in several
|
`@media(prefers-reduced-motion:reduce)` is already respected in several
|
||||||
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
||||||
@@ -100,8 +102,8 @@ stylesheets. Keep it — see [`../rules/animation.md`](../rules/animation.md).
|
|||||||
|
|
||||||
The visual identity is editorial-print: flat colour blocks, hairline `1px`
|
The visual identity is editorial-print: flat colour blocks, hairline `1px`
|
||||||
rules, uppercase monospace eyebrows with wide tracking, very tight negative
|
rules, uppercase monospace eyebrows with wide tracking, very tight negative
|
||||||
letter-spacing on display type (`-.06em` … `-.08em`), grid layouts with `gap:1px`
|
letter-spacing on display type (`-.06em` … `-.08em`), grid layouts with
|
||||||
over a background colour to fake borders, and near-zero border-radius.
|
`gap:1px` over a background colour to fake borders, and near-zero border-radius.
|
||||||
|
|
||||||
That last trick (`gap:1px` + parent background) is used everywhere. It is
|
That last trick (`gap:1px` + parent background) is used everywhere. It is
|
||||||
intentional. Do not replace it with `border`.
|
intentional. Do not replace it with `border`.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
hands-on
|
||||||
|
public/hands-on
|
||||||
|
submitted-skills
|
||||||
|
public/submitted-skills
|
||||||
|
skill-reviews
|
||||||
|
vote-service
|
||||||
|
|
||||||
|
# Legacy site sources, slated for deletion at cutover (task 20). Same list and
|
||||||
|
# same reasoning as .prettierignore: these are minified, single-line
|
||||||
|
# stylesheets. stylelint's `declaration-block-single-line-max-declarations`
|
||||||
|
# fires once per rule in them — ~180 errors for `styles.css` alone — so staging
|
||||||
|
# one to change a single declaration blocks the commit outright. The rule is
|
||||||
|
# about hand-written source readability and says nothing useful about minified
|
||||||
|
# output that is about to be deleted.
|
||||||
|
#
|
||||||
|
# `public/fonts/fonts.css` is deliberately NOT here: it is new, hand-written,
|
||||||
|
# and must stay linted.
|
||||||
|
#
|
||||||
|
# Root-anchored on purpose: a bare `rules` would also swallow .agents/rules/.
|
||||||
|
/styles.css
|
||||||
|
/landing.css
|
||||||
|
/chapters.css
|
||||||
|
/responsive.css
|
||||||
|
/skills-review/
|
||||||
|
/rules/
|
||||||
|
/skills/
|
||||||
|
/full-guide/
|
||||||
@@ -50,7 +50,7 @@ walk into.
|
|||||||
|
|
||||||
## Do not
|
## Do not
|
||||||
|
|
||||||
- Do not add a webfont.
|
- Do not add a webfont. **(Reversed 2026-09-05 — see Decisions.)**
|
||||||
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
||||||
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
||||||
is deliberate house style and appears everywhere.
|
is deliberate house style and appears everywhere.
|
||||||
@@ -60,7 +60,9 @@ walk into.
|
|||||||
- **Palette Consolidation:** The three drifted palettes were canonicalized into
|
- **Palette Consolidation:** The three drifted palettes were canonicalized into
|
||||||
one. For `--blue`, `#527f9f` (from styles.css) was chosen over `#215675` (from
|
one. For `--blue`, `#527f9f` (from styles.css) was chosen over `#215675` (from
|
||||||
chapters.css) to unify the look.
|
chapters.css) to unify the look.
|
||||||
- **Fonts:** The broken `@font-face` rule pointing to a Google Fonts stylesheet
|
- **Fonts (superseded 2026-09-05):** task 02 recorded "delete the dead rule,
|
||||||
was noted to be deleted in future component tasks. The font stacks will be
|
fall back to `Arial, sans-serif` / `ui-monospace, monospace`, zero visual
|
||||||
replaced with `Arial, sans-serif` and `ui-monospace, monospace` to match what
|
change", and deferred even that deletion to "future component tasks", which
|
||||||
has actually been rendering all along. Zero visual change.
|
never picked it up. The human has since chosen the opposite: Manrope and DM
|
||||||
|
Mono are now self-hosted in `public/fonts/` and really render. The "Do not add
|
||||||
|
a webfont" line above no longer applies.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Font licences
|
||||||
|
|
||||||
|
Both families here are licensed under the SIL Open Font License, Version 1.1,
|
||||||
|
which permits redistribution and self-hosting.
|
||||||
|
|
||||||
|
| Family | Designer | Source |
|
||||||
|
| ------- | ---------------------------- | -------------------------------------- |
|
||||||
|
| Manrope | Mikhail Sharanda | https://github.com/sharanda/manrope |
|
||||||
|
| DM Mono | Colophon Foundry for Deja Vu | https://github.com/googlefonts/dm-mono |
|
||||||
|
|
||||||
|
Full licence text: https://openfontlicense.org/open-font-license-official-text/
|
||||||
|
|
||||||
|
The `.woff2` files are the latin and latin-ext subsets as served by Google
|
||||||
|
Fonts. They are unmodified.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,100 @@
|
|||||||
|
/* Manrope and DM Mono, self-hosted.
|
||||||
|
*
|
||||||
|
* `styles.css` line 1 used to carry this:
|
||||||
|
*
|
||||||
|
* @font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?...')}
|
||||||
|
*
|
||||||
|
* `src:` in an @font-face must point at a font binary. That URL returns a CSS
|
||||||
|
* stylesheet, so no browser could ever load a face from it: every
|
||||||
|
* `font-family:Manrope,Arial,sans-serif` fell through to Arial, and 'DM Mono'
|
||||||
|
* was never declared at all, so it fell through to the generic monospace face.
|
||||||
|
* The intended typography has never rendered. This file is the fix.
|
||||||
|
*
|
||||||
|
* Self-hosted rather than linked from fonts.googleapis.com because
|
||||||
|
* `scripts/audit-ui.mjs` rejects any external <link>/<script>, and because the
|
||||||
|
* site is shown in workshop rooms with unreliable networks.
|
||||||
|
*
|
||||||
|
* Both families are SIL Open Font License 1.1 — see OFL.md in this directory.
|
||||||
|
* Subsets are latin and latin-ext only: the site is EN and PT-BR, so the
|
||||||
|
* cyrillic, greek and vietnamese subsets Google also serves are dropped.
|
||||||
|
*
|
||||||
|
* The url()s are relative on purpose. Both consumers resolve them against this
|
||||||
|
* file's own location:
|
||||||
|
* - Astro pages: <link> in BaseLayout.astro, served from `${base}fonts/`
|
||||||
|
* - legacy pages: @import at the top of the root `styles.css`
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* DM Mono 400 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-400-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 400 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-400-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 500 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-500-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DM Mono 500 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('dm-mono-500-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manrope 400 800 — latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('manrope-var-latin.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Manrope 400 800 — latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: Manrope;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('manrope-var-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, lang = 'en' } = Astro.props;
|
const { title, description, lang = 'en' } = Astro.props;
|
||||||
|
|
||||||
|
// Self-hosted Manrope and DM Mono. Lives in `public/fonts/` rather than the
|
||||||
|
// bundled CSS because the legacy stylesheets @import the same file, so there
|
||||||
|
// is exactly one copy of the faces. See public/fonts/fonts.css.
|
||||||
|
const fonts = `${import.meta.env.BASE_URL}fonts/fonts.css`.replace('//', '/');
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -15,6 +20,7 @@ const { title, description, lang = 'en' } = Astro.props;
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
<link rel="stylesheet" href={fonts} />
|
||||||
<slot name="styles" />
|
<slot name="styles" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user