/*
 * Duell — Instrumententafel.
 *
 * Entwurfsthese: Neun Felder, absurd viel Maschinerie dahinter. Die Oberflaeche
 * verkleidet das nicht als Spielzeug, sie zeigt es. Ablesegeraet mit Brett in
 * der Mitte und dem Ereignisprotokoll als Rueckgrat.
 *
 * Zwei Entscheidungen, die aus der Domaene folgen und nicht aus Geschmack:
 *  - Die Palette ist bipolar. X und O sind ebenbuertig, keiner ist "der
 *    Akzent" — Messing gegen Ultramarin, je Farbschema gegen den Untergrund
 *    ausbalanciert. Verdigris ist ausschliesslich Systemzustand, Rost
 *    ausschliesslich Fehler. Ein Sieg leuchtet in der Farbe des Siegers.
 *  - Alles ist Monospace, ausser der Displayschrift. Kodierte Zuege,
 *    Halbzugzaehler, Ereignisnamen, Mitschrift: der Inhalt ist tabellarisch.
 */

@font-face {
  font-family: 'Plex Mono';
  src: url('./fonts/plex-mono-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Plex Mono';
  src: url('./fonts/plex-mono-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'Plex Mono';
  src: url('./fonts/plex-mono-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'Stamp';
  src: url('./fonts/big-shoulders-600-800.woff2') format('woff2');
  font-weight: 600 800;
  font-display: swap;
}

/* Animierbare Eigenschaften. Ohne @property waeren Zahlen und Laengen in
   Custom Properties nicht interpolierbar — der Schlitten spraenge dann. */
@property --travel {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
@property --sweep {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

:root {
  color-scheme: light dark;

  /* Hell ist technisches Papier, nicht Creme: ein Hauch Kühle statt Wärme,
     damit es neben dem Messing der Spielerfarbe nicht nach Kaffeehaus aussieht. */
  --field: light-dark(oklch(0.978 0.003 250), oklch(0.185 0.012 265));
  /* Im Hellen lagen Feld und Seite fast aufeinander — das Brett verschwand in
     neun blassen Kacheln. Jetzt traegt der Untergrund die Felder sichtbar. */
  --field-2: light-dark(oklch(0.928 0.006 250), oklch(0.225 0.014 265));
  --rule: light-dark(oklch(0.842 0.011 250), oklch(0.298 0.016 265));
  --ink: light-dark(oklch(0.25 0.014 265), oklch(0.93 0.008 85));
  --ink-quiet: light-dark(oklch(0.54 0.012 258), oklch(0.62 0.013 265));

  --brass: light-dark(oklch(0.60 0.145 62), oklch(0.82 0.140 78));
  --ultramarine: light-dark(oklch(0.50 0.190 268), oklch(0.72 0.155 268));
  --verdigris: light-dark(oklch(0.52 0.100 178), oklch(0.78 0.105 175));
  --rust: light-dark(oklch(0.52 0.180 30), oklch(0.70 0.155 32));

  --mark-x: var(--brass);
  --mark-o: var(--ultramarine);

  --gap: clamp(1.1rem, 3.5vw, 1.75rem);
  --radius: 3px;
  --settle: linear(
    0, 0.32 8%, 0.62 16%, 0.85 24%, 1.01 32%, 1.06 40%, 1.04 52%, 1 68%, 1
  );
}

* {
  box-sizing: border-box;
}

/* Muss nach den Layoutregeln gewinnen: `.invite` setzt display:flex, und die
   UA-Regel fuer [hidden] verliert dagegen. Ein verstecktes Feld, das trotzdem
   dasteht, ist kein Schoenheitsfehler — es verraet den Einladungslink. */
[hidden] {
  display: none !important;
}

html {
  /* iOS bläht Text in querformatigen Ansichten sonst eigenmächtig auf. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  /*
   * Der eigentliche Übeltäter beim Spielen: Doppeltippen zoomt. Wer zwei
   * Felder schnell hintereinander antippt, landet plötzlich in der
   * Vergrößerung. `manipulation` schaltet genau das ab — und lässt das
   * Zusammenziehen mit zwei Fingern in Ruhe, das jemand zum Lesen braucht.
   */
  touch-action: manipulation;
  padding: clamp(1.5rem, 5vw, 3rem) 1.1rem 5rem;
  background:
    radial-gradient(120% 60% at 50% -10%, color-mix(in oklch, var(--field-2), transparent 35%), transparent 70%),
    var(--field);
  color: var(--ink);
  font: 400 clamp(0.85rem, 2.6vw, 0.9rem) / 1.6 'Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  font-feature-settings: 'zero' 1;
  display: flex;
  justify-content: center;
  min-height: 100svh;
}

#app {
  width: 100%;
  max-width: 38rem;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* --- Kopf ---------------------------------------------------------------- */

.masthead {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  /*
   * Umbrechen, nicht ueberstehen. Angemeldet traegt die Leiste rechts einen
   * Knopf mehr, und auf einem Telefon passt sie dann nicht mehr neben den
   * Schriftzug. Ohne `wrap` wird die Seite breiter als das Geraet: Man kann
   * seitwaerts wischen, und weil der Koerper zentriert, verschwindet der
   * Ueberstand nach links ins Unerreichbare.
   */
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--rule);
}

.wordmark {
  font-family: 'Stamp', 'Plex Mono', sans-serif;
  font-weight: 800;
  font-size: clamp(2.4rem, 11vw, 3.6rem);
  line-height: 0.82;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin: 0;
}

.instruments {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.35rem 0.75rem;
  padding-bottom: 0.25rem;
}

.gauge {
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

button.gauge {
  cursor: pointer;
}
button.gauge:hover {
  color: var(--ink);
}

.gauge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.gauge[data-link='open'] {
  color: var(--verdigris);
}
.gauge[data-link='connecting'] {
  color: var(--brass);
}
.gauge[data-link='closed'] {
  color: var(--rust);
}
.gauge[data-link='open']::before {
  animation: breathe 2.4s ease-in-out infinite;
}
.gauge[data-sound='off']::before {
  background: none;
  box-shadow: inset 0 0 0 1px currentColor;
}

@keyframes breathe {
  50% {
    opacity: 0.35;
  }
}

/* --- Die Schiene zwischen den Gegnern ------------------------------------ */

.duel {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.9rem;
}

.peer {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  font-size: 0.82rem;
  min-width: 0;
}
.peer.o {
  flex-direction: row-reverse;
}

.peer .sigil {
  font-family: 'Stamp', monospace;
  font-weight: 700;
  font-size: 1.6rem;
  line-height: 1;
  transform: translateY(2px);
}
.peer.x .sigil {
  color: var(--mark-x);
}
.peer.o .sigil {
  color: var(--mark-o);
}

.peer .who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 8rem;
  color: var(--ink-quiet);
}
.peer[data-active='true'] .who {
  color: var(--ink);
}

/* Der Schlitten faehrt zu der Seite, die am Zug ist. Die Position ist eine
   interpolierte Zahl, damit das Fahren echtes Fahren ist und kein Umschalten. */
.rail {
  position: relative;
  height: 1px;
  background: linear-gradient(
    90deg,
    color-mix(in oklch, var(--mark-x), transparent 55%),
    var(--rule) 45% 55%,
    color-mix(in oklch, var(--mark-o), transparent 55%)
  );
  --travel: 0;
  transition: --travel 460ms var(--settle);
}
.rail[data-side='O'] {
  --travel: 1;
}

.rail::after {
  content: '';
  position: absolute;
  top: 50%;
  left: calc(var(--travel) * 100%);
  width: 9px;
  height: 9px;
  translate: -50% -50%;
  rotate: 45deg;
  background: color-mix(in oklch, var(--mark-x), var(--mark-o) calc(var(--travel) * 100%));
  box-shadow: 0 0 12px color-mix(in oklch, var(--mark-x), var(--mark-o) calc(var(--travel) * 100%));
}

/* Ein Puls laeuft bei jedem Ereignis ueber die Schiene — das Lebenszeichen. */
.rail::before {
  content: '';
  position: absolute;
  inset: -1px 0;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklch, var(--verdigris), transparent 30%),
    transparent
  );
  background-size: 28% 100%;
  background-repeat: no-repeat;
  background-position: calc(var(--sweep) * 140% - 20%) 0;
  opacity: 0;
}
.rail.pulsing::before {
  animation: sweep 620ms ease-out;
}

@keyframes sweep {
  from {
    --sweep: 0;
    opacity: 1;
  }
  to {
    --sweep: 1;
    opacity: 0;
  }
}

/* --- Stand --------------------------------------------------------------- */

.verdict {
  font-family: 'Stamp', 'Plex Mono', sans-serif;
  font-weight: 700;
  font-size: clamp(1.35rem, 5.5vw, 1.85rem);
  line-height: 1.1;
  letter-spacing: 0.005em;
  text-wrap: balance;
  margin: 0;
}
.verdict[data-mark='X'] {
  color: var(--mark-x);
}
.verdict[data-mark='O'] {
  color: var(--mark-o);
}

.readout {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.25rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin: 0.35rem 0 0;
}
.readout b {
  font-weight: 500;
  color: var(--ink);
}

/* --- Bretter ------------------------------------------------------------- */

.stage {
  container-type: inline-size;
  /* Auf dem Schirm beherrschten neun Kacheln von je 200 Pixeln die Seite.
     Die Grenze steht am Behaelter, nicht am Brett: Die Feldgroesse rechnet in
     cqi, und die muessen sich auf dieselbe Breite beziehen.

     `min(100%, …)` statt `max-width` allein: Als Flex-Element hat der Behaelter
     eine automatische Mindestbreite aus seinem Inhalt. Auf dem Telefon lief das
     Ultimate-Brett damit ueber den Rand hinaus — 30rem sind dort breiter als
     der Bildschirm. */
  width: min(100%, 26rem);
  min-width: 0;
}
.stage:has(.ultimate) {
  width: min(100%, 30rem);
}

.board {
  display: grid;
  gap: 3px;
  /*
   * Schmuck darf die Seite nicht breiter machen. Der Siegerring waechst auf
   * das 1,9-fache und bleibt dort stehen (`forwards`) — unsichtbar, aber mit
   * Platzbedarf. Auf einem Telefon stand die Seite dadurch nach der Partie
   * gut 30 px ueber, und man konnte seitwaerts wischen.
   *
   * `clip` und nicht `hidden`: Es entsteht kein Rollbereich, nur eine Kante.
   * Der Rand laesst den Ring ein Stueck ueber das Brett hinaus, bevor er
   * abgeschnitten wird.
   */
  overflow: clip;
  overflow-clip-margin: 0.5rem;
}
.board.mnk {
  grid-template-columns: repeat(var(--cols), 1fr);
}

.cell {
  appearance: none;
  /* Gitterelemente haben von Haus aus eine Mindestbreite aus ihrem Inhalt.
     Ohne das hier schiebt ein Feld sein Teilbrett auseinander. */
  min-width: 0;
  /* Langes Drücken auf ein Feld soll kein Auswahlmenü öffnen. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  font-family: 'Plex Mono', monospace;
  font-weight: 500;
  border: 1px solid var(--rule);
  background: var(--field-2);
  color: var(--ink);
  border-radius: var(--radius);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 0;
  position: relative;
  transition:
    background-color 140ms ease,
    border-color 140ms ease,
    opacity 320ms ease;
}
.board.mnk .cell {
  /* Gedeckelt: Ein durch die Spaltenzahl geteilter Containerwert waechst auf
     kleinen Brettern sonst ins Absurde. */
  font-size: clamp(1.1rem, calc(52cqi / var(--cols)), 3rem);
}

.cell.mark-x {
  color: var(--mark-x);
}
.cell.mark-o {
  color: var(--mark-o);
}

.cell.playable {
  cursor: pointer;
  border-color: color-mix(in oklch, var(--rule), var(--ink) 22%);
}
.cell.playable:hover {
  background: color-mix(in oklch, var(--field-2), var(--ink) 7%);
}
.cell.last {
  box-shadow: inset 0 0 0 1px color-mix(in oklch, currentColor, transparent 55%);
}
.cell:focus-visible {
  outline: 2px solid var(--verdigris);
  outline-offset: 2px;
  z-index: 1;
}

/* Der Einschlag: der Stein faellt nicht ein, er trifft auf. */
.cell.struck {
  animation: strike 260ms var(--settle);
}
.cell.struck::before {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid currentColor;
  border-radius: var(--radius);
  animation: shock 480ms ease-out forwards;
  /* Der Ring waechst auf das Doppelte und liegt damit ueber den Nachbarfeldern.
     Ohne diese Zeile schluckt er eine halbe Sekunde lang deren Klicks — man
     tippt neben einen frischen Zug, und nichts passiert. */
  pointer-events: none;
}

@keyframes strike {
  from {
    scale: 1.55;
    opacity: 0.2;
  }
}
@keyframes shock {
  from {
    scale: 1;
    opacity: 0.9;
  }
  to {
    scale: 1.9;
    opacity: 0;
  }
}

/* Sobald eine Linie steht, tritt alles andere zurueck — und die Linie zuendet
   Feld fuer Feld in ihrer eigenen Reihenfolge. */
.board:has(.cell.winning) .cell:not(.winning) {
  opacity: 0.25;
}
.cell.winning {
  border-color: currentColor;
  background: color-mix(in oklch, var(--field-2), currentColor 14%);
  animation: ignite 340ms ease-out backwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}
@keyframes ignite {
  from {
    background: var(--field-2);
    border-color: var(--rule);
    scale: 0.94;
  }
}

/* --- Ultimate ------------------------------------------------------------ */

.board.ultimate {
  grid-template-columns: repeat(3, 1fr);
  gap: 7px;
}
.sub {
  position: relative;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--rule);
  border-radius: 5px;
  transition: border-color 200ms ease, box-shadow 200ms ease;
}
.sub .cell {
  font-size: clamp(0.6rem, 5.5cqi, 1.15rem);
  border-radius: 2px;
}
.sub.target {
  border-color: color-mix(in oklch, var(--verdigris), transparent 30%);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--verdigris), transparent 65%);
}
.sub.decided .cell {
  opacity: 0.18;
}
.board:has(.sub.winning) .sub:not(.winning) {
  opacity: 0.3;
}
.sub.winning {
  border-color: currentColor;
  animation: ignite 340ms ease-out backwards;
  animation-delay: calc(var(--i, 0) * 110ms);
}

