Files
immich-folio/app/globals.css
Ralksta ad8b09700b release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
2026-09-29 22:50:30 +02:00

2235 lines
50 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ── THEMES ── */
@import url('./themes/minimal.css');
@import url('./themes/editorial.css');
@import url('./themes/classic.css');
@import url('./themes/noir.css');
@import url('./themes/monograph.css');
@import url('./themes/studio-modern.css');
@import url('./tokens.css');
@import url('./leaflet.css');
/* ── Skip to content link ─────────────────────────── */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--bg-card);
color: var(--text-primary);
padding: 8px 16px;
z-index: 1000;
transition: top 0.2s ease;
border-bottom-right-radius: var(--radius-sm);
font-family: var(--font-sans);
font-weight: 500;
font-size: 0.85rem;
}
.skip-link:focus {
top: 0;
outline: 2px solid var(--text-primary);
}
/* ── Screen-reader-only text ──────────────────────── */
/* Used by the lightbox counter, which announces "Photo 3 of 53" while showing
the compact "3 / 53". Without this rule both strings render on top of each
other. */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Header / Navigation ──────────────────────────── */
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background: rgba(26, 26, 26, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-subtle);
padding: 0 24px;
}
[data-theme='light'] .header {
background: rgba(245, 245, 240, 0.9);
}
.header__nav {
display: flex;
align-items: center;
gap: 0;
max-width: var(--max-width);
margin: 0 auto;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.header__nav::-webkit-scrollbar {
display: none;
}
/* Brand wordmark — only presets that ask for it render it visibly. */
.header__wordmark {
display: none;
}
.header__nav-link {
padding: 18px 16px;
font-family: var(--font-sans);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-secondary);
white-space: nowrap;
transition: color var(--transition-fast);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.header__nav-link:hover,
.header__nav-link.active {
color: var(--text-primary);
border-bottom-color: var(--text-primary);
}
/* ── Header menu (MobileNav) ──────────────────────────
The links (Home, subpages, standalone albums, About, Map) live inside
MobileNav's wrapper. The menu — a button plus a dropdown panel — is the
default here, and the inline row is the exception further down: above the
phone breakpoint, and only while MobileNav has not marked the links
`data-collapsed` because they do not fit (#590).
The header used to rely on the breakpoint alone and scroll sideways with a
hidden scrollbar between 641px and whatever width the links needed, which
put About, Map and the theme toggle off-screen on every tablet. How many
links fit depends on the site's own subpages, so the width is measured
instead of guessed. */
.header__menu-btn {
display: flex;
align-items: center;
justify-content: center;
/* 44px touch target (20px icon + 12px padding); the negative margins
keep the header's height and spacing as before (#696). */
margin: -4px -4px -4px auto;
align-self: center;
padding: 12px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
}
.header__menu-btn svg {
width: 20px;
height: 20px;
display: block;
}
.header__nav-links {
display: none;
}
/* `.header` is the nearest positioned ancestor (it's `position: fixed`),
so `top: 100%` sits the panel right under it regardless of the actual
header height, which differs by preset (plain padding vs. studio-modern's
fixed `--header-height`). `left: 50%` + `width: 100vw` + negative margin
breaks the panel out to the full viewport width regardless of `.header`'s
own horizontal padding, which also differs by preset (`--sm-gutter` for
studio-modern vs. a fixed 24px elsewhere). */
.header__nav-links[data-open='true'] {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
left: 50%;
width: 100vw;
margin-left: -50vw;
max-height: calc(100vh - 64px);
overflow-y: auto;
background: rgba(26, 26, 26, 0.98);
border-bottom: 1px solid var(--border-subtle);
z-index: 101;
}
[data-theme='light'] .header__nav-links[data-open='true'] {
background: rgba(245, 245, 240, 0.98);
}
.header__nav-links[data-open='true'] .header__nav-link,
.header__nav-links[data-open='true'] .header__nav-link--external {
width: 100%;
padding: 16px 24px;
font-size: 0.75rem;
}
/* Covers the full viewport, including the area behind the header — the
header's own `z-index: 100` keeps it crisp above the dim, so this does
not need to know the header's height either. */
.header__nav-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 99;
}
/* The inline row. `display: contents` erases the wrapper's own box, so the
links become direct flex items of `.header__nav` and desktop keeps the
exact layout it always had. The breakpoint is the CSS half — it holds
before hydration, so a phone never paints the row first — and it has to
match PHONE_QUERY in MobileNav. */
@media (min-width: 641px) {
.header__menu-btn:not([data-collapsed='true']) {
display: none;
}
.header__nav-links:not([data-collapsed='true']) {
display: contents;
}
}
/* ── Main Content ─────────────────────────────────── */
.main {
max-width: var(--max-width);
margin: 0 auto;
padding: 104px 24px 96px;
/* 56px header + 48px original padding */
}
/* Simple CSS Page Fade-In (Replaces Framer Motion).
*
* Opacity only — no `transform`. A transform on `.main` (even the identity
* `translateY(0)` this keyframe used to end on, which `forwards` kept applied)
* turns the element into the containing block for every `position: fixed`
* descendant, so the proofing bar and its modal were pinned to the bottom of
* the page instead of floating over it. */
[data-transitions='true'] .main {
animation: page-fade-in 0.5s ease-out forwards;
}
@keyframes page-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Reduced motion, site-wide — visitor pages, admin and /install alike (#696).
*
* Durations collapse to near zero instead of `none`, so every animation and
* transition still lands on its end state (a drawer still opens, a toast still
* shows) and animationend/transitionend still fire for code that waits on
* them. Looping animations — the skeleton shimmer, spinners — run once and
* stop; the element itself stays visible. !important is needed to win over
* component styles and inline `style` animations (MapView's spinner). */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
transition-delay: 0ms !important;
scroll-behavior: auto !important;
}
}
/* Homepage hero + About page use full bleed — override main padding */
.main:has(.hero),
.main:has(.about) {
max-width: none;
padding-top: 56px;
/* Still need space for header */
padding-left: 0;
padding-right: 0;
padding-bottom: 0;
}
/* ── Hero Split Layout (Homepage) ────────────────── */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: calc(100vh - 56px);
}
.hero__left {
display: flex;
align-items: center;
justify-content: center;
padding: 64px 48px;
background: var(--bg-primary);
}
.hero__content {
text-align: center;
max-width: 400px;
}
.hero__title {
font-family: var(--font-serif);
font-size: 3rem;
font-weight: 600;
letter-spacing: 0.04em;
margin-bottom: 8px;
}
.hero__subtitle {
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 300;
letter-spacing: 0.2em;
text-transform: lowercase;
color: var(--text-muted);
font-style: italic;
margin-bottom: 40px;
}
.hero__nav {
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
}
.hero__nav-link {
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 300;
color: var(--text-secondary);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-color: rgba(255, 255, 255, 0.2);
transition:
color var(--transition-fast),
text-decoration-color var(--transition-fast);
padding: 4px 0;
}
.hero__nav-link:hover {
color: var(--text-primary);
text-decoration-color: var(--text-primary);
}
.hero__right {
position: relative;
overflow: hidden;
}
.hero__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.hero__image-placeholder {
width: 100%;
height: 100%;
background: var(--bg-secondary);
}
/* ── Hero Carousel Crossfade ────────────────────── */
.hero__carousel-image {
object-fit: cover;
opacity: 0;
transition: opacity 1.2s ease-in-out;
}
.hero__carousel-image--active {
opacity: 1;
}
/* Pause/play for the crossfade (WCAG 2.2.2). Sits over the photo, so it
takes the theme's background at partial opacity rather than a fixed
black; the 44px box is the touch target. */
.hero__carousel-toggle {
position: absolute;
right: 16px;
bottom: 16px;
z-index: 4;
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid var(--border-subtle);
border-radius: 50%;
background: color-mix(in srgb, var(--bg-primary) 70%, transparent);
color: var(--text-primary);
cursor: pointer;
opacity: 0.75;
transition: opacity var(--transition-fast);
}
.hero__carousel-toggle:hover,
.hero__carousel-toggle:focus-visible {
opacity: 1;
}
.hero__carousel-toggle svg {
width: 16px;
height: 16px;
}
/* Camera chip over the hero image — opt-in per preset. */
.hero__exif-chip {
display: none;
}
/* ── Hero: Fullbleed Layout ─────────────────────── */
.hero--fullbleed {
position: relative;
min-height: calc(100vh - 56px);
overflow: hidden;
}
/* ── Cover / Splash hero (EXPERIMENTAL) ───────────── */
/* Fullscreen welcome page: one image, the title, a single Enter link. */
.hero--cover {
position: relative;
min-height: calc(100vh - 56px);
min-height: calc(100dvh - 56px);
overflow: hidden;
}
.hero--cover > span,
.hero--cover > img {
position: absolute !important;
inset: 0;
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
.hero__cover-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 64px 40px;
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.1) 75%);
color: #fff;
}
.hero--cover .hero__title,
.hero--cover .hero__subtitle {
color: #fff;
}
.hero__cover-enter {
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 36px;
padding: 12px 32px;
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: var(--radius-lg, 4px);
color: #fff;
font-family: var(--font-caption);
font-size: 0.85rem;
letter-spacing: 0.18em;
text-transform: uppercase;
text-decoration: none;
transition:
background var(--transition-normal),
color var(--transition-normal);
}
.hero__cover-enter:hover,
.hero__cover-enter:focus-visible {
background: #fff;
color: #000;
}
.hero__cover-enter-arrow {
transition: transform var(--transition-normal);
}
.hero__cover-enter:hover .hero__cover-enter-arrow {
transform: translateX(4px);
}
/* HeroCarousel images fill the entire container */
.hero--fullbleed > span,
.hero--fullbleed > img {
position: absolute !important;
inset: 0;
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
.hero__fullbleed-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 64px 40px;
/* Multi-stop for contrast on any image brightness */
background:
radial-gradient(ellipse at center, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 70%),
linear-gradient(
to bottom,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.15) 40%,
rgba(0, 0, 0, 0.45) 100%
);
}
.hero--fullbleed .hero__title {
color: #fff;
/* Scales down on a phone instead of running off the edge; 4rem was cut off
at 390px in minimal and noir, whose single long words cannot wrap. */
font-size: clamp(2.25rem, 9vw, 4rem);
overflow-wrap: break-word;
text-shadow:
0 2px 8px rgba(0, 0, 0, 0.7),
0 8px 48px rgba(0, 0, 0, 0.6);
}
.hero--fullbleed .hero__subtitle {
color: rgba(255, 255, 255, 0.92);
margin-bottom: 32px;
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
}
.hero--fullbleed .hero__nav {
background: rgba(0, 0, 0, 0.38);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 14px 28px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.hero--fullbleed .hero__nav-link {
color: rgba(255, 255, 255, 0.7);
text-decoration-color: rgba(255, 255, 255, 0.25);
}
.hero--fullbleed .hero__nav-link:hover {
color: #fff;
text-decoration-color: #fff;
}
/* ── Hero: Minimal Layout ───────────────────────── */
.hero--minimal {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 56px);
padding: 64px 40px;
}
.hero--minimal .hero__content {
text-align: center;
max-width: 500px;
}
.hero--minimal .hero__title {
font-size: 4.5rem;
font-weight: 300;
letter-spacing: 0.08em;
margin-bottom: 12px;
}
.hero--minimal .hero__subtitle {
margin-bottom: 48px;
}
/* ── Hero: Stacked Layout ───────────────────────── */
.hero--stacked {
display: flex;
flex-direction: column;
min-height: calc(100vh - 56px);
}
.hero__stacked-image {
position: relative;
flex: 1;
min-height: 70vh;
min-height: 70svh;
}
.hero__stacked-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
padding: 48px 40px;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.8) 0%,
rgba(0, 0, 0, 0.3) 50%,
transparent 100%
);
}
.hero--stacked .hero__title {
font-family: var(--font-serif);
font-size: 3.5rem;
font-weight: 300;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #fff;
margin-bottom: 4px;
}
.hero--stacked .hero__subtitle {
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 300;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
}
.hero__thumbnail-strip {
display: flex;
gap: 2px;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
background: var(--bg-secondary);
padding: 0;
}
.hero__thumbnail-strip::-webkit-scrollbar {
display: none;
}
.hero__thumbnail-item {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
padding: 16px 28px;
background: var(--bg-card);
transition:
background var(--transition-fast),
color var(--transition-fast);
}
.hero__thumbnail-item:hover {
background: var(--bg-card-hover);
}
.hero__thumbnail-label {
font-family: var(--font-sans);
font-size: 0.7rem;
font-weight: 400;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-secondary);
white-space: nowrap;
transition: color var(--transition-fast);
}
.hero__thumbnail-item:hover .hero__thumbnail-label {
color: var(--text-primary);
}
/* ── Hero: Typographic Layout ───────────────────── */
.hero--typographic {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 56px);
padding: 64px 40px;
}
.hero--typographic .hero__content {
text-align: center;
max-width: 700px;
}
.hero--typographic .hero__title {
font-family: var(--font-serif);
font-size: 8rem;
font-weight: 400;
letter-spacing: -0.02em;
line-height: 0.95;
margin-bottom: 16px;
}
.hero--typographic .hero__subtitle {
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 300;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 32px;
}
.hero__divider {
width: 60px;
height: 1px;
background: var(--border-subtle);
margin: 0 auto 32px;
}
.hero__nav--indexed {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
}
.hero__nav--indexed .hero__nav-link {
display: inline-flex;
align-items: center;
gap: 12px;
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 300;
letter-spacing: 0.06em;
color: var(--text-secondary);
padding: 6px 0;
text-decoration: none;
text-transform: none;
border-bottom: none;
transition: color var(--transition-fast);
}
.hero__nav--indexed .hero__nav-link:hover {
color: var(--text-primary);
}
/* Index + album count ship with every hero nav link; presets opt in to
showing them. Only the indexed nav shows the index by default. */
.hero__nav-index {
display: none;
font-family: var(--font-caption, monospace);
font-size: 0.65rem;
font-weight: 400;
letter-spacing: 0.1em;
color: var(--text-muted);
min-width: 20px;
}
.hero__nav--indexed .hero__nav-index {
display: inline-block;
}
.hero__nav-count,
.hero__thumbnail-index {
display: none;
}
/* ── Hero: Mosaic Layout ────────────────────────── */
.hero--mosaic {
position: relative;
display: block; /* override inherited 1fr 1fr split grid from .hero */
min-height: calc(100vh - 56px);
}
.hero__mosaic-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr 1fr;
gap: 2px;
min-height: calc(100vh - 56px);
}
.hero__mosaic-cell {
position: relative;
overflow: hidden;
}
.hero__mosaic-cell img {
object-fit: cover;
}
/* Cell 1: spans 2 rows on the left */
.hero__mosaic-cell--1 {
grid-column: 1;
grid-row: 1 / 3;
}
/* Cell 2: wide, top right — spans 2 columns */
.hero__mosaic-cell--2 {
grid-column: 2 / 4;
grid-row: 1;
}
/* Cell 3: bottom middle */
.hero__mosaic-cell--3 {
grid-column: 2;
grid-row: 2;
}
/* Cell 4: bottom right */
.hero__mosaic-cell--4 {
grid-column: 3;
grid-row: 2;
}
.hero__mosaic-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
text-align: center;
padding: 40px 48px;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.hero--mosaic .hero__title {
font-family: var(--font-serif);
font-size: 3rem;
font-weight: 300;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #fff;
margin-bottom: 4px;
}
.hero--mosaic .hero__subtitle {
font-family: var(--font-sans);
font-size: 0.8rem;
font-weight: 300;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 24px;
}
.hero--mosaic .hero__nav {
display: flex;
flex-direction: row;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.hero--mosaic .hero__nav-link {
font-size: 0.7rem;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
text-decoration: none;
padding: 4px 0;
border-bottom: 1px solid transparent;
transition:
color var(--transition-fast),
border-color var(--transition-fast);
}
.hero--mosaic .hero__nav-link:hover {
color: #fff;
border-bottom-color: #fff;
}
/* ── Section Labels ──────────────────────────────── */
.section-label {
font-family: var(--font-sans);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--text-muted);
margin-top: 48px;
margin-bottom: 20px;
}
.section-label:first-child {
margin-top: 0;
}
/* ── Subpage Header ──────────────────────────────── */
.subpage-header {
margin-bottom: 40px;
}
/* Kicker + meta block are rendered for every preset but stay hidden
unless a preset opts in (studio-modern uses both). */
.subpage-header__kicker,
.subpage-header__meta {
display: none;
}
.subpage-title {
font-family: var(--font-serif);
font-size: 2.5rem;
font-weight: 400;
letter-spacing: 0.02em;
margin-bottom: 8px;
}
.subpage-subtitle {
font-family: var(--font-sans);
font-size: 0.9rem;
color: var(--text-secondary);
font-weight: 300;
letter-spacing: 0.02em;
}
/* ── Subpage TOC (Typographic Navigation) ────────── */
.subpage-toc {
display: flex;
flex-wrap: wrap;
gap: 0;
margin-bottom: 56px;
margin-top: 8px;
}
.subpage-toc__entry {
display: flex;
align-items: center;
}
.subpage-toc__entry + .subpage-toc__entry::before {
content: '·';
padding: 0 12px;
color: var(--text-muted);
font-size: 1rem;
line-height: 1;
}
.subpage-toc__link {
display: inline-flex;
align-items: baseline;
gap: 6px;
text-decoration: none;
color: var(--text-secondary);
transition: color var(--transition-fast);
}
.subpage-toc__link:hover {
color: var(--text-primary);
}
.subpage-toc__num {
font-family: var(--font-caption, monospace);
font-size: 0.6rem;
font-weight: 400;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.subpage-toc__label {
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 400;
letter-spacing: 0.05em;
}
/* ── Subpage Section ─────────────────────────────── */
.subpage-section {
scroll-margin-top: 80px;
margin-bottom: 72px;
}
.subpage-section__header {
margin-bottom: 28px;
}
.subpage-section__title {
font-family: var(--font-serif);
font-size: 1.6rem;
font-weight: 400;
letter-spacing: 0.02em;
margin-bottom: 6px;
}
.subpage-section__desc {
font-family: var(--font-sans);
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 300;
letter-spacing: 0.02em;
margin-bottom: 16px;
}
.subpage-section__rule {
height: 1px;
background: var(--border-subtle);
width: 100%;
}
/* The album-cover grid. `--subpage-columns` comes from the resolved grid
config (global settings.yaml < per-subpage override); the fallback keeps the
historical two-up layout for anything rendering this grid without the vars.
`--subpage-gap` is a *preset* decision — the theme files below set it, and
only an explicit per-subpage `gap:` overrides it (inline vars win). */
.subpage-grid {
display: grid;
grid-template-columns: repeat(var(--subpage-columns, 2), 1fr);
gap: var(--subpage-gap, 4px);
}
.subpage-grid__item {
position: relative;
overflow: hidden;
aspect-ratio: 3 / 2;
cursor: pointer;
display: block;
}
/* Media wrapper keeps the cover square-on while presets that want a
caption bar below the image can grow the card. */
.subpage-grid__item-media {
position: relative;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
.subpage-grid__item-caption {
display: none;
}
.subpage-grid__item img {
width: 100%;
height: 100%;
object-fit: cover;
transition:
transform var(--transition-slow),
filter var(--transition-slow);
}
.subpage-grid__item:hover img {
transform: scale(1.03);
filter: brightness(0.75);
}
.subpage-grid__item-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 50%);
opacity: 0;
transition: opacity var(--transition-normal);
}
.subpage-grid__item:hover .subpage-grid__item-overlay {
opacity: 1;
}
.subpage-grid__item-title {
font-family: var(--font-serif);
font-size: 1.2rem;
font-weight: 400;
letter-spacing: 0.03em;
}
.subpage-grid__item-count {
font-size: 0.8rem;
color: var(--text-secondary);
font-weight: 300;
margin-top: 2px;
}
/* ── Album Grid (Homepage) ────────────────────────── */
.album-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
}
.album-card {
position: relative;
overflow: hidden;
border-radius: var(--radius-md);
aspect-ratio: 3 / 2;
cursor: pointer;
background: var(--bg-card);
border: 1px solid var(--border-subtle);
transition:
border-color var(--transition-normal),
transform var(--transition-normal);
}
.album-card:hover {
border-color: var(--border-hover);
transform: translateY(-2px);
}
.album-card__image {
width: 100%;
height: 100%;
object-fit: cover;
transition:
transform var(--transition-slow),
filter var(--transition-slow);
}
.album-card:hover .album-card__image {
transform: scale(1.03);
filter: brightness(0.7);
}
.album-card__overlay {
position: absolute;
inset: 0;
display: flex;
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%
);
opacity: 0;
transition: opacity var(--transition-normal);
}
.album-card:hover .album-card__overlay {
opacity: 1;
}
.album-card__title {
font-family: var(--font-serif);
font-size: 1.5rem;
font-weight: 400;
letter-spacing: 0.03em;
margin-bottom: 4px;
}
.album-card__count {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 300;
}
/* ── Photo Grid (Album Detail) ────────────────────── */
/* ── Album Hero Image ────────────────────────────────────────── */
.album-hero {
position: relative;
width: 100%;
height: clamp(240px, 40vw, 520px);
overflow: hidden;
margin-bottom: 0;
}
.album-hero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 40%, var(--bg-primary, #fff) 100%);
pointer-events: none;
}
.album-header {
margin-bottom: 40px;
}
.album-header--has-hero {
margin-top: -60px;
position: relative;
z-index: 1;
}
.album-header__back {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 16px;
transition: color var(--transition-fast);
}
.album-header__back:hover {
color: var(--text-primary);
}
.album-header__back svg {
width: 16px;
height: 16px;
}
/* ── Album navigation (previous / next) ── */
.album-nav {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 16px;
max-width: var(--max-width);
margin: 64px auto 96px;
padding: 24px 24px 0;
border-top: 1px solid var(--border-subtle);
}
.album-nav__link {
display: inline-flex;
align-items: center;
gap: 12px;
max-width: 45%;
color: var(--text-secondary);
transition: color var(--transition-fast);
}
.album-nav__link:hover {
color: var(--text-primary);
}
.album-nav__link--next {
margin-left: auto;
text-align: right;
}
.album-nav__link svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.album-nav__text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.album-nav__kicker {
font-family: var(--font-caption);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
.album-nav__name {
font-family: var(--font-serif);
font-size: 1.05rem;
color: inherit;
/* Long album names must not push the pair apart. */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 600px) {
.album-nav {
margin: 48px auto 64px;
padding: 20px 16px 0;
}
.album-nav__name {
font-size: 0.95rem;
}
}
.album-header__main {
/* A flex child defaults to min-width: auto, so one long word in the title
would widen the header past the viewport on phones. */
min-width: 0;
}
.album-header__title {
font-family: var(--font-serif);
font-size: 2.5rem;
font-weight: 400;
letter-spacing: 0.02em;
hyphens: auto;
overflow-wrap: anywhere;
}
.album-header__description {
margin-top: 12px;
font-size: 1rem;
color: var(--text-secondary);
font-weight: 300;
line-height: 1.6;
max-width: 640px;
}
/* Client proofing link (/proof/<token>): greeting and notes under the title. */
.proof-session-intro {
margin-top: 16px;
max-width: 640px;
}
.proof-session-intro__greeting {
font-size: 1rem;
font-weight: 500;
color: var(--text-primary);
}
.proof-session-intro__text {
margin-top: 6px;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-secondary);
}
.proof-session-intro__meta {
margin-top: 6px;
font-size: 0.85rem;
color: var(--text-muted);
}
.album-header__meta {
margin-top: 8px;
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 300;
}
/* Date + gear ship with every album header but stay hidden unless a preset
opts in, so the meta line reads "86 photos" as it always has. */
.album-header__meta-date,
.album-header__meta-gear {
display: none;
}
.album-header__download {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 16px;
font-size: 0.85rem;
color: var(--text-secondary);
transition: color var(--transition-fast);
}
.album-header__download:hover {
color: var(--text-primary);
}
.album-header__download svg {
width: 15px;
height: 15px;
}
.photo-grid {
/* The gap the grid uses when settings.yaml does not set one. Declared rather
than left to each layout's own `var(--grid-gap, N)` fallback, because those
fallbacks disagree (6px filmstrip, 16px editorial-flow) and were dead code
while the variable was emitted unconditionally — 12px is what every layout
has actually rendered. A preset overrides it, an explicit setting beats
both by landing inline on this element (#513). */
--grid-gap: 12px;
column-count: var(--grid-columns, 3);
column-gap: var(--grid-gap, 12px);
}
/* Uniform layout: CSS Grid with fixed aspect ratio */
.photo-grid--uniform {
display: grid;
grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
gap: var(--grid-gap, 12px);
column-count: unset;
}
.photo-grid__item {
break-inside: avoid;
margin-bottom: var(--grid-gap, 12px);
border-radius: var(--radius-sm);
overflow: hidden;
cursor: pointer;
position: relative;
aspect-ratio: var(--grid-aspect-ratio, 1);
}
.photo-grid--uniform .photo-grid__item {
margin-bottom: 0;
}
/* ── Showcase Layout ────────────────────────────── */
/* First item gets full width; rest in standard grid */
.photo-grid--showcase {
display: grid;
grid-template-columns: repeat(var(--grid-columns, 3), 1fr);
gap: var(--grid-gap, 12px);
column-count: unset;
}
.photo-grid--showcase .photo-grid__featured {
grid-column: 1 / -1;
aspect-ratio: 16 / 9;
margin-bottom: 0;
}
.photo-grid--showcase .photo-grid__item {
margin-bottom: 0;
}
/* ── Filmstrip Layout ───────────────────────────── */
/* Horizontal scroll of tall vertical strips */
.photo-grid--filmstrip {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
gap: var(--grid-gap, 6px);
column-count: unset;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: var(--text-muted) transparent;
padding-bottom: 8px;
}
.photo-grid--filmstrip .photo-grid__item {
flex: 0 0 auto;
width: 280px;
height: 440px;
margin-bottom: 0;
scroll-snap-align: start;
aspect-ratio: unset;
}
@media (max-width: 640px) {
.photo-grid--filmstrip .photo-grid__item {
width: 220px;
height: 340px;
}
}
/* ── Editorial Flow Layout ──────────────────────── */
/* Alternating full-width singles and 2-column pairs */
.photo-grid--editorial-flow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--grid-gap, 16px);
column-count: unset;
}
.photo-grid--editorial-flow .photo-grid__item {
margin-bottom: 0;
aspect-ratio: 4 / 3;
}
/* Every 3rd item (1st, 4th, 7th, …) spans full width */
.photo-grid--editorial-flow .photo-grid__item:nth-child(3n + 1) {
grid-column: 1 / -1;
aspect-ratio: 21 / 9;
}
@media (max-width: 640px) {
.photo-grid--editorial-flow {
grid-template-columns: 1fr;
}
.photo-grid--editorial-flow .photo-grid__item:nth-child(3n + 1) {
aspect-ratio: 16 / 9;
}
.photo-grid--editorial-flow .photo-grid__item {
aspect-ratio: 16 / 9;
}
}
/* ── Justified Rows Layout (EXPERIMENTAL) ───────── */
/* Row-based justified layout: every row fills the container width, all
images in a row share the same height, aspect ratios stay intact.
Flex sizing (grow = aspect ratio) lives inline on the .fade-in wrapper. */
.photo-grid--justified {
display: flex;
flex-wrap: wrap;
gap: var(--grid-gap, 12px);
column-count: unset;
/* More columns → shorter rows, mirroring the density of the other layouts */
--grid-row-height: calc(900px / var(--grid-columns, 3));
}
/* Filler that soaks up leftover space so the last row keeps natural sizes
instead of stretching a lone image to full width */
.photo-grid--justified::after {
content: '';
flex-grow: 999999;
min-width: 0;
}
.photo-grid--justified > .fade-in {
height: var(--grid-row-height);
min-width: 0;
}
.photo-grid--justified .photo-grid__item {
height: 100%;
width: 100%;
margin-bottom: 0;
/* The base .photo-grid__item rule forces aspect-ratio from the grid
setting. Combined with height: 100% that pins the item width to
height × ratio, so it never fills the flex-grown wrapper and rows end
ragged. The wrapper's flex sizing is the only geometry source here. */
aspect-ratio: unset;
}
@media (max-width: 1024px) {
.photo-grid--justified {
--grid-row-height: 240px;
}
}
@media (max-width: 640px) {
.photo-grid--justified {
--grid-row-height: 160px;
}
}
.photo-grid__item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition:
filter var(--transition-normal),
transform var(--transition-slow);
}
.photo-grid__item:hover img {
filter: brightness(0.85);
transform: scale(1.02);
}
/* EXIF hover overlay */
.photo-grid__item-exif {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px 10px 8px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
color: rgba(255, 255, 255, 0.85);
font-size: 0.65rem;
font-weight: 400;
letter-spacing: 0.03em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
opacity: 0;
transition: opacity var(--transition-normal);
z-index: 2;
}
.photo-grid__item:hover .photo-grid__item-exif {
opacity: 1;
}
/* Hover-only overlays leave keyboard and touch visitors out (#696). A tile
with keyboard focus shows its overlay like a hovered one, and on devices
without hover — phones, and tablets wider than the 640px mobile rules — the
overlays are simply always shown. */
.photo-grid__item:is(:focus-visible, :has(:focus-visible)) .photo-grid__item-exif,
.subpage-grid__item:is(:focus-visible, :has(:focus-visible)) .subpage-grid__item-overlay,
.album-card:is(:focus-visible, :has(:focus-visible)) .album-card__overlay {
opacity: 1;
}
@media (hover: none) {
.photo-grid__item .photo-grid__item-exif,
.subpage-grid__item .subpage-grid__item-overlay,
.album-card .album-card__overlay {
opacity: 1;
}
}
.photo-grid__item-play {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.photo-grid__item-play svg {
width: 48px;
height: 48px;
color: white;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
transition: transform var(--transition-fast);
}
.photo-grid__item:hover .photo-grid__item-play svg {
transform: scale(1.1);
}
/* ── Loading States ───────────────────────────────── */
.skeleton {
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);
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ── Fade-In Animations ────────────────────────────── */
.fade-in {
opacity: 0;
transition:
opacity 0.6s ease,
transform 0.6s ease;
}
.fade-in--up {
transform: translateY(20px);
}
.fade-in--visible {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.fade-in {
opacity: 1;
transform: none;
transition: none;
}
}
/* ── Custom Cursor (Photo Grid) ─────────────────── */
.photo-grid__item {
cursor: crosshair;
}
/* ── Scroll-to-Top Button ────────────────────────── */
.scroll-to-top {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 90;
/* 44px: the minimum touch target, same as the proofing hearts (#696). */
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid var(--border-subtle);
background: var(--bg-card);
color: var(--text-secondary);
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
pointer-events: none;
transform: translateY(8px);
transition:
opacity var(--transition-normal),
transform var(--transition-normal),
background var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast);
}
.scroll-to-top__icon {
width: 18px;
height: 18px;
transition: transform var(--transition-fast);
}
.scroll-to-top:hover .scroll-to-top__icon {
transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
.scroll-to-top__icon {
transition: none;
}
.scroll-to-top:hover .scroll-to-top__icon {
transform: none;
}
}
.scroll-to-top--visible {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.scroll-to-top:hover {
background: var(--bg-card-hover);
border-color: var(--border-hover);
color: var(--text-primary);
}
/* ── Theme Toggle ────────────────────────────────── */
.theme-toggle {
display: flex;
align-items: center;
justify-content: center;
/* 44px touch target (16px icon + 14px padding). The negative block
margins keep the header from growing; no negative inline margin, which
pushed the button past the viewport edge on phones (#696). */
padding: 14px;
margin: -6px 0 -6px auto;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
}
.theme-toggle:hover {
color: var(--text-primary);
}
/* ── Footer ──────────────────────────────────────── */
.footer {
border-top: 1px solid var(--border-subtle);
padding: 32px 24px;
max-width: var(--max-width);
margin: 0 auto;
}
.footer__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.footer__left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px 20px;
/* Without wrapping, the name and three legal links in uppercase tracking
ran to about 440px on a phone and widened the page itself: the layout
viewport grew with it, and the fixed lightbox and menu panel grew along,
cutting off the close button and the next arrow. */
min-width: 0;
}
.footer__name {
font-family: var(--font-sans);
font-size: 0.75rem;
font-weight: 400;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
}
.footer__legal-link {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
/* --text-muted is already the quietest colour that clears 4.5:1; an extra
opacity on top halved that to about 2:1 in every light preset. */
color: var(--text-muted);
transition: color var(--transition-fast);
}
.footer__legal-link:hover {
color: var(--text-primary);
}
.footer__links {
display: flex;
gap: 12px;
align-items: center;
}
.footer__link {
color: var(--text-muted);
transition: color var(--transition-fast);
display: flex;
align-items: center;
}
.footer__link:hover {
color: var(--text-primary);
}
/* ── Image Count Badge (Subpage Covers) ─────────── */
.subpage-grid__item-badge {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
padding: 4px 10px;
border-radius: 100px;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
font-family: var(--font-sans);
font-size: 0.65rem;
font-weight: 400;
letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.85);
pointer-events: none;
}
/* ── Error / Empty States ─────────────────────────── */
.empty-state {
text-align: center;
padding: 96px 24px;
}
.empty-state__icon {
font-size: 3rem;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-state__text {
font-size: 1.1rem;
color: var(--text-secondary);
font-weight: 300;
}
.empty-state__title {
font-family: var(--font-serif);
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 400;
letter-spacing: -0.02em;
margin-bottom: 12px;
color: var(--text-primary);
}
.empty-state__actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-top: 32px;
}
/* Matches the PasswordGate button, the only other action button in the app. */
.empty-state__button {
padding: 12px 24px;
background: var(--accent);
color: var(--on-accent);
border: none;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.05em;
cursor: pointer;
text-decoration: none;
transition: opacity var(--transition-fast);
}
.empty-state__button:hover {
opacity: 0.9;
}
.empty-state__button--quiet {
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-subtle);
}
.empty-state__button--quiet:hover {
color: var(--text-primary);
border-color: var(--border-hover);
opacity: 1;
}
/* ── Theme Conditionals ──────────────────────────── */
/* Features toggled via data attributes on <html>, set by theme config */
/* ─ Header dot (accent circle in nav) ─ */
[data-header-dot='true'] .header__nav::before {
content: '';
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
margin-right: 8px;
flex-shrink: 0;
}
/* ─ Passepartout photo frame ─ */
[data-photo-frame='passepartout'] .photo-grid__item {
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px 8px 24px;
margin-bottom: var(--grid-gap, 24px);
background: #111;
overflow: hidden;
}
[data-photo-frame='passepartout'][data-theme='light'] .photo-grid__item {
border-color: rgba(0, 0, 0, 0.1);
background: #fff;
}
[data-photo-frame='passepartout'] .photo-grid__item-exif {
position: relative;
bottom: auto;
left: auto;
right: auto;
background: none;
padding: 8px 0 0;
color: var(--text-muted);
font-family: var(--font-caption);
font-style: italic;
font-size: 0.75rem;
letter-spacing: 0.02em;
white-space: normal;
opacity: 1;
text-align: center;
}
[data-photo-frame='passepartout'] .photo-grid__item:hover .photo-grid__item-exif {
opacity: 1;
}
/* ─ Shadow photo frame ─ */
[data-photo-frame='shadow'] .photo-grid__item {
border-radius: var(--radius-sm);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
[data-photo-frame='shadow'][data-theme='light'] .photo-grid__item {
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.12);
}
/* ─ Film grain overlay ─ */
[data-grain='true'] .photo-grid__item::after {
content: '';
position: absolute;
top: 8px;
left: 8px;
right: 8px;
bottom: 24px;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
background-size: 128px 128px;
pointer-events: none;
z-index: 1;
mix-blend-mode: overlay;
}
/* Grain without passepartout — fill the whole item */
[data-grain='true'][data-photo-frame='none'] .photo-grid__item::after,
[data-grain='true'][data-photo-frame='shadow'] .photo-grid__item::after {
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* ── Gallery Aesthetics (all themes) ─────────────── */
/* Generous spacing — gallery exhibition feel */
.main {
padding: 64px 40px 120px;
}
.album-header {
margin-bottom: 56px;
}
.album-header__title {
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 300;
font-size: 2rem;
}
.section-label {
font-weight: 400;
letter-spacing: 0.2em;
}
/* Photo grid gap */
.photo-grid {
column-gap: var(--grid-gap, 24px);
}
/* Shutter-click feedback */
.photo-grid__item:active img {
filter: brightness(0.6);
transform: scale(0.98);
transition: all 80ms ease;
}
/* Hero — wider, cleaner */
.hero__title {
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 300;
}
.hero__subtitle {
letter-spacing: 0.25em;
text-transform: uppercase;
font-style: normal;
font-weight: 300;
}
.hero__nav-link {
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.15em;
font-size: 0.75rem;
font-weight: 400;
padding: 6px 0;
border-bottom: 1px solid transparent;
transition:
border-color var(--transition-fast),
color var(--transition-fast);
}
.hero__nav-link:hover {
border-bottom-color: var(--accent);
color: var(--accent);
}
/* Album cards */
.album-card {
border-radius: var(--radius-md);
border-color: rgba(255, 255, 255, 0.08);
}
[data-theme='light'] .album-card {
border-color: rgba(0, 0, 0, 0.08);
}
.album-card:hover {
border-color: var(--accent);
transform: none;
}
/* Subpage grid */
.subpage-grid__item {
border: 1px solid rgba(255, 255, 255, 0.08);
}
[data-theme='light'] .subpage-grid__item {
border-color: rgba(0, 0, 0, 0.08);
}
/* Accent hover effects (works with any accent color) */
.header__nav-link:hover,
.header__nav-link.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.footer__link:hover {
color: var(--accent);
}
.scroll-to-top:hover {
border-color: var(--accent);
color: var(--accent);
}
/* ── Tablet (iPad, kleine Laptops) ─────────────────── */
/* Direkt column-count setzen → kein Konflikt mit Inline-Style der Custom Property */
@media (max-width: 1024px) and (min-width: 641px) {
.photo-grid {
column-count: 2;
}
.photo-grid--uniform,
.photo-grid--showcase {
grid-template-columns: repeat(2, 1fr);
}
/* Album covers follow the photo grids here: two-up on tablet widths, so a
configured 4 or 6 columns does not squeeze them below ~300px. The count is
computed server-side because repeat() does not reliably take a min(). */
.subpage-grid {
grid-template-columns: repeat(var(--subpage-columns-tablet, 2), 1fr);
}
}
@media (max-width: 640px) {
.hero {
grid-template-columns: 1fr;
}
.hero__right {
order: -1;
height: 50vh;
height: 50svh;
min-height: 200px;
}
.hero__left {
padding: 40px 24px;
}
.hero__title {
font-size: 2rem;
}
/* Portrait: 2 Spalten — direkt gesetzt, kein !important nötig */
.photo-grid {
column-count: 2;
}
.photo-grid--uniform,
.photo-grid--showcase {
grid-template-columns: repeat(2, 1fr);
}
/* Landscape-Handy → 3 Spalten */
@media (orientation: landscape) {
.photo-grid {
column-count: 3;
}
.photo-grid--uniform,
.photo-grid--showcase {
grid-template-columns: repeat(3, 1fr);
}
}
.album-grid {
grid-template-columns: 1fr;
}
.subpage-grid {
grid-template-columns: 1fr;
}
/* The header is about 37px tall here (the 44px menu button with its
negative margins); 32px left the album back link partly under it. */
.main {
padding: 56px 20px 80px;
}
.header__nav-link {
padding: 14px 10px;
font-size: 0.6rem;
}
.album-header__title {
font-size: 1.8rem;
}
.album-card__overlay {
opacity: 1;
}
.subpage-grid__item-overlay {
opacity: 1;
}
[data-photo-frame='passepartout'] .photo-grid__item {
padding: 6px 6px 18px;
}
/* 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;
}
/* Stacked: compact overlay */
.hero__stacked-overlay {
padding: 24px 20px;
}
.hero--stacked .hero__title {
font-size: 2rem;
}
.hero__thumbnail-item {
padding: 12px 20px;
}
/* Typographic: scale down massive title */
.hero--typographic {
padding: 40px 24px;
}
.hero--typographic .hero__title {
font-size: 3.5rem;
}
/* Mosaic: simplify to 2-column */
.hero__mosaic-grid {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
}
.hero__mosaic-cell--1 {
grid-row: auto;
}
.hero__mosaic-cell--2 {
grid-column: auto;
}
.hero__mosaic-cell:nth-child(n + 5) {
display: none;
}
.hero__mosaic-overlay {
padding: 24px 28px;
}
.hero--mosaic .hero__title {
font-size: 2rem;
}
/* Monograph: scale album header */
[data-preset='monograph'] .album-header__title {
font-size: 2rem;
}
}