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:

ValueCharacter
periwinkleThe default. Cool, quiet blue-violet.
sageMuted green.
clayWarm terracotta.
plumDeep purple.
slateDesaturated blue.
roseMuted 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:

SurfaceDefaultWith accentEmphasis
Section headings (Featured, Recent posts, Related)Body colourAccent
Year rules on the post index--fg-3Accent
Tag chips at restNeutral chipAccent text on --accent-tint
A card’s border on hover--accent-pastel--accent
The blockquote edge--accent-pastel--accent
[params]
  accentEmphasis = true    # default: false

It 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:

assets/css/custom.css
: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:

assets/css/custom.css
@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#

hugo.toml
[params]
  logo = "images/logo.svg"
  logoDark = "images/logo-dark.svg"   # optional

A 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
LayoutWhat it isReach for it when
stackIntro, one featured post with its cover, then a card gridThe default. A blog whose newest post deserves the space
pageThe page’s own title and Markdown, nothing elseThe homepage is a written page: a documentation root, a landing page, a site with no posts yet
profileCentred avatar, name, headline, bio and social row, posts beneathA personal site where the person is the subject
heroThe newest post’s cover at full width, title alongside itThe covers are strong and the newest post is the point
cardThe intro inside one bordered panel, cards belowYou want stack but quieter — the intro as an object rather than a band
backgroundA site-supplied image behind the introYou have one good photograph and accept the trade below
splitIntro pinned in one column, posts in the otherThe intro is worth keeping on screen while the reader scans posts
galleryNo intro furniture; posts as a cover-led gridThe images are the content: photography, projects, screenshots
archiveNo intro, no covers; every post by yearA long-running blog where arriving readers want to find a piece
customRenders your own partialNone 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.backgroundImage set, 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:

KeyWhat it is
ctxThe home page
cfgResolved theme config
homeEverything under [params.home]
postsPages from mainSections, newest first
authorResolved 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.