.verdict-stamp {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: 'Stamp', monospace;
  font-weight: 800;
  font-size: 13cqi;
  line-height: 1;
  pointer-events: none;
}
.verdict-stamp.mark-x {
  color: var(--mark-x);
}
.verdict-stamp.mark-o {
  color: var(--mark-o);
}
.verdict-stamp.drawn {
  color: var(--ink-quiet);
}
.verdict-stamp.fresh {
  animation: stamp 380ms var(--settle);
}
@keyframes stamp {
  from {
    scale: 2.1;
    rotate: -9deg;
    opacity: 0;
  }
}

/* --- Bedienung ----------------------------------------------------------- */

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.action {
  appearance: none;
  font: 500 0.78rem / 1 'Plex Mono', monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.7rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--field);
  cursor: pointer;
  text-decoration: none;
  transition: translate 120ms ease, box-shadow 120ms ease;
}
/* Direkte Kinder der Spalte werden gestreckt — eine Schaltflaeche ueber die
   volle Breite sieht nach Versehen aus, weil sie eins war. */
#app > .action {
  align-self: flex-start;
}
.action:hover {
  translate: 0 -1px;
  box-shadow: 0 2px 0 color-mix(in oklch, var(--ink), transparent 70%);
}
.action:active {
  translate: 0 0;
  box-shadow: none;
}
.action.quiet {
  background: none;
  color: var(--ink);
  border-color: var(--rule);
}
.action.quiet:hover {
  border-color: var(--ink-quiet);
}
.action:focus-visible,
button.gauge:focus-visible {
  outline: 2px solid var(--verdigris);
  outline-offset: 3px;
}
.action[disabled] {
  opacity: 0.4;
  cursor: default;
  translate: 0;
  box-shadow: none;
}

