:root {
  /* --- Flaechen ---
     Neutral-warmes Dunkel statt Blauschwarz, und kleinere Schritte zwischen den
     Ebenen: Die Oberflaeche soll geschichtet wirken, nicht gestapelt. */
  --bg: #131316;
  --surface: #1a1a1e;
  --surface-2: #232328;

  /* Linien als durchscheinendes Weiss statt als feste Farbe. Derselbe Wert
     sitzt damit auf jeder Ebene richtig – ein fester Ton waere auf --bg zu
     hell und auf --surface-2 zu dunkel. */
  --line: rgb(255 255 255 / 0.09);
  --line-strong: rgb(255 255 255 / 0.18);

  --text: #ecebef;
  --muted: #94939d;

  /* --- Akzent ---
     Alles Akzentfarbige auf allen drei Seiten haengt an diesen beiden Werten.
     Der Ton ist sehr hell (Leuchtdichte 0,87), deshalb steht ueberall dort, wo
     er als Flaeche auftritt, dunkle Schrift darauf – Weiss kaeme auf 1,1:1 und
     waere schlicht unlesbar. */
  --accent: #deff00;
  --accent-hi: #e9ff4d;

  /* Zustandsfarben, bewusst gedaempft: die kraeftigen Toene gehoeren der
     Leinwand, nicht der Bedienung. */
  --ok: #7fbf92;
  --frozen: #74b4c4;
  --danger: #e0897a;
  --danger-line: #5e332a;
  --danger-bg: #1e1716;

  /* --- Form ---
     Eine Staffel statt handgesetzter Werte pro Bauteil. */
  --radius: 20px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --radius-xs: 6px;
  --radius-pill: 999px;

  /* Abstand zwischen den Karten. Als Token, weil ihn zwei Raster brauchen:
     .admin fuer Kopfzeile, Anmeldung und Panel, #panel fuer die Karten darin.
     Zwei getrennte Zahlen wuerden frueher oder spaeter auseinanderlaufen. */
  --card-gap: 24px;
  /* Vom Board-Snapshot gesetzt, Fallback = aktuelle Leinwand. */
  --board-w: 100;
  --board-h: 60;
  --cell: 10px;
  /* Aktuell gewaehlte Farbe plus die darauf lesbare Schriftfarbe. Vorrat,
     Cursor und Setzen-Knopf haengen daran, play.js setzt beide um. */
  --ink: #ff4500;
  --ink-on: #131316;
  --kiosk-pad: clamp(16px, 3vmin, 48px);
  --kiosk-gap: clamp(24px, 4vmin, 64px);
  --qr-size: clamp(160px, 22vmin, 360px);
  /* Wochenbahn unter der Leinwand. Feste Hoehe, weil die Leinwand ihre eigene
     Hoehe um genau diesen Block kuerzen muss – gemessen bekommt CSS sie nicht
     zurueck in die Rechnung. */
  --week-h: clamp(32px, 3.9vmin, 54px);
  --week-gap: clamp(10px, 1.6vmin, 26px);
  --track-h: clamp(4px, 0.5vmin, 7px);
  --week-font: clamp(9px, 1.05vmin, 14px);
  /* Breite der Schnitte zwischen den Tagen. In Grundfarbe gezeichnet, sie
     schneiden die Bahn also wirklich durch statt nur darauf zu liegen. */
  --cut-w: clamp(3px, 0.4vmin, 5px);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, system-ui, sans-serif;
}

* { box-sizing: border-box; }

/* Muss nach den display-Regeln der Komponenten gewinnen – sonst hebelt ein
   display: grid auf einer Karte das hidden-Attribut aus. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

h1, h2, p { margin: 0; }

canvas { image-rendering: pixelated; image-rendering: crisp-edges; display: block; }

/* Icons aus dem Lucide-Set (lucide.dev), auf beiden Seiten inline als SVG. */
.lucide { width: 22px; height: 22px; display: block; }
.lucide.sm { width: 16px; height: 16px; }

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Einschlag: gesetzte Pixel kurz markieren ---
   Dieselbe Markierung auf allen drei Seiten: Der Ring reicht knapp ueber den
   Pixel hinaus, blendet aus - und mehr nicht. Das Auge findet ihn, ohne dass
   er neben einem Formular oder unter dem eigenen Daumen stoert.

   Animiert werden ausschliesslich `opacity` und `transform`. Beides laeuft im
   Compositor, also ohne Layout und ohne Neuzeichnen: Ein Handy und ein
   Raspberry Pi tragen die Markierung genauso wie ein Laptop, und der
   Hauptthread bleibt fuer die Bedienung frei. */
