/* ── RESET ── */
*,
*::before,
*::after {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
html,
body {
	height: 100%;
	overflow: hidden;
}
body {
	font-family: var(--ff);
	background: var(--bg);
	color: var(--t1);
	font-size: var(--fs-body);
	/* Bewusst dünner als der Browser-Default 400 (15.09.2026, Nutzerwunsch)
	   — Basis für alle Elemente ohne eigenen font-weight, siehe
	   "Font-Gewicht" in styleguide/foundations.html. */
	font-weight: 300;
	line-height: 1.5;
}
/* Long-press text selection on touch (iPad/iPhone) fights with drag/pinch
   gestures elsewhere in the app (e.g. the intraday chart resize handle,
   js/intraday-chart-resize.js) — the iOS selection magnifier/callout menu
   pops up mid-gesture. Scoped to pointer:coarse so desktop Safari (mouse/
   trackpad) keeps normal text selection. */
@media (pointer: coarse) {
	body {
		-webkit-user-select: none;
		user-select: none;
		-webkit-touch-callout: none;
	}
	input,
	textarea,
	[contenteditable] {
		-webkit-user-select: text;
		user-select: text;
		-webkit-touch-callout: default;
	}
}
input,
button,
select {
	font-family: inherit;
}
button {
	cursor: pointer;
}

/* ── LAYOUT ── */
/* dvh (not vh) matches the device-view shell in device-views.css — on iOS
   Safari in a regular (non-PWA-standalone) tab, 100vh is taller than the
   actually visible viewport (it ignores the collapsing URL bar), pushing
   #content's bottom edge behind it; 100dvh tracks the real visible area. */
#app {
	height: 100dvh;
	display: flex;
	flex-direction: column;
}
/* #main-app ist selbst der Scroller, nicht #content: nur so kann die
   Kopfleisten-Gruppe (.chrome-stack) per position:sticky über dem Inhalt
   stehen bleiben, während dieser darunter durchscrollt. Lag die Leiste wie
   früher als flex-Geschwister NEBEN dem Scroller, war ihr eigenes
   position:sticky wirkungslos (sie stand nur durch das Flex-Layout still),
   und es schob sich unter ihr nie etwas durch — womit auch die klebenden
   Kartenüberschriften keinen brauchbaren Haftpunkt hatten.
   Nebeneffekt, der die frühere Konstruktion ersetzt: es braucht keinerlei
   Höhenausgleich (padding-top o.ä.) am Inhalt, die Leiste beansprucht ihren
   Platz weiterhin ganz normal im Fluss. */
#main-app {
	height: 100dvh;
	overflow-y: auto;
	overflow-x: hidden;
	/* Sprungziele (js/app.js' [data-scroll-target]-Klick) landen sonst unter
	   der feststehenden Leiste. */
	scroll-padding-top: var(--chrome-h);
}
/* Uniform background — no separate "content area" tint against which cards
   used to pop out as distinct boxes (var(--bg2) vs. .card's var(--bg)).
   Flat design relies on whitespace, not surface color, to group content. */
#content {
	background: var(--bg);
}
/* ── CHROME STACK (Topbar + Navtabs als eine klebende Einheit) ── */
/* Blickdicht. Eine durchscheinende Variante mit backdrop-filter war am
   11.09.2026 kurz live und wurde wieder verworfen — sie zwingt jede weitere
   klebende Fläche (Kartenüberschriften) entweder zu einer sichtbaren Kante
   gegen das Glas oder selbst ins Glas, wo zwei getrennte Weichzeichner-Flächen
   nie zu einer durchgehenden verschmelzen. Der gemeinsame Container bleibt
   trotzdem richtig: er hält Topbar und Navtabs als EINE klebende Einheit
   zusammen. */