.note {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-left: 2px solid var(--rust);
  background: color-mix(in oklch, var(--rust), transparent 92%);
  font-size: 0.8rem;
}
.note[data-kind='info'] {
  border-left-color: var(--verdigris);
  background: color-mix(in oklch, var(--verdigris), transparent 92%);
}
.note .row {
  margin-top: 0.6rem;
}
.challenge {
  animation: arrive 320ms var(--settle);
}
@keyframes arrive {
  from {
    opacity: 0;
    translate: 0 -0.5rem;
  }
}

.invite {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.invite .row {
  display: flex;
  gap: 0.5rem;
}
.field {
  width: 100%;
  min-width: 0;
  background: var(--field-2);
  border: 1px solid var(--rule);
  color: var(--ink);
  border-radius: var(--radius);
  padding: 0.6rem 0.7rem;
  /*
   * Mindestens 16 Pixel. Unterhalb davon zoomt iOS beim Antippen eines
   * Eingabefeldes automatisch hinein und kommt nicht von allein wieder
   * heraus — das ist der zweite Grund, aus dem die Seite ungefragt springt.
   */
  font: 400 1rem / 1.3 'Plex Mono', monospace;
}
.field:focus-visible {
  outline: 2px solid var(--verdigris);
  outline-offset: 1px;
}

/* Nur in der Einladungszeile teilt sich das Feld den Platz mit der Schaltfläche.
   Die Kurzschreibweise darf NICHT global stehen: `.setup` ist ein
   Spalten-Container, dort wäre 10rem die Höhe und flex-grow streckte das Feld
   über die halbe Seite. Genau das ist auf dem Telefon passiert. */
.row .field {
  flex: 1 1 10rem;
  width: auto;
}

.caption {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* --- Das Protokoll: das Rueckgrat ---------------------------------------- */

.ledger {
  border-top: 1px solid var(--rule);
  padding-top: 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.entries {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  font-size: 0.74rem;
  max-height: 13.5rem;
  overflow-y: auto;
  scrollbar-width: thin;
}

.entry {
  display: grid;
  grid-template-columns: 2.2rem 1fr auto;
  gap: 0.75rem;
  align-items: baseline;
  padding: 0.22rem 0;
  border-bottom: 1px solid color-mix(in oklch, var(--rule), transparent 55%);
  white-space: nowrap;
}
.entry:last-child {
  border-bottom: 0;
}

.entry .v {
  color: var(--ink-quiet);
  font-variant-numeric: tabular-nums;
}
.entry .what {
  overflow: hidden;
  text-overflow: ellipsis;
}
.entry .meta {
  color: var(--ink-quiet);
  font-variant-numeric: tabular-nums;
}
.entry[data-kind='MatchStarted'] .what {
  color: var(--verdigris);
}
.entry[data-kind='MatchConcluded'] .what {
  color: var(--rust);
}
.entry[data-mark='X'] .what {
  color: var(--mark-x);
}
.entry[data-mark='O'] .what {
  color: var(--mark-o);
}

/* Neue Zeile faehrt heraus wie aus einem Drucker. @starting-style laesst das
   ohne Zusatzklasse laufen; wo es fehlt, erscheint sie schlicht sofort. */
@supports (transition-behavior: allow-discrete) {
  .entry {
    transition:
      opacity 260ms ease,
      translate 260ms var(--settle),
      clip-path 300ms ease;
  }
  @starting-style {
    .entry.fresh {
      opacity: 0;
      translate: -0.6rem 0;
      clip-path: inset(0 100% 0 0);
    }
  }
  .entry.fresh {
    opacity: 1;
    translate: 0 0;
    clip-path: inset(0 0 0 0);
  }
}

/* --- Auswahl: Schalter statt Aufklappmenüs ------------------------------- */

.switches {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
}

.switch {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.chip {
  appearance: none;
  user-select: none;
  -webkit-user-select: none;
  font: 500 0.74rem / 1 'Plex Mono', monospace;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.6rem 0.8rem;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--field-2);
  color: var(--ink-quiet);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
.chip:hover {
  color: var(--ink);
  border-color: var(--ink-quiet);
}
.chip[aria-checked='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--field);
}
.chip:focus-visible {
  outline: 2px solid var(--verdigris);
  outline-offset: 2px;
}

/* Eine Zeile erklärt die getroffene Wahl. Sie ersetzt Beschriftungen an jedem
   einzelnen Schalter — die würden die Reihe unlesbar machen. */
.hint {
  margin: 0;
  font-size: 0.78rem;
  color: var(--ink-quiet);
  min-height: 1.3em;
  text-wrap: balance;
}

/* Der Ablesestreifen: was passiert, wenn ich jetzt drücke. Die einzige Stelle
   auf diesem Bildschirm mit Farbe — und sie beantwortet die Frage, die man
   wirklich hat. */
.readout-line {
  margin: 0;
  padding: 0.85rem 0 0;
  border-top: 1px solid var(--rule);
  font-size: 0.92rem;
  text-wrap: balance;
}
.readout-line b {
  font-weight: 500;
}
.readout-line .as-x {
  color: var(--mark-x);
  font-weight: 600;
}
.readout-line .foe {
  color: var(--mark-o);
  font-weight: 600;
}

/* --- Karten: erst das Spiel, dann das Brett ------------------------------- */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.6rem;
}

.card {
  appearance: none;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--field-2);
  color: var(--ink);
  cursor: pointer;
  font-family: inherit;
  transition: border-color 120ms ease, background-color 120ms ease, translate 120ms ease;
}
.card:hover {
  border-color: var(--ink-quiet);
  translate: 0 -1px;
}
.card:focus-visible {
  outline: 2px solid var(--verdigris);
  outline-offset: 2px;
}
.card[aria-current='true'] {
  border-color: var(--ink);
  background: color-mix(in oklch, var(--field-2), var(--ink) 8%);
}

.card .titel {
  font-family: 'Stamp', 'Plex Mono', sans-serif;
  font-weight: 700;
  font-size: 1.25rem;
  line-height: 1;
  letter-spacing: 0.01em;
}
.card .zeile {
  font-size: 0.78rem;
  color: var(--ink-quiet);
  line-height: 1.4;
}

/* Der Weg zurück: eine Stufe, keine Verlaufsakrobatik. */
.zurueck {
  align-self: flex-start;
  background: none;
  border: 0;
  padding: 0;
  font: 500 0.72rem / 1 'Plex Mono', monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  cursor: pointer;
}
.zurueck:hover {
  color: var(--ink);
}
.zurueck::before {
  content: '← ';
}

/* --- Wer da ist ---------------------------------------------------------- */

.people {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.person {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid color-mix(in oklch, var(--rule), transparent 55%);
  font-size: 0.85rem;
}
.person:last-child {
  border-bottom: 0;
}

.person::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--verdigris);
  flex: none;
}
.person[data-playing='true']::before {
  background: var(--brass);
}

.person .name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.person .tag {
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.person .action {
  padding: 0.4rem 0.7rem;
  font-size: 0.68rem;
}

.empty {
  margin: 0;
  padding: 0.6rem 0;
  font-size: 0.85rem;
  color: var(--ink-quiet);
}

.guest-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.guest-form .field {
  flex: 1 1 8rem;
  width: auto;
}

/* --- Start --------------------------------------------------------------- */

.setup {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
  max-width: 22rem;
}
.setup label {
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin-top: 0.7rem;
}
.setup .action {
  margin-top: 1.2rem;
  align-self: flex-start;
}
.lede {
  margin: 0;
  font-size: 0.92rem;
  color: var(--ink-quiet);
  text-wrap: balance;
  max-width: 30rem;
}
.lede b {
  color: var(--ink);
  font-weight: 500;
}

#spray {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 5;
}

::selection {
  background: color-mix(in oklch, var(--verdigris), transparent 65%);
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
  .rail {
    transition: none;
  }
}


/* --- Das Kreuz ------------------------------------------------------------
 *
 * Ein Brett, das auf einem Tisch liegt: leicht gekippt, mit Tiefe, Loechern
 * statt Kreisen und Steinen, die darauf *stehen*.
 *
 * Drei Ebenen, und die Reihenfolge ist Aufbau, nicht Geschmack:
 *
 *   .ludo-szene    haelt die Perspektive
 *   .ludo-brett    ist die gekippte Platte (preserve-3d)
 *     .ludo-grid   die 56 Loecher
 *     .ludo-steine dieselbe Rasterung, aber die Steine — eigene Elemente,
 *                  damit ein Zug eine Bewegung sein kann und kein Umfaerben
 *
 * Warum das Abschneiden *ausserhalb* der Szene sitzt: `overflow` auf einem
 * Element mit `transform-style: preserve-3d` macht daraus wieder eine flache
 * Ebene. Die Kippung waere weg.
 */

.board.ludo {
  display: block;
  overflow: visible;
  container-type: inline-size;
}

.ludo-szene {
  perspective: 1100px;
  perspective-origin: 50% 42%;
  padding: 0.5rem 0 1.2rem;
}

.ludo-brett {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  transform-style: preserve-3d;
  transform: rotateX(20deg) rotateZ(0.001deg);
  border-radius: 2.5cqi;
  background:
    radial-gradient(80% 70% at 50% 12%, color-mix(in oklch, var(--field-2), var(--ink) 3%), transparent 70%),
    linear-gradient(168deg,
      color-mix(in oklch, var(--field-2), white 22%),
      color-mix(in oklch, var(--field-2), var(--ink) 10%));
  box-shadow:
    /* Die Kante der Platte — ein paar gestapelte Schatten sind billiger als
       echte Geometrie und auf einem Telefon nicht zu unterscheiden.
       Schwarz und nicht `--ink`: Im Dunkelmodus ist die Tinte hell, und die
       Kante leuchtete dann wie ein Neonstreifen unter dem Brett. */
    0 1px 0 rgb(255 255 255 / 0.35) inset,
    0 -2px 0 rgb(0 0 0 / 0.18) inset,
    0 3px 0 rgb(0 0 0 / 0.30),
    0 7px 0 rgb(0 0 0 / 0.20),
    0 11px 0 rgb(0 0 0 / 0.12),
    0 30px 44px -14px rgb(0 0 0 / 0.45);
}

.ludo-grid,
.ludo-steine {
  position: absolute;
  inset: 3.2cqi;
  display: grid;
  grid-template-columns: repeat(11, 1fr);
  grid-template-rows: repeat(11, 1fr);
  gap: 0.5cqi;
}

.ludo-steine {
  transform-style: preserve-3d;
}

/* --- Die Loecher ---------------------------------------------------------- */

.feld {
  border-radius: 50%;
  place-self: center;
  width: 82%;
  aspect-ratio: 1;
  background: color-mix(in oklch, var(--field), var(--ink) 6%);
  box-shadow:
    0 1.5px 2px color-mix(in oklch, var(--ink), transparent 78%) inset,
    0 -1px 0 color-mix(in oklch, white, transparent 60%) inset;
  transition:
    background-color 200ms ease,
    box-shadow 200ms ease,
    transform 200ms var(--settle);
}

.feld.einstieg {
  background: color-mix(in oklch, var(--field), var(--ink) 12%);
  box-shadow:
    0 0 0 1.5px color-mix(in oklch, var(--ink), transparent 60%) inset,
    0 1.5px 3px color-mix(in oklch, var(--ink), transparent 70%) inset;
}

.feld.haus,
.feld.ziel {
  width: 88%;
}
.feld.mark-x {
  /* `oklab`, nicht `oklch`: Beim Mischen im Zylinder nimmt der Farbton den
     kuerzesten Bogen — von Messing zu einem blaeulichen Weiss fuehrt der
     mitten durch Pink. Die Bahn sah aus wie Kaugummi. */
  background: color-mix(in oklab, var(--mark-x), var(--field) 62%);
  box-shadow:
    0 0 0 1.5px color-mix(in oklab, var(--mark-x), transparent 25%) inset,
    0 2px 3px rgb(0 0 0 / 0.22) inset;
}
.feld.mark-o {
  background: color-mix(in oklab, var(--mark-o), var(--field) 62%);
  box-shadow:
    0 0 0 1.5px color-mix(in oklab, var(--mark-o), transparent 25%) inset,
    0 2px 3px rgb(0 0 0 / 0.22) inset;
}

/* Wohin der gewaehlte Zug fuehrt. Ein Loch, in das gleich etwas faellt. */
.feld.tipp {
  background: color-mix(in oklab, var(--verdigris), var(--field) 55%);
  box-shadow:
    0 0 0 2px var(--verdigris) inset,
    0 0 12px color-mix(in oklab, var(--verdigris), transparent 45%);
  animation: tipp-atmet 1.6s ease-in-out infinite;
}
.feld.tipp.schlag {
  background: color-mix(in oklab, var(--rust), var(--field) 55%);
  box-shadow:
    0 0 0 2px var(--rust) inset,
    0 0 16px color-mix(in oklab, var(--rust), transparent 40%);
}

@keyframes tipp-atmet {
  50% {
    box-shadow:
      0 0 0 2.5px currentColor inset,
      0 0 20px color-mix(in oklab, var(--verdigris), transparent 25%);
  }
}

.ludo-mitte {
  place-self: stretch;
  border-radius: 1.2cqi;
  background: linear-gradient(
    90deg,
    color-mix(in oklab, var(--mark-x), var(--field) 45%) 0 50%,
    color-mix(in oklab, var(--mark-o), var(--field) 45%) 50% 100%
  );
  box-shadow:
    0 0 0 1px rgb(0 0 0 / 0.15) inset,
    0 2px 5px rgb(0 0 0 / 0.28) inset;
}

/* --- Die Steine ----------------------------------------------------------- */

.stein {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  place-self: center;
  width: 88%;
  aspect-ratio: 1;
  position: relative;
  transform-style: preserve-3d;
  cursor: default;
  /* Kein `transition` auf transform: Das Umsetzen laeuft ueber die
     Web-Animations-API, und eine Ueberblendung daneben wuerde damit kaempfen. */
}

/* Der Fuss liegt flach auf dem Brett und ist der Schatten, der den Stein
   ueberhaupt erst darauf stellt. */
.stein .fuss {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: -6%;
  height: 46%;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%,
    color-mix(in oklch, var(--ink), transparent 45%),
    transparent 70%);
  filter: blur(1.5px);
}

