/* ==========================================================================
   style.css — the single stylesheet for the whole site.
   --------------------------------------------------------------------------
   Structure of this file:
     1. Design tokens (colors, spacing, radii, type, motion, shadows) — EDIT HERE
     2. Base / reset + utilities (sr-only)
     3. Layout helpers (.container, .section)
     4. Header + navigation + language + mobile menu
     5. Home page (hero, preview strip, teasers, regions)
     6. Gallery page
     7. About page (includes the small-screen portrait collapse)
     8. Components (buttons, cards, badges, separator, tooltip, media)
    9. Footer
    10. Motion, scroll reveals & small delights — reduced-motion aware
    11. Print
   --------------------------------------------------------------------------
   Note: contact/legal (.prose, .detail-list) and the richer home page
   (.preview-strip, .teaser-grid) are styled alongside their sections (5–8).
   --------------------------------------------------------------------------
   Conventions:
     - Component classes are flat lowercase nouns (.card, .badge, .to-top).
     - Element state uses is-* prefixes (.is-open, .is-revealed,
       .is-closing); JS-only markers use js-* (.js, .js-menu); every JS
       hook is a data-* attribute.
     - Type, spacing, z-layers and radii use the section-1 tokens;
       deliberate one-off exceptions stay at the point of use.
     - ONE responsive strategy: mobile-first `min-width` expansions plus a
       single `max-width: 42rem` collapse tier (header/nav in section 4,
       portrait in section 7). A separate `max-width: 68rem` block only
       reserves footer space for the fixed back-to-top button — it changes no
       layout.
   --------------------------------------------------------------------------
   Theme: each palette is defined exactly once. The base `:root` ships the
   light palette, `@media (prefers-color-scheme: dark)` re-points the tokens to
   dark, and the explicit `:root[data-theme="light"|"dark"]` overrides (set by
   the header theme switch) win when present. `auto` = neither attribute.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. DESIGN TOKENS — the one place to change colors, spacing and radii.
   Colors are stored as raw HSL channels ("H S% L%") and wrapped in hsl() at
   use time. This mirrors the shadcn/ui convention and lets one value drive
   many shades via color-mix(). Keep the light and dark sets in sync.
   -------------------------------------------------------------------------- */
:root {
  /* Light palette — the single source of truth for light mode. */
  --light-background: 0 0% 100%;
  --light-foreground: 240 10% 3.9%;
  --light-card: 0 0% 100%;
  --light-card-foreground: 240 10% 3.9%;
  --light-muted: 240 4.8% 95.9%;
  --light-muted-foreground: 240 4% 40%;
  --light-border: 240 5.9% 90%;
  /* Neutral hover surface (shadcn's "accent"): a near-background tint used for
     button/menu hover fills. Renamed to `highlight` so the word "accent" is
     free for the site's one deliberate brand colour below. */
  --light-highlight: 240 4.8% 95.9%;
  --light-highlight-foreground: 240 5.9% 10%;
  /* Interactive-control boundary (borders that ARE the component's visual
     boundary: outline buttons, icon buttons). Chosen to clear the WCAG 2.2
     non-text 3:1 contrast requirement on BOTH the light and dark backgrounds
     (light 3.57:1 on white, dark 5.57:1 on #09090b), unlike the decorative
     --border hairline which is intentionally faint. */
  --light-control-border: 240 5% 55%;

  /* Dark palette — the single source of truth for dark mode. */
  --dark-background: 240 10% 3.9%;
  --dark-foreground: 0 0% 98%;
  --dark-card: 240 10% 3.9%;
  --dark-card-foreground: 0 0% 98%;
  --dark-muted: 240 3.7% 15.9%;
  --dark-muted-foreground: 240 5% 64.9%;
  --dark-border: 240 3.7% 15.9%;
  --dark-highlight: 240 3.7% 15.9%;
  --dark-highlight-foreground: 0 0% 98%;
  --dark-control-border: 240 5% 55%;

  /* ---------------------------------------------------------------------
     THE ACCENT — BLACK, identical in both themes.
     A deliberate neutral: pure black is the calmest possible accent and
     reads as ink rather than a brand colour. `--accent` is #000 in BOTH
     themes and is used for FILLS (primary button, badge fills, selection,
     back-to-top hover) where a black fill is legible either way.

     Visibility handling (documented per-theme variants):
       --accent-foreground  text/icon ON a black fill -> white (21:1).
       --accent-text        accent-coloured TEXT on the page background.
                            Light: pure black (21:1 on white).
                            Dark: near-white (18.3:1 on the dark bg), because
                            black text on a near-black page is invisible.
       --accent-line        hairlines / borders / focus rings, which must be
                            visible against the page background.
                            Light: pure black. Dark: near-white.
     Black-on-dark is 1.06:1, so the near-white dark variants are the
     minimal, documented adjustment that keeps every accented element
     visible and WCAG-compliant in both themes.
     --------------------------------------------------------------------- */
  --accent: 0 0% 0%;               /* #000000 — pure black, both themes */
  --accent-foreground: 0 0% 100%;  /* white on a black fill (21:1) */
  --light-accent-text: 0 0% 0%;    /* #000000 — 21:1 on white */
  --dark-accent-text: 0 0% 96%;    /* #f5f5f5 — 18.3:1 on the dark bg */
  --light-accent-line: 0 0% 0%;    /* black hairline/ring on light */
  --dark-accent-line: 0 0% 90%;    /* #e6e6e6 hairline/ring on dark (15.9:1) */

  /* Lightbox palette — intentionally ALWAYS dark (theme-invariant) so photos
     are judged against a neutral backdrop regardless of the active theme. */
  --lightbox-backdrop: 0 0% 0%;
  --lightbox-image-bg: 0 0% 0%;
  --lightbox-fg: 0 0% 96%;
  --lightbox-fg-muted: 0 0% 88%;
  --lightbox-fg-subtle: 0 0% 70%;
  --lightbox-ctl-bg: 0 0% 0%;
  --lightbox-ctl-border: 0 0% 100%;
  --lightbox-ctl-fg: 0 0% 100%;

  /* Geometry */
  --radius: 0.375rem;      /* small, consistent corner radius */
  --radius-pill: 999px;    /* fully rounded pills (badges, chips, icon buttons) */
  --radius-lg: calc(var(--radius) * 2);  /* larger radii for image frames/cards */
  --max-width: 68rem;      /* page content width */
  --gutter: clamp(1.25rem, 5vw, 3rem);
  --gallery-gap: clamp(0.75rem, 2vw, 1.25rem);

  /* Spacing scale — exact values of the recurring gaps/margins. */
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  /* Z-layers — stacking order in one place. */
  --z-tooltip: 20;
  --z-to-top: 40;
  --z-header: 50;
  --z-lightbox: 100;

  /* Controls (buttons and button-like elements). One height for every variant
     keeps a row of buttons visually aligned; `--control-height` is at least
     44px so it clears the recommended touch-target size on mobile. Widths are
     driven by the horizontal padding + text, so labels never clip. */
  --control-height: 2.75rem;  /* 44px — comfortable, touch-friendly target */
  --control-pad-x: 1.25rem;   /* horizontal padding for a normal-size button */
  --control-gap: 0.5rem;      /* space between an icon and its label */

  /* Type — 100% system fonts, no CDN, GDPR-friendly */
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", sans-serif;

  /* Recurring type steps; deliberate one-off sizes stay at point of use. */
  --text-h1: clamp(2rem, 5vw, 3.25rem);
  --text-h2: clamp(1.4rem, 3vw, 1.9rem);
  --text-h3: clamp(1.1rem, 2vw, 1.3rem);
  --text-xs: 0.72rem;
  --text-sm: 0.85rem;
  --text-caption: 0.78rem;

  /* Motion — short and restrained. Kept here so all timings/easings are
     changed in one place (see the animation section near the bottom). */
  --duration-fast: 160ms;    /* link/button hover, focus */
  --duration-reveal: 520ms;  /* fade/translate-in of content blocks */
  --duration-page: 320ms;    /* page-enter fade */
  /* matches --duration-reveal so image zoom feels of a piece with reveals */
  --duration-zoom: 520ms;
  --duration-shimmer: 1.6s;  /* skeleton shimmer sweep */
  --duration-menu: 280ms;    /* mobile menu open/close */
  --duration-to-top: 320ms;  /* back-to-top entrance/exit */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);   /* gentle deceleration */
  --reveal-shift: 12px;      /* how far blocks travel while fading in */
  --reveal-shift-lg: 16px;   /* larger rise used by the page-enter animation */
  --to-top-shift: 20px;      /* back-to-top entrance/exit slide distance */
  --reveal-step: 60ms;       /* extra delay per staggered sibling */
  --hover-lift: -1px;        /* nav link lift on hover */
  --hover-scale: 1.02;       /* image zoom on hover */

  /* Extra motion for the two opt-in flourishes. `--reveal-shift-gallery`
     (a softer, larger rise) is used only by gallery grids; the underline and
     lightbox tokens drive their own transitions. */
  --reveal-shift-gallery: 18px;   /* gallery-item rise during reveal */
  --reveal-scale-gallery: 0.985;  /* gallery-item start scale during reveal */
  --duration-underline: 220ms;    /* animated underline grow */

  /* Lightbox motion. Open/close use the same coordinated duration + easing so
     the fade/zoom feels symmetric; image-to-image navigation inside an open
     lightbox is deliberately faster and subtler than the open/close. */
  --duration-lightbox: 260ms;       /* open AND close (fade + slight zoom) */
  --duration-lightbox-nav: 180ms;   /* prev/next crossfade between images */
  --lightbox-scale: 0.965;          /* start/end zoom for the figure */
  --lightbox-nav-scale: 0.992;      /* subtler start scale for navigation */

  /* Elevation — shadcn-style soft shadows. Kept as tokens so every card,
     button and hover state shares the same restraint. */
  --shadow-sm: 0 1px 2px 0 hsl(240 6% 10% / 0.05);
  --shadow-md: 0 4px 12px -2px hsl(240 6% 10% / 0.10),
    0 2px 4px -2px hsl(240 6% 10% / 0.06);
  --shadow-lg: 0 12px 28px -8px hsl(240 6% 10% / 0.16),
    0 4px 8px -4px hsl(240 6% 10% / 0.08);

  /* Active theme: defaults to light. The `prefers-color-scheme: dark` media
     query below re-points these to the dark palette when the OS asks. */
  --background: var(--light-background);
  --foreground: var(--light-foreground);
  --card: var(--light-card);
  --card-foreground: var(--light-card-foreground);
  --muted: var(--light-muted);
  --muted-foreground: var(--light-muted-foreground);
  --border: var(--light-border);
  --highlight: var(--light-highlight);
  --highlight-foreground: var(--light-highlight-foreground);
  --control-border: var(--light-control-border);
  /* The accent FILL is identical in both themes; only its text and line
     variants differ so accent text/hairlines stay visible on each background. */
  --accent-text: var(--light-accent-text);
  --accent-line: var(--light-accent-line);

  /* color-scheme tells the browser to theme its OWN surfaces (scrollbars,
     form controls, the canvas behind the page). Declared once here; the
     `light dark` value lets the browser pick the mode matching the OS, and
     the dark media query below keeps the token palette in step. */
  color-scheme: light dark;
}

