/* ==========================================================================
   Public link page
   --------------------------------------------------------------------------
   A link-in-bio page is a centred column, and visitors expect it to be one, so
   the layout does not fight that. The design work goes somewhere more useful:
   making the operator's own colours readable.

   The old page painted every button with its configured background and then
   ran JavaScript after paint to guess a text colour. That meant a flash of
   unreadable text, and a page that needed script to be legible at all. Text
   colour is now computed on the server from the actual WCAG contrast ratio, so
   the first byte of HTML is already correct and the page needs no script.

   Three treatments are available, because one hex value can serve more than one
   look: solid fills the row, tinted keeps the colour as a chip and a wash,
   outline reduces it to an edge.
   ========================================================================== */

/* The hero is a sibling of .page, so the column has to share the viewport with
   it. Without this, .page alone claims the full height, the document is always
   taller than the window, and every visitor gets a scrollbar for nothing. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

/*
 * position and z-index are load-bearing.
 *
 * .hero is position: relative, and positioned elements paint after static ones
 * regardless of document order. So the banner was painted over the logo that
 * was supposed to be sitting on top of it, and all that showed was the sliver
 * hanging below the banner's lower edge. Giving the column its own stacking
 * position puts the profile back in front.
 */
.page {
  position: relative;
  z-index: 1;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 20px 32px;
}

/* The single accent hairline at the top of the viewport. It is the only piece
   of pure decoration on the page and it earns its place by being the first
   evidence that the operator's colour is in charge here. */
.accent-rule {
  position: fixed;
  inset: 0 0 auto;
  height: 3px;
  background: var(--accent);
  z-index: 10;
}

.shell {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding-top: clamp(48px, 9vh, 88px);
}

/* Without a banner the page is a flat dark field with a column of colour in
   the middle of it. A single wide bloom in the operator's own colour gives the
   column something to sit on. Only when there is no banner: with one, the
   photograph already does this job and two of them would fight. */
.page:not(.page--cover)::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 460px;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    72% 100% at 50% 0%,
    var(--accent-soft) 0%,
    transparent 72%
  );
}

/* --------------------------------------------------------------------- hero */

/*
 * The banner sits behind the logo and fades into the page, the way a Linktree
 * or Facebook header does.
 *
 * The previous version used the width:100vw + margin-left:50% +
 * translateX(-50%) trick to break out of a padded container. That works right
 * up until a vertical scrollbar exists: 100vw counts the scrollbar, the element
 * is wider than the viewport, the document gains horizontal overflow, and every
 * centred thing on the page is then centred against the wider document instead
 * of the window. Measured on a 2079px viewport, the banner came out 1678px wide
 * and 200px left of centre while the content sat 300px right of it.
 *
 * There is no trick here now. The hero is a plain block at the top of the body,
 * outside the padded column, so full width means full width.
 */

.hero {
  position: relative;
  width: 100%;
  /* Auf einem Telefon im Hochformat wuerden 210 Pixel fast ein Drittel des
     Bildschirms fuer das Banner beanspruchen, bevor der erste Link kommt. */
  height: clamp(150px, 34vw, 360px);
  overflow: hidden;
  background: var(--surface-2);
}

.hero__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The crop is written into the page's own style block; this is the fallback
     for the moment before it, and for a page with no cover set. */
  object-position: 50% 50%;
}

/*
 * Blendet nach unten in die Hintergrundfarbe aus, damit Logo und Text darunter
 * auf ruhigem Grund stehen. Stufen in rgba statt eines Verlaufs nach
 * `transparent`, weil das Interpolieren nach durchsichtigem Schwarz einen
 * grauen Schleier in die Mitte legt.
 *
 * Die Stufen sind fuer hell und dunkel verschieden, und zwar aus einem
 * handfesten Grund: auf dunklem Grund faellt ein Nachtfoto in Schwarz aus, das
 * sieht aus wie das Ende des Bildes. Auf hellem Grund faellt dasselbe Foto in
 * Weiss aus, und das sieht aus wie Nebel, der ueber dem Bild liegt. Deshalb
 * bleibt der helle Verlauf laenger klar und wird erst spaet deutlich.
 */
.hero__veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(var(--bg-rgb), 0) 0%,
    rgba(var(--bg-rgb), 0.03) 62%,
    rgba(var(--bg-rgb), 0.58) 86%,
    rgba(var(--bg-rgb), 1) 100%
  );
}

:root[data-theme='dark'] .hero__veil {
  background: linear-gradient(
    to bottom,
    rgba(var(--bg-rgb), 0) 0%,
    rgba(var(--bg-rgb), 0.15) 42%,
    rgba(var(--bg-rgb), 0.72) 74%,
    rgba(var(--bg-rgb), 1) 100%
  );
}