/* Der Koerper steht auf. Die Gegenkippung hebt die Brettneigung auf — dadurch
   sieht die Scheibe aus wie eine Spielfigur und nicht wie ein Fleck. */
.stein .koerper {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform: rotateX(-20deg) translateZ(2.2cqi);
  background:
    radial-gradient(38% 32% at 34% 26%, color-mix(in oklch, white, transparent 25%), transparent 62%),
    radial-gradient(80% 80% at 50% 118%, color-mix(in oklch, var(--ink), transparent 55%), transparent 60%),
    var(--stein-farbe, var(--ink));
  box-shadow:
    0 0 0 1px color-mix(in oklch, var(--ink), transparent 65%),
    0 2px 3px color-mix(in oklch, var(--ink), transparent 45%);
}

.stein.mark-x {
  --stein-farbe: var(--mark-x);
}
.stein.mark-o {
  --stein-farbe: var(--mark-o);
}

/* Anfassbar: Der Ring pulsiert, der Stein bewegt sich nicht. Ein Tippziel,
   das wandert, trifft man schlechter — und ein Browser, der auf Stillstand
   wartet, bevor er klickt, trifft es nie. */
.stein.playable {
  cursor: pointer;
}
.stein.playable .koerper {
  box-shadow:
    0 0 0 2px var(--field),
    0 0 0 4px var(--stein-farbe),
    0 3px 6px color-mix(in oklch, var(--ink), transparent 40%);
  animation: stein-ruft 1.5s ease-in-out infinite;
}
.stein.playable:hover .koerper {
  filter: brightness(1.12);
}

