/*!
 * amazing-ui - the Amazing platform design system.
 * Self-hosted, dependency-free, mobile-first.
 *
 * Rules of this file:
 * - Design tokens first; components consume tokens only.
 * - Automatic light/dark via color-scheme + light-dark().
 * - No inline styles anywhere in the platform (CSP: style-src 'self').
 * - Every selector exists because a shipped page uses it.
 *
 * Brand: warm gold over espresso, extracted from the signature logo
 * (champagne #FFF0A8 -> gold #FFD860 -> amber #C09030 on #301818).
 * Typography: Fraunces (editorial display serif) + Inter (UI), both
 * vendored as variable fonts - a single woff2 per family.
 */

/* ----------------------------------------------------------------- fonts */
@font-face {
  font-family: "Inter";
  src: url("../vendor/fonts/inter-latin-opsz-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Fraunces";
  src: url("../vendor/fonts/fraunces-latin-opsz-normal.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Fraunces";
  src: url("../vendor/fonts/fraunces-latin-opsz-italic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

/* ---------------------------------------------------------------- tokens */
:root {
  color-scheme: light dark;

  /* Surfaces - warm in both modes so the UI echoes the brand's espresso/gold
     instead of generic blue-gray. Photos sit on these, so they stay muted. */
  --bg: light-dark(#f6f3ec, #100d0a);
  --surface: light-dark(#fffefb, #181410);
  --surface-2: light-dark(#efeadf, #221c15);
  --border: light-dark(#ddd4c3, #372e21);

  /* Text */
  --text: light-dark(#211b12, #ede8de);
  --text-dim: light-dark(#6f6553, #a89d8c);

  /* Brand gold. Light mode deepens the logo gold for contrast on paper;
     dark mode keeps it luminous, close to the logo's #FFD860. */
  --accent: light-dark(#8a671a, #e8bd4e);
  --accent-contrast: light-dark(#ffffff, #241a08);

  /* The signature gradient (champagne -> gold -> amber), tone-mapped per
     scheme so gradient text stays readable on both backgrounds. */
  --grad-a: light-dark(#b18a28, #fff0a8);
  --grad-b: light-dark(#94701c, #ffd860);
  --grad-c: light-dark(#755713, #c09030);
  --brand-grad: linear-gradient(105deg, var(--grad-a) 0%, var(--grad-b) 48%, var(--grad-c) 100%);

  /* Semantic */
  --danger: light-dark(#b3261e, #f2b8b5);
  --danger-contrast: light-dark(#ffffff, #601410);
  --ok: light-dark(#1b7f4d, #6ddba1);

  /* Geometry */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-lg: 18px;
  --gap: clamp(0.75rem, 2vw, 1.25rem);
  --shell-max: 80rem;

  /* Elevation. Light mode: warm-tinted shadows, never pure black smudges.
     Dark mode: shadows are invisible on near-black, so depth comes from the
     dark-elevation convention instead - a LIGHTER raised surface plus deeper
     black shadows for the few surfaces that float (dialogs, toasts).
     light-dark() cannot wrap a whole shadow LIST (its two arguments are
     comma-separated), so the COLORS flip per scheme, not the lists. */
  --shadow-tint-a: light-dark(rgb(46 32 12 / 0.06), rgb(0 0 0 / 0.45));
  --shadow-tint-b: light-dark(rgb(46 32 12 / 0.06), rgb(0 0 0 / 0.35));
  --shadow-tint-c: light-dark(rgb(20 14 5 / 0.18), rgb(0 0 0 / 0.6));
  --shadow-tint-d: light-dark(rgb(20 14 5 / 0.22), rgb(0 0 0 / 0.5));
  --shadow-1: 0 1px 2px var(--shadow-tint-a), 0 4px 14px var(--shadow-tint-b);
  --shadow-2: 0 2px 6px var(--shadow-tint-c), 0 16px 40px var(--shadow-tint-d);

  /* Raised surfaces (cards, dialogs, toasts). Identical to --surface in
     light mode - paper already reads as lifted via shadow - but one step
     brighter in dark mode, where luminance IS elevation. */
  --surface-raised: light-dark(#fffefb, #1e1913);

  /* Type */
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* Motion */
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Manual appearance override ("Erscheinungsbild"). The whole palette is
   authored with light-dark(), so forcing the color-scheme flips every token
   at once - no second theme exists to drift. Without the attribute the
   system preference rules, exactly as before. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* Cross-page view transitions: a calm crossfade that makes navigating
   between gallery, timeline and album feel continuous instead of jumpy.
   Progressive enhancement - browsers without support simply navigate. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 180ms;
  animation-timing-function: var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* No page crossfade for motion-sensitive users either. */
  @view-transition { navigation: none; }
}

/* Skip link: invisible until focused, then the very first thing. */
.skip-link {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-start: 0.5rem;
  z-index: 100;
  padding: 0.5rem 1rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-300%);
}

.skip-link:focus-visible { transform: none; color: var(--accent-contrast); }

/* ----------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.55;
  min-height: 100dvh;
  font-optical-sizing: auto;
}

img, canvas, svg, video { display: block; max-width: 100%; }

h1, h2 {
  font-family: var(--font-display);
  font-weight: 580;
  letter-spacing: 0.005em;
  line-height: 1.15;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

h1 { font-size: clamp(1.7rem, 4vw, 2.4rem); }
h2 { font-size: clamp(1.25rem, 3vw, 1.6rem); }
h3 { font-size: 1.02rem; font-weight: 620; line-height: 1.25; margin: 0 0 0.5em; text-wrap: balance; }

/* pretty: the paragraph counterpart to the headings' balance - no orphan
   words on the last line. Browsers without support simply ignore it. */
p { margin: 0 0 1em; text-wrap: pretty; }

a { color: inherit; text-underline-offset: 0.2em; }
a:hover { color: var(--accent); }

code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-2); padding: 0.1em 0.35em; border-radius: var(--radius-sm); }

/* Disclosure summaries are buttons in prose clothing: the affordance
   (pointer + accent hover) belongs to the element, not to one component -
   the login's first-run <details> must invite exactly like the search help. */
summary { cursor: pointer; }
summary:hover { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

::selection { background: var(--accent); color: var(--accent-contrast); }

/* ----------------------------------------------------------------- shell */
.shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.shell-header {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--gap);
  padding: 0.65rem clamp(0.75rem, 3vw, 1.5rem);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.12rem;
  text-decoration: none;
  letter-spacing: 0.01em;
}

/* The hood icon next to the wordmark. */
.brand-icon { height: 1.7rem; width: auto; }

/* "Amazing" carries the signature gold gradient. */
.brand em {
  font-style: normal;
  background: var(--brand-grad);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

.brand:hover { color: inherit; }

.shell-nav { display: flex; align-items: center; gap: 0.4rem; margin-inline-start: auto; }

/* ------------------------------------------------------ primary nav links */
/* The content destinations (Galerie / Deine Fotos / Suche). Desktop only -
   on phones the bottom tab bar carries the same destinations natively. */
.shell-links { display: none; }

@media (min-width: 48rem) {
  .shell-links {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-inline-start: 0.75rem;
  }
}

.shell-links a {
  position: relative;
  padding: 0.4rem 0.75rem;
  border-radius: 999px;
  font-size: 0.93rem;
  font-weight: 560;
  color: var(--text-dim);
  text-decoration: none;
  transition: color 140ms var(--ease), background 140ms var(--ease);
}

.shell-links a:hover { color: var(--text); background: var(--surface-2); }

/* The active destination glows quietly in brand gold. */
.shell-links a.is-active { color: var(--accent); font-weight: 640; }

.shell-links a.is-active::after {
  content: "";
  position: absolute;
  inset-inline: 0.85rem;
  inset-block-end: 0.05rem;
  height: 2px;
  border-radius: 999px;
  background: var(--brand-grad);
}

/* ----------------------------------------------------------- header search */
/* Compact search that submits to /suche - pure GET form, works without JS. */
.shell-search { display: none; }

@media (min-width: 54rem) {
  .shell-search {
    display: flex;
    align-items: center;
    flex: 1;
    max-width: 19rem;
    margin-inline-start: auto;
  }

  /* When the search sits in the header the action cluster stops stretching. */
  .shell-search + .shell-nav { margin-inline-start: 0; }
}

.shell-search .search-box { width: 100%; }

/* Shared search input shape (header + search page hero). */
.search-box {
  position: relative;
  display: flex;
  align-items: center;
}

.search-box > svg {
  position: absolute;
  inset-inline-start: 0.8rem;
  width: 1rem;
  height: 1rem;
  color: var(--text-dim);
  pointer-events: none;
}

.search-box > input {
  width: 100%;
  padding: 0.45rem 0.9rem 0.45rem 2.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-size: 0.92rem;
  min-height: 2.25rem;
  transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease);
}

.search-box > input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

/* The search page hero variant: generous, inviting, unmistakably primary. */
.search-box--hero > input {
  font-size: 1.05rem;
  min-height: 3.1rem;
  padding-inline-start: 2.9rem;
  border-radius: var(--radius-lg);
}

.search-box--hero > svg {
  inset-inline-start: 1.05rem;
  width: 1.15rem;
  height: 1.15rem;
}

/* The palette's calling card inside the header search: teaches Strg+K where
   the eyes already are. Hidden on touch (kbd-hint) and while typing - the
   hint yields the moment the box is actually used. */
.search-kbd {
  position: absolute;
  inset-inline-end: 0.7rem;
  display: inline-flex;
  gap: 0.25ch;
  pointer-events: none;
  color: var(--text-dim);
}

.search-box:focus-within .search-kbd { display: none; }

/* Reserve room so placeholder and typed text never run under the hint. */
.shell-search .search-box > input { padding-inline-end: 4.6rem; }

/* ------------------------------------------------------------ mobile tabs */
/* Bottom tab bar: the mental model people bring from native photo apps.
   Same recipe as the action bar (fixed, blurred, safe-area aware). */
.tab-bar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 25;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  padding-block-start: 0.3rem;
  padding-block-end: max(0.3rem, env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px);
  border-block-start: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}

.tab-bar a {
  display: grid;
  justify-items: center;
  gap: 0.15rem;
  padding: 0.35rem 0.25rem;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--text-dim);
  text-decoration: none;
  border-radius: var(--radius-sm);
}

.tab-bar a svg { width: 1.35rem; height: 1.35rem; }

.tab-bar a.is-active { color: var(--accent); }
.tab-bar a:hover { color: var(--accent); }

/* The raised center tab: the native-app "create" affordance. The disc
   floats above the bar line; the label keeps tab rhythm. */
.tab-bar a.tab-add .tab-add-disc {
  display: grid;
  place-items: center;
  width: 2.55rem;
  height: 2.55rem;
  margin-block-start: -1.15rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-contrast);
  box-shadow: var(--shadow-1);
  transition: transform 140ms var(--ease);
}

.tab-bar a.tab-add .tab-add-disc svg { width: 1.2rem; height: 1.2rem; }
.tab-bar a.tab-add:active .tab-add-disc { transform: scale(0.94); }

@media (min-width: 48rem) {
  .tab-bar { display: none; }
}

/* Keep the last content rows reachable above the tab bar on phones. */
@media (max-width: 47.99rem) {
  .shell-main { padding-block-end: 6rem; }
  .shell-footer { padding-block-end: 5.5rem; }
}

.shell-main {
  flex: 1;
  width: 100%;
  max-width: var(--shell-max);
  margin-inline: auto;
  padding: var(--gap) clamp(0.75rem, 3vw, 1.5rem) calc(var(--gap) * 3);
}

/* Full-bleed variant for immersive gallery pages. */
.shell-main--full { max-width: none; padding-inline: clamp(0.25rem, 1vw, 0.75rem); }

.shell-footer {
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  padding: 2.75rem 1rem 2.25rem;
  color: var(--text-dim);
  font-size: 0.8rem;
  text-align: center;
}

/* The personal signature above the footer line - quiet, like an artist's
   mark on the back of a print. */
.footer-signature img { height: 2.1rem; width: auto; opacity: 0.75; }
.footer-signature:hover img { opacity: 1; transition: opacity 200ms var(--ease); }

/* Scheme-specific signature variants. The system preference decides by
   default; a manual data-theme override (higher specificity) wins over the
   media query - <picture> could only ever follow the system. */
.footer-signature .sig--dark { display: none; }

@media (prefers-color-scheme: dark) {
  .footer-signature .sig--light { display: none; }
  .footer-signature .sig--dark { display: block; }
}

:root[data-theme="light"] .footer-signature .sig--light { display: block; }
:root[data-theme="light"] .footer-signature .sig--dark { display: none; }
:root[data-theme="dark"] .footer-signature .sig--light { display: none; }
:root[data-theme="dark"] .footer-signature .sig--dark { display: block; }

/* ------------------------------------------------------- section headers */
/* Small-caps kicker above headings - the editorial voice of the platform. */
.eyebrow {
  display: block;
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-block-end: 0.4rem;
}

.section { margin-block: clamp(1.75rem, 4vw, 2.75rem); }
.section:first-child { margin-block-start: 0.5rem; }

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-block-end: 1.15rem;
}

.section-head h1, .section-head h2 { margin: 0; }

.section-head .section-meta { color: var(--text-dim); font-size: 0.9rem; }

/* Page header for detail pages (album, upload, settings). */
.page-head { margin-block: 0.75rem 1.5rem; }
.page-head h1 { margin-block-end: 0.15em; }
.page-head .page-sub { color: var(--text-dim); max-width: 60ch; margin: 0.25rem 0 0; }
.page-head .page-actions { margin-block-start: 0.9rem; }

/* --------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55ch;
  padding: 0.55rem 1.15rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-weight: 550;
  font-size: 0.95rem;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: background 140ms var(--ease), border-color 140ms var(--ease),
    box-shadow 140ms var(--ease), transform 60ms var(--ease);
  /* Comfortable touch target on phones without bloating desktop. */
  min-height: 2.5rem;
}

.btn svg { width: 1.05em; height: 1.05em; flex: none; }

.btn:hover { background: var(--surface-2); color: var(--text); box-shadow: var(--shadow-1); }
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; box-shadow: none; }

/* A label-as-button hosting a visually-hidden control (the file picker's
   "Fotos auswählen"): Tab focuses the INPUT, whose own focus ring paints
   on a 1px clipped box - invisible. Surface it on the button shell instead,
   the same :has() recipe .choice and .seg already use. */
.btn:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.btn--primary:hover {
  background: color-mix(in srgb, var(--accent) 86%, var(--text));
  border-color: color-mix(in srgb, var(--accent) 86%, var(--text));
  color: var(--accent-contrast);
}

.btn--danger { background: transparent; border-color: var(--danger); color: var(--danger); }
.btn--danger:hover { background: var(--danger); color: var(--danger-contrast); }

.btn--ghost { border-color: transparent; background: transparent; box-shadow: none; }
.btn--ghost:hover { background: var(--surface-2); box-shadow: none; }

/* Active section marker for header action buttons (Konto, Zugriff): the
   quiet gold voice of .shell-links a.is-active, adapted to button shape. */
.btn--ghost.is-active {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  font-weight: 640;
}

.btn--sm { padding: 0.32rem 0.85rem; min-height: 2rem; font-size: 0.88rem; }

/* One pending voice for the whole platform: amazing.js marks guarded POST
   submitters aria-busy, and the JS dialog flows set it on their primary
   buttons. A disabled button alone reads as DEAD on a slow connection -
   this ring says "working". The ::after joins the label as a flex item
   (buttons are inline-flex), spaced by the button's own gap: no text
   replacement, no layout tricks. Under reduced motion the global rule
   flattens the spin; a static ring still marks the state. */
.btn[aria-busy="true"] { cursor: progress; }

/* Busy outranks the disabled fade: 0.55 would dim the work signal into
   exactly the dead look the ring exists to prevent. */
.btn[aria-busy="true"]:disabled { opacity: 0.85; }

.btn[aria-busy="true"]::after {
  content: "";
  flex: none;
  width: 0.85em;
  height: 0.85em;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-block-end-color: transparent;
  animation: btn-spin 0.7s linear infinite;
}

@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* ----------------------------------------------------------------- forms */
.field { display: grid; gap: 0.35rem; margin-block-end: 1.1rem; }

fieldset.field { border: 0; padding: 0; margin-inline: 0; min-width: 0; }

.field > label, .field > legend, .field > .field-label { font-weight: 580; font-size: 0.92rem; padding: 0; }

.field > .hint { color: var(--text-dim); font-size: 0.82rem; }

.input, select.input, textarea.input {
  width: 100%;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  font: inherit;
  min-height: 2.6rem;
  transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease);
}

.input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

.input[aria-invalid="true"] { border-color: var(--danger); }

.validation-error { color: var(--danger); font-size: 0.85rem; }

.checkbox-row { display: flex; align-items: center; gap: 0.6ch; padding-block: 0.25rem; }
.checkbox-row input[type="checkbox"] { width: 1.15rem; height: 1.15rem; accent-color: var(--accent); }

/* Radio cards: visibility choices etc. as tactile, self-explaining options
   instead of a bare <select>. Pure CSS state via :has(). */
.choice-grid { display: grid; gap: 0.6rem; }

.choice {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 140ms var(--ease), background 140ms var(--ease);
}

.choice:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }

.choice:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}

.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Custom radio indicator, filled when checked. */
.choice-dot {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-block-start: 0.15rem;
  border: 2px solid var(--border);
  border-radius: 50%;
  transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease);
}

.choice:has(input:checked) .choice-dot {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 4px var(--accent);
}

.choice-body { display: grid; gap: 0.1rem; }
.choice-body strong { font-weight: 600; font-size: 0.95rem; }
.choice-body span { color: var(--text-dim); font-size: 0.82rem; }

/* ----------------------------------------------------------------- cards */
.card {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(1.1rem, 3vw, 1.75rem);
  box-shadow: var(--shadow-1);
}

.card + .card { margin-block-start: var(--gap); }

.card--narrow { max-width: 27rem; margin-inline: auto; }

/* Danger zone card: quietly separated, never screaming. */
.card--danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }

/* A single centered card for auth ceremonies (login, enroll). */
.ceremony {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: var(--gap);
  position: relative;
  isolation: isolate;
}

/* A faint golden aura behind the ceremony card - warm, not flashy. */
.ceremony::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(38rem 30rem at 50% 12%, color-mix(in srgb, var(--grad-b) 9%, transparent), transparent 70%);
}

.ceremony .card { width: min(100%, 27.5rem); }

/* Brand block at the top of ceremony cards. */
.ceremony-brand { display: grid; justify-items: center; gap: 0.65rem; margin-block-end: 1.4rem; text-align: center; }
.ceremony-brand img { height: 3.4rem; width: auto; }
.ceremony-brand .brand { font-size: 1.2rem; }

/* The quiet way out at the bottom of a ceremony card (enroll): a wrong
   account must never be trapped in a mandatory flow. */
.ceremony-exit {
  display: flex;
  align-items: center;
  gap: 0.6ch;
  margin-block-start: 1.1rem;
}

/* ---------------------------------------------------------------- tables */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

table.table { width: 100%; border-collapse: collapse; font-size: 0.94rem; }

.table th, .table td { text-align: left; padding: 0.65rem 1rem; border-block-end: 1px solid var(--border); }

.table tbody tr:last-child td { border-block-end: 0; }

.table tbody tr:hover { background: color-mix(in srgb, var(--accent) 4%, var(--surface)); }

.table th {
  font-weight: 640;
  color: var(--text-dim);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  background: var(--surface-2);
}

/* ---------------------------------------------------------------- badges */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45ch;
  padding: 0.12rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
}

/* Status dot in the badge's own color. */
.badge--ok, .badge--warn, .badge--danger { border-color: color-mix(in srgb, currentColor 40%, transparent); }
.badge--ok::before, .badge--warn::before, .badge--danger::before {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-radius: 50%;
  background: currentColor;
}

.badge--ok { color: var(--ok); }
.badge--warn { color: var(--accent); }
.badge--danger { color: var(--danger); }

/* ---------------------------------------------------------------- avatar */
/* Initials monogram: the album-blank recipe shrunk to a person. Humanizes
   grant lists and account rows without storing a single profile photo. */
.avatar {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 2.15rem;
  height: 2.15rem;
  border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--border));
  border-radius: 50%;
  background:
    radial-gradient(2.6rem 2.2rem at 30% 20%, color-mix(in srgb, var(--grad-b) 16%, transparent), transparent 75%),
    var(--surface-2);
  font-family: var(--font-display);
  font-size: 1rem;
  font-style: italic;
  font-weight: 580;
  user-select: none;
}

.avatar span {
  background: var(--brand-grad);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

/* ---------------------------------------------------------------- alerts */
.alert {
  display: flex;
  gap: 0.75ch;
  align-items: start;
  padding: 0.8rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-2);
  margin-block-end: 1rem;
}

/* Author display beats the UA [hidden] rule - restate it, or the login and
   enroll ceremonies show their pre-rendered error region as an empty husk
   before any error exists (same convention as .action-bar). */
.alert[hidden] { display: none; }

.alert--danger {
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  background: color-mix(in srgb, var(--danger) 7%, var(--surface));
  color: var(--danger);
}

.alert--ok {
  border-color: color-mix(in srgb, var(--ok) 50%, transparent);
  background: color-mix(in srgb, var(--ok) 7%, var(--surface));
  color: var(--ok);
}

/* ----------------------------------------------------- secret reveal box */
/* One-time temporary password display: large, selectable, copy-friendly. */
.secret {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: 1.25rem;
  letter-spacing: 0.06em;
  background: var(--surface-2);
  border: 1px dashed color-mix(in srgb, var(--accent) 50%, var(--border));
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  user-select: all;
}

/* --------------------------------------------------------- inspire hero */
/* The cinematic highlights carousel on the gallery start page. Slides are
   server-rendered anchors; inspire.js only orchestrates fade and drift.
   Transform+opacity only - fully compositable, no layout thrash. */
.hero {
  position: relative;
  height: clamp(24rem, 68svh, 46rem);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  /* Paint containment: the browser can skip everything offscreen inside. */
  contain: content;
}

.hero-slide {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  transition: opacity 1100ms var(--ease);
  pointer-events: none;
  z-index: 1;
}

.hero-slide.is-active { opacity: 1; pointer-events: auto; z-index: 2; }

/* Media wrapper so blurhash canvas and image drift together. */
.hero-media { position: absolute; inset: 0; }

.hero-media > canvas,
.hero-media > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ken-Burns drift: four directions, assigned round-robin by inspire.js so
   consecutive slides never move the same way. Slight overscale guarantees
   no edge reveal while translating. */
.hero-slide.is-active .hero-media { animation: 14s var(--ease-out) both; }
.hero-slide.kb-a.is-active .hero-media { animation-name: kb-a; }
.hero-slide.kb-b.is-active .hero-media { animation-name: kb-b; }
.hero-slide.kb-c.is-active .hero-media { animation-name: kb-c; }
.hero-slide.kb-d.is-active .hero-media { animation-name: kb-d; }

@keyframes kb-a { from { transform: scale(1.06) translate(1.2%, 0.8%); } to { transform: scale(1.14) translate(-1.2%, -0.8%); } }
@keyframes kb-b { from { transform: scale(1.14) translate(-1.2%, 0.8%); } to { transform: scale(1.06) translate(1.2%, -0.8%); } }
@keyframes kb-c { from { transform: scale(1.06) translate(-1%, -1%); } to { transform: scale(1.14) translate(1%, 1%); } }
@keyframes kb-d { from { transform: scale(1.14) translate(0%, -1.2%); } to { transform: scale(1.06) translate(0%, 1.2%); } }

/* Legibility scrim behind the caption, bottom-weighted. */
.hero-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgb(10 7 3 / 0.72) 0%, rgb(10 7 3 / 0.25) 32%, transparent 60%);
}

