/* ===========================================================================
   einhorn.live — Bausteine
   Setzt tokens.css voraus. Enthaelt nur wiederverwendebare Bausteine,
   keine seiten- oder app-spezifischen Regeln.

   Stand 17.10.2026 — Ueberarbeitung des Fundaments. Klassennamen und
   Struktur sind unveraendert, damit keine der 34 Apps angefasst werden
   muss. Geaendert hat sich, wie die Bausteine aussehen und wie sie sich
   bewegen.
   =========================================================================== */

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

/* Das hidden-Attribut muss jede eigene display-Regel schlagen.
   Ohne diese Zeile bleibt ein Element mit display:flex oder inline-flex
   sichtbar, obwohl hidden gesetzt ist — die Regel aus dem Browser-
   Stylesheet ist schwaecher als jede Klassenregel hier. */
[hidden] { display: none !important; }

html {
    -webkit-text-size-adjust: 100%;
    /* KEIN scroll-behavior: smooth.
       Stand bis zum 17.10.2026 hier und muss weg: Lenis uebernimmt auf den
       Inhaltsseiten das Scrollen des Dokuments und erwartet dafuer
       `auto`. Mit beidem gleichzeitig kaempfen zwei Glaetter gegeneinander
       und das Ergebnis ruckelt.
       Verloren geht dabei nichts: die Apps rufen scrollTo() ohnehin mit
       `behavior: 'smooth'` im Aufruf auf, nicht ueber diese Regel. */
}

body {
    margin: 0;
    color: var(--text);
    font-family: var(--font-sans);
    font-size: var(--fs-base);
    line-height: var(--lh-base);
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;

    /* Nicht mehr eine flache Flaeche, sondern ein schwacher Farbraum.
       Drei weite Verlaeufe in den Markenfarben, so blass, dass man sie
       einzeln nicht benennen kann — zusammen nehmen sie dem Weiss die
       Leere. Reines CSS, also kein Byte Ladezeit und kein Canvas, das auf
       Schulrechnern ohne Grafikbeschleunigung Arbeit macht. */
    background-color: var(--bg);
    background-image:
        radial-gradient(58rem 38rem at 10% -6%,
            color-mix(in srgb, var(--brand-300) 22%, transparent), transparent 62%),
        radial-gradient(46rem 32rem at 94% 2%,
            color-mix(in srgb, var(--mint-300) 20%, transparent), transparent 60%);
    background-repeat: no-repeat;
}

/* Verlaeufe nur auf grossen Zeigergeraeten mitscrollen lassen.
   background-attachment: fixed ruckelt auf iOS und auf schwachen
   Android-Tablets sichtbar — dort bleiben die Verlaeufe am Seitenanfang
   stehen, und das sieht genauso gut aus. */
@media (min-width: 768px) and (pointer: fine) {
    body { background-attachment: fixed; }
}

[data-theme="dark"] body {
    background-image:
        radial-gradient(58rem 38rem at 10% -6%,
            color-mix(in srgb, var(--brand-500) 26%, transparent), transparent 62%),
        radial-gradient(46rem 32rem at 94% 2%,
            color-mix(in srgb, var(--mint-700) 24%, transparent), transparent 60%);
}

/* ---- Ueberschriften ----------------------------------------------------
   Ab jetzt in der Display-Schrift. Fredoka ist runder und kraeftiger als
   Nunito; dadurch trennen sich Ueberschrift und Text auch dann, wenn beide
   gleich gross sind. Die Laufweite muss dabei leicht zusammen, sonst
   fallen die runden Formen auseinander. */
h1, h2, h3, h4 {
    margin: 0 0 var(--sp-3);
    font-family: var(--font-display);
    line-height: var(--lh-tight);
    font-weight: var(--fw-medium);
    letter-spacing: -0.015em;
}
h1 { font-size: var(--fs-3xl); line-height: var(--lh-display); }
h2 { font-size: var(--fs-2xl); }
h3 { font-size: var(--fs-xl); }
h4 { font-size: var(--fs-lg); }

p { margin: 0 0 var(--sp-4); }

a {
    color: var(--primary);
    text-decoration-thickness: 2px;
    text-underline-offset: 2px;
}
a:hover { color: var(--primary-hover); }

/* Tastaturfokus immer sichtbar, Mausklick nicht umranden. */
:focus-visible {
    outline: 3px solid var(--primary);
    outline-offset: 2px;
    border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

img, svg { max-width: 100%; height: auto; }

/* ---- Layout ------------------------------------------------------------- */
.eh-container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--sp-4);
}

.eh-stack > * + * { margin-top: var(--sp-4); }
.eh-stack-sm > * + * { margin-top: var(--sp-2); }
.eh-stack-lg > * + * { margin-top: var(--sp-6); }

.eh-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
}
.eh-row-between {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    flex-wrap: wrap;
}
.eh-spacer { flex: 1 1 auto; }