.chrome-stack {
	position: sticky;
	top: 0;
	z-index: var(--z-sticky);
	background: var(--bg);
}
/* 1px-Bleed gegen den Subpixel-Rest zwischen der realen Höhe von
   .chrome-stack und dem statischen --chrome-h-Token (Haftpunkt von .card-h,
   css/tables.css): .navtabs/.topbar nutzen min-height + fluid --fs-heading
   und können dadurch real geringfügig über --topbar-h/--nav-h hinauswachsen
   (siehe Kommentar bei .navtabs unten) — auf hochauflösenden Screens reisst
   das sonst einen 1px-Spalt zwischen Kopfleiste und klebender
   Kartenüberschrift auf, durch den der scrollende Inhalt durchblitzt. Deckt
   ihn mit derselben Fläche zu, statt --chrome-h exakt nachzumessen. */
.chrome-stack::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 100%;
	height: 1px;
	background: var(--bg);
	pointer-events: none;
}
/* Sanfter Ausblend-Verlauf wie bei .card-h (css/tables.css) — eigenes
   ::before statt Wiederverwendung von ::after oben, da der 1px-Bleed dort
   IMMER exakt bei top:100% bleiben muss (Subpixel-Fix). Ein Versatz nach
   unten (verworfener Zwischenstand) verschiebt das Überlagerungsproblem nur
   auf die erste Content-Zeile darunter, statt es zu lösen, und reisst
   ausserdem eine sichtbare Lücke zwischen Kopfleisten-Hintergrund und
   Verlauf-Start auf. Stattdessen bleibt der Verlauf direkt bei top:100%,
   ist an der Spitze aber nur noch teilweise deckend (color-mix statt reinem
   var(--bg)) — Text darunter bleibt lesbar, auch wo der Verlauf ihn direkt
   überlappt. `.is-stuck` wird per Rect-Vergleich gesetzt
   (js/sticky-headers.js), sobald tatsächlich Content unter der Kopfleiste
   durchscrollt statt sich nur in normaler Fluss-Position (Seite ganz oben)
   zu befinden. */
.chrome-stack::before {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 100%;
	height: 18px;
	background: linear-gradient(to bottom, color-mix(in srgb, var(--bg) 55%, transparent), transparent);
	pointer-events: none;
	opacity: 0;
}
.chrome-stack.is-stuck::before {
	opacity: 1;
}
.tab-content {
	display: none;
}
.tab-content.active {
	display: block;
}
.tab-content.hidden {
	display: none;
}
.hidden {
	display: none !important;
}

/* ── TOPBAR ── */
.topbar {
	/* min-height (not height): under the project's box-sizing:border-box,
	   a fixed height would let padding-top eat into the content area instead
	   of growing the bar — min-height keeps the icon row's own height intact
	   and lets the safe-area inset add on top of it (08.09.2026). */
	min-height: var(--topbar-h);
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding-top: var(--safe-top);
	padding-inline: var(--page-pad-safe-left) var(--page-pad-safe-right);
	/* Kein eigener Hintergrund mehr — die Fläche liegt am gemeinsamen
	   .chrome-stack, damit Topbar und Navtabs eine durchgehende Fläche bilden
	   statt zweier getrennt gefärbter Streifen. Das frühere
	   position:sticky hier war wirkungslos: die Leiste war nie Kind des
	   Scrollers, sticky sitzt jetzt am .chrome-stack. */
	background: transparent;
	/* Bezugsrahmen für den absolut zentrierten .view-switch-group
	   (css/device-views.css) — den lieferte vorher das entfernte
	   position:sticky; ohne dies fiele er auf .chrome-stack zurück. */
	position: relative;
}
/* position:relative als Bezugsrahmen für das absolut überlagerte
   .topbar-price-live unten (Crossfade). Bewusst NICHT display:grid mit
   gemeinsamer grid-area: eine implizite Grid-Spalte bemisst sich an der
   max-content-Breite ALLER Items der Zelle, auch an unsichtbaren
   (opacity:0 nimmt am Track-Sizing weiterhin teil, anders als display:none)
   - .topbar-left würde dadurch dauerhaft auf die Breite des breiteren der
   beiden Blöcke wachsen, selbst ganz oben auf der Seite, wo die Kurs-
   Anzeige nie sichtbar wird (im Code-Review gefunden, vor Deploy korrigiert).
   position:absolute nimmt das überlagerte Element dagegen komplett aus dem
   Fluss - .topbar-left bemisst sich nur noch am sichtbaren
   #position-selector. */
