feat: portfolio-led redesign with Shootings umbrella, original logo and motion layer

This commit is contained in:
desfoto automation
2026-09-19 17:11:25 +02:00
parent 2ccb39a69b
commit 447b909d05
177 changed files with 1853 additions and 773 deletions

View File

@@ -18,12 +18,13 @@ SITE = {
"theme_color": "#17140F",
"default_description": (
"desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster: "
"Businessfotografie, Portraits, Model- und Familienaufnahmen, Minishootings, "
"Videoprojekte und Social-Media-Inhalte."
"Shootings für Business, Portrait, Model und Familien, Minishootings, das mobile "
"Studio mit autarker Blitzanlage sowie Videoprojekte und Social-Media-Inhalte."
),
"keywords": (
"Fotograf Neumünster, Videograf Neumünster, Businessfotografie, Businessportrait, "
"Model-Shooting, Familienfotograf, Minishooting, Musikvideo, Social Media Inhalte, "
"Fotograf Neumünster, Videograf Neumünster, Shootings, Businessfotografie, "
"Businessportrait, Model-Shooting, Familienfotograf, Minishooting, mobiles Studio, "
"Blitzanlage, Musikvideo, Konzertfotografie, Social Media Inhalte, "
"Schleswig-Holstein"
),
}
@@ -67,8 +68,8 @@ VIDEO = {
NAV = [
{
"label": "Fotografie",
"href": "/fotografie/",
"label": "Shootings",
"href": "/shootings/",
"children": [
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
@@ -77,11 +78,128 @@ NAV = [
],
},
{"label": "Video", "href": "/video/"},
{"label": "Social Media", "href": "/social-media/"},
{"label": "Projekte", "href": "/projekte/"},
{"label": "Über", "href": "/ueber/"},
]
# Die zwei Säulen der Arbeit: Foto (Shootings) und Video bleiben getrennt,
# damit beide Bereiche für sich verständlich bleiben.
PILLARS = [
{
"slug": "foto",
"label": "Foto",
"href": "/shootings/",
"title": "Shootings",
"lead": "Der Überbegriff für alle fotografischen Arbeiten.",
"text": (
"Businessfotografie, Portrait & Model, Familien & Paare und Minishootings — "
"ein Shooting, ein Ablauf, ein Ansprechpartner. Du wählst nur, was du "
"wirklich brauchst."
),
"points": [
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
("Familien & Paare", "/familien-und-paare/"),
("Minishootings", "/minishootings/"),
],
"image": "business-portrait-studio",
"fit": "50% 30%",
},
{
"slug": "video",
"label": "Video",
"href": "/video/",
"title": "Videoprojekte",
"lead": "Bewegtbild von der Idee bis zum fertigen Schnitt.",
"text": (
"Musikvideo, Imagefilm, Eventfilm und Formate für Social Media — "
"gedreht, geschnitten und ausgeliefert in dem Format, in dem du es "
"veröffentlichst."
),
"points": [
("Videoprojekte", "/video/"),
("Social Media", "/social-media/"),
],
"image": "studio-lens",
"fit": "50% 45%",
},
]
# Was in einem Shooting steckt — die gemeinsamen Eckpunkte aller Shootings.
SHOOTING_STEPS = [
{
"title": "Vorab klären",
"text": "Wir sprechen kurz über Anlass, Ort und gewünschte Bildsprache. Daraus ergibt sich der Umfang.",
},
{
"title": "Licht planen",
"text": "Ob Tageslicht, mobiles Studio oder Bühne: Das Licht entscheidet, wie nah das Ergebnis an deiner Vorstellung liegt.",
},
{
"title": "Fotografieren",
"text": "Am Termin führe ich durch den Ablauf, gebe Haltung und Tempo vor und löse im richtigen Moment aus.",
},
{
"title": "Auswählen",
"text": "Du bekommst eine geordnete Auswahl und die bearbeiteten Bilddateien in den Größen, die du brauchst.",
},
]
# Das mobile Studio: der Betrieb kommt mit Licht und Hintergrund zum Termin.
STUDIO = {
"title": "Mobiles Studio",
"lead": "Das Studio kommt zu dir.",
"text": [
(
"Nicht jedes Team kann für ein Shooting den Betrieb verlassen. Deshalb"
" baue ich das Studio dort auf, wo deine Leute sowieso sind: im"
" Besprechungsraum, in der Werkstatt, im Vereinsheim oder draußen vor"
" der Halle."
),
(
"Dabei ist die komplette Ausrüstung dabei — Hintergründe, Blitze,"
" Lichtformer und Ersatztechnik. Die Blitzanlage ist autark, ich bin"
" vor Ort also sofort einsatzbereit und brauche nichts aus deinem"
" Haushalt."
),
],
"features": [
{
"title": "Komplette Ausrüstung",
"text": "Kamera, Objektive, Hintergründe, Blitze und Lichtformer kommen mit.",
},
{
"title": "Autarke Blitzanlage",
"text": "Das Licht ist im Gepäck und in wenigen Minuten aufgebaut.",
},
{
"title": "Dein Ort, deine Zeit",
"text": "Geschossen wird zwischen zwei Terminen, in der Mittagspause oder nach Feierabend.",
},
{
"title": "Überall in SH & Hamburg",
"text": "Vom Büro in Neumünster bis zum Betrieb in Kiel, Lübeck oder Hamburg.",
},
],
"image": "studio-setup",
}
# Netzwerk: eigene Projekte des Betreibers (ausdrücklicher Wunsch des Betreibers).
NETWORK = [
{
"label": "dennyschulz.de",
"href": "https://www.dennyschulz.de/",
"title": "dennyschulz.de",
"text": "Hochzeiten, Portraits und Business — die fotografische Arbeit unter eigenem Namen.",
},
{
"label": "dennyapp.de",
"href": "https://dennyapp.de/",
"title": "dennyapp.de",
"text": "Websites und Web-Apps aus Neumünster — Design, Entwicklung und eigene Produkte.",
},
]
SERVICES = [
{
"slug": "businessfotografie",
@@ -152,21 +270,23 @@ SERVICES = [
},
]
# Fotografie-Uebersicht
# Fotografie-Übersicht: Shootings als Überbegriff über den einzelnen Leistungen.
PHOTO_OVERVIEW = [
{
"title": "Menschen",
"title": "Shootings",
"text": (
"Portraits, Sedcards, Bewerbungsbilder und Familienaufnahmen. "
"Wir arbeiten mit Licht, das zu dir passt — nicht mit dem Standardschema."
"Ein Überbegriff, vier Wege: Businessfotografie für Teams und Marken, "
"Portrait & Model für die persönliche Bildsprache, Familien & Paare für "
"die Menschen, die zu dir gehören, und Minishootings für alle, die schnell "
"ein gutes Bild brauchen."
),
"image": "editorial-pose",
},
{
"title": "Marken & Unternehmen",
"title": "Mobiles Studio",
"text": (
"Team-, Standort- und Produktbilder, die auf Website, Kampagne und in "
"Stellenanzeigen funktionieren."
"Komplette Ausrüstung inklusive autarker Blitzanlage, Hintergründen und "
"Lichtformern — aufgebaut an deinem Ort, statt umgekehrt."
),
"image": "business-portrait-sit",
},
@@ -176,18 +296,18 @@ PHOTO_OVERVIEW = [
"Feiern, Events, Vereinsleben und Premieren. Dokumentarisch begleitet, "
"ohne den Ablauf auszubremsen."
),
"image": "event-dance",
"image": "group-feier",
},
]
WORK_GALLERY = [
("editorial-pose", "Model-Shooting im Studio", "g-w3", "4x5", "50% 28%"),
("travel-norway-pano", "Norwegen — Panorama", "g-w3", "4x5", "50% 50%"),
("people-forest", "Paarportrait im Wald", "g-w2", "1x1", "50% 26%"),
("editorial-back", "Editorial — Detail", "g-w2", "1x1", "50% 40%"),
("group-outdoor", "Feier im Freien", "g-w2", "1x1", "50% 36%"),
("movement-light", "Lichtspur aus langer Belichtung", "g-w3", "3x2", "50% 50%"),
("business-portrait-dark", "Businessportrait im Studiolicht", "g-w3", "3x2", "50% 30%"),
("music-guitar-silhouette", "Gitarrist als Silhouette im Bühnennebel", "g-w3", "4x5", "50% 42%"),
("free-fjord", "Steile Felsnadeln vor dem Meer", "g-w3", "4x5", "50% 50%"),
("music-strings", "Nahaufnahme einer Hand an den Saiten", "g-w2", "1x1", "50% 50%"),
("free-cat", "Nahaufnahme einer getigerten Katze", "g-w2", "1x1", "50% 42%"),
("business-portrait-dark", "Businessportrait im Studiolicht", "g-w2", "1x1", "50% 32%"),
("free-fog-forest", "Nebel über einem Nadelwald", "g-w3", "3x2", "50% 45%"),
("editorial-colour", "Model-Shooting im Studio", "g-w3", "3x2", "50% 30%"),
]
PROCESS = [
@@ -208,8 +328,9 @@ PROCESS = [
{
"title": "Dreh",
"text": (
"Am Termin übernehme ich Technik, Licht und Regie. Du musst nichts "
"mitbringen außer Zeit und dich."
"Am Termin übernehme ich Technik, Licht und Regie — auf Wunsch mit dem "
"mobilen Studio inklusive autarker Blitzanlage an deinem Ort. Du musst "
"nichts mitbringen außer Zeit und dich."
),
},
{
@@ -222,6 +343,10 @@ PROCESS = [
]
FACTS = [
{
"title": "Mobiles Studio",
"text": "Komplette Ausrüstung mit autarker Blitzanlage, Hintergründen und Lichtformern — aufgebaut an deinem Ort.",
},
{
"title": "Aus Neumünster",
"text": "Zuhause zwischen Kiel und Hamburg — unterwegs in Schleswig-Holstein und darüber hinaus.",
@@ -234,13 +359,10 @@ FACTS = [
"title": "Klarer Ablauf",
"text": "Umfang, Termin und Preis klären wir vor der Buchung. Keine Überraschungen auf der Rechnung.",
},
{
"title": "Schnelle Lieferung",
"text": "Kleine Aufträge bekommst du meist innerhalb weniger Tage, größere Projekte nach Absprache.",
},
]
FOCUS_ITEMS = [
("Shootings", "/shootings/"),
("Businessfotografie", "/businessfotografie/"),
("Portrait & Model", "/portrait-und-model/"),
("Familien & Paare", "/familien-und-paare/"),

View File

@@ -1,13 +1,8 @@
{
"_comment": "Quellbilder aus dem eigenen Bilderpool (Orgatool-Volume business-suite_app_data/website). 'slug' wird zum Dateinamen der Auslieferungsderivate. 'alt' ist die redaktionelle Bildbeschreibung fuer Alternativtexte. 'raw' ist der Pfad im Pool.",
"_comment": "Bildquellen des Betreibers (Denny Schulz). 'source': 'pool' = eigener Bilderpool (Orgatool-Volume, ausgeliefert ueber die eigene API), 'legacy' = eigene Portfolio-Dateien der bestehenden Seite www.dennyschulz.de/legacy/. 'slug' wird zum Dateinamen der Auslieferungsderivate. 'alt' ist die redaktionelle Bildbeschreibung fuer Alternativtexte; wo moeglich, stammt sie aus dem eigenen Portfolio. 'raw' ist der Pfad in der jeweiligen Quelle. Rechte liegen beim Betreiber (eigene Aufnahmen).",
"pool_base": "https://orga.dennyschulz.de/api/website/media/files",
"legacy_base": "https://www.dennyschulz.de/legacy",
"images": [
{
"slug": "hero-studio",
"raw": "Studio/1777027118997_1b1a51a6_dennyschulz_-07282.jpg",
"alt": "Hand an einer Kamera im Studio bei violettem Licht",
"focus": "50% 45%"
},
{
"slug": "studio-lens",
"raw": "Studio/1777027118366_91e2a99d_dennyschulz_-07285.jpg",
@@ -77,91 +72,73 @@
{
"slug": "editorial-colour",
"raw": "1776343219659_8daa3076_dennyschulz_-02946.jpg",
"alt": "Model in dunklem Kleid in farbigem Licht",
"focus": "50% 35%"
"alt": "Fashion-Portrait in kräftigen Farben",
"focus": "50% 30%"
},
{
"slug": "portrait-natural",
"raw": "1784293240857_deae4788_dennyschulz_-07255.jpg",
"alt": "Lachendes Paar im natürlichen Licht",
"focus": "50% 35%"
"alt": "Natürliches Portrait im Freien",
"focus": "50% 30%"
},
{
"slug": "portrait-couple",
"raw": "1784293231735_a8ccbd3e_dennyschulz_-08620.jpg",
"alt": "Paarportrait im Grünen",
"focus": "50% 35%"
"focus": "50% 30%"
},
{
"slug": "people-joy",
"raw": "1776343134779_e893c351_dennyschulz_-03745114107.jpg",
"alt": "Frau mit Blumenstrauß lacht in die Kamera",
"focus": "50% 35%"
"alt": "Paar lacht im Abendlicht",
"focus": "50% 30%"
},
{
"slug": "people-kiss",
"raw": "1776343151721_e3f65ef1_dennyschulz_-04483.jpg",
"alt": "Paar in inniger Umarmung",
"focus": "50% 40%"
},
{
"slug": "people-forest",
"raw": "Portfolio/hochzeit/1788686124131_7ce60a11_280826_dennyschulz_fav-18.jpg",
"alt": "Paar im Wald, Portrait",
"alt": "Paar küsst sich vor hellem Hintergrund",
"focus": "50% 30%"
},
{
"slug": "people-veil",
"raw": "Portfolio/hochzeit/1788686144082_7e69d21e_280826_dennyschulz_fav-22.jpg",
"alt": "Paar unter einem hellen Schleier, Gegenlicht",
"focus": "50% 40%"
},
{
"slug": "people-walk",
"raw": "Portfolio/hochzeit/1788347325902_9093b843_280826_feier-3.jpg",
"alt": "Paar geht Hand in Hand ueber einen Waldweg",
"focus": "50% 40%"
},
{
"slug": "group-feier",
"raw": "1777033639756_1f5a9dc4_dennyschulz_-03904.jpg",
"alt": "Gruppe feiert gemeinsam vor einem Gebäude",
"focus": "50% 45%"
"alt": "Feiergesellschaft bei einer Veranstaltung",
"focus": "50% 36%"
},
{
"slug": "group-outdoor",
"raw": "1784293233172_7a8a3ed9_dennyschulz_-08435.jpg",
"alt": "Gruppe bei einer Feier im Freien",
"focus": "50% 40%"
"alt": "Gruppe posiert im Freien",
"focus": "50% 36%"
},
{
"slug": "group-posiert",
"raw": "1784293233955_033a5079_dennyschulz_-08456.jpg",
"alt": "Gruppe posiert im Grünen",
"focus": "50% 45%"
"alt": "Gruppe steht zusammen vor der Kamera",
"focus": "50% 38%"
},
{
"slug": "event-motion",
"raw": "1776670469051_dc2993ae_dennyschulz_-04663.jpg",
"alt": "Menschen in Bewegung, bewusst unscharf aufgenommen",
"alt": "Bewegung auf einer Veranstaltung",
"focus": "50% 45%"
},
{
"slug": "event-speaker",
"raw": "1784293225986_a37d21f4_dennyschulz_-07139.jpg",
"alt": "Frau spricht mit Mikrofon bei einer Veranstaltung",
"alt": "Redner auf einer Veranstaltung",
"focus": "50% 35%"
},
{
"slug": "event-venue",
"raw": "1788932455845_78e0851f_dennyschulz_-00451.jpg",
"alt": "Stuhlreihen in einem festlich vorbereiteten Raum",
"focus": "50% 40%"
"alt": "Veranstaltungsort mit Gästen",
"focus": "50% 45%"
},
{
"slug": "event-dance",
"raw": "1784293226489_fa1b9420_dennyschulz_-09046.jpg",
"alt": "Paar tanzt unter einem Kronleuchter",
"alt": "Tanzfläche bei einer Feier",
"focus": "50% 40%"
},
{
@@ -171,40 +148,130 @@
"focus": "50% 50%"
},
{
"slug": "travel-japan",
"raw": "1776263908372_dennyschulz_-05756.jpg",
"alt": "Torii-Tor in einem moosgrünen Garten in Japan",
"focus": "45% 45%"
"slug": "music-vocal-blue",
"source": "legacy",
"raw": "_DES3495.jpg",
"alt": "Sänger mit erhobenem Arm im blauen Bühnenlicht",
"focus": "50% 28%"
},
{
"slug": "travel-norway-lake",
"raw": "1776264398432_dennyschulz_-1044.jpg",
"alt": "Spiegelung einer Hütte auf einem See in Norwegen",
"slug": "music-guitar-silhouette",
"source": "legacy",
"raw": "_DES3745.jpg",
"alt": "Gitarrist als Silhouette im Bühnennebel",
"focus": "50% 42%"
},
{
"slug": "music-strings",
"source": "legacy",
"raw": "_DES3153.jpg",
"alt": "Nahaufnahme einer Hand an den Saiten einer E-Gitarre",
"focus": "50% 50%"
},
{
"slug": "travel-norway-coast",
"raw": "1776332816743_c0361ec0_dennyschulz.jpg",
"alt": "Felsküste an einem Fjord in Norwegen",
"slug": "music-drums",
"source": "legacy",
"raw": "_DES3561.jpg",
"alt": "Schlagzeuger am Drumset in Schwarzweiß",
"focus": "50% 35%"
},
{
"slug": "music-mic",
"source": "legacy",
"raw": "dennyschulz_Jarl_-11.jpg",
"alt": "Bärtiger Sänger am Mikrofon mit erhobener Hand",
"focus": "50% 26%"
},
{
"slug": "band-extinct",
"source": "legacy",
"raw": "dennyschulz_20231211-107.jpg",
"alt": "Die Band Extinct steht hintereinander vor schwarzem Hintergrund",
"focus": "50% 32%"
},
{
"slug": "band-sagenbringer",
"source": "legacy",
"raw": "dennyschulz23-3422.jpg",
"alt": "Musiker in Fellkleidung im Wald, von unten fotografiert",
"focus": "50% 28%"
},
{
"slug": "free-fjord",
"source": "legacy",
"raw": "_DSC6468.jpg",
"alt": "Steile Felsnadeln vor dem Meer",
"focus": "50% 50%"
},
{
"slug": "travel-norway-valley",
"raw": "1776333775618_94cc71b8_dennyschulz_--2.jpeg",
"alt": "Flusstal zwischen Bergen im Morgennebel",
"slug": "free-lake-person",
"source": "legacy",
"raw": "20230830_dennyschulz-8903.jpg",
"alt": "Eine Person am Bergsee spiegelt sich im Wasser",
"focus": "50% 45%"
},
{
"slug": "free-fog-forest",
"source": "legacy",
"raw": "20230830_dennyschulz-8377.jpg",
"alt": "Nebel zieht durch einen Nadelwald am Berghang",
"focus": "50% 45%"
},
{
"slug": "free-mountain-view",
"source": "legacy",
"raw": "_DSC0477-f4c84b16.jpg",
"alt": "Eine Person sitzt über einer wolkenverhangenen Berglandschaft",
"focus": "50% 38%"
},
{
"slug": "free-fox",
"source": "legacy",
"raw": "_DSC8399.jpg",
"alt": "Nahaufnahme eines Rotfuchses",
"focus": "50% 45%"
},
{
"slug": "free-cat",
"source": "legacy",
"raw": "_DSC7748.jpg",
"alt": "Nahaufnahme einer getigerten Katze im Seitenlicht",
"focus": "50% 42%"
},
{
"slug": "free-golden-grass",
"source": "legacy",
"raw": "_DSC0730.jpg",
"alt": "Gräser im goldenen Licht des Sonnenuntergangs",
"focus": "50% 50%"
},
{
"slug": "travel-norway-pano",
"raw": "1776346804976_5ab84574_dennyschulz_pano2.jpg",
"alt": "Panorama einer Berglandschaft in Norwegen",
"focus": "50% 50%"
"slug": "free-woman-coast",
"source": "legacy",
"raw": "_DSC8274.jpg",
"alt": "Eine Frau mit Hut steht an einer felsigen Küste",
"focus": "50% 32%"
},
{
"slug": "travel-blossom",
"raw": "1776344687313_d34fd8f9_dennyschulz_-05183.jpg",
"alt": "Kirschblüte mit einem Eichhörnchen",
"focus": "50% 40%"
"slug": "free-modern-portrait",
"source": "legacy",
"raw": "_DSC2415.jpg",
"alt": "Portrait einer Frau mit Sonnenbrille",
"focus": "50% 28%"
},
{
"slug": "free-portrait-light",
"source": "legacy",
"raw": "_DS_5501.jpg",
"alt": "Frauenportrait im weichen Seitenlicht vor dunklem Hintergrund",
"focus": "50% 28%"
},
{
"slug": "free-arch-garden",
"source": "legacy",
"raw": "dennyschulz_-03704.jpg",
"alt": "Blick durch einen steinernen Torbogen in einen Garten",
"focus": "50% 45%"
}
]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import html
import json
from content import CONTACT, LEGAL, NAV, SITE
from content import CONTACT, LEGAL, NAV, NETWORK, SITE
# Filled by scripts/build-site.py before rendering: slug -> derivatives metadata.
IMAGE_TABLE: dict[str, dict] = {}
@@ -17,19 +17,27 @@ ICONS = {
"external": '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 5h5v5M19 5l-7 7M18 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h4"/></svg>',
}
MARK_SVG = """<svg class="brand__mark" viewBox="0 0 40 40" role="img" aria-label="desfoto Bildmarke">
<circle class="ring" cx="20" cy="20" r="13"/>
<circle class="ring" cx="20" cy="20" r="5.2" stroke-width="2"/>
<path class="blade" d="M20 7 20 1.6"/>
<path class="blade" d="M31.3 26.5 36 29.2"/>
<path class="blade" d="M8.7 26.5 4 29.2"/>
</svg>"""
BRAND_MARK = (
'<span class="brand__mark">'
'<img src="/assets/img/logo-160.png" width="160" height="160" alt="" decoding="async">'
"</span>"
)
def e(value: object) -> str:
return html.escape(str(value), quote=True)
def words(text: str, *, start: int = 0) -> str:
"""Wrap every word in a masked span so hero titles can rise word by word."""
parts = []
for index, word in enumerate(text.split()):
parts.append(
f'<span class="w" style="--i:{index + start}"><span>{e(word)}</span></span>'
)
return " ".join(parts)
def picture(
slug: str,
sizes: str,
@@ -154,6 +162,7 @@ def head(
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/assets/fonts.css">
<link rel="stylesheet" href="/assets/site.css">
<noscript><style>.reveal,.reveal--mask{{opacity:1;transform:none;clip-path:none}}.reveal--mask>*{{clip-path:none}}.hero__title .w>span,.hero__sub,.hero__actions,.hero__facts{{opacity:1;transform:none}}</style></noscript>
{preload_html}
{ld}
</head>
@@ -162,7 +171,7 @@ def head(
<header class="masthead">
<div class="shell masthead__inner">
<a class="brand" href="/" aria-label="desfoto Startseite">
{MARK_SVG}
{BRAND_MARK}
<span class="brand__text"><span>desfoto</span><small>{e(SITE['claim'])}</small></span>
</a>
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="site-nav" data-nav-toggle>
@@ -170,25 +179,32 @@ def head(
</button>
<nav class="nav" id="site-nav" aria-label="Hauptnavigation">{nav_html(path)}</nav>
</div>
<span class="progress" aria-hidden="true"><span class="progress__bar" data-progress></span></span>
</header>
<main id="main">"""
def footer_html() -> str:
network_items = "".join(
f'<a class="footer__ext" href="{e(item["href"])}" target="_blank" rel="noopener">'
f'{e(item["title"])} {ICONS["external"]}</a><small>{e(item["text"])}</small>'
for item in NETWORK
)
return f"""</main>
<footer class="footer">
<div class="shell">
<div class="footer__grid">
<div class="footer__brand">
<span class="brand">
{MARK_SVG}
{BRAND_MARK}
<span class="brand__text"><span>desfoto</span><small>{e(SITE['claim'])}</small></span>
</span>
<p class="footer__claim">{e(SITE['tagline'])}. Ein Angebot von {e(LEGAL['business'])}.</p>
</div>
<div>
<h2>Fotografie</h2>
<h2>Shootings</h2>
<div class="footer__links">
<a href="/shootings/">Überblick Shootings</a>
<a href="/businessfotografie/">Businessfotografie</a>
<a href="/portrait-und-model/">Portrait &amp; Model</a>
<a href="/familien-und-paare/">Familien &amp; Paare</a>
@@ -196,7 +212,7 @@ def footer_html() -> str:
</div>
</div>
<div>
<h2>Weitere Leistungen</h2>
<h2>Video &amp; mehr</h2>
<div class="footer__links">
<a href="/video/">Videoprojekte</a>
<a href="/social-media/">Social Media</a>
@@ -213,6 +229,10 @@ def footer_html() -> str:
<span class="muted">{e(CONTACT['region'])}</span>
</div>
</div>
<div>
<h2>Netzwerk</h2>
<div class="footer__links">{network_items}</div>
</div>
</div>
<div class="footer__bottom">
<nav aria-label="Rechtliches">

View File

@@ -10,14 +10,18 @@ from content import (
CONTACT,
FACTS,
LEGAL,
NETWORK,
PHOTO_OVERVIEW,
PILLARS,
PROCESS,
SERVICES,
SHOOTING_STEPS,
SITE,
STUDIO,
VIDEO,
WORK_GALLERY,
)
from layout import ICONS, cta_band, e, page, page_head, picture, preload, section
from layout import ICONS, cta_band, e, page, page_head, picture, preload, section, words
# ---------------------------------------------------------------- helper bits
@@ -34,14 +38,14 @@ def service_cards(slugs: list[str] | None = None) -> str:
f'<div class="card__foot"><a class="link-arrow" href="{e(service["href"])}">Mehr erfahren {ICONS["arrow"]}</a></div>'
"</article>"
)
return '<div class="grid grid--3">' + "".join(cards) + "</div>"
return '<div class="grid grid--3" data-stagger="0.08">' + "".join(cards) + "</div>"
def work_gallery() -> str:
figures = []
for slug, caption, width_class, ratio, fit in WORK_GALLERY:
figures.append(
f'<figure class="{width_class}">'
f'<figure class="{width_class} reveal reveal--mask">'
+ picture(
slug,
"(min-width: 62rem) 45vw, 92vw",
@@ -51,7 +55,7 @@ def work_gallery() -> str:
)
+ f"<figcaption>{e(caption)}</figcaption></figure>"
)
return '<div class="gallery">' + "".join(figures) + "</div>"
return '<div class="gallery" data-stagger="0.09">' + "".join(figures) + "</div>"
def process_steps() -> str:
@@ -99,6 +103,129 @@ def two_col(*, left: str, right: str, ratio: str = "minmax(0,1fr) minmax(0,1fr)"
return f'<div class="grid" style="grid-template-columns:{ratio}">{left}{right}</div>'
def media_grid(
items: list[tuple[str, str, str, str]],
*,
columns: str = "grid--3",
stagger: str = "0.09",
) -> str:
"""Responsive image grid; items are (slug, alt, ratio, object-position)."""
cells = "".join(
'<div class="reveal reveal--mask">'
+ picture(
slug,
"(min-width: 62rem) 30vw, (min-width: 40rem) 45vw, 92vw",
css_class=f"frame frame--{ratio}",
fit=fit,
alt=alt,
)
+ "</div>"
for slug, alt, ratio, fit in items
)
return f'<div class="grid {columns}" data-stagger="{stagger}">{cells}</div>'
def caption_gallery(
items: list[tuple[str, str, str, str, str]],
*,
stagger: str = "0.08",
) -> str:
"""Editorial gallery with captions; items match the WORK_GALLERY tuples."""
figures = "".join(
f'<figure class="{width_class}">'
+ picture(
slug,
"(min-width: 62rem) 45vw, 92vw",
css_class=f"frame frame--{ratio}",
fit=fit,
alt=caption,
)
+ f"<figcaption>{e(caption)}</figcaption></figure>"
for slug, caption, width_class, ratio, fit in items
)
return f'<div class="gallery" data-stagger="{stagger}">{figures}</div>'
def pillar_cards() -> str:
"""The two big entry cards: Shootings (photo) and Videoprojekte (video).
The body carries its own links, so the card must not be one great ``<a>``:
nested anchors are invalid HTML and the parser would break the grid apart.
``.pillar__go`` stretches over the card and keeps the whole surface clickable.
"""
cards = []
for pillar in PILLARS:
points = "".join(
f'<a href="{e(href)}">{e(label)}</a>' for label, href in pillar["points"]
)
cards.append(
'<article class="pillar spot reveal">'
'<span class="pillar__media">'
+ picture(
pillar["image"],
"(min-width: 62rem) 46vw, 92vw",
css_class="",
fit=pillar.get("fit"),
alt="",
)
+ "</span>"
'<div class="pillar__body">'
f'<span class="pillar__label">{e(pillar["label"])}</span>'
f'<h3 class="pillar__title">{e(pillar["title"])}</h3>'
f'<p class="pillar__text">{e(pillar["lead"])} {e(pillar["text"])}</p>'
f'<div class="pillar__points">{points}</div>'
f'<a class="pillar__go" href="{e(pillar["href"])}">'
f'{e(pillar["title"])} ansehen {ICONS["arrow"]}</a>'
"</div></article>"
)
return '<div class="pillars" data-stagger="0.12">' + "".join(cards) + "</div>"
def marquee(items: list[str]) -> str:
"""CSS-only running band; the list is duplicated for a seamless loop."""
spans = "".join(f"<span>{e(item)}</span>" for item in items)
return (
'<div class="marquee" aria-hidden="true">'
f'<div class="marquee__track">{spans}{spans}</div>'
"</div>"
)
def studio_section(*, variant: str = "paper2", tight: bool = False) -> str:
"""The mobile studio: complete kit including a self-sufficient flash system."""
features = "".join(
f'<div><h3>{e(feature["title"])}</h3><p>{e(feature["text"])}</p></div>'
for feature in STUDIO["features"]
)
paragraphs = "".join(f"<p>{e(paragraph)}</p>" for paragraph in STUDIO["text"])
content = (
'<div class="studio">'
'<div class="studio__media reveal reveal--mask">'
+ picture(
STUDIO["image"],
"(min-width: 62rem) 46vw, 92vw",
css_class="frame frame--4x5",
fit="50% 45%",
alt="Studioequipment mit Logo von Denny Schulz Fotografie",
)
+ '<span class="studio__badge"><strong>Autarke Blitzanlage</strong>'
"<span>Hintergründe, Blitze, Lichtformer</span></span>"
"</div>"
'<div class="stack reveal reveal--right">'
+ paragraphs
+ f'<div class="studio__features" data-stagger="0.07">{features}</div>'
"</div></div>"
)
return section(
kicker="Mobiles Studio",
title="Das Studio kommt zu dir",
lead="Komplette Ausrüstung inklusive autarker Blitzanlage — aufgebaut dort, wo deine Leute sind.",
content=content,
variant=variant,
tight=tight,
)
def contact_form() -> str:
return f"""<form class="form" data-contact-form novalidate action="mailto:{e(CONTACT['email'])}" method="post" enctype="text/plain">
<div class="field-row">
@@ -192,33 +319,34 @@ def service_ld(name: str, path: str, description: str) -> dict:
def home() -> str:
hero = f"""<section class="hero">
<div class="hero__media">
{picture('hero-studio', '100vw', css_class='', priority=True, loading='eager', fit='50% 45%', alt='Hand an einer Kamera im Studio bei violettem Licht')}
{picture('music-vocal-blue', '100vw', css_class='', priority=True, loading='eager', fit='50% 32%', alt='Sänger mit erhobenem Arm im blauen Bühnenlicht')}
</div>
<div class="shell hero__inner">
<span class="kicker hero__kicker">{e(SITE['tagline'])}</span>
<h1 class="hero__title">Bilder und Filme, die nicht beliebig aussehen.</h1>
<h1 class="hero__title">{words('Bilder von Menschen, Marken und Musik.')}</h1>
<p class="hero__sub">
desfoto ist die fotografische und filmische Arbeit von Denny Schulz — für Unternehmen,
Agenturen, Models, Vereine und Menschen, die sich selbst zeigen wollen.
desfoto ist die fotografische und filmische Arbeit von Denny Schulz aus Neumünster:
Shootings für Business, Portrait und Familie, dazu Konzerte, eigene Projekte,
Video und Inhalte für Social Media.
</p>
<div class="btn-row hero__actions">
<a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a>
<a class="btn btn--outline-light" href="/projekte/">Arbeiten ansehen</a>
</div>
<div class="hero__facts">
<div class="hero__fact"><strong>Shootings</strong>Business, Portrait, Familie und Minishootings unter einem Dach.</div>
<div class="hero__fact"><strong>Mobiles Studio</strong>Komplette Ausrüstung inklusive autarker Blitzanlage vor Ort.</div>
<div class="hero__fact"><strong>Foto &amp; Video</strong>Beides aus einer Hand, damit Optik und Ton zusammenpassen.</div>
<div class="hero__fact"><strong>Studio &amp; on location</strong>In Neumünster und überall dort, wo dein Projekt spielt.</div>
<div class="hero__fact"><strong>B2B &amp; privat</strong>Von der Agenturserie bis zum Familienshooting.</div>
<div class="hero__fact"><strong>Kurze Wege</strong>Direkter Kontakt, klare Absprachen, schnelle Lieferung.</div>
<div class="hero__fact"><strong>Musik &amp; Bühne</strong>Konzerte, Bands und Musikvideos aus eigener Erfahrung.</div>
</div>
</div>
<span class="scroll-cue" aria-hidden="true">Scroll<i></i></span>
</section>"""
statement = section(
tight=True,
kicker="Haltung",
content="""<div class="statement">
<div><p class="eyebrow">Kurz gesagt</p></div>
<div><p class="eyebrow">Haltung</p></div>
<div class="statement__body">
<p>Ich mache Bilder, die etwas erzählen und trotzdem <em>funktionieren</em>: auf der Website,
im Feed, im Bewerbungsportfolio, im Plakat. Kein Katalog von der Stange — wir klären kurz,
@@ -227,20 +355,22 @@ def home() -> str:
</div>""",
)
services = section(
kicker="Leistungen",
title="Was ich für dich mache",
lead="Sechs Bereiche, ein Ansprechpartner. Klick dich rein oder schreib direkt, wenn du unsicher bist, was zu deinem Vorhaben passt.",
content=service_cards(),
pillars = section(
kicker="Zwei Bereiche",
title="Shootings und Videoprojekte",
lead="Foto und Video bleiben getrennt, damit beide Bereiche für sich verständlich sind — und kommen trotzdem von derselben Person.",
content=pillar_cards(),
)
works = section(
variant="paper2",
kicker="Aus der Arbeit",
title="Auszug aus Aufträgen und eigenen Projekten",
lead="Studioportraits, Businessbilder, Paare, Veranstaltungen und Reisen — die Auswahl zeigt die Bandbreite, nicht nur den schönsten Moment.",
kicker="Portfolio",
title="Musik, Menschen und freie Arbeiten",
lead="Konzerte und Bands, Portraits und Business, dazu eigene Strecken aus Landschaft und Tierwelt. Das ist die Bandbreite meiner Arbeit.",
content=work_gallery()
+ '<div class="btn-row" style="margin-top:2rem"><a class="btn btn--ink" href="/projekte/">Alle Projekte</a></div>',
+ '<div class="btn-row" style="margin-top:2rem">'
'<a class="btn btn--ink" href="/projekte/">Alle Projekte</a>'
'<a class="btn btn--ghost" href="/shootings/">Shootings ansehen</a></div>',
)
video_sec = f"""<section class="section section--ink">
@@ -260,6 +390,8 @@ def home() -> str:
</div>
</section>"""
studio = studio_section()
process = section(
kicker="Ablauf",
title="Vier Schritte, kein Rätselraten",
@@ -278,7 +410,7 @@ def home() -> str:
title="desfoto · Fotografie & Video aus Neumünster",
description=SITE["default_description"],
path="/",
preload_html=preload("hero-studio", "100vw"),
preload_html=preload("music-vocal-blue", "100vw"),
json_ld=[
{
"@context": "https://schema.org",
@@ -302,42 +434,97 @@ def home() -> str:
"knowsAbout": [s["title"] for s in SERVICES],
}
],
body=hero + statement + services + works + video_sec + process + facts + cta_band(),
body=hero
+ statement
+ pillars
+ marquee(
[
"Musik & Bühne",
"Businessfotografie",
"Portrait & Model",
"Familien & Paare",
"Minishootings",
"Mobiles Studio",
"Videoprojekte",
"Social Media",
]
)
+ works
+ video_sec
+ studio
+ process
+ facts
+ cta_band(),
)
def fotografie() -> str:
path = "/fotografie/"
def shootings() -> str:
path = "/shootings/"
overview = "".join(
f'<article class="card reveal"><div>'
+ picture(item["image"], "(min-width: 62rem) 30vw, 92vw", css_class="frame frame--4x5", fit="50% 30%")
'<article class="card reveal"><div>'
+ picture(
item["image"],
"(min-width: 62rem) 30vw, 92vw",
css_class="frame frame--4x5",
fit="50% 30%",
alt=item["title"],
)
+ f'</div><h3>{e(item["title"])}</h3><p class="card__text">{e(item["text"])}</p></article>'
for item in PHOTO_OVERVIEW
)
body = (
page_head(
kicker="Fotografie",
title="Menschen, Marken und Momente",
lead="Vier Schwerpunkte, ein Handwerk. Entscheide dich für den Bereich, der passt — oder schreib mir, wenn du zwischen zwei Bereichen stehst.",
kicker="Shootings",
title="Shootings — von Business bis Familie",
lead="Alles, was ich fotografisch mache, läuft unter dem Begriff Shooting: vier Schwerpunkte, ein Ablauf, ein Ansprechpartner.",
image="editorial-earring",
breadcrumb=("Fotografie", path),
breadcrumb=("Shootings", path),
fit="45% 30%",
actions='<a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a>'
actions='<a class="btn btn--solid" href="/kontakt/">Shooting anfragen</a>'
'<a class="btn btn--outline-light" href="/projekte/">Arbeiten ansehen</a>',
)
+ section(
kicker="Überblick",
title="Womit ich arbeite",
lead="Studiolicht in Neumünster, available light draußen, Reportage dort, wo es lebendig ist.",
content=f'<div class="grid grid--3">{overview}</div>',
title="Ein Überbegriff, vier Wege",
lead="Du musst dich nicht für eine Schublade entscheiden — erzähl einfach, worum es geht.",
content=f'<div class="grid grid--3" data-stagger="0.09">{overview}</div>',
)
+ section(
variant="paper2",
kicker="Schwerpunkte",
title="Die vier Bereiche im Detail",
content=service_cards(["businessfotografie", "portrait-und-model", "familien-und-paare", "minishootings"]),
title="Die vier Shootings im Detail",
content=service_cards(
["businessfotografie", "portrait-und-model", "familien-und-paare", "minishootings"]
),
tight=True,
)
+ studio_section(variant="")
+ section(
kicker="Ablauf",
title="So läuft ein Shooting",
lead="Bei jedem Shooting derselbe Weg — vom ersten Satz bis zur fertigen Datei.",
content='<div class="steps">'
+ "".join(
f'<div class="step reveal"><h3>{e(step["title"])}</h3><p>{e(step["text"])}</p></div>'
for step in SHOOTING_STEPS
)
+ "</div>",
)
+ section(
variant="paper2",
kicker="Beispiele",
title="Aus Shootings und Aufträgen",
content=media_grid(
[
("business-portrait-dark", "Portrait eines Mannes vor dunklem Hintergrund", "4x5", "50% 28%"),
("editorial-colour", "Fashion-Portrait in kräftigen Farben", "4x5", "50% 30%"),
("portrait-couple", "Paarportrait im Grünen", "4x5", "50% 30%"),
("business-portrait-stool", "Sitzendes Portrait im weichen Studiolicht", "3x2", "50% 35%"),
("group-feier", "Feiergesellschaft bei einer Veranstaltung", "3x2", "50% 36%"),
("portrait-natural", "Natürliches Portrait im Freien", "3x2", "50% 32%"),
]
),
)
+ section(
kicker="Technik & Ausgabe",
title="Was am Ende bei dir landet",
@@ -353,21 +540,32 @@ def fotografie() -> str:
),
right=ticks(
[
"Studiolicht und mobile Blitzanlage im Einsatz",
"Available-light-Fotografie on location",
"Tageslicht, Studiolicht und mobile Blitzanlage",
"Mobiles Studio direkt an deinem Ort",
"Reportagestil bei Veranstaltungen",
"Lieferung per Downloadlink",
]
),
),
)
+ cta_band()
+ cta_band(
title="Welches Shooting passt zu dir?",
text="Schreib kurz, worum es geht und wann es stattfinden soll. Du bekommst Ablauf, Umfang und Preis.",
secondary=("Minishootings ansehen", "/minishootings/"),
)
)
return page(
title="Fotografie · desfoto",
description="Fotografie aus Neumünster: Business, Portrait & Model, Familien & Paare, Minishootings. Studiolicht und available light, ein Ansprechpartner.",
title="Shootings in Neumünster · desfoto",
description="Shootings aus Neumünster: Businessfotografie, Portrait & Model, Familien & Paare und Minishootings — mit mobilem Studio inklusive autarker Blitzanlage.",
path=path,
json_ld=[service_ld("Fotografie", path, "Fotografie für Unternehmen, Models, Familien und Paare."), breadcrumb_ld("Fotografie", path)],
json_ld=[
service_ld(
"Shootings",
path,
"Fotografische Shootings für Business, Portrait & Model, Familien und Paare.",
),
breadcrumb_ld("Shootings", path),
],
body=body,
)
@@ -387,7 +585,7 @@ def businessfotografie() -> str:
+ section(
kicker="Typische Aufträge",
title="Wofür Unternehmen mich buchen",
content='<div class="grid grid--3">'
content='<div class="grid grid--3" data-stagger="0.08">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
@@ -419,8 +617,8 @@ def businessfotografie() -> str:
)
+ "</div>",
)
+ studio_section(variant="paper2")
+ section(
variant="paper2",
kicker="Zusammenarbeit",
title="Was ich von euch brauche",
lead="Ein kurzer Call oder eine E-Mail mit drei Angaben reicht für ein verbindliches Angebot.",
@@ -438,14 +636,18 @@ def businessfotografie() -> str:
),
)
+ section(
variant="paper2",
kicker="Beispiele",
title="Aus der Business- und Studiopraxis",
content='<div class="grid grid--2">'
+ picture("business-portrait-dark", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--4x5", fit="50% 28%")
+ picture("business-portrait-stool", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--4x5", fit="50% 35%")
+ picture("gear-cases", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--3x2")
+ picture("studio-setup", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--3x2")
+ "</div>",
content=media_grid(
[
("business-portrait-dark", "Portrait eines Mannes vor dunklem Hintergrund", "4x5", "50% 28%"),
("business-portrait-stool", "Sitzendes Portrait im weichen Studiolicht", "4x5", "50% 35%"),
("gear-cases", "Kamerakoffer und Objektive vor einem Studiorack", "3x2", "50% 50%"),
("studio-setup", "Studioequipment mit Logo von Denny Schulz Fotografie", "3x2", "50% 45%"),
],
columns="grid--2",
),
)
+ cta_band(
title="Bildpaket für euer Unternehmen?",
@@ -632,11 +834,11 @@ def familien_und_paare() -> str:
+ "".join(
picture(slug, "(min-width: 62rem) 30vw, 92vw", css_class="frame frame--4x5", fit=fit)
for slug, fit in [
("people-forest", "50% 24%"),
("people-veil", "50% 32%"),
("people-walk", "50% 32%"),
("portrait-natural", "50% 30%"),
("people-kiss", "50% 32%"),
("portrait-couple", "50% 30%"),
("group-feier", "50% 36%"),
("group-outdoor", "50% 36%"),
("group-posiert", "50% 38%"),
]
)
@@ -739,6 +941,7 @@ def minishootings() -> str:
("portrait-natural", "50% 28%"),
("business-portrait-studio", "50% 28%"),
("editorial-black", "50% 28%"),
("free-modern-portrait", "50% 30%"),
]
)
+ "</div>",
@@ -768,9 +971,9 @@ def video() -> str:
kicker="Videoprojekte",
title="Vom Treatment bis zur finalen Datei",
lead="Musikvideo, Imagefilm, Recruiting oder Eventnachbericht — ich übernehme Konzept, Dreh, Schnitt, Ton und Ausgabe in den Formaten, die dein Kanal braucht.",
image="studio-lens",
image="music-guitar-silhouette",
breadcrumb=("Video", path),
fit="50% 45%",
fit="50% 42%",
actions='<a class="btn btn--solid" href="/kontakt/">Videoprojekt anfragen</a>',
)
+ f"""<section class="section section--ink">
@@ -788,7 +991,7 @@ def video() -> str:
+ section(
kicker="Formate",
title="Welche Videos ich produziere",
content='<div class="grid grid--3">'
content='<div class="grid grid--3" data-stagger="0.08">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
@@ -822,6 +1025,21 @@ def video() -> str:
)
+ section(
variant="paper2",
kicker="Musik & Bühne",
title="Konzerte, Bands und Musiker",
lead="Als Musiker kenne ich beide Seiten der Bühne. Live fotografiert wird mittendrin statt am Rand — oft im selben Termin wie ein Videoprojekt.",
content=media_grid(
[
("music-vocal-blue", "Sänger mit erhobenem Arm im blauen Bühnenlicht", "4x5", "50% 30%"),
("music-guitar-silhouette", "Gitarrist als Silhouette im Bühnennebel", "4x5", "50% 42%"),
("music-strings", "Nahaufnahme einer Hand an den Saiten einer E-Gitarre", "1x1", "50% 50%"),
("music-drums", "Schlagzeuger am Drumset in Schwarzweiß", "1x1", "50% 45%"),
("music-mic", "Bärtiger Sänger am Mikrofon mit erhobener Hand", "3x2", "50% 35%"),
("band-sagenbringer", "Musiker in Fellkleidung im Wald, von unten fotografiert", "3x2", "50% 30%"),
]
),
)
+ section(
kicker="Produktionsweg",
title="So entsteht ein Video",
content='<div class="steps">'
@@ -837,6 +1055,7 @@ def video() -> str:
+ "</div>",
)
+ section(
variant="paper2",
kicker="Zusammenarbeit",
title="Was ich von dir brauche",
content=two_col(
@@ -853,23 +1072,17 @@ def video() -> str:
),
)
+ section(
variant="paper2",
kicker="Einblicke",
title="Hinter der Kamera",
content='<div class="gallery">'
+ "".join(
f'<figure class="{w}">'
+ picture(slug, "(min-width: 62rem) 45vw, 92vw", css_class=f"frame frame--{ratio}", fit=fit)
+ f"<figcaption>{e(cap)}</figcaption></figure>"
for slug, cap, w, ratio, fit in [
content=caption_gallery(
[
("studio-lens", "Brennweite und Licht im Studio", "g-w3", "3x2", "50% 50%"),
("event-motion", "Bewegung im Bild", "g-w3", "3x2", "50% 45%"),
("movement-light", "Lange Belichtung", "g-w2", "1x1", "50% 50%"),
("event-speaker", "Statement vor der Kamera", "g-w2", "1x1", "50% 30%"),
("event-venue", "Location vor dem Dreh", "g-w2", "1x1", "50% 45%"),
]
)
+ "</div>",
),
)
+ cta_band(
title="Videoidee im Kopf?",
@@ -1001,66 +1214,68 @@ def social_media() -> str:
def projekte() -> str:
path = "/projekte/"
gallery = (
'<div class="gallery">'
+ "".join(
f'<figure class="{w}">'
+ picture(slug, "(min-width: 62rem) 45vw, 92vw", css_class=f"frame frame--{ratio}", fit=fit)
+ f"<figcaption>{e(cap)}</figcaption></figure>"
for slug, cap, w, ratio, fit in [
("travel-japan", "Japan — eigene Reiseserie", "g-w3", "4x5", "45% 42%"),
("travel-norway-lake", "Norwegen — Seen", "g-w3", "4x5", "50% 48%"),
("travel-norway-coast", "Norwegen — Küste", "g-w2", "1x1", "50% 48%"),
("travel-norway-valley", "Morgennebel im Tal", "g-w2", "1x1", "50% 50%"),
("travel-norway-pano", "Panorama", "g-w2", "1x1", "50% 50%"),
("travel-blossom", "Kirschblüte", "g-w3", "3x2", "50% 38%"),
("movement-light", "Lichtmalerei", "g-w3", "3x2", "50% 50%"),
]
)
+ "</div>"
)
body = (
page_head(
kicker="Projekte",
title="Eigene Arbeiten und pro bono",
lead="Neben Auftragsarbeiten entstehen eigene Projekte. Manches davon startet als Idee im Kopf, manches als Anfrage von Menschen, die etwas Unterstützung brauchen.",
image="movement-light",
title="Eigene Arbeiten, Musik und freie Strecken",
lead="Neben Auftragsarbeiten entstehen eigene Projekte: Bands und Konzerte, Landschaft und Tierwelt, Menschen und Momente ohne Termin.",
image="free-mountain-view",
breadcrumb=("Projekte", path),
fit="50% 50%",
fit="50% 45%",
actions='<a class="btn btn--solid" href="/kontakt/">Projekt anfragen</a>'
'<a class="btn btn--outline-light" href="/video/">Musikvideo ansehen</a>',
)
+ section(
kicker="Eigene Projekte",
title="Arbeiten, die ich selbst angestoßen habe",
lead="Reisefotografie, Lichtmalerei und Musikvideo. Diese Projekte entstehen ohne Auftraggeber und zeigen, wie ich mir Bilder vorstelle.",
content='<div class="grid grid--2">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
(
"Skogamor — Musikvideo",
"Eigenes Musikvideo für Thjódrörir mit Konzept, Dreh, Schnitt und Farbkorrektur. Zu sehen auf der Videoseite.",
),
(
"Norwegen",
"Reihe über Küsten, Seen und Berglandschaften. Landschaft als Gegenpol zur Portrait- und Auftragsarbeit.",
),
(
"Japan",
"Ruhige Bilder von Parks, Gärten und Architektur, entstanden auf Reisen statt auf Termin.",
),
(
"Lichtmalerei",
"Experimente mit langer Belichtung und bewegtem Licht — technische Spielwiese mit Ausstellungsqualität.",
),
kicker="Musik & Bühne",
title="Bands, Konzerte und Musikvideos",
lead="Musik ist mein zweites Standbein. Für Extinct und Sagenbringer entstehen Bilder und Videoarbeiten, dazu das eigene Musikvideo für Thjódrörir.",
content=media_grid(
[
("music-vocal-blue", "Sänger mit erhobenem Arm im blauen Bühnenlicht", "4x5", "50% 30%"),
("band-sagenbringer", "Musiker in Fellkleidung im Wald, von unten fotografiert", "4x5", "50% 30%"),
("band-extinct", "Die Band Extinct steht hintereinander vor schwarzem Hintergrund", "3x2", "50% 45%"),
("music-mic", "Bärtiger Sänger am Mikrofon mit erhobener Hand", "1x1", "50% 35%"),
("music-drums", "Schlagzeuger am Drumset in Schwarzweiß", "1x1", "50% 45%"),
("music-strings", "Nahaufnahme einer Hand an den Saiten einer E-Gitarre", "3x2", "50% 50%"),
]
)
+ "</div>"
+ '<div style="margin-top:2rem"><a class="link-arrow" href="/video/">Das Musikvideo ansehen '
+ '<div class="btn-row" style="margin-top:2rem">'
'<a class="link-arrow" href="/video/">Zum Musikvideo '
+ ICONS["arrow"]
+ "</a></div>",
)
+ section(variant="paper2", kicker="Bildstrecke", title="Aus eigenen Serien und Aufträgen", content=gallery)
+ section(
variant="paper2",
kicker="Freie Arbeiten",
title="Strecken ohne Auftrag",
lead="Landschaft, Tierwelt und Menschen, entstanden nebenbei und ohne Auftraggeber. Drei Themen, ein Blick — das, was ich ohnehin fotografiere.",
content=media_grid(
[
("free-mountain-view", "Eine Person sitzt über einer wolkenverhangenen Berglandschaft", "4x5", "50% 45%"),
("free-fjord", "Steile Felsnadeln vor dem Meer", "4x5", "50% 50%"),
("free-woman-coast", "Eine Frau mit Hut steht an einer felsigen Küste", "4x5", "50% 40%"),
("free-fog-forest", "Nebel zieht durch einen Nadelwald am Berghang", "3x2", "50% 45%"),
("free-lake-person", "Eine Person am Bergsee spiegelt sich im Wasser", "3x2", "50% 50%"),
("free-golden-grass", "Gräser im goldenen Licht des Sonnenuntergangs", "3x2", "50% 50%"),
("free-fox", "Nahaufnahme eines Rotfuchses", "1x1", "50% 45%"),
("free-cat", "Nahaufnahme einer getigerten Katze im Seitenlicht", "1x1", "50% 42%"),
("free-arch-garden", "Blick durch einen steinernen Torbogen in einen Garten", "1x1", "50% 45%"),
]
),
)
+ section(
kicker="Menschen",
title="Portraits ohne Anlass",
content=media_grid(
[
("free-portrait-light", "Frauenportrait im weichen Seitenlicht vor dunklem Hintergrund", "4x5", "50% 28%"),
("free-modern-portrait", "Portrait einer Frau mit Sonnenbrille", "4x5", "50% 30%"),
("movement-light", "Lichtspur aus einer langen Belichtung", "4x5", "50% 50%"),
]
),
)
+ section(
variant="paper2",
kicker="Pro bono",
title="Arbeiten, die ich unterstütze",
lead="Manche Projekte sind wichtig, haben aber kein Budget. Wenn es passt, mache ich sie trotzdem.",
@@ -1079,7 +1294,6 @@ def projekte() -> str:
),
)
+ section(
variant="paper2",
kicker="Auftragsarbeiten",
title="Und sonst?",
lead="Business, Portrait, Familie, Events und Video — alles, was in den Leistungen steht. Der beste Weg ist ein kurzer Blick in die Bereiche.",
@@ -1087,9 +1301,9 @@ def projekte() -> str:
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p><div class="card__foot"><a class="link-arrow" href="{h}">Ansehen {ICONS["arrow"]}</a></div></article>'
for t, d, h in [
("Shootings", "Business, Portrait, Familie und Minishootings.", "/shootings/"),
("Businessfotografie", "Portraits, Teams und Produkte für Unternehmen.", "/businessfotografie/"),
("Portrait & Model", "Portraits, Sedcards und Editorial.", "/portrait-und-model/"),
("Video", "Musikvideo, Imagefilm und Eventnachbericht.", "/video/"),
("Videoprojekte", "Musikvideo, Imagefilm und Eventnachbericht.", "/video/"),
]
)
+ "</div>",
@@ -1098,7 +1312,7 @@ def projekte() -> str:
)
return page(
title="Projekte · desfoto",
description="Eigene Projekte und pro-bono-Arbeiten von desfoto: Reisefotografie, Lichtmalerei, Musikvideo und Unterstützung für Initiativen ohne Budget.",
description="Eigene Projekte von desfoto: Bands und Konzerte, Landschaft und Tierwelt, Portraits ohne Anlass und pro-bono-Arbeiten für Initiativen ohne Budget.",
path=path,
json_ld=[breadcrumb_ld("Projekte", path)],
body=body,
@@ -1111,26 +1325,32 @@ def ueber() -> str:
page_head(
kicker="Über",
title="Fotograf und Videograf aus Neumünster",
lead="Ich bin Denny Schulz. Seit vielen Jahren fotografiere ich Menschen, Marken und Anlässe — und seit einiger Zeit genauso gern in Bewegung.",
image="business-portrait-dark",
lead="Ich bin Denny Schulz. Ich fotografiere Menschen, Marken und Musik — und produziere seit einigen Jahren genauso gern Bewegtbild.",
image="studio-setup",
breadcrumb=("Über", path),
fit="50% 25%",
actions='<a class="btn btn--solid" href="/kontakt/">Kennst du noch nicht? Schreib mir</a>',
fit="50% 45%",
actions='<a class="btn btn--solid" href="/kontakt/">Schreib mir</a>',
)
+ section(
content="""<div class="statement">
<div><p class="eyebrow">Arbeitsweise</p></div>
<div class="statement__body">
<p>Ich arbeite schnell, direkt und unaufgeregt. Du sagst mir, was du brauchst, ich sage dir,
was dafür nötig ist — und dann machen wir es. <em>Ohne Umwege und ohne Fachchinesisch.</em></p>
</div>
</div>""",
kicker="Wer ich bin",
title="Vom Reisen zur Kamera",
content='<div class="statement">'
'<div><p class="eyebrow">Kurz</p></div>'
'<div class="statement__body">'
'<p>Ich bin gelernter Kaufmann, Ausbilder und Musiker. Zur Fotografie gekommen bin ich '
'über Reisen, Freunde und die ersten Hochzeiten — erst als Begleitung, dann immer öfter '
'mit der Kamera in der Hand.</p>'
'<p>Was mich fotografisch am meisten zieht, sind die nordischen Länder mit ihrem Licht '
'und ihren Landschaften. Und Menschen, die wissen, dass sie nicht perfekt sein müssen, '
'damit ein Bild stimmt.</p>'
'<p class="quote">„Kameras, Ersatztechnik und mobile Blitze bringe ich passend zum Termin mit."</p>'
"</div></div>",
)
+ section(
variant="paper2",
kicker="Was ich mache",
title="Drei Dinge, richtig",
content='<div class="grid grid--3">'
content='<div class="grid grid--3" data-stagger="0.08">'
+ "".join(
f'<article class="card reveal"><h3>{e(t)}</h3><p class="card__text">{e(d)}</p></article>'
for t, d in [
@@ -1139,18 +1359,20 @@ def ueber() -> str:
"Portrait, Business, Model, Familie und Paar. Die Arbeit an Gesichtern, Licht und Ausdruck ist mein Kern.",
),
(
"Bewegtbild produzieren",
"Von der Idee über den Dreh bis zu Schnitt, Farbkorrektur und Ton. Auch als Ergänzung zu einem Fototermin.",
"Musik & Bühne begleiten",
"Konzerte, Bands und Musikvideos — als Musiker kenne ich beide Seiten der Bühne.",
),
(
"Inhalte für Kanäle liefern",
"Material, das ohne Umweg in Website, Feed oder Stellenanzeige passt — im richtigen Format und Look.",
"Bewegtbild produzieren",
"Von der Idee über den Dreh bis zu Schnitt, Farbkorrektur und Ton. Auch als Ergänzung zu einem Fototermin.",
),
]
)
+ "</div>",
)
+ studio_section(variant="")
+ section(
variant="paper2",
kicker="Werte",
title="Wie ich arbeite",
content=two_col(
@@ -1173,13 +1395,33 @@ def ueber() -> str:
),
)
+ section(
variant="paper2",
kicker="Zuhause & unterwegs",
title="Neumünster und darüber hinaus",
lead="Ich fotografiere im Studio in Neumünster und unterwegs in Schleswig-Holstein, Hamburg und Kiel. Für passende Projekte fahre ich auch weiter.",
lead="Ich fotografiere im Studio in Neumünster, mit dem mobilen Studio bei euch im Betrieb und unterwegs in Schleswig-Holstein, Hamburg und Kiel.",
content=media_grid(
[
("business-portrait-studio", "Mann im weißen Hemd vor dunklem Hintergrund, Softbox im Bild", "4x5", "50% 28%"),
("studio-setup", "Studioequipment mit Logo von Denny Schulz Fotografie", "4x5", "50% 45%"),
("free-fox", "Nahaufnahme eines Rotfuchses", "1x1", "50% 45%"),
],
columns="grid--2",
),
)
+ section(
variant="paper2",
kicker="Netzwerk",
title="Wo ich sonst zu finden bin",
lead="Zwei eigene Projekte, die zu desfoto gehören.",
content='<div class="grid grid--2">'
+ picture("business-portrait-studio", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--4x5", fit="50% 28%")
+ picture("studio-setup", "(min-width: 62rem) 45vw, 92vw", css_class="frame frame--4x5", fit="50% 45%")
+ "".join(
'<article class="card reveal">'
f'<h3>{e(item["title"])}</h3>'
f'<p class="card__text">{e(item["text"])}</p>'
f'<div class="card__foot"><a class="link-arrow" href="{e(item["href"])}" '
f'target="_blank" rel="noopener">Ansehen {ICONS["arrow_up_right"]}</a></div>'
"</article>"
for item in NETWORK
)
+ "</div>",
)
+ section(
@@ -1198,7 +1440,7 @@ def ueber() -> str:
)
return page(
title="Über desfoto · Denny Schulz, Neumünster",
description="desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster. Arbeitsweise, Werte und Kontaktmöglichkeiten.",
description="desfoto ist die Foto- und Videoproduktion von Denny Schulz aus Neumünster. Arbeitsweise, mobiles Studio, Netzwerk und Kontaktmöglichkeiten.",
path=path,
json_ld=[
breadcrumb_ld("Über", path),
@@ -1209,6 +1451,7 @@ def ueber() -> str:
"jobTitle": "Fotograf und Videograf",
"url": SITE["url"] + path,
"worksFor": {"@type": "Organization", "name": LEGAL["business"], "url": SITE["url"]},
"sameAs": [item["href"] for item in NETWORK],
"address": {
"@type": "PostalAddress",
"streetAddress": LEGAL["street"],
@@ -1544,7 +1787,7 @@ def not_found() -> str:
ROUTES = [
("/", "index.html", home, None, "1.0"),
("/fotografie/", "fotografie/index.html", fotografie, None, "0.9"),
("/shootings/", "shootings/index.html", shootings, None, "0.9"),
("/businessfotografie/", "businessfotografie/index.html", businessfotografie, None, "0.9"),
("/portrait-und-model/", "portrait-und-model/index.html", portrait_und_model, None, "0.9"),
("/familien-und-paare/", "familien-und-paare/index.html", familien_und_paare, None, "0.9"),

View File

@@ -98,9 +98,8 @@ hr{border:0;border-top:1px solid var(--line);margin:0}
}
.masthead__inner{display:flex;align-items:center;gap:1rem;min-height:4.4rem;padding-block:.7rem}
.brand{display:inline-flex;align-items:center;gap:.62rem;font-family:var(--font-display);font-size:1.42rem;font-weight:600;letter-spacing:-.015em}
.brand__mark{width:1.72rem;height:1.72rem;flex:none}
.brand__mark .ring{stroke:var(--ink);fill:none;stroke-width:2.4}
.brand__mark .blade{stroke:var(--accent);stroke-width:2.6;stroke-linecap:round}
.brand__mark{width:2.05rem;height:2.05rem;flex:none;display:block;border-radius:6px;overflow:hidden;box-shadow:0 0 0 1px var(--line);background:var(--paper)}
.brand__mark img{width:100%;height:100%;object-fit:cover;display:block}
.brand small{display:block;font-family:var(--font-sans);font-size:.62rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-50);line-height:1}
.brand__text{display:flex;flex-direction:column;gap:.22rem}
@@ -303,7 +302,7 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
.footer a:hover{color:#fff}
.footer__grid{display:grid;gap:2rem;grid-template-columns:minmax(0,1.5fr) repeat(auto-fit,minmax(9rem,1fr))}
.footer__brand .brand{color:var(--paper)}
.footer__brand .brand__mark .ring{stroke:var(--paper)}
.footer__brand .brand__mark{box-shadow:0 0 0 1px rgba(246,242,234,.34)}
.footer__claim{margin-top:1rem;max-width:34ch;color:rgba(246,242,234,.66);font-size:.9rem}
.footer h2{font-family:var(--font-sans);font-size:.74rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:rgba(246,242,234,.5);margin-bottom:.9rem}
.footer__links{display:grid;gap:.5rem}
@@ -316,7 +315,7 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
/* ---------- page hero (sub pages) ---------- */
.page-head{position:relative;isolation:isolate;overflow:hidden;background:var(--ink);color:var(--paper)}
.page-head__bg{position:absolute;inset:0;z-index:-2}
.page-head__bg img{width:100%;height:100%;object-fit:cover}
.page-head__bg img{width:100%;height:100%;object-fit:cover;animation:hero-zoom 44s var(--ease) both}
.page-head::before{
content:"";position:absolute;inset:0;z-index:-1;
background:linear-gradient(115deg,rgba(12,11,9,.9) 0%,rgba(12,11,9,.65) 55%,rgba(12,11,9,.38) 100%);
@@ -352,21 +351,154 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
/* ---------- motion ---------- */
.reveal{opacity:0;transform:translateY(1.4rem);transition:opacity .8s var(--ease),transform .8s var(--ease)}
.reveal.is-in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
.reveal{opacity:1;transform:none;transition:none}
html:focus-within{scroll-behavior:auto}
.btn:hover{transform:none}
.nav__link::after{transition:none}
/* ---------- motion: entrance, reveal, parallax ---------- */
@keyframes hero-zoom{from{transform:scale(1.05)}to{transform:scale(1.18)}}
@keyframes rise{from{opacity:0;transform:translateY(1.15em)}to{opacity:1;transform:none}}
@keyframes cue{0%{transform:scaleY(.12);transform-origin:top}45%{transform:scaleY(1);transform-origin:top}55%{transform:scaleY(1);transform-origin:bottom}100%{transform:scaleY(.12);transform-origin:bottom}}
@keyframes marquee{to{transform:translateX(-50%)}}
@keyframes fade-in{from{opacity:0}to{opacity:1}}
.progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;pointer-events:none}
.progress__bar{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#E6A184,var(--teal));transform:scaleX(var(--scroll,0));transform-origin:left;transition:transform .12s linear}
.hero__media::after{
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(105% 80% at 82% 8%,rgba(192,67,28,.32),transparent 56%);
}
.hero__media img{animation:hero-zoom 36s var(--ease) both}
.hero__title .w{display:inline-block;overflow:hidden;vertical-align:bottom;padding-bottom:.06em}
.hero__title .w>span{display:inline-block;animation:rise 1.05s var(--ease) both;animation-delay:calc(var(--i,0) * .07s + .16s)}
.hero__sub,.hero__actions,.hero__facts{animation:fade-in 1.2s var(--ease) both;animation-delay:.62s}
.hero__actions{animation-delay:.74s}
.hero__facts{animation-delay:.9s}
.scroll-cue{
position:absolute;right:max(4vw,1rem);bottom:2.4rem;z-index:1;
display:grid;justify-items:center;gap:.6rem;
font-size:.62rem;letter-spacing:.26em;text-transform:uppercase;color:rgba(246,242,234,.62);
}
.scroll-cue i{display:block;width:1px;height:3.2rem;background:linear-gradient(180deg,rgba(246,242,234,.85),transparent);animation:cue 2.6s var(--ease) infinite}
.reveal{opacity:0;transform:translate3d(0,1.7rem,0);transition:opacity .95s var(--ease),transform .95s var(--ease),clip-path 1.1s var(--ease);transition-delay:var(--d,0s)}
.reveal--scale{transform:scale(.962)}
.reveal--left{transform:translate3d(-2.4rem,0,0)}
.reveal--right{transform:translate3d(2.4rem,0,0)}
/* The mask lives on the children: Chromium folds an element's own clip-path into
the IntersectionObserver geometry, so a clipped target never reports as visible. */
.reveal--mask{opacity:1;transition:transform 1.15s var(--ease);transition-delay:var(--d,0s)}
.reveal--mask>*{clip-path:inset(0 0 100% 0);transition:clip-path 1.15s var(--ease);transition-delay:var(--d,0s)}
.reveal--mask.is-in>*{clip-path:inset(0 0 0 0)}
.reveal.is-in{opacity:1;transform:none}
.parallax{transform:translate3d(0,var(--py,0),0);will-change:transform}
/* ---------- pillars: the two halves of the work ---------- */
.pillars{display:grid;gap:clamp(1.1rem,2.2vw,2rem);grid-template-columns:repeat(auto-fit,minmax(min(100%,26rem),1fr))}
.pillar{
position:relative;isolation:isolate;display:flex;flex-direction:column;
min-height:clamp(25rem,54vh,33rem);border-radius:var(--radius-lg);overflow:hidden;
color:var(--paper);background:#14110D;cursor:pointer;
transition:transform .6s var(--ease),box-shadow .6s var(--ease);
}
.pillar:hover{transform:translateY(-4px);box-shadow:var(--shadow)}
.pillar__media{position:absolute;inset:0;z-index:-2}
.pillar__media img{width:100%;height:100%;object-fit:cover;transition:transform 1.3s var(--ease)}
.pillar:hover .pillar__media img{transform:scale(1.055)}
.pillar::before{
content:"";position:absolute;inset:0;z-index:-1;
background:linear-gradient(180deg,rgba(12,11,9,.1) 12%,rgba(12,11,9,.62) 55%,rgba(12,11,9,.94) 100%);
}
.pillar__body{margin-top:auto;padding:clamp(1.3rem,2.4vw,2.1rem);display:grid;gap:.85rem}
.pillar__label{
justify-self:start;font-size:.68rem;font-weight:600;letter-spacing:.2em;text-transform:uppercase;
color:#F0B79B;border:1px solid rgba(246,242,234,.34);border-radius:var(--radius-pill);padding:.3rem .8rem;
}
.pillar__title{font-size:clamp(1.7rem,2.6vw,2.3rem)}
.pillar__text{color:rgba(246,242,234,.84);font-size:.95rem;max-width:46ch}
.pillar__points{display:flex;flex-wrap:wrap;gap:.45rem .95rem;font-size:.82rem;color:rgba(246,242,234,.72)}
.pillar__points a{position:relative;z-index:2;border-bottom:1px solid rgba(246,242,234,.3);padding-bottom:.1rem}
.pillar__points a:hover{color:#fff;border-color:#fff}
.pillar__go{margin-top:.35rem;display:inline-flex;align-items:center;gap:.5rem;font-weight:600;font-size:.92rem;color:inherit}
/* Stretched link: the whole card is clickable without nesting anchors. */
.pillar__go::after{content:"";position:absolute;inset:0;z-index:1;border-radius:inherit}
.pillar__go svg{width:.9em;height:.9em;fill:none;stroke:currentColor;stroke-width:2;transition:transform .3s var(--ease)}
.pillar:hover .pillar__go svg{transform:translateX(.3rem)}
.spot::after{
content:"";position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .45s var(--ease);z-index:1;
background:radial-gradient(22rem 22rem at var(--mx,50%) var(--my,50%),rgba(246,242,234,.16),transparent 62%);
}
.spot:hover::after{opacity:1}
/* ---------- media cards ---------- */
.scard{
position:relative;display:flex;flex-direction:column;overflow:hidden;
border:1px solid var(--line);border-radius:var(--radius-lg);background:rgba(255,255,255,.42);
transition:transform .5s var(--ease),box-shadow .5s var(--ease),border-color .5s var(--ease);
}
.scard:hover{transform:translateY(-4px);box-shadow:var(--shadow);border-color:var(--line-soft)}
.scard__media{position:relative;aspect-ratio:4/3;overflow:hidden;background:var(--paper-3)}
.scard__media img{width:100%;height:100%;object-fit:cover;transition:transform 1.1s var(--ease)}
.scard:hover .scard__media img{transform:scale(1.06)}
.scard__body{padding:1.1rem 1.2rem 1.35rem;display:grid;gap:.45rem;align-content:start}
.scard__body h3{font-size:1.14rem}
.scard__body p{font-size:.92rem;color:var(--ink-70)}
.scard__body .link-arrow{margin-top:.35rem}
/* ---------- split + statement upgrades ---------- */
.split{display:grid;gap:clamp(1.4rem,3vw,3.2rem);grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:center}
.split--wide{grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr)}
.split--reverse .split__media{order:2}
.words{font-family:var(--font-display);font-size:clamp(1.6rem,3.6vw,2.9rem);line-height:1.14;letter-spacing:-.02em;font-weight:400;text-wrap:pretty}
.words em{font-style:italic;color:var(--accent)}
.words strong{font-weight:600}
/* ---------- marquee ---------- */
.marquee{overflow:hidden;border-block:1px solid var(--line);padding-block:1.1rem;background:var(--paper-2)}
.marquee__track{display:flex;align-items:center;gap:clamp(1.6rem,3vw,3.2rem);width:max-content;animation:marquee 42s linear infinite}
.marquee__track span{
display:inline-flex;align-items:center;gap:clamp(1.6rem,3vw,3.2rem);
font-family:var(--font-display);font-size:clamp(1.05rem,1.9vw,1.6rem);color:var(--ink-70);white-space:nowrap;
}
.marquee__track span::after{content:"";width:.4rem;height:.4rem;border-radius:50%;background:var(--accent);flex:none}
.marquee:hover .marquee__track{animation-play-state:paused}
/* ---------- mobile studio ---------- */
.studio{display:grid;gap:clamp(1.5rem,3vw,3.2rem);grid-template-columns:minmax(0,1.02fr) minmax(0,1fr);align-items:center}
.studio__media{position:relative}
.studio__media img{border-radius:var(--radius-lg)}
.studio__badge{
position:absolute;left:-.6rem;bottom:1.4rem;background:var(--paper);border:1px solid var(--line);
border-radius:var(--radius);padding:.7rem 1rem;box-shadow:var(--shadow);
}
.studio__badge strong{display:block;font-family:var(--font-display);font-size:1.05rem}
.studio__badge span{font-size:.78rem;color:var(--ink-50)}
.studio__features{display:grid;gap:1.1rem 1.4rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));margin-top:1.6rem}
.studio__features h3{font-size:1rem;margin-bottom:.3rem}
.studio__features p{font-size:.88rem;color:var(--ink-70)}
.section--ink .studio__features p{color:rgba(246,242,234,.72)}
/* ---------- band / project strip ---------- */
.band-strip{display:grid;gap:clamp(.8rem,1.5vw,1.2rem);grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))}
.band-strip figure{margin:0}
.band-strip figcaption{margin-top:.55rem;font-size:.8rem;color:var(--ink-50)}
/* ---------- footer network ---------- */
.footer__links small{display:block;color:rgba(246,242,234,.52);font-size:.78rem;line-height:1.5;max-width:30ch;margin-top:.1rem}
.footer__ext{display:inline-flex;align-items:center;gap:.4rem}
.footer__ext svg{width:.8em;height:.8em;fill:none;stroke:currentColor;stroke-width:2}
/* ---------- responsive ---------- */
@media (max-width:62rem){
.statement{grid-template-columns:1fr}
.contact-grid{grid-template-columns:1fr}
.split,.split--wide{grid-template-columns:1fr}
.split--reverse .split__media{order:0}
.studio{grid-template-columns:1fr}
.gallery{grid-template-columns:repeat(4,1fr)}
.gallery .g-w3{grid-column:span 2}
.gallery .g-w4{grid-column:span 4}
.gallery .g-w2{grid-column:span 2}
.scroll-cue{display:none}
}
@media (max-width:52rem){
.nav{
@@ -393,4 +525,18 @@ img.frame{width:100%;height:auto;object-fit:cover;transition:transform .8s var(-
.hero__facts{grid-template-columns:1fr 1fr}
.page-head__inner{max-width:none}
}
@media (prefers-reduced-motion:reduce){
.reveal,.reveal--mask{opacity:1;transform:none;clip-path:none;transition:none}
.reveal--mask>*{clip-path:none;transition:none}
html:focus-within{scroll-behavior:auto}
.btn:hover,.pillar:hover,.scard:hover{transform:none}
.nav__link::after{transition:none}
.hero__media img,.page-head__bg img{animation:none}
.hero__title .w>span,.hero__sub,.hero__actions,.hero__facts{animation:none;opacity:1;transform:none}
.marquee__track{animation:none;flex-wrap:wrap;width:auto}
.scroll-cue{display:none}
.parallax{transform:none}
.pillar:hover .pillar__media img,.scard:hover .scard__media img{transform:none}
}
"""