/* ── FULL WIDTH CARD ── */
.full-width {
	width: 100%;
}

/* ── CARDS ── */
/* Flat: no card surface/border — a "card" is just a section of the page that
   shares the page's own background, separated from its neighbors by
   whitespace (--gap-group between sections in .page-full) rather than a box.
   See memory-bank/design-system.md. */
.card {
	/* clip statt hidden: hidden macht .card selbst zu einem Scroll-Container
	   und damit zum Haftpunkt für jedes position:sticky-Kind — eine klebende
	   Kartenüberschrift würde dann an der Karte kleben statt am eigentlichen
	   Scroller (#main-app). clip beschneidet identisch, ohne Scroll-Container
	   zu sein. */
	overflow: clip;
	/* Gehört zwingend zu `overflow: clip` dazu. Ein Scroll-Container (was
	   `hidden` erzeugt, `clip` aber nicht) bekommt als automatische
	   Mindestgrösse 0 — die Karte durfte damit unter ihre Inhaltsbreite
	   schrumpfen und den Rest beschneiden. Unter `clip` galt wieder
	   `min-width: auto`, also die inhaltsbasierte Mindestbreite: die
	   CAGR-Projektion (acht Prozent-Tabs in einer Flex-Reihe, zusammen ~559px)
	   riss als Grid-Element die ganze Spalte auf und schob im schmalen Fenster
	   sich selbst UND die Nachbarkarte über den Seitenrand hinaus. */
	min-width: 0;
}
/* Jede Kartenüberschrift bleibt beim Scrollen unter der Kopfleiste stehen,
   solange ihre Karte im Bild ist. Bewusst als Grundverhalten und nicht als
   Opt-in-Modifier: eine Auswahl einzelner Karten wirkt beim Scrollen
   willkürlich, weil nicht erkennbar ist, warum die eine Überschrift stehen
   bleibt und die nächste nicht.
   Blickdicht (--bg), wie die Kopfleiste. Zwei Alternativen wurden am
   11.09.2026 ausprobiert und wieder verworfen: eine durchscheinende
   Glasfläche (backdrop-filter wirkt nur pro Element, zwei getrennte Flächen
   verschmelzen nie zu einer durchgehenden — an der Kante zur Kopfleiste bleibt
   ein sichtbarer Bruch, und überlagern sich beim Scrollen zwei Kartenköpfe,
   verdoppelt sich die Tönung) und gar kein Hintergrund (der wegscrollende
   Inhalt bleibt dann hinter der Überschrift sichtbar).
   Der Abstand nach unten ist padding, nicht margin — ein Rand scrollt beim
   Kleben als Lücke mit und liesse den Inhalt darunter durchscheinen; als
   Polsterung gehört er zur deckenden Fläche. */
.card-h {
	/* Luft nach oben, damit die klebende Überschrift denselben optischen
	   Abstand zur Tab-Reihe hält wie diese zum Portfolionamen darüber (~17px):
	   Topbar und Navtabs zentrieren ihren Text jeweils in ihrer Zeilenhöhe und
	   erzeugen dadurch von sich aus Raum, eine Überschrift ohne padding-top
	   sässe dagegen bündig an der Unterkante der Leiste. */
	padding-block: 0.7rem 1rem;
	padding-inline: 0;
	display: flex;
	/* flex-wrap, damit ::after unten (flex-basis:100%) als eigene Zeile
	   INNERHALB von .card-h umbricht, statt als drittes Flex-Item neben Titel
	   und Aktions-Buttons zu stehen. */
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	position: sticky;
	/* Haftpunkt ist die Unterkante der Kopfleisten-Gruppe. Explizit gesetzt
	   statt über scroll-padding-top des Scrollers: scroll-padding wirkt nur
	   auf scrollIntoView/scroll-snap, nicht auf den Haftpunkt von sticky. */
	top: var(--chrome-h);
	/* Unterhalb von --z-sticky: nie über der Kopfleiste oder einem Dropdown. */
	z-index: var(--z-sticky-card);
	/* KEIN background hier mehr — der sitzt jetzt auf ::before (unten) und
	   deckt bewusst nicht die volle Box, sondern spart die letzten
	   --card-h-fade-h aus. Ein background direkt auf .card-h würde die ganze
	   Box inkl. der ::after-Verlaufszeile blickdicht einfärben, wodurch der
	   Verlauf nur noch gegen seine eigene Farbe fadet statt gegen echten
	   durchscrollenden Content dahinter — optisch nicht von hart abgeschnitten
	   zu unterscheiden, egal wie schwach seine Deckkraft eingestellt ist. */
}
/* Blickdichte Fläche NUR für die Titel-Zeile, nicht für die Verlauf-Zeile
   ganz unten (bottom: var(--card-h-fade-h) spart genau deren Höhe aus). Erst
   dieser Ausschluss macht den Verlauf unten sichtbar wirksam: was durch ihn
   hindurchscheint, ist jetzt tatsächlich der echte, darunter durchscrollende
   Content — nicht wie zuvor .card-h's eigener, bereits opaker Hintergrund. */
.card-h::before {
	content: "";
	position: absolute;
	inset: 0;
	bottom: var(--card-h-fade-h);
	background: var(--bg);
	pointer-events: none;
	/* Ohne z-index malt sich ein positioniertes ::before trotz früherer
	   DOM-Position ÜBER normalen, nicht-positionierten Flow-Content wie
	   .card-title — Stacking-Reihenfolge richtet sich nach Positionierung,
	   nicht nach Quellreihenfolge. .card-h selbst spannt bereits einen
	   eigenen Stacking-Context auf (position:sticky + z-index), -1 bleibt
	   also lokal begrenzt und reisst nichts ausserhalb davon durcheinander. */
	z-index: -1;
}
/* Sanfter Verlauf statt harter Kante, wo klebender Content beim Scrollen
   unter die Überschrift taucht. Bewusst KEIN position:absolute mehr (frühere
   Version) — das nahm keinen Platz im Fluss ein und lag deshalb immer über
   dem nächsten Element, unabhängig von Höhe/Versatz/Deckkraft (siehe
   memory-bank/gotchas-css.md). Als normales Flex-Kind mit flex-basis:100%
   wächst .card-h stattdessen selbst um --card-h-fade-h — der danach folgende
   Content startet dadurch automatisch erst nach dieser Zeile, ganz ohne
   Extra-Regel für „das jeweils nächste Element". Standardmässig unsichtbar
   (opacity:0) — nur `.is-stuck` (js/sticky-headers.js, per Rect-Vergleich
   gesetzt sobald die Überschrift tatsächlich am Haftpunkt klebt) blendet ihn
   ein. Ohne diese Gate zeigte der Verlauf sich permanent auch in der
   normalen, nicht-klebenden Position. */
.card-h::after {
	content: "";
	flex-basis: 100%;
	order: 1;
	height: var(--card-h-fade-h);
	/* Startfarbe voll opak (var(--bg)), NICHT gedämpft (color-mix, früherer
	   Zwischenstand) — die Spitze grenzt direkt an ::before an, das dort noch
	   voll opak ist. Eine gedämpfte Startfarbe erzeugte dort einen sichtbaren
	   Sprung (100% → 55% Deckkraft) statt eines nahtlosen Verlaufs; wirkte wie
	   eine harte Kante mit angehängtem (kaum wahrnehmbarem) Fade statt einem
	   durchgehenden Verlauf. Die Deckkraft-Dämpfung war nur nötig, solange der
	   Verlauf noch über echtem, gerade sichtbarem Text lag (frühere,
	   absolut-positionierte Version) — hier liegt er stattdessen über
	   Content, der tatsächlich unter die Kopfzeile scrollt. */
	background: linear-gradient(to bottom, var(--bg), transparent);
	pointer-events: none;
	opacity: 0;
}
.card-h.is-stuck::after {
	opacity: 1;
}
/* "Ordentliche" Überschrift statt kleinem Versal-Label — grösser, normale
   Gross-/Kleinschreibung, kein Tracking. Siehe memory-bank/design-system.md. */
