/* SpinKit "wave" loader for the shared modal "Please wait…" overlay
   (partials/indicators/loading.html). The spinkit vendor CSS is never linked,
   so its animation was dead everywhere the overlay is used — define just the
   wave rules here (main.css is loaded globally) so it animates again. */
.sk-wave {
  --sk-size: 30px;
  --sk-color: #696cff;
  width: var(--sk-size);
  height: var(--sk-size);
  display: flex;
  justify-content: space-between;
}
.sk-wave-rect {
  background-color: var(--sk-color);
  height: 100%;
  width: 15%;
  animation: sk-wave 1.2s infinite ease-in-out;
}
.sk-wave-rect:nth-child(1) { animation-delay: -1.2s; }
.sk-wave-rect:nth-child(2) { animation-delay: -1.1s; }
.sk-wave-rect:nth-child(3) { animation-delay: -1s; }
.sk-wave-rect:nth-child(4) { animation-delay: -0.9s; }
.sk-wave-rect:nth-child(5) { animation-delay: -0.8s; }
@keyframes sk-wave {
  0%, 40%, 100% { transform: scaleY(0.4); }
  20% { transform: scaleY(1); }
}

/* Avatar photos: crop to a consistent square (circle) regardless of the source
   image's aspect ratio. Sneat's .avatar img is 100% x 100% but sets no
   object-fit, so a portrait/landscape upload gets stretched. `cover` keeps every
   avatar uniform across the app (navbar, lists, cards, profile). */
.avatar img {
  object-fit: cover;
  object-position: center;
}

.modal-dialog.modal-resizable {
  transform-origin: center center;
  will-change: width, max-width, transform;
}

.modal-dialog.modal-resizable.resizing {
  transition:
    width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
    max-width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1),
    transform 0.25s ease;
  transform: scale(0.98);
}

.modal-dialog.modal-resizable:not(.resizing) {
  transform: scale(1);
}

/* Scrollable modal fix.
   The shared HTMX modal nests .modal-header/.modal-body/.modal-footer inside a
   .form-content wrapper — either under a <form> (create/edit, via
   modal_content_with_form.html) or directly under .modal-content (read-only /
   panel modals like the employee detail + reporting manage views, via
   modal_content_without_form.html). Both break Bootstrap's
   .modal-dialog-scrollable flex chain (it expects those as direct children of
   .modal-content). Re-thread the flex column through the wrappers so the body
   scrolls and the header/footer stay pinned in both variants. */
.modal-dialog-scrollable .modal-content > #main-modal-content,
.modal-dialog-scrollable #main-modal-content > form,
.modal-dialog-scrollable #main-modal-content > form > .form-content,
.modal-dialog-scrollable #main-modal-content > .form-content {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
  max-height: 100%;
  overflow: hidden;
}

.modal-dialog-scrollable #main-modal-content > form > .form-content > .modal-body,
.modal-dialog-scrollable #main-modal-content > .form-content > .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* The loading overlay is a child of .modal-content (so it hugs the card, not
   the full-height dialog). But .modal-content has pointer-events: auto, and an
   opacity:0 element still captures clicks — so the idle (hidden) overlay would
   swallow every click in the modal. Keep it click-through when idle; only
   intercept while a request is actually in flight (htmx adds .htmx-request to
   the hx-indicator target during the request). */
#modal-indicator {
  pointer-events: none;
}
#modal-indicator.htmx-request {
  pointer-events: auto;
}

/* The swap target is empty until the first fetch lands; give it height so the
   loading overlay is visible on the very first open. */
#main-modal-content:empty {
  min-height: 6rem;
}

#toast-container {
  position: fixed;
  top: 1rem;
  right: 1rem;
  /* Above the modal (--bs-modal-zindex: 1090) so toasts show over an open
     modal — e.g. "Save & Continue", which keeps the modal open. */
  z-index: 1100;

  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}

#toast-container .toast {
  position: relative;
  width: 340px;
  max-width: 340px;
  margin: 0;
  flex-shrink: 0;
}

.toast-ex.animate__animated {
  --animate-duration: 0.7s;
}

/* ---- Blog ---- */
.blog-cover {
  height: 160px;
  object-fit: cover;
}

.blog-cover-empty {
  height: 160px;
}

.blog-cover-lg {
  max-height: 360px;
  object-fit: cover;
}

.blog-cover-thumb {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border: 1px solid var(--bs-border-color);
  padding: 2px;
  background-color: var(--bs-body-bg);
}

/* ---- Dashboard "Latest Blogs" magazine layout ---- */
/* Featured hero: cover photo (or gradient fallback) with an overlaid summary. */
.blog-feature {
  position: relative;
  display: block;
  height: 220px;
  border-radius: 1rem;
  overflow: hidden;
  text-decoration: none;
  background: linear-gradient(135deg, #7367f0 0%, #9155fd 55%, #f5576c 100%);
  box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.12);
  transition:
    transform 0.2s ease,
    box-shadow 0.2s ease;
}

.blog-feature:hover {
  transform: translateY(-3px);
  box-shadow: 0 0.9rem 1.75rem rgba(0, 0, 0, 0.22);
}

.blog-feature--photo {
  background-image: var(--cover);
  background-size: cover;
  background-position: center;
}

/* The cover slowly zooms on hover for a lively feel. */
.blog-feature--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: inherit;
  background-size: cover;
  background-position: center;
  transition: transform 0.4s ease;
}

.blog-feature:hover.blog-feature--photo::before {
  transform: scale(1.06);
}

.blog-feature-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 1rem 1.1rem;
  color: #fff;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.82) 0%, rgba(0, 0, 0, 0.35) 45%, transparent 75%);
}

.blog-feature-tag {
  align-self: flex-start;
  margin-bottom: auto;
  padding: 0.2rem 0.6rem;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #fff;
  background: rgba(255, 255, 255, 0.22);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 2rem;
  backdrop-filter: blur(4px);
}

.blog-feature-title {
  margin-bottom: 0.25rem;
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.3;
  color: #fff;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.blog-feature-excerpt {
  margin-bottom: 0.6rem;
  font-size: 0.8rem;
  line-height: 1.35;
  color: rgba(255, 255, 255, 0.85);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.blog-feature-meta {
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.9);
}

.blog-feature-avatar img,
.blog-feature-avatar .avatar-initial {
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7);
}

.blog-meta-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.1rem 0.55rem;
  font-size: 0.72rem;
  font-weight: 600;
  border-radius: 2rem;
  background: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(4px);
}

/* Compact rows for the remaining posts. */
.blog-mini {
  text-decoration: none;
  border-radius: 0.5rem;
  transition: background-color 0.15s ease;
}

.blog-mini:hover {
  background: var(--bs-gray-50, rgba(0, 0, 0, 0.03));
}

.blog-mini:hover .text-body {
  color: var(--bs-primary) !important;
}

.blog-mini-thumb {
  width: 56px;
  height: 56px;
  object-fit: cover;
}

.blog-mini-thumb--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 0.5rem;
  font-size: 1.4rem;
  color: #fff;
  background: linear-gradient(135deg, #7367f0 0%, #9155fd 100%);
}

/* Compact list toolbar (search box + add button) */
.blog-search-input {
  width: 200px;
  max-width: 100%;
}

