@layer components { /* ── Base notice — works inline (auth pages, edit pages) and in toasts ── */ .notice { --app-notice-accent: var(--app-muted-color); position: relative; display: flex; align-items: center; gap: calc(var(--app-spacing) * 0.625); padding: calc(var(--app-spacing) * 0.75) calc(var(--app-spacing) * 0.875); background: var(--app-card-background-color); border: 1px solid var(--app-muted-border-color); border-radius: calc(var(--app-border-radius) * 1.1); color: var(--app-color); font-size: var(--text-sm); line-height: var(--leading-snug); margin-bottom: var(--app-spacing); overflow: hidden; } /* Auto-injected variant icon for notices that don't carry an explicit .notice-icon child (= every inline use site). Toasts emit the icon in markup so they opt out via :has(). */ .notice:not(:has(> .notice-icon))::before { content: ""; flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--app-notice-accent) 16%, transparent); color: var(--app-notice-accent); font-family: "Bootstrap-icons"; font-size: var(--text-base); font-weight: var(--font-regular); line-height: var(--leading-none); } .notice[data-variant=success]:not(:has(> .notice-icon))::before { content: "\f26a"; } .notice[data-variant=info]:not(:has(> .notice-icon))::before { content: "\f430"; } .notice[data-variant=warning]:not(:has(> .notice-icon))::before { content: "\f33a"; } .notice[data-variant=error]:not(:has(> .notice-icon))::before { content: "\f626"; } /* Explicit icon span (used by toasts) — same look as the ::before pseudo. */ .notice .notice-icon { flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--app-notice-accent) 16%, transparent); color: var(--app-notice-accent); font-size: var(--text-base); } /* Message content slot. Inline notices without an explicit wrapper still work because flex layout picks up the next direct child as content. */ .notice .notice-content { flex: 1 1 auto; color: var(--app-color); min-width: 0; overflow-wrap: anywhere; } .notice ul { margin: 0; padding-left: 0; list-style: none; flex: 1 1 auto; min-width: 0; } .notice ul li { list-style: none; margin: 0; } .notice ul li + li { margin-top: calc(var(--app-spacing) * 0.25); } .notice p { margin: 0; flex: 1 1 auto; min-width: 0; } /* Variant accent — drives the icon-pill color. Text stays neutral. */ .notice[data-variant=success] { --app-notice-accent: var(--app-notice-success-color); } .notice[data-variant=warning] { --app-notice-accent: var(--app-notice-warning-color); } .notice[data-variant=info] { --app-notice-accent: var(--app-notice-info-color); } .notice[data-variant=error] { --app-notice-accent: var(--app-notice-error-color); } /* ── Toast stack: fixed top-right overlay for ephemeral notifications ── */ .app-toast-stack { position: fixed; top: calc(var(--app-spacing) * 1.5); right: calc(var(--app-spacing) * 1.5); z-index: 1100; display: flex; flex-direction: column; gap: calc(var(--app-spacing) * 0.5); width: 400px; max-width: calc(100vw - var(--app-spacing) * 3); pointer-events: none; } .app-toast-stack:empty { display: none; } /* In admin layout: offset below topbar */ .app-container ~ .app-toast-stack, body:has(.app-container) .app-toast-stack { top: calc(52px + var(--app-spacing)); } @media (max-width: 576px) { .app-toast-stack { top: calc(var(--app-spacing) * 1); right: calc(var(--app-spacing) * 1); left: calc(var(--app-spacing) * 1); width: auto; } body:has(.app-container) .app-toast-stack { top: calc(52px + var(--app-spacing) * 0.5); right: calc(var(--app-spacing) * 0.5); left: calc(var(--app-spacing) * 0.5); } } /* Toast-only enhancements on top of the base .notice. */ .app-toast-stack .notice { margin: 0; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); pointer-events: auto; animation: toast-slide-in 250ms ease-out; } .app-toast-stack .notice.toast-dismissing { animation: toast-slide-out 200ms ease-in forwards; } @keyframes toast-slide-in { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } @keyframes toast-slide-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } } @media (prefers-reduced-motion: reduce) { .app-toast-stack .notice, .app-toast-stack .notice.toast-dismissing { animation: none; } } /* ── Progress bar timer (toast only) ─────────────────────────────────── */ .app-toast-stack .notice.flash-timed::after { content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 100%; background: var(--app-notice-accent); transform-origin: left; transform: scaleX(0); animation: flash-progress var(--flash-timeout, 4000ms) linear forwards; opacity: 0.6; } .app-toast-stack .notice.flash-timed:hover::after { animation-play-state: paused; } @keyframes flash-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } } @media (prefers-reduced-motion: reduce) { .app-toast-stack .notice.flash-timed::after { animation: none; } } /* ── Dismiss button (toast only) ─────────────────────────────────────── */ .app-toast-stack .notice .notice-dismiss-form { margin: 0; flex-shrink: 0; } .app-toast-stack .notice .notice-dismiss { border: none !important; background: transparent; color: var(--app-muted-color); opacity: 0.6; cursor: pointer; padding: 4px 6px; line-height: var(--leading-none); flex-shrink: 0; border-radius: 6px; transition: opacity 120ms ease, background-color 120ms ease; } .app-toast-stack .notice .notice-dismiss:hover, .app-toast-stack .notice .notice-dismiss:focus-visible { opacity: 1; background: color-mix(in srgb, var(--app-color) 8%, transparent); } }