.eh-grid {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* ---- Nur fuer Screenreader --------------------------------------------- */
.eh-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ---- Karten -------------------------------------------------------------
   Statt weisser Flaeche mit grauem Rand jetzt ein Gegenstand: schwacher
   Verlauf von oben nach unten, heller Saum an der Oberkante, farbiger
   Schatten darunter. Alle drei Zutaten stehen in tokens.css. */
.eh-card {
    background: var(--surface-raised);
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    padding: var(--sp-5);
    box-shadow: var(--sh-sm), var(--ring-top);
}
.eh-card-flush { padding: 0; overflow: hidden; }

.eh-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding: var(--sp-4) var(--sp-5);
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.eh-card-head h2,
.eh-card-head h3 { margin: 0; font-size: var(--fs-lg); }

.eh-card-body { padding: var(--sp-5); }

/* ---- Buttons ------------------------------------------------------------
   Der Hauptknopf bekommt einen Verlauf und unter dem Zeiger einen Schein
   in der Leitfarbe. Beim Druecken geht er sichtbar hinein — auf einem
   Tablet ohne Hover-Zustand ist das die einzige Rueckmeldung, die ein Kind
   bekommt, bevor die Seite reagiert. */
.eh-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    min-height: var(--tap-min);
    padding: 0 var(--sp-5);
    border: 1px solid transparent;
    border-radius: var(--r-full);
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--primary) 88%, white) 0%,
        var(--primary) 100%);
    color: var(--text-on-brand);
    font: inherit;
    font-family: var(--font-display);
    font-weight: var(--fw-medium);
    line-height: 1;
    cursor: pointer;
    text-decoration: none;
    box-shadow: var(--sh-sm);
    transition: background var(--dur-fast) var(--ease-soft),
                transform var(--dur-fast) var(--ease-soft),
                box-shadow var(--dur-fast) var(--ease-soft);
    -webkit-tap-highlight-color: transparent;
}
.eh-btn:hover:not(:disabled) {
    background: linear-gradient(180deg,
        var(--primary) 0%,
        var(--primary-hover) 100%);
    color: var(--text-on-brand);
    transform: translateY(-1px);
    box-shadow: var(--sh-glow);
}
.eh-btn:active:not(:disabled) {
    transform: translateY(1px) scale(0.985);
    box-shadow: var(--sh-sm);
}
.eh-btn:disabled,
.eh-btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
    box-shadow: none;
}

.eh-btn-ghost {
    background: transparent;
    color: var(--text);
    border-color: var(--border-strong);
    box-shadow: none;
}
.eh-btn-ghost:hover:not(:disabled) {
    background: var(--surface-hover);
    color: var(--text);
    box-shadow: var(--sh-sm);
}

.eh-btn-soft {
    background: var(--primary-soft);
    color: var(--primary);
    border-color: var(--primary-border);
    box-shadow: none;
}
.eh-btn-soft:hover:not(:disabled) {
    background: var(--primary-border);
    color: var(--primary);
    box-shadow: var(--sh-sm);
}

.eh-btn-danger {
    background: linear-gradient(180deg,
        color-mix(in srgb, var(--danger) 88%, white) 0%, var(--danger) 100%);
}
.eh-btn-danger:hover:not(:disabled) {
    background: linear-gradient(180deg, var(--danger) 0%, var(--danger-text) 100%);
    box-shadow: 0 6px 20px color-mix(in srgb, var(--danger) 30%, transparent);
}

.eh-btn-lg {
    min-height: var(--tap-lg);
    padding: 0 var(--sp-6);
    font-size: var(--fs-lg);
}
.eh-btn-sm {
    min-height: 34px;
    padding: 0 var(--sp-3);
    font-size: var(--fs-sm);
}
.eh-btn-block { width: 100%; }

.eh-btn-icon {
    min-width: var(--tap-min);
    padding: 0;
    border-radius: var(--r-full);
}

/* ---- Formulare ---------------------------------------------------------- */
.eh-field { display: block; margin-bottom: var(--sp-4); }

.eh-label {
    display: block;
    margin-bottom: var(--sp-2);
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);
    color: var(--text-muted);
}

.eh-input,
.eh-select,
.eh-textarea {
    width: 100%;
    min-height: var(--tap-min);
    padding: var(--sp-2) var(--sp-3);
    background: var(--surface);
    color: var(--text);
    border: 2px solid var(--border);
    border-radius: var(--r-md);
    font: inherit;
    transition: border-color var(--dur-fast) var(--ease-soft),
                box-shadow var(--dur-fast) var(--ease-soft);
}
.eh-input:focus,
.eh-select:focus,
.eh-textarea:focus {
    outline: none;
    border-color: var(--primary);
    box-shadow: var(--sh-focus);
}
.eh-input[aria-invalid="true"] { border-color: var(--danger); }
.eh-textarea { min-height: 6rem; resize: vertical; }

.eh-hint {
    margin-top: var(--sp-1);
    font-size: var(--fs-sm);
    color: var(--text-subtle);
}
.eh-error-text {
    margin-top: var(--sp-1);
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);
    color: var(--danger-text);
}

/* Code-Eingabe: gross, mittig, gesperrte Laufweite */
.eh-input-code {
    font-family: var(--font-mono);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    letter-spacing: 0.18em;
    text-align: center;
    text-transform: uppercase;
    min-height: var(--tap-lg);
}

