

/* Wenn der User in der Sidebar einen anderen Chat anklickt, ist das eine
   normale Navigation zu /chats/{id}. Ohne Hilfe bricht der Browser den
   alten Frame hart ab und rendert den neuen — sichtbarer Flicker. Mit
   `@view-transition: navigation: auto` blendet der Browser stattdessen
   weich zwischen alter und neuer Seite über. Chrome 126+ unterstützt das,
   Safari/Firefox no-op (kein Schaden, fällt graceful zurück). */
@view-transition {
  navigation: auto;
}

/* Default-Cross-Fade abschalten für alles, was sich gar nicht geändert
   haben sollte (Sidebar bleibt identisch beim Chat-Wechsel) — sonst
   flackert die Sidebar mit, obwohl sie inhaltlich gleich ist. Wir setzen
   stattdessen einen named view-transition auf die Chat-Pane allein. */
.sidebar { view-transition-name: messenger-sidebar; }
.chat-pane { view-transition-name: messenger-chat-pane; }

::view-transition-old(messenger-sidebar),
::view-transition-new(messenger-sidebar) {
  /* Sidebar nicht animieren — sie bleibt sichtbar gleich. */
  animation: none;
}
::view-transition-old(messenger-chat-pane),
::view-transition-new(messenger-chat-pane) {
  animation-duration: 180ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0.2, 1);
}

/* Ohne @property sind CSS-Variablen nicht interpolierbar — Übergänge
   würden hart snappen. Mit Registrierung als <length> klappt der
   transition-Pfad sauber für die Scroll-Fade-Masken. Browser ohne
   @property-Support fallen graceful auf Snap zurück. */
