/*
   SPDX-FileCopyrightText: 2026 Paul Theile
   SPDX-License-Identifier: AGPL-3.0-or-later
*/

/* ==========================================================================
   Letleva - site stylesheet

   Sections
     1. Tokens and reset
     2. Bootstrap-compatible utilities (no Bootstrap dependency)
     3. Icons
     4. Footer
     5. Hero
     6. Home content column
     7. Plain text pages
   ========================================================================== */

/* 1. Tokens and reset
   ========================================================================== */

:root {
  color-scheme: light dark;

  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    sans-serif;

  /* Plain pages */
  --body-bg: #ffffff;
  --body-color: #1c1b1f;
  --body-secondary-color: #63636b;
  --body-secondary-hover: #1c1b1f;
  --link-color: #9e1b17;
  --rule-color: rgba(0, 0, 0, 0.12);

  /* Reading measure for the legal pages. The footer is a sibling of .page, not
     a child, so it reads this from :root to stay flush with the text column -
     see .container.page__inner and .page + .site-footer. */
  --page-measure: 85ch;

  /* Home */
  --hero-backdrop: #1b0a09;
  --hero-fade: 1500ms;

  /* The home column floats over the photograph rather than covering it. Phone
     default is edge to edge; the breakpoints below pull it in.
     100% not 100vw: 100vw includes the vertical scrollbar and so always
     overhangs the viewport's content box. */
  --column-width: 100%;
  --column-radius: 0px;
  /* How far the top of the column pokes above the fold, as a scroll hint. */
  --column-peek: 2rem;

  /* 0 at the top of the page, 1 once the column has taken over. Written by
     js/column.js; the fallback keeps the wordmark visible without script. */
  --scroll-progress: 0;

  /* Space reserved at the bottom of the home column for the glass footer */
  --footer-clearance: 9rem;

  --focus-ring: #e6321f;
}

@media (min-width: 900px) {
  :root {
    --column-width: 75vw;
    --column-radius: 24px;
    /* The footer is pinned bottom-right and is wider than the gutter beside
       the column, so it still overhangs the column's bottom corner - it only
       gets shorter here, from three stacked rows to two. */
    --footer-clearance: 7rem;
  }
}

@media (min-width: 1200px) {
  :root {
    --column-width: 50vw;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --body-bg: #16151a;
    --body-color: #eae8ee;
    --body-secondary-color: #a4a2ac;
    --body-secondary-hover: #eae8ee;
    --link-color: #ff8d7a;
    --rule-color: rgba(255, 255, 255, 0.14);
  }
}

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

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-sans);
  background-color: var(--body-bg);
  color: var(--body-color);
  -webkit-font-smoothing: antialiased;
}

body.has-hero {
  background-color: var(--hero-backdrop);
}

img {
  max-width: 100%;
}

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

/* 2. Bootstrap-compatible utilities
   --------------------------------------------------------------------------
   The footer markup uses Bootstrap 5 class names. These rules reimplement just
   the handful that markup needs, so the site carries no framework. Keep the
   utilities (.px-*, .py-*, .mx-n*) last within this section - like Bootstrap,
   they are meant to win over component defaults.
   ========================================================================== */

.container {
  width: 100%;
  margin-inline: auto;
  padding-inline: 1rem;
}

@media (min-width: 600px) {
  .container {
    max-width: 540px;
  }
}
@media (min-width: 768px) {
  .container {
    max-width: 720px;
  }
}
@media (min-width: 992px) {
  .container {
    max-width: 960px;
  }
}
@media (min-width: 1200px) {
  .container {
    max-width: 1140px;
  }
}

.nav {
  display: flex;
  flex-wrap: wrap;
  padding-left: 0;
  margin-bottom: 0;
  list-style: none;
}

.nav-link {
  display: block;
  padding-block: 0.5rem;
  padding-inline: 1rem;
  text-decoration: none;
  transition: color 0.15s ease-in-out;
}

.text-body-secondary {
  color: var(--body-secondary-color);
}

a.text-body-secondary:hover,
a.text-body-secondary:focus-visible {
  color: var(--body-secondary-hover);
}

