/* ============================================================
   whymars — Brandboard
   1:1-Umsetzung des Figma-Boards (2000 × 3166).

   Skalierungsmodell
   -----------------
   .stage ist der einzige "äußere" Container. 100cqw = 2000 Design-px,
   also entspricht 1cqw exakt 20 Design-px (Board-Padding, Gaps, Radius).

   Jede Kachel ist selbst ein Container. In .t-in gilt
       --s = 1 Design-px der jeweiligen Kachel
   (= 100cqw / Design-Breite). Alle Innenmaße rechnen in --s und
   skalieren dadurch immer proportional mit der Kachel — der Inhalt
   kann also bei keiner Viewport-Breite umbrechen oder verrutschen.
   ============================================================ */

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

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

body {
  margin: 0;
  /* Embedded in the portfolio detail page — no outer frame/background. */
  padding: 0;
  background: transparent;
  font-family: Montserrat, system-ui, sans-serif;
  color: #1A263E;
}

img { display: block; max-width: none; }
p   { margin: 0; }

/* ── Bühne / Board ───────────────────────────────────────── */

.stage {
  container-type: inline-size;
  width: 100%;
  max-width: 2000px;
  margin-inline: auto;
}

.board {
  --g: 1cqw;                      /* 20 Design-px */
  display: flex;
  flex-direction: column;
  gap: var(--g);
  padding: 0;                     /* flush with the text column above */
  border-radius: 0;
  background: transparent;        /* let the page's card-blue show between the tiles */
}

.row  { display: flex; gap: var(--g); align-items: flex-start; }
.col  { display: flex; flex-direction: column; gap: var(--g); flex: var(--w) 1 0; }

/* In der Spalte läuft die Hauptachse vertikal — die Kacheln dürfen dort
   nicht über flex-basis:0 in der Höhe kollabieren. */
.col > .tile,
.col > .row { flex: none; width: 100%; }
.pair { display: flex; flex-wrap: wrap; gap: var(--g); flex: var(--w) 1 0; align-items: flex-start; }

/* ── Kachel-Grundgerüst ──────────────────────────────────── */

.tile {
  position: relative;
  flex: var(--w) 1 0;
  aspect-ratio: var(--w) / var(--h);
  border-radius: 0.5cqw;                                  /* 10 Design-px */
  box-shadow: 0.1cqw 0.1cqw 0.5cqw rgba(0, 0, 0, .15);    /* 2 2 10 */
  overflow: hidden;
  container-type: inline-size;
}


.tile--outlined { outline: 1px solid rgba(24, 24, 59, .1); outline-offset: -1px; }

.t-in { position: absolute; inset: 0; }

/* --s = 1 Design-px, je Kachelbreite */
.tile[data-w="310"]  > .t-in { --s: 0.3225806cqw; }
.tile[data-w="368"]  > .t-in { --s: 0.2717391cqw; }
.tile[data-w="475"]  > .t-in { --s: 0.2105263cqw; }
.tile[data-w="640"]  > .t-in { --s: 0.1562500cqw; }
.tile[data-w="912"]  > .t-in { --s: 0.1096491cqw; }
.tile[data-w="970"]  > .t-in { --s: 0.1030928cqw; }
.tile[data-w="1300"] > .t-in { --s: 0.0769231cqw; }

/* Kacheln, die vollflächig aus einem Asset bestehen */
.tile-flat > img { width: 100%; height: 100%; }

/* Schwimmender Schrank in der Illustration.

   Der Schrank steht in einer Ellipse, die als Wasser liest. Er sinkt darin ein
   und kommt wieder hoch, als werde er getragen.

   Drei Ebenen, alle mit demselben viewBox und in derselben Größe gezeichnet,
   also von selbst deckungsgleich:

     illu-base.svg       Grund, Person, Wasserfläche        steht
     illu-cabinet.svg    der Schrank                        bewegt sich
     illu-waterline.svg  die Wassereintrittskante           steht

   Die Eintrittskante lag zunächst mit im Schrank und wanderte deshalb mit —
   damit war der Effekt hin: eine Wasserlinie, die sich mit dem Schwimmenden
   mitbewegt, liest sich nicht als Wasser. Sie ist jetzt herausgelöst und liegt
   als oberste, stehende Ebene.

   An Teile einer über <img> geladenen SVG kommt CSS nicht heran; die ganze
   Datei inline ins Markup zu ziehen hieße, 115 kB dorthin zu verschieben.

   Der Schrank ist **exakt bis zur Wasserlinie gezeichnet und keinen Pixel
   darunter** (nachgemessen: untere Kante 249 → 260 → 242, passend zum Bogen
   238 → 257). Deshalb bewegt er sich ausschließlich nach unten: höbe er sich
   über seine gezeichnete Unterkante, klaffte dort der Grund durch. */
.tile-illu { position: relative; }

/* Der Beschnitt steht still, der Schrank bewegt sich darin — deshalb zwei
   Elemente. Läge beides auf einem, wanderte der Beschnitt mit der
   Transformation mit und schnitte nichts ab.

   Das Polygon folgt dem Bogen der Wasserlinie, abgetastet an 29 Stellen und in
   Prozent der Bildfläche umgerechnet (viewBox 8 8 475 445) — dadurch skaliert
   es mit der Kachel. Außerhalb des Bogens läuft es an die Ränder, dort ist
   ohnehin kein Schrank.

   Die Schnittkante selbst muss nicht genau sitzen: die Wasserlinie darüber ist
   6,9 Einheiten breit und deckt sie zu. */
.tile-illu .il-clip {
  position: absolute;
  inset: 0;
  clip-path: polygon(
    0% 0%,
    100% 0%,
    100% 51.64%,
    78.65% 51.64%,
    77.28% 51.75%,
    75.94% 52.43%,
    74.57% 53.10%,
    73.22% 53.60%,
    71.85% 54.09%,
    70.51% 54.47%,
    69.14% 54.81%,
    67.79% 55.08%,
    66.42% 55.33%,
    65.07% 55.53%,
    63.71% 55.69%,
    62.36% 55.82%,
    61.01% 55.91%,
    59.64% 55.98%,
    58.29% 56.00%,
    56.93% 55.93%,
    55.58% 55.84%,
    54.21% 55.73%,
    52.86% 55.57%,
    51.49% 55.39%,
    50.15% 55.15%,
    48.78% 54.90%,
    47.43% 54.58%,
    46.06% 54.22%,
    44.72% 53.75%,
    43.35% 53.26%,
    42.00% 52.70%,
    40.63% 52.63%,
    0% 52.63%);
}