.card-title {
	font-size: var(--fs-heading);
	font-weight: 300;
	letter-spacing: -0.01em;
	color: var(--t1);
}
.card-h-tabs {
	/* padding statt margin, aus demselben Grund wie bei .card-h oben — und
	   derselbe obere Abstand, damit auch diese Variante beim Kleben im
	   gleichen Rhythmus zur Leiste steht. */
	padding: 0.7rem 0 1.4rem;
}
.card-h-actions {
	display: flex;
	gap: 8px;
}

/* ── MILESTONE TABS ── */
.milestone-tabs {
	display: flex;
	width: 100%;
}
/* Flach: kein Hintergrund, kein Rahmen/Divider zwischen den Tabs, kein
   Padding-Inline (Label beginnt bündig am Rand), linksbündig statt zentriert.
   Aktiv/inaktiv nur über Textfarbe. Gleiche Grösse wie .card-title, da das
   Tab-Label inhaltlich die Überschrift dieses Blocks ist.
   08.09.2026 (Nutzerfund: "macht einen Sprung statt sauber mit clamp kleiner
   zu werden"): css/chrome.css hatte bei max-width:900px einen harten
   Override (font-size:var(--fs-2), fixes padding-block/-inline), der die
   fluide --fs-heading-Skalierung hier komplett aushebelte - Breakpoint-
   Sprung statt sauberem Clamp-Verlauf, plus abweichendes Padding.
   Entfernt - --fs-heading ist bereits fluid bis 402px kalibriert, kein
   zusätzlicher Breakpoint nötig (Projektregel "Fluid typography via
   clamp() throughout"). */
.milestone-tab {
	position: relative;
	flex: 1;
	display: block;
	text-align: left;
	padding: 0 0 0.9rem;
	font-size: var(--fs-heading);
	font-weight: 300;
	letter-spacing: -0.01em;
	color: var(--t3);
	background: transparent;
	border: none;
	cursor: pointer;
	transition: color var(--dur-fast);
}
.milestone-tab.active {
	color: var(--t1);
}
@media (hover: hover) and (pointer: fine) {
	.milestone-tab:hover:not(.active) {
		color: var(--t2);
	}
}
/* ── TESLANAIRE: EDIT-UI (R322, 08.09.2026; Layout korrigiert 09.09.2026) ──
   Gleiches Muster wie die CAGR-Projektion (.cagr-proj-tab-add/-del,
   .cagr-proj-edit-btn) - hier auf .milestone-tab statt .cagr-proj-tab, da die
   Tesla­naire-Tabs aus historischen Gründen ihr eigenes, flex:1-basiertes
   Layout haben (jeder echte Tab teilt sich die volle Breite).
   Nutzerkorrektur 09.09.2026 (Screenshot): der Tab/Balken soll UNVERÄNDERT
   die volle 100%-Breite nutzen, das Stift-Icon liegt bewusst als Overlay
   darüber, statt der Tab-Zeile ihm Platz abzutreten - ein vorheriger Versuch
   (.milestone-tabs auf flex:1 verkleinert) liess eine sichtbare Lücke
   zwischen Balkenende und Icon entstehen, genau das wollte der Nutzer nicht.
   .card-h-tabs ist über .card-h ohnehin positioniert (sticky), das Icon wird absolut am
   rechten Rand positioniert - oben statt vertikal zentriert (Nutzerwunsch
   09.09.2026, mehr Abstand zu den grünen Balken am unteren Tab-Rand) -
   .milestone-tabs bleibt unangetastet bei width:100%. */
/* KEIN `position: relative` hier — .card-h ist `position: sticky`, und diese
   Regel steht später bei gleicher Spezifität, würde sticky also überschreiben.
   Die Überschrift behielte dabei das geerbte `top: var(--chrome-h)`, das unter
   `relative` nicht mehr Haftpunkt ist, sondern eine Verschiebung um denselben
   Betrag nach unten: die Kartenüberschrift rutscht über ihren eigenen Inhalt.
   Für das absolut positionierte Stift-Icon unten ist ohnehin kein `relative`
   nötig — `sticky` ist ein positioniertes Element und stellt denselben
   Bezugsrahmen bereit. */
.milestone-tabs-edit-btn {
	position: absolute;
	right: 0;
	/* Am oberen Rand statt vertikal zentriert (Nutzerwunsch 09.09.2026) - mehr
	   Abstand zu den grünen Fortschrittsbalken, die am UNTEREN Rand des Tabs
	   sitzen (.milestone-tab-bars, bottom:0). */
	top: 0;
	opacity: 0;
	transition: opacity var(--dur-fast);
}
.milestone-tabs-edit-btn.active {
	opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
	.card-h-tabs:hover .milestone-tabs-edit-btn {
		opacity: 1;
	}
}
/* Das "+"-Hinzufügen-Element bleibt Kind von .milestone-tabs (Teil der
   Tab-Reihe, wie die echten Tabs) und darf nicht mitwachsen, sonst würde es
   beim Ein-/Ausblenden die Breite der echten Tabs verschieben. Es landet
   dadurch (echte Tabs sind flex:1 und füllen den Rest) am rechten Rand
   von .milestone-tabs - GENAU dort, wo das Stift-Icon als Overlay sitzt
   (Nutzerfund 09.09.2026, Screenshot: beide Icons überlagerten sich). Fix:
   solange der Edit-Modus aktiv ist (Stift-Icon trägt dann .active),
   tritt .milestone-tabs seinen Platz am rechten Rand wieder ans Stift-Icon
   ab (zurück in den normalen Flex-Fluss, kein Overlay mehr) - im Edit-Modus
   ist ohnehin schon mehr los (Lösch-Icons, Formular), das Zurückweichen der
   Tab-Zeile fällt dort nicht ins Gewicht. Nur im RUHEZUSTAND (kein Edit)
   bleibt die Tab-Zeile bei voller 100%-Breite mit dem reinen Overlay-Icon -
   genau das, was der Nutzer für die Normalansicht wollte. */