.hero-caption {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  display: grid;
  gap: 0.15rem;
  padding: clamp(1rem, 4vw, 2.25rem);
  color: #f7f2e8;
}

.hero-caption .eyebrow { color: var(--grad-b); margin-block-end: 0.2rem; }

.hero-title {
  font-family: var(--font-display);
  font-weight: 560;
  font-size: clamp(1.35rem, 3.4vw, 2.3rem);
  line-height: 1.12;
  text-wrap: balance;
  max-width: 24ch;
}

.hero-sub { font-size: 0.92rem; color: rgb(247 242 232 / 0.82); }

/* Hint that appears on hover: "click takes you into the album". */
.hero-open {
  display: inline-flex;
  align-items: center;
  gap: 0.5ch;
  margin-block-start: 0.55rem;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--grad-b);
  opacity: 0;
  translate: 0 4px;
  transition: opacity 300ms var(--ease), translate 300ms var(--ease);
}

.hero-open svg { width: 1em; height: 1em; }

.hero-slide:hover .hero-open, .hero-slide:focus-visible .hero-open { opacity: 1; translate: 0 0; }

/* Carousel UI: dots + pause, floating over the stage's bottom-right. */
.hero-ui {
  position: absolute;
  inset-block-end: clamp(0.9rem, 3vw, 1.6rem);
  inset-inline-end: clamp(0.9rem, 3vw, 1.6rem);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  z-index: 3;
}