.topbar-left {
	display: flex;
	align-items: center;
	gap: 16px;
	position: relative;
}
.topbar-right {
	display: flex;
	align-items: center;
}

.position-selector {
	display: flex;
	align-items: baseline;
	gap: 6px;
	cursor: pointer;
	color: var(--t1);
	/* Padding + kompensierender negativer Margin: Hover/Active-Hintergrund
	   bekommt Luft um den Text, ohne die Topbar-Zeile zu verschieben.
	   Inline-Padding exakt 14px wie .pos-item/.pos-share (css/views.css) —
	   sonst startet der Text im geöffneten Dropdown ein paar Pixel versetzt
	   zum Trigger-Text darüber (Nutzerfeedback 18.08.2026). */
	padding: 4px 14px;
	margin: -4px -14px;
	border-radius: var(--radius);
	/* Für den Topbar-Kurs-Crossfade (.topbar-left--price-live) unten. Kein
	   echter gleichzeitiger Crossfade (beide Texte kurz gemeinsam sichtbar
	   sah überlagert/"komisch" aus, Nutzerfeedback) - stattdessen sequentiell:
	   das AUSBLENDENDE Element startet sofort (transition-delay: 0s, per
	   Zustandsregel unten), das EINBLENDENDE wartet per transition-delay
	   exakt so lange, wie die andere Seite zum Ausblenden braucht (beide
	   Beine --dur-fast lang). Default-Delay hier ist die "blendet wieder EIN"
	   -Richtung (Dropdown erscheint wieder nach dem Zurückscrollen). */
	transition: opacity var(--dur-fast) ease;
	transition-delay: var(--dur-fast);
}
@media (hover: hover) and (pointer: fine) {
	.position-selector:hover {
		color: var(--t2);
	}
}
/* Bei offenem Dropdown verschmilzt der Trigger optisch mit dem Panel
   darunter — derselbe hellere Hintergrund, keine Border/Outline
   (Nutzerwunsch 18.08.2026, Prinzip für alle Dropdowns/Selects). */
.position-selector.active {
	color: var(--t2);
	background: var(--bg-hover);
}
/* Read-only Share-Link: dort ist genau EIN Portfolio sichtbar (der Token ist
   fest daran gebunden), ein Umschalter hätte also nichts, wohin er umschalten
   könnte. Der Trigger bleibt als reine Beschriftung stehen — ohne Chevron,
   ohne Hover/Active-Zustand, ohne Klickbarkeit (togglePositionDropdown()
   steigt für readonly früh aus, js/app.js). */
.position-selector-static {
	cursor: default;
}
.position-selector-static .position-chevron {
	display: none;
}
@media (hover: hover) and (pointer: fine) {
	.position-selector-static:hover {
		color: var(--t1);
	}
}
/* Gleiche Grösse wie .card-title/.milestone-tab (css/tables.css) — das
   Positions-Label ist inhaltlich die Headline der Topbar, seit die
   Kartenüberschriften sticky sind soll die klebende Topbar optisch gleich
   gross wirken wie eine darunter klebende Kartenüberschrift (15.09.2026). */
.position-symbol {
	font-size: var(--fs-heading);
	font-weight: 300;
	font-family: var(--fm);
}
.position-name {
	font-size: var(--fs-hint);
	color: var(--t3);
}
.position-chevron {
	font-size: var(--fs-position-chevron);
	color: var(--t3);
}