.blog-toolbar .btn,
.blog-toolbar .form-control {
  --bs-btn-padding-y: 0.25rem;
  --bs-btn-padding-x: 0.6rem;
  font-size: 0.8125rem;
  padding-block: 0.25rem;
}

/* ---- Dashboard celebrations (birthdays / anniversaries) ---- */
.celebration-avatar {
  --celebration-ring: var(--bs-primary-rgb);
  position: relative;
}

.celebration-avatar--success {
  --celebration-ring: var(--bs-success-rgb);
}

.celebration-avatar img,
.celebration-avatar .avatar-initial {
  box-shadow:
    0 0 0 3px var(--bs-card-bg, #fff),
    0 0 0 5px rgba(var(--celebration-ring), 0.25);
}

.celebration-badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  color: #fff;
  font-size: 0.85rem;
  border: 2px solid var(--bs-card-bg, #fff);
}

/* ---- Dashboard welcome banner ---- */
.dashboard-welcome {
  position: relative;
  overflow: hidden;
  border: 0;
  color: #fff;
  background: linear-gradient(120deg, #6a11cb 0%, #7367f0 36%, #b621fe 66%, #f5576c 100%);
  box-shadow: 0 0.75rem 1.75rem rgba(106, 17, 203, 0.28);
}

/* Two soft floating colour blobs add depth and playfulness. */
.dashboard-welcome::before,
.dashboard-welcome::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
}

.dashboard-welcome::before {
  top: -60px;
  right: -30px;
  width: 220px;
  height: 220px;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.35) 0%, transparent 70%);
}

.dashboard-welcome::after {
  bottom: -80px;
  right: 22%;
  width: 200px;
  height: 200px;
  background: radial-gradient(circle, rgba(255, 214, 0, 0.3) 0%, transparent 70%);
}

.dashboard-welcome-text {
  position: relative;
  z-index: 1;
}

/* Frosted-glass date chip. */
.dashboard-welcome-date {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 600;
  color: #fff;
  background: rgba(255, 255, 255, 0.2);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 2rem;
  backdrop-filter: blur(4px);
}

/* Frosted-glass icon tile. */
.dashboard-welcome-icon {
  position: relative;
  z-index: 1;
  align-items: center;
  justify-content: center;
  width: 68px;
  height: 68px;
  border-radius: 1rem;
  font-size: 2.2rem;
  color: #fff;
  background: rgba(255, 255, 255, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(6px);
}

/* ---- Dashboard shout-out mini cards ---- */
.shoutout-mini-card {
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}

.shoutout-mini-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.08);
}

/* View-more CTA card in the dashboard shout-out grid */
.shoutout-viewmore-card {
  border-style: dashed;
}

/* ---- Employee status summary banner ---- */
.progress-thin {
  height: 8px;
}

/* Clickable status-filter chips: hint interactivity + lift the active one. */
.status-chip {
  cursor: pointer;
}

.status-chip.active {
  box-shadow: 0 0 0 0.15rem rgba(0, 0, 0, 0.06);
}

/* ---- Dashboard birthday banner ---- */
.birthday-banner {
  border: 0;
  color: #fff;
  background: linear-gradient(135deg, #f857a6 0%, #ff5858 100%);
  position: relative;
  overflow: hidden;
}

.birthday-banner::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 92% 12%, rgba(255, 255, 255, 0.18) 0 42px, transparent 43px),
    radial-gradient(circle at 78% 88%, rgba(255, 255, 255, 0.12) 0 26px, transparent 27px);
  pointer-events: none;
}

.birthday-banner .card-body {
  position: relative;
  z-index: 1;
}

.birthday-cake {
  font-size: 2.75rem;
  line-height: 1;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
}

.birthday-banner .avatar img,
.birthday-banner .avatar-initial {
  border: 3px solid rgba(255, 255, 255, 0.85);
}

.birthday-banner .avatar-initial {
  background: rgba(255, 255, 255, 0.25);
  color: #fff;
}

.birthday-avatar-stack {
  margin-left: -1rem;
}

.birthday-confetti {
  font-size: 2rem;
  opacity: 0.6;
}

/* ---- Dashboard work-anniversaries: festive horizontal card scroller ---- */
/* Light, minimal heading — no gradient banner. */
.anniv-head-emoji {
  font-size: 1.35rem;
  line-height: 1;
}

.anniv-head-count {
  font-size: 0.7rem;
  font-weight: 700;
  color: #6b7280;
  background: rgba(0, 0, 0, 0.05);
  border-radius: 1rem;
  padding: 0.15rem 0.6rem;
}

.anniv-scroller {
  display: flex;
  gap: 0.85rem;
  overflow-x: auto;
  padding: 0.5rem 0.25rem 0.75rem;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
}

.anniv-scroller::-webkit-scrollbar {
  height: 6px;
}

.anniv-scroller::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.15);
  border-radius: 3px;
}

/* Each celebrant is a snap-scrolling tile. One tier class recolours it all. */
.anniv-tile {
  --tier: #2193b0;
  --tier-2: #6dd5ed;

  scroll-snap-align: start;
  flex: 0 0 auto;
  width: 152px;
  border-radius: 1rem;
  overflow: hidden;
  text-align: center;
  padding-bottom: 0.85rem;
  background: var(--bs-card-bg, #fff);
  border: 1px solid rgba(0, 0, 0, 0.06);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
  transition:
    transform 0.18s ease,
    box-shadow 0.18s ease;
}

.anniv-tile:hover {
  transform: translateY(-4px);
  box-shadow: 0 0.75rem 1.5rem color-mix(in srgb, var(--tier) 30%, transparent);
}

.anniv-item--teal {
  --tier: #11998e;
  --tier-2: #38ef7d;
}

.anniv-item--purple {
  --tier: #8e2de2;
  --tier-2: #4a00e0;
}

.anniv-item--gold {
  --tier: #f7971e;
  --tier-2: #ffd200;
}

/* Gradient top with a big year number and a confetti sprinkle. */
.anniv-tile-top {
  position: relative;
  height: 82px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding-top: 0.85rem;
  color: #fff;
  background: linear-gradient(135deg, var(--tier) 0%, var(--tier-2) 100%);
}

.anniv-tile-top::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.9) 1.5px, transparent 2px),
    radial-gradient(circle, rgba(255, 255, 255, 0.55) 1.5px, transparent 2px);
  background-size:
    22px 22px,
    32px 32px;
  background-position:
    0 0,
    11px 9px;
  opacity: 0.5;
  pointer-events: none;
}

