diff --git a/README.md b/README.md
index 5b40bef..ef84f87 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,16 @@ A self-hosted photography portfolio powered by [Immich](https://immich.app). Tur
Immich Folio acts as a **secure reverse proxy** between your visitors and your private Immich instance. Your Immich server stays on your local network, completely invisible to the outside world.
+
+
+
+
+
## Features
### Gallery & Layout
-- **Split hero homepage** — full-height hero with site title, subtitle, and navigation links on the left; hero image on the right
+- **Configurable hero layouts** — split (title + image side-by-side), fullbleed (edge-to-edge hero image), or minimal (centered title card)
- **Hero image carousel** — single image or crossfade carousel of multiple Immich assets
- **Masonry photo grid** — responsive layout with natural aspect ratios and configurable columns, gap, and aspect ratio
- **Uniform grid mode** — switch to a fixed-aspect uniform grid per-subpage or globally
@@ -86,12 +91,31 @@ All gallery structure — hero images, albums, subpages, grid layout, footer —
### Theming
-Customize the visual identity of your gallery with built-in presets or fine-grained control over colors, fonts, corners, photo frames, and more.
+Four built-in presets with distinct visual identities — or mix and match with fine-grained control over colors, fonts, corners, photo frames, and hero layout.
```yaml
-theme: minimal # or: studio, editorial, classic
+theme: studio # or: minimal, editorial, classic
```
+
+
+ Studio Leica-inspired, red dot |
+ Minimal Swiss brutalist, fullbleed hero |
+
+
+  |
+  |
+
+
+ Editorial Cinematic magazine, warm charcoal |
+ Classic Gilded gallery, gold accents |
+
+
+  |
+  |
+
+
+
→ **[Theming Guide](docs/theming.md)**
## Docker
diff --git a/app/globals.css b/app/globals.css
index f7f3a0e..5f3ac7d 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -279,10 +279,12 @@
flex-direction: column;
justify-content: flex-end;
padding: 24px;
- background: linear-gradient(to top,
- rgba(0, 0, 0, 0.8) 0%,
- rgba(0, 0, 0, 0.3) 40%,
- transparent 100%);
+ background: linear-gradient(
+ to top,
+ rgba(0, 0, 0, 0.8) 0%,
+ rgba(0, 0, 0, 0.3) 40%,
+ transparent 100%
+ );
opacity: 0;
transition: opacity var(--transition-normal);
}
@@ -412,10 +414,12 @@
/* ── Loading States ───────────────────────────────── */
.skeleton {
- background: linear-gradient(90deg,
- var(--bg-card) 0%,
- var(--bg-card-hover) 50%,
- var(--bg-card) 100%);
+ background: linear-gradient(
+ 90deg,
+ var(--bg-card) 0%,
+ var(--bg-card-hover) 50%,
+ var(--bg-card) 100%
+ );
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: var(--radius-sm);
@@ -792,7 +796,557 @@
color: var(--accent);
}
-/* ── Responsive ───────────────────────────────────── */
+/* ══════════════════════════════════════════════════════
+ THEME: Minimal — Swiss Brutalist
+ ══════════════════════════════════════════════════════ */
+
+/* Header: ultra-stripped, no border, just floating text */
+[data-preset='minimal'] .header {
+ border-bottom: none;
+ background: transparent;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+}
+
+[data-preset='minimal'] .header__nav-link {
+ font-size: 0.6rem;
+ font-weight: 400;
+ letter-spacing: 0.18em;
+ color: var(--text-muted);
+ border-bottom: none;
+}
+
+[data-preset='minimal'] .header__nav-link:hover,
+[data-preset='minimal'] .header__nav-link.active {
+ color: var(--text-primary);
+ border-bottom-color: transparent;
+}
+
+/* Hero: fullbleed image, title overlaid bottom-left */
+[data-preset='minimal'] .hero {
+ grid-template-columns: 1fr;
+ position: relative;
+}
+
+[data-preset='minimal'] .hero__left {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ padding: 32px 40px;
+ background: none;
+ align-items: flex-start;
+ justify-content: flex-end;
+}
+
+[data-preset='minimal'] .hero__content {
+ text-align: left;
+}
+
+[data-preset='minimal'] .hero__title {
+ font-family: var(--font-sans);
+ font-size: 0.7rem;
+ font-weight: 400;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ opacity: 0.6;
+}
+
+[data-preset='minimal'] .hero__subtitle {
+ font-size: 0.6rem;
+ letter-spacing: 0.15em;
+ margin-bottom: 16px;
+ opacity: 0.4;
+}
+
+[data-preset='minimal'] .hero__nav {
+ flex-direction: row;
+ gap: 16px;
+}
+
+[data-preset='minimal'] .hero__nav-link {
+ font-size: 0.6rem;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+ border-bottom: none;
+ opacity: 0.5;
+ transition: opacity var(--transition-fast);
+}
+
+[data-preset='minimal'] .hero__nav-link:hover {
+ opacity: 1;
+ border-bottom-color: transparent;
+ color: var(--text-primary);
+}
+
+[data-preset='minimal'] .hero__right {
+ grid-column: 1;
+ grid-row: 1;
+ height: 100vh;
+ height: 100svh;
+}
+
+/* Photo grid: zero hover effects, clinical stillness */
+[data-preset='minimal'] .photo-grid {
+ column-gap: 2px;
+}
+
+[data-preset='minimal'] .photo-grid__item {
+ margin-bottom: 2px;
+ border-radius: 0;
+ cursor: pointer;
+}
+
+[data-preset='minimal'] .photo-grid__item:hover img {
+ filter: none;
+ transform: none;
+}
+
+[data-preset='minimal'] .photo-grid__item:active img {
+ filter: brightness(0.8);
+ transform: none;
+}
+
+/* Album cards: no overlay, text below */
+[data-preset='minimal'] .album-card {
+ border: none;
+ border-radius: 0;
+}
+
+[data-preset='minimal'] .album-card:hover {
+ border-color: transparent;
+ transform: none;
+}
+
+[data-preset='minimal'] .album-card:hover .album-card__image {
+ transform: none;
+ filter: none;
+}
+
+[data-preset='minimal'] .album-card__overlay {
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, transparent 30%);
+ opacity: 1;
+}
+
+[data-preset='minimal'] .album-card__title {
+ font-family: var(--font-sans);
+ font-size: 0.7rem;
+ font-weight: 400;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
+}
+
+[data-preset='minimal'] .album-card__count {
+ font-size: 0.6rem;
+ font-weight: 400;
+ letter-spacing: 0.1em;
+}
+
+/* Section labels: numbered indices */
+[data-preset='minimal'] .section-label {
+ font-size: 0.6rem;
+ letter-spacing: 0.2em;
+ color: var(--text-muted);
+}
+
+/* Footer: single line, ultra-minimal */
+[data-preset='minimal'] .footer {
+ border-top: none;
+}
+
+[data-preset='minimal'] .footer__name {
+ font-size: 0.6rem;
+ letter-spacing: 0.15em;
+}
+
+/* Album header */
+[data-preset='minimal'] .album-header__title {
+ font-family: var(--font-sans);
+ font-size: 0.85rem;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ font-weight: 400;
+}
+
+[data-preset='minimal'] .album-header__meta {
+ font-size: 0.7rem;
+ letter-spacing: 0.1em;
+}
+
+/* Subpage grid */
+[data-preset='minimal'] .subpage-grid {
+ gap: 2px;
+}
+
+[data-preset='minimal'] .subpage-grid__item {
+ border: none;
+}
+
+[data-preset='minimal'] .subpage-grid__item-title {
+ font-family: var(--font-sans);
+ font-size: 0.75rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+/* ══════════════════════════════════════════════════════
+ THEME: Editorial — Cinematic Magazine
+ ══════════════════════════════════════════════════════ */
+
+/* Header: transparent, magazine masthead feel */
+[data-preset='editorial'] .header {
+ background: transparent;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+[data-preset='editorial'] .header__nav-link {
+ font-family: var(--font-sans);
+ font-size: 0.65rem;
+ font-weight: 400;
+ letter-spacing: 0.18em;
+}
+
+/* Hero: dramatic oversized serif title */
+[data-preset='editorial'] .hero__title {
+ font-size: 4.5rem;
+ font-weight: 300;
+ letter-spacing: 0.02em;
+ text-transform: none;
+ line-height: 1;
+}
+
+[data-preset='editorial'] .hero__subtitle {
+ font-family: var(--font-caption);
+ font-size: 0.9rem;
+ font-weight: 400;
+ letter-spacing: 0.08em;
+ text-transform: none;
+ font-style: italic;
+ margin-bottom: 48px;
+ opacity: 0.7;
+}
+
+[data-preset='editorial'] .hero__nav-link {
+ font-family: var(--font-caption);
+ font-size: 0.8rem;
+ font-style: italic;
+ font-weight: 400;
+ letter-spacing: 0.06em;
+ text-transform: none;
+ border-bottom: none;
+}
+
+[data-preset='editorial'] .hero__nav-link:hover {
+ color: var(--accent);
+ border-bottom-color: transparent;
+}
+
+/* Photo grid: generous spacing, cinematic transitions */
+[data-preset='editorial'] .photo-grid {
+ column-gap: 32px;
+}
+
+[data-preset='editorial'] .photo-grid__item {
+ margin-bottom: 32px;
+}
+
+[data-preset='editorial'] .photo-grid__item img {
+ transition:
+ filter 1.5s ease,
+ transform 1.5s ease;
+ filter: saturate(0.7) brightness(0.95);
+}
+
+[data-preset='editorial'] .photo-grid__item:hover img {
+ filter: saturate(1) brightness(1);
+ transform: scale(1.01);
+}
+
+/* Album cards: text below, editorial layout */
+[data-preset='editorial'] .album-card {
+ border: none;
+ border-radius: 0;
+ aspect-ratio: 16 / 9;
+}
+
+[data-preset='editorial'] .album-card:hover {
+ transform: none;
+ border-color: transparent;
+}
+
+[data-preset='editorial'] .album-card__overlay {
+ justify-content: flex-start;
+ padding: 28px;
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, transparent 40%);
+}
+
+[data-preset='editorial'] .album-card__title {
+ font-size: 2rem;
+ font-weight: 300;
+ letter-spacing: 0.02em;
+ text-transform: none;
+}
+
+[data-preset='editorial'] .album-card__count {
+ font-family: var(--font-caption);
+ font-style: italic;
+ font-size: 0.8rem;
+}
+
+[data-preset='editorial'] .album-card:hover .album-card__image {
+ transform: scale(1.015);
+ filter: brightness(0.8);
+ transition:
+ transform 1.5s ease,
+ filter 1.5s ease;
+}
+
+/* Section labels: pull-quote style with left border */
+[data-preset='editorial'] .section-label {
+ font-family: var(--font-caption);
+ font-size: 0.8rem;
+ font-style: italic;
+ font-weight: 400;
+ letter-spacing: 0.08em;
+ text-transform: none;
+ color: var(--text-secondary);
+ padding-left: 16px;
+ border-left: 2px solid var(--accent);
+}
+
+/* Album header: large cinematic title */
+[data-preset='editorial'] .album-header__title {
+ font-size: 3rem;
+ font-weight: 300;
+ letter-spacing: 0.02em;
+ text-transform: none;
+}
+
+[data-preset='editorial'] .album-header__meta {
+ font-family: var(--font-caption);
+ font-style: italic;
+}
+
+/* Footer: centered italic attribution */
+[data-preset='editorial'] .footer__name {
+ font-family: var(--font-caption);
+ font-style: italic;
+ text-transform: none;
+ letter-spacing: 0.06em;
+ font-size: 0.8rem;
+}
+
+/* Subpage grid */
+[data-preset='editorial'] .subpage-grid {
+ gap: 2px;
+}
+
+[data-preset='editorial'] .subpage-grid__item {
+ border: none;
+ aspect-ratio: 16 / 9;
+}
+
+[data-preset='editorial'] .subpage-grid__item-title {
+ font-size: 1.4rem;
+ letter-spacing: 0.02em;
+ font-weight: 300;
+ text-transform: none;
+}
+
+/* ══════════════════════════════════════════════════════
+ THEME: Classic — Gilded Gallery
+ ══════════════════════════════════════════════════════ */
+
+/* Header: centered nav with gold accent */
+[data-preset='classic'] .header {
+ background: rgba(18, 18, 16, 0.92);
+ border-bottom: 1px solid rgba(196, 154, 60, 0.15);
+}
+
+[data-preset='classic'][data-theme='light'] .header {
+ background: rgba(253, 251, 245, 0.92);
+}
+
+[data-preset='classic'] .header__nav {
+ justify-content: center;
+}
+
+[data-preset='classic'] .header__nav-link {
+ font-size: 0.7rem;
+ font-weight: 400;
+ letter-spacing: 0.14em;
+ padding: 20px 18px;
+}
+
+/* Hero: elegant centered card above banner */
+[data-preset='classic'] .hero__left {
+ padding: 80px 48px;
+}
+
+[data-preset='classic'] .hero__title {
+ font-size: 2.8rem;
+ font-weight: 400;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ line-height: 1.2;
+}
+
+[data-preset='classic'] .hero__subtitle {
+ font-family: var(--font-caption);
+ font-size: 0.85rem;
+ font-weight: 400;
+ letter-spacing: 0.15em;
+ font-style: italic;
+ margin-bottom: 12px;
+}
+
+/* Decorative ornament beneath subtitle */
+[data-preset='classic'] .hero__subtitle::after {
+ content: '◆';
+ display: block;
+ margin-top: 20px;
+ font-size: 0.5rem;
+ color: var(--accent);
+ letter-spacing: 0;
+}
+
+[data-preset='classic'] .hero__nav-link {
+ font-family: var(--font-caption);
+ font-size: 0.8rem;
+ font-weight: 400;
+ letter-spacing: 0.1em;
+ font-style: italic;
+ text-transform: none;
+ border-bottom: none;
+}
+
+[data-preset='classic'] .hero__nav-link:hover {
+ color: var(--accent);
+ border-bottom-color: transparent;
+}
+
+/* Photo grid: warm passepartout, elegant hover */
+[data-preset='classic'] .photo-grid {
+ column-gap: var(--grid-gap, 20px);
+}
+
+[data-preset='classic'] .photo-grid__item {
+ margin-bottom: var(--grid-gap, 20px);
+}
+
+[data-preset='classic'] .photo-grid__item:hover img {
+ filter: brightness(0.9);
+ transform: scale(1.01);
+}
+
+/* Warm passepartout override */
+[data-preset='classic'][data-photo-frame='passepartout'] .photo-grid__item {
+ background: #1a1816;
+ border-color: rgba(196, 154, 60, 0.12);
+}
+
+[data-preset='classic'][data-photo-frame='passepartout'][data-theme='light'] .photo-grid__item {
+ background: #fffdf5;
+ border-color: rgba(196, 154, 60, 0.2);
+}
+
+/* Album cards: warm rounded, gold hover */
+[data-preset='classic'] .album-card {
+ border-radius: 12px;
+ border: 1px solid rgba(196, 154, 60, 0.1);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
+}
+
+[data-preset='classic'][data-theme='light'] .album-card {
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ border-color: rgba(196, 154, 60, 0.15);
+}
+
+[data-preset='classic'] .album-card:hover {
+ border-color: var(--accent);
+ transform: translateY(-3px);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+[data-preset='classic'] .album-card__title {
+ font-weight: 400;
+ letter-spacing: 0.06em;
+}
+
+[data-preset='classic'] .album-card__count {
+ font-family: var(--font-caption);
+ font-style: italic;
+}
+
+/* Section labels: decorative centered dividers */
+[data-preset='classic'] .section-label {
+ text-align: center;
+ font-family: var(--font-caption);
+ font-weight: 400;
+ letter-spacing: 0.25em;
+ font-size: 0.7rem;
+ color: var(--text-muted);
+}
+
+[data-preset='classic'] .section-label::before {
+ content: '— ';
+ color: var(--accent);
+}
+
+[data-preset='classic'] .section-label::after {
+ content: ' —';
+ color: var(--accent);
+}
+
+/* Album header: elegant serif */
+[data-preset='classic'] .album-header__title {
+ font-weight: 400;
+ letter-spacing: 0.1em;
+ font-size: 2.2rem;
+}
+
+[data-preset='classic'] .album-header__meta {
+ font-family: var(--font-caption);
+ font-style: italic;
+}
+
+/* Footer: ornamental */
+[data-preset='classic'] .footer {
+ border-top: 1px solid rgba(196, 154, 60, 0.15);
+}
+
+[data-preset='classic'] .footer__name {
+ font-family: var(--font-caption);
+ font-style: italic;
+ text-transform: none;
+ letter-spacing: 0.08em;
+ font-size: 0.8rem;
+}
+
+/* Scroll-to-top: gold border */
+[data-preset='classic'] .scroll-to-top {
+ border-radius: 12px;
+}
+
+[data-preset='classic'] .scroll-to-top:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+/* Subpage grid: warm borders */
+[data-preset='classic'] .subpage-grid__item {
+ border: 1px solid rgba(196, 154, 60, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+[data-preset='classic'] .subpage-grid__item-title {
+ font-weight: 400;
+ letter-spacing: 0.06em;
+}
+
@media (max-width: 1024px) {
.photo-grid {
--grid-columns: 2;
@@ -859,4 +1413,28 @@
[data-photo-frame='passepartout'] .photo-grid__item {
padding: 6px 6px 18px;
}
-}
\ No newline at end of file
+
+ /* Minimal: adjust fullbleed hero for mobile */
+ [data-preset='minimal'] .hero__left {
+ padding: 20px 24px;
+ }
+
+ [data-preset='minimal'] .hero__right {
+ height: 70vh;
+ height: 70svh;
+ }
+
+ /* Editorial: scale down oversized title */
+ [data-preset='editorial'] .hero__title {
+ font-size: 2.5rem;
+ }
+
+ [data-preset='editorial'] .album-header__title {
+ font-size: 2rem;
+ }
+
+ /* Classic: scale title */
+ [data-preset='classic'] .hero__title {
+ font-size: 2rem;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index a6892e8..7700f45 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -57,6 +57,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
lang="en"
suppressHydrationWarning
style={themeVars as React.CSSProperties}
+ data-preset={theme.preset}
data-grain={theme.grain}
data-header-dot={theme.headerDot}
data-photo-frame={theme.photoFrame}
diff --git a/app/tokens.css b/app/tokens.css
index 675fa55..1624e6f 100644
--- a/app/tokens.css
+++ b/app/tokens.css
@@ -54,6 +54,99 @@
--accent-dim: rgba(200, 0, 15, 0.08);
}
+/* ── Minimal: Swiss Brutalist Palette ────────────── */
+[data-preset='minimal'] {
+ --bg-primary: #000000;
+ --bg-secondary: #0a0a0a;
+ --bg-card: #111111;
+ --bg-card-hover: #1a1a1a;
+ --bg-overlay: rgba(0, 0, 0, 0.98);
+
+ --text-primary: #ffffff;
+ --text-secondary: #888888;
+ --text-muted: #555555;
+
+ --border-subtle: #1a1a1a;
+ --border-hover: #333333;
+}
+
+[data-preset='minimal'][data-theme='light'] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f7f7f7;
+ --bg-card: #ffffff;
+ --bg-card-hover: #fafafa;
+ --bg-overlay: rgba(255, 255, 255, 0.98);
+
+ --text-primary: #000000;
+ --text-secondary: #666666;
+ --text-muted: #999999;
+
+ --border-subtle: #eeeeee;
+ --border-hover: #cccccc;
+}
+
+/* ── Editorial: Cinematic Magazine Palette ────────── */
+[data-preset='editorial'] {
+ --bg-primary: #1a1714;
+ --bg-secondary: #141210;
+ --bg-card: #1e1b17;
+ --bg-card-hover: #252119;
+ --bg-overlay: rgba(20, 18, 16, 0.98);
+
+ --text-primary: #ede8e0;
+ --text-secondary: #9a9086;
+ --text-muted: #6b6359;
+
+ --border-subtle: #2a2620;
+ --border-hover: #3a3530;
+}
+
+[data-preset='editorial'][data-theme='light'] {
+ --bg-primary: #faf8f4;
+ --bg-secondary: #f0ece4;
+ --bg-card: #ffffff;
+ --bg-card-hover: #f5f2ec;
+ --bg-overlay: rgba(250, 248, 244, 0.96);
+
+ --text-primary: #1a1714;
+ --text-secondary: #5a5550;
+ --text-muted: #8a857f;
+
+ --border-subtle: #e5e0d8;
+ --border-hover: #ccc7c0;
+}
+
+/* ── Classic: Gilded Gallery Palette ─────────────── */
+[data-preset='classic'] {
+ --bg-primary: #121210;
+ --bg-secondary: #0e0e0c;
+ --bg-card: #181816;
+ --bg-card-hover: #1e1e1a;
+ --bg-overlay: rgba(14, 14, 12, 0.98);
+
+ --text-primary: #f0ece4;
+ --text-secondary: #a09888;
+ --text-muted: #706858;
+
+ --border-subtle: #2a2822;
+ --border-hover: #3a3830;
+}
+
+[data-preset='classic'][data-theme='light'] {
+ --bg-primary: #fdfbf5;
+ --bg-secondary: #f5f0e6;
+ --bg-card: #ffffff;
+ --bg-card-hover: #faf6ee;
+ --bg-overlay: rgba(253, 251, 245, 0.96);
+
+ --text-primary: #1a1810;
+ --text-secondary: #5a5545;
+ --text-muted: #8a8575;
+
+ --border-subtle: #e5dfcf;
+ --border-hover: #d0c8b5;
+}
+
/* ── Reset ────────────────────────────────────────── */
*,
*::before,
@@ -84,4 +177,4 @@ a {
img {
display: block;
max-width: 100%;
-}
\ No newline at end of file
+}
diff --git a/content/gallery.yaml.example b/content/gallery.yaml.example
index b2806ea..82bbc84 100644
--- a/content/gallery.yaml.example
+++ b/content/gallery.yaml.example
@@ -52,10 +52,10 @@ subpages:
# ── Theme ─────────────────────────────────────────────────────
# Control the visual identity of your gallery. Use a preset name:
-# theme: studio # Leica-inspired, red accent, passepartout frames, film grain
-# theme: minimal # Clean, no distractions, grey accent
-# theme: editorial # Magazine feel, shadow frames
-# theme: classic # Warm, golden accent, rounded corners
+# theme: studio # Leica-inspired, red dot, passepartout frames, film grain
+# theme: minimal # Swiss brutalist, fullbleed hero, true B&W, zero-gap grid
+# theme: editorial # Cinematic magazine, warm charcoal, Bodoni serif, shadow frames
+# theme: classic # Gilded gallery, gold accents, Cinzel headings, passepartout
#
# Or customize individual properties (defaults from the preset):
# theme:
diff --git a/docs/theming.md b/docs/theming.md
index afec869..bcbf364 100644
--- a/docs/theming.md
+++ b/docs/theming.md
@@ -14,32 +14,42 @@ That's it. Your entire gallery switches to the minimal theme.
## Built-in Presets
-| Preset | Look | Accent | Fonts | Corners | Frame | Grain |
-|--------|------|--------|-------|---------|-------|-------|
-| **studio** | Leica-inspired, editorial | 🔴 `#e60012` | Playfair Display + DM Sans | Sharp | Passepartout | ✓ |
-| **minimal** | Clean, no distractions | ⚪ `#666666` | Inter | Sharp | None | ✗ |
-| **editorial** | Magazine feel | ⚫ `#1a1a1a` | Libre Baskerville + Source Sans | Slight | Shadow | ✗ |
-| **classic** | Warm, inviting | 🟡 `#8b6914` | Cormorant Garamond + Lato | Rounded | None | ✗ |
+| Preset | Style | Accent | Fonts | Hero | Frame | Grain |
+| ------------- | ------------------------------ | ------------ | -------------------------- | --------- | ------------ | ----- |
+| **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 | ✗ |
Default is `studio` if no theme is specified.
+### What makes each theme unique
+
+**Studio** (default) — 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.
+
## Custom Theme
Start from any preset and override individual properties:
```yaml
theme:
- preset: studio # base preset to extend (default: studio)
- accent: "#2563eb" # brand/accent color (any hex)
+ preset: studio # base preset to extend (default: studio)
+ 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"
+ 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"
```
All properties are optional — omitted values fall back to the preset defaults.
@@ -61,6 +71,7 @@ Google Fonts names. Fonts are loaded automatically — just use the name as it a
### `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
@@ -84,38 +95,53 @@ When `true`, shows a small accent-colored dot in the navigation bar (inspired by
### `heroStyle`
Controls the homepage hero layout:
-- **`split`** — title/nav on the left, hero image on the right (default)
-- **`fullbleed`** — hero image fills the entire viewport
-- **`minimal`** — text-only, no hero image
+
+- **`split`** — title/nav on the left, hero image on the right (used by Studio and Editorial)
+- **`fullbleed`** — hero image fills the entire viewport, title overlaid in the corner (used by Minimal)
+- **`minimal`** — centered title card with decorative ornament, hero image as a banner below (used by Classic)
## Examples
### Minimal portfolio
+
```yaml
theme:
preset: minimal
- accent: "#0066cc"
+ accent: '#0066cc'
fonts:
- heading: "Outfit"
- body: "Outfit"
+ heading: 'Outfit'
+ body: 'Outfit'
```
### Film photography blog
+
```yaml
theme:
preset: studio
- accent: "#d4a017"
+ accent: '#d4a017'
grain: true
photoFrame: passepartout
```
### Modern magazine
+
```yaml
theme:
preset: editorial
radius: 12
fonts:
- heading: "Fraunces"
- body: "Inter"
- caption: "IBM Plex Mono"
+ heading: 'Fraunces'
+ body: 'Inter'
+ caption: 'IBM Plex Mono'
+```
+
+### Luxury gallery
+
+```yaml
+theme:
+ preset: classic
+ accent: '#b8860b'
+ fonts:
+ heading: 'Playfair Display'
+ body: 'Lora'
```
diff --git a/lib/config.ts b/lib/config.ts
index d731a58..cd472be 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -58,19 +58,19 @@ const THEME_PRESETS: Record = {
},
minimal: {
preset: 'minimal',
- accent: '#666666',
- fonts: { heading: 'Inter', body: 'Inter', caption: 'JetBrains Mono' },
+ accent: '#000000',
+ fonts: { heading: 'Geist', body: 'Geist', caption: 'IBM Plex Mono' },
radius: 0,
photoFrame: 'none',
grain: false,
headerDot: false,
- heroStyle: 'split',
+ heroStyle: 'fullbleed',
},
editorial: {
preset: 'editorial',
- accent: '#1a1a1a',
- fonts: { heading: 'Libre Baskerville', body: 'Source Sans 3', caption: 'Source Sans 3' },
- radius: 4,
+ accent: '#8B2500',
+ fonts: { heading: 'Bodoni Moda', body: 'Newsreader', caption: 'Spectral' },
+ radius: 0,
photoFrame: 'shadow',
grain: false,
headerDot: false,
@@ -78,13 +78,13 @@ const THEME_PRESETS: Record = {
},
classic: {
preset: 'classic',
- accent: '#8b6914',
- fonts: { heading: 'Cormorant Garamond', body: 'Lato', caption: 'Lato' },
- radius: 8,
- photoFrame: 'none',
+ accent: '#c49a3c',
+ fonts: { heading: 'Cinzel', body: 'Crimson Pro', caption: 'Crimson Pro' },
+ radius: 12,
+ photoFrame: 'passepartout',
grain: false,
- headerDot: false,
- heroStyle: 'split',
+ headerDot: true,
+ heroStyle: 'minimal',
},
};