/* 1b. Theme application.
   Each palette is written exactly once above and mapped onto the shared
   public tokens below, so light and dark values never drift. */

/* Automatic dark mode: apply the dark palette when the OS asks for it. */
@media (prefers-color-scheme: dark) {
  :root {
    --background: var(--dark-background);
    --foreground: var(--dark-foreground);
    --card: var(--dark-card);
    --card-foreground: var(--dark-card-foreground);
    --muted: var(--dark-muted);
    --muted-foreground: var(--dark-muted-foreground);
    --border: var(--dark-border);
    --highlight: var(--dark-highlight);
    --highlight-foreground: var(--dark-highlight-foreground);
    --control-border: var(--dark-control-border);
    --accent-text: var(--dark-accent-text);
    --accent-line: var(--dark-accent-line);
  }
}

/* Manual override (the header theme switch). `data-theme` is set on <html>
   by the inline head script and by app.js. These selectors are more specific
   than the media query's `:root` (0,2,0 vs 0,1,0), so an explicit choice
   always wins over the OS — including a forced LIGHT on a dark OS. `auto`
   means neither attribute is present, so the media query above rules. */
:root[data-theme="light"] {
  --background: var(--light-background);
  --foreground: var(--light-foreground);
  --card: var(--light-card);
  --card-foreground: var(--light-card-foreground);
  --muted: var(--light-muted);
  --muted-foreground: var(--light-muted-foreground);
  --border: var(--light-border);
  --highlight: var(--light-highlight);
  --highlight-foreground: var(--light-highlight-foreground);
  --control-border: var(--light-control-border);
  --accent-text: var(--light-accent-text);
  --accent-line: var(--light-accent-line);
  color-scheme: light;
}
:root[data-theme="dark"] {
  --background: var(--dark-background);
  --foreground: var(--dark-foreground);
  --card: var(--dark-card);
  --card-foreground: var(--dark-card-foreground);
  --muted: var(--dark-muted);
  --muted-foreground: var(--dark-muted-foreground);
  --border: var(--dark-border);
  --highlight: var(--dark-highlight);
  --highlight-foreground: var(--dark-highlight-foreground);
  --control-border: var(--dark-control-border);
  --accent-text: var(--dark-accent-text);
  --accent-line: var(--dark-accent-line);
  color-scheme: dark;
}

/* High-contrast preference: strengthen the muted text and borders a little so
   the site stays comfortably readable for users who ask for more contrast.
   Only the values that commonly sit near the AA threshold are adjusted. */
@media (prefers-contrast: more) {
  :root {
    --light-muted-foreground: 240 8% 26%;
    --light-border: 240 6% 70%;
    --light-control-border: 240 6% 40%;
    --dark-muted-foreground: 0 0% 82%;
    --dark-border: 240 4% 40%;
    --dark-control-border: 240 6% 70%;
  }
}