.flashes {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Die Stelle steht in Board-Pixeln (--x/--y aus flash.js) und wird hier in
   Prozent umgerechnet. Damit gilt dieselbe Regel fuer die gezoomte Leinwand
   auf dem Handy wie fuer die kleine Vorschau im Admin - und ein Formatwechsel
   zieht sie ueber --board-w/--board-h von selbst nach. */
.flash {
  position: absolute;
  left: calc(var(--x) * 100% / var(--board-w));
  top: calc(var(--y) * 100% / var(--board-h));
  width: calc(100% / var(--board-w));
  height: calc(100% / var(--board-h));
  /* Heller Ring mit dunklem Saum nach aussen: Die Palette reicht von Weiss bis
     Schwarz, eine einzelne Farbe verschwaende auf dem einen oder anderen Ende.
     Beide Schatten liegen ausserhalb - innen bleibt der gesetzte Pixel in
     seiner Farbe stehen, darum geht es ja. */
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.9), 0 0 0 2px rgb(0 0 0 / 0.45);
  animation: flash 600ms ease-out forwards;
}

@keyframes flash {
  from { opacity: 1; transform: scale(1); }
  to { opacity: 0; transform: scale(2.2); }
}

/* Ohne Bewegung bleibt das Ausblenden: Es zeigt dieselbe Stelle, ohne dass
   etwas ueber den Schirm waechst. */
@keyframes flash-still {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* ---------- Übersicht (Kiosk / grosser Screen) ---------- */

/* Die Leinwand mit der Wochenleiste darunter, daneben der QR-Code. */
.kiosk {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100dvh;
  padding: var(--kiosk-pad);
}

/* flex-end setzt den QR auf die Unterkante der Spalte; sein margin-bottom hebt
   ihn von dort auf die Unterkante der Zeichenfläche. Die Zeile ist nur so hoch
   wie Leinwand plus Leiste, .kiosk zentriert sie als Ganzes. */
.stage {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: var(--kiosk-gap);
}

/* Wunschbreite = so breit, wie die Höhe es hergibt (Abzug: doppeltes Padding,
   dazu die Wochenleiste samt ihrem Abstand). Passt das zusammen mit dem QR
   nicht in die Zeile, schrumpft Flexbox die Spalte und aspect-ratio zieht die
   Höhe der Leinwand nach. */
.canvas-col {
  flex: 0 1 calc(
    (100dvh - 2 * var(--kiosk-pad) - var(--week-h) - var(--week-gap))
    * var(--board-w) / var(--board-h)
  );
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--week-gap);
}

.frame {
  width: 100%;
  aspect-ratio: var(--board-w) / var(--board-h);
  position: relative;
  border-radius: var(--radius);
  background: #fff;
  box-shadow: 0 0 0 1px var(--line);
}

/* Der weiche Schatten liegt auf einer eigenen Ebene hinter den Canvases statt
   auf dem Rahmen selbst. Auf dem Rahmen teilt er sich die Zeichenflaeche mit
   ihnen und wird auf einer schwachen GPU bei jedem Pixel mitgemalt. */
.frame::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 32px 70px -34px rgb(0 0 0 / 0.75);
}

/* Die Ecken rundet die Leinwand selbst. Ein `overflow: hidden` auf dem Rahmen
   taete dasselbe, zwaenge Chromium dafuer aber zu einer Maskenebene - und die
   loest es auf dem Kiosk in jedem Frame neu auf. */
.frame canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }

/* Weiss auf transparent - der QR sitzt direkt auf dem Seitenhintergrund. Der
   wird damit zur Ruhezone, deshalb kommt der QR ohne eigenen Rand (margin=0)
   und steht wirklich bündig auf der Kante. --kiosk-gap hält links Abstand. */