/* No flex gap: each dot's own padding provides the rhythm AND the target
   spacing, so neighboring hit areas tile seamlessly without dead zones. */
.hero-dots { display: flex; }

.hero-dot {
  /* 0.55rem of paint on a ~25px button: the padding is pure hit area
     (WCAG 2.5.8 minimum target size - these were 9px and genuinely hard
     to tap), background-clip keeps the pill on the content box, and
     content-box sizing lets the is-active width morph keep speaking in
     dot dimensions. */
  box-sizing: content-box;
  width: 0.55rem;
  height: 0.55rem;
  padding: 0.5rem;
  border: 0;
  border-radius: 999px;
  background: rgb(247 242 232 / 0.42);
  background-clip: content-box;
  cursor: pointer;
  transition: background 200ms var(--ease), width 250ms var(--ease);
}

.hero-dot.is-active { width: 1.5rem; background: var(--grad-b); }

.hero-ctl {
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.45);
  color: #f7f2e8;
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background 200ms var(--ease);
}

.hero-ctl:hover { background: rgb(10 7 3 / 0.7); }
.hero-ctl svg { width: 0.95rem; height: 0.95rem; }

/* The play/pause glyphs swap via the host's data-state - no JS re-render. */
.hero-ctl .i-play { display: none; }
[data-inspire-state="paused"] .hero-ctl .i-pause { display: none; }
[data-inspire-state="paused"] .hero-ctl .i-play { display: block; }

/* ------------------------------------------------------------ photo grid */
/* Mosaic grid: squares by default; wide/tall images span cells so every
   photo keeps presence without JS layout. Gap kept tight: photos are the UI. */
.pg {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  grid-auto-rows: 9.5rem;
  grid-auto-flow: dense;
  gap: 0.375rem;
}

@media (min-width: 48rem) {
  .pg { grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); grid-auto-rows: 13rem; }
}

.pg-item {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  /* Contain layout+paint: large grids stay cheap to scroll. */
  contain: content;
}

.pg-item--wide { grid-column: span 2; }
.pg-item--tall { grid-row: span 2; }

@media (min-width: 48rem) {
  .pg-item--pano { grid-column: span 3; }
}

.pg-img, .pg-ph {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pg-ph { transform: scale(1.05); }

.pg-img { opacity: 0; transition: opacity 240ms var(--ease); }
.pg-img.is-loaded { opacity: 1; }

.pg-item:hover .pg-img { transform: scale(1.02); transition: transform 300ms var(--ease), opacity 240ms var(--ease); }

/* Deep-link highlight: when the inspire hero drops a visitor onto a photo,
   the tile pulses a golden ring once so the eye lands instantly. */
.pg-item.is-flash { animation: pg-flash 2.2s var(--ease) both; z-index: 1; }

@keyframes pg-flash {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
  18% { box-shadow: 0 0 0 4px var(--accent); }
  70% { box-shadow: 0 0 0 4px var(--accent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
}

/* Honest failure mark: a thumbnail whose derivative never arrived keeps
   its blurhash, but the tile must not pretend forever - a quiet corner
   chip (the .pg-fav recipe, opposite corner) names the state. amazing.js
   adds the class from the image's error event; rare path by design. */
.pg-item.is-broken::after {
  content: "!";
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-end: 0.45rem;
  display: grid;
  place-items: center;
  width: 1.45rem;
  height: 1.45rem;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.55);
  color: #f7f2e8;
  font-size: 0.85rem;
  font-weight: 700;
  backdrop-filter: blur(4px);
}

/* Hover caption: titled photos whisper their name on a bottom gradient
   (the album-card recipe). Hidden on touch - tap opens the lightbox with
   the full caption anyway, and gradients over thumbs cost legibility. */
.pg-cap {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: 1.6rem 0.65rem 0.5rem;
  background: linear-gradient(transparent, rgb(10 7 3 / 0.62));
  color: #f7f2e8;
  font-size: 0.78rem;
  font-weight: 560;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  translate: 0 4px;
  transition: opacity 220ms var(--ease), translate 220ms var(--ease);
}

@media (hover: hover) {
  .pg-item:hover .pg-cap, .pg-item:focus-visible .pg-cap { opacity: 1; translate: 0 0; }
}

/* Favorite badge: a quiet golden star, only on favorited tiles. */
.pg-fav {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-start: 0.45rem;
  display: none;
  place-items: center;
  width: 1.45rem;
  height: 1.45rem;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.5);
  color: var(--grad-b);
  backdrop-filter: blur(4px);
}

.pg-fav svg { width: 0.8rem; height: 0.8rem; }

.pg-item[data-favorite] .pg-fav { display: grid; }

/* Favorite as a TILE shortcut (hover-capable pointers only): the badge
   doubles as the toggle, so the most frequent micro-action skips the
   lightbox round trip. The span stays aria-hidden decoration - gallery.js
   intercepts the click; keyboard and touch users keep the fully accessible
   star in the lightbox toolbar. A shortcut, never the only path. */
@media (hover: hover) {
  .pg-item:hover .pg-fav,
  .pg-item:focus-visible .pg-fav { display: grid; }

  .pg-fav {
    cursor: pointer;
    transition: transform 160ms var(--ease-out), color 160ms var(--ease),
      background 160ms var(--ease);
  }

  .pg-fav:hover { color: var(--grad-a); background: rgb(10 7 3 / 0.7); transform: scale(1.1); }
}

/* Not-yet-favorited tiles whisper a hollow star while hovered - the state
   is readable BEFORE the click on TWO channels, because one proved too
   subtle at 0.8rem: fill (outline vs. solid) AND color (neutral white
   invitation vs. gold state). A gold-ish star on every hovered tile read
   as "this is already a favorite". */
.pg-item:not([data-favorite]) .pg-fav svg path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linejoin: round;
}

.pg-item:not([data-favorite]) .pg-fav { color: #f7f2e8; }

/* Hovering the badge itself previews the outcome: the glyph warms to gold
   ("this click makes it a favorite"). Outranks the base hover recolor. */
@media (hover: hover) {
  .pg-item:not([data-favorite]) .pg-fav:hover { color: var(--grad-b); }
}

/* One soft pop when the state flips - physics, not teleportation. */
.pg-fav.is-pop { animation: fav-pop 320ms var(--ease-out); }

@keyframes fav-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.35); }
  100% { transform: scale(1); }
}

/* --------------------------------------------------------- grid density */
/* "Rasterdichte": a personal preference applied via <html data-density>.
   Only the track sizing changes - the mosaic logic stays untouched. */
:root[data-density="compact"] .pg {
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  grid-auto-rows: 7rem;
  gap: 0.25rem;
}

:root[data-density="large"] .pg {
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  grid-auto-rows: 12rem;
}

@media (min-width: 48rem) {
  :root[data-density="compact"] .pg {
    grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    grid-auto-rows: 9.5rem;
  }

  :root[data-density="large"] .pg {
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    grid-auto-rows: 17rem;
  }
}

/* ---------------------------------------------------- album manage mode */
/* Owner curation inside the album grid: while active, tiles stop opening
   the lightbox and instead offer drag-reorder plus per-photo actions. */
.pg.is-manage .pg-item { cursor: grab; }
.pg.is-manage .pg-item:active { cursor: grabbing; }
.pg.is-manage .pg-cap { display: none; }

/* Curation owns every tile click - the favorite shortcut steps aside. */
.pg.is-manage .pg-fav { display: none; }

.pg.is-manage .pg-item.is-dragging {
  opacity: 0.35;
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
}

.pg.is-manage .pg-item.is-drop-target { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Per-tile curation actions (cover / delete), revealed in manage mode. */
.pg-manage {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-end: 0.45rem;
  display: none;
  gap: 0.3rem;
}

.pg.is-manage .pg-manage { display: flex; }

.pg-manage button {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.55);
  color: #f7f2e8;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background 140ms var(--ease), color 140ms var(--ease);
}

.pg-manage button:hover { background: rgb(10 7 3 / 0.8); color: var(--grad-b); }
.pg-manage button svg { width: 0.95rem; height: 0.95rem; }
.pg-manage button[data-cover-set] { color: var(--grad-b); }

/* --------------------------------------------------- gallery select mode */
/* "Auswählen" on the timeline, in search results and inside albums: while
   active, tiles stop opening the lightbox and toggle membership in a
   selection instead (select-mode.js drives it; the bulk verbs live in the
   shared action bar). Same mode recipe as album curation - browsing stays
   browsing. */

/* Album pages host BOTH modes over the same grid (curation + selection).
   They are rivals by design; while one is active the other's entry
   affordance steps aside - the cleanest mutual exclusion is not offering
   the conflict in the first place. */
body:has(.pg.is-manage) [data-select-toggle],
body:has(.pg.is-select) [data-manage-toggle] { display: none; }

