/* The notification stack (toast-stack.tsx). Fill and edge come from the floating level in styles/surfaces.css; this file owns the stack's geometry and motion. Collapsed, the newest card stands in front and up to two older ones show a sliver beneath it, each a little smaller, all at the front card's height so the pile reads as one object. Hovering or focusing the stack fans it out into a list at each card's natural height. Base UI supplies the numbers: --toast-index (0 is the front), --toast-height, --toast-offset-y (the heights in front of this card), --toast-frontmost-height, and the swipe offsets. */.app-toast-viewport { --toast-gap: var(--space-4); --toast-icon-size: 16px; --toast-peek: var(--space-4); position: fixed; z-index: var(--z-toast); width: 320px; max-width: calc(100vw - 32px); outline: none;}/* 52px clears the 40px title bar with the same 12px the cards keep from the edge. */.app-toast-viewport[data-position^="top"] { top: 52px; }/* 44px at the bottom keeps the cards off the panels' status row. */.app-toast-viewport[data-position^="bottom"] { bottom: 44px; }.app-toast-viewport[data-position$="right"] { right: var(--space-8); }.app-toast-viewport[data-position$="center"] { right: 0; left: 0; margin-inline: auto; }.app-toast { --toast-dir: 1; /* Each card behind is 5% smaller, down to the third; a fourth waits unseen. */ --toast-scale: calc(1 - min(var(--toast-index), 3) * 0.05); position: absolute; top: 0; right: 0; left: 0; box-sizing: border-box; height: var(--toast-frontmost-height, var(--toast-height)); border-radius: var(--radius-surface); color: var(--text-primary); z-index: calc(10 - var(--toast-index)); cursor: default; user-select: none; /* Scaled about the edge away from the stack's anchor, so a smaller card still shows its full sliver past the one in front. */ transform-origin: 50% 100%; transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) + var(--toast-dir) * var(--toast-index) * var(--toast-peek))) scale(var(--toast-scale)); transition: transform var(--motion-slow), opacity var(--motion-moderate), height var(--motion-moderate), box-shadow var(--motion-fast);}[data-position^="bottom"] > .app-toast { --toast-dir: -1; top: auto; bottom: 0; transform-origin: 50% 0;}/* A card beyond the third sits behind the pile until the stack fans out. */.app-toast:not([data-expanded]) { opacity: calc(1 - max(0, var(--toast-index) - 2)); }/* Fanned out: every card at its own height, one gap apart. The ::after bridges that gap so the pointer can travel between cards without collapsing them. */.app-toast[data-expanded] { height: var(--toast-height); transform: translateX(var(--toast-swipe-movement-x)) translateY(calc(var(--toast-swipe-movement-y) + var(--toast-dir) * (var(--toast-offset-y) + var(--toast-index) * var(--toast-gap))));}.app-toast::after { content: ""; position: absolute; top: 100%; right: 0; left: 0; height: calc(var(--toast-gap) + 1px);}[data-position^="bottom"] > .app-toast::after { top: auto; bottom: 100%; }/* Arriving, a card drops in from the stack's edge; leaving, it lifts back out, or carries on the way it was swiped. The leaving card takes the exit tweens: the slow tier's spring overshoots, and a dismissal must not bounce. */.app-toast[data-starting-style] { opacity: 0; transform: translateY(calc(var(--toast-dir) * -100%)) scale(0.96); }.app-toast[data-ending-style] { opacity: 0; transform: translateY(calc(var(--toast-dir) * -40% + var(--toast-swipe-movement-y))) scale(0.96); transition: transform var(--motion-slow-exit), opacity var(--motion-moderate-exit), height var(--motion-moderate-exit), box-shadow var(--motion-fast-exit);}.app-toast[data-ending-style][data-swipe-direction="right"] { transform: translateX(calc(var(--toast-swipe-movement-x) + 110%)) translateY(var(--toast-swipe-movement-y)); }.app-toast[data-ending-style][data-swipe-direction="up"] { transform: translateY(calc(var(--toast-swipe-movement-y) - 120%)); }.app-toast[data-ending-style][data-swipe-direction="down"] { transform: translateY(calc(var(--toast-swipe-movement-y) + 120%)); }.app-toast[data-limited] { opacity: 0; }.app-toast:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }/* One line, one axis: icon, message and dismiss share a centre. Past one line, centring would float the icon and the dismiss button against the middle of a paragraph, so both pin to the title's line box instead. */.app-toast-content { display: grid; grid-template-columns: var(--toast-icon-size) minmax(0, 1fr) var(--control-size-icon-compact); align-items: center; gap: var(--space-4); box-sizing: border-box; height: 100%; padding: var(--space-5) var(--space-5) var(--space-5) var(--space-6); overflow: hidden; transition: opacity var(--motion-fast);}/* Behind the front card only the sliver shows; the words would be noise. */.app-toast-content[data-behind]:not([data-expanded]) { opacity: 0; }.app-toast.multiline .app-toast-content { align-items: start; }.app-toast.multiline .app-toast-icon { margin-top: calc((var(--type-subtitle-line-height) - var(--toast-icon-size)) / 2); }.app-toast.multiline .app-toast-content > button { margin-top: calc((var(--type-subtitle-line-height) - var(--control-size-icon-compact)) / 2); }.app-toast-icon { display: flex; align-items: center; justify-content: center; height: var(--toast-icon-size); }/* The icon carries the level; the text stays in the primary colour so a long message is read as prose, not as an alarm. */.app-toast.info .app-toast-icon { color: var(--status-info); }.app-toast.success .app-toast-icon { color: var(--status-success); }.app-toast.warning .app-toast-icon { color: var(--status-warning); }.app-toast.error .app-toast-icon { color: var(--status-danger); }.app-toast-body { min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }/* The title is read, not clicked: the subtitle's size and leading at medium weight, a step above the detail's caption. */.app-toast-title { display: -webkit-box; margin: 0; overflow: hidden; font-size: var(--type-subtitle-size); line-height: var(--type-subtitle-line-height); font-weight: var(--weight-medium); overflow-wrap: anywhere; -webkit-box-orient: vertical; -webkit-line-clamp: 3;}.app-toast-description { display: -webkit-box; margin: 0; max-height: calc(4 * var(--type-caption-line-height)); overflow: hidden; color: var(--text-secondary); font-size: var(--type-caption-size); line-height: var(--type-caption-line-height); overflow-wrap: anywhere; white-space: pre-line; -webkit-box-orient: vertical; -webkit-line-clamp: 4;}/* A toast's actions are compact Buttons, the step dense chrome uses. */.app-toast-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); }.app-toast-progress-row { display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-3); }.app-toast-progress { position: relative; flex: 1 1 auto; height: var(--space-2); border-radius: var(--radius-pill); background: color-mix(in srgb, var(--text-primary) 12%, transparent); overflow: hidden;}.app-toast-progress-fill { height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--control-active); transition: width var(--motion-moderate);}/* Indeterminate: a short segment sweeps across until the work can say how far it is. */.app-toast-progress[data-indeterminate] .app-toast-progress-fill { width: 35%; animation: app-toast-sweep 1.4s ease-in-out infinite;}.app-toast-progress-value { flex: none; min-width: 4ch; color: var(--text-secondary); font-size: var(--type-caption-size); line-height: var(--type-caption-line-height); font-variant-numeric: tabular-nums; text-align: right;}@keyframes app-toast-sweep { from { transform: translateX(-100%); } to { transform: translateX(290%); }}
/* The notification stack (toast-stack.tsx). Fill and edge come from the
floating level in styles/surfaces.css; this file owns the stack's geometry
and motion.
Collapsed, the newest card stands in front and up to two older ones show a
sliver beneath it, each a little smaller, all at the front card's height so
the pile reads as one object. Hovering or focusing the stack fans it out
into a list at each card's natural height. Base UI supplies the numbers:
--toast-index (0 is the front), --toast-height, --toast-offset-y (the
heights in front of this card), --toast-frontmost-height, and the swipe
offsets. */
.app-toast-viewport {
--toast-gap: var(--space-4);
--toast-icon-size: 16px;
--toast-peek: var(--space-4);
position: fixed;
z-index: var(--z-toast);
width: 320px;
max-width: calc(100vw - 32px);
outline: none;
}
/* 52px clears the 40px title bar with the same 12px the cards keep from the edge. */
.app-toast-viewport[data-position^="top"] { top: 52px; }
/* 44px at the bottom keeps the cards off the panels' status row. */
.app-toast-viewport[data-position^="bottom"] { bottom: 44px; }
.app-toast-viewport[data-position$="right"] { right: var(--space-8); }
.app-toast-viewport[data-position$="center"] { right: 0; left: 0; margin-inline: auto; }
.app-toast {
--toast-dir: 1;
/* Each card behind is 5% smaller, down to the third; a fourth waits unseen. */
--toast-scale: calc(1 - min(var(--toast-index), 3) * 0.05);
position: absolute;
top: 0;
right: 0;
left: 0;
box-sizing: border-box;
height: var(--toast-frontmost-height, var(--toast-height));
border-radius: var(--radius-surface);
color: var(--text-primary);
z-index: calc(10 - var(--toast-index));
cursor: default;
user-select: none;
/* Scaled about the edge away from the stack's anchor, so a smaller card
still shows its full sliver past the one in front. */
transform-origin: 50% 100%;
transform:
translateX(var(--toast-swipe-movement-x))
translateY(calc(var(--toast-swipe-movement-y) + var(--toast-dir) * var(--toast-index) * var(--toast-peek)))
scale(var(--toast-scale));
transition:
transform var(--motion-slow),
opacity var(--motion-moderate),
height var(--motion-moderate),
box-shadow var(--motion-fast);
}
[data-position^="bottom"] > .app-toast {
--toast-dir: -1;
top: auto;
bottom: 0;
transform-origin: 50% 0;
}
/* A card beyond the third sits behind the pile until the stack fans out. */
.app-toast:not([data-expanded]) { opacity: calc(1 - max(0, var(--toast-index) - 2)); }
/* Fanned out: every card at its own height, one gap apart. The ::after bridges
that gap so the pointer can travel between cards without collapsing them. */
.app-toast[data-expanded] {
height: var(--toast-height);
transform:
translateX(var(--toast-swipe-movement-x))
translateY(calc(var(--toast-swipe-movement-y) + var(--toast-dir) * (var(--toast-offset-y) + var(--toast-index) * var(--toast-gap))));
}
.app-toast::after {
content: "";
position: absolute;
top: 100%;
right: 0;
left: 0;
height: calc(var(--toast-gap) + 1px);
}
[data-position^="bottom"] > .app-toast::after { top: auto; bottom: 100%; }
/* Arriving, a card drops in from the stack's edge; leaving, it lifts back out,
or carries on the way it was swiped. The leaving card takes the exit tweens:
the slow tier's spring overshoots, and a dismissal must not bounce. */
.app-toast[data-starting-style] { opacity: 0; transform: translateY(calc(var(--toast-dir) * -100%)) scale(0.96); }
.app-toast[data-ending-style] {
opacity: 0;
transform: translateY(calc(var(--toast-dir) * -40% + var(--toast-swipe-movement-y))) scale(0.96);
transition:
transform var(--motion-slow-exit),
opacity var(--motion-moderate-exit),
height var(--motion-moderate-exit),
box-shadow var(--motion-fast-exit);
}
.app-toast[data-ending-style][data-swipe-direction="right"] { transform: translateX(calc(var(--toast-swipe-movement-x) + 110%)) translateY(var(--toast-swipe-movement-y)); }
.app-toast[data-ending-style][data-swipe-direction="up"] { transform: translateY(calc(var(--toast-swipe-movement-y) - 120%)); }
.app-toast[data-ending-style][data-swipe-direction="down"] { transform: translateY(calc(var(--toast-swipe-movement-y) + 120%)); }
.app-toast[data-limited] { opacity: 0; }
.app-toast:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }
/* One line, one axis: icon, message and dismiss share a centre. Past one line,
centring would float the icon and the dismiss button against the middle of
a paragraph, so both pin to the title's line box instead. */
.app-toast-content {
display: grid;
grid-template-columns: var(--toast-icon-size) minmax(0, 1fr) var(--control-size-icon-compact);
align-items: center;
gap: var(--space-4);
box-sizing: border-box;
height: 100%;
padding: var(--space-5) var(--space-5) var(--space-5) var(--space-6);
overflow: hidden;
transition: opacity var(--motion-fast);
}
/* Behind the front card only the sliver shows; the words would be noise. */
.app-toast-content[data-behind]:not([data-expanded]) { opacity: 0; }
.app-toast.multiline .app-toast-content { align-items: start; }
.app-toast.multiline .app-toast-icon { margin-top: calc((var(--type-subtitle-line-height) - var(--toast-icon-size)) / 2); }
.app-toast.multiline .app-toast-content > button { margin-top: calc((var(--type-subtitle-line-height) - var(--control-size-icon-compact)) / 2); }
.app-toast-icon { display: flex; align-items: center; justify-content: center; height: var(--toast-icon-size); }
/* The icon carries the level; the text stays in the primary colour so a long
message is read as prose, not as an alarm. */
.app-toast.info .app-toast-icon { color: var(--status-info); }
.app-toast.success .app-toast-icon { color: var(--status-success); }
.app-toast.warning .app-toast-icon { color: var(--status-warning); }
.app-toast.error .app-toast-icon { color: var(--status-danger); }
.app-toast-body { min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
/* The title is read, not clicked: the subtitle's size and leading at medium
weight, a step above the detail's caption. */
.app-toast-title {
display: -webkit-box;
margin: 0;
overflow: hidden;
font-size: var(--type-subtitle-size);
line-height: var(--type-subtitle-line-height);
font-weight: var(--weight-medium);
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.app-toast-description {
display: -webkit-box;
margin: 0;
max-height: calc(4 * var(--type-caption-line-height));
overflow: hidden;
color: var(--text-secondary);
font-size: var(--type-caption-size);
line-height: var(--type-caption-line-height);
overflow-wrap: anywhere;
white-space: pre-line;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
}
/* A toast's actions are compact Buttons, the step dense chrome uses. */
.app-toast-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); }
.app-toast-progress-row { display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-3); }
.app-toast-progress {
position: relative;
flex: 1 1 auto;
height: var(--space-2);
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--text-primary) 12%, transparent);
overflow: hidden;
}
.app-toast-progress-fill {
height: 100%;
border-radius: inherit;
corner-shape: inherit;
background: var(--control-active);
transition: width var(--motion-moderate);
}
/* Indeterminate: a short segment sweeps across until the work can say how far it is. */
.app-toast-progress[data-indeterminate] .app-toast-progress-fill {
width: 35%;
animation: app-toast-sweep 1.4s ease-in-out infinite;
}
.app-toast-progress-value {
flex: none;
min-width: 4ch;
color: var(--text-secondary);
font-size: var(--type-caption-size);
line-height: var(--type-caption-line-height);
font-variant-numeric: tabular-nums;
text-align: right;
}
@keyframes app-toast-sweep {
from { transform: translateX(-100%); }
to { transform: translateX(290%); }
}