.qr {
  flex: 0 0 auto;
  width: var(--qr-size);
  aspect-ratio: 1;
  display: block;
  /* Hebt den Code um die Leiste an: seine Unterkante trifft damit die der
     Zeichenfläche, nicht die der Spalte. */
  margin-bottom: calc(var(--week-h) + var(--week-gap));
}

/* --- Wochenbahn: eine Spur, die sich fuellt --- */

/* Halb so breit wie die Leinwand und mittig darunter: schmal genug, um
   Grundlinie zu bleiben statt Bauteil zu werden. */
.week {
  position: relative;
  width: 50%;
  height: var(--week-h);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(var(--week-h) * 0.22);
}

/* Der heutige Tag liegt auf einer eigenen Spalte – ueber Bahn und Beschriftung
   hinweg, damit beides zusammen als ein Tag liest. Das ist der
   Hervorhebungszustand je Tag; am Wochenende faellt er weg. */
.today {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--day-index, 0) * 20%);
  width: 20%;
  border-radius: var(--radius-sm);
  background: rgb(255 255 255 / 0.05);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: left 0.4s ease;
}

.track {
  position: relative;
  height: var(--track-h);
  border-radius: var(--radius-pill);
  background: var(--line);
}

/* Die Spur waechst kontinuierlich, nicht in Tagesschritten: ueber einen
   Nachmittag wandert sie sichtbar weiter. Das ist der Unterschied zwischen
   einer Anzeige und einer Uhr. */
.trail {
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--progress, 0) * 100%);
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.6s ease;
}

/* Vier Schnitte in Grundfarbe teilen die Bahn in fuenf Tage. Sie liegen ueber
   Bahn und Spur, trennen also beide – ein Strich nur auf dem Untergrund waere
   genau da verschwunden, wo die Spur schon liegt. */
.cuts i {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--cut-w);
  translate: -50% 0;
  background: var(--bg);
}

.cuts i:nth-child(1) { left: 20%; }
.cuts i:nth-child(2) { left: 40%; }
.cuts i:nth-child(3) { left: 60%; }
.cuts i:nth-child(4) { left: 80%; }

/* Der Pinsel an der Spitze der Spur: derselbe Gedanke wie der Cursor auf der
   Spielseite, und er blinkt genauso. Ring innen dunkel und aussen in
   Grundfarbe – auf dem hellen Akzent waere ein weisser Ring unsichtbar. */
.brush {
  position: absolute;
  top: 50%;
  left: calc(var(--progress, 0) * 100%);
  translate: -50% -50%;
  width: calc(var(--week-h) * 0.3);
  height: calc(var(--week-h) * 0.3);
  border-radius: 3px;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
  transition: left 0.6s ease;
  z-index: 1;
}

/* Der innere Ring blinkt als eigene Ebene ueber `opacity`. Als Schatten getarnt
   liefe dasselbe Blinken nicht im Compositor, sondern zwaenge zu einem Repaint
   je Frame - rund um die Uhr, auch in der Nacht, in der niemand malt. Auf dem
   Kiosk-Pi ist das die einzige Last, die ohne jedes Zutun anfaellt. */
.brush::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1.5px rgb(0 0 0 / 0.5);
  animation: cursor-blink 1.7s ease-in-out infinite;
}

/* 0.24 von 0.5 ist die 0.12, die der Ring vorher im Schatten annahm. */
@keyframes cursor-blink {
  50% { opacity: 0.24; }
}

/* Fuenf Marken unter der Bahn, jede mittig ueber ihrem Fuenftel. */
.days {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--week-font);
  font-weight: 600;
  line-height: 1;
  color: var(--muted);
}

.days li { text-align: center; }

.days li[aria-current] { color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .brush::after { animation: none; }
  .trail, .brush, .today { transition: none; }
}

@media (max-width: 760px) {
  .kiosk { height: auto; min-height: 100dvh; }
  .stage { flex-direction: column; align-items: center; }
  /* Untereinander gibt es keine gemeinsame Grundlinie mehr, die der QR treffen
     muesste – und die Leinwand richtet sich nach der Breite statt der Hoehe. */
  .canvas-col { flex: 0 0 auto; width: 100%; }
  .qr { width: clamp(140px, 40vw, 240px); margin-bottom: 0; }
}