@keyframes stein-ruft {
  50% {
    box-shadow:
      0 0 0 2px var(--field),
      0 0 0 5.5px var(--stein-farbe),
      0 0 16px color-mix(in oklab, var(--stein-farbe), transparent 30%),
      0 3px 6px color-mix(in oklch, var(--ink), transparent 40%);
  }
}

/* Angekommen. */
.stein.daheim .koerper {
  box-shadow:
    0 0 0 1.5px color-mix(in oklch, white, transparent 40%) inset,
    0 0 10px color-mix(in oklab, var(--stein-farbe), transparent 55%),
    0 2px 3px color-mix(in oklch, var(--ink), transparent 45%);
}

/* Geschlagen: einmal um sich selbst, waehrend er nach Hause fliegt. */
.stein.geschlagen .koerper {
  animation: geschlagen 620ms var(--settle);
}

@keyframes geschlagen {
  0% {
    transform: rotateX(-20deg) translateZ(9cqi) rotate(0deg) scale(1.35);
    filter: brightness(1.6) saturate(0.4);
  }
  100% {
    transform: rotateX(-20deg) translateZ(2.2cqi) rotate(540deg) scale(1);
    filter: none;
  }
}

/* Gewonnen: die Steine im Ziel hüpfen der Reihe nach. */
.stein.winning .koerper {
  animation: sieg-huepfer 900ms var(--settle) calc(var(--i, 0) * 110ms) 3;
}