@property --fade-top {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
@property --fade-bottom {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

/* --- Fonts ------------------------------------------------- */

@font-face {
  font-family: "Inter";
  src: url("/static/fonts/InterVariable.woff2") format("woff2-variations"),
       url("/static/fonts/InterVariable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/static/fonts/JetBrainsMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Source Serif 4";
  src: url("/static/fonts/SourceSerif4-Semibold.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* --- Tokens — Light (Default) ----------------------------- */

:root {
  color-scheme: light dark;

  /* Surfaces */
  --bg-canvas:      #f6f5f2;   /* Hintergrund hinter allem (warm white) */
  --bg-surface:     #ecebe6;   /* Sidebar, Header */
  --bg-elevated:    #ffffff;   /* Karten, erhoehte Flaechen */
  --bg-input:       #ffffff;
  --bg-hover:       rgba(0, 0, 0, 0.035);
  --bg-active:      rgba(0, 0, 0, 0.06);
  --bg-bubble-out:  #1c1d20;   /* Eigene Nachrichten — dunkel auf hell */
  --bg-bubble-in:   #ffffff;

  /* Foreground */
  --fg-primary:     #15161a;
  --fg-secondary:   #5a5e67;
  --fg-tertiary:    #8e929b;
  --fg-on-bubble-out: #f3f2ee;
  --fg-on-accent:   #ffffff;

  /* Borders */
  --border-hairline: rgba(0, 0, 0, 0.08);
  --border-soft:     rgba(0, 0, 0, 0.12);
  --border-strong:   rgba(0, 0, 0, 0.20);

  /* Akzent — gedämpftes Brass; einziger Akzent im UI */
  --accent:        #a37d2c;
  --accent-hover:  #876724;
  --accent-soft:   rgba(163, 125, 44, 0.12);

  /* Status */
  --danger:        #b3261e;
  --danger-soft:   rgba(179, 38, 30, 0.10);
  /* Hyperlinks in Karteninhalten — bewusst klassisches Blau statt Brass-Akzent
     (Niklas' Vorgabe 2026-07-27: Links sollen als Links erkennbar sein, nicht
     im Akzent-Ton der restlichen UI untergehen). */
  --link:          #1a56db;
  --link-hover:    #1e40af;

  /* Schatten — sehr zurückhaltend */
  --shadow-sm:  0 1px 2px rgba(20, 20, 25, 0.04);
  --shadow-md:  0 6px 24px -8px rgba(20, 20, 25, 0.10),
                0 2px 6px -2px rgba(20, 20, 25, 0.06);
  --shadow-lg:  0 24px 56px -16px rgba(20, 20, 25, 0.18),
                0 4px 12px -4px rgba(20, 20, 25, 0.08);

  /* Geometrie */
  --radius-xs: 4px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-xl: 28px;
  --radius-pill: 999px;

  
  /* Vom Nutzer ueber die Sidebar-Fusszeile gesetzt (theme.js schreibt den
     Wert vor dem ersten Paint, sonst springt die Seite beim Laden).
     Skaliert die Wurzel-Schriftgroesse und damit alle rem-Masse. */
  --font-scale: 1;
  --font-ui:   "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas,
               monospace;

  /* Halbtransparente Canvas-Flaeche fuer klebende Tagesmarken. Eine
     Hex-Farbe mit Alpha laesst sich nicht aus --bg-canvas ableiten, deshalb
     ein eigenes Token je Theme. */
  --bg-canvas-veil: rgba(246, 245, 242, 0.86);

  /* Sizing */
  --sidebar-w: 320px;
  --header-h:  64px;

  /* Bewegung */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --t-fast:   140ms;
  --t-base:   220ms;
  --t-slow:   420ms;
}

/* --- Tokens — Dark ---------------------------------------
   Greift in zwei Fällen:
   1) System-Präferenz ist dunkel UND der User hat NICHT manuell auf
      Light gestellt (`data-theme="light"`-Override) — Auto-Modus.
   2) Der User hat explizit Dark gewählt (`data-theme="dark"`),
      unabhängig von der System-Präferenz.
   Light ist Default und braucht keinen extra Block — `:root` ohne
   data-theme greift dann automatisch. */
:root[data-theme="dark"],
:root[data-theme="oled"] { color-scheme: dark; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark; }
}

:root[data-theme="dark"],
:root[data-theme="oled"] {
  --bg-canvas-veil: rgba(12, 13, 16, 0.86);
  --bg-canvas:      #0c0d10;
  --bg-surface:     #131418;
  --bg-elevated:    #181a1f;
  --bg-input:       #0a0b0d;
  --bg-hover:       rgba(255, 255, 255, 0.04);
  --bg-active:      rgba(255, 255, 255, 0.07);
  --bg-bubble-out:  #f3f2ee;
  --bg-bubble-in:   #1c1e23;
  --fg-primary:     #ededee;
  --fg-secondary:   #a4a8b1;
  --fg-tertiary:    #70747d;
  --fg-on-bubble-out: #15161a;
  /* Im Dunkelmodus ist der Akzent HELL (#d4b072) — weisser Text darauf kommt
     nur auf 2,05:1 und ist auf den Ungelesen-Zaehlern kaum lesbar. Dunkler
     Text darauf liegt bei rund 9:1. */
  --fg-on-accent:   #17140e;
  --border-hairline: rgba(255, 255, 255, 0.06);
  --border-soft:     rgba(255, 255, 255, 0.10);
  --border-strong:   rgba(255, 255, 255, 0.18);
  --accent:        #d4b072;
  --accent-hover:  #e6c388;
  --accent-soft:   rgba(212, 176, 114, 0.14);
  --danger:        #ef6b66;
  --danger-soft:   rgba(239, 107, 102, 0.14);
  --link:          #60a5fa;
  --link-hover:    #93c5fd;
  --shadow-sm:  0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow-md:  0 6px 24px -8px rgba(0, 0, 0, 0.55),
                0 2px 6px -2px rgba(0, 0, 0, 0.40);
  --shadow-lg:  0 24px 56px -16px rgba(0, 0, 0, 0.70),
                0 4px 12px -4px rgba(0, 0, 0, 0.45);
}

/* Wurzel-Schriftgroesse = Basis 15px mal Nutzer-Faktor. :root statt html,
   damit die Regel die gemeinsame `html, body`-Regel weiter unten schlaegt
   (hoehere Spezifitaet) -- sonst gewinnt deren font-size und der Regler
   bleibt wirkungslos. Bei Faktor 1 exakt der bisherige Wert. */
:root { font-size: calc(15px * var(--font-scale)); }

/* --- Tokens — OLED ----------------------------------------
   Echtes Schwarz statt des weichen #0c0d10. Auf dem OLED-Display eines
   Pixel sind schwarze Pixel aus: mehr Kontrast, weniger Akku. Nur die
   Flaechen aendern sich, alles andere erbt von Dark (Selektorliste oben).
   Randfarben werden leicht angehoben -- auf reinem Schwarz verschwinden
   0.06er-Hairlines sonst vollstaendig. */
:root[data-theme="oled"] {
  --bg-canvas-veil: rgba(0, 0, 0, 0.88);
  --bg-canvas:      #000000;
  --bg-surface:     #07080a;
  --bg-elevated:    #0e1014;
  --bg-input:       #000000;
  --border-hairline: rgba(255, 255, 255, 0.10);
  --border-soft:     rgba(255, 255, 255, 0.15);
  --border-strong:   rgba(255, 255, 255, 0.24);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]):not([data-theme="oled"]) {
    --bg-canvas-veil: rgba(12, 13, 16, 0.86);
    --bg-canvas:      #0c0d10;
    --bg-surface:     #131418;
    --bg-elevated:    #181a1f;
    --bg-input:       #0a0b0d;
    --bg-hover:       rgba(255, 255, 255, 0.04);
    --bg-active:      rgba(255, 255, 255, 0.07);
    --bg-bubble-out:  #f3f2ee;
    --bg-bubble-in:   #1c1e23;
    --fg-primary:     #ededee;
    --fg-secondary:   #a4a8b1;
    --fg-tertiary:    #70747d;
    --fg-on-bubble-out: #15161a;
    /* Im Dunkelmodus ist der Akzent HELL (#d4b072) — weisser Text darauf kommt
       nur auf 2,05:1 und ist auf den Ungelesen-Zaehlern kaum lesbar. Dunkler
       Text darauf liegt bei rund 9:1. */
    --fg-on-accent:   #17140e;
    --border-hairline: rgba(255, 255, 255, 0.06);
    --border-soft:     rgba(255, 255, 255, 0.10);
    --border-strong:   rgba(255, 255, 255, 0.18);
    --accent:        #d4b072;
    --accent-hover:  #e6c388;
    --accent-soft:   rgba(212, 176, 114, 0.14);
    --danger:        #ef6b66;
    --danger-soft:   rgba(239, 107, 102, 0.14);
    --link:          #60a5fa;
    --link-hover:    #93c5fd;
    --shadow-sm:  0 1px 2px rgba(0, 0, 0, 0.40);
    --shadow-md:  0 6px 24px -8px rgba(0, 0, 0, 0.55),
                  0 2px 6px -2px rgba(0, 0, 0, 0.40);
    --shadow-lg:  0 24px 56px -16px rgba(0, 0, 0, 0.70),
                  0 4px 12px -4px rgba(0, 0, 0, 0.45);
  }
}

/* --- Reset / Basis --------------------------------------- */

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

/* Das HTML-`[hidden]`-Attribut wird von komponenten-spezifischen Regeln mit
   eigenem `display` überschrieben (z.B. der „N neue laden"-Knopf), was dazu
   führte, dass mit JS auf hidden=true gesetzte Elemente trotzdem sichtbar
   blieben. Globale !important-Regel löst das ein für alle Mal. */
[hidden] { display: none !important; }

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg-canvas);
  color: var(--fg-primary);
  font-family: var(--font-ui);
  /* KEIN font-size hier: die Wurzelgroesse setzt :root weiter oben (skalierbar),
     body erbt sie. Ein eigenes px-Mass hier haette den Regler ausgehebelt. */
  line-height: 1.55;
  font-feature-settings: "ss01", "cv05", "cv11";
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Document selbst NIEMALS scrollen lassen — sonst rutscht auf Mobile-
     Browsern beim URL-Bar-Hide/Show das ganze Layout, und Header + Composer
     "verschwinden" obwohl sie sticky aussehen sollten. Scrollen passiert
     ausschließlich innerhalb der scrollenden Bereiche. */
  overflow: hidden;
  overscroll-behavior: none;
}

/* Auth-/Setup-Seiten haben kein App-Shell-Layout, sondern eine zentrierte
   Karte — die DARF im Document scrollen, falls die Karte mal länger wird
   (z.B. Setup mit Anleitung). overflow: hidden auf body würde das blocken. */