/* --------------------------------------------------------------------------
   2. BASE / RESET
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Paint the root element with the theme background too. The root's
     background is propagated to the canvas, which is what the browser shows
     in the overscroll / rubber-band strip past the top or bottom edge; without
     this the canvas falls back to the UA color-scheme default (gray in light,
     white in dark) instead of the site palette. `body` keeps its own
     background for the in-flow page. Overscroll-behavior is deliberately left
     at its default so the natural bounce is preserved (the preview strip sets
     its own `overscroll-behavior-x`). */
  background-color: hsl(var(--background));
}

/* Theme changes swap colour tokens. We deliberately do NOT animate them (no
   cross-fade class): the switch is instant, which keeps it crisp and avoids
   any first-paint flash. This applies to the OS preference AND the explicit
   header theme switch. */

body {
  margin: 0;
  background-color: hsl(var(--background));
  color: hsl(var(--foreground));
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  font-family: var(--font-serif);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0 0 0.5em;
}

h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }

p { margin: 0 0 1em; }

/* Plain links carry a static hairline (keyboard/print safety). Content links
   covered by the scoped rule in section 11 drop the static border and get a
   gradient underline that grows from the left instead. */
a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid hsl(var(--border));
  transition: border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
a:hover { border-color: hsl(var(--accent-line)); }

/* Highlighted text uses the accent as a soft background with a foreground
   chosen to clear AA over the accent fill. */
::selection {
  background-color: hsl(var(--accent));
  color: hsl(var(--accent-foreground));
}

/* Consistent, visible keyboard focus (never remove outlines entirely). */
:focus-visible {
  outline: 2px solid hsl(var(--accent-line));
  outline-offset: 2px;
}

img {
  display: block;
  max-width: 100%;
  height: auto;
}

/* Uppercase, letterspaced section label used above headings and in the nav */
.eyebrow {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: lowercase;
  color: hsl(var(--muted-foreground));
  margin-bottom: 0.75rem;
}

/* Visually hidden but available to screen readers (used for live regions and
   link labels whose icon carries the meaning). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   3. LAYOUT HELPERS
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

main { display: block; }

/* Language gateway (root "/"). A minimal, language-neutral landing whose only
   job is to let the visitor pick DE or EN (with optional JS auto-redirect on
   first visit — see index.html). Kept centred and quiet. */
.gateway {
  display: grid;
  place-items: center;
  min-height: 100vh;
  min-height: 100svh;
  text-align: center;
}
.gateway-inner {
  padding: var(--gutter);
  max-width: 28rem;
}
.gateway-name {
  font-family: var(--font-serif);
  font-size: 1.5rem;
  margin: 0 0 0.25rem;
}
.gateway-eyebrow {
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: lowercase;
  color: hsl(var(--muted-foreground));
  margin: 0 0 2rem;
}
.gateway-title {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.5rem, 4vw, 2rem);
  margin: 0 0 0.25rem;
}
.gateway-sub {
  color: hsl(var(--muted-foreground));
  margin: 0 0 1.75rem;
}
.gateway-choices {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  flex-wrap: wrap;
}

/* Vertical rhythm between page sections */
.section { padding-block: clamp(2.5rem, 7vw, 5rem); }
.section + .section { border-top: 1px solid hsl(var(--border)); }

.lead {
  font-size: clamp(1.05rem, 2vw, 1.25rem);
  color: hsl(var(--muted-foreground));
  max-width: 60ch;
}

/* --------------------------------------------------------------------------
   4. HEADER / NAV / LANGUAGE / MOBILE MENU
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky;   /* stays visible at all scroll positions */
  top: 0;
  z-index: var(--z-header);
  /* Always fully opaque and identical at the top and while scrolled: a solid
     theme background plus a hairline border. No backdrop-filter / no
     transparency, so page content never shows through the header. */
  border-bottom: 1px solid hsl(var(--border));
  background-color: hsl(var(--background));
  /* Notch / rounded-corner / status-area insets. Applied to the header ELEMENT
     (not .container), so the header background still spans edge to edge while
     its in-flow content is nudged clear of the notch in landscape and the
     status area in standalone/PWA mode. .container keeps its own --gutter;
     the inset stacks on top of it rather than replacing it, so there is no
     double-pad jump (env() is 0 on devices without insets). */
  padding-top: env(safe-area-inset-top);
  padding-inline: env(safe-area-inset-right) env(safe-area-inset-left);
}

.header-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-6);
  padding-block: 1.1rem;
}

.brand {
  font-family: var(--font-serif);
  font-size: 1.15rem;
  border-bottom: none;
  white-space: nowrap;
  margin-right: auto;   /* pushes nav + controls to the right */
}

/* The right-hand control cluster: language switcher and (on small screens)
   the mobile menu button. The main nav is a sibling of this cluster inside
   .header-inner. */
.header-controls {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--control-height); /* 44px: matches the rest of the header row */
  border-bottom: 1px solid transparent;
  color: hsl(var(--muted-foreground));
  font-size: 0.9rem;
  transition: color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}
.site-nav a:hover { color: hsl(var(--foreground)); }
/* aria-current marks the page you are on. The muted accent is used for the
   active hairline and the language state — a quiet, consistent highlight. */
.site-nav a[aria-current="page"] {
  color: hsl(var(--accent-text));
  border-bottom-color: hsl(var(--accent-line));
}

.lang-switch {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  letter-spacing: 0.08em;
}
.lang-switch a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-height); /* 44px: a primary header tap target */
  min-width: 2.25rem;
  padding-inline: 0.4rem;
  border-bottom: 1px solid transparent;
  color: hsl(var(--muted-foreground));
}
.lang-switch a:hover { color: hsl(var(--foreground)); }
.lang-switch a[aria-current="true"] {
  color: hsl(var(--accent-text));
  font-weight: 600;
  border-bottom-color: hsl(var(--accent-line));
}
.lang-switch .sep { color: hsl(var(--border)); }

/* --- Theme switch ------------------------------------------------------
   One ghost icon button cycling auto -> light -> dark. Three stacked icons;
   only the one matching `data-theme-state` (set on <html> by the inline head
   script, kept in sync by app.js) is shown, so the correct icon is painted
   before first paint. The three icons are visually distinct at small size:
     .icon-auto  a circle half-filled (system / follow-OS)
     .icon-light a full sun with rays (forced light)
     .icon-dark  a crescent moon (forced dark)
   The button is a fixed 44x44 square with `overflow: hidden`, so it can NEVER
   clip, overflow or shift layout in any state. Hover is deliberately minimal:
   a faint background tint only (no tooltip / no label / no transform), because
   a hover tooltip clipped and cramped the label. Keyboard focus is provided by
   the global `:focus-visible` outline (independent of hover). ------------- */
.theme-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  overflow: hidden;          /* hard guarantee: no icon/text can spill out */
  border: 1px solid transparent;
  border-radius: var(--radius);
  background-color: transparent;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