/* ---- Badges ------------------------------------------------------------- */
.eh-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 0.2em 0.7em;
    border-radius: var(--r-full);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.eh-badge-primary { background: var(--primary-soft); color: var(--primary); }
/* Zeigt in der Kopfzeile an, dass die Grundschul-Fassung einer App offen
   ist — sonst sucht man vergeblich nach den fehlenden Modi. */
.eh-badge-stufe   { background: var(--surface-2); color: var(--text-muted); flex: none; }
.eh-badge-success { background: var(--success-soft); color: var(--success-text); }
.eh-badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.eh-badge-danger  { background: var(--danger-soft);  color: var(--danger-text); }
.eh-badge-info    { background: var(--info-soft);    color: var(--info-text); }

.eh-badge-mathe   { background: var(--info-soft);    color: var(--info-text); }
.eh-badge-deutsch { background: var(--success-soft); color: var(--success-text); }
.eh-badge-spiel   { background: var(--primary-soft); color: var(--primary); }

/* ---- Klassencode zum Vorlesen ------------------------------------------ */
.eh-code {
    display: inline-block;
    padding: var(--sp-2) var(--sp-4);
    background: var(--primary-soft);
    border: 2px dashed var(--primary-border);
    border-radius: var(--r-md);
    color: var(--primary);
    font-family: var(--font-mono);
    font-size: var(--fs-lg);
    font-weight: var(--fw-bold);
    letter-spacing: 0.12em;
    user-select: all;
}

/* ---- Hinweisboxen ------------------------------------------------------- */
.eh-note {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-md);
    border-left: 4px solid var(--info);
    background: var(--info-soft);
    color: var(--info-text);
    font-size: var(--fs-sm);
}
/* Ein Link in einer Hinweisbox nimmt deren eigene Textfarbe.
   Vorher stand er in der Markenfarbe — die ist auf der Seitenflaeche
   kraeftig, auf dem eingefaerbten Grund der Box aber nur noch 4,19:1
   gemessen. Die Textfarbe der Box ist genau fuer diesen Grund gemacht.
   Unterstrichen, damit der Link weiter als Link zu erkennen ist. */
.eh-note a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.eh-note a:hover { text-decoration-thickness: 2px; }
.eh-note-success { border-left-color: var(--success); background: var(--success-soft); color: var(--success-text); }
.eh-note-warning { border-left-color: var(--warning); background: var(--warning-soft); color: var(--warning-text); }
.eh-note-danger  { border-left-color: var(--danger);  background: var(--danger-soft);  color: var(--danger-text); }

/* ---- Tabellen -----------------------------------------------------------
   Zahlen in der Mono-Schrift mit gleich breiten Ziffern. In einer
   Bestenliste springen die Spalten damit nicht mehr. */
.eh-table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    -webkit-overflow-scrolling: touch;
}
.eh-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
}
.eh-table th,
.eh-table td {
    padding: var(--sp-3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}
.eh-table th {
    background: var(--surface-2);
    font-family: var(--font-display);
    font-weight: var(--fw-medium);
    color: var(--text-muted);
    white-space: nowrap;
    position: sticky;
    top: 0;
    z-index: 1;
}
.eh-table tbody tr:last-child td { border-bottom: none; }
.eh-table tbody tr:hover { background: var(--surface-hover); }
.eh-table-num {
    text-align: right;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
}

/* ---- Fortschrittsbalken ------------------------------------------------- */
.eh-progress {
    height: 12px;
    background: var(--surface-2);
    border-radius: var(--r-full);
    overflow: hidden;
    box-shadow: inset 0 1px 2px rgba(42, 22, 89, 0.08);
}
.eh-progress-bar {
    height: 100%;
    width: 0;
    background: linear-gradient(90deg, var(--brand-400), var(--mint-500));
    border-radius: var(--r-full);
    transition: width var(--dur-slow) var(--ease-out);
}

/* ---- Reiter ------------------------------------------------------------- */
.eh-tabs {
    display: flex;
    gap: var(--sp-1);
    overflow-x: auto;
    border-bottom: 2px solid var(--border);
    scrollbar-width: none;
    /* Seitlich scrollende Leiste: Lenis darf das nicht schlucken.
       Das Attribut setzt motion.js, die Regel hier ist nur die Erinnerung. */
    overscroll-behavior-x: contain;
}
.eh-tabs::-webkit-scrollbar { display: none; }

.eh-tab {
    flex: 0 0 auto;
    min-height: var(--tap-min);
    padding: 0 var(--sp-4);
    border: none;
    border-bottom: 3px solid transparent;
    margin-bottom: -2px;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-family: var(--font-display);
    font-weight: var(--fw-medium);
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--dur-fast) var(--ease-soft),
                border-color var(--dur-fast) var(--ease-soft);
}
.eh-tab:hover { color: var(--text); }
.eh-tab[aria-selected="true"] {
    color: var(--primary);
    border-bottom-color: var(--primary);
}

/* ---- Kopfzeile ----------------------------------------------------------
   Bleibt oben kleben. Neu: der Schatten kommt erst, wenn wirklich
   gescrollt wurde — eine Kopfzeile, die am Seitenanfang schon schwebt,
   schwebt ueber nichts. Die Klasse setzt einhorn.js. */