@keyframes sieg-huepfer {
  30% {
    transform: rotateX(-20deg) translateZ(12cqi) scale(1.2);
  }
  60% {
    transform: rotateX(-20deg) translateZ(2.2cqi) scale(0.92);
  }
}

/* --- Der Würfel ----------------------------------------------------------- */

.ludo-tray {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 1.4rem;
  flex-wrap: wrap;
}

.wuerfel {
  /*
   * Zwei Groessen, und das ist kein Versehen: Der Koerper ragt in alle
   * Richtungen ueber seine Kantenlaenge hinaus — Seitenflaeche, Perspektive,
   * Schatten. Der Knopf ist darum groesser als der Wuerfel, sonst laeuft der
   * in den Text daneben.
   */
  --kante: 2.9rem;
  --halb: 1.45rem;
  display: grid;
  place-items: center;
  /* Elfenbein im Hellen, Graphit im Dunkeln — der Wuerfel darf nicht dieselbe
     Farbe haben wie die Seite, sonst ist er nur ein weisses Rechteck. */
  --wuerfel-hell: light-dark(oklch(0.97 0.015 85), oklch(0.36 0.012 265));
  --wuerfel-dunkel: light-dark(oklch(0.9 0.025 80), oklch(0.26 0.012 265));
  --wuerfel-auge: light-dark(oklch(0.28 0.03 60), oklch(0.9 0.02 85));
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  width: 5rem;
  height: 4.4rem;
  flex: none;
  /* Brennweite: zu kurz und die vordere Flaeche waechst ins Absurde, zu lang
     und der Koerper sieht aus wie ein Kaertchen. */
  perspective: 520px;
  cursor: default;
  /*
   * Der Schlagschatten gehoert hierher und **nicht** auf den Wuerfel selbst.
   * Ein `filter` erzwingt auf seinem Element `transform-style: flat` — der
   * Wuerfel wurde damit plattgedrueckt, alle sechs Seiten auf eine Ebene. Er
   * sah aus wie eine Oblate, und keine Drehung der Welt half dagegen.
   */
  filter: drop-shadow(0 12px 14px rgb(0 0 0 / 0.5));
}

