The theme is named after north light: the soft, even, neutral light from a north-facing window that has no glare and stays consistent all day. That is the whole design brief.
Colour modes#
Two modes, designed separately rather than one inverted into the other. Mode switching
uses color-scheme plus the CSS light-dark() function, so each mode-dependent colour
is declared once with both values rather than being repeated in an override block.
A reader with JavaScript disabled still follows their operating system, because that mechanism is pure CSS. The toggle in the header only exists to override it, and it removes the stored preference when your choice matches the system again, handing control back rather than freezing the page.
Note
There is no flash on load. A small render-blocking script in <head> pins the mode
before first paint. It is the only script the theme loads that way, and it is worth
the cost precisely once.
Palettes#
Six, set with colorScheme:
| Value | Character |
|---|---|
periwinkle | The default. Cool, quiet blue-violet. |
sage | Muted green. |
clay | Warm terracotta. |
plum | Deep purple. |
slate | Desaturated blue. |
rose | Muted pink-red. |
Your own icons#
Drop name.svg into your site’s assets/icons/ and call it by name — {{< icon "name" >}},
or partial "icon.html" "name" from a template. A site file wins over a built-in of the
same name, so this overrides as well as adds.
The file is inlined rather than referenced, which is what lets it take currentColor and
size in em like every built-in icon. An <img> would do neither.
Right-to-left#
rtl = true sets the direction site-wide, and rtl: true in front matter sets it for one
page — a site in one direction can still carry a page in the other. The
direction test page on this site is that case, and it exists to
be measured rather than assumed: every directional property in the CSS is a logical one,
verified against it.
Two things stay physical on purpose, and both are marked in the source with a
/* physical: … */ note the test suite reads:
- Code blocks are left-to-right on a right-to-left page, because the code is not Arabic. Without that, the bidirectional algorithm reorders punctuation inside lines.
- The series caret is two edges of a square rotated into an arrowhead. Swapping them would rotate the caret rather than mirror the layout.
Every accent is measured before it ships, not chosen by eye. The binding case is not
the page background — almost anything clears that — but the accent sitting on its own
--accent-tint, which is where a featured card’s eyebrow lands. Measured there in light
mode: periwinkle 4.90, plum 4.82, rose 4.63, slate 4.54, sage 4.17, clay 4.16.
A fourth candidate, an ochre, measured 4.40 — below every palette already shipped — and was dropped rather than tuned. A palette that needs special pleading to be added will need it again the next time something changes.
Six considered palettes rather than sixteen. Every accent is measured against its own background in both modes, which is work that does not scale to a colour picker.
More of it: accentEmphasis#
The accent is rationed by default. It carries links, the eyebrow above a title, the active item in the table of contents and the reading-progress bar, and stops there — the rest of the page is a neutral base, which is the whole north-light brief.
accentEmphasis = true lets it reach five more surfaces:
| Surface | Default | With accentEmphasis |
|---|---|---|
Section headings (Featured, Recent posts, Related) | Body colour | Accent |
| Year rules on the post index | --fg-3 | Accent |
| Tag chips at rest | Neutral chip | Accent text on --accent-tint |
| A card’s border on hover | --accent-pastel | --accent |
| The blockquote edge | --accent-pastel | --accent |
[params]
accentEmphasis = true # default: falseIt invents no colours. Every value is a token the palette already defines and that has
already been measured in both modes, so all six palettes get this for free and none of them
needed a second round of contrast work. The one combination that carries text — accent on
--accent-tint — is the binding case the palettes are measured against, and it is what a
tag already does on hover.
This page and the rest of this site have it on, so the tag chips below a post, the year rules on the index and the headings above the related posts are the switch rather than a description of it.
Two things it deliberately leaves alone: prose headings and the article title. Colouring
the text a reader is actually reading is where more colour stops being trim and starts
competing with the words. If you want that anyway, it is four lines in your own
assets/css/custom.css — see below.
Your own CSS#
Create assets/css/custom.css in your site. The theme picks it up automatically, with
nothing to configure, and appends it to its own stylesheet, so it is minified and
fingerprinted with everything else and costs no extra request. Being last in the
cascade, a plain rule beats the theme’s equivalent at the same specificity.
Retuning a token is usually tidier than overriding a rule:
:root {
--measure: 40rem; /* a narrower prose column */
}
html[data-palette="clay"] {
--accent: #9c4737; /* your own take on clay */
}Every token is a custom property in assets/css/tokens.css. Colour, spacing, type
scale, radii and motion durations all live there, and component CSS references
var(--x) rather than hardcoding a value. Adding a palette means adding one block.
This very site uses the hook twice: for the documentation index cards on /docs/, which
are a layout the theme does not have, and for print styles, which the theme has no
opinion about.
Custom fonts#
Same hook. Put the files in your site’s assets/ or static/, declare the faces in
custom.css, and point the type tokens at them:
@font-face {
font-family: "Your Face";
src: url("/fonts/your-face.woff2") format("woff2");
font-display: swap;
}
:root {
--font-sans: "Your Face", system-ui, sans-serif;
}Warning
The theme’s own faces ship with metric-matched fallbacks so the page does not reflow
when the webfont arrives. A replacement without matched metrics reintroduces that
shift. The measure is deliberately a fixed length rather than ch for the same
reason: ch depends on the current font, so the column itself moves when the font
swaps.
Logos#
[params]
logo = "images/logo.svg"
logoDark = "images/logo-dark.svg" # optionalA logo replaces the dot and the wordmark together, because a mark next to a name next to
a dot is three brand elements competing. With only logo set, the same file is used in
both modes, which is right for anything with transparency and enough contrast either
way.
Overriding a template#
Hugo’s lookup order means any theme file can be replaced by putting a file of the same name at the same path in your own site. Nothing needs patching and nothing is lost on upgrade.
The page listing at /docs/ is a live example: the theme’s section.html is a post
index with covers and year headings, which is right for a blog and wrong for a manual.
This site ships layouts/docs/section.html and it wins. Deleting that one file restores
the theme’s behaviour.
Two partials exist purely as escape hatches and are meant to be overridden:
extend-head.html and extend-footer.html. See Integrations.
Home page layouts#
Cycle through all ten live — each one rendered with this site’s real content, with a switcher to move between them.
The home page has ten arrangements, chosen with one setting:
[params.home]
layout = "stack" # default| Layout | What it is | Reach for it when |
|---|---|---|
stack | Intro, one featured post with its cover, then a card grid | The default. A blog whose newest post deserves the space |
page | The page’s own title and Markdown, nothing else | The homepage is a written page: a documentation root, a landing page, a site with no posts yet |
profile | Centred avatar, name, headline, bio and social row, posts beneath | A personal site where the person is the subject |
hero | The newest post’s cover at full width, title alongside it | The covers are strong and the newest post is the point |
card | The intro inside one bordered panel, cards below | You want stack but quieter — the intro as an object rather than a band |
background | A site-supplied image behind the intro | You have one good photograph and accept the trade below |
split | Intro pinned in one column, posts in the other | The intro is worth keeping on screen while the reader scans posts |
gallery | No intro furniture; posts as a cover-led grid | The images are the content: photography, projects, screenshots |
archive | No intro, no covers; every post by year | A long-running blog where arriving readers want to find a piece |
custom | Renders your own partial | None of the above, and you would rather not fork the theme |
stack is the layout the theme shipped with, so leaving layout unset changes nothing.
An unknown value fails the build rather than quietly falling back to the default, which
would look like the setting had no effect.
Every layout renders with no author configured, no cover images and no posts at all — an empty site is the first thing a new adopter sees, and a layout that renders a stray heading over an empty grid is a poor first impression.
background is the one with a trade#
A photograph behind text is the glare this theme exists to avoid, so it is built with guards rather than left open:
- A flat scrim sits between image and text — a solid colour at a fixed opacity, not a gradient, so the contrast it buys is the same at the top of the block as at the bottom.
- The text is fixed light-on-dark in both colour modes. The photograph does not invert when the palette does, so text that followed the theme would be legible in one mode and not the other over the same image.
- Without
home.backgroundImageset, it renders as the ordinary intro. No empty band.
None of that rescues text over an image with a bright patch exactly where the heading sits. Check your own image in both modes — the theme cannot do that for you.
[params.home]
layout = "background"
backgroundImage = "images/backdrop.jpg" # assets/ or static/custom#
Create layouts/_partials/home/custom.html in your own site. It receives the same data
every built-in layout gets:
| Key | What it is |
|---|---|
ctx | The home page |
cfg | Resolved theme config |
home | Everything under [params.home] |
posts | Pages from mainSections, newest first |
author | Resolved author config |
Selecting custom without providing that file emits a build warning rather than a blank
page, so the mistake surfaces instead of looking like a broken theme.
Printing#
Printing needs no configuration. A @media print pass at the end of the stylesheet hides
the chrome — header, footer, table of contents, sharing, pagination — lets the article take
the full measure, wraps code blocks rather than truncating them, and prints each external
link’s destination after it. Everything renders on the light palette, whatever the toggle
said on screen, because paper is light. Print this page to see it.
The browser’s own chrome follows the page as well: theme-color meta tags tint it to the
page background in both colour modes.