.py-3 {
  padding-block: 1rem;
}

@media (min-width: 600px) {
  .py-sm-5 {
    padding-block: 3rem;
  }
}

.px-2 {
  padding-inline: 0.5rem;
}

.mx-n2 {
  margin-inline: -0.5rem;
}

/* 3. Icons
   ========================================================================== */

.icon-sprite {
  display: none;
}

.icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  vertical-align: -0.125em;
  flex: none;
}

/* 4. Footer
   ========================================================================== */

.site-footer p {
  margin: 0 0 0.25rem;
}

.site-footer .nav-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* Glass variant: pinned bottom-right over the hero and the gradient column. */
.site-footer.site-footer--glass {
  --body-secondary-color: rgba(255, 255, 255, 0.82);
  --body-secondary-hover: #ffffff;

  position: fixed;
  inset: auto 0 0 auto;
  z-index: 20;
  width: max-content;
  max-width: 100%;
  margin: 0;
  padding: 0.85rem 1.35rem 1rem;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  border-left: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 16px 0 0 0;
  background: rgba(20, 8, 8, 0.38);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 -2px 28px rgba(0, 0, 0, 0.35);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}

/* Without backdrop-filter the panel would be near-transparent: go opaque. */
@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .site-footer.site-footer--glass {
    background: rgba(20, 8, 8, 0.85);
  }
}

@media (max-width: 699.98px) {
  .site-footer.site-footer--glass {
    inset: auto 0 0 0;
    width: auto;
    border-left: 0;
    border-radius: 0;
    padding: 0.6rem 0.75rem 0.7rem;
  }

  .site-footer.site-footer--glass .nav {
    justify-content: center;
  }

  .site-footer.site-footer--glass p {
    text-align: center;
    font-size: 0.875rem;
  }
}

/* Phone. Must stay below the 699.98px block: both match under 600px, and
   these rules have to win. */
@media (max-width: 599.98px) {
  :root {
    /* No longer clearance for an overlay - the footer flows inside the column
       now, so this is just breathing room under the last card. */
    --footer-clearance: 1.5rem;
  }

  /* A fixed footer stacked one link per line would eat a third of the screen,
     so on phones it leaves the overlay and flows at the end of the column.

     position: relative, not static: a static footer joins the non-positioned
     paint layer, which renders *below* every positioned element - including
     the fixed, full-viewport .hero - so it would be hidden behind the photo.
     Relative keeps it in flow while making z-index apply. */
  .site-footer.site-footer--glass {
    position: relative;
    z-index: 1;
    width: auto;
    border: 0;
    border-radius: 0;
    /* Transparent: it sits on the column's gradient, which now extends behind
       it because the footer is inside .site-main. */
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-top: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: none;
  }

  .site-footer.site-footer--glass .nav {
    flex-direction: column;
    align-items: center;
    /* .mx-n2 only makes sense for a row of links. */
    margin-inline: 0;
  }
}

/* 5. Hero
   --------------------------------------------------------------------------
   Fixed layer behind the content. Scrolling lifts the content over an image
   that never moves - no background-attachment: fixed (which does not work on
   iOS Safari). The layout itself needs no script; the only scroll input is
   --scroll-progress, which crossfades the wordmark from the photograph to the
   column header.
   ========================================================================== */

.hero {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background-color: var(--hero-backdrop);
}

.hero__slides {
  position: absolute;
  inset: 0;
}

.hero__slide {
  position: absolute;
  inset: 0;
  background-image: var(--slide-1x);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity var(--hero-fade) ease-in-out;
  will-change: opacity;
}

.hero__slide.is-active {
  opacity: 1;
}

/* Larger and HiDPI displays get the 2560px derivative. */
@media (min-width: 1600px), (min-resolution: 1.5dppx) {
  .hero__slide {
    background-image: var(--slide-2x);
  }
}