.wuerfel.bereit {
  cursor: pointer;
}

/* Der Wuerfel selbst dreht sich; der Knopf darum steht still. Sonst wartet ein
   Browser beim Klicken auf einen Stillstand, der nie kommt. */
.cube {
  position: relative;
  display: block;
  width: var(--kante);
  height: var(--kante);
  transform-style: preserve-3d;
  transform: rotateX(var(--kipp-x, -18deg)) rotateY(var(--kipp-y, 26deg));
}

.wuerfel.bereit .cube {
  animation: wuerfel-wartet 2.6s ease-in-out infinite;
}

@keyframes wuerfel-wartet {
  50% {
    transform: rotateX(-12deg) rotateY(34deg) translateY(-4px);
  }
}

/*
 * Licht. Ohne unterschiedlich helle Seiten ist ein Wuerfel eine Ansammlung
 * gleicher Quadrate und liest sich flach — die Kanten verschwinden.
 * Gegenueberliegende Seiten ergeben sieben; oben ist hell, die Seiten sind
 * dunkler.
 */
.seite[data-zahl='3'] { --dunkel: 0; }
.seite[data-zahl='1'] { --dunkel: 0.06; }
.seite[data-zahl='2'] { --dunkel: 0.2; }
.seite[data-zahl='5'] { --dunkel: 0.16; }
.seite[data-zahl='6'] { --dunkel: 0.24; }
.seite[data-zahl='4'] { --dunkel: 0.3; }

.seite {
  position: absolute;
  inset: 0;
  border-radius: 0.55rem;
  display: grid;
  grid-template: repeat(3, 1fr) / repeat(3, 1fr);
  padding: 14%;
  gap: 6%;
  backface-visibility: hidden;
  background: linear-gradient(150deg, var(--wuerfel-hell), var(--wuerfel-dunkel));
  box-shadow:
    0 0 0 1px rgb(0 0 0 / 0.3) inset,
    0 0 16px rgb(0 0 0 / 0.14) inset,
    /* Der Schatten der Seite. Ein `filter` waere naheliegender, wuerde die
       Flaeche aber aus dem Raum des Wuerfels loesen — dieser hier faerbt nur
       den Hintergrund und laesst die Augen darueber stehen. */
    0 0 0 999px rgb(0 0 0 / var(--dunkel, 0)) inset;
}

.auge {
  place-self: center;
  width: 74%;
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(40% 40% at 35% 30%, rgb(255 255 255 / 0.4), transparent 60%),
    var(--wuerfel-auge);
  box-shadow:
    0 1px 1px rgb(255 255 255 / 0.35),
    0 0 2px rgb(0 0 0 / 0.4) inset;
}

.wurf-hinweis {
  margin: 0;
  color: var(--ink-quiet);
  flex: 1 1 8rem;
  min-width: 0;
}