.eh-header {
    position: sticky;
    top: 0;
    z-index: 20;
    background: color-mix(in srgb, var(--surface) 82%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border-bottom: 1px solid transparent;
    transition: border-color var(--dur-base) var(--ease-soft),
                box-shadow var(--dur-base) var(--ease-soft),
                background var(--dur-base) var(--ease-soft);
}
.eh-header.eh-header-gescrollt {
    border-bottom-color: var(--border);
    box-shadow: var(--sh-sm);
    background: color-mix(in srgb, var(--surface) 92%, transparent);
}
/* Ohne backdrop-filter (aeltere Schul-PCs, Firefox mit abgeschaltetem
   Filter) braucht die Leiste eine deckende Flaeche, sonst liest man den
   Inhalt durch sie hindurch. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .eh-header { background: var(--surface); border-bottom-color: var(--border); }
}
.eh-header-inner {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    min-height: 60px;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--sp-4);
}
/* Flex-Kinder schrumpfen von sich aus nicht unter ihre Inhaltsbreite
   (min-width: auto). Auf einem schmalen Handy schoben Zurueck-Knopf,
   App-Titel und ein langer Anzeigename die Kopfzeile deshalb ueber den
   Bildschirmrand hinaus und die ganze Seite bekam einen Querbalken. */
.eh-header-inner > * { min-width: 0; }
.eh-header-inner > .eh-btn { flex: none; }
/* Das Benutzerfach darf NICHT schrumpfen. Es durfte es, sein Inhalt aber
   nicht: der Anmelden-Knopf behielt seine Breite und stand auf einem
   390-px-Handy halb neben dem Bildschirm — anklicken ging nur nach
   seitlichem Scrollen, und genau der Knopf ist fuer ein Kind ohne Konto
   der wichtigste. Schrumpfen soll der App-Titel, der kann das: er hat
   schon overflow:hidden und text-overflow:ellipsis. */
.eh-header-inner > [data-eh="userslot"] { display: flex; flex: none; }
.eh-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-family: var(--font-display);
    font-weight: var(--fw-medium);
    font-size: var(--fs-lg);
    letter-spacing: -0.01em;
    color: var(--text);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.eh-brand > span { flex: none; transition: transform var(--dur-base) var(--ease-back); }
.eh-brand:hover { color: var(--primary); }
/* Das Einhorn dreht sich beim Darueberfahren einmal kurz an. Die einzige
   Spielerei, die ich mir in der Kopfzeile erlaube — sie ist da, wo ein
   Kind sie sucht, und stoert niemanden, der sie nicht findet. */
.eh-brand:hover > span { transform: rotate(-12deg) scale(1.12); }

/* ---- Werkzeuggruppe in der Kopfzeile ------------------------------------
   Vollbild, Punkte und Hell/Dunkel. Drei Schalter, die zusammengehoeren —
   also sehen sie auch wie ein Stueck aus: eine gemeinsame Pille mit
   duennen Trennstrichen, wie die Segmentschalter, die Kinder vom Handy
   kennen.

   Der Grund, warum es diese Gruppe ueberhaupt gibt, ist aber kein
   aesthetischer. Einzeln standen die drei Knoepfe direkt in
   .eh-header-inner, und dort setzt "> * { min-width: 0 }" jedem Kind die
   Mindestbreite auf null — gleiche Spezifitaet wie .eh-btn-icon, aber
   weiter unten in der Datei, also gewinnt es. Auf einem 390-px-Handy
   waren die Flaechen dadurch gemessene 16, 17 und 22 Pixel breit. Das ist
   kein Schoenheitsfehler, das ist ein Knopf, den ein Drittklaesslerdaumen
   nicht trifft.

   Die Gruppe schrumpft nicht (flex: none), und weil die Knoepfe jetzt in
   ihr stecken statt in der Kopfzeile, greift deren Mindestbreite wieder. */
.eh-werkzeuge {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 2px;
    padding: 3px;
    background: color-mix(in srgb, var(--surface-2) 75%, transparent);
    border: 1px solid var(--border);
    border-radius: var(--r-full);
}

.eh-werkzeug {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 40px Flaeche in einer 46px hohen Pille. Zusammen mit dem Abstand zu
       den Nachbarn liegt die anklickbare Flaeche ueber den 44px, die eine
       Fingerspitze braucht. */
    min-width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: var(--r-full);
    background: transparent;
    color: var(--text);
    /* Emoji haben in der Display-Schrift keine eigene Form, sie kommen vom
       Geraet. Feste Groesse statt geerbter Schriftgroesse, damit die drei
       auf jedem Geraet gleich gross stehen. */
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-soft),
                transform var(--dur-fast) var(--ease-back);
}
/* Duenne Trennstriche zwischen den Segmenten, nicht um jedes herum. */
.eh-werkzeug + .eh-werkzeug { position: relative; }
.eh-werkzeug + .eh-werkzeug::before {
    content: "";
    position: absolute;
    left: -3px;
    top: 20%;
    bottom: 20%;
    width: 1px;
    background: var(--border);
}
.eh-werkzeug:hover:not(:disabled) {
    background: var(--surface);
    box-shadow: var(--sh-sm);
}
.eh-werkzeug:active:not(:disabled) { transform: scale(0.92); }
.eh-werkzeug:focus-visible {
    outline: none;
    box-shadow: var(--sh-focus);
}
/* Ein gedrueckter Zustand, den man auch sieht: der Punkte-Schalter ist
   ein Schalter, kein Knopf. Ohne das unterscheidet nur das Emoji ⭐/☆
   zwischen an und aus, und das ist auf einem Handy kaum zu erkennen. */