.card-h-tabs:has(.milestone-tabs-edit-btn.active) .milestone-tabs {
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
}
.card-h-tabs:has(.milestone-tabs-edit-btn.active) .milestone-tabs-edit-btn {
	position: static;
	flex: none;
	margin-left: 0.6rem;
	/* .card-h-tabs zentriert seine Flex-Kinder vertikal (align-items:center,
	   von .card-h geerbt) - im Ruhezustand (position:absolute, top:0) ist das
	   irrelevant, im Edit-Modus (jetzt ein normales Flex-Kind) würde das
	   Icon dadurch mittig statt oben landen und nicht mehr auf gleicher Höhe
	   wie der "+"-Button/die Tab-Labels sitzen (beide oben ausgerichtet, kein
	   vertical-centering - Nutzerfund 09.09.2026, Screenshot). */
	align-self: flex-start;
}
.milestone-tab-add {
	/* Eigene Box statt der von .milestone-tab geerbten (grosser --fs-heading-
	   Font, Block-Layout mit padding-bottom für die Balken-Reserve) - dieser
	   Button hat keine Balken und soll optisch wie ein normales .icon-btn
	   neben dem Stift-Icon wirken, nicht wie ein Tab-Label (Nutzerfund
	   09.09.2026: sass sonst spürbar höher als das Stift-Icon daneben). */
	flex: none;
	align-self: flex-start;
	margin-left: 0.6rem;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	padding: 0;
	font-size: var(--fs-icon);
	color: var(--t3);
	font-weight: 300;
}
.milestone-tab-del {
	color: var(--neg);
	font-size: 0.85em;
	margin-left: 0.4rem;
}
@media (hover: hover) and (pointer: fine) {
	.milestone-tab-del:hover {
		color: var(--neg);
		filter: brightness(1.3);
	}
}
.teslanaire-add-form {
	flex: none;
	display: flex;
	align-items: baseline;
	gap: 0.4rem;
}
.teslanaire-input-label {
	width: 6em;
}
/* ── MILESTONE TAB PROGRESS LINES ── */
/* Thin, unlabeled lines flush to the tab's edges — one per currency ($/€/CHF). */
.milestone-tab-bars {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	display: flex;
	flex-direction: column;
	gap: 1px;
}
.milestone-tab-bar {
	position: relative;
	height: 2px;
	/* No track tint — the unfilled portion is simply the page background
	   (no color set at all), so only the colored fill and the notch (below)
	   are visible. */
	overflow: hidden;
}
/* 1M/2M(/3M/…)-Schwelle zwischen zwei benachbarten Milestone-Tabs: kein
   durchgehend sichtbarer Strich, sondern ein 1px breiter, hintergrund-
   farbener "Unterbruch" genau an der rechten Kante eines Tabs (ausser dem
   letzten — danach folgt keine weitere Schwelle). Da der unbefüllte Bereich
   ohnehin Seitenhintergrund ist, bleibt die Notch unsichtbar, solange die
   Füllung weit von der Schwelle entfernt ist — erst wenn eine Währung nahe
   an ihr eigenes Ziel herankommt, schneidet die Notch eine sichtbare Lücke
   in die durchgehende Farbe. Siehe memory-bank/design-system.md. */
.milestone-tab:not(:last-child) .milestone-tab-bar::before {
	content: '';
	position: absolute;
	top: 0;
	right: 0;
	width: 1px;
	height: 100%;
	background: var(--bg);
	z-index: var(--z-base);
}
/* Zweiter, unabhängiger Notch (gleiche Technik wie oben): markiert innerhalb
   EINES Balkens die Stelle, an der die heutige Bewegung begonnen hat
   (pClose, `computeBarState()` in js/views/overview.js) — unabhängig von der
   1M/2M-Schwelle oben. Ursprünglich ein reiner Zufallsbefund: --pulse-l/-w
   wurden bis 28.08.2026 auf 1 Nachkommastelle gerundet, während --bar-w
   volle Fliesskomma-Präzision behielt; bei einem Verlust-Tag (wo diese
   Kante mit dem äusseren, unbedeckten Rand der Füllung zusammenfällt)
   blitzte dadurch gelegentlich ein Hintergrund-Haarriss durch, je nachdem
   wie die Rundung gerade ausfiel — auf Gewinn-Tagen unsichtbar, da dieselbe
   Kante dort innerhalb der helleren Füllung liegt. Nutzerfeedback
   28.08.2026: der Effekt gefiel ("sieht man schon vor der Animation, wohin
   es geht") — bewusst nachgebaut statt behoben, jetzt IMMER 1px sichtbar
   und in BEIDEN Richtungen (nicht mehr vom Rundungszufall abhängig).
   `.has-day-move` wird nur gesetzt, wenn `hasBracketMovement` true ist
   (kein Notch ohne heutige Bewegung). */
.milestone-tab-bar.has-day-move::after {
	content: '';
	position: absolute;
	top: 0;
	left: var(--today-notch-l);
	width: 1px;
	height: 100%;
	background: var(--bg);
	z-index: var(--z-base);
}
.milestone-tab-bar-fill {
	height: 100%;
	width: var(--bar-w);
	background: var(--bar-color);
	transition: width var(--dur-base);
}
/* Resting background for the day-change slice — SAME treatment regardless of
   direction (an earlier version left the gain side transparent, reasoning
   that the base fill there is already at full brightness so a dark overlay
   would just dim already-achieved progress — explicitly dropped per user
   request: the resting slice must always read as a darkened version of the
   bar's own --bar-color, on top of whatever the base fill is doing
   underneath, gain or loss alike):
     - opaque, DARKENED shade of the bar's own --bar-color (not a generic
       black tint) — without a colored resting shade here this just sat on
       the plain track and read as "nothing here" rather than "this is
       today's specific move, dimmed until its next flash".
   On top of that resting state, a solid block in the bar's own plain
   --bar-color (no tinting) fills up, growing from the left on a gain, from
   the right on a loss, slowly fading out once full before restarting — see
   the pulse keyframes below. --bar-color is set once on the parent
   .milestone-tab-bar and inherited down to both the fill and this
   pseudo-element (the pulse is a sibling of the fill, not a descendant, so
   it has no other way to reach it). Opacity also fades in (0→1) over the
   first 15% instead of appearing at full brightness immediately — without
   that, even a sliver-thin start of the fill already renders at full
   opacity, which reads as a brief flash/spark rather than a gentle
   appearance. Opacity reaches 0 again at 100% and the loop restarts at 0%
   (opacity 0, width back to 0) — the width jump happens while invisible, so
   the loop reset doesn't read as a visible glitch either. */
.milestone-tab-bar-pulse {
	position: absolute;
	top: 0;
	left: var(--pulse-l);
	width: var(--pulse-w);
	height: 100%;
	overflow: hidden;
	/* Shift LIGHTNESS only, keep the bar-color's own chroma/hue intact —
	   NOT color-mix() toward black (crushed light mode's already-dark
	   --pos/--neg/--amber to near-black) NOR toward --bg3 (an achromatic
	   gray/beige — mixing enough of it in to look noticeably lighter also
	   drags chroma down proportionally, reading as a generic washed-out
	   beige/gray rather than "a muted version of THIS color", per user
	   feedback on the --bg3 attempt). A relative oklch lightness shift keeps
	   the hue/saturation fully recognizable while moving lightness toward
	   what each theme's own background needs: LIGHTER for light mode (its
	   --pos/--neg/--amber are already dark, so dimming further would nearly
	   reach black — the resting slice needs to go the other way instead,
	   toward a pale/pastel tint) and DARKER for dark mode (its tokens are
	   already bright, so darkening is what reads as "dimmed" there) —
	   opposite directions per theme, hence --pulse-lighten (positive in
	   :root/light, negative under the dark blocks, see tokens.css) rather
	   than one fixed formula. */
	background: oklch(from var(--bar-color) calc(l + var(--pulse-lighten)) c h);
}
.milestone-tab-bar-pulse::after {
	content: '';
	position: absolute;
	top: 0;
	height: 100%;
	background: var(--bar-color);
}
/* Timing functions: Material Design's "decelerate" (settles into place, used
   for the growing fill — an element arriving/entering) and "accelerate" (speeds
   away, used for the draining fill — an element leaving). Both replace the
   generic ease-in-out, which — applied per keyframe segment — decelerates to a
   near-standstill at EVERY keyframe stop, not just the start/end. That's fine
   when a property only has one segment (as fill's width always has), but it
   visibly stutters wherever a property crosses an intermediate keyframe with
   its own explicit value, since the segment before it eases to ~0 velocity
   right as the next segment eases back up from ~0 — exactly what happened to
   milestone-pulse-drain below before this fix (see its width curve). */
.milestone-tab-bar-pulse.pulse-right::after {
	left: 0;
	animation: milestone-pulse-fill 2.2s cubic-bezier(0, 0, 0.2, 1) infinite;
	/* --pulse-delay: set by syncCrossingPulses()/patchTabBars() (overview.js),
	   phase-locked so a freshly-rebuilt pulse resumes mid-cycle instead of
	   restarting — see pulsePhase() there. During an active threshold-
	   crossing relay, the .relay-first/.relay-second rules below take over
	   instead of this one (higher specificity via the extra class). */
	animation-delay: var(--pulse-delay, 0s);
}
/* Loss: anchored left (not right) and DRAINS from 100% down to 0%, rather than
   growing 0→100% from the right — reads as value receding/emptying out instead
   of just "the same fill-up animation running backwards", which is what a
   right-anchored 0→100% growth (mirrored) would otherwise look like. */
