/* ==========================================================================
   Premium modal system — ECVALLA Helpdesk
   Applies to every Bootstrap modal in the app (#newTicketModal,
   #addAssetModal, #confirmSendDigestModal today; automatically covers any
   future one too, since it targets the stock Bootstrap classes rather than
   per-modal ids). Builds entirely on the existing "Command Deck" tokens
   defined in style.css's :root — no new colors are introduced here, so
   this reads as a natural evolution of the current design system rather
   than a bolt-on. Loaded after style.css/vendor CSS (see base.html) so
   these rules win on equal specificity.
   ========================================================================== */

/* ---------------------------------------------------------------- backdrop
   Bootstrap's own backdrop already blocks interaction with the page behind
   the modal; this only tunes its opacity to match the app's darker, glassier
   surfaces instead of Bootstrap's flat default. */
.modal-backdrop.show { opacity: .64; background: var(--void, #05060a); }

/* The floating AI chat launcher/panel (see .ai-chat-launcher/.ai-chat-panel
   in style.css) sits at z-index 1500 — ABOVE Bootstrap's own modal
   (~1055) and its backdrop (~1050). Without this, the launcher visually
   floats on top of an open modal and stays clickable through it, most
   visibly on narrow screens where the two overlap — exactly the
   "accidental interaction with the page behind the modal" the redesign
   spec calls out. Modal + backdrop are bumped above it (and above
   everything else on the page); the toast stack (2100) and the brief
   page-transition splash (2000) still correctly render above the modal
   itself. */
.modal { z-index: 1600; }
.modal-backdrop.show { z-index: 1590; }

/* -------------------------------------------------------------- structure
   Every modal fits within the viewport and never grows past it: the dialog
   is capped at 90vh, modal-content becomes a column flex box so the header
   and footer stay put while only modal-body scrolls. Accounts for small
   laptops/tablets/phones and in-page browser zoom because it's all done in
   vh/rem, not fixed pixel heights. */
.modal-dialog {
  display: flex;
  align-items: center;
  min-height: calc(100% - 3.5rem);
}
.modal-content {
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  background: var(--surface-solid);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0,0,0,.35);
  overflow: hidden; /* keeps the rounded corners clean around the scrolling body */
}
:root[data-bs-theme="light"] .modal-content {
  box-shadow: 0 16px 44px rgba(20,24,42,.16);
}

.modal-header {
  flex: 0 0 auto;
  border-bottom: 1px solid var(--border-soft);
  padding: 1.1rem 1.4rem;
  background: var(--surface-solid);
}
.modal-title {
  color: var(--ink-primary);
  font-weight: 700;
  font-size: 1.05rem;
  display: flex;
  align-items: center;
  gap: .55rem;
  letter-spacing: -.005em;
}
.modal-title i { color: var(--brand-accent-ink); font-size: 1rem; }

.modal-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 1.4rem;
  /* thin, unobtrusive scrollbar so long forms don't feel like a raw
     browser default dropped into a premium surface */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.modal-body::-webkit-scrollbar { width: 8px; }
.modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.modal-body::-webkit-scrollbar-track { background: transparent; }

.modal-footer {
  flex: 0 0 auto;
  border-top: 1px solid var(--border-soft);
  padding: 1rem 1.4rem;
  background: var(--surface-solid);
  gap: .6rem;
}

/* Small screens: let the dialog use nearly the full viewport height/width
   and tighten padding so multi-section forms stay comfortable on a phone. */
@media (max-width: 575.98px) {
  .modal-dialog { margin: .75rem; min-height: calc(100% - 1.5rem); }
  .modal-content { max-height: calc(100vh - 1.5rem); border-radius: 14px; }
  .modal-header, .modal-footer { padding: .9rem 1rem; }
  .modal-body { padding: 1.1rem; }
}

/* ---------------------------------------------------------- form grouping
   Sections give a long modal form real visual hierarchy instead of one
   undifferentiated stack of fields. Use:
     <div class="modal-form-section">
       <div class="modal-form-section-title">Section name</div>
       <div class="modal-form-grid"> ...fields... </div>
     </div>
   A field that should span every column (descriptions, notes, long text)
   gets class="modal-field-full" alongside modal-field. */
.modal-form-section + .modal-form-section {
  margin-top: 1.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border-soft);
}
.modal-form-section-title {
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: .85rem;
  display: flex;
  align-items: center;
  gap: .5rem;
}
.modal-form-section-title i { color: var(--brand-accent-ink); font-size: .82rem; }