.eh-werkzeug[aria-pressed="true"] {
    background: var(--surface);
    box-shadow: var(--sh-sm);
}
.eh-werkzeug:disabled { opacity: 0.4; cursor: not-allowed; }

/* Auf dem Handy wird der Platz knapp: Marke, drei Schalter und der
   Anmelden-Knopf. Die Marke darf kuerzen (sie hat ellipsis), die
   Schalter nicht — sie haben keinen Text, den man kuerzen koennte. */
@media (max-width: 480px) {
    .eh-header-inner { gap: var(--sp-2); padding-inline: var(--sp-3); }
    .eh-werkzeuge { gap: 2px; padding: 2px; }
    /* 42px statt 40: der Platz dafuer kommt aus der Marke, nicht aus den
       Schaltern. Die Marke darf kuerzen, ein Knopf ohne Text kann das
       nicht — und von den vieren hier ist der Knopf das, was getroffen
       werden muss. */
    .eh-werkzeug { min-width: 42px; height: 42px; font-size: 1.1rem; }
    .eh-werkzeug + .eh-werkzeug::before { left: -2px; }
    /* Eine Stufe kleiner, damit "einhorn.live" neben den Schaltern und
       dem Anmelden-Knopf noch ganz dasteht statt abgeschnitten. */
    .eh-brand { font-size: var(--fs-base); }

    /* Der Zurueck-Knopf gibt sein Wort her und wird zum Pfeil. Auf einer
       App-Seite ist der Platz zu dritt — Zurueck, App-Name, drei Schalter —
       sonst nicht zu verteilen, und von den beiden Beschriftungen ist der
       App-Name die, die man wirklich lesen will: dass der Pfeil zurueck
       fuehrt, weiss jedes Kind. Das aria-label bleibt, fuer Vorlesesoftware
       aendert sich also nichts. */
    .eh-back-wort { display: none; }
    .eh-header-inner > [data-eh="back"] {
        min-width: 42px;
        padding-inline: 0;
        justify-content: center;
        font-size: 1.1rem;
    }

    /* Dasselbe fuer den Anmelden-Knopf. Er bleibt farbig und damit vom
       Rest unterscheidbar -- er ist kein Schalter, sondern der Weg zum
       eigenen Konto. */
    .eh-anmelden-wort { display: none; }
    .eh-anmelden {
        min-width: 42px;
        padding-inline: 0;
        justify-content: center;
        font-size: 1.1rem;
    }

    /* Ein paar Pixel aus den Abstaenden, damit der App-Name nicht als
       Erster kuerzen muss. */
    .eh-header-inner { gap: 6px; }
}

/* Auf den schmalsten Geraeten (iPhone SE, viele Android-Einsteiger) fehlten
   der Wortmarke gemessene fuenf Pixel und sie wurde zu "einhorn.li…".
   Die hole ich aus den Schaltern: 40 statt 42 Pixel sind mit dem Abstand
   dazwischen immer noch 42 Pixel Treffer-Flaeche, und ein abgeschnittener
   Name sieht nach Fehler aus. */
@media (max-width: 400px) {
    .eh-werkzeug { min-width: 40px; height: 40px; }
}
@media (prefers-reduced-motion: reduce) {
    .eh-werkzeug { transition: none; }
    .eh-werkzeug:active:not(:disabled) { transform: none; }
}

/* ---- Benutzermenue ------------------------------------------------------ */
.eh-user {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: var(--tap-min);
    padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-full);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    cursor: pointer;
    /* Lange Anzeigenamen duerfen die Kopfzeile nicht ueber den Bildschirm
       hinausschieben. Ohne das schob "Ida Anna Kohler" auf einem schmalen
       Handy die ganze Seite nach rechts und erzeugte einen Querbalken. */
    min-width: 0;
    max-width: 45vw;
    overflow: hidden;
    transition: background var(--dur-fast) var(--ease-soft),
                box-shadow var(--dur-fast) var(--ease-soft);
}
.eh-user > .eh-nowrap {
    overflow: hidden;
    text-overflow: ellipsis;
}
.eh-user > .eh-avatar { flex: none; }
.eh-user:hover { background: var(--surface-hover); box-shadow: var(--sh-sm); }

.eh-avatar {
    display: grid;
    place-items: center;
    width: 32px; height: 32px;
    border-radius: var(--r-full);
    background: linear-gradient(140deg, var(--brand-400), var(--brand-600));
    color: #fff;
    font-family: var(--font-display);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    flex: 0 0 auto;
}
[data-theme="dark"] .eh-avatar {
    background: linear-gradient(140deg, var(--brand-300), var(--brand-500));
    color: var(--grey-900);
}