.anniv-tile-num {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
  font-size: 1.9rem;
  font-weight: 800;
  line-height: 1;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.anniv-tile-lbl {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.9;
}

/* Avatar overlaps the gradient band — position + z-index keep it above the
   (positioned) top band, which would otherwise paint over it. */
.anniv-tile-avatar {
  position: relative;
  z-index: 1;
  width: 68px;
  height: 68px;
  margin: -34px auto 0.4rem;
  border-radius: 50%;
  box-shadow:
    0 0 0 4px var(--bs-card-bg, #fff),
    0 3px 8px rgba(0, 0, 0, 0.15);
}

.anniv-tile-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.anniv-tile-avatar .avatar-initial {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  color: #fff;
  font-weight: 600;
  font-size: 1.25rem;
  background: linear-gradient(135deg, var(--tier) 0%, var(--tier-2) 100%);
}

.anniv-tile-name {
  font-weight: 600;
  font-size: 0.85rem;
  line-height: 1.2;
  padding: 0 0.5rem;
  color: var(--bs-heading-color, inherit);
}

.anniv-tile-date {
  display: inline-block;
  margin-top: 0.45rem;
  padding: 0.15rem 0.6rem;
  font-size: 0.7rem;
  font-weight: 600;
  border-radius: 1rem;
  color: var(--tier);
  background: color-mix(in srgb, var(--tier) 13%, #fff);
}

/* ---- Dashboard shout-out kudos cards ---- */
.shoutout-kudos {
  border-top: 3px solid transparent;
  transition:
    transform 0.15s ease,
    box-shadow 0.15s ease;
}

.shoutout-kudos:hover {
  transform: translateY(-3px);
  box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.1);
}

.shoutout-kudos--primary {
  border-top-color: var(--bs-primary);
}

.shoutout-kudos--success {
  border-top-color: var(--bs-success);
}

.shoutout-kudos--info {
  border-top-color: var(--bs-info);
}

.shoutout-kudos--warning {
  border-top-color: var(--bs-warning);
}

.shoutout-kudos--danger {
  border-top-color: var(--bs-danger);
}

.shoutout-quote {
  font-size: 1.75rem;
  line-height: 1;
  opacity: 0.55;
}

.shoutout-snippet {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.shoutout-kudos .avatar {
  width: 4rem;
  height: 4rem;
}

.shoutout-kudos .avatar .avatar-initial {
  font-size: 1.375rem;
}

.shoutout-award {
  position: absolute;
  right: -3px;
  bottom: -3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  color: #fff;
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--bs-card-bg, #fff);
}

/* ---- Employee directory ---- */
/* The whole card is the click target (a <button>, so it stays keyboard
   reachable); strip the button chrome and lift it on hover/focus instead. */
.directory-card {
  background-color: var(--bs-card-bg, #fff);
  cursor: pointer;
  transition:
    transform 0.2s ease,
    box-shadow 0.2s ease;
}

.directory-card:hover,
.directory-card:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.12);
}

/* Directory search box: icon on the left, our own clear button on the right. */
.directory-search {
  min-width: 17.5rem;
}

/* The icon and the clear button are overlaid on the field, so the text has to
   be inset past both. !important because the theme re-asserts .form-control
   padding once the field holds a value, which otherwise slides the text back
   under the search icon mid-typing. Logical properties keep it correct in RTL. */
.directory-search .directory-search-input {
  padding-inline-start: 2.15rem !important;
  padding-inline-end: 2.15rem !important;
}

/* Chrome/Safari draw their own clear "x" inside type=search; it sits in a
   closed shadow root (unstylable, and its click does not reliably reach htmx),
   so hide it and use .directory-search-clear instead. */
.directory-search-input::-webkit-search-cancel-button {
  display: none;
}

/* Stretched full height and centred with flexbox rather than a translate: a
   transform would be overwritten by any hover/active rule that sets its own,
   which is what made the clear button jump away from the pointer. */
.directory-search-icon,
.directory-search-clear,
.directory-search-spinner {
  position: absolute;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The icon font fixes its own 1em box, so it cannot stretch between top/bottom
   like the others -- centre it with a translate instead. Safe here: it is
   pointer-events:none, so no hover rule ever competes for the transform. */
.directory-search-icon {
  inset-inline-start: 0.65rem;
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
  color: var(--bs-secondary-color);
  pointer-events: none;
}

.directory-search-clear,
.directory-search-spinner {
  inset-inline-end: 0.35rem;
}

/* The indicator shares its slot with the clear button and, though invisible
   between requests, would otherwise sit on top of it and swallow the click --
   which is also what stops the browser's own "x" from ever firing. */
.directory-search-spinner {
  pointer-events: none;
}

/* Self-contained styling (no .btn): nothing from the theme can restyle or
   displace it on hover. */
.directory-search-clear {
  width: 1.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--bs-secondary-color);
  line-height: 1;
  cursor: pointer;
}

.directory-search-clear:hover,
.directory-search-clear:focus-visible {
  color: var(--bs-body-color);
}

/* The spinner and the clear button share the same slot; while a search is in
   flight the spinner wins. */
.directory-search:has(.htmx-request) .directory-search-clear {
  visibility: hidden;
}

/* ---------------------------------------------------------------------------
   Vertical menu: settle the scroll position before showing the sidebar
   ---------------------------------------------------------------------------
   The menu overflows its viewport, so on a page whose entry sits low in it the
   sidebar painted at scroll 0 and then jumped down to reveal the active row.
   That reveal cannot be moved earlier: while the document is parsing,
   .menu-inner is not yet height-constrained (clientHeight 1047 during parse vs
   971 after layout), so it does not overflow and there is nothing to scroll.

   The partial therefore hides the menu during parse, positions it once the DOM
   is complete, and drops this class — so the sidebar fades in already in the
   right place instead of lurching into it. See
   templates/layout/partials/menu/vertical/vertical_menu.html. */
.menu-inner {
  /* Decelerating curve (easeOutExpo-ish): most of the distance is covered in
     the first few frames, so the menu reads as already there and merely
     settling, rather than as something being played at the user. Duration is
     matched to the submenu reveal below so that when a page loads with a group
     already open, the two land together instead of stacking two motions.

     Composited properties only (opacity + transform) — this runs on every
     navigation, so it must never touch layout. */
  transition:
    opacity 0.26s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Applied for one frame when the settle produced no movement, so the sidebar is
   revealed instantly instead of fading. Most navigations land here: the scroll
   offset carries over from the previous page and the active row is already in
   view, which means the menu is pixel-identical to the one the user was just
   looking at. Animating that would announce a re-render that did not visually
   occur — the fade is reserved for the case where the menu genuinely moved. */
.menu-inner.menu-no-settle-anim {
  transition: none;
}

.menu-inner.menu-settling {
  opacity: 0;
  /* Drifts in from the sidebar's own edge: a horizontal settle suits a rail
     pinned to the left, and — unlike a vertical one — cannot be misread as the
     menu scrolling, which is the exact motion this whole mechanism exists to
     remove. 8px is enough to register as movement without being a slide-in. */
  transform: translate3d(-8px, 0, 0);
  /* Failsafe: if the script never removes the class (JS error, blocked script),
     reveal the menu anyway. A sidebar stuck invisible is far worse than the
     jump this is fixing. Zero duration + delay = a one-shot switch at 700ms.

     The 700ms is also read by the script as FAILSAFE_MS
     (templates/layout/partials/menu/vertical/vertical_menu.html): once this has
     fired, settle() is behind the reveal and drops its cosmetic scroll restore
     rather than moving a menu the user is already looking at. Change both. */
  animation: menu-settle-failsafe 0s 0.7s forwards;
}

@keyframes menu-settle-failsafe {
  to {
    opacity: 1;
    /* Must clear the offset too, or the failsafe would reveal a menu that is
       permanently sitting 8px to the left. */
    transform: none;
  }
}

/* Respect a reduced-motion preference: still position before showing, but cut
   the motion so the menu simply appears. The offset has to be cleared as well —
   with the transition off, an untransitioned translate would just leave the menu
   sitting 8px to the left. */
@media (prefers-reduced-motion: reduce) {
  .menu-inner {
    transition: none;
  }

  .menu-inner.menu-settling {
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
   Vertical menu: smooth open/close
   ---------------------------------------------------------------------------
   Sneat's Menu runs with animate:true, but it animates by setting an inline
   height while .menu-sub computes transition-duration: 0s — so the submenu
   snapped open in a single frame. A plain transition cannot fix it either: the
   submenu toggles display none <-> flex, and display is not an animatable
   property, so there is no start frame to transition from.

   A keyframe animation does work, because it runs from the moment the element
   becomes displayed. It plays on open only; on close the element is display:none
   immediately and there is nothing left to animate, which is the cheaper half to
   lose — the eye follows things appearing, not disappearing. */
@keyframes menu-sub-reveal {
  from {
    opacity: 0;
    transform: translate3d(0, -0.375rem, 0);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* Scoped to .menu-interactive, which the menu partial sets on the user's first
   click inside the sidebar. A group that is open only because the server
   rendered it that way therefore appears already open, with no motion: replaying
   that reveal on every page load was the loudest signal that the sidebar had
   been rebuilt. Same curve and duration as the settle above, so when the menu
   does move, the two resolve as one movement rather than two. */
.menu-vertical.menu-interactive .menu-item.open > .menu-sub {
  animation: menu-sub-reveal 0.26s cubic-bezier(0.16, 1, 0.3, 1);
}

/* The expand/collapse chevron rotates via a transform on the toggle; the theme
   transitions colour and position on .menu-link but not transform, so the arrow
   flipped instantly while the rows slid. */
.menu-vertical .menu-link.menu-toggle::after {
  transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Honour a reduced-motion preference: the submenu still opens, just without the
   slide -- and the settle fade above is already disabled for these users. */
@media (prefers-reduced-motion: reduce) {
  .menu-vertical.menu-interactive .menu-item.open > .menu-sub {
    animation: none;
  }

  .menu-vertical .menu-link.menu-toggle::after {
    transition: none;
  }
}

/* ---- News (app_news) ---- */
.news-cover {
  height: 180px;
  object-fit: cover;
}

.news-cover-empty {
  height: 180px;
  color: #7367f0;
}

.news-cover-lg {
  max-height: 380px;
  object-fit: cover;
}

.news-dash-thumb {
  width: 100%;
  height: 110px;
  object-fit: cover;
}

.news-dash-thumb--fallback {
  background: rgba(115, 103, 240, 0.12);
  color: #7367f0;
}

.news-dash-item {
  text-decoration: none;
}

.news-dash-item:hover .fw-medium {
  color: #7367f0;
}

/* ---- URJA Awards (app_urja) ---- */
/* Nomination-open banner: warm "energy" gradient, sibling of .dashboard-welcome. */
.urja-banner {
  position: relative;
  overflow: hidden;
  border: 0;
  color: #fff;
  background: linear-gradient(120deg, #b02a06 0%, #e2571d 45%, #f7912a 80%, #ffc837 100%);
  box-shadow: 0 0.75rem 1.75rem rgba(176, 42, 6, 0.28);
}

.urja-banner::before,
.urja-banner::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
}

.urja-banner::before {
  top: -60px;
  right: -30px;
  width: 220px;
  height: 220px;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.32) 0%, transparent 70%);
}

.urja-banner::after {
  bottom: -80px;
  right: 24%;
  width: 200px;
  height: 200px;
  background: radial-gradient(circle, rgba(255, 214, 0, 0.35) 0%, transparent 70%);
}

.urja-banner-text {
  position: relative;
  z-index: 1;
}

.urja-banner-kicker {
  display: inline-block;
  padding: 0.2rem 0.65rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.18);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.urja-banner-chip {
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
  font-weight: 500;
}

.urja-banner-icon {
  position: relative;
  z-index: 1;
  width: 64px;
  height: 64px;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  font-size: 2rem;
}

/* Spotlight / winner cards share the medallion-avatar treatment. */
.urja-spotlight-item,
.urja-winner-card {
  padding: 0.75rem;
  border-radius: 0.5rem;
  transition: background-color 0.2s ease;
}

.urja-spotlight-item:hover,
.urja-winner-card:hover {
  background: rgba(115, 103, 240, 0.06);
}

.urja-spotlight-avatar {
  position: relative;
}

.urja-spotlight-badge {
  position: absolute;
  right: -4px;
  bottom: -4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid #fff;
  color: #fff;
  font-size: 0.7rem;
}

.urja-spotlight-excerpt {
  display: block;
}

/* Review board: preserve the nominator's line breaks in answers. */
.urja-answer {
  white-space: pre-line;
}

.urja-review-card details > summary {
  cursor: pointer;
  list-style: none;
}

.urja-review-card details > summary::before {
  content: "\25B8";
  margin-right: 0.35rem;
  color: #a1acb8;
}

.urja-review-card details[open] > summary::before {
  content: "\25BE";
}

.urja-cta-card {
  border: 1px dashed rgba(115, 103, 240, 0.4);
}

.urja-category-description {
  max-width: 380px;
}

/* ---- Dashboard cockpit: pulse tiles, leave gauges, absences, holidays ----
   Colours come from Bootstrap/Sneat custom properties wherever a surface or
   text colour is involved, so the whole section follows the light/dark switch
   without a second rule set. Only the gradient banner, which is always dark,
   hard-codes white. */

/* The welcome banner now carries the headline metrics, so its body is a
   two-part flex: identity on the left, tiles on the right. */
.dashboard-welcome-body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* Frosted metric tiles sitting on the banner gradient. */
.pulse-strip {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.pulse-tile {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 8.5rem;
  padding: 0.7rem 0.9rem;
  color: #fff;
  text-decoration: none;
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 0.6rem;
  backdrop-filter: blur(6px);
  transition:
    transform 0.18s ease,
    background-color 0.18s ease;
}

.pulse-tile:hover,
.pulse-tile:focus-visible {
  color: #fff;
  background: rgba(255, 255, 255, 0.28);
  transform: translateY(-2px);
}

.pulse-tile-icon {
  margin-bottom: 0.15rem;
  font-size: 1.1rem;
  opacity: 0.85;
}

.pulse-tile-value {
  font-size: 1.4rem;
  font-weight: 700;
  line-height: 1.1;
}

.pulse-tile-suffix {
  margin-left: 0.2rem;
  font-size: 0.72rem;
  font-weight: 500;
  opacity: 0.8;
}

.pulse-tile-label {
  font-size: 0.72rem;
  font-weight: 500;
  opacity: 0.85;
}

@media (max-width: 575.98px) {
  .pulse-strip {
    display: grid;
    grid-template-columns: 1fr 1fr;
    width: 100%;
  }

  .pulse-tile {
    min-width: 0;
  }
}

/* Shared shell for the three cockpit panels. */
.dash-panel .card-header {
  border-bottom: 0;
}

/* Neutral empty state — a panel with nothing in it should still look
   deliberate rather than broken. */
.dash-empty {
  padding: 1.5rem 0.5rem;
  text-align: center;
}

.dash-empty i {
  font-size: 2rem;
  color: var(--bs-secondary-color, #a1acb8);
  opacity: 0.6;
}

.dash-empty p {
  margin-top: 0.5rem;
  font-weight: 500;
}

/* ---- My Leave: one radial gauge per entitlement-bearing leave type ---- */
.leave-gauge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.5rem 0.75rem;
}

.leave-gauge {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.25rem;
  text-align: center;
  border-radius: 0.6rem;
  transition: background-color 0.18s ease;
}

.leave-gauge:hover {
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.06);
}

/* ApexCharts sets its own inline height; this keeps the row from jumping
   before the chart renders. */
.leave-gauge-chart {
  width: 100%;
  min-height: 150px;
}

.leave-gauge-meta {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  width: 100%;
  margin-top: -0.35rem;
}

.leave-gauge-name {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--bs-heading-color, inherit);
}

.leave-gauge-sub {
  font-size: 0.72rem;
  color: var(--bs-secondary-color, #a1acb8);
}

.leave-gauge-pending {
  align-self: center;
  margin-top: 0.15rem;
  font-size: 0.65rem;
  font-weight: 500;
}

.leave-gauge-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding-top: 0.9rem;
  margin-top: 0.9rem;
  border-top: 1px solid var(--bs-border-color);
}

.leave-gauge-footer a {
  text-decoration: none;
}

/* ---- Out of office ---- */
.absence-today {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.absence-person {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  width: 3.75rem;
  color: inherit;
  text-decoration: none;
}

.absence-person-name {
  max-width: 100%;
  font-size: 0.7rem;
  font-weight: 500;
}

.absence-person--more:hover .avatar-initial {
  filter: brightness(1.1);
}

/* Nobody out is good news, so it reads as a statement rather than an error. */
.absence-full-house {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  font-size: 0.85rem;
  font-weight: 500;
  background: rgba(var(--bs-success-rgb, 113, 221, 55), 0.1);
  border-radius: 0.5rem;
}

.absence-full-house i {
  font-size: 1.15rem;
}

.absence-forecast {
  padding-top: 0.9rem;
  margin-top: 0.9rem;
  border-top: 1px solid var(--bs-border-color);
}

.absence-forecast-title {
  font-size: 0.68rem;
  letter-spacing: 0.03em;
}

/* ---- Holiday countdown rail ---- */
.holiday-rail {
  padding: 0;
  margin: 0;
  list-style: none;
}

.holiday-item {
  display: flex;
  gap: 0.7rem;
  align-items: center;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--bs-border-color);
}

.holiday-item:last-child {
  border-bottom: 0;
}

/* Date medallion. The nearest holiday gets the solid treatment — it is the
   only one anyone is actually counting down to. */
.holiday-date {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 2.9rem;
  height: 2.9rem;
  color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.12);
  border-radius: 0.5rem;
}

.holiday-date strong {
  font-size: 1.05rem;
  line-height: 1;
}

.holiday-date small {
  font-size: 0.62rem;
  text-transform: uppercase;
}

.holiday-item--next .holiday-date {
  color: #fff;
  background: var(--bs-primary);
  box-shadow: 0 0.35rem 0.9rem rgba(var(--bs-primary-rgb, 105, 108, 255), 0.4);
}

.holiday-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.holiday-name {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--bs-heading-color, inherit);
}

.holiday-away {
  font-size: 0.72rem;
  color: var(--bs-secondary-color, #a1acb8);
}

/* ---- Dashboard noticeboard: Company Buzz tabs + merged celebrations ----
   These replaced five stacked full-width bands (news, birthday banner,
   shout-outs, anniversaries, blogs) with two side-by-side cards. Rows are
   deliberately dense — this is ambient information people scan, not read. */

.buzz-header {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: center;
  justify-content: space-between;
  border-bottom: 0;
}

/* Pill tabs, sized down so three fit beside the card title. */
.buzz-tabs {
  gap: 0.25rem;
}

.buzz-tabs .nav-link {
  display: flex;
  gap: 0.35rem;
  align-items: center;
  padding: 0.3rem 0.7rem;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--bs-secondary-color, #a1acb8);
  border-radius: 2rem;
}

.buzz-tabs .nav-link:hover:not(.active) {
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.08);
}

.buzz-tab-count {
  padding: 0 0.35rem;
  font-size: 0.65rem;
  font-weight: 600;
  background: rgba(0, 0, 0, 0.08);
  border-radius: 1rem;
}

.buzz-tabs .nav-link.active .buzz-tab-count {
  background: rgba(255, 255, 255, 0.25);
}

@media (max-width: 400px) {
  .buzz-tab-label {
    display: none;
  }
}

.buzz-list {
  padding: 0;
  margin: 0;
  list-style: none;
}

.buzz-row {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--bs-border-color);
}

.buzz-row:last-child {
  border-bottom: 0;
}

.buzz-thumb {
  flex: 0 0 auto;
  width: 3.25rem;
  height: 3.25rem;
  object-fit: cover;
  border-radius: 0.5rem;
}

.buzz-thumb--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.1);
}