.modal-form-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem 1.1rem;
}
@media (min-width: 768px) {
  .modal-form-grid.cols-2 { grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1200px) {
  .modal-form-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
}
.modal-field-full { grid-column: 1 / -1; }

/* ---------------------------------------------------------- catalog tiles
   Visual picker for the New Ticket form's department/category step —
   icon tiles instead of two plain dropdowns, each drawing its icon and
   description straight from that category's own record (Categories page),
   never invented copy. Pure progressive enhancement: the real <select>
   elements these mirror stay in the DOM (see _ticket_form.html) and still
   carry the submitted value — a tile click just sets the matching
   select's value and dispatches its 'change' event, so every existing
   cascading-select/AI-suggest/validation behavior keeps working exactly
   as before with zero duplicated logic. */
.catalog-tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: .6rem;
}
.catalog-tile {
  display: flex; flex-direction: column; align-items: flex-start; gap: .5rem;
  text-align: start;
  padding: .85rem .9rem;
  border-radius: .7rem;
  border: 1px solid var(--border);
  background: var(--surface-2);
  cursor: pointer;
  transition: border-color .12s ease, background .12s ease, transform .12s ease;
}
.catalog-tile:hover { border-color: var(--brand-accent); transform: translateY(-1px); }
.catalog-tile:focus-visible { outline: 2px solid var(--brand-accent); outline-offset: 2px; }
.catalog-tile-icon {
  width: 34px; height: 34px; border-radius: .55rem;
  display: flex; align-items: center; justify-content: center;
  background: var(--brand-accent-soft); color: var(--brand-accent-ink);
  font-size: 1.05rem; flex-shrink: 0;
}
.catalog-tile-label { font-size: .84rem; font-weight: 700; color: var(--ink-primary); line-height: 1.25; }
.catalog-tile-desc {
  font-size: .74rem; color: var(--ink-muted); line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.catalog-tile.is-selected {
  border-color: var(--brand-accent);
  background: var(--brand-accent-soft);
  box-shadow: 0 0 0 1px var(--brand-accent);
}
.catalog-tile.is-selected .catalog-tile-icon { background: var(--brand-accent); color: var(--brand-dark); }
.catalog-tile-empty { color: var(--ink-muted); font-size: .82rem; padding: .5rem 0; }

/* ---------------------------------------------------- department tiles —
   signature backgrounds
   Each department (IT/ERP/AI/Other today, any admin can add more from
   the new Departments page — see models.py's Department + the
   Departments admin routes in app.py) gets its own identity instead of
   identical cards. --dept-accent/--dept-accent-rgb are set PER TILE
   inline (see renderDeptTiles() in _ticket_form.html) from that
   department's own color_hex, so grid/lattice/dots below work for any
   color an admin picks — nothing here is hardcoded to a specific
   department anymore. data-pattern picks WHICH shape (grid/lattice/dots/
   neural), matching the department's own `pattern` field.

   neural is the one exception that stays hardcoded: it reuses the app's
   OWN existing --ai-grad/--ai-glow "intelligence" signature and its
   aiGradientShift/aiGlowBreathe motion (defined above in the AI-accent
   block) rather than inventing a new visual language, and that block's
   own comment is explicit that ambient motion is reserved ONLY for
   AI-touched UI — so grid/lattice/dots stay static (color only) and only
   react on hover, same as every other tile in the app; picking "neural"
   for a non-AI department is a deliberate admin choice, not a mistake
   this CSS needs to guard against.

   A soft raised "premium" card treatment (glass gradient, inner top
   sheen, a lift + colored glow on hover using the tile's OWN accent
   instead of the universal brand gold) applies to every department tile
   regardless of pattern, so a plain "Other"-style tile still feels
   crafted, not like a placeholder. Scoped to .catalog-tile-dept so none
   of this touches the category tiles below, which share the same
   .catalog-tile shell but are dynamic, DB-driven records. */
.catalog-tile-dept {
  position: relative; overflow: hidden;
  background:
    linear-gradient(160deg, rgba(255,255,255,.05), rgba(255,255,255,0) 45%),
    var(--surface-2);
  transition: border-color .18s ease, background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.catalog-tile-dept::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  opacity: .65; transition: opacity .2s ease;
  pointer-events: none;
}
.catalog-tile-dept:hover {
  border-color: var(--dept-accent, var(--brand-accent));
  transform: translateY(-2px);
  box-shadow: 0 10px 26px -12px rgba(var(--dept-accent-rgb, 216,179,112), .55);
}
.catalog-tile-dept:hover::before { opacity: .9; }
.catalog-tile-dept.is-selected {
  border-color: var(--dept-accent, var(--brand-accent));
  box-shadow: 0 0 0 1px var(--dept-accent, var(--brand-accent)),
              0 10px 26px -12px rgba(var(--dept-accent-rgb, 216,179,112), .55);
}
.catalog-tile-dept .catalog-tile-icon,
.catalog-tile-dept .catalog-tile-label { position: relative; z-index: 1; }
.catalog-tile-dept .catalog-tile-icon {
  width: 38px; height: 38px; border-radius: .6rem;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.16);
}
.catalog-tile-dept:not(.is-selected) .catalog-tile-icon {
  background: rgba(var(--dept-accent-rgb, 154,160,180), .18);
  color: var(--dept-accent, var(--ink-secondary));
}