/* Keeps the wordmark legible over bright skies. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.5) 0%,
    rgba(0, 0, 0, 0.18) 45%,
    rgba(0, 0, 0, 0.1) 70%,
    rgba(20, 8, 8, 0.55) 100%
  );
}

/* Hands the wordmark over to .site-main__brand as the column rises. */
.hero__title {
  position: absolute;
  top: 32%;
  left: 50%;
  transform: translate(-50%, -50%) scale(calc(1 - 0.08 * var(--scroll-progress)));
  opacity: calc(1 - var(--scroll-progress));
  width: 100%;
  margin: 0;
  padding-inline: 1rem;
  text-align: center;
  font-size: clamp(3rem, 12vw, 9rem);
  font-weight: 800;
  letter-spacing: 0.02em;
  line-height: 1;
  color: #fff;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.5);
}

/* 6. Home content column
   --------------------------------------------------------------------------
   A panel that rises out of the fold and then floats over the photograph,
   which stays visible in the gutters at every scroll position. No overflow
   here: it would break the sticky wordmark.
   ========================================================================== */

.site-main {
  position: relative;
  z-index: 1;
  width: min(100%, var(--column-width));
  margin-inline: auto;
  margin-top: calc(100vh - var(--column-peek));
  margin-top: calc(100svh - var(--column-peek));
  min-height: 100vh;
  color: #fff;

  background: #3f1210;
  background: linear-gradient(
    159deg,
    rgba(63, 18, 16, 1) 0%,
    rgba(127, 23, 16, 1) 25%,
    rgba(141, 23, 35, 1) 50%,
    rgba(184, 25, 24, 1) 75%,
    rgba(230, 50, 31, 1) 100%
  );
  border-radius: var(--column-radius) var(--column-radius) 0 0;

  /* Lifts the column off the photograph on every edge, not just the top. */
  box-shadow: 0 -12px 48px rgba(0, 0, 0, 0.5), 0 0 40px rgba(0, 0, 0, 0.35);
}

/* The padding lives here, not on .site-main, so the sticky wordmark below can
   sit flush against the top edge of the column. */
.container.site-main__inner {
  max-width: 880px;
  padding-block: clamp(2rem, 5vw, 3.5rem) var(--footer-clearance);
}

/* Takes the wordmark over from .hero__title, then pins to the top of the
   column with the cards scrolling underneath. */
.site-main__brand {
  position: sticky;
  top: 0;
  z-index: 5;
  margin: 0;
  padding: 0.85rem 1rem;
  border-radius: var(--column-radius) var(--column-radius) 0 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(63, 18, 16, 0.72);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
  text-align: center;
  font-size: clamp(1.75rem, 3vw, 4rem);
  font-weight: 800;
  letter-spacing: 0.02em;
  line-height: 1.2;
  color: #fff;
  opacity: var(--scroll-progress);
}

@supports not (
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
) {
  .site-main__brand {
    background: rgba(63, 18, 16, 0.95);
  }
}