/* ---------- Spielseite ---------- */

.play {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  overflow: hidden;
  padding-top: env(safe-area-inset-top);
}

/* --- Leinwand --- */

.viewport {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  background: var(--bg);
  cursor: crosshair;
}

.surface {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.surface canvas {
  width: calc(var(--board-w) * var(--cell));
  height: calc(var(--board-h) * var(--cell));
  background: #fff;
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.12), 0 20px 56px -22px rgb(0 0 0 / 0.7);
}

/* Der Cursor zeigt die gewählte Farbe schon an der Stelle, an der sie landen
   würde - das Pulsieren macht den Unterschied zu einem gesetzten Pixel. */
.cursor {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  pointer-events: none;
  background: var(--ink);
  box-shadow: 0 0 0 1.5px #000, inset 0 0 0 1.5px #fff;
  display: none;
}

.cursor.visible { display: block; animation: pulse 1.4s ease-in-out infinite; }

@keyframes pulse { 50% { opacity: 0.45; } }

/* Schwebende Elemente über der Leinwand. */
.coord, .fab, .keyhints {
  position: absolute;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  background: rgb(26 26 30 / 0.78);
  backdrop-filter: blur(14px);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: var(--text);
}

.coord {
  left: 12px;
  padding: 7px 11px;
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  pointer-events: none;
}

.fab {
  right: 12px;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  padding: 0;
  cursor: pointer;
}

.fab:active { transform: scale(0.94); }

/* Zweiter Knopf in derselben rechten Spalte, eine Etage ueber der Uebersicht:
   44 px Knopf plus 12 px Abstand plus die 12 px, die unten schon stehen. */
.keyhints-toggle {
  display: none;
  bottom: calc(68px + env(safe-area-inset-bottom, 0px));
  color: var(--muted);
}

/* Aufgeklappt bleibt der Knopf sichtbar hell - er ist jetzt der Schliesser. */
.keyhints-toggle[aria-expanded="true"] { color: var(--text); }

/* --- Tastenhilfe --- */

/* Steht ueber dem Uebersicht-Knopf in derselben rechten Spalte. Auf dem Handy
   gibt es keine Tastatur - dort bleibt sie komplett weg (siehe unten), deshalb
   kostet sie dort auch keinen Platz. */
.keyhints {
  display: none;
  /* Klappt neben dem Knopf auf, auf gleicher Grundlinie: 12 + 44 + 12 px. */
  right: 68px;
  bottom: calc(68px + env(safe-area-inset-bottom, 0px));
  grid-template-columns: auto auto;
  align-items: center;
  column-gap: 8px;
  row-gap: 5px;
  padding: 9px 11px;
  /* Dezent wird die Hilfe ueber Groesse und Farbe, nicht ueber Transparenz:
     ein durchscheinender Kasten wuerde auf buntem Board unruhig. Die Tasten
     stehen in Textfarbe, die Erklaerung daneben gedaempft. */
  color: var(--muted);
  font-size: 11px;
  /* Reine Beschriftung - Klicks und Zeiger gehoeren der Leinwand darunter. */
  pointer-events: none;
}

.keyhints .keys { display: flex; align-items: center; gap: 3px; }

.keyhints .plus { margin: 0 1px; opacity: 0.6; }

/* Tastenkappe: heller Rand oben, dunkler Schatten unten - das reicht, damit
   man die Form als Taste liest, ohne dass sie Aufmerksamkeit zieht. */
.key {
  display: grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 3px;
  border-radius: var(--radius-xs);
  background: rgb(255 255 255 / 0.08);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.12), 0 1px 0 rgb(0 0 0 / 0.4);
  color: var(--text);
  font: inherit;
  font-size: 10px;
  font-weight: 600;
}

.key.wide { padding: 0 6px; }

.key .lucide { width: 12px; height: 12px; }

/* Nur mit Maus und Tastatur. Ein Touchscreen hat keine Pfeiltasten, und auf
   dem Handy ist der Platz ueber dem Knopf ohnehin Leinwand. */