/* grid — a faint circuit-board grid with node points. Good for
   infrastructure-flavored departments (IT's own default). */
.catalog-tile-dept[data-pattern="grid"]::before {
  background-image:
    radial-gradient(circle 2.5px at 26px 24px, rgba(var(--dept-accent-rgb), .65) 50%, transparent 52%),
    radial-gradient(circle 2.5px at 66px 50px, rgba(var(--dept-accent-rgb), .65) 50%, transparent 52%),
    radial-gradient(circle 2px at 46px 8px, rgba(var(--dept-accent-rgb), .5) 50%, transparent 52%),
    repeating-linear-gradient(90deg, rgba(var(--dept-accent-rgb), .22) 0 1px, transparent 1px 24px),
    repeating-linear-gradient(0deg, rgba(var(--dept-accent-rgb), .22) 0 1px, transparent 1px 24px),
    radial-gradient(ellipse 140px 100px at 90% -15%, rgba(var(--dept-accent-rgb), .3) 0%, transparent 65%);
}

/* lattice — a fine diamond weave (two crossed line grids, not a filled
   checker — a filled 90° conic checker read as a distracting glitch
   rather than a pattern). Good for structured/process departments
   (ERP's own default). */
.catalog-tile-dept[data-pattern="lattice"]::before {
  background-image:
    repeating-linear-gradient(45deg, rgba(var(--dept-accent-rgb), .18) 0 1px, transparent 1px 20px),
    repeating-linear-gradient(-45deg, rgba(var(--dept-accent-rgb), .18) 0 1px, transparent 1px 20px),
    radial-gradient(ellipse 140px 100px at 90% -15%, rgba(var(--dept-accent-rgb), .3) 0%, transparent 65%);
}

/* dots — deliberately the quietest option: a bare, low-contrast dot
   grid. Good for a catch-all department (Other's own default). */
.catalog-tile-dept[data-pattern="dots"]::before {
  background-image: radial-gradient(circle 1.4px at 16px 16px, rgba(var(--dept-accent-rgb), .5) 50%, transparent 52%);
  background-size: 20px 20px;
}