.theme-toggle:hover,
.theme-toggle:active {
  /* Restrained: a faint neutral tint plus a visible control boundary (no
     colour change, no movement). The border is always 1px; only its colour
     changes, so there is zero layout shift. */
  background-color: hsl(var(--highlight));
  border-color: hsl(var(--control-border));
}
.theme-toggle svg {
  width: 1.2rem;
  height: 1.2rem;
  display: block;
  flex: none;
}
.theme-toggle .icon-auto,
.theme-toggle .icon-light,
.theme-toggle .icon-dark { display: none; }
:root[data-theme-state="auto"] .theme-toggle .icon-auto,
:root:not([data-theme-state]) .theme-toggle .icon-auto { display: block; }
:root[data-theme-state="light"] .theme-toggle .icon-light { display: block; }
:root[data-theme-state="dark"] .theme-toggle .icon-dark { display: block; }

/* --- Mobile menu button + panel (CSS-first, tiny JS toggle) ------------
   Hidden on wide screens, where the nav sits inline. Below 42rem the nav is
   tucked into a collapsible panel so the header never crowds. The button
   matches the shared control height so it aligns with the language switcher. */
.menu-toggle {
  display: none;   /* shown only on small screens, below */
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  border: 1px solid hsl(var(--control-border));
  border-radius: var(--radius);
  background-color: transparent;
  color: hsl(var(--foreground));
  cursor: pointer;
}
.menu-toggle:hover { background-color: hsl(var(--highlight)); }
.menu-toggle svg { width: 1.2rem; height: 1.2rem; }

/* Burger icon morph. The SVG is three 24x24 <line>s at y≈7/12/17; the top and
   bottom shift 5px to the middle (y≈12) before rotating into an X. Keyed on
   the button's `aria-expanded` so no class toggling is needed. Stroke colour
   comes from the markup (stroke="currentColor"), never from CSS. */
.menu-toggle svg .menu-icon-line {
  transition: transform var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
  transform-origin: center;
}
.menu-toggle[aria-expanded="true"] .menu-icon-line--top {
  transform: translateY(5px) rotate(45deg);
}
.menu-toggle[aria-expanded="true"] .menu-icon-line--mid { opacity: 0; }
.menu-toggle[aria-expanded="true"] .menu-icon-line--bottom {
  transform: translateY(-5px) rotate(-45deg);
}

@media (max-width: 42rem) {
  .menu-toggle { display: inline-flex; }

  /* The nav collapses into a full-width panel under the header — but ONLY
     when JS is available (app.js adds `js-menu` to the nav). Without JS the
     nav stays visible and simply wraps, so mobile navigation is never
     unreachable. The panel itself stays in flow (clipped) and its inner <ul>
     animates open, so the reveal no longer snaps via display: none/block. */
  .site-nav.js-menu {
    flex-basis: 100%;
    order: 3;
    overflow: hidden;
  }
  .site-nav.js-menu ul {
    flex-direction: column;
    gap: 0;
    /* Collapsed: no padding-top so the measured height is exact. The open rule
       introduces the 0.5rem spacing, which is absorbed by the expanded
       max-height, so nothing jumps. */
    max-height: 0;
    opacity: 0;
    visibility: hidden;
    overflow: hidden;
    transform: translateY(-0.25rem);
    transition: max-height var(--duration-menu) var(--ease-out),
      opacity var(--duration-menu) var(--ease-out),
      transform var(--duration-menu) var(--ease-out),
      visibility 0s linear var(--duration-menu);
  }
  .site-nav.js-menu.is-open ul {
    max-height: 30rem;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
    padding-top: 0.5rem;
  }
  .site-nav li { border-top: 1px solid hsl(var(--border)); }
  .site-nav a {
    display: block;
    padding: 0.75rem 0.25rem;
    font-size: 0.95rem;
  }

}

/* Footer clearance for the fixed back-to-top button. The button sits in the
   viewport's bottom-right and stays visible all the way to the page bottom, so
   the footer must reserve space for it wherever the page container still
   reaches the button's corner — i.e. until the content column is capped and
   centred (--max-width 68rem), after which the button floats in the empty side
   margin. .footer-bottom gets right padding (so the last link clears the
   button horizontally) and .site-footer gets bottom padding (so the footer
   content clears the button's vertical band: bottom offset + control height).
   This is the spacing-based replacement for the old hide-on-footer behaviour. */
@media (max-width: 68rem) {
  .footer-bottom { padding-right: calc(var(--control-height) + 1rem); }
  /* Reserve room for the back-to-top button AND add the device's bottom safe
     inset, so the footer's last links clear both the button's vertical band
     and the home indicator. */
  .site-footer {
    padding-bottom: calc(
      var(--control-height) + 2rem + env(safe-area-inset-bottom)
    );
  }
}

/* --------------------------------------------------------------------------
   5. HOME — split hero: text column and lead image
   -------------------------------------------------------------------------- */
.hero {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  padding-block: clamp(2.5rem, 7vw, 5.5rem);
}
@media (min-width: 48rem) {
  .hero { grid-template-columns: 1fr 1.15fr; }
}

.hero-text h1 { margin-bottom: 0.6em; }
.hero-text .lead { margin-bottom: 1.5em; }

.hero-figure {
  margin: 0;
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius-lg);
  overflow: hidden;
  background-color: hsl(var(--muted));
}
.hero-figure img { width: 100%; }

/* Small link list under the intro (e.g. "selected work", "about") */
.link-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  font-size: 0.92rem;
}

/* --- Richer home page: preview strip, teasers, regions note ------------- */

/* A horizontally scrollable strip of selected works. It is a plain list, so
   it degrades to a normal grid wrapped by the browser if scrolling is off. */
.preview-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(14rem, 17rem);
  gap: var(--gallery-gap);
  list-style: none;
  margin: 0;
  padding: 0 0 0.5rem;
  overflow-x: auto;
  overflow-y: hidden;          /* horizontal strip only — never scrolls vertically */
  touch-action: pan-x;         /* horizontal panning; vertical swipes scroll the PAGE */
  overscroll-behavior-x: contain; /* no back/refresh chaining from the strip's momentum */
  scroll-snap-type: x proximity;
}
.preview-item {
  margin: 0;
  scroll-snap-align: start;
}
/* Previews are image-only (no caption line). The surrounding link is a plain
   block so the whole image is clickable. */
.preview-item a {
  display: block;
  border-bottom: none;
}

/* On wide screens the strip fits as a centered grid instead of scrolling.
   Reset `touch-action` back to auto because there is no longer any scrollable
   axis to pan; `overscroll-behavior` is already inert without overflow. */
@media (min-width: 52rem) {
  .preview-strip {
    display: grid;
    grid-auto-flow: row;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    overflow: visible;
    touch-action: auto;
  }
}

/* Two-column teaser grid for "gallery" and "about" sections. */
.teaser-grid {
  display: grid;
  gap: clamp(1.25rem, 4vw, 2.5rem);
  margin-top: 1.75rem;
}
@media (min-width: 44rem) {
  .teaser-grid { grid-template-columns: 1fr 1fr; }
}
.teaser-link { font-size: 0.9rem; }