@media (hover: hover) and (pointer: fine) {
  .keyhints:not([hidden]) { display: grid; }
  .keyhints-toggle:not([hidden]) { display: grid; }
}

/* --- Bedienleiste --- */

.controls {
  flex: 0 0 auto;
  background: var(--surface);
  border-top: 1px solid var(--line);
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
  display: grid;
  gap: 12px;
}

/* Auf breiten Fenstern sollen die Farbfelder nicht auf Handtellergröße wachsen.
   min-width: 0 verhindert, dass ein breiter Zeileninhalt das Grid aufblaeht. */
.controls > * {
  width: 100%;
  min-width: 0;
  max-width: 560px;
  margin-inline: auto;
}

/* --- Pixel-Vorrat --- */

.stock { display: flex; align-items: center; gap: 12px; }

.stock-count {
  flex: 0 0 auto;
  min-width: 2ch;
  text-align: right;
  font-size: 26px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  transition: color 0.25s;
}

.stock-count[data-empty] { color: var(--muted); }

/* Der Vorrat als Reihe echter Pixel in der gewählten Farbe: gefüllt = sofort
   setzbar, der Rest ist Platz zum Ansparen. Segmente statt fester Kacheln,
   damit auch ein hohes MAX_BANKED in die Zeile passt. */
.mag {
  flex: 1;
  min-width: 0;
  display: flex;
  gap: 3px;
  height: 20px;
}

.cell {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  overflow: hidden;
  border-radius: 5px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.09);
  transition: background 0.25s, box-shadow 0.25s;
}

/* Der Rahmen bleibt in beiden Zuständen neutral - sonst verschwindet ein
   schwarz gefülltes Feld im dunklen Panel. */
.cell.on { background: var(--ink); }

/* Das nachwachsende Pixel füllt sich von unten auf - ein Verlaufsbalken, der
   nebenbei sagt, wie weit der Cooldown ist. */
.cell.charging::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: var(--fill, 0%);
  background: var(--ink);
  opacity: 0.8;
}

.cell.charging { animation: breathe 2.4s ease-in-out infinite; }

@keyframes breathe { 50% { box-shadow: inset 0 0 0 1px var(--ink); } }

@keyframes cell-pop { from { transform: scale(0.35); } to { transform: scale(1); } }

.cell.pop { animation: cell-pop 0.34s cubic-bezier(0.2, 0.9, 0.3, 1.5); }

/* Ab ~40 Plätzen wäre jedes Segment nur noch ein Strich - dann lieber ein
   durchgehender Balken, die Zahl daneben trägt die Genauigkeit. */
.mag.meter { display: block; border-radius: var(--radius-pill); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; }
.mag.meter .fill { display: block; height: 100%; background: var(--ink); transition: width 0.3s; }

.stock-timer {
  flex: 0 0 auto;
  min-width: 4ch;
  text-align: right;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* --- Farben --- */

.palette {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 6px;
}

.swatch {
  aspect-ratio: 1;
  border: none;
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.35);
  cursor: pointer;
  padding: 0;
  transition: transform 0.12s, box-shadow 0.12s;
}

/* Doppelter Ring - erst in der Panel-Farbe, dann hell. Sitzt damit auf jedem
   der 16 Farbfelder sichtbar, auch auf Weiss. */
.swatch[aria-pressed="true"] {
  transform: scale(1.08);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.35), 0 0 0 2px var(--surface), 0 0 0 3.5px var(--text);
  z-index: 1;
}

/* --- Setzen --- */

/* Der Knopf nimmt die gewählte Farbe an - damit muss nirgends stehen, welche
   Farbe gleich gesetzt wird. --ink-on ist Schwarz oder Weiss, je nachdem was
   auf der Farbe besser lesbar ist. */
.place {
  display: block;
  padding: 16px 18px;
  border: none;
  border-radius: var(--radius-md);
  background: var(--ink);
  color: var(--ink-on, #fff);
  /* Zwei Haarlinien: die dunkle trägt helle Farben, die helle dunkle. */
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18), 0 0 0 1px rgb(255 255 255 / 0.12);
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, transform 0.08s;
}

.place:active:not(:disabled) { transform: scale(0.985); }