.eh-menu {
    position: absolute;
    right: var(--sp-4);
    top: 64px;
    min-width: 230px;
    padding: var(--sp-2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--sh-lg), var(--ring-top);
    z-index: 30;
    animation: eh-menu-auf var(--dur-base) var(--ease-back) backwards;
    transform-origin: top right;
}
.eh-menu[hidden] { display: none; }
@keyframes eh-menu-auf {
    from { opacity: 0; transform: translateY(-6px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}
.eh-menu-head {
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--sp-2);
}
.eh-menu-item {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    width: 100%;
    min-height: var(--tap-min);
    padding: 0 var(--sp-3);
    border: none;
    border-radius: var(--r-sm);
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease-soft);
}
.eh-menu-item:hover { background: var(--surface-hover); color: var(--text); }
.eh-menu-item-danger { color: var(--danger-text); }

/* ---- Dialoge ------------------------------------------------------------ */
.eh-modal-backdrop {
    position: fixed;
    inset: 0;
    display: grid;
    place-items: center;
    padding: var(--sp-4);
    /* Im Markenton abgedunkelt statt neutralschwarz: der Dialog sieht
       dadurch aus wie Teil derselben Seite. */
    background: color-mix(in srgb, var(--brand-900) 58%, transparent);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    z-index: 100;
    animation: eh-fade-auf var(--dur-base) var(--ease-soft) backwards;
}
.eh-modal-backdrop[hidden] { display: none; }
@keyframes eh-fade-auf { from { opacity: 0; } to { opacity: 1; } }

.eh-modal {
    width: 100%;
    max-width: 480px;
    max-height: 90vh;
    overflow-y: auto;
    background: var(--surface-raised);
    border-radius: var(--r-lg);
    box-shadow: var(--sh-lg), var(--ring-top);
    padding: var(--sp-5);
    animation: eh-modal-auf var(--dur-base) var(--ease-back) backwards;
    /* Eigener Scrollbereich: Lenis darf ihn nicht uebernehmen, sonst
       scrollt bei einem langen Dialog die Seite dahinter. */
    overscroll-behavior: contain;
}
@keyframes eh-modal-auf {
    from { opacity: 0; transform: translateY(14px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}
.eh-modal-wide { max-width: 760px; }
.eh-modal-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-bottom: var(--sp-4);
}
.eh-modal-head h2 { margin: 0; font-size: var(--fs-xl); }
.eh-modal-foot {
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-5);
}

/* ---- Kurzmeldungen ----------------------------------------------------- */
.eh-toasts {
    position: fixed;
    bottom: var(--sp-4);
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    z-index: 200;
    pointer-events: none;
    width: min(440px, calc(100vw - 2 * var(--sp-4)));
}
.eh-toast {
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-md);
    background: var(--grey-800);
    color: var(--grey-50);
    box-shadow: var(--sh-lg);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    animation: eh-toast-in var(--dur-base) var(--ease-back);
}
.eh-toast-success { background: var(--mint-700); }
.eh-toast-error   { background: var(--rose-700); }

@keyframes eh-toast-in {
    from { opacity: 0; transform: translateY(14px) scale(0.96); }
    to   { opacity: 1; transform: none; }
}

/* ---- Leere Zustaende --------------------------------------------------- */
.eh-empty {
    padding: var(--sp-7) var(--sp-4);
    text-align: center;
    color: var(--text-muted);
}
.eh-empty-icon { font-size: var(--fs-3xl); margin-bottom: var(--sp-3); }

/* ---- Ladezustand ------------------------------------------------------- */
.eh-spinner {
    width: 24px; height: 24px;
    border: 3px solid var(--border);
    border-top-color: var(--primary);
    border-radius: var(--r-full);
    animation: eh-spin 700ms linear infinite;
}
@keyframes eh-spin { to { transform: rotate(360deg); } }