.milestone-tab-bar-pulse.pulse-left::after {
	left: 0;
	animation: milestone-pulse-drain 2.2s cubic-bezier(0.4, 0, 1, 1) infinite;
	animation-delay: var(--pulse-delay, 0s);
}
@keyframes milestone-pulse-fill {
	0%   { width: 0%;   opacity: 0; }
	15%  { opacity: 1; }
	70%  { width: 100%; opacity: 1; }
	100% { width: 100%; opacity: 0; }
}
/* ── CROSSING RELAY: strictly sequential, one continuous pass, same total
   duration as a standalone loop ──
   During a same-day threshold crossing, syncCrossingPulses() (overview.js)
   adds .relay-first to the lower-threshold tab's pulse and .relay-second to
   the higher-threshold tab's pulse (per currency — each currency's own
   crossing is sequenced independently, USD/EUR/CHF don't wait on each
   other). Both share ONE clock, RELAY_DURATION in overview.js — kept equal
   to the standalone loop's own 2.2s (not doubled to 4.4s): a currency mid-
   crossing should take the SAME total time to complete its 1M→2M passage
   as any other currency's single standalone lap, so all three visibly stay
   in the same rhythm instead of USD appearing to take "two laps' worth" of
   time per crossing while EUR/CHF each take one, per user feedback.
   first plays its width-growth during [0%,35%], second during [35%,70%]
   — TOGETHER spanning exactly [0%,70%], matching the STANDALONE fill
   keyframe's own growth window (milestone-pulse-fill above also stops
   widening at 70%, then just holds/fades opacity from 70%→100% — it does
   NOT keep growing all the way to 100%). Splitting the growth 50/50 across
   [0%,100%] instead (an earlier version) made a crossing currency's own
   visible widening motion continue all the way to 100% (2.2s) while
   EUR/CHF's had already visibly stopped growing at 70% (1.54s) and were
   just sitting there fading — reading as "the crossing one is still going
   while the others are already done", exactly per user feedback ("die
   [andere] läuft noch während die anderen fertig sind... beginnen tun sie
   aber wieder gleichzeitig"), even though the underlying CSS durations were
   already identical. Matching the SAME 70%-of-cycle growth-complete point
   fixes that: both "look done" at the same instant.
   BOTH first and second then HOLD at full width/opacity from 70% through
   100% instead of resetting — visually completing ("going back to null")
   only at the very end of the WHOLE cycle, together — per explicit user
   request: first shouldn't disappear the moment its own turn ends, it
   should stay showing "done" while second catches up, and both reset
   together only once second is done too. That also removes the earlier
   fade-out-at-handoff/fade-in-at-handoff dip entirely: the only fade left
   is first's single fade-IN at the very start of a fresh cycle (0%→7.5%)
   and second's own hard cut to visible at 35% (instant, matching first's
   still-full opacity there) — no fade anywhere in the middle of the pass.
   The 100%→0% loop wrap is deliberately a visible, simultaneous hard reset
   for BOTH first and second (unlike the standalone keyframes' own loop
   point, which resets while invisible) — that reset IS the "second
   animation over 1M finished" event the user described.
   Timing function: `linear` for the WHOLE animation, deliberately — first
   and second are two separate DOM elements/bars, but per explicit user
   feedback they're meant to be perceived as ONE continuous pass ("ich sehe
   die Animation als Ganzes, also der Teil unter 1M und der Teil über 1M
   sollen als Ganzes gesehen werden"). An eased curve (tried once, reverted)
   made first decelerate INTO its own stop while second — an instant later —
   accelerated away from a standstill, so the perceived SPEED of the sweep
   visibly changed right at the handoff even though the visibility toggle
   there is already an intentional hard cut. Linear keeps the width-growth
   velocity constant across both halves, so the only abrupt thing at the
   boundary is the (wanted) instant opacity/element handoff, not a speed
   change layered on top of it.

   ASYMMETRY FIX (28.08.2026): milestone-pulse-relay-second-drain used to
   mirror -second-fill's "hidden for the first half, hard-cut visible at the
   handoff" shape — correct for -fill (the 2M bracket really was at 0% before
   an upward crossing, so starting invisible/empty is accurate) but wrong for
   -drain (the 1M bracket was already at ~100% before a downward crossing —
   it had genuinely been full all day, not empty-and-waiting). Hiding it for
   the first half then popping it to full width at 50% read as "the lower
   segment fills instantly right when the upper one finishes" (reported by
   the user while simulating a date where the gain crossed back below 1M).
   Fixed by keeping second's drain variant visible at width:100%/opacity:1
   from 0%, so it reads as continuously-already-filled while first plays its
   own turn, then drains 100%→0% from 50% exactly as before — no fade, no
   pop, same seamless-single-sweep feel as the fill direction already had. */
.milestone-tab-bar-pulse.pulse-right.relay-first::after {
	left: 0;
	animation: milestone-pulse-relay-first-fill 2.2s linear infinite;
	animation-delay: var(--pulse-delay, 0s);
}
.milestone-tab-bar-pulse.pulse-right.relay-second::after {
	left: 0;
	animation: milestone-pulse-relay-second-fill 2.2s linear infinite;
	animation-delay: var(--pulse-delay, 0s);
}
.milestone-tab-bar-pulse.pulse-left.relay-first::after {
	left: 0;
	animation: milestone-pulse-relay-first-drain 2.2s linear infinite;
	animation-delay: var(--pulse-delay, 0s);
}
.milestone-tab-bar-pulse.pulse-left.relay-second::after {
	left: 0;
	animation: milestone-pulse-relay-second-drain 2.2s linear infinite;
	animation-delay: var(--pulse-delay, 0s);
}
@keyframes milestone-pulse-relay-first-fill {
	0%     { width: 0%;   opacity: 0; }
	7.5%   { opacity: 1; }
	35%    { width: 100%; opacity: 1; }
	100%   { width: 100%; opacity: 1; }
}
@keyframes milestone-pulse-relay-second-fill {
	0%     { width: 0%;   opacity: 0; }
	34.99% { opacity: 0; }
	35%    { width: 0%;   opacity: 1; }
	70%    { width: 100%; opacity: 1; }
	100%   { width: 100%; opacity: 1; }
}
@keyframes milestone-pulse-relay-first-drain {
	0%     { width: 100%; opacity: 0; }
	7.5%   { opacity: 1; }
	50%    { width: 0%;   opacity: 1; }
	100%   { width: 0%;   opacity: 1; }
}
@keyframes milestone-pulse-relay-second-drain {
	0%     { width: 100%; opacity: 1; }
	50%    { width: 100%; opacity: 1; }
	100%   { width: 0%;   opacity: 1; }
}
/* Mirror of milestone-pulse-fill: starts full and shrinks to 0% instead of
   growing, width defined ONLY at 0%/100% (a single continuous segment, exactly
   like fill's) so the accelerate curve above governs the whole shrink without a
   seam. An earlier version added a 55% width:30% checkpoint to keep opacity
   high only while still "thick enough" (avoiding a thin-sliver-at-full-
   brightness flash) — but that turned width into TWO segments, and two
   ease-in-out segments meeting at a shared boundary both ease toward ~0
   velocity right at that seam, then re-accelerate — a visible stutter
   (confirmed numerically: sampling the old two-segment curve at t≈0.5–0.58
   showed velocity per step dropping to ~5% of its peak, vs. the new single
   accelerate curve's velocity rising smoothly and monotonically throughout).
   The accelerate curve solves the ORIGINAL flash concern on its own: it stays
   near its start value for most of the duration and only plunges to 0 right at
   the end, so by the time width is actually thin, the 70%→100% opacity fade
   (below) is already most of the way through — never thin AND fully bright at
   the same time, without needing the extra keyframe. */