/* Kurs+Veränderung, ersetzt #position-selector per Crossfade sobald die
   Live-Kurs-KPI-Kachel (.kpi-price) beim Scrollen aus dem sichtbaren Bereich
   verschwindet (js/topbar-price-crossfade.js). Klick löst wie die KPI-Kachel
   selbst einen Kurs-Refresh aus (js/topbar-price-crossfade.js) - pointer-
   events daher nur im AUSGEBLENDETEN Zustand none (unten), damit während der
   Crossfade-Übergänge nichts Unsichtbares klickbar ist. */
.topbar-price-live {
	position: absolute;
	top: 0;
	left: 0;
	height: 100%;
	display: flex;
	align-items: baseline;
	gap: 8px;
	white-space: nowrap;
	opacity: 0;
	pointer-events: none;
	/* Gegenstück zu .position-selector oben: dort ist die Default-Delay die
	   "wieder einblenden"-Richtung, hier die "ausblenden"-Richtung (Default-
	   Zustand ist unsichtbar) - keine Delay nötig, startet sofort. */
	transition: opacity var(--dur-fast) ease;
}
.topbar-left.topbar-left--price-live .position-selector {
	opacity: 0;
	pointer-events: none;
	transition-delay: 0s;
}
.topbar-left.topbar-left--price-live .topbar-price-live {
	opacity: 1;
	pointer-events: auto;
	cursor: pointer;
	transition-delay: var(--dur-fast);
}
/* Kurswert wie .position-symbol ("TSLA") - dieselbe Grösse, da beide die
   Topbar-Headline sind, nur zeitlich versetzt sichtbar. Betrag+Prozent
   (.kpi-price-chg, js/kpi-price.js/css/kpi.css) bleiben unverändert eine
   Stufe kleiner (--fs-kpi-price-chg) und liefern die p/n-Farblogik der
   Live-KPI-Kachel gleich mit - keine eigene Farbregel hier (AGENTS.md: keine
   Logik duplizieren, die bereits an einer Stelle existiert). */
.topbar-price-live-value {
	font-size: var(--fs-heading);
	font-weight: 300;
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
	color: var(--t1);
}

@keyframes blink {
	0%,
	100% {
		opacity: 1;
	}
	50% {
		opacity: 0.3;
	}
}

/* Flach: kein Rahmen/Hintergrund am Icon selbst — aktiv/inaktiv/hover nur
   über Textfarbe, siehe design-system.md ("Header ohne Hintergrundflächen"). */
.icon-btn {
	width: 30px;
	height: 30px;
	border: none;
	background: transparent;
	color: var(--t3);
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: var(--fs-icon);
}
@media (hover: hover) and (pointer: fine) {
	.icon-btn:hover {
		color: var(--t1);
	}
}

/* ── ICON BUTTON GROUP ──
   Reine Layout-Gruppierung (Abstand), kein Rahmen/Hintergrund mehr um die
   Gruppe oder zwischen den Buttons — aktiv/inaktiv ausschliesslich über
   Textfarbe (used by .topbar-right - shared 1:1 by the device-view topbar
   seit 30.08.2026, kein eigenes .device-topbar-right mehr - and
   .view-switch-group, see css/device-views.css). */
.icon-btn-group {
	display: flex;
	align-items: center;
	gap: 12px;
}
.icon-btn-group .icon-btn.active {
	color: var(--t1);
}
/* Nur der Feiertags-Trigger: bekommt bei geöffnetem Dropdown zusätzlich
   einen Hintergrund (--bg-hover), analog zu .position-selector.active —
   "Dropdown-Trigger ist hinterlegt, solange sein Panel offen ist" gilt als
   Prinzip für ALLE Dropdown-Trigger (Nutzerwunsch 18.08.2026), nicht nur
   für Textfarbe wie beim generischen .icon-btn.active oben (das bleibt für
   #settings-btn unverändert — dort markiert .active den aktuell offenen
   Tab, kein Dropdown, andere Semantik). Beide Trigger (#holidays-btn +
   device-view mirror #device-holidays-btn, js/app.js toggleHolidaysDropdown())
   - ID-Selektor statt Klasse, da .icon-btn.active oben sonst überall nur
   Textfarbe will, nicht diesen Extra-Hintergrund. */