/* Regions note: a quiet list of areas the photographer works in. */
.regions {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 0.75rem;
}
/* Region chips reuse the `.badge` accent treatment, but this rule intentionally
   OVERRIDES `.badge` (higher specificity, 0,1,1 vs 0,1,0) so the regions list
   keeps its own sizing and padding while the markup stays `<li class="badge">`. */
.regions li {
  font-size: var(--text-caption);
  letter-spacing: 0.06em;
  color: hsl(var(--accent-text));
  border: 1px solid hsl(var(--accent-line) / 0.6);
  border-radius: var(--radius-pill);
  padding: 0.25rem 0.85rem;
}

/* --------------------------------------------------------------------------
   6. GALLERY — responsive CSS grid, no JavaScript
   -------------------------------------------------------------------------- */
.category + .category { margin-top: clamp(2rem, 5vw, 3.5rem); }

.category-head {
  margin-bottom: 1.25rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid hsl(var(--border));
}

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--gallery-gap);
  list-style: none;
  margin: 0;
  padding: 0;
}

.gallery-item { margin: 0; }

/* The image frame (border, radius, ratio) comes from the shared .media
   component, so gallery images are wrapped in <span class="media">. */

.gallery-item figcaption {
  margin-top: 0.6rem;
  font-size: 0.8rem;
  color: hsl(var(--muted-foreground));
}

/* --------------------------------------------------------------------------
   7. ABOUT — portrait beside the bio
   -------------------------------------------------------------------------- */
.about {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
}
@media (min-width: 48rem) {
  .about { grid-template-columns: 0.8fr 1.2fr; align-items: start; }
}

.portrait {
  margin: 0;
  position: sticky;         /* portrait stays visible while bio scrolls */
  top: 2rem;
}
/* Small-screen collapse: the sticky portrait becomes a normal in-flow block. */
@media (max-width: 42rem) {
  .portrait { position: static; }
}
.portrait .media {
  /* Portrait uses the shared .media frame with a 3:4 ratio. The ratio is set
     here (not inline via --media-ratio) so the frame's geometry lives with
     the component styling. */
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
}
.portrait figcaption {
  margin-top: 0.6rem;
  font-size: var(--text-caption);
  color: hsl(var(--muted-foreground));
}

/* Simple data list used on the contact page */
.detail-list {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  border-top: 1px solid hsl(var(--border));
}
.detail-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid hsl(var(--border));
  font-size: 0.95rem;
}
.detail-list .label {
  min-width: 8rem;
  color: hsl(var(--muted-foreground));
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  text-transform: lowercase;
  align-self: center;
}

/* Legal / imprint documents are easier to read with a narrower measure */
.prose { max-width: 65ch; }
.prose h2 { margin-top: var(--space-8); }

/* Small vertical spacer used between prose blocks. */
.spaced-top { margin-top: var(--space-8); }

/* --------------------------------------------------------------------------
   8. COMPONENTS — small framework-free shadcn/ui-style building blocks.

   Everything here is optional and composable. The patterns are:
     .btn / .btn-primary / .btn-outline
     .card (with .card-body, .card-title, .card-text)
     .badge
     .separator
     .tooltip (CSS-only, [data-tooltip] + .has-tooltip)
     .media (image frame with skeleton shimmer + hover)
   Documented in README section "Components".
   -------------------------------------------------------------------------- */

/* --- Button -------------------------------------------------------------
   shadcn-style buttons: small radius, one-line height, transparent border so
   all variants keep the same box. Sizing is token-driven: every variant shares
   `--control-height`, so a row of buttons lines up, and the label is centred
   with flexbox (no baseline drift and no reliance on line-height alone).
   `min-height` guarantees a comfortable, touch-friendly target (>=44px) even
   when a variant uses a smaller font. Animates transform/colour only. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--control-gap);
  min-height: var(--control-height);
  font-family: var(--font-sans);
  font-size: 0.9375rem;     /* 15px — a touch larger than before, easier to read */
  font-weight: 500;
  line-height: 1.2;         /* allows wrapping without clipping; flex centres it */
  text-align: center;
  /* Symmetric padding: equal top and bottom so the flex-centred label sits
     dead-centre in every variant. (Content-link rules that would add a 1px
     bottom padding to buttons are excluded via `:not(.btn)` further down.) */
  padding: 0.5rem var(--control-pad-x);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background-color: transparent;
  /* Guards against the content-link gradient underline (background-image). */
  background-image: none;
  color: hsl(var(--foreground));
  cursor: pointer;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}
.btn > * { text-align: center; }
/* On hover the button lifts a hair and its resting shadow turns into a
   slightly stronger one — a quiet "inner highlight", no colour change. */
.btn:hover {
  transform: translateY(var(--hover-lift));
  box-shadow: var(--shadow-md);
}
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); }

/* Primary button = a solid BLACK fill (the accent) with white text. A subtle
   accent-line border keeps it discernible against a dark page. */
.btn-primary {
  background-color: hsl(var(--accent));
  color: hsl(var(--accent-foreground));
  border-color: hsl(var(--accent-line) / 0.25);
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
  background-color: hsl(var(--accent) / 0.85);
  box-shadow: var(--shadow-md);
}

.btn-outline {
  border-color: hsl(var(--control-border));
  background-color: hsl(var(--background));
}
.btn-outline:hover { background-color: hsl(var(--highlight)); }

/* --- Badge / tag --------------------------------------------------------
   Small, quiet pills: a restrained accent outline with accent text (the accent
   text variant keeps AA contrast in both themes). */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-family: var(--font-sans);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 0.3rem 0.6rem;
  border: 1px solid hsl(var(--accent-line) / 0.6);
  border-radius: var(--radius-pill);
  color: hsl(var(--accent-text));
  background-color: hsl(var(--background));
}

/* --- Lightbox (fullscreen gallery view) -------------------------------
   Uses the native <dialog> element for the modal + focus trap. The dialog
   covers the viewport with a dark backdrop; the figure holds the image and
   the caption/counter/controls. Space is reserved (fixed layout, no reflow),
   so opening never shifts the page.

   Open AND close are a coordinated fade + slight zoom (same duration + easing
   token, `--duration-lightbox`). `app.js` drives close by adding `.is-closing`
   and waiting for the animation to finish before it actually hides the dialog,
   so ESC / backdrop / close-button all animate identically and the close never
   snaps. Navigation between images crossfades faster and subtler
   (`--duration-lightbox-nav`). Only opacity/transform animate, so nothing
   reflows. Under reduced motion both are instant. */
.lightbox {
  padding: 0;
  border: 0;
  max-width: 100vw;
  max-height: 100vh;
  max-height: 100dvh;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  background: transparent;
  overflow: hidden;
}
.lightbox::backdrop {
  background: hsl(var(--lightbox-backdrop) / 0.86);
  /* The backdrop fades in/out in step with the figure. */
  opacity: 1;
  transition: opacity var(--duration-lightbox) var(--ease-out);
}
.lightbox.is-closing::backdrop {
  opacity: 0;
}
/* Fallback overlay (no <dialog> support): a fixed full-screen box. */
.lightbox:not(dialog) {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: none;
}
.lightbox:not(dialog)[open] { display: block; }