.place:disabled {
  background: var(--surface-2);
  color: var(--muted);
  box-shadow: none;
  cursor: not-allowed;
}


.toast {
  position: fixed;
  left: 50%;
  top: calc(12px + env(safe-area-inset-top));
  /* Versteckt: um die eigene Hoehe plus den top-Abstand nach oben geschoben.
     Prozente allein reichen nicht - ein leerer Toast ist nur 24 px hoch, und
     -140 % davon liessen die Pille 2 px am oberen Rand stehen. */
  transform: translate(-50%, calc(-100% - 16px - env(safe-area-inset-top)));
  opacity: 0;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--text);
  padding: 11px 18px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 14px 34px -16px rgb(0 0 0 / 0.8);
  transition: transform 0.28s cubic-bezier(0.2, 0.9, 0.3, 1.3), opacity 0.18s;
  z-index: 10;
  pointer-events: none;
  max-width: calc(100vw - 32px);
  text-align: center;
}

.toast.show { transform: translate(-50%, 0); opacity: 1; }
.toast.error { border-color: var(--danger-line); background: var(--danger-bg); }

@media (prefers-reduced-motion: reduce) {
  .cursor.visible, .cell.charging, .cell.pop { animation: none; }
  .flash { animation-name: flash-still; }
}

/* ---------- Admin ---------- */

.admin {
  max-width: 880px;
  margin: 0 auto;
  padding: 24px 16px 64px;
  display: grid;
  gap: var(--card-gap);
}

/* Die Karten liegen in #panel und nicht direkt in .admin - ohne eigenes Raster
   stossen sie stumpf aneinander, und der gap von .admin geht daran vorbei.
   Das :not([hidden]) ist kein Schmuck: `display: grid` wuerde das
   hidden-Attribut ueberstimmen, und der Admin staende vor der Anmeldung offen. */
#panel:not([hidden]) {
  display: grid;
  gap: var(--card-gap);
}

.settings {
  display: grid;
  gap: var(--card-gap);
}

/* Ab hier ist Platz fuer zwei Spalten: links die Einstellungen in ihrer
   gewohnten Breite, rechts die Live-Leinwand, die beim Scrollen stehen bleibt.
   Die Schwelle ist die Summe aus beidem - darunter stapelt sich beides, und
   die Leinwand steht oben, weil sie in der Reihenfolge des Dokuments zuerst
   kommt.

   Breiter wird die Seite nur, wenn das Panel wirklich offen ist: Vor der
   Anmeldung steht dort ein einzelnes Passwortfeld, und das braucht keine
   1320 px. */
@media (min-width: 1180px) {
  .admin:has(#panel:not([hidden])) { max-width: 1320px; }

  #panel:not([hidden]) {
    grid-template-columns: minmax(0, 1fr) 380px;
    /* Ohne start dehnt sich die Spalte auf volle Hoehe, und sticky haette
       keinen Weg, an dem es kleben koennte. */
    align-items: start;
  }

  .settings { grid-area: 1 / 1; }

  .live {
    grid-area: 1 / 2;
    position: sticky;
    top: 24px;
  }
}

.admin-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 4px;
}

.admin-head .wordmark { font-size: 17px; font-weight: 600; letter-spacing: -0.02em; color: var(--muted); }
.admin-head .wordmark b { color: var(--text); font-weight: 800; }

.chip {
  display: inline-block;
  margin-left: 4px;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: 2px;
}

.dim { color: var(--muted); font-size: 13px; line-height: 1.55; }

.linkish {
  background: none;
  border: none;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  padding: 4px;
  text-decoration: underline;
  text-underline-offset: 3px;
}

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

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
  display: grid;
  gap: 14px;
}

.card h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.row { display: flex; gap: 10px; align-items: center; }
.row.wrap { flex-wrap: wrap; }

.input {
  flex: 1;
  min-width: 0;
  padding: 11px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}

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

/* Auswahlfelder sollen nur so breit sein wie ihr Inhalt, nicht die Zeile fuellen. */
select.input { flex: 0 0 auto; cursor: pointer; }

.error-text { color: var(--danger); font-size: 13px; font-weight: 600; }

/* Formatauswahl: gleich breite Kacheln, die aktive ist gedrueckt und inaktiv. */
.options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 12px;
}