#holidays-btn.active,
#device-holidays-btn.active,
/* R336: Glocke nachgezogen - beim Bau des Push-Dropdowns (R330) wurde das
   Hinterlegungs-Prinzip übersehen, das Icon blieb als einziger Trigger ohne
   Hintergrund. Sitzt wie die Feiertags-Glocke in .icon-btn-group, braucht die
   Textfarbe also NICHT selbst (anders als .intraday-controls-trigger unten). */
#push-btn.active,
#device-push-btn.active {
	background: var(--bg-hover);
}
/* Gleiches Prinzip für die beiden neuen responsiven Header-Dropdown-Trigger
   (View-Switch-Trigger + Hamburger-Menü, css/device-views.css) - Trigger
   bleibt hinterlegt, solange sein Panel offen ist. */
.view-switch-trigger.active,
.hamburger-btn.active {
	background: var(--bg-hover);
}
/* Gleiches Prinzip für den Chart-Einstellungen-Trigger (Nutzerwunsch
   10.09.2026, js/intraday-controls.js): sitzt nicht in der Topbar, sondern
   unter dem Intraday-Chart, ist aber ein Dropdown-Trigger wie die anderen
   vier und folgt deshalb demselben Hinterlegungs-Prinzip. Anders als die vier
   oben braucht er die Textfarbe (--t3 -> --t1) hier selbst mit - er steckt
   nicht in .icon-btn-group, bekommt die Farbe also nicht von der generischen
   Regel oben mitgeliefert (Nutzerfund 10.09.2026: Hintergrund allein reichte
   nicht, das Icon blieb grau). */
.intraday-controls-trigger.active {
	background: var(--bg-hover);
	color: var(--t1);
}

/* ── HAMBURGER-ICON MORPH (Hamburger → X beim Öffnen) ──
   Drei CSS-Balken statt Icon-Font-Glyph (ti-menu-2) - ein Icon-Font kann
   nicht zwischen zwei Glyphen morphen, nur hart umspringen (Nutzerwunsch
   02.09.2026: sichtbare Animation). Reines CSS, .active kommt bereits von
   toggleHamburgerDropdown()/closeHamburgerDropdown() (js/app.js) - kein
   JS-Zusatz nötig (siehe "CSS vor JS wo möglich"-Regel oben in dieser
   Datei/AGENTS.md). background:currentColor übernimmt automatisch die
   bestehende .icon-btn-group .icon-btn.active-Farblogik (--t3 → --t1). */
.hamburger-icon {
	position: relative;
	width: 16px;
	height: 12px;
}
.hamburger-icon span {
	position: absolute;
	left: 0;
	width: 100%;
	height: 2px;
	background: currentColor;
	border-radius: 1px;
	transition: top 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
}
.hamburger-icon span:nth-child(1) {
	top: 0;
}
.hamburger-icon span:nth-child(2) {
	top: 5px;
}
.hamburger-icon span:nth-child(3) {
	top: 10px;
}
.hamburger-btn.active .hamburger-icon span:nth-child(1) {
	top: 5px;
	transform: rotate(45deg);
}
.hamburger-btn.active .hamburger-icon span:nth-child(2) {
	opacity: 0;
}
.hamburger-btn.active .hamburger-icon span:nth-child(3) {
	top: 5px;
	transform: rotate(-45deg);
}

/* ── HOLIDAY COUNTDOWN BADGE ──
   Small red counter pinned to the top-right corner of the holidays icon
   itself (deliberately a countdown only, no name/date - the user checks
   which holiday it is in the existing dropdown). Shown only when the next
   FULL closure is ≤7 days out (js/app.js updateHolidayBadge()); becomes a
   pulsing "!" (reuses the existing @keyframes blink above, same animation
   as the live-price status dot in css/kpi.css) on the day itself. */