.eh-skeleton {
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
    background-size: 200% 100%;
    border-radius: var(--r-lg);
    animation: eh-shimmer 1.3s infinite;
}
@keyframes eh-shimmer {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

/* ---- Hilfsklassen ------------------------------------------------------ */
.eh-muted  { color: var(--text-muted); }
.eh-subtle { color: var(--text-subtle); }
.eh-center { text-align: center; }
.eh-right  { text-align: right; }
.eh-nowrap { white-space: nowrap; }
.eh-mono   { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.eh-bold   { font-weight: var(--fw-bold); }
.eh-fs-sm  { font-size: var(--fs-sm); }
.eh-fs-lg  { font-size: var(--fs-lg); }
/* Display-Schrift auf Wunsch, fuer Zahlen und kurze Hervorhebungen in
   Apps, die keine Ueberschrift benutzen koennen. */
.eh-display { font-family: var(--font-display); font-weight: var(--fw-medium); }

@media print {
    .eh-no-print { display: none !important; }
}

/* =========================================================================
   Bewegung

   Fuer die Uebungs-Apps gibt es hier nur CSS — kein GSAP, kein Lenis.
   Begruendung steht in motion.js: eine App mit vier Antwortknoepfen
   braucht keine 140 KB Scroll-Bibliothek, und auf einem Schul-Tablet ist
   der Download nicht umsonst. Die Inhaltsseiten laden die Bibliotheken
   zusaetzlich; diese Regeln hier gelten ueberall.

   Benutzung in einer App, ohne eine Zeile JavaScript:

     <div class="eh-auftritt">...</div>          einzeln
     <div class="eh-auftritt-reihe">             Nachbarn nacheinander
       <button>...</button> <button>...</button>
     </div>

   Der Versatz kommt aus :nth-child, nicht aus einem Zaehler im Code.
   Deckt zwoelf Nachbarn ab; was darueber liegt, startet gemeinsam mit dem
   zwoelften — bei mehr als zwoelf Kacheln auf einmal faellt niemandem
   mehr auf, ob die dreizehnte versetzt kommt.
   ========================================================================= */

@keyframes eh-auftritt {
    from { opacity: 0; transform: translateY(14px) scale(0.98); }
    to   { opacity: 1; transform: none; }
}

.eh-auftritt {
    animation: eh-auftritt var(--dur-slow) var(--ease-back) backwards;
}

.eh-auftritt-reihe > * {
    animation: eh-auftritt var(--dur-slow) var(--ease-back) backwards;
}
.eh-auftritt-reihe > *:nth-child(1)  { animation-delay: 0ms; }
.eh-auftritt-reihe > *:nth-child(2)  { animation-delay: 45ms; }
.eh-auftritt-reihe > *:nth-child(3)  { animation-delay: 90ms; }
.eh-auftritt-reihe > *:nth-child(4)  { animation-delay: 135ms; }
.eh-auftritt-reihe > *:nth-child(5)  { animation-delay: 180ms; }
.eh-auftritt-reihe > *:nth-child(6)  { animation-delay: 225ms; }
.eh-auftritt-reihe > *:nth-child(7)  { animation-delay: 270ms; }
.eh-auftritt-reihe > *:nth-child(8)  { animation-delay: 315ms; }
.eh-auftritt-reihe > *:nth-child(9)  { animation-delay: 360ms; }
.eh-auftritt-reihe > *:nth-child(10) { animation-delay: 405ms; }
.eh-auftritt-reihe > *:nth-child(11) { animation-delay: 450ms; }
.eh-auftritt-reihe > *:nth-child(n+12) { animation-delay: 495ms; }

/* Rueckmeldung auf eine Antwort. Kurz und einmalig — eine Animation, die
   nach einer richtigen Antwort laenger laeuft als das Nachdenken ueber die
   naechste, haelt das Kind auf. */
@keyframes eh-richtig-puls {
    0%   { transform: none; }
    35%  { transform: scale(1.06); }
    100% { transform: none; }
}
@keyframes eh-falsch-wackeln {
    0%, 100% { transform: none; }
    20% { transform: translateX(-5px); }
    40% { transform: translateX(5px); }
    60% { transform: translateX(-3px); }
    80% { transform: translateX(3px); }
}
.eh-richtig { animation: eh-richtig-puls 420ms var(--ease-back); }
.eh-falsch  { animation: eh-falsch-wackeln 380ms var(--ease-soft); }

/* Eine Flaeche, die auf Beruehrung reagiert. Fuer Kacheln und
   Antwortknoepfe in den Apps. */
.eh-hebt {
    transition: transform var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out);
}
.eh-hebt:hover, .eh-hebt:focus-visible {
    transform: translateY(-3px);
    box-shadow: var(--sh-md);
}
.eh-hebt:active { transform: translateY(0) scale(0.99); }

/* Elemente, die auf ihren Auftritt warten. Die Klasse am <html> setzt
   motion.js — ohne JavaScript bleibt also alles sichtbar, und wenn GSAP
   nicht laedt, ist die Seite trotzdem benutzbar. Genau deshalb steht hier
   eine Klasse und nicht einfach opacity: 0. */
html.eh-motion [data-reveal] { opacity: 0; }

/* Zeile fuer die zeilenweise aufgedeckte Ueberschrift (SplitText).
   overflow: hidden ist der ganze Trick — die Zeile schiebt sich aus ihrem
   eigenen Rahmen heraus nach oben. */
.eh-zeile { overflow: hidden; display: block; }

@media (prefers-reduced-motion: reduce) {
    .eh-auftritt, .eh-auftritt-reihe > *,
    .eh-menu, .eh-modal, .eh-modal-backdrop, .eh-toast {
        animation: none !important;
    }
    .eh-richtig, .eh-falsch { animation: none !important; }
    .eh-hebt:hover, .eh-hebt:focus-visible, .eh-btn:hover:not(:disabled) {
        transform: none;
    }
    html.eh-motion [data-reveal] { opacity: 1; }
}

/* ---- Rechtsleiste ------------------------------------------------------
   Haengt auf jeder Seite am Ende, eingefuegt von mountHeader(). Impressum
   und Datenschutz muessen von ueberall erreichbar sein. */
.eh-recht {
    display: flex; gap: var(--sp-3); justify-content: center; align-items: center;
    flex-wrap: wrap;
    padding: var(--sp-5) var(--sp-4) var(--sp-6);
    font-size: var(--fs-sm); color: var(--text-muted);
}
.eh-recht a { color: var(--text-muted); text-decoration: underline; }
.eh-recht a:hover, .eh-recht a:focus-visible { color: var(--primary); }

/* ---- Persoenliche Einstellungen ---------------------------------------
   Die beiden Klassen setzt assets/core/einhorn.js am <html>, sobald der
   angemeldete Nutzer bekannt ist. So muss keine App eigenen Code dafuer
   haben - sie markiert ihre Anzeigen nur.

   eh-ohne-stats   Das Kind hat Punkte und Serie im Konto abgeschaltet.
                   Betrifft [data-eh-stat] waehrend des Spiels und im
                   Ergebnis. Richtig-Zahl und Prozent bleiben stehen,
                   das ist Rueckmeldung und kein Wettbewerb.

   eh-ohne-board   Die Lehrkraft hat die Bestenlisten der Klasse aus.
                   Dann verschwinden auch die Knoepfe dorthin. Der Server
                   prueft es zusaetzlich selbst, die Regel hier ist nur
                   die Anzeige.
   ------------------------------------------------------------------- */
.eh-ohne-stats [data-eh-stat] { display: none !important; }

/* Der Bestwert auf den Menuekacheln ist eine Punktzahl wie jede andere.
   Die Apps schreiben ihn alle in [data-best-slot], deshalb steht die Regel
   hier einmal statt in zwanzig Dateien. */
.eh-ohne-stats [data-best-slot] { display: none !important; }

.eh-ohne-board [data-go="board"],
.eh-ohne-board [data-eh-board]  { display: none !important; }

/* ---------------------------------------------------------------- Emoji-Bilder

   Emoji liegen als SVG unter /assets/emoji/, weil die Schrift des Geraets sie
   sonst zeichnet — und auf aelteren Schul-PCs fehlen die neueren Zeichen
   dort. Siehe Einhorn.ui.emojiBild().

   Groesse setzt die App ueber eine eigene Klasse; hier steht nur, was immer
   gilt: quadratisch, nicht verzerrt, nicht anfassbar. Das Verschieben per
   Maus muss weg, sonst zieht ein Kind beim Tippen das Bild aus dem Knopf. */
.eh-emoji {
    display: inline-block;
    width: 1em;
    height: 1em;
    object-fit: contain;
    vertical-align: -0.15em;
    user-select: none;
    -webkit-user-drag: none;
}

/* ------------------------------------------------- Angehefteter Weiter-Knopf

   Rueckmeldung von Kiba: der Knopf steht unter der Aufgabe und ist bei
   langen Aufgaben ausserhalb des Sichtfelds — man muss erst scrollen, um
   weiterzukommen.

   Den Mechanismus beschreibt haengeWeiterAuf() in einhorn.js. Kurz: ein
   Platzhalter haelt die Stelle, und sobald die unter dem Bildrand liegt,
   wird der Knopf unten festgesetzt. `position: sticky` allein hat nicht
   gereicht — das wirkt nur innerhalb des Elternelements, und in
   /teilbarkeit/ ist das ein kurzer DIV. */
.eh-weiter-schwebt {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    /* env() wegen der Gestenleiste auf iPhone und iPad. */
    bottom: calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
    z-index: 40;
    width: min(calc(100vw - 2 * var(--sp-4)), 32rem);
    box-shadow: 0 6px 24px color-mix(in srgb, var(--brand-900) 32%, transparent);
    animation: eh-weiter-auf var(--dur-base) var(--ease-back) backwards;
}
@keyframes eh-weiter-auf {
    from { opacity: 0; transform: translateX(-50%) translateY(16px); }
    to   { opacity: 1; transform: translateX(-50%); }
}
/* Haelt die Stelle frei, damit die Seite beim Festsetzen nicht springt. */
.eh-weiter-platz { height: 0; }

/* ------------------------------------------------------------------ Vollbild

   Im Vollbild gibt es keine Browserleisten mehr, also kann der Inhalt die
   ganze Hoehe nutzen. Mehr ist hier nicht noetig: die Kopfzeile klebt
   sowieso oben, und der Weiter-Knopf setzt sich unten fest. */
.eh-vollbild, .eh-vollbild body { height: 100%; }
.eh-vollbild body { overflow-y: auto; }

/* Lenis setzt auf <html> die Klasse `lenis` und dazu
   `html.lenis, html.lenis body { height: auto }` (assets/vendor/lenis.css).
   Dieser Selektor ist spezifischer als `.eh-vollbild body` und wuerde die
   beiden Zeilen darueber aushebeln — im Vollbild liesse sich dann auf den
   Inhaltsseiten nicht mehr scrollen.

   Zwei Klassen auf demselben Element gewinnen gegen eine, also steht die
   Regel hier noch einmal staerker. Zusaetzlich haelt motion.js Lenis im
   Vollbild an; das hier ist der Guertel zum Hosentraeger. */
html.eh-vollbild.lenis,
html.eh-vollbild.lenis body { height: 100%; }
html.eh-vollbild.lenis body { overflow-y: auto; }