@media (prefers-color-scheme: dark) {
  :root[data-theme='auto'] .hero__veil {
    background: linear-gradient(
      to bottom,
      rgba(var(--bg-rgb), 0) 0%,
      rgba(var(--bg-rgb), 0.15) 42%,
      rgba(var(--bg-rgb), 0.72) 74%,
      rgba(var(--bg-rgb), 1) 100%
    );
  }
}

/* The profile climbs back up into the lower part of the image, far enough that
   the logo sits on the banner rather than under it, but not so far that the
   name lands in the middle of the photograph. */
.page--cover .shell {
  padding-top: 0;
}

/* Der Zug nach oben sitzt auf .profile, nicht auf .shell.
   --avatar-size ist auf .profile deklariert, und Custom Properties vererben
   sich nur nach unten; vom Elternelement gelesen haette stillschweigend der
   Ersatzwert gegolten und jede Groesse gleich weit ueberlappt.

   Drei Viertel der Marke liegen auf dem Banner. Bei zwei Dritteln stand sie zu
   weit unten und sass im ausgeblendeten Bereich statt im Bild. */
.page--cover .profile {
  margin-top: calc(var(--avatar-size) * -0.78);
}

.page--cover .profile__avatar {
  border: 3px solid var(--bg);
  background: var(--bg);
}

/* ------------------------------------------------------------------ profile */

.profile {
  text-align: center;
  margin-bottom: 32px;
}

/* Drei Groessen statt einer. Ein Schriftzug auf einer Kachel braucht mehr Platz
   als ein Portraet, und ob die Buchstaben darin lesbar bleiben, entscheidet der
   Unterschied zwischen 100 und 168 Pixeln.

   Alle drei sind mit clamp an die Fensterbreite gekoppelt: 168 Pixel sind auf
   einem Telefon fast die halbe Breite, und dann steht die Marke groesser da als
   der Name darunter. */
.profile {
  --avatar-size: clamp(104px, 26vw, 132px);
}

.profile--klein {
  --avatar-size: clamp(84px, 20vw, 100px);
}

.profile--mittel {
  --avatar-size: clamp(104px, 26vw, 132px);
}

.profile--gross {
  --avatar-size: clamp(120px, 32vw, 168px);
}