@keyframes milestone-pulse-drain {
	0%   { width: 100%; opacity: 0; }
	15%  { opacity: 1; }
	70%  { opacity: 1; }
	100% { width: 0%;   opacity: 0; }
}
/* ── MILESTONE TABLE ROW HOVER ── */
@media (hover: hover) and (pointer: fine) {
	.milestone-tbl tbody tr:hover td {
		background: var(--bg-hover);
		cursor: default;
	}
	.milestone-tbl tbody tr.clickable-row:hover td {
		cursor: pointer;
	}
}

/* ── MILESTONE TABLE ── */
.milestone-tbl {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
	/* Fixed 25%-per-column layout, damit die Spaltenbreite nicht mehr vom
	   Zelleninhalt abhängt (z.B. beim Umschalten Kursgrenze-Modus oder
	   Streak-Tage-Anzeige) und dadurch nicht mehr "springt". */
	table-layout: fixed;
}
.milestone-tbl thead td {
	width: 25%;
	padding: 0 0 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t3);
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.milestone-tbl thead td:first-child {
	text-align: left;
}
.milestone-tbl td {
	width: 25%;
	padding: 0.5rem 0;
	/* Gleiche Grösse wie .stbl/.wtbl/.freq-tbl/.t10 (16.09.2026, Nutzerwunsch)
	   — alle Stat-Tabellen-Bodies der Übersicht sollen gleich gross wirken. */
	font-size: var(--fs-table-body);
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.milestone-tbl tbody tr:last-child td {
	border-bottom: none;
}
.milestone-tbl td:first-child {
	text-align: left;
	color: var(--t2);
	font-family: var(--ff);
}
.milestone-tbl td.lbl {
	color: var(--t2);
	font-family: var(--ff);
}
.milestone-tbl td.mono {
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.milestone-tbl td .p {
	color: var(--pos);
}
.milestone-tbl td .n {
	color: var(--neg);
}
/* Gruppierung über Weissraum statt einer kräftigeren Linie — die group-start-
   Zeile bekommt dieselbe dünne Trennlinie wie jede andere Zeile, nur deutlich
   mehr Abstand darüber. Konsequente Anwendung von "Weissraum als
   Gruppierungssignal" auch innerhalb einer Tabelle, siehe design-system.md. */
.milestone-tbl tr.group-start td {
	padding-top: 1.6rem;
}

/* ── STAT TABLE ROW HOVER ── */
@media (hover: hover) and (pointer: fine) {
	.stbl tbody tr:hover td {
		background: var(--bg-hover);
		cursor: default;
	}
}

/* ── STAT TABLES ── */
.stbl {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
}
.stbl thead td {
	padding: 0 0 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	letter-spacing: 0.07em;
	text-transform: uppercase;
	color: var(--t3);
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.stbl thead td:first-child {
	text-align: left;
}
.stbl-col-h {
	text-align: right;
}
.stbl td {
	padding: 0.5rem 0;
	/* Gleiche Grösse wie .wtbl/.freq-tbl/.t10 (16.09.2026, Nutzerwunsch) —
	   alle Stat-Tabellen-Bodies der Übersicht sollen gleich gross wirken. */
	font-size: var(--fs-table-body);
	border-bottom: 1px solid var(--line);
}
.stbl tbody tr:last-child td {
	border-bottom: none;
}
.stbl td:last-child {
	text-align: right;
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.stbl td.lbl {
	color: var(--t2);
}
.stbl td.p {
	color: var(--pos);
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.stbl td.n {
	color: var(--neg);
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.stbl td.mono {
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.stbl td.p,
.stbl td.n,
.stbl td.mono {
	text-align: right;
}

/* ── ÜBERSICHT-WÄHRUNGSUMSCHALTER ──
   08.09.2026; 09.09.2026 zweimal überarbeitet (Nutzerideen): unter 960px
   zeigen alle USD/EUR/CHF-Tabellen auf der Übersicht (.milestone-tbl, .stbl
   - renderMilestoneTable()/renderCagr()/renderCagrProjection() in
   js/views/overview.js) bei den WERTEN nur noch die aktive Währung,
   synchron über alle drei Tabellen. Die Auswahl passiert über ein
   .currency-picker-Element (drei <label>s) DIREKT ÜBER JEDER Tabelle, an
   der Stelle des früheren Tabellenkopfs - nicht mehr über die Köpfe selbst,
   und nicht als einzelnes Element ganz oben über allen drei Karten (zwei
   verworfene Zwischenstände). Der eigentliche thead wird bei diesem
   Breakpoint komplett ausgeblendet (sonst stünde die aktive Währung
   doppelt: im Picker UND im Tabellenkopf) - jede Body-Zeile hat ohnehin
   ihr eigenes Zeilen-Label.
   Vorgeschichte, warum der Picker nicht mehr in den Tabellenköpfen sitzt:
   zwei Anläufe liessen USD/EUR/CHF direkt in den Köpfen (alle drei
   sichtbar/klickbar) und versuchten nur die Werte-Spalten kollabieren zu
   lassen (table-layout:auto, dann zusätzlich der width:1%-Trick) - beide
   scheiterten in echtem Safari-Test am selben Grundproblem: <table> teilt
   Spaltenbreiten ZEILENÜBERGREIFEND, jede Heuristik gegen eine leere
   Restbreite in einer Spalte wird vom Platzbedarf JEDER anderen Zeile in
   derselben Spalte mitbeeinflusst. Mit dem Picker ausserhalb der
   Spalten-Logik müssen die Tabellen nur noch EINHEITLICH (jede Zeile
   identisch) die inaktiven Spalten per nth-child verstecken - echte 0px,
   ganz ohne Browser-Layout-Heuristiken.
   Die drei Radios (#cur-usd/-eur/-chf, index.html, Kind von
   <main id="content">, AUSSERHALB von #tab-overview) bleiben über jeden
   Live-Refresh hinweg erhalten, weil OverviewView.render() nur
   #tab-overview's Inhalt ersetzt, nie die Radios selbst - kein
   JS-State/localStorage nötig. Der Picker selbst wird dagegen bei jedem
   Render neu erzeugt (Teil von renderMilestoneTable()/renderCagr()/
   renderCagrProjection()s Rückgabe-HTML) - unproblematisch, da er nur ein
   zustandsloser Zeiger auf die stabilen Radios ist, kein eigener Zustand.
   #content:has(#cur-x:checked) steuert von einem gemeinsamen, ebenfalls
   stabilen Vorfahren aus alle drei Tabellen UND alle drei Picker-Kopien
   gleichzeitig. Alle Tabellen-Selektoren unter #tab-overview verankert,
   damit js/views/analysis.js's eigene .stbl-Szenariotabelle (Analyse-Tab)
   unberührt bleibt. Bekannter Trade-off: toggleStreakDaysDisplay() könnte
   die aktive Wert-Spalte hier theoretisch springen lassen - table-layout
   ist unter diesem Breakpoint seit 09.09.2026 ohnehin auf auto gesetzt
   (s.u., Label-Spalte darf sich ausbreiten), das macht die Spaltenbreite
   grundsätzlich content-abhängig - nicht behandelt (Sekundärfeature).
   09.09.2026, Nutzerfund: die generische .stbl-Klasse wird auch von
   renderDays() ("Tage gesamt") für eine simple 2-Spalten-Tabelle ohne
   jeden Währungsbezug benutzt - die obigen Regeln griffen fälschlich auch
   dort und blendeten die einzige Wert-Spalte aus, sobald EUR/CHF aktiv
   war (kein nth-child(3)/(4) vorhanden, das hätte einspringen können).
   Fix: nur renderCagr()/renderCagrProjection() bekommen zusätzlich die
   Klasse .stbl-currency, alle Selektoren unten zielen auf diese statt auf
   die generische .stbl - renderDays() bleibt unberührt. */
.currency-scope {
	/* Visuell versteckt, aber weiterhin per <label> aktivierbar - bewusst
	   NICHT display:none, das würde die Label-Aktivierung in manchen
	   Browsern unzuverlässig machen. */
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
}
.currency-picker {
	display: none;
}
@media (max-width: 960px) {
	#tab-overview .milestone-tbl thead,
	#tab-overview .stbl-currency thead {
		display: none;
	}
	/* .milestone-tbl nutzt table-layout:fixed mit festen 25%-Spalten
	   (css/tables.css, gegen Content-Längen-Jumping bei vollen 4 Spalten).
	   Unter diesem Breakpoint sind aber nur noch 2 Spalten sichtbar (Label +
	   die eine aktive Währung) - die feste 25% liess die Label-Spalte
	   unnötig eng bleiben und mehrzeilig umbrechen, obwohl rechts Platz frei
	   war (Nutzerwunsch 09.09.2026: Label-Spalte soll sich mehr ausbreiten
	   dürfen). Nur hier auf auto zurückgesetzt - die 2-Spalten-Situation ist
	   ohnehin eine bewusste Ausnahme vom 4-Spalten-Normalfall. */
	#tab-overview .milestone-tbl {
		table-layout: auto;
	}
	#tab-overview .milestone-tbl thead td,
	#tab-overview .milestone-tbl td {
		width: auto;
	}
	.currency-picker {
		display: flex;
		justify-content: flex-end;
		gap: 12px;
		padding-block: 0 0.6rem;
	}
	.currency-picker label {
		cursor: pointer;
		font-size: var(--fs-hint);
		font-weight: 400;
		letter-spacing: 0.07em;
		text-transform: uppercase;
		color: var(--t3);
	}
	#content:has(#cur-usd:checked) .currency-picker label[for="cur-usd"],
	#content:has(#cur-eur:checked) .currency-picker label[for="cur-eur"],
	#content:has(#cur-chf:checked) .currency-picker label[for="cur-chf"] {
		color: var(--t1);
	}
	/* Body-Zeilen verstecken die inaktiven Spalten EINHEITLICH per
	   nth-child - jede Zeile ist gleich aufgebaut, deshalb hier keine
	   table-layout-/width-Tricks nötig. */
	#tab-overview .milestone-tbl tbody td:nth-child(2),
	#tab-overview .milestone-tbl tbody td:nth-child(3),
	#tab-overview .milestone-tbl tbody td:nth-child(4),
	#tab-overview .stbl-currency tbody td:nth-child(2),
	#tab-overview .stbl-currency tbody td:nth-child(3),
	#tab-overview .stbl-currency tbody td:nth-child(4) {
		display: none;
	}
	#content:has(#cur-usd:checked) #tab-overview .milestone-tbl tbody td:nth-child(2),
	#content:has(#cur-usd:checked) #tab-overview .stbl-currency tbody td:nth-child(2) {
		display: table-cell;
	}
	#content:has(#cur-eur:checked) #tab-overview .milestone-tbl tbody td:nth-child(3),
	#content:has(#cur-eur:checked) #tab-overview .stbl-currency tbody td:nth-child(3) {
		display: table-cell;
	}
	#content:has(#cur-chf:checked) #tab-overview .milestone-tbl tbody td:nth-child(4),
	#content:has(#cur-chf:checked) #tab-overview .stbl-currency tbody td:nth-child(4) {
		display: table-cell;
	}
}