.lightbox-inner {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: 1fr auto;
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  gap: 1rem;
}
.lightbox-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
}
.lightbox-figure img {
  max-width: 100%;
  max-height: 78vh;
  max-height: 78svh;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  background: hsl(var(--lightbox-image-bg) / 0.2);
}
.lightbox-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
  color: hsl(var(--lightbox-fg));
  font-size: var(--text-sm);
}
.lightbox-caption { color: hsl(var(--lightbox-fg-muted)); }
.lightbox-counter {
  color: hsl(var(--lightbox-fg-subtle));
  font-variant-numeric: tabular-nums;
  flex: none;
}
.lightbox-btn {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  border: 1px solid hsl(var(--lightbox-ctl-border) / 0.25);
  border-radius: var(--radius-pill);
  background: hsl(var(--lightbox-ctl-bg) / 0.35);
  color: hsl(var(--lightbox-ctl-fg));
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}
/* The lightbox is intentionally always-dark so photos are judged against a
   neutral backdrop. The accent appears only as a quiet ring on hover. */
.lightbox-btn:hover {
  background: hsl(var(--lightbox-ctl-bg) / 0.6);
  border-color: hsl(var(--accent-line) / 0.8);
  transform: scale(1.05);
}
.lightbox-btn svg { width: 1.3rem; height: 1.3rem; }
.lightbox-close { top: clamp(0.75rem, 3vw, 1.5rem); right: clamp(0.75rem, 3vw, 1.5rem); }
.lightbox-prev { left: clamp(0.75rem, 3vw, 1.5rem); top: 50%; transform: translateY(-50%); }
.lightbox-next { right: clamp(0.75rem, 3vw, 1.5rem); top: 50%; transform: translateY(-50%); }
.lightbox-prev:hover, .lightbox-next:hover { transform: translateY(-50%) scale(1.05); }

/* Open animation. Applied while `[open]` is set and the dialog is not closing,
   so it runs on open but never fights the reverse close animation. It targets
   the whole `.lightbox` unit (inner figure AND the controls, which are its
   siblings) so every element—image, caption, counter, close and prev/next—
   moves and fades as ONE coordinated unit. */
.lightbox[open]:not(.is-closing) {
  animation: lightbox-in var(--duration-lightbox) var(--ease-out) both;
}
@keyframes lightbox-in {
  from { opacity: 0; transform: scale(var(--lightbox-scale)); }
  to   { opacity: 1; transform: none; }
}
/* Close animation. `app.js` adds `.is-closing` first, then hides the dialog on
   `animationend` (with a timeout safety net). It animates the SAME whole-unit
   element as the open, so on close the figure, caption, counter and all
   controls fade out together with the backdrop instead of the buttons
   lingering until the dialog is hidden. */
.lightbox.is-closing {
  animation: lightbox-out var(--duration-lightbox) var(--ease-out) both;
}
@keyframes lightbox-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(var(--lightbox-scale)); }
}

/* Image reveal inside the lightbox. `app.js` resets `.is-loaded` before each
   new src and sets it on the image's `load` event (and immediately for cached
   images). The transition uses the short navigation duration, so prev/next
   crossfades faster and subtler than the open/close. Only opacity/transform,
   so nothing reflows. */
.lightbox [data-lightbox-image] {
  opacity: 0;
  transform: scale(var(--lightbox-nav-scale));
  transition: opacity var(--duration-lightbox-nav) var(--ease-out),
    transform var(--duration-lightbox-nav) var(--ease-out);
}
.lightbox [data-lightbox-image].is-loaded {
  opacity: 1;
  transform: none;
}

/* --- Separator ----------------------------------------------------------
   A single solid hairline, consistent with `.section + .section` (same 1px,
   same --border colour, same full width). No gradient. */
.separator {
  height: 1px;
  border: 0;
  margin: clamp(1.5rem, 4vw, 2.5rem) 0;
  background-color: hsl(var(--border));
  background-image: none;
}

/* --- Card --------------------------------------------------------------- */
.card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius-lg);
  background-color: hsl(var(--card));
  color: hsl(var(--card-foreground));
  transition: transform var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}
/* Only interactive cards get the border + lift, on pointer hover and keyboard
   focus, so plain info cards stay quiet. An interactive `.card` (e.g. one
   wrapped in a link) can add `.card-interactive` to get the pointer lift. */
a.card:hover,
a.card:focus-visible,
.card-interactive:hover,
.card-interactive:focus-visible {
  box-shadow: var(--shadow-md);
  border-color: hsl(var(--accent-line) / 0.6);
  transform: translateY(calc(var(--hover-lift) * 2));
}
.card-body { padding: clamp(1rem, 3vw, 1.35rem); }
.card-title { margin: 0 0 0.35em; font-size: 1.15rem; }
.card-text { margin: 0 0 1em; color: hsl(var(--muted-foreground)); font-size: 0.92rem; }

/* --- Tooltip (CSS-only) ------------------------------------------------
   Add .has-tooltip to a focusable element and data-tooltip="…". The bubble
   shows on hover AND keyboard focus, and is announced via aria-describedby
   is not automatic, so keep tooltip text non-essential. */
.has-tooltip { position: relative; }
.has-tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  left: 50%;
  bottom: calc(100% + 0.5rem);
  transform: translateX(-50%) translateY(2px);
  white-space: nowrap;
  font-size: var(--text-xs);
  font-family: var(--font-sans);
  color: hsl(var(--background));
  background-color: hsl(var(--foreground));
  padding: 0.3rem 0.55rem;
  border-radius: var(--radius);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
  z-index: var(--z-tooltip);
}
.has-tooltip:hover::after,
.has-tooltip:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* --- Media frame: consistent ratio + skeleton shimmer + hover ----------
   Wrap an <img> in .media. The FRAME owns the aspect ratio (default 4/3) so
   the layout never shifts while the image loads; the img simply fills it. A
   soft shimmer shows while the image loads, then stops. This unifies gallery
   and home preview presentation. Override the ratio per use by setting
   `--media-ratio` from a component rule (see .portrait .media) — never via a
   style attribute, so the strict CSP can omit style-src 'unsafe-inline'. */
.media {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: var(--media-ratio, 4 / 3);
  border: 1px solid hsl(var(--border));
  border-radius: var(--radius);
  background-color: hsl(var(--muted));
  /* Depth on hover/focus: the frame deepens its shadow and its hairline warms
     toward the muted accent. The image itself is not transformed here, so
     nothing moves or reflows — this is deliberately parallax-free. */
  transition: box-shadow var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}
.media:hover,
.media:focus-within,
a:hover .media {
  box-shadow: var(--shadow-md);
  border-color: hsl(var(--accent-line) / 0.6);
}
.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-zoom) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}
.media:hover img,
a:hover .media img {
  transform: scale(var(--hover-scale));
  opacity: 0.95;
}
/* Skeleton shimmer: a subtle moving highlight behind the (still invisible)
   image. It is decorative and removed for reduced motion below. */