/* neural — the flagship treatment: a small neural-network of nodes and
   edges (center node in the brand's own gold, echoing this file's own
   "gold = heritage, gradient = the AI layer running underneath it" line
   above), with the app's existing AI shimmer sweeping across it and the
   existing AI glow breathing on the card itself — the same motion
   vocabulary as the AI-suggest button elsewhere in this form, so it
   reads as the same assistant, not a one-off effect. Uses --ai-grad
   directly (not --dept-accent) — this is the app's own fixed signature,
   not a per-department color. */
.catalog-tile-dept[data-pattern="neural"]::before {
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20viewBox%3D%270%200%20160%20100%27%3E%3Cg%20fill%3D%27none%27%20stroke-width%3D%271%27%3E%3Cline%20x1%3D%2720%27%20y1%3D%2720%27%20x2%3D%2770%27%20y2%3D%2715%27%20stroke%3D%27%236a5bd6%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%2770%27%20y1%3D%2715%27%20x2%3D%27120%27%20y2%3D%2730%27%20stroke%3D%27%233d6fd0%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%27120%27%20y1%3D%2730%27%20x2%3D%27140%27%20y2%3D%2775%27%20stroke%3D%27%232a8fc8%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%27140%27%20y1%3D%2775%27%20x2%3D%2785%27%20y2%3D%2785%27%20stroke%3D%27%2317a8ae%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%2785%27%20y1%3D%2785%27%20x2%3D%2735%27%20y2%3D%2770%27%20stroke%3D%27%2317b3a3%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%2735%27%20y1%3D%2770%27%20x2%3D%2720%27%20y2%3D%2720%27%20stroke%3D%27%235b6bd6%27%20stroke-opacity%3D%27.4%27/%3E%3Cline%20x1%3D%2760%27%20y1%3D%2745%27%20x2%3D%2720%27%20y2%3D%2720%27%20stroke%3D%27%236a5bd6%27%20stroke-opacity%3D%27.35%27/%3E%3Cline%20x1%3D%2760%27%20y1%3D%2745%27%20x2%3D%2770%27%20y2%3D%2715%27%20stroke%3D%27%235571d3%27%20stroke-opacity%3D%27.35%27/%3E%3Cline%20x1%3D%2760%27%20y1%3D%2745%27%20x2%3D%2785%27%20y2%3D%2785%27%20stroke%3D%27%232a9ab8%27%20stroke-opacity%3D%27.35%27/%3E%3Cline%20x1%3D%2760%27%20y1%3D%2745%27%20x2%3D%2735%27%20y2%3D%2770%27%20stroke%3D%27%233a8fc4%27%20stroke-opacity%3D%27.35%27/%3E%3C/g%3E%3Cg%3E%3Ccircle%20cx%3D%2720%27%20cy%3D%2720%27%20r%3D%272.6%27%20fill%3D%27%236a5bd6%27/%3E%3Ccircle%20cx%3D%2770%27%20cy%3D%2715%27%20r%3D%272.6%27%20fill%3D%27%235a72d4%27/%3E%3Ccircle%20cx%3D%27120%27%20cy%3D%2730%27%20r%3D%272.6%27%20fill%3D%27%232a78d6%27/%3E%3Ccircle%20cx%3D%27140%27%20cy%3D%2775%27%20r%3D%272.6%27%20fill%3D%27%2320a0b8%27/%3E%3Ccircle%20cx%3D%2785%27%20cy%3D%2785%27%20r%3D%272.6%27%20fill%3D%27%2317b3a3%27/%3E%3Ccircle%20cx%3D%2735%27%20cy%3D%2770%27%20r%3D%272.6%27%20fill%3D%27%233a6fce%27/%3E%3Ccircle%20cx%3D%2760%27%20cy%3D%2745%27%20r%3D%273.2%27%20fill%3D%27%23e8c988%27/%3E%3C/g%3E%3C/svg%3E"),
    linear-gradient(120deg, rgba(106,91,214,.30), rgba(42,120,214,.22) 55%, rgba(23,179,163,.30));
  background-size: 100% 100%, 220% 220%;
  background-repeat: no-repeat, no-repeat;
  animation: aiGradientShift 6s ease-in-out infinite;
}
.catalog-tile-dept[data-pattern="neural"] { animation: aiGlowBreathe 3.4s ease-in-out infinite; }
.catalog-tile-dept[data-pattern="neural"]:not(.is-selected) .catalog-tile-icon {
  background: var(--ai-grad); color: #fff;
}
@media (prefers-reduced-motion: reduce) {
  .catalog-tile-dept[data-pattern="neural"],
  .catalog-tile-dept[data-pattern="neural"]::before { animation: none; }
}
/* Canned-report tiles on the Reports page reuse the same tile shell as an
   <a> instead of a <button>, so they need the link defaults stripped. */