/* ── 2-COL FULL WIDTH ── */
/* var(--col-gap), NOT --gap-group — the bigger inter-section gap between the
   two halves of a row reads as unwanted empty padding toward the row's
   middle (confirmed in an earlier mockup iteration, see design-system.md). */
.g2-full {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--col-gap);
	width: 100%;
}
/* Zwei unabhängige Card-Sections vertikal gestapelt innerhalb EINER Grid-
   Spalte (z.B. Tage gesamt + Wochentag-Analyse in einer Hälfte von .g2-full,
   Positive/Negative Tage in der anderen) — --gap-group, weil das der
   Abstand zwischen unabhängigen Abschnitten ist (siehe --gap-group-Doku
   oben), nicht --col-gap (das ist für die zwei Hälften EINER Zeile). */
.stack-group {
	display: flex;
	flex-direction: column;
	gap: var(--gap-group);
}

/* ── OTHER GRIDS ── */
.g2 {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--col-gap);
}

/* ── PILLS ── */
.pill {
	display: inline-block;
	font-size: var(--fs-hint);
	font-weight: 400;
	padding: 0.12rem 0.5rem;
	border-radius: var(--radius);
}
.pill-yes {
	background: var(--pos-bg);
	color: var(--pos);
}
.pill-no {
	background: var(--neg-bg);
	color: var(--neg);
}
.pill-wait {
	background: var(--amber-bg);
	color: var(--amber);
}
.pill-info {
	background: var(--blue-bg);
	color: var(--blue);
}

/* ── WOCHENTAG TABLE ROW HOVER ── */
@media (hover: hover) and (pointer: fine) {
	.wtbl tbody tr:hover td {
		background: var(--bg-hover);
		cursor: default;
	}
}

/* ── WOCHENTAG TABLE ── */
.wtbl {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
	font-size: var(--fs-table-body);
}
.wtbl th {
	padding: 0.5rem 0.3rem 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	letter-spacing: 0.07em;
	text-transform: uppercase;
	color: var(--t3);
	border-bottom: 1px solid var(--line);
	text-align: center;
}
.wtbl th:first-child {
	text-align: left;
}
.wtbl td {
	padding: 0.5rem 0.3rem;
	border-bottom: 1px solid var(--line);
	text-align: center;
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
}
.wtbl td:first-child {
	text-align: left;
	font-family: var(--ff);
	font-size: var(--fs-table-body);
	font-weight: 400;
	color: var(--t2);
}
.wtbl tbody tr:last-child td {
	border-bottom: none;
}
.wt-p {
	color: var(--pos);
}
.wt-n {
	color: var(--neg);
}
.wt-d {
	color: var(--t2);
}