.media::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 30%,
    hsl(var(--foreground) / 0.04) 50%,
    transparent 70%
  );
  background-size: 220% 100%;
  animation: shimmer var(--duration-shimmer) var(--ease-out) infinite;
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
  pointer-events: none;
}
/* Only shimmer while the image is not yet loaded. The tiny inline loader in
   app.js (or the native `load` event) toggles .is-loaded on the frame. */
.media.is-loading::before { opacity: 1; }
.media.is-loaded::before { opacity: 0; }
/* A load failure also stops the shimmer (the frame keeps its muted background). */
.media.is-error::before { opacity: 0; }
@keyframes shimmer {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}

/* --------------------------------------------------------------------------
   9. FOOTER
   -------------------------------------------------------------------------- */
.site-footer {
  border-top: 1px solid hsl(var(--border));
  margin-top: clamp(3rem, 8vw, 6rem);
  /* Clear the home indicator / rounded bottom corners on notched devices.
     The side insets sit on the footer ELEMENT, outside the .container gutter,
     so they stack rather than replace it (env() is 0 where unsupported). */
  padding-bottom: env(safe-area-inset-bottom);
  padding-inline: env(safe-area-inset-right) env(safe-area-inset-left);
}

/* Richer footer: a short site line plus small link columns, then a bottom
   bar with copyright and the legal link. */
.footer-grid {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  padding-block: clamp(2rem, 5vw, 3rem);
}
@media (min-width: 44rem) {
  .footer-grid { grid-template-columns: 1.4fr 1fr 1fr; }
}
.footer-col .eyebrow {
  margin: 0 0 0.9rem;
}
.footer-site {
  font-family: var(--font-serif);
  font-size: 1.05rem;
  margin: 0 0 0.5rem;
}
.footer-note {
  font-size: var(--text-sm);
  color: hsl(var(--muted-foreground));
  max-width: 34ch;
  margin: 0;
}
.footer-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
}
.footer-col a {
  font-size: var(--text-sm);
  color: hsl(var(--muted-foreground));
  border-bottom: none;
  transition: color var(--duration-fast) var(--ease-out);
}
.footer-col a:hover { color: hsl(var(--foreground)); }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-6);
  padding-block: 1.25rem;
  border-top: 1px solid hsl(var(--border));
  font-size: var(--text-caption);
  color: hsl(var(--muted-foreground));
}
.footer-bottom p { margin: 0; }
.footer-bottom a { border-bottom: 1px solid transparent; }
.footer-bottom a:hover { border-bottom-color: currentColor; }

/* --------------------------------------------------------------------------
   10. MOTION, REVEALS & DELIGHTS — subtle, editorial, transform/opacity only.

   Three layers, all driven by the same tokens in section 1:
     a) page-enter   — a one-shot fade of <main> on every load/navigation;
     b) scroll reveal — content blocks fade/translate in via [data-reveal],
        switched on by js/app.js (progressive enhancement);
     c) hover states  — quiet transforms on nav, links and gallery images.

   Only `transform` and `opacity` are animated, so the compositor can handle
   everything without layout or paint thrash.
   -------------------------------------------------------------------------- */

/* a) Page-enter. The header and footer are persistent (the router swaps only
   <main>), so the document never reloads. This one-shot animation still runs
   on the <main> element: on a full load AND on every router swap a *new*
   <main> node is inserted, so it fades + rises in cleanly while the header,
   footer and back-to-top button stay perfectly still. */
main {
  animation: page-enter var(--duration-page) var(--ease-out) both;
}
@keyframes page-enter {
  from { opacity: 0; transform: translateY(var(--reveal-shift-lg)); }
  to   { opacity: 1; transform: none; }
}

/* b) Scroll reveals with per-element stagger. Elements marked [data-reveal]
   start hidden and shift up slightly, then animate to rest when the
   IntersectionObserver in /js/app.js adds .is-revealed.
   The `.js` class is set on <html> by an inline snippet in <head>, so:
     - WITHOUT JavaScript: these rules never apply and all content is visible
       immediately (content is never gated behind a failed script);
     - WITH JavaScript: blocks animate in as they enter the viewport.
   Two escape hatches keep content safe: the app.js <script> tag carries an
   `onerror` that removes `.js`, and the inline head snippet also removes it
   after a short timeout if the script never ran.

   Stagger is driven by a per-element `--delay` custom property
   (set in markup or by app.js), NOT by :nth-child, so it survives reordering
   and has no fixed sibling cap. `--transition-delay` is applied only while
   revealing so hovering later is not delayed. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(var(--reveal-shift));
}
.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  transition: opacity var(--duration-reveal) var(--ease-out) var(--delay, 0ms),
    transform var(--duration-reveal) var(--ease-out) var(--delay, 0ms);
}
/* Items that are direct children of a reveal group (in DOM order) cascade via
   `--delay`. app.js assigns --delay from --reveal-step when it observes the
   group, so no per-element inline styles are needed in the HTML. */
.js [data-reveal-group] > .is-revealed { transition-delay: var(--delay, 0ms); }

/* Gallery grids rise a little further and settle from a soft 0.985 scale,
   giving the photo wall a slightly more deliberate build-up. app.js marks any
   `.gallery-grid[data-reveal-group]` with the `data-gallery` attribute; the
   marker only affects items inside it, and only transform/opacity is animated.
   Other reveal blocks keep --reveal-shift and the default timing. */
.js [data-reveal-group][data-gallery] > [data-reveal] {
  transform: translateY(var(--reveal-shift-gallery))
    scale(var(--reveal-scale-gallery));
}
.js [data-reveal-group][data-gallery] > [data-reveal].is-revealed {
  transform: none;
}

/* c) Hover states — soft and restrained. */

/* Nav and language links get a hairline underline that grows on hover. */
.site-nav a:hover,
.lang-switch a:hover { transform: translateY(var(--hover-lift)); }

/* Content links (prose, link rows, teaser links, footer columns) use the
   animated gradient underline from the base layer. This is scoped to content
   areas only, so .site-nav a / .lang-switch a / .brand keep their own
   treatments. IMPORTANT: `.btn` is explicitly excluded — otherwise the more
   specific `.link-row a` selector would override the button's symmetric
   padding with `padding-bottom: 1px`, pushing its label downward and adding an
   unwanted underline (the root cause of the off-centre button text). */