.icon-btn-badge-host {
	position: relative;
}
.holiday-badge {
	position: absolute;
	top: 0px;
	right: -2px;
	min-width: 14px;
	height: 14px;
	padding: 0 3px;
	border-radius: 50%;
	background: var(--badge-neg);
	color: var(--white);
	font-size: 9px;
	font-weight: 600;
	line-height: 14px;
	text-align: center;
	font-family: var(--fm);
	pointer-events: none;
}
.holiday-badge-today {
	animation: blink 2s infinite;
}

/* ── API HEALTH WARNING DOT ──
   Same corner-badge box as .holiday-badge above (identical size/position, so
   the two read as one consistent badge system) pinned to the settings-cog
   icon — shown with a static "!" (no count, unlike the holiday badge) when
   at least one backend data-source API has crossed its consecutive-failure
   threshold (api_health_degraded(), api/lib/api_usage.php; js/app.js
   updateApiHealthDot()). */
.api-health-badge {
	position: absolute;
	top: 0px;
	right: -2px;
	min-width: 14px;
	height: 14px;
	padding: 0 3px;
	border-radius: 50%;
	background: var(--badge-neg);
	color: var(--white);
	font-size: 9px;
	font-weight: 600;
	line-height: 14px;
	text-align: center;
	font-family: var(--fm);
	pointer-events: none;
	/* Always pulsing while shown (unlike .holiday-badge-today, which only
	   pulses on the day itself) — an active API outage is urgent every time
	   the badge is visible, not just on a specific day. */
	animation: blink 2s infinite;
}

/* ── NAV TABS ── */
/* Keine Trennlinie mehr unter der Leiste, aktiv/inaktiv nur über Textfarbe
   (kein Underline/Hintergrund) — siehe design-system.md. */
.navtabs {
	display: flex;
	gap: 20px;
	/* min-height statt height (15.09.2026, gleicher Grund wie .topbar oben):
	   .tab nutzt jetzt --fs-heading, dessen Max deutlich über --nav-h liegt —
	   eine feste height hätte die Zeile gestaucht/abgeschnitten. --chrome-h
	   (Sticky-Haftpunkt für .card-h) misst weiterhin --nav-h als Basiswert,
	   siehe Kommentar dort — bei normaler Zeilenhöhe wächst .navtabs real
	   kaum über --nav-h hinaus, align-items:center zentriert den Rest. */
	min-height: var(--nav-h);
	/* Kein eigener Hintergrund — siehe .topbar oben, die Fläche liegt am
	   gemeinsamen .chrome-stack. */
	background: transparent;
	padding-block: 0;
	/* --page-pad-safe-left/-right statt --page-pad (08.09.2026, gefunden per
	   grep beim Safe-Area-Umbau) - kein padding-top nötig, die Leiste sitzt
	   unterhalb der .topbar, die den oberen Inset bereits abdeckt; seitlich
	   ist sie aber genauso von Landscape-Notch-Insets betroffen wie .topbar. */
	padding-inline: var(--page-pad-safe-left) var(--page-pad-safe-right);
	overflow-x: auto;
}
/* Gleiche Grösse wie .card-title/.milestone-tab (css/tables.css), analog zu
   .position-symbol oben — das Tab-Label ist inhaltlich die Headline der
   jeweiligen Seite (15.09.2026). */
.tab {
	font-size: var(--fs-heading);
	font-weight: 300;
	color: var(--t3);
	padding: 0;
	border: none;
	background: transparent;
	white-space: nowrap;
}
.tab.active {
	color: var(--t1);
}
@media (hover: hover) and (pointer: fine) {
	.tab:hover:not(.active) {
		color: var(--t2);
	}
}

/* ── PAGE CONTAINER ── */
/* .page-full and .page are identical; two class names are kept because call
   sites use them semantically (full-width KPI/overview pages vs. regular
   tab pages) even though the layout itself doesn't currently differ.
   --gap-group between top-level sections (KPI strip, Teslanaire block,
   Tage/CAGR row, …) is the main structural expression of "Weissraum als
   Gruppierungssignal" — see design-system.md. */