/* Long-press is a first-class gesture on selectable surfaces: the iOS link
   callout would fight it mid-press, so it yields here (and only here). */
[data-select-host] .pg-item { -webkit-touch-callout: none; }

.pg.is-select .pg-item { cursor: pointer; }
.pg.is-select .pg-cap { display: none; }

/* Selecting owns the tile corners: the favorite badge yields to the check. */
.pg.is-select .pg-fav { display: none; }

/* The check circle: present on EVERY tile while selecting (the affordance
   is visible BEFORE the first tap - the workbench convention), filled once
   picked. Injected by select-mode.js, pure decoration to the AT layer. */
.pg-select-check {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-start: 0.45rem;
  z-index: 1;
  display: none;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.45);
  color: transparent;
  border: 2px solid rgb(247 242 232 / 0.85);
  backdrop-filter: blur(4px);
  transition: background 140ms var(--ease), color 140ms var(--ease);
}

.pg-select-check svg { width: 0.85rem; height: 0.85rem; }

.pg.is-select .pg-select-check { display: grid; }

.pg-item.is-picked .pg-select-check {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

/* Picked tiles settle back like a pressed key - the iOS Photos gesture.
   The transform needs its own transition here: the base .pg-img only
   animates opacity (fade-in), and a snap would break the press illusion. */
.pg.is-select .pg-img,
.pg.is-select .pg-ph { transition: transform 200ms var(--ease), opacity 240ms var(--ease); }

.pg.is-select .pg-item.is-picked .pg-img,
.pg.is-select .pg-item.is-picked .pg-ph { transform: scale(0.94); }

.pg.is-select .pg-item.is-picked {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
}

/* ------------------------------------------------------------ album cards */
.albums { display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: var(--gap); }

.album-card {
  position: relative;
  display: block;
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 4 / 3;
  background: var(--surface-2);
  text-decoration: none;
  box-shadow: var(--shadow-1);
  contain: content;
  transition: box-shadow 300ms var(--ease), translate 300ms var(--ease);
}

.album-card:hover { box-shadow: var(--shadow-2); translate: 0 -3px; }

.album-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 700ms var(--ease-out);
}

.album-card:hover img { transform: scale(1.055); }

/* Cover-less album: an editorial monogram instead of an empty gray box. */
.album-blank {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  font-family: var(--font-display);
  font-size: 4.5rem;
  font-weight: 560;
  font-style: italic;
  background:
    radial-gradient(18rem 14rem at 30% 20%, color-mix(in srgb, var(--grad-b) 14%, transparent), transparent 70%),
    var(--surface-2);
}

.album-blank span {
  background: var(--brand-grad);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
}

.album-card .album-meta {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  display: grid;
  gap: 0.1rem;
  padding: 2.6rem 1.1rem 0.9rem;
  background: linear-gradient(transparent, rgb(10 7 3 / 0.38) 40%, rgb(10 7 3 / 0.78));
  color: #f7f2e8;
}

.album-meta h2 { margin: 0; font-size: 1.14rem; font-weight: 560; }

/* One-line description: editorial texture between title and facts. */
.album-meta .album-desc {
  font-size: 0.82rem;
  color: rgb(247 242 232 / 0.85);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.album-meta .album-info { display: flex; align-items: center; gap: 0.6ch; font-size: 0.8rem; color: rgb(247 242 232 / 0.8); }
.album-meta .album-info svg { width: 0.85em; height: 0.85em; }

/* Slide-in arrow chip on hover - "this card goes somewhere". */
.album-arrow {
  position: absolute;
  inset-block-start: 0.8rem;
  inset-inline-end: 0.8rem;
  display: grid;
  place-items: center;
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.5);
  color: var(--grad-b);
  backdrop-filter: blur(6px);
  opacity: 0;
  translate: 0 6px;
  transition: opacity 250ms var(--ease), translate 250ms var(--ease);
}

.album-arrow svg { width: 1rem; height: 1rem; }

.album-card:hover .album-arrow, .album-card:focus-visible .album-arrow { opacity: 1; translate: 0 0; }

/* ------------------------------------------------------------- album hero */
/* The album page opener: the cover breathes behind the title like the
   first page of a book chapter. A positioned <img> plus scrim keeps the
   block CSP-safe - no background-image, no inline styles. */
.album-hero {
  position: relative;
  display: grid;
  align-content: end;
  min-height: clamp(13rem, 34svh, 21rem);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-block-end: 1.1rem;
  background: var(--surface-2);
  box-shadow: var(--shadow-1);
  contain: content;
}

.album-hero > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Shared-element morph target: the clicked album card hands its cover
     over to this banner (gallery.js sets the name on the outgoing card). */
  view-transition-name: album-cover;
}

/* Bottom-weighted legibility scrim - same recipe as the inspire stage. */
.album-hero .hero-scrim {
  background: linear-gradient(to top, rgb(10 7 3 / 0.78) 0%, rgb(10 7 3 / 0.32) 45%, rgb(10 7 3 / 0.05) 75%, transparent 100%);
}

.album-hero-body {
  position: relative;
  display: grid;
  gap: 0.2rem;
  padding: clamp(1rem, 3.5vw, 1.75rem);
  color: #f7f2e8;
}

.album-hero-body .eyebrow { color: var(--grad-b); margin-block-end: 0.1rem; }
.album-hero-body h1 { margin: 0; color: inherit; }

.album-hero-body .album-hero-sub {
  margin: 0.15rem 0 0;
  max-width: 62ch;
  color: rgb(247 242 232 / 0.88);
}

.album-hero-body .album-hero-facts { font-size: 0.85rem; color: rgb(247 242 232 / 0.72); }

.album-hero-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-block-start: 0.6rem; }

/* The Rückblick year hero is a LINK into its moment: it must invite like
   one (breathing cover on hover) without breaking the poster look. */
a.album-hero { text-decoration: none; }
a.album-hero > img { transition: transform 700ms var(--ease-out); }
a.album-hero:hover > img { transform: scale(1.03); }
a.album-hero .hero-title {
  display: block;
  font-family: var(--font-display);
  font-weight: 560;
  font-size: clamp(1.35rem, 3.4vw, 2.3rem);
  line-height: 1.12;
  text-wrap: balance;
}

/* ------------------------------------------------------------ empty state */
/* Crafted, welcoming emptiness - the first thing a new user ever sees. */
.empty {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: 0.4rem;
  padding: clamp(2.5rem, 8vw, 5rem) 1.5rem;
}

.empty-art { margin-block-end: 1rem; color: var(--text-dim); }
.empty-art .art-accent { color: var(--accent); }

.empty h2 { margin-block-end: 0.2em; }

.empty p { color: var(--text-dim); max-width: 44ch; margin-block-end: 0.75rem; }

.empty .btn { margin-block-start: 0.5rem; }

/* Author display beats the UA [hidden] rule - restate it, or the filter
   nomatch panel on /verwalten could never hide (see .action-bar). */
.empty[hidden] { display: none; }

/* Panel variant: an empty state INSIDE an otherwise populated page (e.g. the
   albums section while the shelves above already show photos). The dashed
   frame reads as "something will appear here" - present, not shouting. */
.empty--panel {
  padding: clamp(1.75rem, 4vw, 2.75rem) 1.5rem;
  border: 1px dashed color-mix(in srgb, var(--border) 85%, transparent);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.empty--panel .empty-art { margin-block-end: 0.5rem; }

/* --------------------------------------------------------------- dropzone */
.dropzone {
  display: grid;
  place-items: center;
  gap: 0.55rem;
  min-height: 13rem;
  border: 2px dashed var(--border);
  border-radius: var(--radius-lg);
  color: var(--text-dim);
  text-align: center;
  padding: 2rem 1.5rem;
  background: var(--surface);
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}

.dropzone svg { width: 2.4rem; height: 2.4rem; color: var(--accent); }

.dropzone strong { color: var(--text); font-size: 1.05rem; font-weight: 600; }

.dropzone.is-drag {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}

.upload-list { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.5rem; }

/* Summary row above the list: the aggregate count ("3 von 12 fertig") the
   individual progress bars cannot give, plus the collective escape hatch
   ("Alle abbrechen") while transfers are still stoppable. */
.upload-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-block-start: 1rem;
}

/* Author display beats the UA [hidden] rule - restate it (see .action-bar). */
.upload-controls[hidden] { display: none; }

.upload-summary { margin: 0; font-weight: 580; font-size: 0.92rem; }

.upload-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-rows: auto auto;
  align-items: center;
  gap: 0.25rem 0.75rem;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
}

/* Local preview thumb: instant recognition of WHICH photo each row is. */
.upload-thumb {
  grid-row: 1 / 3;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--surface-2);
}

.upload-name {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.upload-status { display: inline-flex; align-items: center; gap: 0.6ch; justify-self: end; white-space: nowrap; }

/* Failed rows offer recovery in place - never "find the file again". */
.upload-retry {
  padding: 0.1rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--accent);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 140ms var(--ease), background 140ms var(--ease);
}

.upload-retry:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }

/* In-flight escape hatch: while bytes still leave the device, each row can
   stop its own transfer. Quiet by default, decisive on hover - cancelling
   is a way out, never a call to action. */
.upload-cancel {
  padding: 0.1rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-dim);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 140ms var(--ease), color 140ms var(--ease);
}

.upload-cancel:hover { border-color: var(--danger); color: var(--danger); }

.upload-item progress { grid-column: 2 / -1; width: 100%; height: 0.35rem; accent-color: var(--accent); }

/* ----------------------------------------------------------------- toast */
.toast-host {
  position: fixed;
  inset-block-end: max(1rem, env(safe-area-inset-bottom));
  inset-inline: 0;
  display: grid;
  justify-items: center;
  gap: 0.5rem;
  z-index: 100;
  pointer-events: none;
}

/* The viewport's bottom edge can be occupied: by the tab bar on phones and
   by the bulk action bar during an active selection. Toasts step ABOVE
   those bars instead of covering their verbs - an undo button must never
   sit on top of "Aufheben", and the raised "Neu" tab stays tappable.
   The action-bar rule out-ranks the tab-bar rule by specificity, matching
   the bars' own stacking (the selection bar overlays the tab bar). */
@media (max-width: 47.99rem) {
  body:has(.tab-bar) .toast-host {
    /* Bar (3.4rem) plus the protruding center disc (~1rem) plus air. */
    inset-block-end: calc(4.75rem + env(safe-area-inset-bottom));
  }

  body:has(.action-bar:not([hidden])) .toast-host {
    /* The action bar may wrap to two button rows on narrow screens. */
    inset-block-end: calc(7rem + env(safe-area-inset-bottom));
  }
}

@media (min-width: 48rem) {
  body:has(.action-bar:not([hidden])) .toast-host {
    inset-block-end: calc(4.5rem + env(safe-area-inset-bottom));
  }
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: var(--shadow-2);
  padding: 0.55rem 1.2rem;
  font-size: 0.92rem;
  animation: toast-in 200ms var(--ease);
  /* The whole toast is tap-to-dismiss (amazing.js); its timer also pauses
     under the pointer, so reading never races the auto-expiry. */
  cursor: pointer;
}

/* Optional inline action ("Rückgängig"): quiet gold, generous hit area. */
.toast-action {
  margin: -0.3rem -0.5rem -0.3rem 0;
  padding: 0.3rem 0.6rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 640;
  cursor: pointer;
  white-space: nowrap;
}

.toast-action:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }

@keyframes toast-in {
  from { opacity: 0; translate: 0 0.5rem; }
  to { opacity: 1; translate: 0 0; }
}

/* ------------------------------------------------------------ footer nav */
.footer-nav { display: flex; gap: 1.25rem; }
.footer-nav a { color: var(--text-dim); text-decoration: none; font-weight: 550; }
.footer-nav a:hover { color: var(--accent); text-decoration: underline; }

/* ----------------------------------------------------------- grant lists */
/* Album sharing: one row per account, revocation always within reach. */
.grant-list { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.45rem; }

.grant-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.grant-name { display: grid; gap: 0.05rem; min-width: 0; }
.grant-name strong { font-weight: 600; font-size: 0.95rem; }

/* ----------------------------------------------------------- account page */
.account-grid { display: grid; gap: var(--gap); margin-block-end: 0.5rem; }

@media (min-width: 48rem) {
  .account-grid { grid-template-columns: 3fr 2fr; align-items: start; }
}

/* Key/value identity facts, copy affordances inline. */
.info-list { margin: 0 0 1.25rem; display: grid; gap: 0.85rem; }
.info-row { display: grid; gap: 0.15rem; }
.info-row dt { font-size: 0.78rem; font-weight: 640; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); }
.info-row dd { margin: 0; display: grid; gap: 0.25rem; }
.info-value { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.info-row .hint { color: var(--text-dim); font-size: 0.82rem; max-width: 46ch; }

.stat-row { display: flex; gap: 2rem; margin-block: 0.5rem 1rem; }
.stat { display: grid; gap: 0.1rem; }
.stat strong { font-family: var(--font-display); font-size: 1.9rem; font-weight: 580; line-height: 1; }
.stat span { color: var(--text-dim); font-size: 0.85rem; }

/* Keyboard hint line: pointless on touch-only devices, hidden there. */
kbd {
  font-family: var(--font-mono);
  font-size: 0.78em;
  padding: 0.1em 0.45em;
  border: 1px solid var(--border);
  border-block-end-width: 2px;
  border-radius: var(--radius-sm);
  background: var(--surface);
}

@media (hover: none) {
  .kbd-hint { display: none; }

  /* The header search reserves trailing room for the Strg+K pill; the pill
     is gone here (above), so typed text gets the field's real edge back -
     no dead 4.6rem on iPads. Mirrors the input's base inline padding. */
  .shell-search .search-box > input { padding-inline-end: 0.9rem; }
}

/* ---------------------------------------------------- photo library (lib) */
/* The personal management grid: tiles select on tap, metadata below the
   image, actions in the corner. Distinct from .pg on purpose - this is a
   workbench, not a gallery. */
.lib {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 0.6rem;
}

@media (min-width: 48rem) {
  .lib { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); }
}

.lib-tile {
  position: relative;
  display: grid;
  gap: 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 0.45rem;
  transition: border-color 140ms var(--ease), box-shadow 140ms var(--ease);
  contain: content;
}

.lib-tile.is-selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* The selectable media area - a real button, so Space/Enter work for free. */
.lib-media {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  cursor: pointer;
}

.lib-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 200ms var(--ease), opacity 200ms var(--ease);
}

.lib-tile.is-selected .lib-media img { transform: scale(0.94); opacity: 0.85; }

/* Placeholder for photos still processing (or failed). */
.lib-pending {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--text-dim);
  font-size: 0.8rem;
  padding: 0.5rem;
  text-align: center;
}

/* Selection checkmark: always present as a quiet circle, filled when
   selected - the affordance is visible BEFORE the first tap. */
.lib-check {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-start: 0.45rem;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.45);
  color: transparent;
  border: 2px solid rgb(247 242 232 / 0.85);
  backdrop-filter: blur(4px);
  transition: background 140ms var(--ease), color 140ms var(--ease);
}

.lib-check svg { width: 0.85rem; height: 0.85rem; }

.lib-tile.is-selected .lib-check {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.lib-meta { display: grid; gap: 0.15rem; min-width: 0; }

.lib-title {
  font-size: 0.88rem;
  font-weight: 580;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.lib-sub {
  font-size: 0.76rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Recovery hint on failed uploads: unlike the one-line subtitle it wraps
   freely - on an error path, guidance beats grid rhythm. */
.lib-hint { font-size: 0.74rem; line-height: 1.35; color: var(--text-dim); }

.lib-labels { display: flex; flex-wrap: wrap; gap: 0.25rem; min-height: 0; }

/* Corner actions (open in album / edit). */
.lib-actions {
  position: absolute;
  inset-block-start: 0.65rem;
  inset-inline-end: 0.65rem;
  display: flex;
  gap: 0.3rem;
}

.lib-action {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.5);
  color: #f7f2e8;
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background 140ms var(--ease);
}

.lib-action:hover { background: rgb(10 7 3 / 0.75); color: var(--grad-b); }
.lib-action svg { width: 0.95rem; height: 0.95rem; }

.lib-more { display: grid; justify-items: center; margin-block-start: 1.25rem; }

/* ------------------------------------------------------------ label chips */
.label-chip {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  padding: 0.08rem 0.55rem;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--text);
  font-size: 0.72rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.label-chip--more { border-style: dashed; color: var(--text-dim); background: transparent; }

/* Removable variant inside chip editors. */
.label-chip--edit { cursor: pointer; font: inherit; font-size: 0.8rem; }
.label-chip--edit:hover { border-color: var(--danger); color: var(--danger); }

.chip-editor {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.chip-editor:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

.chip-host { display: contents; }

.chip-input {
  flex: 1;
  min-width: 10ch;
  border: 0;
  padding: 0.25rem 0.15rem;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.92rem;
}

.chip-input:focus-visible { outline: none; }

/* -------------------------------------------------------- bulk action bar */
/* Sticky bottom bar that appears with a selection. Safe-area aware so it
   never hides behind an iPhone home indicator. */
/* Author display beats the UA [hidden] rule at equal specificity, so the
   hidden state must be restated explicitly - otherwise the bar never hides. */
.action-bar[hidden] { display: none; }

/* Same rule for every element the platform hides programmatically while
   an author display would otherwise win (filtered tiles, fallback links,
   optimistically removed grid tiles awaiting their undo window, the
   unsaved-changes badge on /access). */
.btn[hidden], .lib-tile[hidden], .pg-item[hidden], .badge[hidden] { display: none; }

/* Keep the last content rows reachable while the bar overlays the bottom. */
body:has(.action-bar:not([hidden])) .shell-main { padding-block-end: 8rem; }

.action-bar {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.65rem clamp(0.75rem, 3vw, 1.5rem);
  padding-block-end: max(0.65rem, env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px);
  border-block-start: 1px solid var(--border);
  animation: action-bar-in 200ms var(--ease);
}

@keyframes action-bar-in {
  from { opacity: 0; translate: 0 0.5rem; }
  to { opacity: 1; translate: 0 0; }
}

.action-count { font-weight: 620; font-size: 0.95rem; }

/* ----------------------------------------------------------------- dialogs */
.dlg {
  width: min(92vw, 26rem);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  color: var(--text);
  padding: clamp(1.1rem, 3vw, 1.5rem);
  box-shadow: var(--shadow-2);
}

.dlg::backdrop { background: rgb(10 7 3 / 0.55); backdrop-filter: blur(3px); }

/* Dialog entrance: every other surface eases into place (toasts, action
   bar, sheets) - dialogs join them instead of teleporting in. Entry-only
   BY DESIGN: closing stays instant (dismissal must feel immediate, and
   exit animation through the top layer is still inconsistent across
   engines). Browsers without @starting-style keep the instant open; the
   global reduced-motion rule flattens the element's transition. */
.dlg[open] {
  transition: opacity 200ms var(--ease), translate 240ms var(--ease-out);
}

@starting-style {
  .dlg[open] { opacity: 0; translate: 0 0.5rem; }
}

/* ::backdrop resolves custom properties only in very recent engines, so
   its curve stays literal - the same bezier as --ease. */
.dlg[open]::backdrop {
  transition:
    background 240ms cubic-bezier(0.2, 0, 0, 1),
    backdrop-filter 240ms cubic-bezier(0.2, 0, 0, 1);
}

@starting-style {
  .dlg[open]::backdrop { background: rgb(10 7 3 / 0); backdrop-filter: blur(0); }
}

.dlg h2 { margin-block-end: 0.75rem; }

/* ------------------------------------------------------------ hero arrows */
/* Explicit prev/next controls: keyboard- and touch-friendly companions to
   the dots. Styled like the pause control so the hero UI reads as one set. */
.hero-arrow {
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgb(10 7 3 / 0.45);
  color: #f7f2e8;
  cursor: pointer;
  backdrop-filter: blur(6px);
  transition: background 200ms var(--ease);
}

.hero-arrow:hover { background: rgb(10 7 3 / 0.7); }
.hero-arrow svg { width: 0.95rem; height: 0.95rem; }

/* Horizontal swipes belong to the carousel; vertical scrolling stays native. */
.hero { touch-action: pan-y; }

/* ---------------------------------------------------------- timeline (tl) */
/* "Deine Fotos": month sections with sticky editorial headers give the
   endless photo stream a narrative rhythm instead of a wall. */
.tl-month {
  margin-block-end: clamp(1.5rem, 3vw, 2.25rem);
  /* Anchor and date-jump landings settle below the sticky bars. */
  scroll-margin-block-start: 4.5rem;
}

/* Offscreen months skip style/layout/paint entirely - the difference
   between "scrolls" and "flies" on thousand-photo timelines. timeline.js
   adds the class only AFTER measuring the real height and pinning it via
   an element-level contain-intrinsic-size, so the scrubber's px-projection
   of the document never skews (the box is identical, rendered or skipped). */
.tl-month.cv-auto { content-visibility: auto; }

.tl-head {
  position: sticky;
  /* Sits right below the blurred shell header; same blur recipe so the
     two bars visually merge while scrolling. */
  inset-block-start: 3.35rem;
  z-index: 10;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin-block-end: 0.65rem;
  padding-block: 0.45rem;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px);
}

.tl-head h2 {
  margin: 0;
  font-size: clamp(1.05rem, 2.4vw, 1.35rem);
}

.tl-head .tl-count { color: var(--text-dim); font-size: 0.82rem; }

/* Year jump rail: quiet vertical scrubbing on wide screens, pure anchors. */
.tl-rail {
  display: none;
  position: fixed;
  inset-block-start: 50%;
  inset-inline-end: 0.65rem;
  translate: 0 -50%;
  z-index: 15;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.4rem 0.3rem;
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px);
}

@media (min-width: 72rem) {
  .tl-rail { display: flex; }
}

.tl-rail a {
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 640;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  text-decoration: none;
  transition: color 140ms var(--ease), background 140ms var(--ease);
}

.tl-rail a:hover { color: var(--accent); background: var(--surface-2); }

/* ------------------------------------------------------------- date jump */
/* Mobile companion to the desktop scrubber: a floating calendar button
   opens the year/month jump sheet. Hidden where the scrubber takes over. */
.tl-jump {
  position: fixed;
  inset-inline-end: 1rem;
  /* Clears the bottom tab bar on phones. */
  inset-block-end: calc(4.25rem + env(safe-area-inset-bottom));
  z-index: 24;
  display: grid;
  place-items: center;
  width: 2.9rem;
  height: 2.9rem;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  color: var(--accent);
  cursor: pointer;
  box-shadow: var(--shadow-1);
}

.tl-jump svg { width: 1.25rem; height: 1.25rem; }
.tl-jump[hidden] { display: none; }

/* Tablet band without a tab bar: hug the corner. */
@media (min-width: 48rem) {
  .tl-jump { inset-block-end: 1.25rem; }
}

/* The scrubber owns wide screens. */
@media (min-width: 72rem) {
  .tl-jump { display: none; }
}

/* Jump sheet content: years as headings, months as tappable chips. */
.jump-years {
  display: grid;
  gap: 1rem;
  max-height: min(55vh, 24rem);
  overflow-y: auto;
  padding-block-end: 0.25rem;
}

.jump-year h3 { margin: 0 0 0.45rem; font-variant-numeric: tabular-nums; }

.jump-months { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.jump-month {
  padding: 0.28rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 580;
  cursor: pointer;
  transition: border-color 140ms var(--ease), background 140ms var(--ease);
}

.jump-month:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
}

/* ------------------------------------------------------------ on this day */
/* "An diesem Tag": a warm rediscovery strip on the gallery start page.
   Horizontal scroll on purpose - it is a shelf of memories, not a grid. */
.otd { margin-block: clamp(1.5rem, 4vw, 2.5rem); }

.otd-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(11rem, 14rem);
  gap: 0.6rem;
  overflow-x: auto;
  padding-block-end: 0.5rem;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
}