/* The award pip on a shout-out recipient's avatar. */
.buzz-avatar {
  position: relative;
  flex: 0 0 auto;
}

.buzz-avatar-badge {
  position: absolute;
  right: -0.15rem;
  bottom: -0.15rem;
  padding: 0.1rem;
  font-size: 0.6rem;
  color: #fff;
  background: var(--bs-warning, #ffab00);
  border-radius: 50%;
}

.buzz-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: 0.1rem;
}

.buzz-title {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--bs-heading-color, inherit);
  text-decoration: none;
}

.buzz-title:hover {
  color: var(--bs-primary);
}

/* Two lines of excerpt, then ellipsis — enough to judge whether to open it. */
.buzz-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 0.75rem;
  color: var(--bs-secondary-color, #a1acb8);
}

.buzz-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.5rem;
  align-items: center;
  font-size: 0.7rem;
  color: var(--bs-secondary-color, #a1acb8);
}

.buzz-meta .badge {
  padding: 0.1rem 0.35rem;
  font-size: 0.6rem;
}

.buzz-more {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  margin-top: 0.75rem;
  font-size: 0.78rem;
  font-weight: 500;
  text-decoration: none;
}

/* ---- Merged celebrations ---- */
.celebrate-emoji {
  font-size: 1.05rem;
}