.weiter {
  flex: none;
}

@media (prefers-reduced-motion: reduce) {
  .feld.tipp,
  .stein.playable .koerper,
  .wuerfel.bereit .cube,
  .stein.winning .koerper {
    animation: none;
  }
  .ludo-brett {
    transform: none;
  }
  .stein .koerper {
    transform: none;
  }
}

/* --- Das Kreuz in drei Dimensionen ---------------------------------------- */

.board.ludo3d {
  display: block;
  overflow: visible;
  container-type: normal;
}

.ludo-buehne {
  position: relative;
  width: 100%;
  /* Flacher als quadratisch: Ein gekipptes Brett ist breit und niedrig. */
  aspect-ratio: 25 / 18;
  border-radius: 0.6rem;
  overflow: hidden;
  /* Der Tisch reicht ueber den Rand der Szene hinaus — sonst sieht man, wo die
     Welt aufhoert. */
  background: radial-gradient(120% 90% at 50% 15%, #1b2136, #070a12 75%);
  box-shadow:
    0 1px 0 rgb(255 255 255 / 0.06) inset,
    0 18px 34px -18px rgb(0 0 0 / 0.7);
}

.ludo-leinwand {
  /*
   * Die Leinwand fuellt ihre Buehne — immer, in beiden Modi.
   *
   * Vorher gab die Leinwand ihre Hoehe selbst vor und die Buehne richtete sich
   * danach; im Vollbild war es umgekehrt. Zwei Richtungen fuer dieselbe Groesse
   * sind eine zu viel: Im Querformat kam eine Hoehe aus dem falschen Modus
   * heraus, und das Brett stand klein in einem grossen schwarzen Feld.
   */
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /*
   * `none` und nicht `manipulation`: Der Wuerfel wird mit dem Finger geworfen,
   * und dafuer muss die Seite die Geste in Ruhe lassen. Gescrollt wird
   * ausserhalb des Bretts — Platz dafuer ist genug.
   */
  touch-action: none;
  cursor: pointer;
}

.ludo-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.7rem;
}

.ludo-controls .zug {
  flex: 0 1 auto;
}

.board.ludo3d .wurf-hinweis {
  margin: 0.8rem 0 0;
  color: var(--ink-quiet);
}

/* --- Vollbild und Querformat ---------------------------------------------- */

.vollbild-schalter {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 0.5rem);
  right: calc(env(safe-area-inset-right) + 0.5rem);
  z-index: 2;
  appearance: none;
  border: 1px solid rgb(255 255 255 / 0.25);
  border-radius: 999px;
  background: rgb(10 13 22 / 0.6);
  color: rgb(255 255 255 / 0.85);
  font: inherit;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.35rem 0.75rem;
  cursor: pointer;
  backdrop-filter: blur(6px);
}
.vollbild-schalter:hover {
  background: rgb(10 13 22 / 0.85);
  color: #fff;
}

/*
 * Der eigene Vollbildmodus.
 *
 * Nicht die Fullscreen-API, sondern eine feste Lage ueber der ganzen Seite:
 * iOS Safari kennt die API fuer normale Elemente nicht. Wo es sie gibt, wird
 * sie zusaetzlich benutzt — dann verschwindet auch die Browserleiste.
 */
.board.ludo3d.vollbild {
  position: fixed;
  inset: 0;
  z-index: 60;
  overflow: hidden;
  margin: 0;
  background: #05070d;
  display: grid;
  place-items: stretch;
}

/*
 * Im Vollbild liegen Hinweis und Schaltflaechen *ueber* dem Brett.
 *
 * Untereinander gestapelt nehmen sie der Buehne fast ein Viertel der Hoehe —
 * und weil das Bild dadurch noch breiter wird als es ohnehin ist, bleibt links
 * und rechts umso mehr Schwarz. Uebergelegt bekommt die Szene den ganzen
 * Bildschirm.
 */
.board.ludo3d.vollbild .ludo-rahmen {
  display: block;
  padding: 0;
}

.board.ludo3d.vollbild .ludo-buehne {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
  border-radius: 0;
}

.board.ludo3d.vollbild .wurf-hinweis {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(env(safe-area-inset-bottom) + 3.4rem);
  z-index: 2;
  margin: 0;
  text-align: center;
  color: rgb(255 255 255 / 0.72);
  pointer-events: none;
}

.board.ludo3d.vollbild .ludo-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(env(safe-area-inset-bottom) + 0.7rem);
  z-index: 2;
  margin: 0;
  justify-content: center;
}

.board.ludo3d.vollbild .ludo-controls .zug {
  border-color: rgb(255 255 255 / 0.3);
  color: rgb(255 255 255 / 0.92);
}

/*
 * Hochkant im Vollbild wird der *Inhalt* quer gelegt, nicht das Geraet.
 *
 * Die Ausrichtung laesst sich auf iOS nicht sperren. Wer das Telefon dann
 * dreht, bekommt vom Betriebssystem Querformat — und weil diese Regel nur
 * hochkant greift, hebt sie sich in dem Moment selbst auf. Beides zusammen
 * ergibt: quer, egal wie man es haelt.
 */
@media (orientation: portrait) {
  .board.ludo3d.vollbild .ludo-rahmen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100vh;
    height: 100vw;
    transform-origin: top left;
    /* Erst drehen (die Kante zeigt danach nach unten), dann im gedrehten
       Koordinatensystem zurueckschieben. Genau so liegt der Rahmen wieder
       deckungsgleich auf dem Bildschirm. */
    transform: rotate(90deg) translateY(-100%);
  }
}