body:has(> .auth-shell) { overflow: auto; }

/* Text-Selection — solid Brass-Hintergrund mit kontraststarkem Text.
   Die alte rgba(brass, 0.12)-Variante war auf dunklen Flaechen nahezu
   unsichtbar. Jetzt: Light Mode = Brass + Weiß (ratio ≈ 4.7), Dark Mode =
   helleres Brass + Tiefschwarz (ratio ≈ 10) — auf jedem Hintergrund lesbar. */
::selection {
  background: var(--accent);
  color: #ffffff;
}
@media (prefers-color-scheme: dark) {
  ::selection {
    /* Dark mode hat ein helleres Brass — auf dem ist schwarzer Text
       deutlich kontrastreicher als weißer. */
    background: var(--accent);
    color: #0c0d10;
  }
}

a { color: inherit; text-decoration: none; }

button {
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  border: 0;
  background: none;
  color: inherit;
}

input,
textarea,
select {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

/* Fokus — sichtbar, aber nicht laut */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
button:focus-visible,
.btn:focus-visible,
.icon-btn:focus-visible,
.conv-item:focus-visible {
  outline-offset: 3px;
}

/* Scrollbars — dezent */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb {
  background: var(--border-soft);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--border-strong); background-clip: padding-box; }

/* --- Typografie ------------------------------------------ */

h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--fg-primary);
}
h1 { font-size: 1.75rem; letter-spacing: -0.022em; line-height: 1.15; }
h2 { font-size: 1.0625rem; letter-spacing: -0.012em; }
h3 { font-size: 0.9375rem; }

.muted { color: var(--fg-secondary); font-size: 0.875rem; }

.mono { font-family: var(--font-mono); font-feature-settings: "tnum"; }

code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--bg-elevated);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-xs);
  padding: 0.1em 0.4em;
}

/* --- Buttons --------------------------------------------- */

.btn,
button.primary,
button.secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  padding: 0.625rem 1.25rem;
  font-family: inherit;
  font-weight: 500;
  font-size: 0.9375rem;
  letter-spacing: -0.005em;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
  white-space: nowrap;
}

/* Primary — kräftig, dominant. */
.btn,
button.primary {
  background: var(--fg-primary);
  color: var(--bg-canvas);
  box-shadow: var(--shadow-sm);
}
.btn:hover,
button.primary:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}
.btn:active,
button.primary:active { transform: translateY(0); }

/* Secondary — tonale Brass-Variante. Subtiler Brass-Soft-Hintergrund mit
   Brass-Text und brass-getöntem Hairline-Border. Hover füllt zur vollen
   Akzentfarbe um — fühlt sich wie eine zweite, leise Hierarchie unter
   dem Primary an, ohne ins Generische abzufallen. */
.btn.secondary,
button.secondary {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: rgba(163, 125, 44, 0.22);
  box-shadow: none;
}
.btn.secondary:hover,
button.secondary:hover {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}
.btn.secondary:active,
button.secondary:active { transform: translateY(0); }
.btn.secondary .icon,
button.secondary .icon {
  width: 16px;
  height: 16px;
  stroke-width: 1.8;
}
@media (prefers-color-scheme: dark) {
  .btn.secondary,
button.secondary {
    border-color: rgba(212, 176, 114, 0.28);
  }
  .btn.secondary:hover,
button.secondary:hover {
    /* Helleres Brass im Dark Mode → schwarzer Text gibt mehr Kontrast. */
    color: var(--bg-canvas);
  }
}

/* Icon-Button — quadratisch, dezent */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border-radius: var(--radius-sm);
  color: var(--fg-secondary);
  background: transparent;
  transition: color var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out);
}
.icon-btn:hover { color: var(--fg-primary); background: var(--bg-hover); }
.icon-btn:active { background: var(--bg-active); }
.icon-btn svg { width: 18px; height: 18px; }

/* SVG-Icons — Strich folgt currentColor */
svg.icon {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
}

/* Theme-Toggle: progressive Kreis-States in einem SVG.
   `.theme-toggle-ring`  = Outline-Kreis, immer sichtbar
   `.theme-toggle-half`  = halber Fill (Auto-Mode)
   `.theme-toggle-full`  = voller Fill (Dark-Mode)
   Sichtbarkeit wird per data-theme auf <html> gesteuert. */
.theme-toggle-ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
}
.theme-toggle-half {
  fill: currentColor;
  stroke: none;
}
.theme-toggle-full {
  fill: currentColor;
  stroke: none;
}

/* Default (kein data-theme = Auto): Half sichtbar, Full versteckt. */
.theme-toggle-half { display: inline; }
.theme-toggle-full { display: none; }

/* Light = expliziter Override: Half UND Full versteckt → nur Outline. */
:root[data-theme="light"] .theme-toggle-half,
:root[data-theme="light"] .theme-toggle-full { display: none; }

/* Dark = expliziter Override: Full sichtbar, Half versteckt. */
:root[data-theme="dark"] .theme-toggle-half { display: none; }
:root[data-theme="dark"] .theme-toggle-full { display: inline; }

/* SVGs dürfen NICHT vom Flex-Shrink zerquetscht werden — sonst landen wir
   bei 3×18-Bugs (gesehen am Send-Button) oder zerrutschtem Play-Icon im
   Video-Overlay. Universell, damit auch SVGs in <span>-Containern (z.B.
   Avatar-Wrapper, Play-Button-Overlays) ihre intrinsic Größe behalten. */
svg { flex-shrink: 0; }

/* --- Forms ---------------------------------------------- */

.field {
  display: block;
  margin-top: 1.1rem;
}
.field-label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--fg-secondary);
  margin-bottom: 0.4rem;
  letter-spacing: 0.005em;
}

input[type="text"],
input[type="password"],
textarea {
  width: 100%;
  padding: 0.7rem 0.85rem;
  background: var(--bg-input);
  color: var(--fg-primary);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  line-height: 1.45;
  resize: vertical;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out);
}
input[type="text"]:hover,
input[type="password"]:hover,
textarea:hover {
  border-color: var(--border-strong);
}
input[type="text"]:focus,
input[type="password"]:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

input[type="text"].mono,
input.mono {
  font-family: var(--font-mono);
  letter-spacing: 0.05em;
}

/* --- Auth / Setup — zentrierte Karte ---------------------- */