.profile__avatar {
  width: var(--avatar-size);
  height: var(--avatar-size);
  margin: 0 auto 22px;
  border-radius: calc(var(--r-avatar) + 4px);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.profile__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A tilt and a lean-in on hover. The 1.x stylesheet did scale(1.08)
   rotate(3deg); this goes a little past it, because at 1.06 the effect was
   there in the code and barely there on the screen.

   The easing overshoots slightly before settling, which is what makes it read
   as the logo responding rather than as a size being set. */
.profile__avatar--spin {
  transition: transform 340ms cubic-bezier(0.34, 1.45, 0.5, 1);
}

.profile__avatar--spin:hover,
.profile__avatar--spin:focus-within {
  transform: scale(1.11) rotate(4deg);
}

/* The breathing glow steps aside while the pointer is on the logo, so the two
   effects do not argue about the same edge. */
.profile__avatar--pulse:hover,
.profile__avatar--pulse:focus-within,
.reveal .profile__avatar--pulse:hover,
.reveal .profile__avatar--pulse:focus-within {
  animation-play-state: paused;
  box-shadow:
    0 0 0 6px var(--accent-soft),
    0 16px 40px -12px var(--accent);
}

/* The glow breathes in the site's own colour. Slow and shallow on purpose:
   the logo sits above everything else on the page, so anything faster reads as
   a notification demanding to be dealt with. */
.profile__avatar--pulse {
  animation: avatar-pulse 3.2s ease-in-out infinite;
}

@keyframes avatar-pulse {
  0%, 100% {
    box-shadow:
      0 0 0 4px var(--accent-soft),
      0 0 18px -2px rgba(0, 0, 0, 0);
  }
  50% {
    box-shadow:
      0 0 0 9px transparent,
      0 0 34px 4px var(--accent-line);
  }
}

@media (prefers-reduced-motion: reduce) {
  .profile__avatar--spin {
    transition: none;
  }

  .profile__avatar--spin:hover,
  .profile__avatar--spin:focus-within {
    transform: none;
  }

  .profile__avatar--pulse {
    animation: none;
  }

  /* Der Fall mit eingeschaltetem Einlauf wird weiter unten geregelt, nach der
     Regel, die beide Animationen zusammenfasst. An dieser Stelle haette er
     dieselbe Spezifitaet und stuende davor, also verloren. */
}

.profile__avatar--empty {
  display: grid;
  place-items: center;
  color: var(--text-faint);
}

/* Larger and tighter than before. A name set at 2rem with default tracking
   reads as a heading in a document; at this size with negative tracking it
   reads as a wordmark, which is what it is. */
.profile__name {
  font-size: clamp(1.75rem, 1.35rem + 1.9vw, 2.6rem);
  letter-spacing: -0.03em;
  line-height: 1.1;
  margin-bottom: 10px;
  text-wrap: balance;
}

.profile__description {
  color: var(--text-muted);
  font-size: 0.9375rem;
  max-width: 38ch;
  margin: 0 auto;
  text-wrap: balance;
}

/* ---------------------------------------------------------------- entrance */

/*
 * The page arrives in order: logo, name, description, then the links one after
 * another. The delays are written into the page's own style block, because the
 * position of a link in the list is data, not something a stylesheet can know.
 *
 * The fill mode is `backwards`, not `both`, and the difference is the whole
 * point. `backwards` applies the opening keyframe during the delay, which is
 * what stops the finished page flashing before it fades in. `both` does that
 * too, but it also pins the closing keyframe in place forever afterwards — and
 * since animations outrank ordinary declarations in the cascade, that leaves
 * `transform: none` permanently glued to every element that ran the animation.
 * Every hover on the page then does nothing: the logo does not tilt, the links
 * do not lift. The animation runs for half a second and disables the interface
 * for the rest of the visit.
 */
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.reveal .profile__avatar,
.reveal .profile__name,
.reveal .profile__description,
.reveal .link {
  animation: rise 520ms var(--ease) backwards;
}

.reveal .profile__avatar { animation-delay: 0ms; }
.reveal .profile__name { animation-delay: 70ms; }
.reveal .profile__description { animation-delay: 130ms; }

/* Anything that cannot be reached by the generated per-link delays still
   arrives, just without its own place in the queue. */
.reveal .link { animation-delay: 200ms; }

/*
 * The logo runs two animations, and they have to be declared together.
 *
 * `animation` is a shorthand: it resets every animation property, including the
 * list of names. So `.reveal .profile__avatar` at 0,2,0 quietly replaced
 * `.profile__avatar--pulse` at 0,1,0, and the glow never ran as long as the
 * entrance was switched on — which it is by default. Listing both here, in a
 * rule that comes after the one above and matches it in specificity, is what
 * lets them coexist.
 *
 * The glow starts once the entrance has finished, so the element is not moving
 * and breathing at the same time.
 */
.reveal .profile__avatar--pulse {
  animation:
    rise 520ms var(--ease) backwards,
    avatar-pulse 3.2s ease-in-out 520ms infinite;
}

/*
 * Someone who asked their system for less movement gets the finished page
 * immediately. Setting opacity explicitly matters: without it, a cancelled
 * animation with fill mode `both` can leave the element at its starting opacity.
 */
@media (prefers-reduced-motion: reduce) {
  .reveal .profile__avatar,
  .reveal .profile__avatar--pulse,
  .reveal .profile__name,
  .reveal .profile__description,
  .reveal .link {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* -------------------------------------------------------------------- links */

.links {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.link {
  --link-color: var(--accent);
  --link-ink: var(--text);
  --link-face: var(--surface);
  --link-edge: var(--border);

  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 68px;
  padding: 13px 18px;
  border: 1px solid var(--link-edge);
  border-radius: 14px;
  background: var(--link-face);
  color: var(--link-ink);
  text-decoration: none;
  /* A one-pixel highlight along the top edge. It is the difference between a
     coloured rectangle and something that looks like it can be pressed, and it
     costs nothing: no gradient, no image, no extra element. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    var(--shadow-sm);
  transition: transform var(--speed) var(--ease), box-shadow var(--speed) var(--ease),
    border-color var(--speed) var(--ease);
}

/*
 * Hover does three things at once, none of them large: the row lifts, it throws
 * a shadow in its own colour, and the mark leans in. The colour in the shadow is
 * what makes it feel like the button reacting rather than a generic hover state
 * that any element could have.
 */
.link:hover {
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.3),
    0 14px 30px -14px var(--link-color),
    var(--shadow-md);
}

.link:focus-visible {
  transform: translateY(-2px);
}

.link:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
  transition-duration: 60ms;
}

.link:hover .link__mark {
  transform: scale(1.08);
}

.link--solid:hover .link__mark {
  background: rgba(255, 255, 255, 0.26);
}

@media (prefers-reduced-motion: reduce) {
  .link:hover,
  .link:focus-visible,
  .link:active {
    transform: none;
  }

  .link:hover .link__mark {
    transform: none;
  }
}

/* Grid rather than inline content, so the symbol is centred by the tile
   instead of sitting on a text baseline inside it. */
.link__mark {
  flex: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 11px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  transition: transform var(--speed) var(--ease), background var(--speed) var(--ease);
}

.link__mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.link__body {
  flex: 1 1 auto;
  min-width: 0;
}

.link__title {
  display: block;
  font-weight: 600;
  font-size: 0.9875rem;
  letter-spacing: -0.005em;
  overflow-wrap: anywhere;
}

.link__subtitle {
  display: block;
  font-size: 0.8125rem;
  opacity: 0.72;
  overflow-wrap: anywhere;
}

.link__arrow {
  flex: none;
  display: grid;
  place-items: center;
  opacity: 0.45;
  transition: transform var(--speed) var(--ease), opacity var(--speed) var(--ease);
}

.link:hover .link__arrow {
  opacity: 0.95;
  transform: translateX(5px);
}

/* Solid: the row is the colour. Foreground comes from the server. */
.link--solid {
  --link-face: var(--link-color);
  --link-edge: transparent;
}

.link--solid .link__mark {
  background: rgba(255, 255, 255, 0.16);
}

/* Tinted: the colour becomes a chip and a wash. Text stays on the page's own
   foreground colour, so contrast holds no matter what hex was chosen. */
.link--tinted .link__mark {
  background: var(--link-tint);
  color: var(--link-color);
}

.link--tinted {
  --link-edge: var(--border);
}

.link--tinted:hover {
  border-color: var(--link-color);
}

/* Outline: colour reduced to an edge and the mark. */
.link--outline {
  --link-face: transparent;
  --link-edge: var(--link-color);
}

.link--outline .link__mark {
  color: var(--link-color);
}

.link--outline:hover {
  background: var(--link-tint);
}

/* ------------------------------------------------------------------- empty */

.empty {
  padding: 40px 24px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-panel);
  text-align: center;
  color: var(--text-muted);
}

.empty h2 {
  margin-bottom: 6px;
  font-size: 1rem;
  color: var(--text);
}

/* Schmale Fenster: weniger Rand, engere Abstaende, damit die Links selbst mehr
   Platz bekommen. */
@media (max-width: 480px) {
  .page {
    padding: 0 14px 24px;
  }

  .links {
    gap: 10px;
  }

  .link {
    min-height: 62px;
    padding: 11px 14px;
    gap: 12px;
  }

  .link__mark {
    width: 38px;
    height: 38px;
  }

  .profile__description {
    font-size: 0.875rem;
  }
}

/* --------------------------------------------------------- Hinweisseiten */

.hinweis-seite {
  max-width: 46ch;
  margin: 0 auto;
  padding: 26px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  color: var(--text-muted);
  font-size: 0.9375rem;
  line-height: 1.65;
}

.hinweis-seite p {
  margin: 0 0 14px;
}

.hinweis-seite p:last-of-type {
  margin-bottom: 20px;
}

.hinweis-seite__links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 10px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.hinweis-seite__links a {
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.8125rem;
}

.hinweis-seite__links a:hover {
  border-color: var(--border-strong);
  text-decoration: none;
}

/* ------------------------------------------------------------------ footer */

.footer {
  margin-top: auto;
  padding-top: 48px;
  width: 100%;
  max-width: 560px;
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 6px 10px;
  font-size: 0.8125rem;
}

/* Separate pills rather than a hairline with text under it. The rule across
   the page was drawing a line under nothing in particular. */
.footer a,
.footer button {
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: border-color var(--speed) var(--ease), color var(--speed) var(--ease);
}

.footer a:hover,
.footer button:hover {
  border-color: var(--border-strong);
  text-decoration: none;
}

.footer a,
.footer button {
  color: var(--text-muted);
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}

.footer a:hover,
.footer button:hover {
  color: var(--text);
  text-decoration: underline;
}

/* -------------------------------------------------------------- maintenance */

.maintenance {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
  text-align: center;
}

.maintenance__card {
  max-width: 420px;
  padding: 40px 32px;
  border: 1px solid var(--border);
  border-radius: var(--r-panel);
  background: var(--surface);
}

.maintenance__card h1 {
  margin-bottom: 10px;
  font-size: 1.25rem;
}

.maintenance__card p {
  color: var(--text-muted);
}