.canned-report-tile { text-decoration: none; color: inherit; }
.canned-report-tile:hover { color: inherit; }
/* The raw <select> elements stay in the DOM and keep the submitted value —
   visually replaced by the tiles above once JS has built them. */
.catalog-select-fallback.is-enhanced { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }

.modal-field { display: flex; flex-direction: column; }
.modal-field .form-label {
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-secondary);
  margin-bottom: .4rem;
}
.modal-field .form-label .required-dot {
  color: var(--status-critical);
  margin-inline-start: .25rem;
  font-weight: 700;
}

/* Consistent input heights across text/select/date so a grid row never
   looks jagged, plus a calmer default border than Bootstrap's stock one. */
.modal-body .form-control,
.modal-body .form-select {
  min-height: 2.6rem;
  border-color: var(--border);
  background: var(--surface-2);
}
.modal-body textarea.form-control { min-height: 6rem; }
.modal-body .form-control:hover,
.modal-body .form-select:hover { border-color: var(--brand-accent); }
.modal-body .form-text { font-size: .76rem; margin-top: .35rem; }

/* --------------------------------------------------------- field errors
   Server-returned field-level errors (see modal-forms.js) render right
   under the offending input, and mark the input itself so it's visible
   without reading the text (color + icon + message, never color alone). */
.modal-body .is-invalid { border-color: var(--status-critical) !important; }
.modal-body .invalid-feedback {
  display: none;
  font-size: .78rem;
  color: var(--status-critical);
  margin-top: .35rem;
  align-items: center;
  gap: .35rem;
}
.modal-body .is-invalid ~ .invalid-feedback,
.modal-body .invalid-feedback.is-shown { display: flex; }

/* ------------------------------------------------------- collapse toggle
   The "specs" collapsible in #addAssetModal — refined to match the section
   look above instead of a plain outline button. */
.modal-body .modal-collapse-toggle {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; padding: .65rem .9rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--border-soft);
  color: var(--ink-primary); font-weight: 600; font-size: .86rem;
  transition: background-color .15s ease, border-color .15s ease;
}
.modal-body .modal-collapse-toggle:hover {
  background: var(--brand-accent-soft); border-color: var(--brand-accent);
}
.modal-body .modal-collapse-toggle .chevron { transition: transform .2s ease; }
.modal-body .modal-collapse-toggle[aria-expanded="true"] .chevron { transform: rotate(180deg); }

/* ----------------------------------------------------- submit / loading
   Applied by modal-forms.js to the primary action button while a
   submission is in flight: shows a spinner, swaps the label, and blocks
   further clicks/keyboard activation without a visual "dead" look. */
.btn[data-busy="1"] {
  position: relative;
  pointer-events: none;
  opacity: .88;
}
.btn[data-busy="1"] .btn-spinner {
  display: inline-block;
  width: .95em; height: .95em;
  border: .15em solid currentColor;
  border-inline-end-color: transparent;
  border-radius: 50%;
  animation: modalSpin .6s linear infinite;
  margin-inline-end: .5rem;
  vertical-align: -.15em;
}
@keyframes modalSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn[data-busy="1"] .btn-spinner { animation-duration: 1.4s; }
}

/* Secondary/close controls while a submission is in flight — dimmed and
   inert rather than removed, so the modal's layout never jumps. */
.modal.is-submitting [data-bs-dismiss="modal"],
.modal.is-submitting .btn-close {
  opacity: .4;
  pointer-events: none;
}

/* Disabled state used across the modal forms — same visual language for
   both native `disabled` inputs/buttons and the busy state above. */