.otd-card {
  position: relative;
  display: block;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
  scroll-snap-align: start;
  box-shadow: var(--shadow-1);
  contain: content;
  transition: box-shadow 300ms var(--ease), translate 300ms var(--ease);
}

.otd-card:hover { box-shadow: var(--shadow-2); translate: 0 -3px; }

.otd-card img, .otd-card canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.otd-card img { transition: transform 700ms var(--ease-out); }
.otd-card:hover img { transform: scale(1.05); }

/* Year badge: "Vor 3 Jahren" floats like a postmark. */
.otd-badge {
  position: absolute;
  inset-block-start: 0.6rem;
  inset-inline-start: 0.6rem;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  background: rgb(10 7 3 / 0.55);
  color: var(--grad-b);
  font-size: 0.72rem;
  font-weight: 640;
  backdrop-filter: blur(6px);
}

.otd-card .otd-meta {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: 1.8rem 0.75rem 0.65rem;
  background: linear-gradient(transparent, rgb(10 7 3 / 0.72));
  color: #f7f2e8;
  font-size: 0.8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --------------------------------------------------------------- toolbar */
/* Sub-header control row (timeline density, library filters, search rail). */
.toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-block: 0.75rem 1.1rem;
}

.toolbar .toolbar-spacer { flex: 1; }

.toolbar select.input {
  width: auto;
  min-height: 2.25rem;
  padding-block: 0.3rem;
  font-size: 0.9rem;
}

.toolbar .search-box { flex: 1; min-width: 12rem; max-width: 22rem; }

/* Pages with the fixed chronology rail ("Deine Fotos"): the scrubber owns
   the right viewport edge (0.5rem inset + 4.75rem expanded, z-index above
   flow content), which would bury the toolbar's trailing control underneath
   it. Reserving the expanded footprint keeps the density switch clickable at
   EVERY scroll position - no dead zone, no z-index war, no rail geometry
   churn (the minimap is a px-projection of the document; resizing it would
   force full re-renders). 5rem = 5.25rem footprint minus the ~0.75rem page
   padding, plus breathing room. Applies only at rail widths (>= 72rem). */
@media (min-width: 72rem) {
  .toolbar--clear-rail { margin-inline-end: 5rem; }
}

/* Segmented control: radios in disguise - full keyboard/AT semantics free. */
.seg {
  display: inline-flex;
  padding: 0.2rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
}

.seg label {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4ch;
  padding: 0.28rem 0.8rem;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 580;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 140ms var(--ease), background 140ms var(--ease);
}

.seg label:hover { color: var(--text); }

.seg label:has(input:checked) {
  background: var(--accent);
  color: var(--accent-contrast);
}

.seg label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

.seg svg { width: 0.95em; height: 0.95em; }

/* ---------------------------------------------------------- label rail */
/* One-tap label chips on the search page: the drawer handles the filing
   system never had. Chips are plain links - zero JS. */
.chip-rail { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-block: 0.9rem; }

a.label-chip { text-decoration: none; transition: border-color 140ms var(--ease), background 140ms var(--ease); }

a.label-chip:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, var(--surface));
  color: var(--text);
}

/* A chip that is part of the current query reads as "pressed". */
.label-chip.is-active {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-contrast);
}

.label-chip.is-active .chip-count { color: var(--accent-contrast); }

.label-chip .chip-count { margin-inline-start: 0.45ch; color: var(--text-dim); font-weight: 500; }

/* ------------------------------------------------------ recent searches */
/* "Zuletzt gesucht": device-local memory under the search hero. Chips reuse
   the label-chip voice; the clear affordance stays quiet but present. */
.recent-searches { margin-block-start: 1rem; }
.recent-searches[hidden] { display: none; }

.recent-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.recent-head .eyebrow { margin-block-end: 0; }
.recent-searches .chip-rail { margin-block: 0.5rem 0; }

/* Search syntax hint block (collapsible, quiet). */
.search-help { margin-block-start: 1rem; color: var(--text-dim); font-size: 0.85rem; }
.search-help summary { font-weight: 580; }
.search-help table { margin-block-start: 0.5rem; border-spacing: 0 0.15rem; }
.search-help td { padding-inline-end: 1rem; vertical-align: top; }

/* ---------------------------------------------------------------- skeleton */
/* Token-based shimmer while the next timeline page streams in - the same
   philosophy as blurhash: never a spinner, always a shaped preview. */
.skeleton {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 20%,
    color-mix(in srgb, var(--text) 7%, transparent) 50%,
    transparent 80%);
  animation: skeleton-sheen 1.3s var(--ease) infinite;
}

@keyframes skeleton-sheen {
  from { translate: -100% 0; }
  to { translate: 100% 0; }
}

/* ------------------------------------------------------------- deine reise */
/* Personal insights: per-year bars as pure SVG attributes (CSP-safe, no
   inline styles, no chart library) plus label chips and span facts. */
.journey-years { display: grid; gap: 0.35rem; margin-block: 0.9rem; }

.journey-year {
  display: grid;
  grid-template-columns: 3.2rem 1fr 2.6rem;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.85rem;
}

.journey-year .jy-label { font-weight: 640; font-variant-numeric: tabular-nums; }
.journey-year .jy-count { color: var(--text-dim); font-variant-numeric: tabular-nums; text-align: end; }

.journey-year svg { width: 100%; height: 0.6rem; display: block; }
.journey-year svg rect.jy-track { fill: var(--surface-2); }
.journey-year svg rect.jy-fill { fill: var(--accent); }

.journey-facts { display: grid; gap: 0.3rem; margin-block-start: 1rem; color: var(--text-dim); font-size: 0.88rem; }
.journey-facts strong { color: var(--text); font-weight: 620; }

/* The door from the journey card to the yearly Rückblick page. */
.journey-cta { margin-block-start: 1.1rem; }

/* -------------------------------------------------------- lightbox extras */
/* Custom PhotoSwipe UI: caption bar, details panel, favorite/download
   buttons. Colors stay literal (the lightbox is always a dark stage). */

/* Toolbar contrast guarantee, part 1 of 2: the buttons float directly over
   arbitrary photos, and a white glyph over a blown-out sky simply vanished.
   The caption already solves this at the bottom edge; the same scrim
   recipe lands here as the bar's own BACKGROUND - deliberately not a
   negative-z pseudo-element: the vendor promotes the bar into a composited
   opacity layer (will-change on .pswp__hide-on-close), and a background
   paints under the buttons by definition, with no stacking semantics left
   to argue with. It fades to nothing inside the bar's 60px, rides the
   bar's opacity (tap-to-hide chrome hides it too), and pointer-events on
   the bar stay none - the photo underneath remains tappable. Where the
   details panel slides under the bar the gradient lands on the panel's
   near-black surface and disappears into it. */
.pswp .pswp__top-bar {
  background: linear-gradient(to bottom, rgb(10 7 3 / 0.65), rgb(10 7 3 / 0.32) 55%, transparent);
}

/* Part 2 of 2, glyph-level insurance: the vendor's own icons (close, zoom)
   carry a dark .pswp__icn-shadow outline, our five custom buttons did not -
   which is exactly why they drowned first. Two jobs in one filter chain:

   1. CONTOUR - the two zero-offset hairline passes stack into a crisp
      counter-colored rim hugging the glyph silhouette (drop-shadow has no
      spread parameter; stacking is the idiom, and each pass re-shadows
      the previous output, so the rim densifies without growing). It cuts
      the icon out of the photo the way subtitle lettering is cut out of
      film: local contrast that is felt, never seen.
   2. AMBIENCE - the trailing soft pass is the gentle lift-off shadow.

   Every glyph up here is light (warm white, gold star), so the inverted
   rim is always the dark ink; a future dark-on-light glyph would need
   the opposite rim. */
.pswp .pswp__top-bar .pswp__icn {
  filter:
    drop-shadow(0 0 0.6px rgb(10 7 3 / 0.9))
    drop-shadow(0 0 0.6px rgb(10 7 3 / 0.9))
    drop-shadow(0 1px 1.5px rgb(10 7 3 / 0.6));
}

/* The counter is a glyph too: same recipe in its text-shadow dialect,
   but with a SINGLE hairline - at 14px the letterforms' strokes are so
   thin that a doubled rim swallows them into grey mud instead of cutting
   them out. The vendor's faint grey shadow at 0.85 opacity was the least
   readable element on bright panoramas. */
.pswp .pswp__counter {
  text-shadow:
    0 0 1px rgb(10 7 3 / 0.8),
    0 1px 2px rgb(10 7 3 / 0.65);
  opacity: 1;
}

/* Custom glyph color, targeted at the SVG ON PURPOSE: the vendor sheet
   loads after this one and paints `color: var(--pswp-icon-color-secondary)`
   (#4f4f4f) directly onto every .pswp__icn - a button-level color never
   reaches the icons through inheritance. Without this two-class override
   the currentColor strokes of all five custom buttons silently render
   dark grey (which is also why the inactive star used to drown on dark
   photos). Warm white matches the vendor glyphs' #fff fills, so the whole
   bar reads as ONE family: light glyph, dark rim, soft scrim. */