.auth-shell {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 2rem 1.25rem;
  background:
    radial-gradient(1200px 700px at 50% -10%, var(--accent-soft), transparent 60%),
    var(--bg-canvas);
}

.auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-lg);
  padding: 2.25rem 2.25rem 2rem;
  box-shadow: var(--shadow-lg);
  animation: card-in var(--t-slow) var(--ease-out) both;
}
.auth-card .brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 1.5rem;
  color: var(--fg-secondary);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.auth-card .brand-mark {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--fg-primary);
  color: var(--bg-canvas);
  display: grid;
  place-items: center;
}
.auth-card .brand-mark svg { width: 16px; height: 16px; }

.auth-card h1 {
  font-size: 1.625rem;
  margin-bottom: 0.4rem;
}
.auth-card .lede {
  margin: 0 0 1.25rem;
  color: var(--fg-secondary);
  font-size: 0.9375rem;
}

.auth-card form button[type="submit"] {
  width: 100%;
  margin-top: 1.5rem;
}

.error {
  margin-top: 1rem;
  padding: 0.65rem 0.85rem;
  background: var(--danger-soft);
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  color: var(--danger);
  font-size: 0.875rem;
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
}

@keyframes card-in {
  from { opacity: 0; transform: translateY(8px) scale(0.99); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* --- App-Shell (Chat) ------------------------------------ */

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  /* dvh (dynamic viewport height) ist auf Mobile die EINZIG verlässliche
     Maßangabe — sie reflektiert die echte sichtbare Fläche, inkl. dem
     Hide/Show der URL-Bar. vh als Fallback für ältere Browser. */
  height: 100vh;
  height: 100dvh;
  /* 100% statt 100vw: 100vw rechnet auf Mobilgeraeten die Scrollbar-Breite
     mit ein und erzeugt dadurch horizontalen Ueberlauf. */
  width: 100%;
  /* Safe-Area (Notch, abgerundete Ecken, Gestenleiste). Auf dem Desktop und
     im normalen Browser-Tab sind die env()-Werte 0 -- die Regeln kosten dort
     nichts und wirken nur in der installierten PWA. */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  box-sizing: border-box;
  background: var(--bg-canvas);
  overflow: hidden;     /* doppelte Sicherung gegen Layout-Sprünge */
}

/* --- Sidebar --------------------------------------------- */

.sidebar {
  display: flex;
  flex-direction: column;
  background: var(--bg-surface);
  border-right: 1px solid var(--border-hairline);
  min-height: 0;            /* damit overflow im inner-scroller greift */
}

.sidebar-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: env(safe-area-inset-top) 1rem 0;
  height: calc(var(--header-h) + env(safe-area-inset-top));
  border-bottom: 1px solid var(--border-hairline);
  flex-shrink: 0;
}
.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex: 1;
  min-width: 0;
}
.sidebar-brand .brand-mark {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--fg-primary);
  color: var(--bg-canvas);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.sidebar-brand .brand-mark svg { width: 16px; height: 16px; }
.sidebar-brand .brand-text {
  display: flex;
  flex-direction: column;
  /* line-height: 1.1 ließ die Sub-Caps direkt am „g"-Descender von „Signal"
     kleben — wir geben den zwei Zeilen Luft mit einer großzügigeren
     Leading. */
  line-height: 1.25;
  min-width: 0;
}
.sidebar-brand .brand-name {
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
}
.sidebar-brand .brand-sub {
  font-size: 0.6875rem;
  color: var(--fg-tertiary);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Vertikaler Atem zur Zeile darüber, sonst klebt das „E" am Descender
     des „g" von „Signal". */
  margin-top: 3px;
  line-height: 1;
}

.sidebar-section {
  padding: 1rem 1rem 0.4rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-tertiary);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.sidebar-scroll {
  flex: 1;
  overflow-y: auto;
  min-height: 0;
  /* Wie der Chat-Pane: Fade nur dort, wo wirklich Inhalt verborgen liegt.
     JS setzt --fade-top / --fade-bottom basierend auf Scroll-Position. */
  --fade-top: 0px;
  --fade-bottom: 0px;
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),
    transparent 100%
  );
}

.conv-list {
  list-style: none;
  margin: 0;
  padding: 0.25rem 0.5rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.conv-item {
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  gap: 0.7rem;
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius-sm);
  color: var(--fg-primary);
  text-decoration: none;
  position: relative;
  transition: background var(--t-fast) var(--ease-out);
  cursor: pointer;
}
.conv-item:hover { background: var(--bg-hover); }
.conv-item.active {
  background: var(--bg-elevated);
  box-shadow: var(--shadow-sm), inset 0 0 0 1px var(--border-hairline);
}
/* Active-Indicator-Bar steckt im Polish-Layer am Ende des Files —
   dort mit smoothem Slide-In bei .active-Toggle. */

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--bg-canvas);
  border: 1px solid var(--border-hairline);
  display: grid;
  place-items: center;
  color: var(--fg-secondary);
  font-size: 0.8125rem;
  font-weight: 600;
  flex-shrink: 0;
  letter-spacing: -0.01em;
  overflow: hidden;
}
.avatar svg { width: 16px; height: 16px; }
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}

/* Initialen-Avatar (Sidebar-Fusszeile): etwas mehr Kontrast zur Flaeche. */
.avatar {
  background: var(--bg-elevated);
  color: var(--fg-primary);
  border-color: var(--border-soft);
}

.conv-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.conv-name {
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: -0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Vorschau auf den neuesten Beitrag. Zwei Zeilen, danach abgeschnitten —
   genug fuer eine Schlagzeile, ohne dass ein Kanal die halbe Liste einnimmt.
   Steht nur bei Kanaelen mit Ungelesenem (siehe _sidebar.html). */
.conv-preview {
  font-size: 0.8125rem;
  color: var(--fg-tertiary);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 0.1rem;
}
.conv-when {
  font-family: var(--font-mono);
  font-size: 0.92em;
  color: var(--fg-tertiary);
  opacity: 0.85;
}
.conv-when::after {
  content: " · ";
  opacity: 0.7;
}
/* Abgearbeitete Kanaele bleiben einzeilig und treten zurueck. */
.conv-item.is-read .conv-name { font-weight: 400; }
.conv-item.active .conv-name { color: var(--fg-primary); }
.conv-item.active .conv-preview { color: var(--fg-secondary); }

.conv-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  flex-shrink: 0;
}
.conv-unread {
  background: var(--accent);
  color: var(--fg-on-accent);
  border-radius: var(--radius-pill);
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 1px 7px;
  min-width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}