.modal-body .form-control:disabled,
.modal-body .form-select:disabled,
.modal-body .form-control[readonly] {
  opacity: .65;
  cursor: not-allowed;
}

/* Focus visibility — the app's existing gold focus ring, applied
   consistently across every control a modal can contain (not just
   .form-control/.form-select) so keyboard navigation is always legible. */
.modal-content :focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 2px;
}

/* ------------------------------------------------------- general alert
   A calmer, on-brand look for the plain <div class="alert"> banners some
   modals show inline (e.g. "mail is off" in the digest confirm modal),
   replacing Bootstrap's stock saturated fills. */
.modal-body .alert {
  border: 1px solid var(--border-soft);
  background: var(--surface-2);
  color: var(--ink-secondary);
  font-size: .85rem;
  border-radius: 10px;
}
.modal-body .alert-warning { color: var(--status-warning-ink); border-color: rgba(251,191,61,.35); }
.modal-body .alert-danger  { color: var(--status-critical); border-color: rgba(255,92,92,.35); }

/* ==========================================================================
   Toast / notification stack
   The app currently only has server-rendered flash banners; this adds the
   client-side "polished success/error notification" the modal spec calls
   for, in the same visual language as everything above. See
   static/js/toast.js for the JS that populates this container (injected
   once into <body> by base.html). ========================================================================== */
.app-toast-stack {
  position: fixed;
  top: 1.1rem;
  inset-inline-end: 1.1rem;
  z-index: 2100; /* above modal-backdrop (Bootstrap's is ~1055) and the
                     page-transition overlay isn't shown while a toast from
                     a modal action would appear, so this is safely on top */
  display: flex;
  flex-direction: column;
  gap: .6rem;
  width: min(360px, calc(100vw - 2.2rem));
  pointer-events: none;
}
.app-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: .65rem;
  padding: .85rem 1rem;
  border-radius: 12px;
  background: var(--surface-solid);
  border: 1px solid var(--border);
  box-shadow: 0 12px 32px rgba(0,0,0,.28);
  color: var(--ink-primary);
  font-size: .87rem;
  line-height: 1.45;
  animation: appToastIn .28s cubic-bezier(.22,.9,.34,1) both;
}
:root[data-bs-theme="light"] .app-toast { box-shadow: 0 10px 26px rgba(20,24,42,.14); }
.app-toast.is-leaving { animation: appToastOut .18s ease both; }
@keyframes appToastIn { from { opacity: 0; transform: translateY(-8px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes appToastOut { to { opacity: 0; transform: translateY(-6px) scale(.98); } }
@media (prefers-reduced-motion: reduce) {
  .app-toast, .app-toast.is-leaving { animation: none; }
}

.app-toast-icon {
  flex: 0 0 auto;
  width: 1.9rem; height: 1.9rem;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: .95rem;
}
.app-toast.toast-success .app-toast-icon { background: rgba(46,204,113,.16); color: var(--status-good); }
.app-toast.toast-error   .app-toast-icon { background: rgba(255,92,92,.16);  color: var(--status-critical); }
.app-toast.toast-warning .app-toast-icon { background: rgba(251,191,61,.16); color: var(--status-warning-ink); }
.app-toast.toast-success { border-inline-start: 3px solid var(--status-good); }
.app-toast.toast-error   { border-inline-start: 3px solid var(--status-critical); }
.app-toast.toast-warning { border-inline-start: 3px solid var(--status-warning); }

.app-toast-body { flex: 1 1 auto; min-width: 0; }
.app-toast-title { font-weight: 700; margin-bottom: .1rem; }
.app-toast-msg { color: var(--ink-secondary); word-wrap: break-word; }
.app-toast-close {
  flex: 0 0 auto;
  background: none; border: 0; color: var(--ink-muted);
  font-size: .95rem; line-height: 1; padding: .1rem;
  cursor: pointer;
}
.app-toast-close:hover { color: var(--ink-primary); }

@media (max-width: 575.98px) {
  .app-toast-stack { top: .7rem; inset-inline-end: .7rem; inset-inline-start: .7rem; width: auto; }
}