.tile-illu .il-person,
.tile-illu .il-cabinet,
.tile-illu .il-waterline {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.tile-illu .il-cabinet {
  transform-origin: 61.4% 38.3%;
  animation: il-float 4.5s ease-in-out infinite;
}

/* Einsinken um 8 Design-Pixel und zurück, dazu vier Zehntel Grad Neigung — die
   nimmt der Bewegung das Fahrstuhlhafte. Drei ungleiche Stationen wie beim
   schwebenden Produktbild, damit es nicht im Takt pendelt.

   Gedreht wird um die Mitte des Schranks, nicht um die Bildmitte: das Bild ist
   die ganze Kachel, eine Drehung um deren Mitte risse ihn weit zur Seite. Der
   Schrank sitzt bei (201…398 | 99…258) im viewBox, der bei (8|8) beginnt;
   seine Mitte liegt damit auf 61,4 % | 38,3 %.

   Die Neigung hebt eine Seite um gut einen halben Design-Pixel an. Das bleibt
   unter der halben Breite der Wasserlinie und fällt deshalb nicht auf.

   Eine Fassung mit ±1,5 Grad und eigenem Takt fürs Kippen ist verworfen: sie
   war zwar deutlich zu sehen, das Kippen trat aber vor das Einsinken und der
   Schrank wirkte eher schaukelnd als treibend. */
@keyframes il-float {
  0%   { transform: translateY(0)                    rotate(-.35deg); }
  36%  { transform: translateY(calc(8 * var(--s)))   rotate(.3deg); }
  68%  { transform: translateY(calc(2.5 * var(--s))) rotate(-.1deg); }
  100% { transform: translateY(0)                    rotate(-.35deg); }
}

/* Wellenringe um Person und Schrank.

   Je drei Ellipsen laufen nacheinander aus dem Stand nach außen und blenden
   dabei ab. Gezeichnet sind sie als CSS-Rahmen, nicht als Asset — Größe und
   Deckkraft müssen sich bewegen, und dafür braucht es kein Bild.

   **Die Farbe ist nicht frei gewählt.** Die vorhandenen Ringe der Vorlage
   (Vector_10 und Vector_12 um die Person, Vector_2 und Vector_3 um den
   Schrank) sind in #ABACCC gestrichen, also in der Hintergrundfarbe, und
   liegen außerhalb der dunklen Ellipse — sie sind dadurch gar nicht zu sehen.
   Neue Ringe müssen deshalb dunkler sein als der Grund, und #1A263E ist der
   Ton, den auch der Wasserschatten nutzt.

   Beide Gruppen liegen im Markup vor Person und Schrank und damit **unter**
   ihnen — beide verdecken ihre Ringe selbst. Anders ginge es nicht: die Ringe
   um den Schrank reichen bis x = 103 und kreuzen dort den hellen Pullover der
   Person. Lägen sie darüber, zöge eine dunkle Linie quer über sie hinweg.

   Die Person war dafür aus illu-base.svg herauszulösen. Vorher trug allein ein
   Zufall die Überdeckung — die Hosenbeine haben dasselbe #1A263E wie die Ringe
   und ließen sie dort verschwinden. Das half aber nur unten; über Pullover und
   Händen hätten sie durchgezogen.

   Die Mitten sitzen auf den Schatten der Vorlage — Vector_9 spannt
   (66…167 | 344…380), Vector (170…405 | 183…268); der viewBox beginnt bei
   (8|8). Das Seitenverhältnis 2,75 ist von den Schatten übernommen und hält
   die Aufsicht.

   Beide Gruppen wachsen um dieselben 59 Design-Pixel im Halbmesser, damit die
   Welle überall gleich weit trägt — die Ringe um den Schrank starten nur
   größer, weil er größer ist. */
.tile-illu .il-ripple {
  position: absolute;
  transform: translate(-50%, -50%);
  box-sizing: border-box;
  border: calc(1.25 * var(--s)) solid #1A263E;
  border-radius: 50%;
  opacity: 0;
  animation: il-ripple 6s linear infinite;
  animation-delay: calc(var(--i) * var(--takt));
}

.tile-illu .il-ripple--mensch {
  --takt: 2s;
  --von-b: 96;  --von-h: 35;
  --bis-b: 214; --bis-h: 78;
  left: calc(108.5 * var(--s));
  top:  calc(354 * var(--s));
}

/* 7 s statt 6 und ein anderer Versatz: beide Gruppen sollen nicht im
   Gleichtakt schlagen. */
.tile-illu .il-ripple--schrank {
  --takt: 2.33s;
  --von-b: 235; --von-h: 85;
  --bis-b: 353; --bis-h: 128;
  left: calc(279.5 * var(--s));
  top:  calc(217.5 * var(--s));
  animation-duration: 7s;
}

/* Bewegt werden Breite und Höhe, nicht ein Maßstab: eine Skalierung zöge den
   Rahmen mit in die Breite, und die Ringe würden nach außen hin dicker. Die
   Verschiebung um die halbe eigene Größe hält sie dabei zentriert.

   Die Maße kommen aus Eigenschaften der Elemente, deshalb reicht ein
   Keyframe-Satz für beide Gruppen. */
@keyframes il-ripple {
  0%   { width: calc(var(--von-b) * var(--s));
         height: calc(var(--von-h) * var(--s)); opacity: 0; }
  15%  { opacity: .14; }
  100% { width: calc(var(--bis-b) * var(--s));
         height: calc(var(--bis-h) * var(--s)); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tile-illu .il-ripple { animation: none; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tile-illu .il-cabinet { animation: none; transform: none; }
}

/* ── Wiederverwendbare Wortmarke ─────────────────────────── */
/* Innengeometrie relativ zur Logo-Box (523 × 76.1107 im Original) */

.wm-logo   { position: absolute; display: block; }
.wm-logo .m { position: absolute; left: 0;         top: 0; width: 22.6815%; height: 100%; }
.wm-logo .w { position: absolute; left: 27.3665%;  top: 0; width: 69.7315%; height: 100%; }
.wm-logo .r { position: absolute; left: 97.0937%;  top: 0; width: 2.9059%;  height: 19.968%; }

/* ── 1 · iPhone-Mockup (640 × 445) ───────────────────────── */
/* Export enthält 10 Design-px Rand — wird von der Kachel beschnitten. */

.tile-iphone > img {
  position: absolute;
  left: calc(-10 * var(--s));
  top:  calc(-10 * var(--s));
  width:  calc(660 * var(--s));
  height: calc(465 * var(--s));
}

/* iOS-Mitteilungs-Badge auf dem whymars-App-Icon im Dock.
   Alle Verhältnisse an einem echten iOS-Homescreen abgemessen
   (iPhone 17 Pro, 3×, Nachrichten-App im Dock):

     App-Icon      192 px
     Badge         78 px          = 40,625 % der Icon-Kantenlänge
     Überstand     33 px          = 17,1875 % über die rechte obere Ecke
     Ziffernhöhe   33 px          = 42,3 % des Badge-Durchmessers
     Farbe         #FF383C        flach, ohne Rand und ohne Schatten
     Ziffer        weiß, mittig, in beiden Achsen exakt zentriert

   Das App-Icon liegt in der Kachel bei 299,918 / 170,490 und ist
   101,768 Design-Pixel groß (aus dem Figma-Knotenbaum des Mockups). */
.tile-iphone .ios-badge {
  --icon:  calc(101.768 * var(--s));
  --badge: calc(var(--icon) * 0.40625);

  position: absolute;
  left: calc(299.918 * var(--s) + var(--icon) * 0.765625);
  top:  calc(170.490 * var(--s) - var(--icon) * 0.171875);
  width:  var(--badge);
  height: var(--badge);

  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #FF383C;

  font-family: -apple-system, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-weight: 500;
  font-size: calc(var(--badge) * 0.592);
  line-height: 1;
  color: #fff;
  font-variant-numeric: tabular-nums;
  user-select: none;

  animation: ios-badge-cycle 11s infinite;
}

.tile-iphone .ios-badge > span {
  grid-area: 1 / 1;
  opacity: 0;
}

/* Jede Ziffer hat ihr eigenes Keyframe-Set mit absoluten Zeitpunkten statt
   eines gemeinsamen mit animation-delay — nur so lassen sich ungleiche
   Abstände zwischen den Meldungen setzen. */
.tile-iphone .ios-badge > span:nth-child(1) { animation: ios-badge-d1 11s infinite; }
.tile-iphone .ios-badge > span:nth-child(2) { animation: ios-badge-d2 11s infinite; }
.tile-iphone .ios-badge > span:nth-child(3) { animation: ios-badge-d3 11s infinite; }
.tile-iphone .ios-badge > span:nth-child(4) { animation: ios-badge-d4 11s infinite; }

@keyframes ios-badge-d1 {   0%,     13.63% { opacity: 1 }  13.64%, 100% { opacity: 0 } }
@keyframes ios-badge-d2 {   0%,     13.63% { opacity: 0 }  13.64%, 20.90% { opacity: 1 }  20.91%, 100% { opacity: 0 } }
@keyframes ios-badge-d3 {   0%,     20.90% { opacity: 0 }  20.91%, 39.99% { opacity: 1 }  40%,    100% { opacity: 0 } }
@keyframes ios-badge-d4 {   0%,     39.99% { opacity: 0 }  40%,    82.27% { opacity: 1 }  82.28%, 100% { opacity: 0 } }

/* Zyklus 11 s. Die Meldungen trudeln in ungleichen Abständen ein — erst eine
   Pause, dann zwei kurz hintereinander, dann wieder eine längere Pause:

     0,0 s        1 erscheint
     1,5 s        2          (Abstand 1,5 s)
     2,3 s        3          (Abstand 0,8 s)
     4,4 s        4          (Abstand 2,1 s)
     4,4–8,6 s    4 bleibt stehen
     8,6–9,05 s   Badge verschwindet, als wären die Meldungen gelesen
     9,05–11,0 s  Pause, dann von vorn

   Federweg je Wechsel: hoch auf 1,12, minimal unter 1 durchschwingen, dann
   einpendeln — zusammen 0,42 s (3,82 % des Zyklus). */
@keyframes ios-badge-cycle {
  /* Auftritt mit der 1 */
  0%      { opacity: 0; transform: scale(.3);   animation-timing-function: ease-out; }
  2.18%   { opacity: 1; transform: scale(1.15); animation-timing-function: ease-in-out; }
  4.36%   { opacity: 1; transform: scale(1); }

  /* Wechsel auf 2 — 1,5 s */
  13.64%  { transform: scale(1);    animation-timing-function: ease-out; }
  14.73%  { transform: scale(1.12); animation-timing-function: ease-in-out; }
  16.09%  { transform: scale(.985); animation-timing-function: ease-in-out; }
  17.45%  { transform: scale(1); }

  /* Wechsel auf 3 — 2,3 s */
  20.91%  { transform: scale(1);    animation-timing-function: ease-out; }
  22.00%  { transform: scale(1.12); animation-timing-function: ease-in-out; }
  23.36%  { transform: scale(.985); animation-timing-function: ease-in-out; }
  24.73%  { transform: scale(1); }

  /* Wechsel auf 4 — 4,4 s */
  40%     { transform: scale(1);    animation-timing-function: ease-out; }
  41.09%  { transform: scale(1.12); animation-timing-function: ease-in-out; }
  42.45%  { transform: scale(.985); animation-timing-function: ease-in-out; }
  43.82%  { transform: scale(1); }

  /* Abgang nach langem Stehen */
  78.18%  { opacity: 1; transform: scale(1); }
  82.27%,
  100%    { opacity: 0; transform: scale(.3); }
}

@media (prefers-reduced-motion: reduce) {
  .tile-iphone .ios-badge            { animation: none; opacity: 1; transform: none; }
  .tile-iphone .ios-badge > span     { animation: none; opacity: 0; }
  .tile-iphone .ios-badge > span:first-child { opacity: 1; }
}

/* ── 2 · Hero (1300 × 445) ───────────────────────────────── */

.tile-hero {
  background: linear-gradient(108.896deg, #1B2740 0%, #0D0D21 100%);
  /* Der Verlauf ist der Backdrop, mit dem das Video verrechnet wird —
     isolate hält die Verrechnung innerhalb der Kachel. */
  isolation: isolate;
  /* Stellschrauben für das Hintergrundvideo.
     Das Video ist deutlich kontrastreicher als das Standbild im Design —
     ohne das Abdunkeln brennen die Tropfen unter color-dodge zu Weiß aus. */
  --hero-blend: color-dodge;
  --hero-grade: grayscale(1) brightness(.48) contrast(1.15);
  /* Ausblendung nach rechts. Die Zwischenstopps machen den Verlauf weicher,
     als es zwei Stopps allein täten. */
  --hero-fade: linear-gradient(to right,
      rgba(0,0,0,1)   0%,
      rgba(0,0,0,1)  24%,
      rgba(0,0,0,.7) 45%,
      rgba(0,0,0,.3) 63%,
      rgba(0,0,0,0)  80%);
}

.tile-hero .hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Bewegtbild: entsättigt, mit dem Verlauf verrechnet und nach rechts
   ausgeblendet, damit nicht die ganze Kachel voller Regen liegt. */
.tile-hero .hero-bg--video {
  filter: var(--hero-grade);
  mix-blend-mode: var(--hero-blend);
  -webkit-mask-image: var(--hero-fade);
          mask-image: var(--hero-fade);
}

/* Rückfallebene ohne Animation: das fertig komponierte Standbild aus dem
   Design. Bewusst als CSS-Hintergrund statt als <img> — so lädt die Datei
   nur, wenn die Media Query auch wirklich greift. */
@media (prefers-reduced-motion: reduce) {
  .tile-hero {
    background:
      url("assets/hero-bg.png") center / cover no-repeat,
      linear-gradient(108.896deg, #1B2740 0%, #0D0D21 100%);
  }
  .tile-hero .hero-bg--video,
  .tile-hero .hero-source { display: none; }
}

.tile-hero .hero-mark {
  position: absolute;
  left: calc(220 * var(--s));
  top:  calc(183 * var(--s));
  width:  calc(124.686 * var(--s));
  height: calc(80 * var(--s));
}

.tile-hero .hero-slash {
  position: absolute;
  left: calc(363.603 * var(--s));
  top:  calc(195.820 * var(--s));
  width:  calc(36.4039 * var(--s));
  height: calc(53.8865 * var(--s));
}

.tile-hero .hero-head {
  position: absolute;
  left: calc(418.924 * var(--s));
  top:  calc(195.821 * var(--s));
  font-style: italic;
  font-weight: 600;
  font-size: calc(41.026 * var(--s));
  line-height: 1.3;
  color: #fff;
  white-space: nowrap;
}

/* ── 3 · Icon-Set (640 × 445) ────────────────────────────── */

.tile-icons { background: #A0FF00; }

.tile-icons .ic {
  position: absolute;
  left: calc(var(--x) * var(--s));
  top:  calc(var(--y) * var(--s));
  width:  calc(32 * var(--s));
  height: calc(32 * var(--s));

  /* Icons gehen einzeln zwischen aktiv (100 %) und inaktiv (50 %) hin und her */
  animation: icon-idle var(--dur) ease-in-out var(--del) infinite;
}

/* Zwei kurze Absenkungen je Runde an ungleichen Stellen — zusammen mit den
   unterschiedlichen Laufzeiten unten ergibt das ein Muster, das sich
   praktisch nicht wiederholt. Die Haltezeit unten ist bewusst knapp, damit
   nie zu viele Icons gleichzeitig inaktiv wirken. */
@keyframes icon-idle {
  0%,   16%  { opacity: 1; }
  23%,  29%  { opacity: .5; }
  36%,  67%  { opacity: 1; }
  74%,  80%  { opacity: .5; }
  87%,  100% { opacity: 1; }
}

/* Laufzeiten teilerfremd gewählt, damit die Icons nie gemeinsam in Takt
   geraten. Die negativen Verzögerungen setzen jedes Icon an einem anderen
   Punkt seiner Runde ein — sonst starteten beim Laden alle gleichzeitig. */
.tile-icons .ic:nth-child(1)  { --dur: 13s; --del:  -2.4s; }
.tile-icons .ic:nth-child(2)  { --dur: 17s; --del:  -7.1s; }
.tile-icons .ic:nth-child(3)  { --dur: 11s; --del:  -0.6s; }
.tile-icons .ic:nth-child(4)  { --dur: 19s; --del: -11.3s; }
.tile-icons .ic:nth-child(5)  { --dur: 23s; --del:  -5.8s; }
.tile-icons .ic:nth-child(6)  { --dur: 14s; --del:  -9.2s; }
.tile-icons .ic:nth-child(7)  { --dur: 21s; --del:  -3.7s; }
.tile-icons .ic:nth-child(8)  { --dur: 12s; --del: -14.1s; }
.tile-icons .ic:nth-child(9)  { --dur: 18s; --del:  -6.4s; }
.tile-icons .ic:nth-child(10) { --dur: 16s; --del:  -1.9s; }
.tile-icons .ic:nth-child(11) { --dur: 25s; --del: -12.7s; }
.tile-icons .ic:nth-child(12) { --dur: 15s; --del:  -8.3s; }
.tile-icons .ic:nth-child(13) { --dur: 22s; --del:  -4.2s; }
.tile-icons .ic:nth-child(14) { --dur: 20s; --del: -16.5s; }
.tile-icons .ic:nth-child(15) { --dur: 24s; --del: -10.8s; }

@media (prefers-reduced-motion: reduce) {
  .tile-icons .ic { animation: none; opacity: 1; }
}

/* Warenkorb liegt im Original leicht versetzt im 32er-Raster */
.tile-icons .ic-cart {
  position: absolute;
  left: calc(307 * var(--s));
  top:  calc(138.25 * var(--s));
  width:  calc(24.75 * var(--s));
  height: calc(25.75 * var(--s));
}

/* ── 4 · Feature-Liste (368 × 445) ───────────────────────── */

.tile-feat { background: linear-gradient(140.410deg, #1A253F 0%, #0D0D21 100%); }

/* Auswahlrad wie die Zeiteinstellung in einer App.

   Sichtbar sind immer genau drei Einträge; der vierte steht auf der
   Rückseite der Trommel und ist ausgeblendet. Plätze in Design-Pixeln,
   gemessen ab der Kachelmitte:

     -96  hinter der Trommel   unsichtbar
     -48  oben                 50 %, leicht verkleinert
       0  Mitte                100 %, volle Größe  ← aktiv
     +48  unten                50 %, leicht verkleinert
     +96  hinter der Trommel   unsichtbar

   Die Verkleinerung nach außen und die weichen Kanten oben und unten
   erzeugen den Eindruck einer gewölbten Trommel, über die man scrollt. */
.tile-feat .ft-wheel {
  --ft-fade: linear-gradient(to bottom,
      transparent 0%, #000 26%, #000 74%, transparent 100%);

  position: absolute;
  left: 0;
  top: calc(114.5 * var(--s));
  width: 100%;
  height: calc(216 * var(--s));

  -webkit-mask-image: var(--ft-fade);
          mask-image: var(--ft-fade);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

.tile-feat .ft-item {
  position: absolute;
  left: 0;
  top: calc(92 * var(--s));         /* aktive Position auf halber Kachelhöhe */
  width: 100%;
  height: calc(32 * var(--s));
  /* Verkleinert wird um die Icon-Mitte, damit das Icon in der Spur bleibt
     und nur die Zeile zu ihm hin schrumpft. */
  transform-origin: calc(92 * var(--s)) center;
  animation: ft-wheel 8s infinite;
}

/* Phasen so gesetzt, dass die Einträge in ihrer Listenreihenfolge aktiv
   werden — NFC, Versand, Material, Versicherung — und unterhalb des aktiven
   Eintrags immer der als Nächstes folgende steht. */
.tile-feat .ft-item:nth-child(1) { animation-delay: -4s; }   /* startet aktiv */
.tile-feat .ft-item:nth-child(2) { animation-delay: -2s; }
.tile-feat .ft-item:nth-child(3) { animation-delay:  0s; }
.tile-feat .ft-item:nth-child(4) { animation-delay: -6s; }

.tile-feat .ft-item img {
  position: absolute;
  left: calc(76 * var(--s));
  top: 0;
  width:  calc(32 * var(--s));
  height: calc(32 * var(--s));
}

.tile-feat .ft-item span {
  position: absolute;
  left: calc(116 * var(--s));
  top:  calc(4 * var(--s));
  font-family: "Geist Mono", ui-monospace, monospace;
  font-weight: 400;
  font-size: calc(14 * var(--s));
  line-height: calc(25 * var(--s));
  color: #fff;
  white-space: nowrap;
}

/* 8 s je Runde, 2 s je Platz: 1,45 s Ruhe, dann 0,55 s weiterscrollen.
   Kein Überschwingen — die Bewegung bremst aus wie ein Rad, das einrastet. */
@keyframes ft-wheel {
  0%,   18%  { transform: translateY(calc(96 * var(--s)))  scale(.7);  opacity: 0;
               animation-timing-function: cubic-bezier(.25, 1, .3, 1); }
  25%,  43%  { transform: translateY(calc(48 * var(--s)))  scale(.86); opacity: .5;
               animation-timing-function: cubic-bezier(.25, 1, .3, 1); }
  50%,  68%  { transform: translateY(0)                    scale(1);   opacity: 1;
               animation-timing-function: cubic-bezier(.25, 1, .3, 1); }
  75%,  93%  { transform: translateY(calc(-48 * var(--s))) scale(.86); opacity: .5;
               animation-timing-function: cubic-bezier(.25, 1, .3, 1); }
  /* oben in die Trommel hineinrollen … */
  99%        { transform: translateY(calc(-96 * var(--s))) scale(.7);  opacity: 0;
               animation-timing-function: step-end; }
  /* … und unsichtbar auf die Gegenseite umsetzen */
  99.01%,
  100%       { transform: translateY(calc(96 * var(--s)))  scale(.7);  opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tile-feat .ft-item { animation: none; opacity: .5; }
  .tile-feat .ft-item:nth-child(1) { transform: translateY(0) scale(1); opacity: 1; }
  .tile-feat .ft-item:nth-child(2) { transform: translateY(calc(48 * var(--s))) scale(.86); }
  .tile-feat .ft-item:nth-child(3) { opacity: 0; }
  .tile-feat .ft-item:nth-child(4) { transform: translateY(calc(-48 * var(--s))) scale(.86); }
}

/* ── 5 · Shieldbase (912 × 445) ──────────────────────────── */

.tile-shield { background: #ABACCC; }

.tile-shield .sb-shot {
  position: absolute;
  left: calc(82 * var(--s));
  top:  calc(100 * var(--s));
  width:  calc(358 * var(--s));
  height: calc(246 * var(--s));
  overflow: hidden;
  /* Bewegt wird der Ausschnitt samt Inhalt — der Beschnitt bleibt dadurch
     unverändert, es verschiebt sich nur das fertige Bild als Ganzes. */
  animation: sb-float 7s ease-in-out infinite;
}

/* Schweben: rund 15 Design-Pixel Hub, knapp zwei Grad Neigungsspanne und
   ein wenig seitliche Drift, auf drei ungleiche Stationen verteilt, damit
   es nicht wie ein Pendel im Takt wirkt. Die Drift nimmt der Bewegung das
   Schienenhafte — das Produkt wirkt eher frei im Raum als auf und ab. */
@keyframes sb-float {
  0%   { transform: translate(calc(-1.5 * var(--s)), calc(-8 * var(--s))) rotate(-.80deg); }
  34%  { transform: translate(calc(2 * var(--s)),    calc(7 * var(--s)))  rotate(.90deg); }
  67%  { transform: translate(calc(-.5 * var(--s)),  calc(-3 * var(--s))) rotate(.25deg); }
  100% { transform: translate(calc(-1.5 * var(--s)), calc(-8 * var(--s))) rotate(-.80deg); }
}

@media (prefers-reduced-motion: reduce) {
  .tile-shield .sb-shot { animation: none; transform: none; }
}

.tile-shield .sb-shot img {
  position: absolute;
  left: calc(-187.592 * var(--s));
  top:  calc(-74.784 * var(--s));
  width:  calc(733.184 * var(--s));
  height: calc(385.605 * var(--s));
}

.tile-shield .sb-title {
  position: absolute;
  left: calc(550.124 * var(--s));
  top:  calc(156 * var(--s));
  font-style: italic;
  font-weight: 600;
  font-size: calc(30 * var(--s));
  line-height: normal;
  color: #1A263E;
  white-space: nowrap;
}

.tile-shield .sb-sub {
  position: absolute;
  left: calc(550 * var(--s));
  top:  calc(199.5 * var(--s));
  width: calc(303 * var(--s));
  font-weight: 500;
  font-size: calc(16 * var(--s));
  line-height: calc(20 * var(--s));
  color: #1A263E;
}

.tile-shield .sb-btn {
  position: absolute;
  left: calc(550.124 * var(--s));
  top:  calc(242.5 * var(--s));
  width:  calc(247 * var(--s));
  height: calc(47 * var(--s));
}

.tile-shield .sb-btn-label {
  position: absolute;
  left: calc(598.123 * var(--s));
  top:  calc(253.5 * var(--s));
  width: calc(150.743 * var(--s));
  text-align: center;
  font-weight: 600;
  font-size: calc(14 * var(--s));
  line-height: calc(25 * var(--s));
  color: #1A263E;
}

/* ── 6 · Watershield (970 × 445) ─────────────────────────── */

.tile-water { background: #ABACCC; }

.tile-water .wt-tr {
  position: absolute;
  left: calc(742 * var(--s)); top: 0;
  width: calc(582 * var(--s)); height: calc(217 * var(--s));
}
.tile-water .wt-b1 {
  position: absolute;
  left: calc(309 * var(--s)); top: calc(217 * var(--s));
  width: calc(433 * var(--s)); height: calc(228 * var(--s));
}
.tile-water .wt-b2 {
  position: absolute;
  left: calc(-176 * var(--s)); top: calc(217 * var(--s));
  width: calc(433 * var(--s)); height: calc(228 * var(--s));
}
.tile-water .wt-shot {
  position: absolute;
  left: calc(211 * var(--s)); top: calc(56 * var(--s));
  width: calc(548.273 * var(--s)); height: calc(332 * var(--s));
  object-fit: cover;
}
.tile-water .wt-logo {
  left: calc(466.385 * var(--s));
  top:  calc(238.755 * var(--s));
  width:  calc(154.737 * var(--s));
  height: calc(22.5185 * var(--s));
  opacity: .8;
}

/* ── Wasserstand an der Skala ─────────────────────────────
   Das Fenster deckt exakt die dunkelblaue Sperrfläche ab. Die Maße stammen
   aus dem Produktbild: die Fläche liegt dort bei Quellpixel x 66…1533,
   y 165…927, umgerechnet über die object-fit-Skalierung (332/966) auf die
   Kachel. Die beiden Skalen sitzen bei 360,8 (cm) und 727,5 (Zoll) — beide
   liegen im Fenster, der Pegel ist also an beiden ablesbar.

   Die cm-Skala reicht von 0 bei y≈369,5 bis 50 bei y≈116,5. Der Pegel
   wandert zwischen rund 3 cm und rund 40 cm — er bleibt damit sichtbar
   unter der Oberkante: die Sperre wird nicht überspült. */
/* Das Wasserfeld reicht links und rechts über die Sperre hinaus und blendet
   zu beiden Seiten aus — die Flut hört nicht am Schild auf, sie umgibt es.
   Sperrfläche 233…737, Wasserfeld 150…820: rund 83 Design-Pixel Überstand je
   Seite. Über der Sperre steht das Wasser voll, ausgeblendet wird erst im
   Überstand. */
/* Das Feld reicht bis 440 und damit deutlich unter die Sperre (Unterkante
   375). Dadurch endet das Wasser nicht an der Schildkante, sondern läuft
   darüber hinweg und unten aus. Ausgeblendet wird zu drei Seiten:
   links und rechts im seitlichen Überstand, nach unten ab 68 % der
   Feldhöhe (y ≈ 336) bis auf null bei 440. */
.tile-water .wt-flood {
  --wt-sides:  linear-gradient(to right,
      transparent 0%, #000 8%, #000 92%, transparent 100%);
  --wt-bottom: linear-gradient(to bottom,
      #000 0%, #000 68%, transparent 100%);

  position: absolute;
  left: calc(150 * var(--s));
  top:  calc(114 * var(--s));
  width:  calc(670 * var(--s));
  height: calc(326 * var(--s));
  overflow: hidden;
  pointer-events: none;

  -webkit-mask-image: var(--wt-sides), var(--wt-bottom);
          mask-image: var(--wt-sides), var(--wt-bottom);
  -webkit-mask-repeat: no-repeat, no-repeat;
          mask-repeat: no-repeat, no-repeat;
  -webkit-mask-size: 100% 100%, 100% 100%;
          mask-size: 100% 100%, 100% 100%;
  /* Beide Blenden müssen sich überlagern, nicht addieren */
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* Wasserkörper in Weiß- und Lavendeltönen statt Blau. Direkt unter der
   Oberfläche am dichtesten, danach auf einen schwachen Grundton auslaufend —
   die Staffelung ist in Design-Pixeln angegeben, damit sie sich immer auf die
   Wasseroberfläche bezieht und nicht mit dem Pegel mitskaliert.
   Das Ausblenden nach unten übernimmt die Maske auf dem Feld. */
/* Aufbau: oben die laufende Wellenlinie, darunter eine weiße Fläche mit
   geringerer Deckkraft, die nach unten und zu den Seiten ausläuft.

   Entscheidend ist, dass die Fläche ihre Oberkante nicht als Rechteck hat,
   sondern über dieselbe Wellenform maskiert wird wie die Linie. Vorher lag
   unter der Linie eine eigene Füllung in der Grafik und darunter die
   rechteckige Fläche — an deren Nahtstelle blieb immer eine gerade Kante
   sichtbar. Jetzt gibt es nur noch eine Fläche, und die ist oben selbst
   gewellt.

   Beide Ebenen teilen sich Kachelweite (240) und Streifenhöhe (40) der
   Grafik und laufen mit derselben Dauer — dadurch bleiben Linie und
   Flächenkante deckungsgleich. Die Wellenmitte liegt bei viewBox 20, also
   20 Design-Pixel unter der Oberkante des Körpers. */
.tile-water .wt-flood-body {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 100%;
  animation: wt-tide 18s ease-in-out infinite;
}

/* Die weiße Fläche */
.tile-water .wt-flood-body::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
      rgba(255, 255, 255, .42) calc(20 * var(--s)),
      rgba(233, 234, 244, .32) calc(46 * var(--s)),
      rgba(196, 198, 222, .22) calc(96 * var(--s)),
      rgba(171, 172, 204, .16) 100%);

  /* Ebene 1: gewellte Oberkante (Grafik deckt von der Kurve bis 40 ab).
     Ebene 2: die Fläche darunter. Beide werden addiert (Vorgabewert von
     mask-composite).

     Ebene 2 setzt bewusst schon bei 30 ein und nicht erst bei 40: das
     Wellental liegt bei 28, ab dort deckt Ebene 1 über die volle Breite.
     Stießen die beiden Lagen stattdessen bei 40 stumpf aneinander, ergäbe
     die Antialiasing-Kante beider Ränder in Summe knapp weniger als volle
     Deckung — und genau das war die feine waagerechte Linie. Der Übergang
     läuft zusätzlich über vier Pixel statt hart. */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 Q 60 4 120 20 T 240 20 V40 H0 Z' fill='%23000'/%3E%3C/svg%3E"),
      linear-gradient(to bottom, transparent calc(30 * var(--s)), #000 calc(34 * var(--s)));
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 Q 60 4 120 20 T 240 20 V40 H0 Z' fill='%23000'/%3E%3C/svg%3E"),
      linear-gradient(to bottom, transparent calc(30 * var(--s)), #000 calc(34 * var(--s)));
  -webkit-mask-size: calc(240 * var(--s)) calc(40 * var(--s)), 100% 100%;
          mask-size: calc(240 * var(--s)) calc(40 * var(--s)), 100% 100%;
  -webkit-mask-repeat: repeat-x, no-repeat;
          mask-repeat: repeat-x, no-repeat;
  -webkit-mask-position: 0 0, 0 0;
          mask-position: 0 0, 0 0;

  /* Die Wellenform steckt hier in der Maske, nicht im Hintergrund — bewegt
     werden muss deshalb mask-position. Mit background-position stand die
     Fläche still, während die Linie lief, und beide passten nicht zusammen.
     Nur die erste Lage wandert; die zweite bleibt stehen, sonst risse die
     Fläche seitlich ab. */
  animation: wt-surface-mask 4.5s linear infinite;
}

/* Die Linie darüber — dieselbe Kurve, nur als Strich statt als Fläche */
.tile-water .wt-surface {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: calc(40 * var(--s));
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 Q 60 4 120 20 T 240 20' fill='none' stroke='%23fff' stroke-opacity='.92' stroke-width='4'/%3E%3C/svg%3E");
  background-repeat: repeat-x;
  background-size: calc(240 * var(--s)) 100%;
  animation: wt-surface-run 4.5s linear infinite;
}

/* Gezeiten: langsam auflaufen, oben kurz drücken und nachsacken, dann wieder
   ablaufen. translateY in Prozent der Feldhöhe (326) — 71,5 % entspricht rund
   4 cm auf der Skala, 16,8 % rund 40 cm. */
@keyframes wt-tide {
  0%,  6%  { transform: translateY(65.4%); }
  44%      { transform: translateY(11.4%); }
  52%      { transform: translateY(15.3%); }
  60%      { transform: translateY(10.6%); }
  94%,
  100%     { transform: translateY(65.4%); }
}

/* Ein voller Wellenzug Versatz — dadurch läuft die Welle nahtlos um */
/* Ein voller Wellenzug Versatz je Runde (4,5 s, also rund 53 Design-Pixel
   je Sekunde) — dadurch läuft die Welle nahtlos um. Bewusst gleichmäßig und
   ohne Zusätze: ein Versuch mit höherem Tempo,
   überlagertem Auf und Ab und schwankendem Vorschub wirkte unruhiger statt
   lebendiger. Die Wellenform ist ein starres Profil; sie verträgt keine
   Zusatzbewegung, ohne dass das Starre daran auffällt.

   Linie und Fläche bekommen dieselbe Verschiebung, nur einmal als
   background-position und einmal als mask-position. Bei der Maske wandert
   ausschließlich die erste Lage; die zweite muss stehen bleiben, sonst
   risse die Fläche seitlich ab. */
@keyframes wt-surface-run  { to { background-position-x: calc(240 * var(--s)); } }

@keyframes wt-surface-mask {
  to {
    -webkit-mask-position: calc(240 * var(--s)) 0, 0 0;
            mask-position: calc(240 * var(--s)) 0, 0 0;
  }
}

/* Tropfen: sitzen fest auf der Sperre und laufen kurz nach, sobald der
   fallende Pegel sie freigibt. --t legt fest, wann das im Zyklus passiert;
   die Werte folgen der Wasserlinie auf ihrem Weg nach unten. Sichtbar ist
   jeder Tropfen nur rund 2 s. */
.tile-water .wt-drips {
  position: absolute;
  left: calc(233 * var(--s));
  top:  calc(114 * var(--s));
  width:  calc(504 * var(--s));
  height: calc(261 * var(--s));
  overflow: hidden;
  pointer-events: none;
}

.tile-water .wt-drips > span {
  position: absolute;
  left: calc((var(--x) - 233) * var(--s));
  top:  calc((var(--y) - 114) * var(--s));
  width:  calc(3.4 * var(--s));
  height: calc(4.8 * var(--s));
  border-radius: 50% 50% 46% 46% / 62% 62% 38% 38%;
  background: linear-gradient(to bottom,
      rgba(255, 255, 255, .9), rgba(214, 216, 233, .6));
  opacity: 0;
  animation: wt-drip 18s linear var(--t) infinite;
}

/* Die Schleifspur hinter dem Tropfen */
.tile-water .wt-drips > span::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 55%;
  width: calc(1.3 * var(--s));
  height: calc(12 * var(--s));
  transform: translateX(-50%);
  border-radius: calc(1 * var(--s));
  background: linear-gradient(to top,
      rgba(255, 255, 255, .38), rgba(255, 255, 255, 0));
}

@keyframes wt-drip {
  0%          { opacity: 0;   transform: translateY(0); }
  1.6%        { opacity: .72; transform: translateY(0); }
  6%          { opacity: .58; transform: translateY(calc(7 * var(--s))); }
  11%,
  100%        { opacity: 0;   transform: translateY(calc(15 * var(--s))); }
}

@media (prefers-reduced-motion: reduce) {
  .tile-water .wt-flood-body { animation: none; transform: translateY(45%); }
  .tile-logo .lg-logo,
  .tile-logo .lg-logo .m,
  .tile-logo .lg-logo .w,
  .tile-logo .lg-logo .r { animation: none; opacity: 1; transform: none; }
  .tile-logo .lg-holo,
  .tile-logo .lg-metal { display: none; }
  .tile-water .wt-surface,
  .tile-water .wt-flood-body::before { animation: none; }
  .tile-water .wt-drips > span { animation: none; opacity: 0; }
}

/* ── 7 · Suchfeld (970 × 445) ────────────────────────────── */

.tile-search {
  background: linear-gradient(114.644deg, #1A263F 0%, #0D0D21 100%);
  /* Textanfang im Suchfeld — Text und Cursor hängen beide daran */
  --se-x: calc(300 * var(--s));
}

.tile-search .se-bar {
  position: absolute;
  left: calc(196 * var(--s)); top: calc(197 * var(--s));
  width: calc(577.482 * var(--s)); height: calc(51 * var(--s));
}
.tile-search .se-chip {
  position: absolute;
  left: calc(196 * var(--s)); top: calc(197 * var(--s));
  width: calc(97.4822 * var(--s)); height: calc(51 * var(--s));
}
.tile-search .se-icon {
  position: absolute;
  left: calc(229 * var(--s)); top: calc(207 * var(--s));
  width: calc(32 * var(--s)); height: calc(32 * var(--s));
}
/* Getippte Suchbegriffe. Die Schrift ist dicksten-gleich (Geist Mono),
   deshalb lässt sich das Tippen über die Breite steuern: ein Zeichen ist
   exakt 1ch breit. `steps()` je Wortlänge lässt die Breite in
   Zeichenschritten wachsen und wieder schrumpfen, `overflow: hidden`
   schneidet den Rest ab.

   Über ch statt fester Pixel bleibt das unabhängig von den Metriken der
   Schrift — und der Cursor läuft auf demselben Raster, weil er dieselbe
   Staffelung als left-Wert bekommt. Zykluslänge 14.96 s. */
.tile-search .se-type {
  position: absolute;
  inset: 0;
}

.tile-search .se-type > span {
  position: absolute;
  left: var(--se-x);
  top:  calc(210 * var(--s));
  width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-family: "Geist Mono", ui-monospace, monospace;
  font-weight: 400;
  font-size: calc(16 * var(--s));
  line-height: calc(25 * var(--s));
  color: #1A263E;
}

.tile-search .se-type > span:nth-child(1) { animation: se-type-1 14.96s infinite; }
.tile-search .se-type > span:nth-child(2) { animation: se-type-2 14.96s infinite; }
.tile-search .se-type > span:nth-child(3) { animation: se-type-3 14.96s infinite; }
.tile-search .se-type > span:nth-child(4) { animation: se-type-4 14.96s infinite; }

@keyframes se-type-1 {
  0%, 0% { width: 0; animation-timing-function: steps(16, end); }
  8.556%, 18.583% { width: 16ch; animation-timing-function: steps(16, end); }
  22.861%, 100% { width: 0; }
}

@keyframes se-type-2 {
  0%, 25.201% { width: 0; animation-timing-function: steps(15, end); }
  33.222%, 43.249% { width: 15ch; animation-timing-function: steps(15, end); }
  47.259%, 100% { width: 0; }
}

@keyframes se-type-3 {
  0%, 49.599% { width: 0; animation-timing-function: steps(14, end); }
  57.086%, 67.112% { width: 14ch; animation-timing-function: steps(14, end); }
  70.856%, 100% { width: 0; }
}

@keyframes se-type-4 {
  0%, 73.195% { width: 0; animation-timing-function: steps(18, end); }
  82.821%, 92.848% { width: 18ch; animation-timing-function: steps(18, end); }
  97.66%, 100% { width: 0; }
}

@keyframes se-caret {
  0% { left: var(--se-x); }
  0% { left: var(--se-x); animation-timing-function: steps(16, end); }
  8.556%, 18.583% { left: calc(var(--se-x) + 16ch); animation-timing-function: steps(16, end); }
  22.861% { left: var(--se-x); }
  25.201% { left: var(--se-x); animation-timing-function: steps(15, end); }
  33.222%, 43.249% { left: calc(var(--se-x) + 15ch); animation-timing-function: steps(15, end); }
  47.259% { left: var(--se-x); }
  49.599% { left: var(--se-x); animation-timing-function: steps(14, end); }
  57.086%, 67.112% { left: calc(var(--se-x) + 14ch); animation-timing-function: steps(14, end); }
  70.856% { left: var(--se-x); }
  73.195% { left: var(--se-x); animation-timing-function: steps(18, end); }
  82.821%, 92.848% { left: calc(var(--se-x) + 18ch); animation-timing-function: steps(18, end); }
  97.66% { left: var(--se-x); }
  100% { left: var(--se-x); }
}

.tile-search .se-caret {
  position: absolute;
  left: var(--se-x);
  top: calc(213 * var(--s));
  width: calc(1 * var(--s));
  height: calc(19 * var(--s));
  background: #1A263E;
  opacity: .4;
  /* Eigene Schriftangaben, damit sich ch hier auf dasselbe Zeichenmaß
     bezieht wie beim Text — sonst liefe der Cursor aus dem Raster. */
  font-family: "Geist Mono", ui-monospace, monospace;
  font-size: calc(16 * var(--s));
  animation: se-caret 14.96s infinite;
}

.tile-search .se-close {
  position: absolute;
  left: calc(716.076 * var(--s)); top: calc(215.076 * var(--s));
  width: calc(15.8485 * var(--s)); height: calc(15.8485 * var(--s));
}

/* ── 8 · Wortmarke (1300 × 657) ──────────────────────────── */

.tile-logo { background: linear-gradient(103.054deg, #1B2740 0%, #0D0D21 100%); }

.tile-logo .lg-logo {
  left: calc(389 * var(--s));
  top:  calc(290 * var(--s));
  width:  calc(523 * var(--s));
  height: calc(76.1107 * var(--s));
}


/* Auftritt der Wortmarke — nach der vorgegebenen Referenz.

   Ablauf über 7 s:

     0,00–2,60 s   Y steht mittig auf der Kachel
     2,60–2,95 s   schiebt sich nach links, acht Pixel über den Platz hinaus
     2,95–3,15 s   rutscht zurück auf den Platz
     2,87–3,25 s   Schrift kommt aus der Marke heraus und blendet auf
     3,25–5,30 s   vollständige Wortmarke, zentriert
     5,30–5,65 s   Schrift geht, Marke fährt zurück und schießt über die Mitte
     5,65–5,85 s   rutscht in die Mitte
     5,85–7,00 s   steht, dann von vorn

   Die Bildmarke wird weder skaliert noch aus- und eingeblendet — sie steht
   über den ganzen Zyklus in voller Größe und Deckkraft da und bewegt sich
   ausschließlich seitlich. Der Schwung kommt allein aus dem Überschwingen
   beim Verschieben.

   Verschoben wird die ganze Gruppe, nicht die Marke allein: läge die Schrift
   an ihrer Endstelle, während sich nur die Marke bewegt, stünde sie zeitweise
   links davon und würde quer über die Marke gezeichnet.

   Der Versatz in der Ruhelage ist 198,99 Design-Pixel und nicht 201,69, wie
   die Kastenmitte es verlangte (650 − 389 − 59,31). Er ist um 2,70 kleiner,
   weil das Y in seinem Kasten nicht ausgewogen sitzt: der linke Arm ist ein
   dünner Keil, rechts steht der schwere Abstrich. Aus der Silhouette gemessen
   liegt der Flächenschwerpunkt bei x = 62,01 statt bei 59,31 — genau diese
   Differenz wird hier abgezogen, damit die Fläche mittig steht und nicht die
   Umrandung.

   Senkrecht bleibt es bei der Kastenmitte. Der Schwerpunkt läge 6,41 höher,
   aber die Vorlage setzt die Wortmarke selbst auf Kastenmitte (290 + 38,06 =
   328,06 bei Kachelmitte 328,5), und eine Korrektur nur für die allein
   stehende Marke brächte eine senkrechte Bewegung in den Übergang zurück. */
.tile-logo .lg-logo {
  animation: lg-enter 7s infinite;
}

.tile-logo .lg-logo .w { opacity: 0; animation: lg-word 7s infinite; }
.tile-logo .lg-logo .r { opacity: 0; animation: lg-reg  7s infinite; }

@keyframes lg-enter {
  0%,
  37.14%   { transform: translateX(calc(198.99 * var(--s)));
             animation-timing-function: cubic-bezier(.35, 0, .15, 1); }
  /* nach links, über den Platz hinaus und zurück */
  42.14%   { transform: translateX(calc(-8 * var(--s)));
             animation-timing-function: ease-out; }
  45%,
  75.71%   { transform: translateX(0);
             animation-timing-function: cubic-bezier(.35, 0, .15, 1); }
  /* zurück in die Mitte, ebenfalls mit Überschwinger */
  80.71%   { transform: translateX(calc(209.3 * var(--s)));
             animation-timing-function: ease-out; }
  83.57%,
  100%     { transform: translateX(calc(198.99 * var(--s))); }
}

/* Irisierender Metallschimmer über der Bildmarke.

   Nachgebaut nach der zweiten Referenz. Dort ist das Logo durchgehend Chrom
   — mehrere helle und dunkle Bänder wandern ununterbrochen darüber, an jeder
   Kante dazwischen ein Farbsaum. Die Säume machen den Look aus, nicht der
   helle Kern.

   Aufbau im Markup, nicht hier: die Bildmarke ist als eingebettetes SVG
   gesetzt. Darin liegt das grüne Y als Pfad, darüber ein übergroßes Rechteck
   mit einem sich wiederholenden Verlauf (spreadMethod="repeat"), begrenzt
   durch einen clipPath in Form des Y.

   Zwei frühere Fassungen legten den Verlauf stattdessen als CSS-Fläche mit
   `mask-image: url(assets/logo-mark.svg)` darüber. Das zeichnet Safari nicht
   — externe SVG-Dateien als Maske fallen dort aus, und die Fläche bleibt
   unsichtbar. Alle übrigen Masken im Projekt sind Data-URIs und davon nicht
   betroffen. Mit clipPath im eingebetteten SVG entfällt die Abhängigkeit
   ganz.

   Der Verlaufsvektor ist (41,3 | 15), seine Länge also 44 Einheiten — bei
   118,624 Markenbreite knapp drei Bänder. Verschoben wird um genau diesen
   Vektor, dadurch läuft die Schleife nahtlos um. */
.tile-logo .lg-mark {
  display: block;
  overflow: visible;
}

.tile-logo .lg-holo {
  opacity: 0;
  animation: lg-holo-flow 2.2s linear infinite,
             lg-holo-an   7s   linear infinite;
}

/* Die Bänder fließen ununterbrochen — ein voller Vektor je Runde */
@keyframes lg-holo-flow {
  to { transform: translate(41.3px, 15px); }
}

/* Auch hier nur ein kurzer Durchgang, im selben Fenster wie beim Shader.
   Der Verlauf kann sich nicht wie dort auf ein Band beschränken, also
   übernimmt die Deckkraft den Auf- und Abgang. */
@keyframes lg-holo-an {
  0%          { opacity: 0; }
  4%,   17%   { opacity: .85; }   /* Mitte bei 10,5 % — wie beim Shader */
  21%,  100%  { opacity: 0; }
}

/* Flüssiges Metall auf der Bildmarke.

   Löst den Verlauf oben ab, sobald assets/metallic-paint.js läuft — der
   Shader setzt dann .mp-on auf die Kachel. Ohne JavaScript, ohne WebGL 2
   oder bei verlorenem Grafikkontext bleibt es beim Verlauf; beide zugleich
   gibt es nie.

   Die Fläche ist quadratisch und ragt über die Marke hinaus: 12 Einheiten
   links und rechts, oben und unten so viel, dass ein Quadrat entsteht. Der
   Shader braucht diesen Rand, sonst schneidet er die Silhouette ab, und er
   rechnet in einem quadratischen Raum — bei anderem Seitenverhältnis
   verzerrt seine Abtastung. Beides steckt schon in der Tiefenkarte
   (tools/logo-depth-map.py), die Maße hier müssen dazu passen:

     142,624 = 118,624 Markenbreite + 2 × 12
      33,2567 = (142,624 − 76,1107) / 2 — halber Überstand nach oben */
.tile-logo .lg-metal {
  position: absolute;
  display: block;
  left:   calc(-12 * var(--s));
  top:    calc(-33.2567 * var(--s));
  width:  calc(142.624 * var(--s));
  height: calc(142.624 * var(--s));
  opacity: 0;
}

.tile-logo.mp-on .lg-holo { display: none; }

/* Ein einzelner Lichtstreif, kein Dauerzustand.

   Der Shader rechnet die ganze Marke als Metall — sichtbar wird davon aber
   nur ein schmales Band, das einmal je Runde darüber zieht. Das Band ist ein
   Verlauf in der Maske; bewegt wird es über mask-position, wie bei der
   Wasseroberfläche.

   Die Maske ist 2,6-mal so breit wie die Fläche. Positionsprozente beziehen
   sich auf die Differenz aus Flächen- und Bildbreite, hier also −1,6 Breiten:
   bei 100 % steht das Band links daneben, bei 0 % rechts. Der Lauf geht
   deshalb von 100 % nach 0 %, und in beiden Ruhelagen liegt es vollständig
   außerhalb — die Schleife springt nicht.

   Die Breite der Maske bestimmt mit, wie viel vom Lauf überhaupt zu sehen
   ist: von den 1,6 Breiten Weg liegt genau eine über der Fläche, also 62 %.
   Dreifache Breite (die erste Fassung) ließ nur die Hälfte übrig — vom Lauf
   verpuffte zu viel außerhalb.

   Der Lauf dauert 27 % des Zyklus (1,89 s), über der Marke davon rund 1,33 s.
   Gleichmäßiges Tempo statt einer Beschleunigungskurve: die schöbe den größten
   Teil der Bewegung in die unsichtbaren Randlagen.

   Liegen soll er mittig in der Standphase — also zwischen Einfahren und
   erneutem Ausfahren der Wortmarke:

     Marke rastet ein          83,57 %
     Marke fährt wieder los    37,14 %, also 137,14 %
     Standphase               53,57 % lang, Mitte bei 110,36 %

   Mittig ist der Streif, wenn sein Band auf der Fläche steht — bei
   mask-position 50 %, lokal also bei 19,5 % (die Mitte zwischen 6 % und 33 %).

   Das Fenster läuft damit über den Zyklusanfang hinweg. In den Keyframes
   ließe sich das nur mit einem Sprung an der Nahtstelle schreiben.
   Stattdessen bleiben sie bei 6 %…33 % und ein negativer animation-delay
   verschiebt das Ganze — die Schleife wandert dabei sauber über die Grenze:

     lokal 19,5 % soll auf Takt 110,36 % fallen
     0,195 × 7 s − (1,1036 × 7 s − 7 s) = 1,365 s − 0,725 s = 0,640 s
     → animation-delay: −0,64s */
.tile-logo.mp-on .lg-metal {
  /* Volle Deckkraft: der Dunkelton des Shaders ist selbst ein aufgehelltes
     Neongrün, das Band bleibt also auch ohne Durchscheinen in der Hausfarbe.
     Bei 85 % (eine frühere Fassung) mischte sich die ruhende Marke darunter
     und nahm dem Durchgang die Schärfe. */
  opacity: 1;
  -webkit-mask-image: linear-gradient(100deg,
    transparent 42%, #000 47%, #000 53%, transparent 58%);
          mask-image: linear-gradient(100deg,
    transparent 42%, #000 47%, #000 53%, transparent 58%);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 260% 100%;
          mask-size: 260% 100%;
  animation: lg-metal-sweep 7s linear -0.64s infinite;
}

@keyframes lg-metal-sweep {
  0%, 6%     { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
  33%, 100%  { -webkit-mask-position: 0% 0;   mask-position: 0% 0; }
}

/* Die Schrift kommt ein Stück aus der Marke heraus, statt nur aufzublenden */
@keyframes lg-word {
  0%,  41%     { opacity: 0; transform: translateX(calc(-18 * var(--s)));
                 animation-timing-function: cubic-bezier(.2, .7, .2, 1); }
  46.43%,
  75.71%       { opacity: 1; transform: translateX(0);
                 animation-timing-function: ease-in; }
  79%, 100%    { opacity: 0; transform: translateX(calc(-18 * var(--s))); }
}

@keyframes lg-reg {
  0%,  44%   { opacity: 0; }
  48%, 74%   { opacity: 1; }
  77%, 100%  { opacity: 0; }
}

/* ── 9 · Farbfelder (310 × 124) ──────────────────────────── */

.sw-dark     { background: linear-gradient(94.383deg, #1B2741 0%, #0D0D21 100%); }
.sw-green    { background: #A0FF00; }
.sw-lavender { background: #ABACCC; }
.sw-white    { background: #FFFFFF; }

/* ── 10 · Portrait (640 × 801) ───────────────────────────── */

.tile-person { background: linear-gradient(154.704deg, #DDDEEB 7.2481%, #ABACCC 49.291%); }

.tile-person .pe-circle {
  position: absolute;
  left: calc(140.533 * var(--s)); top: calc(188.199 * var(--s));
  width: calc(505.228 * var(--s)); height: calc(505.228 * var(--s));
  animation: pe-far 18s ease-in-out infinite;
}

.tile-person .pe-photo {
  position: absolute;
  left: calc(123.44 * var(--s)); top: calc(41.825 * var(--s));
  width: calc(609.853 * var(--s)); height: calc(914.779 * var(--s));
  object-fit: cover;
  transform: scaleX(-1);
  filter: grayscale(1);
  animation: pe-near-flip 18s ease-in-out infinite;
}

/* Räumliche Tiefe: zwei Ebenen schwenken waagerecht gegeneinander.

     vorn    Person, Zitat und Zitatwinkel   ±22
     hinten  der grüne Kreis                 ∓14

   Maßgeblich ist nicht der Ausschlag, sondern wie schnell sich die Kanten
   bewegen — und beides lässt sich getrennt einstellen. Der Weg dahin ging
   über drei Fassungen:

     ±14 über  8 s   10 px/s   nicht zu sehen
     ±26 über  8 s   33 px/s   zu hektisch
     ±26 über 13 s   20 px/s   sichtbar, noch zu lebhaft
     ±22 über 18 s   12 px/s   jetzt

   Ein weiter, langsamer Weg liest sich ruhig, ein kurzer schneller zappelt.
   Beruhigt wird deshalb vorrangig über die Dauer: sie ist von 8 auf 18 s
   gewachsen, der Ausschlag nur von 26 auf 22 zurückgegangen.

   Achtung beim Weiterdrehen — 12 px/s liegt nah an der Schwelle. Die erste
   Fassung mit 10 px/s war nicht mehr zu sehen. Der größere Weg hilft hier
   allerdings mit: 44 Design-Pixel Spanne statt der damaligen 28 fallen auch
   dann noch auf, wenn man nur gelegentlich hinsieht.

   Nach oben begrenzt das nur der Kreis, siehe unten — Zitat und Person
   liegen auf derselben Ebene, ihr Abstand von 97 Design-Pixeln bleibt also
   unabhängig vom Ausschlag erhalten. Die Person ist zudem nur unten
   angeschnitten, links und rechts hat sie 58 bzw. 73 Pixel transparenten
   Rand; waagerechtes Schwenken legt dort keine Kante frei.

   Getragen wird der Eindruck nicht von der Bewegung selbst, sondern vom
   Unterschied zwischen den Ebenen. Sie laufen deshalb **gegeneinander**: das
   entspricht einer Kamera, die um einen Punkt zwischen Person und Kreis
   schwenkt, und liest sich räumlicher als ein gemeinsames Verschieben, bei
   dem alles nur zusammen zur Seite rutscht. Zwischen den Ebenen liegen damit
   bis zu 42 Design-Pixel Versatz, in der Spanne 84.

   Nur waagerecht. Beide teilen sich Dauer und Stationen, sonst zerfiele der
   Verbund in Einzelbewegungen. Die Stationen sind ungleich verteilt, damit es
   nicht wie ein Pendel im Takt wirkt.

   Nicht bewegt werden der Blob und das Logo: der Blob ist eine weiche
   Vignette am unteren Rand, das Logo eine Signatur — beides gehört zur
   Kachel, nicht zur Szene.

   Der Kreis ragt in der Vorlage 5,8 px über die rechte Kachelkante und wird
   dort zu einer flachen Sehne beschnitten. Beim Schwenk nach links löst er
   sich davon; bei ∓16 klafft im Äußersten eine Lücke von gut 10 px. Das ist
   der Grund, warum die hintere Ebene weniger ausschlägt als die vordere.

   pe-near und pe-near-flip beschreiben dieselbe Bewegung und müssen
   gleichlaufen. Getrennt sind sie, weil die Person `scaleX(-1)` trägt: eine
   Animation auf `transform` ersetzt den Wert vollständig, die Spiegelung muss
   also in jedem ihrer Keyframes stehen — sonst dreht sie sich beim Start um.
   Sie steht hinten, weil Transformationen von rechts nach links wirken: erst
   spiegeln, dann verschieben. Andersherum liefe die Verschiebung mitgespiegelt
   und die Person zöge gegen Zitat und Pfeil. */
@keyframes pe-near {
  0%   { transform: translateX(calc(-22 * var(--s))); }
  34%  { transform: translateX(calc(22 * var(--s))); }
  67%  { transform: translateX(calc(-8.8 * var(--s))); }
  100% { transform: translateX(calc(-22 * var(--s))); }
}

@keyframes pe-near-flip {
  0%   { transform: translateX(calc(-22 * var(--s))) scaleX(-1); }
  34%  { transform: translateX(calc(22 * var(--s)))  scaleX(-1); }
  67%  { transform: translateX(calc(-8.8 * var(--s))) scaleX(-1); }
  100% { transform: translateX(calc(-22 * var(--s))) scaleX(-1); }
}

@keyframes pe-far {
  0%   { transform: translateX(calc(14 * var(--s))); }
  34%  { transform: translateX(calc(-14 * var(--s))); }
  67%  { transform: translateX(calc(5.6 * var(--s))); }
  100% { transform: translateX(calc(14 * var(--s))); }
}

@media (prefers-reduced-motion: reduce) {
  .tile-person .pe-circle,
  .tile-person .pe-quote,
  .tile-person .pe-bar { animation: none; transform: none; }
  /* Ohne Spiegelung schaute die Person in die falsche Richtung */
  .tile-person .pe-photo { animation: none; transform: scaleX(-1); }
}

.tile-person .pe-quote {
  position: absolute;
  left: calc(110.018 * var(--s)); top: calc(386.636 * var(--s));
  width: calc(164 * var(--s));
  font-weight: 600;
  font-size: calc(20.775 * var(--s));
  line-height: 1.3;
  color: #18183B;
  animation: pe-near 18s ease-in-out infinite;
}

.tile-person .pe-bar {
  position: absolute;
  left: calc(268.722 * var(--s)); top: calc(380.663 * var(--s));
  width: calc(11.1665 * var(--s)); height: calc(11.1665 * var(--s));
  animation: pe-near 18s ease-in-out infinite;
}

.tile-person .pe-blob {
  position: absolute;
  left: calc(-55.183 * var(--s)); top: calc(483.086 * var(--s));
  width: calc(1165.786 * var(--s)); height: calc(732.154 * var(--s));
  display: flex; align-items: center; justify-content: center;
}
.tile-person .pe-blob-in {
  position: relative; flex: none;
  width: calc(1081.43 * var(--s)); height: calc(468.504 * var(--s));
  transform: rotate(-14.98deg);
}
.tile-person .pe-blob-in img {
  position: absolute;
  left: calc(-318.4 * var(--s)); top: calc(-318.4 * var(--s));
  width: calc(1718.25 * var(--s)); height: calc(1105.32 * var(--s));
}

.tile-person .pe-logo {
  left: calc(246.217 * var(--s)); top: calc(730.580 * var(--s));
  width: calc(147.576 * var(--s)); height: calc(21.5302 * var(--s));
}

/* ── 11/12 · Typografie (970 × 445) ──────────────────────── */

.tile-type--geist { background: linear-gradient(114.644deg, #1A253F 0%, #0D0D21 100%); color: #ABACCC; }
.tile-type--mont  { background: #A0FF00; color: #1A263E; }

.tile-type--geist { font-family: "Geist Mono", ui-monospace, monospace; font-weight: 400; font-style: normal; }
.tile-type--mont  { font-family: Montserrat, sans-serif; font-weight: 500; font-style: italic; }

.tile-type .ty-aa {
  position: absolute;
  left: calc(110 * var(--s)); top: 0;
  font-size: calc(300 * var(--s));
  white-space: nowrap;
}
.tile-type--geist .ty-aa { line-height: 1.3; }
.tile-type--mont  .ty-aa { line-height: 1.22; }

.tile-type .ty-name {
  position: absolute;
  top: calc(330 * var(--s));
  font-size: calc(20 * var(--s));
  line-height: calc(30 * var(--s));
  white-space: nowrap;
}
.tile-type--geist .ty-name { left: calc(110 * var(--s)); }
.tile-type--mont  .ty-name { left: calc(90 * var(--s)); }

.tile-type .ty-upper,
.tile-type .ty-lower,
.tile-type .ty-num {
  position: absolute;
  left: calc(636 * var(--s));
  font-size: calc(20 * var(--s));
  line-height: calc(30 * var(--s));
}
.tile-type .ty-upper { top: calc(113 * var(--s)); }
.tile-type .ty-lower { top: calc(213 * var(--s)); }
.tile-type .ty-num   { top: calc(313 * var(--s)); white-space: nowrap; }

/* Getippte Zeichensätze: A…Z, a…z, 0…9 erscheinen eins nach dem anderen und
   werden in derselben Reihenfolge wieder abgeräumt. Beide Karten laufen
   gleich, 14 s je Runde.

   Jedes Zeichen steht als eigenes span im Markup und trägt dort seinen
   fortlaufenden Index --i (0…61, über alle drei Absätze durchgezählt). Daraus
   kommt allein die Verzögerung — eine einzige Regel für 124 Zeichen.

   Warum nicht wie im Suchfeld über `ch` und `steps()`: das setzt eine feste
   Zeichenbreite voraus. Für Geist Mono träfe es zu, für Montserrat nicht — dort
   ist jedes Zeichen anders breit, und ein Schritt landete mitten im Buchstaben.
   Pro Zeichen ein Element ist unabhängig von der Schrift exakt.

   `backwards` ist wichtig: während der Verzögerung gilt damit der Wert aus dem
   0-%-Bild, also unsichtbar. Die Grundregel bleibt dadurch auf voller Deckkraft
   — läuft die Animation aus irgendeinem Grund nicht, steht der Text trotzdem
   da, statt unsichtbar zu bleiben. */
.tile-type .ty-upper span,
.tile-type .ty-lower span,
.tile-type .ty-num   span {
  animation: ty-ch 14s linear infinite backwards;
  animation-delay: calc(var(--i) * .06s);
}

/* 62 Zeichen à 0,06 s ergeben 3,66 s Tippdauer.

   Der Zeitpunkt des Abräumens bestimmt, wie lange die Karte leer steht: das
   letzte Zeichen verschwindet 3,66 s nach dem ersten, und erst danach beginnt
   die nächste Runde. Bei 60 % wären das 12,1 s, also fast zwei Sekunden
   Leerstand. Bei 71 % (9,94 s) verschwindet das letzte Zeichen um 13,6 s und
   die Lücke schrumpft auf gut drei Zehntel — genug, dass der Neubeginn als
   solcher zu erkennen ist, zu wenig, um als Loch zu wirken.

   Weiter ließe sich das nur treiben, indem die nächste Runde ins Abräumen der
   vorigen hineinläuft; ab rund 74 % überlappen beide.

   Auf- und Abblenden dauern je 0,4 % (56 ms) und lesen sich dadurch als
   Setzen, nicht als Überblenden. */
@keyframes ty-ch {
  0%           { opacity: 0; }
  0.4%,  71%   { opacity: 1; }
  71.4%, 100%  { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tile-type .ty-upper span,
  .tile-type .ty-lower span,
  .tile-type .ty-num   span { animation: none; }
}

.tile-type .ty-rule { position: absolute; }
.tile-type .ty-rule--1 {
  left: calc(636 * var(--s)); top: calc(193 * var(--s));
  width: calc(225 * var(--s)); height: calc(1 * var(--s));
}
.tile-type .ty-rule--2 {
  left: calc(636 * var(--s)); top: calc(296 * var(--s));
  width: calc(225 * var(--s)); height: calc(1 * var(--s));
}
.tile-type .ty-rule--v {
  left: calc(571 * var(--s)); top: calc(118 * var(--s));
  width: calc(1 * var(--s)); height: calc(225 * var(--s));
}
.tile-type--geist .ty-rule { background: #ABACCC; opacity: .3; }
.tile-type--mont  .ty-rule { background: #1A263E; opacity: .2; }

/* Warnhinweise auf der Europakarte.

   Sie ploppen auf und wieder weg — Extremwetter kommt und geht, überall in
   Europa.

   Alle acht teilen sich **eine Rundendauer** und unterscheiden sich nur im
   Einsatz. Das ist nicht Bequemlichkeit, sondern der Grund, warum die Karte
   nie ganz leer steht: bei verschiedenen Dauern driften die Phasen, und
   irgendwann sind alle zugleich aus — gemessen entstand so eine Lücke von
   6,85 s, und das liest sich als Fehler, nicht als Flaute.

   Mit gemeinsamer Dauer lässt sich Lückenlosigkeit dagegen zeigen: jeder
   Hinweis steht 11,5 s, der größte Abstand zwischen zwei Einsätzen ist 4,7 s.
   Solange dieser Abstand kleiner bleibt als die Standzeit, überlappen sich die
   Fenster lückenlos — und weil alle dieselbe Runde haben, gilt das für immer
   und nicht nur für den geprüften Ausschnitt.

   Die Verzögerungen sind **negativ**. Mit positiven liefen die späteren
   Hinweise in der ersten Runde noch gar nicht und die Karte füllte sich erst
   über eine halbe Minute; negative setzen jeden sofort an seine Stelle im
   Zyklus.

   Dafür sind die acht Dreiecke aus tile-map.svg herausgelöst. An Teile einer
   über <img> geladenen SVG kommt CSS nicht heran.

   Alle acht teilen sich eine Datei: ausgemessen haben sie exakt dasselbe
   Seitenverhältnis (1,1111) und Strichstärken, die proportional mitskalieren —
   es ist dieselbe Form in acht Größen. Die Vorlage stammt aus dem größten der
   acht, dort ist die Auflösung am besten.

   Lage und Größe stehen im Markup. Der viewBox der Karte beginnt bei (8|8),
   die Kachelkoordinaten sind deshalb um 8 kleiner als die gemessenen.

   **Um jedes Dreieck liegen 1,4 Einheiten Rand.** Die Maße stammen aus
   getBBox(), und das liefert die Geometrie *ohne* Strich. Die Dreiecke sind
   aber umrandet — bei 2,54 Strichstärke und stroke-linejoin round ragt die
   Linie 1,27 über die Geometrie hinaus, und ein knapper viewBox schnitt sie
   ringsum ab. Nachgemessen am gerenderten Bild: Überstand 1,28 bis 1,32
   (halbe Strichstärke plus Kantenglättung).

   Der Rand ist symmetrisch, die Mitte jedes Dreiecks bleibt dadurch genau da,
   wo sie war. */
.tile-map { position: relative; }

.tile-map .mp-warn {
  position: absolute;
  left: calc(var(--wx) * var(--s));
  top:  calc(var(--wy) * var(--s));
  width:  calc(var(--ww) * var(--s));
  height: calc(var(--wh) * var(--s));
  background: url("assets/map-warn.svg") center / contain no-repeat;
  opacity: 0;
  animation: mp-warn 34s linear infinite;
  animation-delay: var(--wt);
}

/* Das Ploppen.

   Entscheidend ist die Deckkraft: sie springt in 0,12 s auf voll, nicht in
   0,68 s wie in einer ersten Fassung — das war der Unterschied zwischen
   Aufploppen und Einblenden. Der Maßstab schießt dabei auf 118 % und fällt in
   0,32 s zurück.

   Die Kurven stehen deshalb in den Keyframes und nicht an der Animation: dort
   müsste eine für alle Abschnitte taugen, und `ease-in-out` bremste
   ausgerechnet den Anfang aus. An der Animation steht jetzt `linear`, jeder
   Abschnitt bringt seine eigene Kurve mit.

   Danach steht der Hinweis rund ein Drittel der Runde, dann schrumpft er auf
   85 % und ist weg — das Verschwinden darf weicher sein als das Kommen. */
@keyframes mp-warn {
  0%     { opacity: 0; transform: scale(.3);
           animation-timing-function: cubic-bezier(.1, .8, .2, 1); }
  0.35%  { opacity: 1; transform: scale(1.18);
           animation-timing-function: ease-out; }
  1.3%   { opacity: 1; transform: scale(1); }
  30%    { opacity: 1; transform: scale(1); }
  32%    { opacity: 0; transform: scale(.85); }
  100%   { opacity: 0; transform: scale(.3); }
}

@media (prefers-reduced-motion: reduce) {
  /* Ohne Bewegung stehen alle Hinweise, wie in der Vorlage */
  .tile-map .mp-warn { animation: none; opacity: 1; transform: none; }
}

/* ── 13 · EU-Motiv (970 × 445) ───────────────────────────── */

.tile-eu { background: linear-gradient(114.644deg, #1B273F 0%, #0D0D21 100%); }

/* Morsezeichen im Hintergrund.

   Sie buchstabieren WHYMARS: .-- / .... / -.-- / -- / .- / .-. / ...
   Die Höhenversätze in der Vorlage sind Optik, keine Bedeutung — gelesen wird
   von links nach rechts.

   Jede der 21 Marken steht einzeln, damit sie für sich blinken kann; in der
   Vorlage steckten sie zu siebt in Gruppen-SVGs. Sie tragen ihre Lage als
   --mx/--my und ihren Einsatz als --t in Morse-Einheiten.

   Der Takt folgt der Norm: Punkt 1 Einheit, Strich 3, Lücke zwischen Marken 1,
   zwischen Buchstaben 3, zwischen Wörtern 7. Daraus ergeben sich 78 Einheiten
   je Runde. Bei 0,14 s je Einheit sind das 10,92 s und rund 8,6 Wörter je
   Minute — langsames, gut lesbares Morsen.

   Zwei Keyframe-Sätze, weil Punkt und Strich verschieden lang leuchten. Der
   Einsatz kommt dagegen aus --t und braucht keinen eigenen Satz. */
.tile-eu .eu-mark {
  position: absolute;
  left: calc(var(--mx) * var(--s));
  top:  calc(var(--my) * var(--s));
  height: calc(14.562 * var(--s));
  background: no-repeat center / contain;
  opacity: .1;
  animation-duration: 10.92s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-delay: calc(var(--t) * .14s);
}

.tile-eu .eu-mark--di {
  width: calc(27.6176 * var(--s));
  background-image: url("assets/eu-dot.svg");
  animation-name: eu-di;
}

.tile-eu .eu-mark--dah {
  width: calc(39.0372 * var(--s));
  background-image: url("assets/eu-dash.svg");
  animation-name: eu-dah;
}

/* Eine Einheit sind 100/78 = 1,282 % des Zyklus. Auf- und Abblenden nehmen je
   ein Fünftel davon — kurz genug, dass der Takt scharf bleibt, lang genug,
   dass es nicht hart schaltet. */
@keyframes eu-di {
  0%            { opacity: .1; }
  0.26%, 1.03%  { opacity: .55; }
  1.28%, 100%   { opacity: .1; }
}

@keyframes eu-dah {
  0%            { opacity: .1; }
  0.26%, 3.59%  { opacity: .55; }
  3.85%, 100%   { opacity: .1; }
}

@media (prefers-reduced-motion: reduce) {
  .tile-eu .eu-mark { animation: none; }
}

/* Sternenkranz: der Kranz dreht sich, die Sterne bleiben aufrecht.

   Wie die Gondeln eines Riesenrads — der Kranz dreht um seine Mitte, jeder
   Stern steuert mit derselben Dauer dagegen. Beide Drehungen heben sich für
   den Stern auf, seine Lage auf dem Kranz aber nicht.

   **Die beiden Dauern müssen übereinstimmen.** Weichen sie voneinander ab,
   hebt sich das Gegensteuern nicht mehr auf und die Sterne trudeln langsam
   weg. Wer das Tempo ändert, ändert beide — 76,2 s sind 80 / 1,05.

   Dafür braucht jeder Stern ein eigenes Element; in der Vorlage stecken alle
   zwölf in einem einzigen Pfad. Sie tragen ihre Mitte aus der Vorlage als
   --sx/--sy im Markup, gemessen an den je fünf äußeren Spitzen.

   Der Kranz ist **keine exakte Kreisform**: die Abstände zur Mitte schwanken
   zwischen 71,9 und 77,6, die Winkelabstände zwischen 28,7° und 32,4°. Das
   stört nicht — eine Drehung bewegt die Anordnung starr, jeder Stern läuft
   also auf seinem eigenen Kreis, und der Kranz behält seine Form. Gedreht wird
   um den Schwerpunkt der zwölf Mitten, 50 % | 50,703 %.

   Alle zwölf teilen sich eine Sterndatei. In der Vorlage sind sie durch die
   ungleichmäßige Skalierung minimal verschieden — Spitzenabstände 13,67 bis
   14,73, Ausrichtung ±0,77° —, der Unterschied liegt bei Anzeigegröße aber
   unter einem halben Pixel. */
.tile-eu .eu-ring {
  position: absolute;
  left: calc(393 * var(--s)); top: calc(138 * var(--s));
  width: calc(183.058 * var(--s)); height: calc(170 * var(--s));
  transform-origin: 50% 50.703%;
  animation: eu-ring 76.2s linear infinite;
}

.tile-eu .eu-star {
  position: absolute;
  left: calc(var(--sx) * var(--s));
  top:  calc(var(--sy) * var(--s));
  width:  calc(28.756 * var(--s));
  height: calc(28.756 * var(--s));
  /* Um die eigene Mitte auf --sx/--sy setzen, damit das Gegensteuern dort
     ansetzt und der Stern beim Drehen nicht auswandert. */
  margin: calc(-14.378 * var(--s)) 0 0 calc(-14.378 * var(--s));
  background: url("assets/eu-star.svg") center / contain no-repeat;
  animation: eu-star 76.2s linear infinite;
}

@keyframes eu-ring { to { transform: rotate(360deg); } }
@keyframes eu-star { to { transform: rotate(-360deg); } }

@media (prefers-reduced-motion: reduce) {
  .tile-eu .eu-ring,
  .tile-eu .eu-star { animation: none; }
}

/* ============================================================
   Responsive Umbrüche

   Im Querformat gilt immer das Original-Raster — egal wie klein
   das Gerät ist. Umgebrochen wird ausschließlich im Hochformat.
   Die Kachel-Innengeometrie bleibt in jedem Fall unangetastet;
   es ändert sich nur, wie viele Kacheln nebeneinander stehen.
   ============================================================ */

/* Tablet hochkant: höchstens zwei Kacheln pro Zeile */
@media (orientation: portrait) and (max-width: 1023px) {
  .board { --g: 14px; border-radius: 16px; }
  .row   { flex-wrap: wrap; }

  .tile { border-radius: 10px; box-shadow: 2px 2px 10px rgba(0, 0, 0, .15); }

  /* Zeilen und Spalten geben ihre Box auf. Dadurch werden die Kacheln zu
     Kindern des Boards und lassen sich frei ordnen — `order` wirkt nur unter
     Geschwistern im selben Flex-Container, und in der Querformat-Schachtelung
     liegen sie über mehrere Container verteilt.

     Die Farbfeldzeile und das Paar behalten ihre Box: dort sollen die Kacheln
     nebeneinander bleiben, nicht untereinander. */
  .board > .row,
  .board .col { display: contents; }

  /* Jede Einheit volle Breite, Reihenfolge aus --o im Markup.
     `flex: 0 0 auto` statt `0 0 100 %`: das Board ist eine Spalte, dort meint
     die Flex-Basis die Höhe — die soll weiter aus dem Seitenverhältnis kommen. */
  .board > .row > .tile,
  .board .col > .tile,
  .board .col > .row--swatches,
  .board .pair { flex: 0 0 auto; width: 100%; order: var(--o); }

  /* Icon-Set + Feature-Liste bleiben ein Paar */
  .board .pair > .tile { flex: var(--w) 1 0; }

  /* Farbfelder bleiben vierspaltig */
  .row--swatches > .tile { flex: var(--w) 1 0; }
}

/* Mobil hochkant: alles einspaltig, Farbfelder im 2×2-Raster */
@media (orientation: portrait) and (max-width: 699px) {
  .board { --g: 12px; border-radius: 14px; }

  /* Auf dem Handy steht auch das Paar untereinander */
  .board .pair { display: contents; }
  .board .pair > .tile { flex: 0 0 auto; width: 100%; order: var(--o); }

  .row--swatches > .tile { flex: 0 0 calc((100% - var(--g)) / 2); }
}