.sidebar-empty {
  padding: 1.25rem 1rem;
  color: var(--fg-secondary);
  font-size: 0.875rem;
}

.sidebar-footer {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Ohne den unteren Inset liegen Logout und Theme-Umschalter in der
     Android-Gestenleiste der installierten PWA. */
  padding: 0.65rem 0.75rem calc(0.65rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border-hairline);
  flex-shrink: 0;
}
.sidebar-footer .user-chip {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius-sm);
  min-width: 0;
}
.sidebar-footer .avatar-user {
  width: 28px;
  height: 28px;
  font-size: 0.75rem;
  position: relative;
  overflow: visible;          /* Dot darf außerhalb sitzen */
}
/* Grüner Live-Indikator analog zu Apple Mail / Slack — ein 8px-Dot mit
   Canvas-Ring, der eine kleine optische Trennung zur Avatar-Kante schafft. */
.presence-dot {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: #4cae5c;
  box-shadow: 0 0 0 2px var(--bg-surface);
}
.sidebar-footer .user-name {
  flex: 1;
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: -0.005em;
}
.sidebar-footer form { margin: 0; }

/* --- Chat-Pane ------------------------------------------- */

.chat-pane {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--bg-canvas);
  position: relative;
  /* Filter + Opacity sind GPU-accelerated, will-change-Hint signalisiert
     dem Compositor, eine eigene Layer aufzuziehen. Kein Layout-Reflow. */
  will-change: filter, opacity;
}

/* Cross-fade-Trick: alt und neu treffen sich am Swap-Punkt im IDENTISCHEN
   Zwischenstadium (blur(6px) + opacity 0.4). Der eigentliche DOM-Tausch
   passiert genau dort — visuell ein Übergang ohne Bruchstelle, weil das
   Bild davor und danach denselben „weichen Schleier" zeigt. */
.chat-pane.htmx-swapping {
  animation: chat-pane-fade-out 160ms cubic-bezier(0.4, 0, 1, 0.7) forwards;
}
.chat-pane.htmx-added {
  animation: chat-pane-fade-in 260ms cubic-bezier(0.2, 0.6, 0.2, 1) forwards;
}
@keyframes chat-pane-fade-out {
  from { filter: blur(0); opacity: 1; }
  to   { filter: blur(6px); opacity: 0.4; }
}
@keyframes chat-pane-fade-in {
  from { filter: blur(6px); opacity: 0.4; }
  to   { filter: blur(0); opacity: 1; }
}

/* --- Empty State (kein Chat ausgewählt) ------------------- */

.empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 2rem;
  gap: 1rem;
}
.empty-state .empty-mark {
  width: 64px; height: 64px;
  border-radius: 18px;
  background: var(--bg-surface);
  border: 1px solid var(--border-hairline);
  display: grid; place-items: center;
  color: var(--fg-secondary);
  margin-bottom: 0.5rem;
}
.empty-state .empty-mark svg { width: 28px; height: 28px; }
.empty-state h2 { font-size: 1.25rem; }
.empty-state p { max-width: 360px; }

.empty-state form { margin: 0; }
.empty-state form button { margin: 0; }

/* Mobile: Single-Pane. Wenn ein Chat aktiv ist (.app-shell hat dann eine
   .chat-header im Chat-Pane), wird die Sidebar versteckt. Sonst zeigen wir
   die Sidebar voll und der leere Chat-Pane (Empty-State) tritt zurück. */
@media (max-width: 820px) {
  /* Master-Detail: entweder Kanalliste oder Kanal, nie beides.
     Gekoppelt an .feed-header, das nur im geoeffneten Kanal existiert. */
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
  }
  .sidebar,
  .chat-pane {
    grid-column: 1;
    grid-row: 1;
    border-right: 0;
  }
  .sidebar { display: flex; }
  .chat-pane { display: none; }
  .app-shell:has(.feed-header) .sidebar { display: none; }
  .app-shell:has(.feed-header) .chat-pane { display: flex; }

  /* Defensive Sicherung: kein horizontaler Scroll auf der Chat-Pane
     selbst — falls eine Komponente überhängt, fällt der Effekt nicht
     direkt auf den Viewport durch. */
  .chat-pane { overflow-x: clip; }
}

/* --- Sidebar: Avatar reagiert auf Hover --------------------
   Statt das ganze List-Item zu shiften (was unruhig wirkt),
   skaliert nur der Avatar minimal. Apple Music in der Mediathek-
   Liste verwendet den gleichen Trick.
   ----------------------------------------------------------- */
.conv-item .avatar {
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.conv-item:hover .avatar {
  transform: scale(1.06);
}
.conv-item:active .avatar {
  transform: scale(1.02);
  transition-duration: 80ms;
}

/* --- Sidebar: Active-Indicator ----------------------------
   Linker, brassfarbener Stab am aktiven Item — bekommt eine
   sanfte Slide-In-Animation, sobald die `.active`-Klasse gesetzt
   wird. Nutzt ::before, damit das Item-Layout selbst stabil
   bleibt. */
.conv-item {
  position: relative;
}
.conv-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 18%;
  bottom: 18%;
  width: 3px;
  border-radius: 0 2px 2px 0;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: center;
  opacity: 0;
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
              opacity 200ms ease-out;
  pointer-events: none;
}
.conv-item.active::before {
  transform: scaleY(1);
  opacity: 1;
}

/* --- Reduced motion -------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
*::before,
*::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Sidebar: Kanal-Punkt statt Avatar, gelesene Kanäle gedämpft. */
.channel-item { gap: 0.7rem; }
.channel-dot {
  width: 10px;
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  flex-shrink: 0;
}
.channel-item.is-read .conv-name { color: var(--fg-secondary); font-weight: 500; }
/* Punkt ist ein Unread-Indikator (wie Discord). Gelesen = Hohlkreis statt
   voller Punkt (statt unsichtbar — leere Fläche in der Spalte sah unruhig
   aus). Bleibt sichtbar, damit Spalte 1 von `.conv-item`s Grid (36px 1fr
   auto) belegt bleibt. `!important` nötig: `_sidebar.html` setzt die
   Akzentfarbe des Kanals per Inline-`style="background: …"` auf denselben
   Punkt, das schlägt sonst diese Klassen-Regel. */