.option {
  display: grid;
  gap: 2px;
  justify-items: start;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  text-align: left;
}

.option:hover:not(:disabled) { border-color: var(--line-strong); }
.option b { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.option small { color: var(--muted); font-size: 12px; }

.option[aria-pressed="true"] {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-2));
  cursor: default;
  opacity: 1;
}

.option:disabled { cursor: default; }

/* Vorschau der Vorlage: harte Pixelkanten, damit man Einzelpixel beurteilen kann. */
.tpl-frame {
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #fff;
  border: 1px solid var(--line);
}

.tpl-frame canvas {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}

input[type="file"].input { padding: 9px 12px; cursor: pointer; }

/* Zahlenfelder nebeneinander, auf dem Handy untereinander. */
.fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  align-items: end;
}

.fields label { display: grid; gap: 5px; }
.fields label > span { font-size: 12px; color: var(--muted); font-weight: 600; }
.fields .input { width: 100%; }
.fields small { font-size: 11px; }
.fields button { align-self: end; height: 43px; }

/* Eine Zeile je Sorte Leute: links wer, rechts die zwei Stellschrauben. Das
   Ergebnis steht unter dem Namen und nicht in einer eigenen Spalte - es ist
   eine Folge der Eingabe, keine dritte Eingabe. */
.players { display: grid; gap: 12px; }

.player {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 104px 104px;
  gap: 10px 12px;
  align-items: end;
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}

.player .who { display: grid; gap: 3px; min-width: 0; }
.player .who b { font-size: 14px; font-weight: 700; }
.player .who small { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.player label { display: grid; gap: 5px; }
.player label > span { font-size: 11px; color: var(--muted); font-weight: 600; }
.player .input { width: 100%; }

@media (max-width: 560px) {
  .player { grid-template-columns: 1fr 1fr; }
  .player .who { grid-column: 1 / -1; }
}

.sep { border: none; border-top: 1px solid var(--line); margin: 0; width: 100%; }

/* --- Knoepfe --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 11px 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}

.btn:hover:not(:disabled) { border-color: var(--line-strong); }
.btn:active:not(:disabled) { transform: scale(0.98); }

/* Dunkle Schrift auf dem Akzent: Weiss kaeme auf dem zurueckgenommenen Orange
   nur auf 3,4:1, Dunkel auf 5,3:1. */
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--bg); }
.btn.primary:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }

.btn.danger { border-color: var(--danger-line); color: var(--danger); background: var(--danger-bg); }
.btn.danger:hover:not(:disabled) { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }

.btn:disabled { opacity: 0.4; cursor: not-allowed; }

.btn.icon { width: 42px; padding: 0; aspect-ratio: 1; }

/* Play/Pause im selben Knopf - gesteuert ueber data-playing, nicht ueber das
   hidden-Attribut, das auf einem SVG nicht greift. */
.btn .when-playing { display: none; }
.btn[data-playing="true"] .when-paused { display: none; }
.btn[data-playing="true"] .when-playing { display: block; }

.btn .lucide { width: 18px; height: 18px; }

/* --- Zustand: offen oder eingefroren --- */

.state {
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
}

.state h2 { font-size: 16px; }

.state-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--ok) 15%, transparent);
  transition: background 0.25s, box-shadow 0.25s;
}

.state-text { display: grid; gap: 3px; min-width: 0; }

/* Eingefroren faerbt die ganze Karte - das ist der Zustand, der auffallen muss. */
.state[data-frozen="true"] {
  border-color: color-mix(in srgb, var(--frozen) 40%, transparent);
  background: color-mix(in srgb, var(--frozen) 9%, var(--surface));
}
.state[data-frozen="true"] .state-dot {
  background: var(--frozen);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--frozen) 17%, transparent);
}

.toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  padding: 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  cursor: pointer;
}

.toggle .knob {
  width: 40px;
  height: 24px;
  border-radius: 999px;
  background: var(--line-strong);
  position: relative;
  transition: background 0.2s;
}

.toggle .knob::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform 0.2s, background 0.2s;
}