.prose a:not(.btn),
.link-row a:not(.btn),
.teaser-link:not(.btn),
.footer-col a:not(.btn) {
  border-bottom: 0;
  background-image: linear-gradient(
    hsl(var(--accent-line)),
    hsl(var(--accent-line))
  );
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  padding-bottom: 1px;
  transition: background-size var(--duration-underline) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
.prose a:not(.btn):hover,
.link-row a:not(.btn):hover,
.teaser-link:not(.btn):hover,
.footer-col a:not(.btn):hover,
.prose a:not(.btn):focus-visible,
.link-row a:not(.btn):focus-visible,
.teaser-link:not(.btn):focus-visible,
.footer-col a:not(.btn):focus-visible {
  background-size: 100% 1px;
  color: hsl(var(--accent-text));
}

/* d) Back-to-top button. Fixed bottom-right, hidden until the page is scrolled
   past ~30% of a viewport (js/chrome.js applies hysteresis via
   TO_TOP_SHOW_RATIO / TO_TOP_HIDE_RATIO), then fades/slides in and stays
   visible to the page bottom. `visibility: hidden` while away keeps it out of
   the tab order so keyboard users never land on an invisible control; it
   becomes focusable only once it is shown. */
.to-top {
  position: fixed;
  /* Keep the button clear of the rounded corners and the home indicator on
     notched devices; env() is 0 where unsupported, so this is a no-op there. */
  right: calc(clamp(1rem, 4vw, 2rem) + env(safe-area-inset-right));
  bottom: calc(clamp(1rem, 4vw, 2rem) + env(safe-area-inset-bottom));
  z-index: var(--z-to-top);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  border: 1px solid hsl(var(--control-border));
  border-radius: var(--radius-pill);
  background-color: hsl(var(--background));
  color: hsl(var(--foreground));
  box-shadow: var(--shadow-md);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translateY(var(--to-top-shift));
  pointer-events: none;
  /* The opacity/transform and the visibility delay all use --duration-to-top,
     so the slide visibly completes before `visibility: hidden` lands (rather
     than popping while still moving). The colour transitions stay fast. */
  transition: opacity var(--duration-to-top) var(--ease-out),
    transform var(--duration-to-top) var(--ease-out),
    visibility 0s linear var(--duration-to-top),
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
.to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition-delay: 0s;
}
.to-top:hover {
  background-color: hsl(var(--accent) / 0.14);
  border-color: hsl(var(--accent-line) / 0.6);
  color: hsl(var(--accent-text));
}
.to-top svg { width: 1.1rem; height: 1.1rem; }

/* e) Scroll progress bar — a 2px accent line pinned to the very bottom edge
   of the sticky header (i.e. the top of the viewport, just under the header
   border). Its width is driven by `--scroll-progress` (0..1), which app.js
   updates at most once per animation frame so the bar cannot jitter. */
.scroll-progress {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(var(--scroll-progress, 0));
  transform-origin: left center;
  background-color: hsl(var(--accent-line));
  pointer-events: none;
}

/* Respect users who ask for less motion: switch every animation and
   transition off, and make sure revealed content is fully visible. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  /* Content must never stay hidden when motion is reduced. */
  .js [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }
  /* The shimmer is decorative; remove it entirely for reduced motion. */
  .media::before { animation: none !important; opacity: 0 !important; }
  /* Back-to-top should still work, just without an entrance animation. */
  .to-top { transform: none; }
  /* Mobile menu + burger morph are instant under reduced motion. The open
     state still applies .is-open's opacity/visibility, and `max-height: none`
     guarantees the panel is never clipped while collapsed rules persist. */
  .site-nav.js-menu ul { transition: none !important; }
  .site-nav.js-menu.is-open ul { max-height: none !important; }
  .menu-toggle svg .menu-icon-line { transition: none !important; }
  /* Animated underlines rely on `background-size`, which the global duration
     reset above does not disable. Pin every one to a static full hairline so
     the hover state is still legible without any growth animation. */
  .prose a:not(.btn),
  .link-row a:not(.btn),
  .teaser-link:not(.btn),
  .footer-col a:not(.btn) {
    transition: none !important;
    background-size: 100% 1px !important;
  }
  /* Gallery items must be visible: drop the extra rise/scale as well as the
     transitions (the global reset already removes the transition duration). */
  .js [data-reveal-group][data-gallery] > [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }
  /* The lightbox image is shown at rest — no fade/scale on load. */
  .lightbox [data-lightbox-image] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  /* Lightbox open/close are instant under reduced motion: remove the
     animations entirely (the JS fallback still hides the dialog). */
  .lightbox[open]:not(.is-closing),
  .lightbox.is-closing {
    animation: none !important;
  }
  .lightbox::backdrop { transition: none !important; }
  /* Hover state changes stay instantaneous AND motionless: pin every hover
     transform (the global duration reset alone would still jump states). */
  .btn:hover, .btn:active,
  .lightbox-btn:hover { transform: none; }
  .lightbox-prev:hover, .lightbox-next:hover { transform: translateY(-50%); }
  a.card:hover, a.card:focus-visible,
  .card-interactive:hover, .card-interactive:focus-visible,
  .site-nav a:hover, .lang-switch a:hover,
  .media:hover img, a:hover .media img { transform: none; }
  /* The tooltip bubble is positioned (and nudged 2px) via transform; pin it
     to its centred rest position so showing it on hover/focus cannot move. */
  .has-tooltip::after,
  .has-tooltip:hover::after,
  .has-tooltip:focus-visible::after { transform: translateX(-50%); }
}

/* --------------------------------------------------------------------------
   11. PRINT — a clean, ink-light stylesheet for gallery/about pages.
   Removes chrome (header controls, footer columns, buttons, back-to-top),
   forces black-on-white, and prints images in a simple grid.
   -------------------------------------------------------------------------- */
@media print {
  /* Force a plain ink-friendly palette regardless of the OS dark preference,
     so printed output is always dark text on white paper. */
  :root {
    /* Override to a plain palette so printed output is ink-friendly. */
    --background: 0 0% 100%;
    --foreground: 0 0% 0%;
    --muted: 0 0% 96%;
    --muted-foreground: 0 0% 25%;
    --border: 0 0% 60%;
    --card: 0 0% 100%;
    --card-foreground: 0 0% 0%;
    --highlight: 0 0% 96%;
    --highlight-foreground: 0 0% 0%;
    /* Print in ink: the black accent is already ink-friendly, so keep fills
       black and text black; hairlines/rings print black too. */
    --accent: 0 0% 0%;
    --accent-foreground: 0 0% 100%;
    --accent-text: 0 0% 0%;
    --accent-line: 0 0% 0%;
    color-scheme: light;
  }
  html, body {
    background: #fff !important;
    color: #000 !important;
  }
  * { box-shadow: none !important; text-shadow: none !important; }
  /* Remove on-screen chrome from the printed sheet: the sticky header, the
     footer columns, the back-to-top button, the scroll-progress bar, the
     mobile menu button and the language/theme controls are all screen-only. */
  .site-header,
  .site-footer,
  .to-top,
  .scroll-progress,
  .menu-toggle,
  .lang-switch,
  .theme-toggle,
  .footer-bottom { display: none !important; }
  main { animation: none !important; }
  .js [data-reveal] { opacity: 1 !important; transform: none !important; }
  a { border-bottom: none; color: #000; }
  /* Show the URL after external links so printed copies stay useful. */
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.8em; }
  .gallery-grid { gap: 0.5rem; }
  .gallery-item, .card { break-inside: avoid; }
  .media img { aspect-ratio: auto; }
  h1, h2, h3 { break-after: avoid; }
}