/* Today's birthday keeps the festive gradient, but as a strip rather than a
   full-width band. */
.celebrate-today {
  position: relative;
  display: flex;
  gap: 0.6rem;
  align-items: center;
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.75rem;
  overflow: hidden;
  color: #fff;
  background: linear-gradient(120deg, #f5576c 0%, #f093fb 100%);
  border-radius: 0.6rem;
  box-shadow: 0 0.4rem 1rem rgba(245, 87, 108, 0.3);
}

.celebrate-cake {
  font-size: 1.5rem;
}

.celebrate-today-text {
  min-width: 0;
  font-size: 0.85rem;
  line-height: 1.3;
}

.celebrate-today-text small {
  font-size: 0.72rem;
  opacity: 0.9;
}

.celebrate-confetti {
  margin-left: auto;
  font-size: 1.15rem;
}

.celebrate-list {
  padding: 0;
  margin: 0;
  list-style: none;
}

.celebrate-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--bs-border-color);
}

.celebrate-row:last-child {
  border-bottom: 0;
}

/* The ring colour carries the milestone tier for anniversaries; birthdays
   fall back to the primary ring. */
.celebrate-avatar {
  position: relative;
  flex: 0 0 auto;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--celebrate-ring, var(--bs-primary));
}

.celebrate-avatar--teal {
  --celebrate-ring: #03c3e3;
}