.toggle[aria-pressed="true"] { border-color: color-mix(in srgb, var(--frozen) 40%, transparent); }
.toggle[aria-pressed="true"] .knob { background: var(--frozen); }
.toggle[aria-pressed="true"] .knob::after { transform: translateX(16px); background: var(--bg); }

/* --- Status --- */

.grid-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }

/* Wert oben, Beschriftung darunter - so stehen die Zahlen untereinander und
   lassen sich in einem Blick vergleichen. */
.stat {
  display: grid;
  gap: 4px;
  padding: 14px 16px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}

.stat .value {
  font-size: 26px;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.stat .label { color: var(--muted); font-size: 12px; }

/* Die aktive Konfiguration als Paare statt als punktgetrennte Zeile. */
.config {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 8px 24px;
  margin: 0;
}

.config > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}

.config dt { color: var(--muted); font-size: 12px; }
.config dd { margin: 0; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* --- Timelapse --- */

/* Eigene Masse statt --board-w/--board-h: Die gehoeren der Live-Leinwand, und
   der Ausschnitt, den die Timelapse zeigt, ist oft ein anderer - das volle
   Speicherformat zum Beispiel. admin.js setzt die beiden Werte je Ausschnitt. */
.tl-frame {
  --tl-w: 100;
  --tl-h: 60;
  background: #fff;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--line);
  aspect-ratio: var(--tl-w) / var(--tl-h);
}

.tl-frame canvas { width: 100%; height: 100%; }

/* --- Live-Leinwand ---
   Dieselben Masse wie auf den anderen beiden Seiten: --board-w/--board-h
   stehen ueberall fuer das, was gerade auf der Leinwand steht, und board.js
   setzt sie aus dem Snapshot des Servers. */
.live-frame {
  /* Gedeckelt fuer den Fall, dass die Karte nicht neben der Spalte steht,
     sondern darueber: In voller Spaltenbreite waere die Leinwand hoeher als
     der halbe Bildschirm und schoebe alles, was man bedienen will, hinaus.
     In der schmalen Spalte daneben greift der Deckel nie. */
  max-width: 380px;
  /* Bezug fuer die Einschlag-Markierungen, die deckungsgleich darueber liegen. */
  position: relative;
  background: #fff;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--line);
  aspect-ratio: var(--board-w) / var(--board-h);
}

.live-frame canvas { width: 100%; height: 100%; }

/* Rechts in der Ueberschrift, damit die Verbindung dort steht, wo man sie
   sucht, wenn sich eine Minute lang nichts bewegt hat. */
.live-badge {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.live-badge i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--muted);
  transition: background 0.25s, box-shadow 0.25s;
}

.live-badge[data-live="true"] { color: var(--ok); }

.live-badge[data-live="true"] i {
  background: var(--ok);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 15%, transparent);
}

.tl-time { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.tl-time strong { font-size: 16px; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }

.scrub { width: 100%; height: 22px; accent-color: var(--accent); cursor: pointer; }
.scrub:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--radius-xs); }

.transport { display: flex; align-items: center; gap: 10px; }
.transport .speed { margin-left: auto; }

.progress { height: 6px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.progress .bar { height: 100%; width: 0; background: var(--accent); transition: width 0.2s; }

/* --- Testlauf --- */

/* Waehrend er laeuft, faerbt sich die Karte im Akzent. Derselbe Gedanke wie
   beim eingefrorenen Board: Ein Zustand, den man versehentlich anlaesst, muss
   sich beim Scrollen von selbst melden. */
.test[data-running="true"] {
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
}
.test[data-running="true"] #test-out { color: var(--text); font-variant-numeric: tabular-nums; }

/* Ein Feld und ein Knopf: Ohne das hier zoege das Raster den Knopf auf die
   halbe Kartenbreite auf. */
.test .fields button { justify-self: start; min-width: 150px; }

/* --- Gefahrenzone --- */

.danger-zone { border-color: var(--danger-line); }
.danger-zone h2 { color: var(--danger); }
.danger-zone .input { flex: 1 1 220px; }

@media (max-width: 560px) {
  .state { grid-template-columns: auto 1fr; }
  .state .toggle { grid-column: 2; justify-self: start; }
  .transport .speed { margin-left: 0; }
}