.channel-item.is-read .channel-dot {
  background: transparent !important;
  border: 1.5px solid var(--fg-secondary);
}

/* Kanal-Pane */
.feed-pane {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.feed-header {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: env(safe-area-inset-top) 1.1rem 0;
  min-height: calc(var(--header-h) + env(safe-area-inset-top));
  border-bottom: 1px solid var(--border-hairline);
  background: var(--bg-surface);
  flex-shrink: 0;
}
.feed-header .back-btn { display: none; }
.feed-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  flex: 1;
}
.feed-title h1 {
  font-size: 1.05rem;
  font-weight: 650;
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.feed-category { font-size: 0.8rem; flex-shrink: 0; }
.feed-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }

.feed-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: clip;
  /* Kein Innenabstand OBEN: der klebende Tagestrenner haftet sonst erst
     unterhalb davon, und in dem Streifen darueber scrollt der Karteninhalt
     sichtbar vorbei. Der Abstand sitzt stattdessen auf .feed-list. */
  padding: 0 1.1rem calc(1.1rem + env(safe-area-inset-bottom));
  /* contain statt des globalen none: haelt Overscroll in der Liste (kein
     Durchschlagen auf die Seite), erlaubt aber die eigene
     Pull-to-Refresh-Geste aus app.js. */
  overscroll-behavior-y: contain;
}
.feed-list {
  padding-top: 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  max-width: 820px;
  margin: 0 auto;
}
.feed-empty { padding: 2rem 1rem; text-align: center; }

/* Karte = eine bot_notices-Zeile */
.feed-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: 0.95rem 1.1rem;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out);
}
.feed-card.is-unread {
  border-left: 3px solid var(--accent);
  padding-left: calc(1.1rem - 2px);
}
.feed-card .unread-dot { display: none; }
.feed-card.is-unread .unread-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--accent);
}

/* Quellfarbe der Karte als schmaler Randstreifen. Die Farbe steckte bisher
   nur im Datenmodell (--embed-accent, von app.js gesetzt) und wurde von keiner
   einzigen Regel gelesen -- jede Karte sah gleich aus, egal aus welcher Quelle
   sie kam. In Posteingang und Suche ist der Streifen die schnellste Auskunft
   darueber, woher eine Karte stammt. */
.embed {
  position: relative;
  padding-left: 0.8rem;
}
.embed::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  bottom: 0.15em;
  width: 2px;
  border-radius: var(--radius-pill);
  background: var(--embed-accent, var(--border-soft));
}
.embed + .embed { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border-hairline); }
.embed-author { font-size: 0.82rem; color: var(--fg-secondary); margin-bottom: 0.2rem; }
.embed-author a { color: var(--link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.embed-author a:hover { color: var(--link-hover); }
.embed-title {
  font-family: "Source Serif 4", Georgia, serif;
  font-size: 1.125rem;
  font-weight: 600; /* matches the static Semibold file loaded above — no variable instance available */
  line-height: 1.35;
  margin: 0 0 0.35rem;
}
.embed-title a { color: inherit; text-decoration: none; }
.embed-title a:hover { color: var(--link-hover); }
/* Aufzaehlungen aus dem Kartentext (siehe embed_render._lists). Haengender
   Einzug: mehrzeilige Punkte stehen unter ihrem eigenen Text, nicht unter
   der Marke. */
.md-list {
  margin: 0.35rem 0;
  padding-left: 1.1rem;
  list-style: none;
}
.md-list li {
  position: relative;
  margin: 0.15rem 0;
}
.md-list li::before {
  content: "";
  position: absolute;
  left: -0.8rem;
  top: 0.62em;
  width: 4px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--fg-tertiary);
}

.embed-desc {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--fg-primary);
  overflow-wrap: anywhere;
}
.embed-desc a { color: var(--link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.embed-desc a:hover { color: var(--link-hover); }
.embed-media {
  display: block;
  width: 100%;
  margin-top: 0.65rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-hairline);
}
/* Bilder sind Beiwerk, nicht Hauptdarsteller: viele og:image sind
   1200x630-Logos, die sonst die halbe Karte fuellen und den Titel nach oben
   aus dem Blick draengen. Feste, moderate Hoehe mit Beschnitt haelt den
   Kartenrhythmus stabil — unabhaengig davon, was ein Verlag hinterlegt. */
/* Erst sichtbar, wenn das Bild wirklich geladen ist (app.js setzt is-loaded).
   Vorher stand waehrend des Ladens ein 168px hoher leerer Block in der Karte —
   und blieb als schwarze Flaeche stehen, wenn die Quelle gar kein Bild hat.
   Ohne reservierte Hoehe springt zwar das Layout beim Erscheinen, aber ein
   nachrutschendes Bild ist harmloser als ein Loch, das vielleicht nie gefuellt
   wird (viele Artikel haben kein og:image). */
img.embed-media {
  display: none;
  height: 168px;
  object-fit: cover;
  object-position: center;
  background: var(--bg-canvas);
}
img.embed-media.is-loaded {
  display: block;
  animation: media-in var(--t-base) var(--ease-out) both;
}
@keyframes media-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
video.embed-media {
  max-height: 60vh;
  background: #000;
}
.embed-fields {
  margin: 0.6rem 0 0;
  font-size: 0.88rem;
}
/* Überschrift immer ÜBER dem Inhalt, nie nebeneinander (Niklas' Vorgabe
   2026-07-27) — normaler Blockfluss statt Grid, dt/dd laufen untereinander.
   Ersetzt die vorherige Zwei-Spalten-Grid-Lösung vom Vortag. */
.embed-fields dt { color: var(--fg-secondary); font-weight: 600; margin-top: 0.6rem; }
.embed-fields dt:first-child { margin-top: 0; }
.embed-fields dd { margin: 0.15rem 0 0; overflow-wrap: anywhere; }
.embed-fields dd a { color: var(--link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.embed-fields dd a:hover { color: var(--link-hover); }
.embed-footer { font-size: 0.78rem; margin-top: 0.55rem; }

.card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.7rem;
  padding-top: 0.55rem;
  border-top: 1px solid var(--border-hairline);
}
.feed-ts {
  font-family: var(--font-mono);
  font-size: 0.74rem;
  color: var(--fg-tertiary);
  /* Nie umbrechen: in der engen Fusszeile (Etikett + Zeit + zwei Knoepfe)
     riss der Zeitstempel sonst in zwei Zeilen. Kuerzen darf das Etikett. */
  white-space: nowrap;
}

.feed-more { display: flex; justify-content: center; padding: 1rem; }
/* ── Suche ────────────────────────────────────────────────────────────────
   Kein weisses Formularfeld auf weissem Grund: das Feld sitzt auf der
   Canvas-Flaeche wie eine Zeile im Register. Lupe als Strich-Icon in der
   Groesse der uebrigen Icons, Fokus in Messing wie jeder andere Fokus. */
.sidebar-search {
  position: relative;
  padding: 0.65rem 0.75rem 0.35rem;
  flex-shrink: 0;
}
.sidebar-search svg,
.feed-search svg {
  position: absolute;
  left: 1.4rem;
  top: calc(50% + 0.15rem);
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--fg-tertiary);
  pointer-events: none;
  transition: color var(--t-fast) var(--ease-out);
}
.sidebar-search input,
.feed-search input {
  width: 100%;
  height: 34px;
  padding: 0 0.7rem 0 2.1rem;
  border: 1px solid var(--border-hairline);
  border-radius: var(--radius-sm);
  background: var(--bg-canvas);
  color: var(--fg-primary);
  font: inherit;
  font-size: 0.84rem;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}
.sidebar-search input::placeholder,
.feed-search input::placeholder { color: var(--fg-tertiary); }
.sidebar-search input:focus-visible,
.feed-search input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.sidebar-search:focus-within svg,
.feed-search:focus-within svg { color: var(--accent); }
.feed-search {
  position: relative;
  margin-left: auto;
  min-width: 12rem;
}
.feed-search svg { left: 0.7rem; top: 50%; }
.feed-search input { padding-left: 1.95rem; }

/* ── Posteingang in der Sidebar ───────────────────────────────────────────
   Kein Kanal, sondern eine Sammlung — deshalb ein Stapel-Zeichen im
   Punkt-Slot statt eines Farbpunkts. Gestrichelte Kreise kommen in dieser
   Oberflaeche sonst nirgends vor. */
.inbox-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: var(--fg-tertiary);
  transition: color var(--t-fast) var(--ease-out);
}
.inbox-mark svg { width: 16px; height: 16px; }
.conv-item.inbox-item:not(.is-read) .inbox-mark,
.conv-item.inbox-item.active .inbox-mark { color: var(--accent); }