.app-list {
  display: grid;
  /* minmax(0, …) lets the track shrink past its content's min-content width.
     A plain 1fr floors at min-content and a long word widens the page. */
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 2.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Recessed: pressed into the gradient rather than floating above it. */
.app-card {
  /* NOT a no-op: grid items default to min-width:auto, which refuses to shrink
     below the widest unbreakable word in the card. Without this, one long
     title (a German compound, or PROJECT_NAME_PLACEHOLDER) pushes the whole
     page wider than a phone screen and a horizontal scrollbar appears. */
  min-width: 0;
  padding: 1rem 1rem 1.35rem;
  border-radius: 18px;
  background: rgba(0, 0, 0, 0.22);
  box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    0 1px 0 rgba(255, 255, 255, 0.07);
}

/* Screenshot carousel
   --------------------------------------------------------------------------
   Slides are matched on height and left to find their own width, so portrait
   phone captures and landscape tablet captures can sit in the same track.
   Snapping is native; the buttons only nudge the scroll position.
   ========================================================================== */

.carousel {
  --carousel-height: clamp(220px, 45vh, 420px);
  position: relative;
}

.carousel__viewport {
  overflow-x: auto;
  overflow-y: hidden;
  height: var(--carousel-height);
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.35);
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.carousel__viewport::-webkit-scrollbar {
  display: none;
}

/* position: relative makes this the offset parent, so a slide's offsetLeft is
   its position within the track - which is what carousel.js scrolls to. */
.carousel__track {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  height: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.carousel__slide {
  flex: 0 0 auto;
  height: 100%;
  scroll-snap-align: center;
}

.carousel__slide img {
  display: block;
  height: 100%;
  width: auto;
  max-width: none;
  object-fit: contain;
  border-radius: 10px;
}

/* On a phone there is no room for a slide to keep its natural width, so each
   one becomes exactly the column width and the image is letterboxed inside it
   - one screenshot per view instead of a wide shot overhanging the card. */
@media (max-width: 599.98px) {
  .carousel__slide {
    flex-basis: 100%;
  }

  .carousel__slide img {
    width: 100%;
    max-width: 100%;
  }
}

/* Centred on the viewport, not on .carousel - which is taller by the dots. */
.carousel__nav {
  position: absolute;
  top: calc(var(--carousel-height) / 2);
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  background: rgba(20, 8, 8, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: #fff;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.carousel__nav:hover,
.carousel__nav:focus-visible {
  background: rgba(20, 8, 8, 0.8);
  border-color: rgba(255, 255, 255, 0.5);
}

.carousel__nav--prev {
  left: 0.5rem;
}

.carousel__nav--next {
  right: 0.5rem;
}

/* Touch devices swipe; the buttons would only cover the screenshots. */
@media (hover: none) {
  .carousel__nav {
    display: none;
  }
}

.carousel__dots {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
}

.carousel__dot {
  display: block;
  width: 0.55rem;
  height: 0.55rem;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.35);
  cursor: pointer;
  transition: background-color 0.15s ease-in-out, transform 0.15s ease-in-out;
}

.carousel__dot:hover {
  background: rgba(255, 255, 255, 0.6);
}

.carousel__dot[aria-current="true"] {
  background: #fff;
  transform: scale(1.25);
}

/* German compounds get long. Break them rather than overflow the card; the
   page is lang="de", so hyphens: auto breaks at correct syllable boundaries. */
.app-card__title,
.app-card__description {
  overflow-wrap: break-word;
  hyphens: auto;
}

.app-card__title {
  margin: 1.1rem 0 0.4rem;
  font-size: clamp(1.25rem, 1rem + 1vw, 1.6rem);
  line-height: 1.2;
}

.app-card__description {
  margin: 0 0 1.25rem;
  color: rgba(255, 255, 255, 0.82);
  line-height: 1.6;
}

.app-card__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.app-card__link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 1.1rem;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-weight: 600;
  text-decoration: none;
  transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.app-card__link:hover,
.app-card__link:focus-visible {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.5);
}

/* Outline only, so Google Play stays the primary action on the card. */
.app-card__link--secondary {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.22);
  font-weight: 500;
}

.app-card__link--secondary:hover,
.app-card__link--secondary:focus-visible {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.4);
}

/* 7. Plain text pages
   ========================================================================== */

.page {
  padding-block: clamp(2.5rem, 6vw, 4.5rem) 1rem;
  line-height: 1.65;
}

.container.page__inner {
  max-width: var(--page-measure);
}

/* Headings are the one place a single German compound can be wider than a
   narrow viewport: "Datenschutzerklaerung" alone is ~280px at the h1's 28px
   floor, against 288px of usable width on a 320px screen. Body copy never hits
   this because it is set much smaller. hyphens works here because
   _layouts/default.html sets lang on <html>; break-word is the fallback for
   the browsers that ignore it. */
.page h1,
.page h2 {
  overflow-wrap: break-word;
  hyphens: auto;
}

.page h1 {
  margin: 0 0 1.5rem;
  font-size: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem);
  line-height: 1.15;
}

.page h2 {
  margin: 2rem 0 0.5rem;
  font-size: 1.2rem;
}

.page p {
  margin: 0 0 1rem;
}

.page a {
  color: var(--link-color);
}

.page__meta {
  color: var(--body-secondary-color);
  font-size: 0.9375rem;
}

/* .m-table is the two-column "Speicherort / Inhalt" table in the app privacy
   policy. Rules rather than boxes: the horizontal lines are enough to bind a
   row together, and a full grid would fight the plain-text look of the page. */