/* ── TOP10 TABLE ── */
.t10 {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
}
.t10 th {
	padding: 0 0 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	letter-spacing: 0.07em;
	text-transform: uppercase;
	color: var(--t3);
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.t10 th:first-child {
	text-align: left;
}
.t10 td {
	padding: 0.5rem 0;
	font-size: var(--fs-table-body);
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.t10 td:first-child {
	font-family: var(--ff);
	color: var(--t2);
	font-size: var(--fs-table-body);
	text-align: left;
}
.t10 tbody tr:last-child td {
	border-bottom: none;
}
.t10 td.pp {
	color: var(--pos);
}
.t10 td.nn {
	color: var(--neg);
}
.t10-dim {
	opacity: 0.5;
}

/* ── TOP10 ROW HOVER ── */
@media (hover: hover) and (pointer: fine) {
	.t10 tbody tr:hover td {
		background: var(--bg-hover);
		cursor: default;
	}
}

/* ── TOP10 CURRENT YEAR HIGHLIGHT ── */
.t10 tr.t10-current-year td {
	color: var(--t1);
	font-weight: 400;
}
.t10 tr.t10-current-year td.pp {
	color: var(--pos);
}
.t10 tr.t10-current-year td.nn {
	color: var(--neg);
}

/* ── TOP10 LIVE-ROW PULSE (heutige Zeile, solange intraday 'regular' live
   nachgeführt, siehe $top10_series_live in compute.php) ──
   Opacity-Puls auf der Schrift (wie .kpi-status-dot's blink), KEIN
   Hintergrund-Fill - erster Entwurf (Hintergrund-Farbton via color-mix)
   vom Nutzer verworfen, sollte die Schrift selbst pulsieren, nicht die
   Zeilenfläche. Timing (2.2s) vom Nutzer bestätigt, unverändert. */
.t10 tr.t10-live-pulse td {
	animation: t10-row-pulse 2.2s ease-in-out infinite;
}
@keyframes t10-row-pulse {
	0%, 100% { opacity: 1; }
	50%      { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
	.t10 tr.t10-live-pulse td {
		animation: none;
	}
}

/* ── 5-COL TOP10 BLOCK ── */
.top10-block {
	display: grid;
	grid-template-columns: repeat(5, 1fr);
	/* Gleicher Gap horizontal wie vertikal (Nutzerentscheid 09.09.2026: der
	   Abstand Card-Headline->Content ist bewusst überall gleich gross, s.
	   .card-h - kein Sonderfall für Top10. row-gap:var(--col-gap) statt des
	   deutlich grösseren --gap-stack, sonst wirkt die Headline nach dem
	   Umbrechen der Spalten optisch nur noch mit der ersten Spalte
	   zusammengehörig statt mit der ganzen Card). Wirkt nur, sobald
	   chrome.css' Breakpoints (1200/900/600px) die 5 Spalten umbrechen lassen
	   - bei voller Breite unsichtbar. */
	gap: var(--col-gap);
}
.top10-col-title {
	font-size: var(--fs-heading-sm);
	font-weight: 400;
	letter-spacing: -0.005em;
	color: var(--t1);
	margin-bottom: 0.6rem;
}

/* ── FREQ TABLE ROW HOVER ── */
@media (hover: hover) and (pointer: fine) {
	.freq-tbl tbody tr:hover td {
		background: var(--bg-hover);
		cursor: default;
	}
}

/* ── FREQ TABLE ── */
.freq-tbl {
	width: 100%;
	border-collapse: separate;
	border-spacing: 0;
}
.freq-tbl th {
	padding: 0 0 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	letter-spacing: 0.07em;
	text-transform: uppercase;
	color: var(--t3);
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.freq-tbl th:first-child {
	text-align: left;
}
.freq-tbl td {
	padding: 0.5rem 0;
	font-size: var(--fs-table-body);
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
	border-bottom: 1px solid var(--line);
	text-align: right;
}
.freq-tbl td:first-child {
	color: var(--t2);
	text-align: left;
}
.freq-tbl th.pos,
.freq-tbl td.pos {
	color: var(--pos);
}
.freq-tbl th.neg,
.freq-tbl td.neg {
	color: var(--neg);
}
.freq-tbl tbody tr:last-child td {
	border-bottom: none;
}

/* ── CAGR PROJEKTION: EDIT-BUTTON NEBEN DER ÜBERSCHRIFT ──
   Direkt im Anschluss an den Titeltext statt am rechten Rand der Card-Header-
   Zeile (Nutzerwunsch 25.08.2026) - unsichtbar, bis über die Überschrift
   gehovert wird (oder der Edit-Modus aktiv ist, damit man ihn zum Ausschalten
   wiederfindet, ohne erneut hovern zu müssen). */
.cagr-proj-title {
	display: flex;
	align-items: center;
	gap: 0.4rem;
}
.cagr-proj-edit-btn {
	opacity: 0;
	transition: opacity var(--dur-fast);
}
.cagr-proj-edit-btn.active {
	opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
	.cagr-proj-title:hover .cagr-proj-edit-btn {
		opacity: 1;
	}
}

/* ── CAGR PROJEKTION: SZENARIO-TABS ──
   Ein Tab pro Szenario + "Aktuell" (nicht löschbar), direkt auf der Übersicht
   konfigurierbar (Nutzerwunsch 25.08.2026, siehe js/views/overview.js
   renderCagrProjection()). Kompakter als .milestone-tabs (mehr Tabs möglich,
   horizontal scrollbar statt flex:1 pro Tab), sonst gleiches Flach-Muster:
   kein Hintergrund/Rahmen, nur Textfarbe active/inactive. Editier-Steuerelemente
   (Drag-Handle/Löschen) existieren nur im DOM, wenn der Edit-Modus (Stift-Icon
   neben dem Karten-Titel, Nutzervorgabe 25.08.2026) aktiv ist - dadurch bleibt
   die normale Ansicht kompakt (kein reservierter Platz für Icons, die man dort
   ohnehin nicht braucht). */
/* Höhe/Padding/Border-Bottom sind bewusst identisch zu .stbl thead td
   (Nutzerwunsch 25.08.2026), damit die Trennlinie unter den Tabs exakt auf
   der Höhe der Kopfzeilen-Trennlinie einer nebenstehenden Karte (z.B. "CAGR &
   Investition") liegt, statt leicht versetzt zu wirken. */
.cagr-proj-tabs {
	display: flex;
	/* baseline statt center: das Eingabefeld für ein neues Szenario (Input +
	   Buttons, andere Box-Struktur/Padding als ein .cagr-proj-tab) sass unter
	   center-Alignment auf einer anderen Textgrundlinie als die Tab-Zahlen -
	   in Safari besonders auffällig, da dort <input>/<button> etwas anders
	   vertikal zentriert werden als in Chrome. align-items:baseline ist das
	   dafür vorgesehene, browserübergreifend zuverlässige CSS-Werkzeug -
	   ändert die Gesamthöhe der Zeile nicht, solange .cagr-proj-tab (mit
	   seinem grossen padding-bottom) weiterhin das grösste Element bleibt. */
	align-items: baseline;
	gap: 1.2rem;
	overflow-x: auto;
	margin-bottom: 0.6rem;
	border-bottom: 1px solid var(--line);
	scrollbar-width: none;
}
.cagr-proj-tabs::-webkit-scrollbar {
	display: none;
}
/* Im Edit-Modus etwas mehr Gap, weil Drag-Handle + Lösch-Icon pro Tab dazukommen
   und sonst optisch aneinanderkleben würden. */
.cagr-proj-tabs-edit {
	gap: 1.6rem;
}
.cagr-proj-tab {
	position: relative;
	flex: none;
	display: flex;
	align-items: center;
	gap: 0.3rem;
	/* Ein <button> bekommt vom UA-Stylesheet line-height:normal statt des von
	   <body> ererbten 1.5 (Formularelemente sind von der Vererbung
	   ausgenommen) - eine <td> ist kein Formularelement und erbt 1.5 ganz
	   normal, wodurch dieselben padding/font-size-Werte auf einem Tab-Button
	   trotzdem eine spürbar kleinere Gesamthöhe ergeben als auf .stbl thead td.
	   line-height hier explizit auf denselben Wert wie <body> setzen behebt
	   das robust über alle Viewport-Breiten (keine feste px-Korrektur nötig,
	   die bei anderer Schriftgrösse wieder danebenläge). */
	line-height: 1.5;
	padding: 0 0 0.6rem;
	font-size: var(--fs-table-head);
	font-weight: 400;
	color: var(--t3);
	background: transparent;
	border: none;
	cursor: pointer;
	white-space: nowrap;
	transition: color var(--dur-fast);
}
.cagr-proj-tab.active {
	color: var(--t1);
}
.cagr-proj-tab-add {
	color: var(--t3);
	font-weight: 400;
}
.cagr-proj-tab-del {
	color: var(--neg);
	font-size: 0.85em;
}
@media (hover: hover) and (pointer: fine) {
	.cagr-proj-tab:hover:not(.active) {
		color: var(--t2);
	}
	.cagr-proj-tab-del:hover {
		color: var(--neg);
		filter: brightness(1.3);
	}
}

/* ── DRAG & DROP: generisches Zeilen-Umsortieren (js/drag-reorder.js) ──
   Neutrale, tabellenunabhängige Klassen für Drag&Drop-Zeilen - ursprünglich
   CAGR-Projektion-spezifisch, am 28.08.2026 generalisiert, weil die CAGR-
   Zeilen seither automatisch chronologisch sortieren und kein Drag&Drop mehr
   brauchen. Aktuell nirgends im Live-Code verwendet (nur Styleguide-Demo),
   aber für eine künftige sortierbare Tabelle erhalten. Kein Vorbild im
   Projekt - natives HTML5 Drag&Drop statt Pointer-Events, da kein Live-
   Feedback während des Ziehens nötig ist (nur Drop-Ziel hervorheben). */
.drag-reorder-row {
	cursor: grab;
}
.drag-reorder-handle {
	color: var(--t3);
	cursor: grab;
	font-size: 0.9em;
}
.drag-reorder-dragging {
	opacity: 0.4;
}
/* Zeigt die tatsächliche Einfüge-Semantik von DragReorder.reorderArray()
   (Element landet exakt am Drop-Index - bei Vorwärtsbewegung also NACH, bei
   Rückwärtsbewegung VOR dem bisherigen Nachbarn an dieser Stelle, siehe
   DragReorder.dragOver()) statt eines unspezifischen Rahmens um das ganze
   Element - eine Linie an der Kante, an der das gezogene Element landen wird. */
tr.drag-reorder-over-before td {
	box-shadow: inset 0 2px 0 0 var(--t1);
}
tr.drag-reorder-over-after td {
	box-shadow: inset 0 -2px 0 0 var(--t1);
}

/* ── CAGR PROJEKTION: MITROLLENDES DATUM HINTER JAHRESZAHL ──
   Bei einer Jahres-Zeile (z.B. "5") zeigt dieses gedimmte Datum das daraus
   errechnete Kalenderdatum (State.today() + n Jahre) - wird bei jedem Render
   neu berechnet, "rollt" also automatisch mit dem aktuellen Datum mit,
   statt gespeichert zu werden (siehe dateForYears() in overview.js). */
.cagr-proj-row-date {
	color: var(--t3);
	font-size: 0.85em;
}

/* ── CAGR PROJEKTION: ZEILE LÖSCHEN (Edit-Modus) ── */
.cagr-proj-row-del-cell {
	width: 1.5em;
	text-align: right !important;
}
.cagr-proj-row-del {
	color: var(--neg);
	cursor: pointer;
	font-size: 0.9em;
}
@media (hover: hover) and (pointer: fine) {
	.cagr-proj-row-del:hover {
		filter: brightness(1.3);
	}
}

/* ── CAGR PROJEKTION: JAHR HINZUFÜGEN (Edit-Modus) ──
   Zentrierter Button unter der Tabelle statt Hover-Reveal-Zeile (Nutzerwunsch
   25.08.2026: bei aktivem Edit-Modus ist ohnehin schon klar, dass hier etwas
   editierbar ist - ein Hover-Geheimnis ist dann nicht mehr nötig). */
.cagr-proj-add-form-center {
	display: flex;
	justify-content: center;
	margin-top: 0.75rem;
}
.cagr-proj-add-year-btn {
	width: 28px;
	height: 28px;
	display: flex;
	align-items: center;
	justify-content: center;
	color: var(--t3);
	background: transparent;
	border: none;
	cursor: pointer;
	transition: color var(--dur-fast);
}
@media (hover: hover) and (pointer: fine) {
	.cagr-proj-add-year-btn:hover {
		color: var(--t1);
	}
}
.cagr-proj-input {
	width: 5em;
	padding: 0.2rem 0.4rem;
	font-family: var(--fm);
	font-variant-numeric: tabular-nums;
	font-size: var(--fs-form-label);
	color: var(--t1);
	background: var(--bg);
	border: 1px solid var(--line);
}
.cagr-proj-input[type="date"] {
	width: 9em;
}

/* ── CAGR PROJEKTION: JAHRE/DATUM-UMSCHALTER (Add-Formular) ── */
.cagr-proj-mode-toggle {
	display: flex;
	flex: none;
	margin-right: 0.4rem;
	border: 1px solid var(--line);
	overflow: hidden;
}
.cagr-proj-mode-btn {
	flex: none;
	padding: 0.2rem 0.5rem;
	font-size: var(--fs-cagr-proj-mode);
	color: var(--t3);
	background: transparent;
	border: none;
	cursor: pointer;
	transition: color var(--dur-fast), background-color var(--dur-fast);
}
.cagr-proj-mode-btn:not(:last-child) {
	border-right: 1px solid var(--line);
}
.cagr-proj-mode-btn.active {
	color: var(--t1);
	background: var(--line);
}
.cagr-proj-add-confirm,
.cagr-proj-add-cancel {
	margin-left: 0.4rem;
	padding: 0.1rem 0.5rem;
	font-size: var(--fs-form-label);
	background: transparent;
	border: 1px solid var(--line);
	cursor: pointer;
	color: var(--t2);
}
@media (hover: hover) and (pointer: fine) {
	.cagr-proj-add-confirm:hover {
		color: var(--pos);
		border-color: var(--pos);
	}
	.cagr-proj-add-cancel:hover {
		color: var(--neg);
		border-color: var(--neg);
	}
}
.cagr-proj-add-form {
	display: inline-flex;
	align-items: center;
}
/* Nur das Eingabefeld für ein NEUES SZENARIO sitzt inline in der Tabs-Zeile
   (Nutzerwunsch 25.08.2026: maximal so hoch wie die Tabs-Zeile selbst, sonst
   wächst die Zeile beim Öffnen des Formulars über die mit .stbl thead
   abgestimmte Höhe hinaus). Das Jahr-Eingabefeld (.cagr-proj-add-form-center,
   unter der Tabelle) ist davon bewusst nicht betroffen - dort gibt es keine
   Höhen-Vorgabe. line-height:1.5 (statt einer festen height) matcht
   .cagr-proj-tab und lässt align-items:baseline oben die Textgrundlinie
   sauber ausrichten - eine feste height hätte das kaputt gemacht (Nutzer-
   Feedback 25.08.2026: Baseline stimmte trotz kompakter Höhe nicht, gerade
   in Safari sichtbar). */
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-input {
	padding: 0.05rem 0.3rem;
	font-size: var(--fs-hint);
	line-height: 1.5;
}
/* Chromes Spinner-Pfeile für type=number ragen sonst über die kompakte
   Feldhöhe hinaus und verzerren zusätzlich die Baseline-Berechnung des
   Elements - hier nicht gebraucht (Bestätigen/Abbrechen-Icons daneben
   reichen als Eingabehilfe). */
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-input::-webkit-outer-spin-button,
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-input::-webkit-inner-spin-button {
	-webkit-appearance: none;
	margin: 0;
}
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-input[type="number"] {
	-moz-appearance: textfield;
}
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-add-confirm,
.cagr-proj-add-form:not(.cagr-proj-add-form-center) .cagr-proj-add-cancel {
	padding: 0.05rem 0.4rem;
	font-size: var(--fs-hint);
	line-height: 1.5;
}
.cagr-proj-status {
	margin-top: 0.6rem;
	font-size: var(--fs-hint);
	color: var(--neg);
}
.cagr-proj-status:empty {
	display: none;
}
