/* ── 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('./themes/kunsthalle.css'); @import url('./themes/ma.css'); @import url('./themes/cyanotype.css'); @import url('./themes/salon.css'); @import url('./themes/birch.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(--focus-ring); } /* ── 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 */ } /* Sticky footer: on a short page (a journal with two entries, the 404) the footer used to end wherever the content did, floating mid-viewport. The visitor chrome is a column whose
takes the spare height. Scoped to pages that carry that chrome — the admin, the setup screen and the site gate render straight into and keep their own layout. The header, skip link, lightbox and dialogs are fixed or absolute, so they stay out of the column. */ body:has(> #main-content) { display: flex; flex-direction: column; min-height: 100vh; min-height: 100dvh; } body:has(> #main-content) > .main { flex: 1 0 auto; /* A flex item with auto side margins shrinks to its content; the width puts it back to full width, --max-width still caps it. */ width: 100%; } body:has(> #main-content) > .footer { flex-shrink: 0; width: 100%; } /* 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; } /* The aperture sign inside EXIF lines (components/ExifText.tsx). Presets that set EXIF in capitals would otherwise print "Ƒ/4". */ .exif-fsign { text-transform: 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/): 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; } /* The wall label (PhotoGrid's figcaption): number, caption, EXIF under the photo. Hidden unless a preset hangs its photos with one (kunsthalle), so every other preset lays out as if it were not there. */ .photo-grid__label { display: none; } /* 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 , 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; } /* ─ Photo box inside a grid tile ─ */ /* Covers the tile (the box next/image `fill` used to take from the tile itself) unless a frame sets the mat variables. Clips the hover zoom to the photo, so it cannot spill onto a mat. */ .photo-grid__media { position: absolute; inset: var(--photo-mat, 0px) var(--photo-mat, 0px) var(--photo-mat-caption, 0px); overflow: hidden; } /* ─ Passepartout photo frame ─ */ /* A mat board around each photo, with the EXIF line printed in the wider bottom band like the caption under a print: always shown, never over the photo. The tile is the mat; PhotoGrid's .photo-grid__media holds the photo and is inset by the mat below (without a frame it covers the tile, the variables being unset). The geometry lives in these variables so the grain overlay and the justified row math (lib/justifiedRow.ts) inset by exactly the same amounts. Colours are the --mat-* tokens in tokens.css. */ [data-photo-frame='passepartout'] .photo-grid { --photo-mat: 8px; --photo-mat-caption: 32px; --photo-mat-border: 1px; /* Everything around the photo, border included, across and down. */ --photo-mat-x: calc(2 * (var(--photo-mat) + var(--photo-mat-border))); --photo-mat-y: calc(var(--photo-mat) + var(--photo-mat-caption) + 2 * var(--photo-mat-border)); } /* Also reaches the essay images, which share the class but have no mat. */ [data-photo-frame='passepartout'] .photo-grid__item { border: 1px solid var(--mat-border); padding: 8px 8px 24px; margin-bottom: var(--grid-gap, 24px); background: var(--mat-bg); overflow: hidden; } [data-photo-frame='passepartout'] .photo-grid .photo-grid__item { /* The aspect ratio a layout gives the tile (the photo's own in masonry) has to size the photo, not the photo plus its mat. */ box-sizing: content-box; border-width: var(--photo-mat-border); padding: var(--photo-mat) var(--photo-mat) var(--photo-mat-caption); } /* The rule above re-adds the masonry row spacing; these layouts space their rows with the grid or flex gap, and doubled it under this frame. */ [data-photo-frame='passepartout'] .photo-grid--uniform .photo-grid__item, [data-photo-frame='passepartout'] .photo-grid--showcase .photo-grid__item, [data-photo-frame='passepartout'] .photo-grid--filmstrip .photo-grid__item, [data-photo-frame='passepartout'] .photo-grid--editorial-flow .photo-grid__item, [data-photo-frame='passepartout'] .photo-grid--justified .photo-grid__item { margin-bottom: 0; } /* Justified tiles fill a wrapper whose size already counts the mat. */ [data-photo-frame='passepartout'] .photo-grid--justified .photo-grid__item { box-sizing: border-box; } [data-photo-frame='passepartout'] .photo-grid .photo-grid__item-play { inset: var(--photo-mat) var(--photo-mat) var(--photo-mat-caption); } /* Specific enough to beat the presets' own EXIF styles (noir, monograph, studio-modern), which are overlays meant for a frameless tile. */ [data-photo-frame='passepartout'] .photo-grid .photo-grid__item-exif { display: block; position: absolute; top: auto; left: var(--photo-mat); right: var(--photo-mat); bottom: 0; height: var(--photo-mat-caption); line-height: var(--photo-mat-caption); padding: 0; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; transform: none; color: var(--mat-text); font-family: var(--font-caption); font-style: italic; font-size: 0.75rem; letter-spacing: 0.02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; 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 ─ */ /* Over the photo only: the passepartout mat variables, 0 without a frame. */ [data-grain='true'] .photo-grid__item::after { content: ''; position: absolute; top: var(--photo-mat, 0px); left: var(--photo-mat, 0px); right: var(--photo-mat, 0px); bottom: var(--photo-mat-caption, 0px); 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; } /* ── 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-text); } /* 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-text); border-bottom-color: var(--accent); } .footer__link:hover { color: var(--accent-text); } .scroll-to-top:hover { border-color: var(--accent); color: var(--accent-text); } /* ── 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 { --photo-mat: 6px; --photo-mat-caption: 26px; } [data-photo-frame='passepartout'] .photo-grid .photo-grid__item-exif { font-size: 0.6875rem; } /* 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; } }