/* ── Kanal-Etikett auf Karten gemischter Ansichten ────────────────────────
   Dieselbe Sprache wie die Sidebar: Punkt in Kanalfarbe plus Name. Vorher
   eine gerahmte Pille — ein zweites Vokabular fuer dieselbe Aussage. */
.card-channel {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.74rem;
  font-weight: 500;
  color: var(--fg-secondary);
  text-decoration: none;
  transition: color var(--t-fast) var(--ease-out);
}
.card-channel::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--card-channel-accent, var(--fg-tertiary));
  flex-shrink: 0;
}
.card-channel:hover { color: var(--fg-primary); }
.card-channel {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.card-meta { min-width: 0; }

/* ── Tagesmarke ───────────────────────────────────────────────────────────
   Datum in Mono, wie jeder Zeitstempel dieser App. Die Marke klebt beim
   Scrollen und legt sich mit verschleierter Canvas-Flaeche ueber die Karten
   — dieselbe Technik wie die Kopfzeilen. */
.feed-day {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin: 0.45rem 0 -0.2rem;
  padding: 0.45rem 0.15rem;
  background: var(--bg-canvas-veil);
  backdrop-filter: blur(10px) saturate(1.15);
  -webkit-backdrop-filter: blur(10px) saturate(1.15);
}
.feed-day-label {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-tertiary);
  font-variant-numeric: tabular-nums;
}
.feed-day::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-hairline);
  order: 2;
}
.feed-day form { margin: 0; order: 3; }

/* Ghost-Pille im Button-Stil der App (vorher ein unterstrichener Textlink —
   die Oberflaeche kennt sonst keine Textlinks). Zurueckhaltend, bis man
   hinzeigt; auf Touch dauerhaft sichtbarer, weil es dort kein Hover gibt. */
.feed-day-read {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  border-radius: var(--radius-pill);
  padding: 0.18rem 0.6rem;
  font-family: inherit;
  font-size: 0.7rem;
  font-weight: 500;
  color: var(--fg-tertiary);
  cursor: pointer;
  opacity: 0.5;
  transition: opacity var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.feed-day:hover .feed-day-read,
.feed-day-read:focus-visible { opacity: 1; }
.feed-day-read:hover,
.feed-day-read:focus-visible {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: rgba(163, 125, 44, 0.22);
}
@media (hover: none) {
  .feed-day-read { opacity: 0.8; }
}

/* ── Schriftgrad ──────────────────────────────────────────────────────────
   Echte Strich-Icons wie alle anderen Knoepfe der Fusszeile (vorher „A−"/„A+"
   als Text — der billigste Fleck der ganzen Oberflaeche). */
.font-steps { display: inline-flex; gap: 0.05rem; }
.font-step { width: 30px; }
.font-step svg { width: 18px; height: 18px; }
.font-step[disabled] { opacity: 0.28; pointer-events: none; }

/* Marke im Kopf der gemischten Ansichten — sitzt an der Stelle, an der die
   Kanalseite ihren Farbpunkt zeigt, damit beide Koepfe denselben Takt haben. */
.view-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: var(--accent);
  flex-shrink: 0;
}
.view-mark svg { width: 17px; height: 17px; }

/* ── Ziehen zum Nachladen ─────────────────────────────────────────────────
   Sitzt UNTER der Liste (siehe app.js): der Feed laeuft chronologisch, Neues
   kommt unten herein. Der Ring fuellt sich mit dem Zugweg (--pull von 0 bis 1)
   und rastet bei Erreichen der Schwelle sichtbar ein. */