.celebrate-avatar--purple {
  --celebrate-ring: #8c57ff;
}

.celebrate-avatar--gold {
  --celebrate-ring: #ffab00;
}

.celebrate-kind {
  position: absolute;
  right: -0.3rem;
  bottom: -0.3rem;
  font-size: 0.7rem;
  line-height: 1;
}

.celebrate-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin-right: auto;
}

.celebrate-name {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--bs-heading-color, inherit);
}

.celebrate-detail {
  overflow: hidden;
  font-size: 0.7rem;
  color: var(--bs-secondary-color, #a1acb8);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.celebrate-when {
  flex: 0 0 auto;
  padding: 0.15rem 0.5rem;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.1);
  border-radius: 1rem;
}

/* ---- The viewer's own celebration ----
   On your birthday or work anniversary the welcome banner changes clothes:
   a warmer gradient, a medallion, and confetti falling behind the text. It is
   one day a year, so it is allowed to be louder than the rest of the page. */

.dashboard-welcome--celebrate {
  background: linear-gradient(120deg, #f5576c 0%, #f093fb 34%, #7367f0 68%, #ffab00 100%);
  box-shadow: 0 0.75rem 1.75rem rgba(245, 87, 108, 0.32);
}

/* An anniversary is a milestone rather than a party — gold and violet, not pink. */
.dashboard-welcome--anniversary {
  background: linear-gradient(120deg, #6a11cb 0%, #7367f0 34%, #ffab00 82%, #ffd76e 100%);
  box-shadow: 0 0.75rem 1.75rem rgba(106, 17, 203, 0.32);
}

.celebrate-medallion {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  margin-bottom: 0.5rem;
  font-size: 1.6rem;
  line-height: 1;
  background: rgba(255, 255, 255, 0.22);
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 50%;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
  backdrop-filter: blur(6px);
  animation: celebrate-pop 2.4s ease-in-out infinite;
}

@keyframes celebrate-pop {
  0%,
  100% {
    transform: scale(1) rotate(-4deg);
  }

  50% {
    transform: scale(1.08) rotate(4deg);
  }
}

/* Confetti: a dozen absolutely-positioned bits drifting down the banner.
   Each gets its colour, size, start offset and timing from :nth-child rather
   than from markup, so the template stays a bare list of empty spans. */
.confetti {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.confetti-bit {
  position: absolute;
  top: -12%;
  width: 8px;
  height: 14px;
  background: #fff;
  border-radius: 1px;
  opacity: 0.85;
  animation: confetti-fall 5.5s linear infinite;
}

.confetti-bit:nth-child(3n) {
  width: 7px;
  height: 7px;
  border-radius: 50%;
}

.confetti-bit:nth-child(4n) {
  width: 6px;
  height: 12px;
}

.confetti-bit:nth-child(5n + 1) {
  background: #ffd76e;
}

.confetti-bit:nth-child(5n + 2) {
  background: #a5f3e0;
}

.confetti-bit:nth-child(5n + 3) {
  background: #ffffff;
}

.confetti-bit:nth-child(5n + 4) {
  background: #ffc4dd;
}

.confetti-bit:nth-child(5n + 5) {
  background: #c9c2ff;
}

.confetti-bit:nth-child(1) { left: 4%; animation-delay: -0.2s; animation-duration: 5.1s; }
.confetti-bit:nth-child(2) { left: 12%; animation-delay: -1.7s; animation-duration: 6.3s; }
.confetti-bit:nth-child(3) { left: 21%; animation-delay: -3.1s; animation-duration: 4.6s; }
.confetti-bit:nth-child(4) { left: 29%; animation-delay: -0.9s; animation-duration: 5.8s; }
.confetti-bit:nth-child(5) { left: 37%; animation-delay: -2.4s; animation-duration: 6.9s; }
.confetti-bit:nth-child(6) { left: 46%; animation-delay: -4.2s; animation-duration: 5.3s; }
.confetti-bit:nth-child(7) { left: 54%; animation-delay: -1.1s; animation-duration: 6.1s; }
.confetti-bit:nth-child(8) { left: 63%; animation-delay: -3.6s; animation-duration: 4.9s; }
.confetti-bit:nth-child(9) { left: 71%; animation-delay: -0.5s; animation-duration: 6.6s; }
.confetti-bit:nth-child(10) { left: 79%; animation-delay: -2.8s; animation-duration: 5.5s; }
.confetti-bit:nth-child(11) { left: 87%; animation-delay: -4.7s; animation-duration: 6.2s; }
.confetti-bit:nth-child(12) { left: 93%; animation-delay: -1.9s; animation-duration: 5.0s; }
.confetti-bit:nth-child(13) { left: 97%; animation-delay: -3.9s; animation-duration: 6.8s; }

@keyframes confetti-fall {
  0% {
    transform: translateY(0) rotate(0deg);
    opacity: 0;
  }

  8% {
    opacity: 0.9;
  }

  100% {
    transform: translateY(320px) rotate(540deg);
    opacity: 0;
  }
}

/* Motion is decoration here, never information: stop it entirely for anyone
   who has asked the OS for less of it, leaving the colours and medallion. */
@media (prefers-reduced-motion: reduce) {
  .confetti-bit,
  .celebrate-medallion {
    animation: none;
  }

  .confetti-bit {
    opacity: 0.5;
  }
}

/* ---- Noticeboard row: Company Buzz and Celebrations end level ----
   The two cards carry unrelated content of unrelated length, so left alone
   one finishes well above the other and the row reads as broken. Both stretch
   to the row height; inside, the list takes the slack and the footer link is
   pinned to the bottom, so the extra height becomes even spacing between rows
   instead of a void under the last one. */

.dash-feed .card-body {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.dash-feed .tab-content,
.dash-feed .tab-pane.active {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
}

/* space-around rather than flex-start: a short list breathes into the spare
   height instead of huddling at the top of a tall card. Capped by max-height
   so five rows never drift a finger-width apart. */
.dash-feed .buzz-list,
.dash-feed .celebrate-list {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  justify-content: space-around;
  min-height: 0;
}

.dash-feed .buzz-more,
.dash-feed .celebrate-more {
  margin-top: auto;
  padding-top: 0.75rem;
}

/* The footer link sits on the card's bottom edge, so the rule above it does
   the separating rather than a gap. */
.celebrate-more {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  font-size: 0.78rem;
  font-weight: 500;
  text-decoration: none;
}

/* ---- URJA winners: a horizontal roll ----
   Winners used to be portrait cards in a 4-column grid. A quarter with one
   winner then produced a tall band that was three-quarters empty. On their
   side the tiles are short, they centre when there are few and wrap when
   there are many, and the band costs about half the height it did. */

.urja-winner-roll {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  align-items: stretch;
  justify-content: center;
}

.urja-winner-tile {
  display: flex;
  flex: 1 1 20rem;
  /* A flex item defaults to min-width:auto, which refuses to shrink below its
     own content — on a 360px phone the 20rem basis then pushed the tile past
     the card instead of narrowing. */
  min-width: 0;
  gap: 0.85rem;
  align-items: center;
  /* Deliberately uncapped: a quarter with a single winner fills the band
     rather than floating as an island in the middle of it. Several winners
     share the width evenly and wrap at 20rem each. */
  padding: 0.75rem 0.9rem;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.6rem;
}

.urja-winner-figure {
  flex: 0 0 auto;
}

.urja-winner-detail {
  min-width: 0;  /* lets text-truncate actually truncate inside a flex child */
}

.urja-winner-citation {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  -webkit-line-clamp: 2;  /* a long citation must not re-inflate the band */
  /* A citation can carry a URL, a product code or a long unhyphenated name;
     without this the token refuses to wrap and widens the whole tile. */
  overflow-wrap: anywhere;
}

/* ---- Celebration poster download (HR only) ----
   A quiet icon button on the row it belongs to. Kept low-contrast until
   hover: HR scans this list for the celebration, not for the button. */

.celebrate-poster {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  color: var(--bs-secondary-color, #a1acb8);
  text-decoration: none;
  border-radius: 0.4rem;
  transition:
    color 0.15s ease,
    background-color 0.15s ease;
}

.celebrate-poster:hover,
.celebrate-poster:focus-visible {
  color: var(--bs-primary);
  background: rgba(var(--bs-primary-rgb, 105, 108, 255), 0.12);
}

/* On the festive today-strip the ground is a gradient, so the button borrows
   the white it sits on rather than the muted body colour. */
.celebrate-poster--on-strip {
  color: rgba(255, 255, 255, 0.85);
}

.celebrate-poster--on-strip:hover,
.celebrate-poster--on-strip:focus-visible {
  color: #fff;
  background: rgba(255, 255, 255, 0.22);
}

/* ---- Concept credit ----
   "Concept by <person>", sitting under a feature page's own subtitle — see
   app_base/templatetags/credits.py. Left-aligned with the heading it follows,
   and small enough not to compete with it. */

.concept-credit {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
  justify-content: flex-start;
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
  color: var(--bs-secondary-color, #a1acb8);
}

.concept-credit i {
  font-size: 0.95rem;
  color: var(--bs-warning);
}

.concept-credit-role::before {
  content: "·";
  margin-right: 0.4rem;
}

/* ---- Who's online ----
   Presence states are carried by colour alone in the dot, so every dot is
   paired with its word ("Online", "Idle") rather than left to speak for
   itself. */

.presence-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  align-items: center;
  margin-bottom: 1rem;
  font-size: 0.82rem;
  color: var(--bs-secondary-color, #a1acb8);
}

.presence-count {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
}

.presence-dot {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: var(--presence-colour, #a1acb8);
}

.presence-dot--online {
  --presence-colour: #28c76f;
  box-shadow: 0 0 0 3px rgba(40, 199, 111, 0.2);
}

.presence-dot--idle {
  --presence-colour: #ff9f43;
  box-shadow: 0 0 0 3px rgba(255, 159, 67, 0.2);
}

.presence-dot--away {
  --presence-colour: #a1acb8;
}

.presence-pill {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.15rem 0.6rem;
  font-size: 0.72rem;
  font-weight: 600;
  border-radius: 2rem;
  background: rgba(161, 172, 184, 0.12);
  color: var(--bs-secondary-color, #a1acb8);
}

.presence-pill--online {
  background: rgba(40, 199, 111, 0.14);
  color: #28c76f;
}

.presence-pill--idle {
  background: rgba(255, 159, 67, 0.14);
  color: #ff9f43;
}

/* The status ring repeats on the avatar so a long list can be scanned down the
   left edge without reading across to the pill. */
.presence-avatar {
  box-shadow: 0 0 0 2px var(--presence-ring, transparent);
}

.presence-avatar--online {
  --presence-ring: #28c76f;
}

.presence-avatar--idle {
  --presence-ring: #ff9f43;
}

.presence-path {
  display: inline-block;
  max-width: 22rem;
  overflow: hidden;
  font-size: 0.75rem;
  color: var(--bs-secondary-color, #a1acb8);
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* Bootstrap has no min-width utility, and a flex child needs min-width:0
   before text-truncate does anything inside it. */
.min-w-0 {
  min-width: 0;
}

/* A campaign button as it appears inside the composer. The stored element is
   an inert marker (see marketing-editor.js); this is only so the author sees a
   button rather than a stray line of text. The email's own markup is built at
   render time and owes nothing to these rules. */
.ql-editor .email-cta {
  display: inline-block;
  padding: 12px 26px;
  margin: 8px 0;
  border-radius: 6px;
  background-color: #891b03;
  color: #fff;
  font-weight: 600;
  cursor: default;
  user-select: none;
}

/* Paragraph spacing in the campaign composer, matched to an inbox. Quill's own
   stylesheet gives paragraphs no margin, so a body looked tight in the editor
   and arrived spread out: every mail client puts a line's worth of space after
   a <p>. Showing that gap here is what makes the "tight lines" button visible
   — rendering.inline_editor_styles() sends the same zero margin inline. */
#marketing-editor .ql-editor p {
  margin: 0 0 1em;
}
#marketing-editor .ql-editor p:last-child,
#marketing-editor .ql-editor .ql-compact-tight {
  margin: 0;
}

/* The size/alignment bar that appears over a selected image, in the composer
   (#image-tools) and in the shared editor (built by richtext.js). */
#image-tools .btn-xs,
.richtext-image-tools .btn-xs {
  padding: 0.1rem 0.4rem;
  font-size: 0.75rem;
  line-height: 1.4;
}

/* Rich-text editor (blog / news / shout out forms). The host is mounted next
   to the hidden textarea by richtext.js; the editor's own chrome comes from
   vendor/libs/quill/editor.css, this only sizes it and marks an invalid
   field the way a .form-control would be marked. */
.richtext-editor {
  min-height: 14rem;
  border-bottom-left-radius: var(--bs-border-radius);
  border-bottom-right-radius: var(--bs-border-radius);
}
.richtext-editor .ql-editor {
  min-height: 14rem;
}
.richtext-editor.is-invalid {
  border-color: var(--bs-danger);
}
/* Paragraph spacing matched to the rendered post (Bootstrap gives a <p> a
   1rem gap; Quill's own stylesheet gives it none), which is also what makes
   the "tight lines" button visible in the editor — the same rule the
   composer uses. */
.richtext-editor .ql-editor p {
  margin: 0 0 1em;
}
.richtext-editor .ql-editor p:last-child,
.richtext-editor .ql-editor .ql-compact-tight {
  margin: 0;
}
.richtext-editor .ql-editor img {
  max-width: 100%;
  height: auto;
}

/* The panel that collects a button's label and link, under the editor. Its
   own panel rather than a modal: the form is already inside one. */
.richtext-cta {
  margin-top: 0.5rem;
  padding: 0.75rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  background: var(--bs-tertiary-bg);
}

/* The source view: the field's own textarea, shown in place of the editor
   while the HTML is being edited by hand. It joins the toolbar the way the
   editor does, so the toggle reads as a change of view rather than a change
   of field. */
.richtext-source:not(.d-none) {
  min-height: 14rem;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  font-family: var(--bs-font-monospace);
  font-size: 0.8125rem;
  line-height: 1.5;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}
/* Formatting controls act on the hidden editor while the source is showing,
   so they are put out of reach until the view comes back. */
.richtext-toolbar--source .ql-formats:not(:has(.ql-source)) {
  opacity: 0.35;
  pointer-events: none;
}

/* The heading picker. Sneat's editor.css names the options "Heading 2" /
   "Heading 3" and renders each at its real heading size, which in a
   three-entry list reads as a 38px item over a 28px one over a 15px one. Post
   authors think in "heading" and "subheading", not h-levels, so name them
   that and keep the list a readable size (bold and slightly larger is enough
   to tell them apart). Same rules on the label so the closed picker agrees
   with the open one. */
.richtext-toolbar .ql-picker.ql-header {
  inline-size: 7.5rem;
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-options {
  padding-block: 0.25rem;
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-item {
  padding-block: 0.375rem;
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-label::before,
.richtext-toolbar .ql-picker.ql-header .ql-picker-item::before {
  content: "Normal";
  font-size: 0.9375rem;
  font-weight: 400;
  line-height: 1.375rem;
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.richtext-toolbar .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  content: "Heading";
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.richtext-toolbar .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  content: "Subheading";
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  font-size: 1.25rem;
  font-weight: 600;
}
.richtext-toolbar .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  font-size: 1.0625rem;
  font-weight: 600;
}

/* Rendered bodies, outside the editor. Quill records alignment and indent as
   classes, which editor.css only styles inside .ql-editor. */
.richtext-content {
  overflow-wrap: anywhere;
}
.richtext-content p:last-child {
  margin-bottom: 0;
}
.richtext-content h2,
.richtext-content h3 {
  margin-top: 1.25rem;
}
.richtext-content blockquote {
  border-left: 4px solid var(--bs-border-color);
  margin: 0 0 1rem;
  padding: 0.5rem 1rem;
  color: var(--bs-secondary-color);
}
.richtext-content pre {
  background: var(--bs-tertiary-bg);
  border-radius: var(--bs-border-radius);
  padding: 0.75rem 1rem;
  white-space: pre-wrap;
}
.richtext-content img {
  max-width: 100%;
  height: auto;
}
/* "Tight lines": a paragraph with no gap after it (see richtext.js). */
.richtext-content .ql-compact-tight {
  margin: 0;
}
/* A button marker, rendered as a link by the richtext filter. */
.richtext-content .richtext-button {
  margin: 0.5rem 0 1rem;
}
.richtext-content .ql-align-center {
  text-align: center;
}
.richtext-content .ql-align-right {
  text-align: right;
}
.richtext-content .ql-align-justify {
  text-align: justify;
}
.richtext-content .ql-indent-1 { padding-left: 2rem; }
.richtext-content .ql-indent-2 { padding-left: 4rem; }
.richtext-content .ql-indent-3 { padding-left: 6rem; }
.richtext-content .ql-indent-4 { padding-left: 8rem; }
.richtext-content .ql-indent-5 { padding-left: 10rem; }
.richtext-content .ql-indent-6 { padding-left: 12rem; }
.richtext-content .ql-indent-7 { padding-left: 14rem; }
.richtext-content .ql-indent-8 { padding-left: 16rem; }

/* Small "Beta" pill shown next to features still under trial (menu, dashboard tiles, page headings). */
.badge-beta {
  font-size: 0.625rem;
  font-weight: 500;
  line-height: 1.2;
  padding: 0.15em 0.5em;
  letter-spacing: 0.02em;
  vertical-align: middle;
}

/* Events — same shape as the news cover so cards line up in shared grids. */
.event-cover {
  height: 180px;
  object-fit: cover;
}

.event-cover-empty {
  height: 180px;
  color: #7367f0;
}

.event-cover-lg {
  max-height: 380px;
  object-fit: cover;
}

/* Inline PDF viewer on an event resource page: taller than wide. */
.pdf-frame {
  --bs-aspect-ratio: 130%;
}

.register-type-card:hover {
  border-color: #696cff !important;
  background-color: rgba(105, 108, 255, 0.04);
}

/* Registration pages: the details form is a two-column grid, so the card is
   wider than the sign-in one. The vendor rule that caps the auth card has three
   classes and loads later, so match its specificity here. */
.authentication-wrapper.authentication-basic .authentication-inner.wide {
  max-inline-size: 720px;
  /* page-auth.css locks html/body scrolling and pins the card at the vertical
     centre, so a form taller than the screen (job applications) loses its
     submit button off the bottom. Scroll inside the card instead. */
  max-block-size: calc(100vh - 2rem);
  overflow-y: auto;
}

.register-event-thumb {
  width: 96px;
  height: 50px;
  object-fit: cover;
}

/* "New here?" chooser on the sign-in pages (app_register/partials/_register_cta.html).
   Three equal cards that lift on hover; the blurb is hidden on narrow screens
   so the trio stays on one row down to phone width. */
.register-cta-card {
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.register-cta-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 0.25rem 0.75rem rgba(105, 108, 255, 0.15) !important;
}
.register-cta-card .avatar-initial {
  transition: background-color 0.15s ease, color 0.15s ease;
}
.register-cta-card:hover .avatar-initial {
  background-color: #696cff !important;
  color: #fff !important;
}
.register-cta-blurb {
  font-size: 0.72rem;
  line-height: 1.25;
}
.register-cta-perks .list-inline-item:not(:last-child) {
  margin-right: 0.6rem;
}
@media (max-width: 480px) {
  .register-cta-blurb {
    display: none;
  }
}

/* Sign-in pages: the form on the left, the "new here?" chooser on the right.
   The auth card is fixed-centred and capped by --bs-auth-basic-inner-max-width,
   so widen it here (same specificity as the vendor rule, declared later) and
   fall back to stacked panes below the lg breakpoint. */
.authentication-wrapper.authentication-basic .authentication-inner.auth-split {
  max-inline-size: 960px;
}
@media (min-width: 992px) {
  .auth-split-left {
    padding-right: 2.5rem;
  }
  .auth-split-right {
    padding-left: 2.5rem;
    border-left: 1px solid var(--bs-border-color);
  }
  .auth-split-right .register-cta {
    width: 100%;
  }
}
@media (max-width: 991.98px) {
  .auth-split-right {
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--bs-border-color);
  }
}

/* Recruitment: the checkbox lists on the public application form. Django's
   CheckboxSelectMultiple nests each option in its own <div>; give them the
   Bootstrap form-check spacing without a wrapper class on the widget. */
.recruit-checks > div > div {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.recruit-checks label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0;
}
.recruit-sticky {
  top: 1rem;
}