.page,
.page-full {
	/* Lokale Custom Property statt zweimal denselben clamp()-Wert
	   auszuschreiben (code-reviewer-Nitpick, 08.09.2026) - ein künftiges
	   Ändern nur einer der beiden Stellen könnte sonst Top-/Bottom-Padding
	   sonst unbemerkt auseinanderlaufen lassen. */
	--page-block-pad: clamp(1.25rem, 1rem + 1vw, 2rem);
	padding-top: var(--page-block-pad);
	/* Home-Indicator-Schutz: max() statt padding-block, damit die Bodenkante
	   nicht dichter am Indicator sitzt als bisher am Bildschirmrand (08.09.2026). */
	padding-bottom: max(var(--page-block-pad), var(--safe-bottom));
	padding-inline: var(--page-pad-safe-left) var(--page-pad-safe-right);
	display: flex;
	flex-direction: column;
	gap: var(--gap-group);
	width: 100%;
}

/* Wrapper um alles ab dem Tageskurs-Chart (js/views/overview.js render()),
   ausschliesslich damit updateLiveValues()' flipBelowKpi() einen Höhen-
   sprung der .kpi-strip darüber (z.B. hasExt-Wechsel beim Rein-/Rausgehen
   aus Regular Hours, .kpi-reg-block in css/kpi.css) per Transform statt
   eines harten Reflow-Sprungs ausgleichen kann. Selbst wieder flex/gap wie
   .page-full, da es dessen direkte Kinder um eine Ebene verschachtelt und
   sonst deren --gap-group-Abstand untereinander verloren ginge. --flip-y
   wird nur von flipBelowKpi() gesetzt (0px im Ruhezustand). */
.page-below-kpi {
	--flip-y: 0px;
	display: flex;
	flex-direction: column;
	gap: var(--gap-group);
	/* Der transform steht bewusst NICHT hier, sondern nur unter .is-flipping:
	   ein transform - auch translateY(0px) - macht dieses Element dauerhaft zu
	   einem eigenen, kompositierten Layer mit eigenem Stacking-Kontext. Genau
	   darauf zeigte die Nutzerbeobachtung 07.09.2026: die Chevrons der
	   Tages-Navigation (css/kpi.css) liegen in diesem Wrapper und blieben in
	   Safari nach dem Weghovern sichtbar, obwohl ihr opacity rechnerisch
	   bereits 0 war. Im Ruhezustand ist der Wrapper jetzt ein ganz normaler
	   Block - die Navigation liegt damit effektiv ausserhalb des Layers, ohne
	   dass sie dafür aus dem Container heraus verschoben werden musste (was
	   den Chart aus der Gleitanimation genommen hätte). Der Layer entsteht nur
	   noch für die Dauer der Animation, gesetzt/entfernt von flipBelowKpi()
	   (js/views/overview.js). */
}
.page-below-kpi.is-flipping {
	transform: translateY(var(--flip-y));
	/* 1s statt der kurzen var(--dur-base) der KPI-Roll-Ziffern (css/kpi.css) - ein
	   Layout-Sprung mit spürbarer Masse (mehrere hundert Pixel Inhalt)
	   braucht länger, um als sanftes Gleiten statt als Zucken wahrgenommen
	   zu werden. cubic-bezier(0.16, 1, 0.3, 1) ("ease-out-expo"): schneller
	   Start, langes, weiches Ausklingen - fühlt sich eher wie ein
	   natürliches Einpendeln an als das kurze/knackige (0.22, 1, 0.36, 1)
	   der Ziffern-Rolle (Nutzerfeedback 20.08.2026: Animation kaum sichtbar,
	   danach var(--dur-base)→0.5s→1s auf Nutzerwunsch). */
	transition: transform var(--dur-slow) var(--ease-out-expo);
}
.page-below-kpi.flip-instant {
	transition: none;
}