.pswp__button--amz-fav .pswp__icn,
.pswp__button--amz-download .pswp__icn,
.pswp__button--amz-info .pswp__icn,
.pswp__button--amz-link .pswp__icn,
.pswp__button--amz-play .pswp__icn { color: #f7f2e8; }

.pswp__button--amz-fav.pswp__button--amz-active .i-star {
  fill: var(--grad-b);
  stroke: var(--grad-b);
}

/* Slideshow toggle: the glyph mirrors the state it would ENTER. */
.pswp__button--amz-play .i-pause { display: none; }
.pswp__button--amz-play.pswp__button--amz-playing .i-pause { display: block; }
.pswp__button--amz-play.pswp__button--amz-playing .i-play { display: none; }

.pswp__button[hidden] { display: none; }

/* STACKING ORDER, hardened. PhotoSwipe adds .pswp__hide-on-close to every
   root-appended UI element, and its stylesheet loads AFTER this one with
   `.pswp .pswp__hide-on-close { z-index: 10; }` - the same level as the
   toolbar, which is an EARLIER sibling and therefore painted UNDER our
   sheets. These three-class selectors out-rank the vendor rule regardless
   of load order, so the toolbar (z 10) always stays clickable above the
   panel (z 9) above the caption (z 8). */
.pswp .pswp__amz-panel.pswp__hide-on-close { z-index: 9; }
.pswp .pswp__amz-caption.pswp__hide-on-close {
  z-index: 8;
  /* The vendor's `.pswp--ui-visible .pswp__hide-on-close` flips clickable
     elements back on; the caption is pure decoration and must never
     swallow taps meant for the photo underneath. */
  pointer-events: none;
}

/* Caption bar: bottom-left, over a soft scrim, never covering controls. */
.pswp__amz-caption {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  display: grid;
  gap: 0.1rem;
  padding: 2.2rem clamp(1rem, 4vw, 1.75rem) max(0.9rem, env(safe-area-inset-bottom));
  background: linear-gradient(transparent, rgb(10 7 3 / 0.6));
  color: #f7f2e8;
  pointer-events: none;
}

.pswp-amz-title {
  font-family: var(--font-display);
  font-weight: 560;
  font-size: clamp(1rem, 2.6vw, 1.3rem);
  line-height: 1.2;
}

.pswp-amz-sub { font-size: 0.82rem; color: rgb(247 242 232 / 0.78); }

/* Details panel: a right-hand sheet on desktop, a bottom sheet on phones.
   Toggled via a root class so the button needs no state of its own.
   While closed it is also visibility-hidden (delayed until the slide-out
   ends) so its controls - the label editor holds real form fields - can
   never be reached by keyboard or screen reader through an invisible
   sheet. The top padding keeps content clear of the floating toolbar. */
.pswp__amz-panel {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: min(21rem, 86vw);
  padding: 3.6rem 1.25rem 1.25rem;
  background: rgb(16 12 8 / 0.92);
  color: #f7f2e8;
  backdrop-filter: blur(12px);
  border-inline-start: 1px solid rgb(247 242 232 / 0.12);
  overflow-y: auto;
  visibility: hidden;
  translate: 100% 0;
  transition: translate 240ms var(--ease), visibility 0s 240ms;
}

.pswp--amz-panel-open .pswp__amz-panel {
  visibility: visible;
  translate: 0 0;
  transition: translate 240ms var(--ease);
}

@media (max-width: 47.99rem) {
  .pswp__amz-panel {
    inset-block: auto 0;
    inset-inline: 0;
    width: auto;
    max-height: 60vh;
    padding: 1.1rem 1.25rem max(1.1rem, env(safe-area-inset-bottom));
    border-inline-start: 0;
    border-block-start: 1px solid rgb(247 242 232 / 0.12);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    translate: 0 100%;
  }

  .pswp--amz-panel-open .pswp__amz-panel { translate: 0 0; }
}

/* Panel header: title plus an always-reachable close button. */
.pswp-amz-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-block-end: 0.75rem;
}

.pswp-amz-panel-head h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 580;
  font-size: 1.05rem;
}

.pswp-amz-close {
  display: grid;
  place-items: center;
  width: 1.8rem;
  height: 1.8rem;
  padding: 0;
  border: 1px solid rgb(247 242 232 / 0.25);
  border-radius: 50%;
  background: rgb(247 242 232 / 0.08);
  color: #f7f2e8;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 140ms var(--ease), color 140ms var(--ease);
}

.pswp-amz-close:hover { border-color: var(--grad-b); color: var(--grad-b); }

.pswp-amz-list { margin: 0; display: grid; gap: 0.7rem; }
.pswp-amz-row { display: grid; gap: 0.1rem; }

.pswp-amz-row dt {
  font-size: 0.68rem;
  font-weight: 640;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgb(247 242 232 / 0.6);
}

.pswp-amz-row dd { margin: 0; font-size: 0.9rem; overflow-wrap: anywhere; }
.pswp-amz-row dd a { color: var(--grad-b); }

.pswp-amz-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; padding-block-start: 0.15rem; }

/* One label mutation at a time: the busy container dims as feedback. */
.pswp-amz-chips[data-busy] { opacity: 0.6; }

/* Chips inside the dark panel need literal dark-stage colors. */
.pswp-amz-chips .label-chip {
  border-color: rgb(247 242 232 / 0.25);
  background: rgb(247 242 232 / 0.08);
  color: #f7f2e8;
}

.pswp-amz-chips a.label-chip:hover { border-color: var(--grad-b); color: var(--grad-b); background: rgb(247 242 232 / 0.08); }

/* Editable chips hold a sibling link + remove button (never nested). */
.pswp-amz-chips .label-chip a { color: inherit; text-decoration: none; }
.pswp-amz-chips .label-chip a:hover { color: var(--grad-b); }

.pswp-amz-chips .chip-x {
  display: inline-grid;
  place-items: center;
  width: 1rem;
  height: 1rem;
  margin-inline: 0.3rem -0.25rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: 1;
  cursor: pointer;
}

.pswp-amz-chips .chip-x:hover { color: var(--danger); background: rgb(247 242 232 / 0.14); }

/* Add-label editor: full-width row beneath the chips. */
.pswp-amz-add {
  display: flex;
  gap: 0.35rem;
  flex-basis: 100%;
  margin-block-start: 0.35rem;
}

.pswp-amz-add input {
  flex: 1;
  min-width: 0;
  padding: 0.28rem 0.65rem;
  border: 1px solid rgb(247 242 232 / 0.25);
  border-radius: 999px;
  background: rgb(247 242 232 / 0.08);
  color: #f7f2e8;
  font: inherit;
  font-size: 0.8rem;
}

.pswp-amz-add input::placeholder { color: rgb(247 242 232 / 0.45); }
.pswp-amz-add input:focus-visible { outline: none; border-color: var(--grad-b); }

.pswp-amz-add button {
  padding: 0.28rem 0.7rem;
  border: 1px solid rgb(247 242 232 / 0.25);
  border-radius: 999px;
  background: rgb(247 242 232 / 0.08);
  color: #f7f2e8;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 140ms var(--ease), color 140ms var(--ease);
}

.pswp-amz-add button:hover { border-color: var(--grad-b); color: var(--grad-b); }
.pswp-amz-add button:disabled { opacity: 0.55; cursor: default; }

/* The caption must never sit under the open panel on desktop. */
@media (min-width: 48rem) {
  .pswp--amz-panel-open .pswp__amz-caption { inset-inline-end: min(21rem, 86vw); }
}

/* ------------------------------------------------------------- utilities */
/* Actions whose role the mobile tab bar already carries (e.g. the header
   "Hochladen" button vs. the raised "Neu" tab). Placed in the utilities
   layer so it outranks component display rules in the cascade. */
@media (max-width: 47.99rem) {
  .hide-mobile { display: none; }
}

.row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.row--between { justify-content: space-between; }
.stack { display: grid; gap: var(--gap); }
.dim { color: var(--text-dim); }
.mono { font-family: var(--font-mono); }
.small { font-size: 0.85rem; }
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==================================================== timeline scrubber */
/* scrubber.js upgrades the plain .tl-rail into a full-height density map of
   the whole collection. Everything here is scoped to .is-scrubber so the
   no-JS fallback rail (plain .tl-rail year links) keeps its pill look.

   Progressive disclosure: at rest the rail is a slim MINIMAP strip (bands,
   ticks, viewport indicator - position at a glance, zero label clutter).
   It expands to the full labeled map only on intent: hover, keyboard focus
   inside it, or an active drag (.is-engaged, pinned by scrubber.js because
   pointer capture lets the pointer leave the rail mid-drag). */
.tl-rail.is-scrubber {
  display: none;
  position: fixed;
  inset-block: 4.5rem 1.25rem;
  inset-inline-end: 0.5rem;
  /* The base pill centers itself via translate; the full-height scrubber
     is positioned by inset-block alone, so the shift MUST be neutralized. */
  translate: none;
  width: 1.2rem;
  padding: 0;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(10px);
  overflow: hidden;
  touch-action: none;
  user-select: none;
  cursor: pointer;
  /* Collapse waits 320ms: overshooting the rail while reading it must not
     snap it shut. (Reduced motion keeps the delays but drops the motion -
     they are hover INTENT, not animation.) */
  transition: width 220ms var(--ease) 320ms, box-shadow 220ms var(--ease) 320ms;
}

.tl-rail.is-scrubber:hover,
.tl-rail.is-scrubber:focus-within,
.tl-rail.is-scrubber.is-engaged {
  width: 4.75rem;
  box-shadow: var(--shadow-1);
  /* Expansion needs 100ms of dwell: mouse travel toward the window
     scrollbar crosses the strip and must not flicker it open. */
  transition-delay: 100ms;
}

@media (min-width: 72rem) {
  .tl-rail.is-scrubber { display: block; }
}

/* All rail children are absolutely positioned in rail space by the module. */
.tl-rail.is-scrubber > * { position: absolute; }

/* Alternating year bands: a whisper of tint on even years for rhythm. */
.scrub-band {
  inset-inline: 0;
  background: transparent;
}

.scrub-band--alt { background: color-mix(in srgb, var(--accent) 6%, transparent); }

/* Boundary ticks: hairline marks at year/label positions. */
.scrub-tick {
  inset-inline: 0.35rem;
  height: 1px;
  background: color-mix(in srgb, var(--border) 80%, transparent);
  pointer-events: none;
}

/* Label buttons: quiet, left-aligned, brighten to accent on hover/focus. */
.scrub-label {
  inset-inline: 0;
  padding-block: 0;
  padding-inline: 0.5rem;
  margin: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  text-align: start;
  white-space: nowrap;
  cursor: pointer;
  transition: color 140ms var(--ease);
}

.scrub-label:hover { color: var(--accent); }
.scrub-label:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 2px; }

.scrub-label--year { font-size: 0.72rem; font-weight: 650; color: var(--accent); }
.scrub-label--month { font-size: 0.66rem; font-weight: 580; }
.scrub-label--fine { font-size: 0.6rem; }
.scrub-label--hour { font-size: 0.55rem; }

/* Collapsed strip: labels vanish (and stop catching clicks - a tap on the
   slim rail ratio-jumps like a scrollbar). They fade back in as the rail
   opens; keyboard focus re-opens it via :focus-within, so tabbing into an
   invisible button is impossible. */
.tl-rail.is-scrubber .scrub-label {
  opacity: 0;
  pointer-events: none;
  transition: color 140ms var(--ease), opacity 180ms var(--ease) 320ms;
}

.tl-rail.is-scrubber:hover .scrub-label,
.tl-rail.is-scrubber:focus-within .scrub-label,
.tl-rail.is-scrubber.is-engaged .scrub-label {
  opacity: 1;
  pointer-events: auto;
  transition-delay: 0s, 140ms;
}

/* Viewport indicator: a soft accent block tracking the visible page. */
.scrub-view {
  inset-inline: 2px;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 4px;
  pointer-events: none;
}

/* Drag tooltip: a floating date pill beside the rail (lives on <body>). */
.scrub-tip {
  position: fixed;
  inset-inline-end: 5.75rem;
  transform: translateY(-50%);
  z-index: 40;
  padding: 0.25rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-2);
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
}

/* ===================================================== search assistant */
/* The combobox popover that suche.js drives: qualifier menu, completion
   lists and native date pickers. Consumes only existing tokens, so it flips
   with light/dark automatically. */
.search-assist { position: relative; }