.m-table {
  width: 100%;
  margin: 0 0 1.5rem;
  border-collapse: collapse;
  font-size: 0.9375rem;
  /* Fixed layout keeps the narrow first column from being stretched by the
     long prose in the second - auto layout hands most of the width to whichever
     column holds the most text, which here is the wrong one. */
  table-layout: fixed;
}

.m-table th,
.m-table td {
  padding: 0.6rem 0.75rem 0.6rem 0;
  text-align: left;
  vertical-align: top;
  /* German compounds ("Verknuepfungsverzeichnis") are longer than the first
     column is wide on a phone, and would otherwise push the table sideways. */
  overflow-wrap: break-word;
  hyphens: auto;
}

.m-table th:last-child,
.m-table td:last-child {
  padding-right: 0;
}

.m-table th {
  font-weight: 600;
  border-bottom: 1px solid var(--body-secondary-color);
}

.m-table td {
  border-top: 1px solid var(--rule-color);
}

/* thead's own bottom rule already separates the first body row. */
.m-table tbody tr:first-child td {
  border-top: 0;
}

.m-table th:first-child,
.m-table td:first-child {
  width: 34%;
}

/* Phone: a 34% first column leaves too little for the prose beside it, so give
   both columns an equal share and tighten the padding. */
@media (max-width: 599.98px) {
  .m-table th,
  .m-table td {
    padding: 0.5rem 0.5rem 0.5rem 0;
  }

  .m-table th:first-child,
  .m-table td:first-child {
    width: 42%;
  }
}

/* The remaining classes come from the Datenschutz-Generator markup and are used
   by both privacy policies. They are styled here rather than left to the browser
   so the legal pages read as part of the site.
   -------------------------------------------------------------------------- */

/* Table of contents at the top of a policy. Markers are dropped because every
   row is a link: the underline already marks it as a list item to the eye, and
   the bullets only add a second column of noise. */
.index {
  margin: 0 0 1.5rem;
  padding-left: 0;
  list-style: none;
}

.index li {
  margin-bottom: 0.35rem;
}

/* .page a already sets the colour; this only lifts the underline off the
   descenders, which matters here because the list is nothing but links. */
.index-link {
  text-underline-offset: 0.15em;
}

/* The "Verarbeitete Datenarten / Rechtsgrundlagen / ..." lists that close a
   processing section. Indented and set slightly smaller so they read as an
   annex to the prose above rather than as more prose. */
.m-elements {
  margin: 0 0 1.5rem;
  padding-left: 1.25rem;
  font-size: 0.9375rem;
}

.m-elements li {
  margin-bottom: 0.5rem;
}

/* Glossary entries are each a full paragraph, so they need more air between
   them than a normal list to stay separable. */
.glossary {
  padding-left: 1.25rem;
}

.glossary li {
  margin-bottom: 0.85rem;
}

/* Monospace fonts render larger than the surrounding text at the same nominal
   size, which makes the obfuscated mail addresses jump out of the paragraph.
   0.9em pulls them back onto the line. break-word keeps a long address from
   widening the page on a phone, since it has no spaces to break at. */
.page code {
  font-size: 0.9em;
  overflow-wrap: break-word;
}

/* Attribution line closing the document. Same treatment as .page__meta: it is
   housekeeping, not part of the text. */
.seal {
  margin-top: 2.5rem;
  color: var(--body-secondary-color);
  font-size: 0.875rem;
}

/* The footer carries .container, so on its own it would follow the site-wide
   540/720/960/1140px steps and sit narrower than the text above it. Both
   selectors are two classes deep, so this wins over the .container breakpoints
   without !important - and both sides keep .container's auto margins and 1rem
   gutters, so the edges line up exactly. */
.page + .site-footer {
  border-top: 1px solid var(--rule-color);
  max-width: var(--page-measure);
}

/* Motion
   ========================================================================== */

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

  /* The crossfade still tracks the scroll position; only the zoom goes. */
  .hero__title {
    transform: translate(-50%, -50%);
  }

  .carousel__dot {
    transition: none;
  }

  * {
    scroll-behavior: auto !important;
  }
}