.feed-pull {
  height: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.feed-pull-spinner {
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
  background: conic-gradient(var(--fg-tertiary) calc(var(--pull, 0) * 360deg),
                             transparent 0);
  -webkit-mask: radial-gradient(circle, transparent 7px, #000 7.5px);
  mask: radial-gradient(circle, transparent 7px, #000 7.5px);
  transition: background var(--t-fast) linear, transform var(--t-fast) var(--ease-out);
}
.feed-pull.is-armed .feed-pull-spinner {
  background: conic-gradient(var(--accent) 360deg, var(--accent) 0);
  transform: scale(1.12);
}
.feed-pull.is-loading .feed-pull-spinner {
  background: conic-gradient(var(--accent) 90deg, transparent 0);
  transform: none;
  animation: feed-spin 0.7s linear infinite;
}

.feed-more-spinner {
  width: 18px;
  height: 18px;
  border: 2px solid var(--border-soft);
  border-top-color: var(--accent);
  border-radius: var(--radius-pill);
  animation: feed-spin 0.7s linear infinite;
}
@keyframes feed-spin { to { transform: rotate(360deg); } }

/* Mobile: Master-Detail wie beim Messenger, aber an .feed-header gekoppelt. */
@media (max-width: 820px) {
  .feed-header .back-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-sm);
    color: var(--fg-secondary);
    text-decoration: none;
    flex-shrink: 0;
  }
  .feed-header .back-btn:hover { color: var(--fg-primary); background: var(--bg-hover); }
  .feed-scroll { padding: 0 0.85rem calc(0.85rem + env(safe-area-inset-bottom)); }
  .feed-list { padding-top: 0.85rem; }

  /* Auf Handybreite passen Titel und Beschreibung nicht in eine Zeile — der
     Titel wurde vorher auf „P…" zusammengedrueckt. Beides bricht deshalb
     um: Marke und Titel in der ersten Zeile, die Beschreibung darunter.
     Das Suchfeld bekommt eine dritte Zeile. */
  .feed-header {
    flex-wrap: wrap;
    padding-bottom: 0.5rem;
    align-items: flex-start;
    padding-top: calc(0.55rem + env(safe-area-inset-top));
  }
  .feed-header .back-btn { margin-top: 0.1rem; }
  .feed-title {
    /* Basis 0 statt auto: der Titelblock darf schrumpfen, sonst draengt er
       die Kopfzeilen-Knoepfe in eine eigene Zeile. Der Umbruch findet INNEN
       statt (Beschreibung unter den Titel), nicht im Kopf. */
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: wrap;
    row-gap: 0.05rem;
    align-items: baseline;
  }
  .feed-title h1 {
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .feed-actions { order: 2; margin-left: auto; }
  .feed-category {
    order: 3;
    flex: 1 0 100%;
    line-height: 1.35;
  }
  .feed-search { order: 4; }
  .feed-search {
    order: 3;
    flex: 1 0 100%;
    margin-left: 0;
  }
  .feed-search input { width: 100%; min-width: 0; }

  /* Kopfzeile bleibt immer einzeilig — Buttons auf Icon/Zähler ohne
     Label schrumpfen statt umzubrechen (Niklas' Vorgabe 2026-07-28). */
  .feed-action-label { display: none; }
  .feed-actions button.secondary {
    height: 36px;
    min-width: 36px;
    padding: 0 0.5rem;
    gap: 0;
  }
}

/* ── Slice 2: Aktions-Buttons (✅/❌) + Erledigt-Badge ────────────────────── */
.card-meta-right { display: flex; align-items: center; gap: 0.4rem; }
.card-actions { display: flex; gap: 0.4rem; }

/* Aktions-Knoepfe. Bis 2026-08-30 standen hier die Emoji ✅/❌ aus der
   Discord-Parität — daneben lauter feine Strich-Icons, der lauteste Bruch der
   Oberflaeche. Jetzt dieselbe Zeichensprache wie ueberall: 24er-Raster,
   runde Enden, Farbe erst bei Beruehrung (Messing fuer Uebernehmen, Rot fuer
   Verwerfen). 32px statt 30 — auf dem Handy sind zwei Knoepfe nebeneinander
   sonst schwer zu treffen. */
.action-btn {
  appearance: none;
  border: 1px solid var(--border-soft);
  background: var(--bg-elevated);
  border-radius: var(--radius-pill);
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--fg-tertiary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease-out),
              background var(--t-fast) var(--ease-out),
              color var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out);
}
.action-btn svg { width: 16px; height: 16px; }
.action-btn:active { transform: scale(0.92); }
.action-approve:hover,
.action-approve:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.action-discard:hover,
.action-discard:focus-visible {
  color: var(--danger);
  border-color: var(--danger);
  background: var(--danger-soft);
}

/* htmx-request-Zustand: Karte signalisiert "laeuft noch" (v.a. fuers langsame
   promote_legal), Buttons weg vom Zugriff statt Doppelklick zu riskieren. */
.feed-card.htmx-request { opacity: 0.6; pointer-events: none; }
.feed-card.htmx-request .card-meta-right::after {
  content: "";
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-soft);
  border-top-color: var(--accent);
  border-radius: var(--radius-pill);
  animation: feed-spin 0.7s linear infinite;
  display: inline-block;
}

/* ── Rueckmeldung nach einer Aktion ───────────────────────────────────────
   Eine Randnotiz unter der Meta-Zeile, kein farbiger Kasten: diese
   Oberflaeche erzaehlt Zustand ueber Punkte, Pillen und Haken, nie ueber
   Flaechen. Lebt nur in der HTMX-Antwort (siehe routes/feed.py) und blendet
   sich einmal kurz ein. */
.card-flash {
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
  margin: 0.5rem 0 0;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--fg-secondary);
  animation: card-flash-in var(--t-base) var(--ease-out) both;
}
.card-flash::before {
  content: "";
  width: 6px;
  height: 6px;
  margin-top: 0.35em;
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}
.card-flash-ok::before { background: var(--accent); }
.card-flash-error::before { background: var(--danger); }
.card-flash-error { color: var(--danger); }

@keyframes card-flash-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

/* Erledigt: gefuellter Messing-Punkt mit Haken — dieselbe Aussage wie der
   Ungelesen-Zaehler in der Sidebar, nur andersherum. */
.card-done-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
}
.card-done-badge svg { width: 13px; height: 13px; }