.search-popover {
  position: absolute;
  inset-inline: 0;
  top: calc(100% + 0.4rem);
  z-index: 30;
  max-height: min(22rem, 55vh);
  overflow-y: auto;
  padding: 0.35rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}

.search-popover[hidden] { display: none; }

.sa-list { list-style: none; margin: 0; padding: 0; }

.sa-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  padding: 0.5rem 0.7rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.sa-option:hover,
.sa-option.is-active { background: var(--surface-2); }

.sa-key {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.05rem 0.45rem;
  white-space: nowrap;
}

.sa-name {
  font-weight: 580;
  font-size: 0.92rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sa-desc {
  color: var(--text-dim);
  font-size: 0.78rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sa-count {
  margin-inline-start: auto;
  flex: none;
  color: var(--text-dim);
  font-size: 0.78rem;
}

.sa-hint {
  color: var(--text-dim);
  font-size: 0.8rem;
  padding: 0.45rem 0.7rem;
  /* Hints render as <p> inside panels; the global paragraph margin would
     tear the compact popover rhythm apart. */
  margin: 0;
}

.sa-panel { display: grid; gap: 0.55rem; padding: 0.55rem 0.7rem; }

.sa-panel label { font-size: 0.8rem; font-weight: 580; }

.sa-actions { display: flex; justify-content: flex-end; }

.sa-option:focus-visible,
.search-popover .btn:focus-visible,
.search-popover .input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ===================================================== command palette */
/* Ctrl+K quick-jump: a top-anchored dialog reusing the assistant's option
   rows (.sa-option), so both surfaces speak one visual language. */
.palette {
  width: min(92vw, 34rem);
  margin-block-start: clamp(2rem, 12vh, 8rem);
  padding: 0.65rem;
}

.palette .search-box { margin-block-end: 0.45rem; }

.palette-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: min(50vh, 22rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Quiet group captions between result clusters (Seiten / Alben / Labels). */
.palette-group {
  padding: 0.55rem 0.7rem 0.15rem;
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.palette-empty { margin: 0; padding: 0.7rem; color: var(--text-dim); font-size: 0.9rem; }

/* Footer hint line: the palette teaches its own keys. */
.palette-hint {
  display: flex;
  gap: 1rem;
  padding: 0.5rem 0.7rem 0.15rem;
  border-block-start: 1px solid var(--border);
  margin-block-start: 0.45rem;
  color: var(--text-dim);
  font-size: 0.75rem;
}

/* ==================================================== shortcuts overlay */
/* The "?" cheat sheet: one dialog mapping the whole keyboard model, grouped
   by context. Self-documenting systems keep their power users. */
.shortcuts { width: min(92vw, 30rem); }

.shortcuts h2 { margin-block-end: 1rem; }

.shortcuts h3 {
  margin: 1.1rem 0 0.4rem;
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
}

.shortcuts h3:first-of-type { margin-block-start: 0; }

.shortcut-list { margin: 0; display: grid; gap: 0.35rem; }

.shortcut-list > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.shortcut-list dt { font-size: 0.9rem; }
.shortcut-list dd { margin: 0; white-space: nowrap; }
.shortcut-list kbd + kbd { margin-inline-start: 0.2ch; }

/* ======================================================== album picker */
/* "Zu Album hinzufügen" dialog. Wider than .dlg so album titles never wrap
   at family names, split into head/body/foot so the search bar and the
   commit button stay in reach while the list scrolls. Every row is a
   native label wrapping a native checkbox - the visual affordance sits on
   the tick and the accent tint, the semantics stay pure HTML. */
.picker {
  width: min(94vw, 32rem);
  max-height: min(88vh, 40rem);
  padding: 0;
  overflow: hidden; /* Head/foot stay put; scrolling belongs to .picker-body. */
}

.picker-form {
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-height: inherit;
}

.picker-head {
  padding: clamp(1rem, 3vw, 1.35rem) clamp(1rem, 3vw, 1.35rem) 0.75rem;
  border-block-end: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  background: var(--surface);
}

.picker-head h2 {
  margin: 0 0 0.15rem;
  font-size: clamp(1.1rem, 1.05rem + 0.3vw, 1.3rem);
  line-height: 1.25;
}

.picker-desc {
  margin: 0 0 0.8rem;
  color: var(--text-dim);
  font-size: 0.85rem;
  line-height: 1.4;
}

.picker-search { margin: 0; }

.picker-body {
  flex: 1 1 auto;
  min-height: 8rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.35rem 0.5rem;
}

.picker-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px; /* Rows read as one column; the gap is the divider. */
}

/* ---- one row ---- */
.picker-row { margin: 0; padding: 0; }

.picker-row-label {
  display: grid;
  grid-template-columns: 2.5rem 1fr auto 1.1rem;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease);
}

.picker-row-label:hover { background: var(--surface-2); }

/* Focus-visible ring on the WHOLE row via the hidden checkbox: :has()
   catches keyboard focus without duplicating a fallback outline in modern
   browsers (Chrome 105+, Safari 15.4+, Firefox 121+ - the LTS floor). */
.picker-row-label:has(.picker-row-check:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}

.picker-row.is-picked .picker-row-label {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.picker-row.is-picked .picker-row-tick svg { opacity: 1; transform: scale(1); }
.picker-row-tick svg { opacity: 0; transform: scale(0.6); transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out); }
.picker-row-tick { color: var(--accent); display: grid; place-items: center; width: 1.1rem; height: 1.1rem; }

/* Cover thumb: tasteful square, respects blurhash-free empty state. */
.picker-row-cover {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border) 60%, transparent);
}
.picker-row-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.picker-row-cover--empty { color: var(--text-dim); }
.picker-row-cover--empty svg { width: 1.4rem; height: 1.4rem; }

.picker-row-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.picker-row-title {
  font-weight: 550;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.picker-row-meta { font-size: 0.78rem; color: var(--text-dim); }

.picker-row[data-contains="all"] .picker-row-meta { color: color-mix(in srgb, var(--ok) 85%, var(--text-dim)); }
.picker-row[data-contains="some"] .picker-row-meta { color: color-mix(in srgb, var(--accent) 85%, var(--text-dim)); }

/* ---- visibility pill (trust signal in-context) ---- */
.picker-vis {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.55rem 0.15rem 0.4rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 550;
  letter-spacing: 0.01em;
  line-height: 1;
  white-space: nowrap;
  border: 1px solid transparent;
}
.picker-vis svg { width: 0.85rem; height: 0.85rem; }

.picker-vis--private {
  background: var(--accent);
  color: var(--accent-contrast);
}
.picker-vis--members {
  background: color-mix(in srgb, var(--ok) 14%, transparent);
  color: color-mix(in srgb, var(--ok) 88%, var(--text));
  border-color: color-mix(in srgb, var(--ok) 45%, transparent);
}
.picker-vis--granted {
  background: transparent;
  color: color-mix(in srgb, var(--accent) 88%, var(--text));
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

/* ---- skeleton, empty, no-match ---- */
.picker-skeleton { padding: 0.35rem 0.2rem; display: flex; flex-direction: column; gap: 2px; }
.picker-row--skel {
  height: 3.3rem;
  border-radius: var(--radius);
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    color-mix(in srgb, var(--surface-2) 55%, var(--surface)) 50%,
    var(--surface-2) 100%);
  background-size: 200% 100%;
  animation: pickerShimmer 1.4s var(--ease) infinite;
}
@keyframes pickerShimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

.picker-empty {
  padding: 1.5rem 1.25rem 1.75rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
}
.picker-empty svg { width: 4rem; height: auto; color: var(--text-dim); }
.picker-empty h3 { margin: 0.2rem 0 0; font-size: 1rem; }
.picker-empty p { margin: 0 0 0.4rem; }

.picker-nomatch {
  padding: 0.9rem 0.9rem 1rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  align-items: center;
}
.picker-nomatch p { margin: 0; font-size: 0.9rem; }

/* ---- footer ---- */
.picker-foot {
  padding: 0.75rem clamp(1rem, 3vw, 1.35rem);
  border-block-start: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.picker-foot-new {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.picker-foot-new svg { width: 0.9rem; height: 0.9rem; }

/* Busy state: dim the list, spin nothing (the primary button carries the
   feedback via its own aria-disabled and label), keep Cancel reachable. */
.picker-form.is-busy .picker-body { opacity: 0.55; pointer-events: none; }
.picker-form.is-busy [data-picker-submit] { position: relative; padding-inline-end: 2.2rem; }
.picker-form.is-busy [data-picker-submit]::after {
  content: "";
  position: absolute;
  right: 0.85rem;
  top: 50%;
  width: 0.9rem;
  height: 0.9rem;
  margin-top: -0.45rem;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-block-end-color: transparent;
  animation: pickerSpin 0.7s linear infinite;
}
@keyframes pickerSpin {
  to { transform: rotate(360deg); }
}

/* Reduced-motion: honor the OS preference. The picker is still perfectly
   usable without animation - snap changes, no shimmer, no spin. */
@media (prefers-reduced-motion: reduce) {
  .picker-row-label,
  .picker-row-tick svg { transition: none; }
  .picker-row--skel { animation: none; background: var(--surface-2); }
  .picker-form.is-busy [data-picker-submit]::after { animation: none; }
}

/* Compact mobile: on narrow screens the pill row wraps below the body so
   the title never gets clipped. Cover shrinks a touch. */
@media (max-width: 26rem) {
  .picker-row-label {
    grid-template-columns: 2.2rem 1fr 1rem;
    grid-template-rows: auto auto;
    gap: 0.55rem;
  }
  .picker-row-body { grid-column: 2; }
  .picker-vis { grid-column: 2; grid-row: 2; justify-self: start; }
  .picker-row-tick { grid-column: 3; grid-row: 1 / span 2; }
  .picker-row-cover { width: 2.2rem; height: 2.2rem; grid-row: 1 / span 2; }
}

/* ================================================== forced colors (WHCM) */
/* Windows High Contrast strips author backgrounds and shadows - exactly the
   two channels several checked/selected states speak through. Restate ONLY
   those states in system colors (SelectedItem carries the user's own
   contrast choice); everything else follows the forced palette untouched. */
@media (forced-colors: active) {
  /* Segmented controls and quick-filter chips: the active option spoke
     purely through a filled accent background. */
  .seg label:has(input:checked),
  .label-chip.is-active {
    forced-color-adjust: none;
    background: SelectedItem;
    color: SelectedItemText;
    border-color: SelectedItem;
  }

  /* Radio cards: the checked state was a tint plus an inset box-shadow
     filling the dot - both stripped. */
  .choice:has(input:checked) { border-color: SelectedItem; }

  .choice:has(input:checked) .choice-dot {
    forced-color-adjust: none;
    border-color: SelectedItem;
    box-shadow: inset 0 0 0 4px SelectedItem;
  }

  /* Selection checks over photos (gallery select mode + workbench tiles):
     picked was a filled circle, unpicked an outline - keep that contrast. */
  .pg-item.is-picked .pg-select-check,
  .lib-tile.is-selected .lib-check {
    forced-color-adjust: none;
    background: SelectedItem;
    border-color: SelectedItem;
    color: SelectedItemText;
  }

  /* Current-section markers spoke through accent color and a gradient
     underline (background-image - stripped): an underline survives. */
  .shell-links a.is-active,
  .tab-bar a.is-active,
  .btn--ghost.is-active {
    text-decoration: underline;
    text-underline-offset: 0.3em;
  }
}
