# Theming
Control the visual identity of your gallery from `content/settings.yaml`. Choose a built-in preset or customize individual properties β no CSS editing required.
## Quick Start
Add a single line to your `settings.yaml`:
```yaml
theme: minimal
```
That's it. Your entire gallery switches to the minimal theme. Leave it out and
you get `studio-modern`, the default.
## Built-in Presets
| Preset | Style | Accent | Fonts | Hero | Frame | Grain |
| ----------------- | ------------------------------ | ------------ | --------------------------------- | ----------- | ------------ | ----- |
| **studio-modern** | Leica precision, grotesque | π΄ `#e60012` | Archivo + IBM Plex Mono | Split | None | β |
| **studio** | Leica-inspired, editorial | π΄ `#e60012` | Playfair Display + DM Sans | Split | Passepartout | β |
| **minimal** | Swiss brutalist, high contrast | β« `#000000` | Geist + IBM Plex Mono | Fullbleed | None | β |
| **editorial** | Cinematic magazine, warm tones | π€ `#8B2500` | Bodoni Moda + Newsreader | Split | Shadow | β |
| **classic** | Gilded gallery, warm luxury | π‘ `#c49a3c` | Cinzel + Crimson Pro | Minimal | Passepartout | β |
| **noir** | Darkroom analog, film noir | π `#ff6b35` | Libre Baskerville + Source Sans 3 | Fullbleed | Passepartout | β |
| **monograph** | Typographic, book-like | β¬ `#333333` | Instrument Serif + Inter | Typographic | None | β |
| **kunsthalle** | Grey gallery wall, wall labels | π΅ `#1d545c` | Libre Franklin | Mosaic | Shadow | β |
| **ma** | Japanese restraint, washi/sumi | π΄ `#b0381f` | Cormorant + Hanken Grotesk | Split | None | β |
| **cyanotype** | Prussian blue, drawing sheet | π΅ `#1a4d88` | Jost + DM Mono | Split | None | β |
| **salon** | Oxblood salon, fine fillet | π€ `#7d1f2c` | Gloock + Work Sans | Fullbleed | None | β |
| **birch** | Nordic daylight, soft corners | π’ `#2c5d44` | Albert Sans | Stacked | None | β |
**`studio-modern` is the default** if no theme is specified.
### Theme Gallery
_From left to right, top to bottom: Studio Modern, Studio, Minimal, Editorial, Classic, Noir, Monograph_
### Grid View Examples
_Same order as above._
### Light Mode
Every preset ships with a light variant. Visitors switch with the toggle in the
navigation bar; the choice is remembered in `localStorage`, and the presets keep
their accent, fonts and framing in both modes.
### What makes each theme unique
**Studio Modern** (default) β The Leica language of `studio` rebuilt around the precision grotesque Archivo, with IBM Plex Mono for every piece of photographic metadata (EXIF chips, counters, indices). Hairline rules at 8% opacity, zero radius, an indexed hero nav with album counts, an always-visible caption bar under album covers, and a film-edge EXIF strip in the lightbox. Red is signal only β nav dot, active state, hover, indices, map markers.
**Studio** β Leica-inspired with a red nav dot, passepartout photo frames, film grain, and a split hero layout. Clean editorial feel with Playfair Display headings.
**Minimal** β Swiss Brutalist aesthetic inspired by MΓΌller-Brockmann and Dieter Rams. Fullbleed hero image, true black/white palette, zero-gap photo grid with no hover effects, ultra-tiny navigation text. Every pixel earns its place.
**Editorial** β Cinematic magazine feel inspired by Aperture and Magnum Photos. Oversized Bodoni Moda serif titles, warm charcoal backgrounds, generous grid spacing, slow cinematic transitions with desaturation-on-idle photos, and pull-quote section labels.
**Classic** β Fine art gallery aesthetic with warm gold accents, Cinzel Roman capitals for headings, decorative ornamental dividers, warm passepartout frames, rounded corners, and an elegant gold header dot. Feels like a luxury exhibition catalog.
**Noir** β Darkroom analog aesthetic with warm amber accents on a deep cool-black base. Sepia-tinted photos with vignette hover effects, film-edge EXIF labels in monospace, and a grain overlay. Inspired by wet-plate photography and film noir cinematography.
**Monograph** β Type-first book design with no hero image. Features an 8rem serif title, numbered photo indices via CSS counters, slide-up EXIF captions, hairline dividers, and generous whitespace. Feels like an artist monograph.
**Kunsthalle** β Photographs hung on a neutral grey gallery wall, the surround ISO 3664 viewing booths use because white pushes photos darker and black pushes them lighter: lighter than every dark preset, darker than every light one. Libre Franklin in all three slots like museum signage, sentence-case titles, a wall label under every photo (its number in the album, βNo. 07β, then the photo's caption when `exif.caption` publishes one, then camera Β· lens), a tight hung shadow and 56px of wall between works. With `photoFrame: passepartout` the mat is a grey board toned to the wall, and the camera line stays printed on the mat. The mosaic hero is a wall of works with the exhibition title below it; petrol is kept for wayfinding (links and hover). Suits fine-art series, documentary and black and white. Pairs best with `layout: masonry`.
**Ma** β Japanese restraint: negative space as the material. Warm washi-grey paper in light mode (not cream), sumi ink in dark, and a single vermilion accent shaped as a square hanko seal in the header, before the active link and the hero kicker. Cormorant Garamond 300 titles stand in for a Mincho, Hanken Grotesk 300 carries the text; the hero subtitle runs vertically (tategaki) on desktop, the split hero sits asymmetric with the photo inset in the paper, and album pages are offset to the right with 64px of air between photos. No EXIF in the grid, no hover transforms, slow fades. Suits landscape, film and black and white. Pairs best with `layout: masonry` and `columns: 2`; the washi look needs `mode: light`.
**Cyanotype** β A Prussian-blue print: ink-blue night in dark mode, and in light mode every piece of text in Prussian ink on cool paper instead of black. Set like an architectural drawing sheet: Jost (geometric, Bauhaus line) for type, DM Mono 300 for EXIF and numbers, a ruled title block for the hero EXIF chip and the album metadata, registration marks on the photo corners on hover, and hero links and album covers indexed as sheets (A-01, A-02). Suits architecture, urban and night work, blue hour and interiors. Pairs best with `layout: uniform` and `aspectRatio: "3/2"`.
**Salon** β The oxblood picture gallery of a 19th-century museum, done quietly: velvet-dark and plaster-rose walls, Gloock titles (never italic), Work Sans text, and a fine fillet line floating 6px outside every photo that brightens to the accent on hover. With `photoFrame: passepartout` the mat is an oxblood board in dark mode and plaster rose in light mode. The fullbleed hero uses a scrim tinted from the wall colour instead of black, with the title bottom left. A tinted surround changes how colour is judged: right for portrait, wedding and fashion work, wrong for colour-critical landscape work. Pairs best with `layout: showcase`.
**Birch** β Scandinavian daylight: birch-white and pine-night over green-tinted greys, with pine and lichen as the only accent. Albert Sans throughout and no uppercase or letter-spacing anywhere in the interface, where every other preset uses tracked caps. Soft 6px corners on photos and covers, EXIF as a pill chip on hover, a stacked hero with the photo inset and sentence-case strip labels. Suits landscape, outdoor, hiking, travel and family work. Pairs best with `layout: justified` (experimental) or `masonry`.
## Custom Theme
Start from any preset and override individual properties:
```yaml
theme:
preset: studio # base preset to extend (default: studio-modern)
accent: '#2563eb' # brand/accent color (any hex)
fonts:
heading: 'Inter' # Google Fonts name for headings
body: 'Inter' # body text
caption: 'JetBrains Mono' # EXIF captions
radius: 8 # border-radius in px (0 = sharp corners)
photoFrame: none # "none" | "passepartout" | "shadow"
grain: false # film grain overlay on photos
headerDot: false # accent-colored dot in the nav bar
heroStyle: split # "split" | "fullbleed" | "minimal" | "stacked" | "typographic" | "mosaic" | "cover"
```
All properties are optional β omitted values fall back to the preset defaults.
## Properties Reference
### `accent`
The brand color used for hover effects, navigation highlights, the header dot, and OG social preview images.
### `fonts`
Google Fonts names, as they appear on [fonts.google.com](https://fonts.google.com). Three font slots:
- **heading** β album titles, hero title, section labels
- **body** β navigation, descriptions, UI text
- **caption** β EXIF metadata, photo captions
Visitors never contact Google. The server fetches the stylesheet and the font files on first use, keeps them in `content/.fonts/` and serves them from your own domain through `/api/fonts`. Loading Google Fonts straight from Google sends every visitor's IP address there without consent, which a German court ruled unlawful in 2022.
The server therefore needs outbound HTTPS to `fonts.googleapis.com` and `fonts.gstatic.com` the first time a font is used. Once cached, the fonts keep working without it. If Google cannot be reached before that, the page falls back to the system fonts until it can.
### `radius`
Border radius in pixels applied to photo grid items and album cards:
- `0` β sharp, editorial corners
- `4-8` β subtle rounding
- `12+` β soft, modern feel
### `photoFrame`
How photos are framed in the grid:
- **`none`** β photos fill the grid cell directly
- **`passepartout`** β museum-style mat border around each photo with print-style EXIF captions below
- **`shadow`** β subtle drop shadow behind each photo
### `grain`
When `true`, a subtle film grain texture overlays each photo in the grid. Adds an analog, filmic character.
### `headerDot`
When `true`, shows a small accent-colored dot in the navigation bar (inspired by the Leica red dot).
### `heroStyle`
Controls the homepage hero layout:
- **`split`** β title/nav on the left, hero image on the right (used by Studio, Editorial)
- **`fullbleed`** β hero image fills the entire viewport, title overlaid in the corner (used by Minimal, Noir)
- **`minimal`** β centered title card with decorative ornament, hero image as a banner below (used by Classic)
- **`stacked`** β full-viewport hero image with title gradient-overlaid at the bottom, horizontal thumbnail navigation strip below
- **`typographic`** β no hero image; massive centered title with numbered album navigation list (used by Monograph)
- **`mosaic`** β asymmetric multi-image grid with frosted-glass title overlay centered on top
- **`cover`** _(experimental)_ β a splash screen: one fullscreen image, the site title and subtitle, and a single **Enter** link into the first navigation entry. The rest of the homepage is not rendered β visitors arrive at a title page and click through.
| Split |
Fullbleed |
Minimal |
 |
 |
 |
| Stacked |
Typographic |
Mosaic |
 |
 |
 |
### `grid.layout`
Controls the photo grid layout on album pages:
- **`masonry`** β Pinterest-style stacked columns, images shown at natural aspect ratios (default)
- **`uniform`** β CSS Grid with fixed aspect ratio cells
- **`showcase`** β first image displayed at full width (16:9), rest in standard grid
- **`filmstrip`** β horizontal scroll of tall vertical image strips with scroll snapping
- **`editorial-flow`** β alternating full-width (21:9) and side-by-side (4:3) image pairs
- **`justified`** _(experimental)_ β row-based: every row fills the container width and all images in a row share one height, so aspect ratios stay intact and nothing is cropped. `columns` sets the density β more columns give shorter rows. The last row keeps its natural sizes rather than stretching.
- **`essay`** β not a grid: renders the page as a photo essay. See **[Journal & Photo Essays](journal.md)**.
Set it globally in `settings.yaml`, per subpage in `gallery.yaml`, or on a
single album (experimental) β the three merge, most specific winning:
```yaml
grid:
layout: showcase
```
The same album in five of the grid layouts (theme `studio-modern`):
| Masonry (default) |
Uniform |
 |
 |
| Showcase |
Filmstrip |
 |
 |
| Editorial Flow |
|
### Album cover spacing
The album-cover grid on a subpage is sized by two CSS custom properties on
`.subpage-grid`:
| Property | Set by |
| ------------------- | -------------------------------------------------------------------- |
| `--subpage-columns` | the app, from the resolved `grid.columns` (site-wide or per subpage) |
| `--subpage-gap` | the **theme preset** β a per-subpage `grid.gap` overrides it |
The column count follows the configuration, the spacing belongs to the look.
`monograph` sets 1px, `minimal` and `editorial` 2px, `studio-modern` 20px;
`classic` and `noir` set nothing and get the 4px default from `globals.css`.
Of the later presets, `kunsthalle` sets 40px, `ma` 48px, `cyanotype` 24px,
`salon` 32px and `birch` 16px.
A custom preset does the same:
```css
[data-preset='mytheme'] .subpage-grid {
--subpage-gap: 12px;
}
```
Do not set `gap` directly there β an explicit per-page gap is passed as an
inline custom property and would no longer win.
## Examples
### Minimal portfolio
```yaml
theme:
preset: minimal
accent: '#0066cc'
fonts:
heading: 'Outfit'
body: 'Outfit'
```
### Film photography blog
```yaml
theme:
preset: studio
accent: '#d4a017'
grain: true
photoFrame: passepartout
```
### Modern magazine
```yaml
theme:
preset: editorial
radius: 12
fonts:
heading: 'Fraunces'
body: 'Inter'
caption: 'IBM Plex Mono'
```
### Luxury gallery
```yaml
theme:
preset: classic
accent: '#b8860b'
fonts:
heading: 'Playfair Display'
body: 'Lora'
```
### Dark analog portfolio
```yaml
theme:
preset: noir
heroStyle: stacked
```
### Type-forward monograph
```yaml
theme:
preset: monograph
accent: '#444'
fonts:
heading: 'DM Serif Text'
```
### Nature / travel journal
```yaml
theme:
preset: editorial
heroStyle: mosaic
grid:
layout: editorial-flow
```
## Upgrading & rolling back
Adding a preset never requires a migration. Presets are additive, the
`gallery.yaml` / `settings.yaml` schema is unchanged, and existing presets keep
rendering exactly as before β pull the new version and restart.
The one case that needs attention is a **rollback after switching presets**. The
selected preset is stored in `content/settings.yaml`, so if you pick a preset
that was added later and then downgrade to a version that predates it, config
loading fails:
```
Unknown theme preset "studio-modern". Valid presets: studio, minimal, editorial, classic, noir, monograph
```
The message lists the presets the running version accepts. Set `theme.preset` to
one of them:
```yaml
theme:
preset: studio # any preset the older version knows
```
`/admin` stays reachable when this happens, so you can also fix it from the
settings editor after the downgrade rather than beforehand.
The same error appears if you typo a preset name or copy an example for a preset
that does not exist β it is not specific to downgrades.