/* ==========================================================================
 * Global typography scale — site-wide readability knob.
 *
 * Dial --vilvik-font-scale to grow/shrink all text site-wide: body,
 * headings, form labels, Phoenix .fs-* utility classes, and .small /
 * <small>. Layout dimensions (padding, margins, widths) are NOT scaled,
 * they stay anchored to Phoenix's 1rem so the layout is undisturbed.
 *
 *   1.0    = Phoenix default (~16px body)
 *   1.0625 = +6.25%  (~17px,  subtle nudge)
 *   1.125  = +12.5%  (~18px,  current default, comfortable)
 *   1.2    = +20%    (~19px,  accessibility-leaning)
 *
 * To change the global text size, edit the single value below and reload.
 * ========================================================================== */
:root {
  --vilvik-font-scale: 1.125;
}

body {
  font-size: calc(1rem * var(--vilvik-font-scale));
}

h1, .h1 { font-size: calc((1.369140625rem + 1.4296875vw) * var(--vilvik-font-scale)); }
h2, .h2 { font-size: calc((1.3203125rem + 0.84375vw) * var(--vilvik-font-scale)); }
h3, .h3 { font-size: calc((1.28125rem + 0.375vw) * var(--vilvik-font-scale)); }
h4, .h4 { font-size: calc(1.25rem * var(--vilvik-font-scale)); }
h5, .h5 { font-size: calc(1rem * var(--vilvik-font-scale)); }
h6, .h6 { font-size: calc(0.8rem * var(--vilvik-font-scale)); }
@media (min-width: 1200px) {
  h1, .h1 { font-size: calc(2.44140625rem * var(--vilvik-font-scale)); }
  h2, .h2 { font-size: calc(1.953125rem * var(--vilvik-font-scale)); }
  h3, .h3 { font-size: calc(1.5625rem * var(--vilvik-font-scale)); }
}

/* Phoenix .fs-* utility classes (theme.css uses !important on these, so
 * we must too). Smallest sizes (.fs-9, .fs-10, .fs-11) are also lifted off
 * their too-small Phoenix defaults of 0.8 / 0.64 / 0.512 rem. */
.fs-1  { font-size: calc(4.768371582rem * var(--vilvik-font-scale)) !important; }
.fs-2  { font-size: calc(3.8146972656rem * var(--vilvik-font-scale)) !important; }
.fs-3  { font-size: calc(3.0517578125rem * var(--vilvik-font-scale)) !important; }
.fs-4  { font-size: calc(2.44140625rem * var(--vilvik-font-scale)) !important; }
.fs-5  { font-size: calc(1.953125rem * var(--vilvik-font-scale)) !important; }
.fs-6  { font-size: calc(1.5625rem * var(--vilvik-font-scale)) !important; }
.fs-7  { font-size: calc(1.25rem * var(--vilvik-font-scale)) !important; }
.fs-8  { font-size: calc(1rem * var(--vilvik-font-scale)) !important; }
.fs-9  { font-size: calc(0.875rem * var(--vilvik-font-scale)) !important; }
.fs-10 { font-size: calc(0.8rem * var(--vilvik-font-scale)) !important; }
.fs-11 { font-size: calc(0.7rem * var(--vilvik-font-scale)) !important; }

/* Phoenix hardcodes .form-label to 0.64rem uppercase (theme.css:3561 and
 * :27893). The uppercase / bold / colour treatment is preserved untouched,
 * only the font-size is lifted to a readable baseline then scaled. */
.form-label {
  font-size: calc(0.875rem * var(--vilvik-font-scale)) !important;
}

.small, small {
  font-size: calc(0.875rem * var(--vilvik-font-scale)) !important;
}


/* ==========================================================================
 * Profile picture dropdown — custom overrides on top of the Phoenix theme.
 * Trigger gets idle pulse, hover glow, rotating-border focus state, and a
 * tap-down feedback. Dropdown panel auto-sizes to its content (no forced
 * scroll area), uses a horizontal header, and groups the admin entry under
 * a divider with a danger-emphasis treatment that stays readable in both
 * Phoenix light and dark themes.
 * ========================================================================== */

/* ---------- Trigger avatar (top navbar) ---------- */

.profile-trigger-link,
.profile-trigger-link:hover,
.profile-trigger-link:focus,
.profile-trigger-link:focus-visible,
.profile-trigger-link:active {
  cursor: pointer;
  text-decoration: none;
  outline: none;
  box-shadow: none;
}

.profile-trigger-avatar {
  position: relative;
  display: inline-block;
  isolation: isolate;
  border-radius: 50%;
  transition: transform .2s ease, filter .25s ease;
  will-change: transform;
}

.profile-trigger-avatar .avatar-initials {
  width: 100%;
  height: 100%;
  color: #fff;
  font-weight: 600;
  font-size: .8125rem;
  letter-spacing: .02em;
  user-select: none;
}

/* Idle: gentle pulsing ring */
.profile-trigger-avatar::before {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  border: 2px solid var(--phoenix-primary);
  opacity: .4;
  animation: profile-pulse 2.4s ease-out infinite;
  pointer-events: none;
  z-index: 1;
}

@keyframes profile-pulse {
  0%   { transform: scale(1);    opacity: .5;  }
  70%  { transform: scale(1.22); opacity: 0;   }
  100% { transform: scale(1.22); opacity: 0;   }
}

/* Hover: subtle scale + colored glow */
.profile-trigger-link:hover .profile-trigger-avatar,
.profile-trigger-link:focus-visible .profile-trigger-avatar {
  transform: scale(1.06);
  filter: drop-shadow(0 0 10px rgba(56, 116, 255, .55));
}

/* Active / tap-down feedback (mouse + touch) */
.profile-trigger-link:active .profile-trigger-avatar {
  transform: scale(.94);
  transition-duration: .08s;
}

/* Open state: rotating conic-gradient ring + pause the idle pulse */
#navbarDropdownUser[aria-expanded="true"] .profile-trigger-avatar::before {
  animation: none;
  opacity: 0;
}

#navbarDropdownUser[aria-expanded="true"] .profile-trigger-avatar::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    var(--phoenix-primary),
    var(--phoenix-info),
    var(--phoenix-success),
    var(--phoenix-warning),
    var(--phoenix-primary));
  animation: profile-rotate 3.2s linear infinite;
  z-index: -1;
  pointer-events: none;
}

@keyframes profile-rotate {
  to { transform: rotate(360deg); }
}

/* Online status dot — Phoenix already supplies .status-online; this just
   nudges it to the bottom-right corner of the trigger avatar. */
.profile-trigger-status {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
}

/* Reduced motion — keep static states, drop loops. */
@media (prefers-reduced-motion: reduce) {
  .profile-trigger-avatar::before,
  #navbarDropdownUser[aria-expanded="true"] .profile-trigger-avatar::after {
    animation: none;
  }
  .profile-trigger-avatar,
  .profile-trigger-link:hover .profile-trigger-avatar,
  .profile-trigger-link:active .profile-trigger-avatar {
    transition: none;
  }
}

/* ---------- Dropdown panel ---------- */

.dropdown-profile-compact {
  min-width: 17rem;
  border-radius: .75rem;
  overflow: hidden;
  transform-origin: top right;
}

.dropdown-profile-compact.show {
  animation: profile-dropdown-in .18s ease-out both;
}

@keyframes profile-dropdown-in {
  from { opacity: 0; transform: translateY(-6px) scale(.97); }
  to   { opacity: 1; transform: translateY(0)    scale(1);   }
}

@media (prefers-reduced-motion: reduce) {
  .dropdown-profile-compact.show { animation: none; }
}

/* Header (avatar + name, horizontal) */
.profile-header .avatar-initials {
  width: 100%;
  height: 100%;
  color: #fff;
  font-weight: 600;
  font-size: 1rem;
}

.profile-header-text { min-width: 0; }
.profile-header-username:hover { text-decoration: underline; }

/* ---------- Menu items ---------- */

.profile-menu-list { list-style: none; padding-left: 0; }

.profile-menu-item {
  position: relative;
  display: flex;
  align-items: center;
  margin: .125rem .5rem;
  padding: .5rem .75rem;
  border-radius: .5rem;
  color: var(--phoenix-body-color);
  transition:
    background-color .15s ease,
    color .15s ease,
    padding-left .15s ease;
}

.profile-menu-item [data-feather] {
  width: 16px;
  height: 16px;
  transition: transform .15s ease;
}

.profile-menu-item:hover,
.profile-menu-item:focus-visible {
  background-color: var(--phoenix-secondary-bg);
  color: var(--phoenix-body-emphasis-color);
  padding-left: 1rem;
  text-decoration: none;
}

.profile-menu-item:hover [data-feather],
.profile-menu-item:focus-visible [data-feather] {
  transform: translateX(3px);
}

.profile-menu-divider {
  list-style: none;
  margin: 0;
}

/* Admin row — Phoenix dark-safe danger treatment (subtle bg + emphasis text,
   never raw bg-danger / text-white). */
.profile-menu-admin,
.profile-menu-admin [data-feather] {
  color: var(--phoenix-danger-text-emphasis, var(--phoenix-danger));
}

.profile-menu-admin:hover,
.profile-menu-admin:focus-visible {
  background-color: var(--phoenix-danger-bg-subtle, rgba(237, 32, 0, .1));
  color: var(--phoenix-danger-text-emphasis, var(--phoenix-danger));
}

.profile-menu-admin:hover [data-feather],
.profile-menu-admin:focus-visible [data-feather] {
  color: var(--phoenix-danger-text-emphasis, var(--phoenix-danger));
}

/* ---------- Sign-out footer ---------- */

.profile-menu-footer { background-color: transparent; }

.profile-signout-btn [data-feather] {
  width: 16px;
  height: 16px;
  transition: transform .15s ease;
}

.profile-signout-btn:hover [data-feather] {
  transform: translateX(-3px);
}

/* ==========================================================================
 * Animated VILVIK logo — used by the partial home/_animated_logo_mark.html
 * on the sign-in page, the email-verified success page, and the page
 * navigation overlay. The mark has SIX parts: outer V left/right strokes,
 * inner V left/right strokes, and two horns. Each part animates from its
 * own scatter direction into a six-source build.
 *   --once    plays the build on page load and settles.
 *   --overlay plays the build every time `.page-navigation-overlay.active`
 *             becomes true, then a quiet idle pulse so a long wait still
 *             shows life.
 * Variants:
 *   --color   full-colour V-mark (default; use on dark backgrounds).
 *   --dark    theme-aware monochrome via CSS fill override; the transparent
 *             gaps between paths keep the V structure readable on light
 *             pages.
 *
 * Structural class names map to the SVG groups:
 *   __outer-l / __outer-r   the two blue strokes of the outer V
 *   __inner-l / __inner-r   the two blue strokes of the inner V
 *   __horn-l  / __horn-r    the two gold horns
 * ========================================================================== */

.vlogo-anim {
  display: inline-block;
  line-height: 0;
}

.vlogo-anim svg {
  display: block;
  overflow: visible;
}

.vlogo-anim__outer-l,
.vlogo-anim__outer-r,
.vlogo-anim__inner-l,
.vlogo-anim__inner-r,
.vlogo-anim__horn-l,
.vlogo-anim__horn-r {
  transform-box: fill-box;
  transform-origin: center;
  transition: fill .35s ease;
}

/* Dark variant: replace each fill with a theme-aware emphasis colour. The
   transparent gaps between paths keep the V structure visible. Hover/focus
   reveals the brand palette + amplifies the glow, mirroring the footer
   V-mark behaviour (homeapp/templates/home/default.html .footer-vmark). */
.vlogo-anim--dark .vlogo-anim__outer-l path,
.vlogo-anim--dark .vlogo-anim__outer-r path,
.vlogo-anim--dark .vlogo-anim__inner-l path,
.vlogo-anim--dark .vlogo-anim__inner-r path,
.vlogo-anim--dark .vlogo-anim__horn-l,
.vlogo-anim--dark .vlogo-anim__horn-r {
  /* --phoenix-emphasis-color is #141824 on light theme and #eff2f6 on dark,
     so the silhouette stays high-contrast against the page bg in both
     themes. (--phoenix-body-emphasis-color isn't redefined by Phoenix.) */
  fill: var(--phoenix-emphasis-color, #141824);
}

.vlogo-anim--dark:hover               .vlogo-anim__outer-l path,
.vlogo-anim--dark:focus-visible       .vlogo-anim__outer-l path,
:focus-visible .vlogo-anim--dark      .vlogo-anim__outer-l path,
.vlogo-anim--dark:hover               .vlogo-anim__outer-r path,
.vlogo-anim--dark:focus-visible       .vlogo-anim__outer-r path,
:focus-visible .vlogo-anim--dark      .vlogo-anim__outer-r path,
.vlogo-anim--dark:hover               .vlogo-anim__inner-l path,
.vlogo-anim--dark:focus-visible       .vlogo-anim__inner-l path,
:focus-visible .vlogo-anim--dark      .vlogo-anim__inner-l path,
.vlogo-anim--dark:hover               .vlogo-anim__inner-r path,
.vlogo-anim--dark:focus-visible       .vlogo-anim__inner-r path,
:focus-visible .vlogo-anim--dark      .vlogo-anim__inner-r path { fill: #2563EB; }
.vlogo-anim--dark:hover               .vlogo-anim__horn-l,
.vlogo-anim--dark:focus-visible       .vlogo-anim__horn-l,
:focus-visible .vlogo-anim--dark      .vlogo-anim__horn-l,
.vlogo-anim--dark:hover               .vlogo-anim__horn-r,
.vlogo-anim--dark:focus-visible       .vlogo-anim__horn-r,
:focus-visible .vlogo-anim--dark      .vlogo-anim__horn-r { fill: #F59E0B; }

/* Subtle primary-coloured glow at rest, stronger on hover/focus. Same drop-
   shadow values as .footer-vmark-img so the two marks feel consistent. */
.vlogo-anim--dark svg {
  filter: drop-shadow(0 0 14px rgba(var(--phoenix-primary-rgb), 0.18));
  transition: filter .4s ease;
}
.vlogo-anim--dark:hover svg,
.vlogo-anim--dark:focus-visible svg,
:focus-visible .vlogo-anim--dark svg {
  filter: drop-shadow(0 0 22px rgba(var(--phoenix-primary-rgb), 0.45));
}

/* ==========================================================================
 * V-mark animation candidates.
 *
 * Each candidate is a `vlogo-anim--<name>` modifier class applied to the
 * partial wrapper. The active candidate is picked by SiteSettings and
 * injected via the `effective_logo_candidate` template tag, which lives
 * at homeapp/templatetags/logo_anim.py. Independent slots for once-mode
 * (page-load build) and overlay-mode (loading loop) — admins pick from
 * /admincustom/logo-animation-preview/.
 *
 * Six candidate keys: cascade, scatter6, draw-on, spiral, color-wave,
 * origami. Each candidate animates ALL six SVG parts as distinct actors
 * (no synchronous left/right mirror pairing) so the split-V structure is
 * always perceptible. The hand-edited candidate CSS files at
 * static/css/logo-animations/* were inlined here so the rules ship with
 * the rest of the theme on every page that loads user.min.css.
 *
 * A single `prefers-reduced-motion: reduce` block at the bottom disables
 * every candidate's animation; the static logo renders in its assembled
 * state with no transforms / fills overrides.
 * ========================================================================== */

/* Footer V-mark style breathing pulse used by the dark variant so the
   assembled mark gently lives at rest. */
@keyframes vlogo-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}
@media (prefers-reduced-motion: no-preference) {
  .vlogo-anim--dark svg {
    animation: vlogo-breathe 4.5s ease-in-out 1.7s infinite;
    transform-origin: center;
  }
}

/* ---------- Candidate: Cascade --------------------------------------------
   Sequential anatomical drop. Perimeter order:
     horn-l → outer-l → inner-l → inner-r → outer-r → horn-r
   ------------------------------------------------------------------------- */
@keyframes vlogo-cascade-drop {
  0%   { opacity: 0; transform: translateY(-220%) scale(.75); }
  60%  { opacity: 1; transform: translateY(8%)    scale(1.04); }
  80%  { transform: translateY(-3%) scale(.98); }
  100% { opacity: 1; transform: translateY(0)     scale(1); }
}
@keyframes vlogo-cascade-loop {
  0%   { opacity: 0; transform: translateY(-220%) scale(.75); }
  20%  { opacity: 1; transform: translateY(8%)    scale(1.04); }
  28%  { transform: translateY(-3%) scale(.98); }
  35%, 65% { opacity: 1; transform: translateY(0) scale(1); }
  85%  { opacity: 1; transform: translateY(40%)   scale(.9); }
  100% { opacity: 0; transform: translateY(120%)  scale(.7); }
}
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__horn-l  { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both .00s; }
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__outer-l { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both .20s; }
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__inner-l { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both .40s; }
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__inner-r { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both .60s; }
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__outer-r { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both .80s; }
.vlogo-anim--cascade.vlogo-anim--once .vlogo-anim__horn-r  { animation: vlogo-cascade-drop .85s cubic-bezier(.34,1.56,.64,1) both 1.00s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__horn-l  { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__outer-l { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .15s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__inner-l { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .30s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__inner-r { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .45s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__outer-r { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .60s; }
.page-navigation-overlay.active .vlogo-anim--cascade.vlogo-anim--overlay .vlogo-anim__horn-r  { animation: vlogo-cascade-loop 3.0s cubic-bezier(.34,1.56,.64,1) infinite .75s; }

/* ---------- Candidate: Six-Point Scatter ----------------------------------
   Each part flies in from its own compass direction with its own rotation;
   mirror symmetry is broken on purpose.
   ------------------------------------------------------------------------- */
@keyframes vlogo-scatter6-outer-l { 0% { opacity: 0; transform: translate(-160%, -100%) rotate(-25deg) scale(.6); } 70% { opacity: 1; transform: translate(0, 0) rotate(8deg) scale(1.06); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-outer-r { 0% { opacity: 0; transform: translate(180%, 120%) rotate(28deg) scale(.6); } 70% { opacity: 1; transform: translate(0, 0) rotate(-6deg) scale(1.05); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-inner-l { 0% { opacity: 0; transform: translate(0, -200%) rotate(-180deg) scale(.4); } 70% { opacity: 1; transform: translate(0, 0) rotate(12deg) scale(1.12); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-inner-r { 0% { opacity: 0; transform: translate(0, 200%) rotate(180deg) scale(.4); } 70% { opacity: 1; transform: translate(0, 0) rotate(-10deg) scale(1.1); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-horn-l { 0% { opacity: 0; transform: translate(-220%, 0) rotate(-90deg) scale(.5); } 70% { opacity: 1; transform: translate(0, 0) rotate(15deg) scale(1.08); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-horn-r { 0% { opacity: 0; transform: translate(220%, 0) rotate(90deg) scale(.5); } 70% { opacity: 1; transform: translate(0, 0) rotate(-12deg) scale(1.08); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-outer-l { 0%, 100% { opacity: 0; transform: translate(-160%, -100%) rotate(-25deg) scale(.6); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-outer-r { 0%, 100% { opacity: 0; transform: translate(180%, 120%) rotate(28deg) scale(.6); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-inner-l { 0%, 100% { opacity: 0; transform: translate(0, -200%) rotate(-180deg) scale(.4); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-inner-r { 0%, 100% { opacity: 0; transform: translate(0, 200%) rotate(180deg) scale(.4); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-horn-l { 0%, 100% { opacity: 0; transform: translate(-220%, 0) rotate(-90deg) scale(.5); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-scatter6-loop-horn-r { 0%, 100% { opacity: 0; transform: translate(220%, 0) rotate(90deg) scale(.5); } 30%, 70% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__outer-l { animation: vlogo-scatter6-outer-l 1.0s cubic-bezier(.34,1.56,.64,1) both .00s; }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__outer-r { animation: vlogo-scatter6-outer-r 1.0s cubic-bezier(.34,1.56,.64,1) both .12s; }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__inner-l { animation: vlogo-scatter6-inner-l  .9s cubic-bezier(.34,1.56,.64,1) both .25s; }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__inner-r { animation: vlogo-scatter6-inner-r  .9s cubic-bezier(.34,1.56,.64,1) both .37s; }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__horn-l  { animation: vlogo-scatter6-horn-l   .9s cubic-bezier(.34,1.56,.64,1) both .50s; }
.vlogo-anim--scatter6.vlogo-anim--once .vlogo-anim__horn-r  { animation: vlogo-scatter6-horn-r   .9s cubic-bezier(.34,1.56,.64,1) both .62s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__outer-l { animation: vlogo-scatter6-loop-outer-l 3.2s cubic-bezier(.34,1.56,.64,1) infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__outer-r { animation: vlogo-scatter6-loop-outer-r 3.2s cubic-bezier(.34,1.56,.64,1) infinite .06s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__inner-l { animation: vlogo-scatter6-loop-inner-l 3.2s cubic-bezier(.34,1.56,.64,1) infinite .12s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__inner-r { animation: vlogo-scatter6-loop-inner-r 3.2s cubic-bezier(.34,1.56,.64,1) infinite .18s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__horn-l  { animation: vlogo-scatter6-loop-horn-l  3.2s cubic-bezier(.34,1.56,.64,1) infinite .24s; }
.page-navigation-overlay.active .vlogo-anim--scatter6.vlogo-anim--overlay .vlogo-anim__horn-r  { animation: vlogo-scatter6-loop-horn-r  3.2s cubic-bezier(.34,1.56,.64,1) infinite .30s; }

/* ---------- Candidate: Draw-On (calligraphy) ------------------------------
   Sibling stroke-clone <path>s in the partial (pathLength=100) trace via
   `stroke-dashoffset`; fills fade in once the stroke completes.
   ------------------------------------------------------------------------- */
.vlogo-anim--draw-on .vlogo-anim__outer-l,
.vlogo-anim--draw-on .vlogo-anim__outer-r,
.vlogo-anim--draw-on .vlogo-anim__inner-l,
.vlogo-anim--draw-on .vlogo-anim__inner-r,
.vlogo-anim--draw-on .vlogo-anim__horn-l,
.vlogo-anim--draw-on .vlogo-anim__horn-r { opacity: 0; }
.vlogo-anim--draw-on .vlogo-anim__stroke { stroke-dasharray: 100 100; stroke-dashoffset: 100; opacity: 1; }
@keyframes vlogo-drawon-trace      { 0% { stroke-dashoffset: 100; opacity: 1; } 60% { stroke-dashoffset: 0; opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vlogo-drawon-fill       { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes vlogo-drawon-trace-loop { 0% { stroke-dashoffset: 100; opacity: 1; } 35% { stroke-dashoffset: 0; opacity: 1; } 55%, 75% { stroke-dashoffset: 0; opacity: 0; } 85% { stroke-dashoffset: 0; opacity: 1; } 100% { stroke-dashoffset: 100; opacity: 1; } }
@keyframes vlogo-drawon-fill-loop  { 0%, 30% { opacity: 0; } 55%, 80% { opacity: 1; } 100% { opacity: 0; } }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--horn-l   { animation: vlogo-drawon-trace .9s ease-out both .00s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__horn-l           { animation: vlogo-drawon-fill  .35s ease-out both .55s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--outer-l  { animation: vlogo-drawon-trace .9s ease-out both .25s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__outer-l          { animation: vlogo-drawon-fill  .35s ease-out both .80s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--inner-l  { animation: vlogo-drawon-trace .9s ease-out both .50s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__inner-l          { animation: vlogo-drawon-fill  .35s ease-out both 1.05s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--inner-r  { animation: vlogo-drawon-trace .9s ease-out both .75s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__inner-r          { animation: vlogo-drawon-fill  .35s ease-out both 1.30s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--outer-r  { animation: vlogo-drawon-trace .9s ease-out both 1.00s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__outer-r          { animation: vlogo-drawon-fill  .35s ease-out both 1.55s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__stroke--horn-r   { animation: vlogo-drawon-trace .9s ease-out both 1.25s; }
.vlogo-anim--draw-on.vlogo-anim--once .vlogo-anim__horn-r           { animation: vlogo-drawon-fill  .35s ease-out both 1.80s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--horn-l   { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__horn-l           { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--outer-l  { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .10s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__outer-l          { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .10s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--inner-l  { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .20s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__inner-l          { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .20s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--inner-r  { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .30s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__inner-r          { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .30s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--outer-r  { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .40s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__outer-r          { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .40s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__stroke--horn-r   { animation: vlogo-drawon-trace-loop 4.2s ease-in-out infinite .50s; }
.page-navigation-overlay.active .vlogo-anim--draw-on.vlogo-anim--overlay .vlogo-anim__horn-r           { animation: vlogo-drawon-fill-loop  4.2s ease-in-out infinite .50s; }

/* ---------- Candidate: Spiral-In ------------------------------------------
   Each part spirals from its own offset distance + angle to centre while
   scaling 0 → 1.
   ------------------------------------------------------------------------- */
@keyframes vlogo-spiral-outer-l { 0% { opacity: 0; transform: translate(-180%, 80%) rotate(-540deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(40deg) scale(1.1); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-outer-r { 0% { opacity: 0; transform: translate(180%, -80%) rotate(540deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(-35deg) scale(1.1); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-inner-l { 0% { opacity: 0; transform: translate(-90%, -130%) rotate(-720deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(25deg) scale(1.15); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-inner-r { 0% { opacity: 0; transform: translate(90%, 130%) rotate(720deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(-25deg) scale(1.15); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-horn-l  { 0% { opacity: 0; transform: translate(-230%, -120%) rotate(-450deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(20deg) scale(1.1); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-horn-r  { 0% { opacity: 0; transform: translate(230%, 120%) rotate(450deg) scale(0); } 60% { opacity: 1; transform: translate(0, 0) rotate(-18deg) scale(1.1); } 100% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-outer-l { 0%, 100% { opacity: 0; transform: translate(-180%, 80%) rotate(-540deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-outer-r { 0%, 100% { opacity: 0; transform: translate(180%, -80%) rotate(540deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-inner-l { 0%, 100% { opacity: 0; transform: translate(-90%, -130%) rotate(-720deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-inner-r { 0%, 100% { opacity: 0; transform: translate(90%, 130%) rotate(720deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-horn-l  { 0%, 100% { opacity: 0; transform: translate(-230%, -120%) rotate(-450deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
@keyframes vlogo-spiral-loop-horn-r  { 0%, 100% { opacity: 0; transform: translate(230%, 120%) rotate(450deg) scale(0); } 35%, 65% { opacity: 1; transform: translate(0, 0) rotate(0) scale(1); } }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__horn-l  { animation: vlogo-spiral-horn-l  1.2s cubic-bezier(.34,1.56,.64,1) both .00s; }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__outer-l { animation: vlogo-spiral-outer-l 1.2s cubic-bezier(.34,1.56,.64,1) both .12s; }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__inner-l { animation: vlogo-spiral-inner-l 1.2s cubic-bezier(.34,1.56,.64,1) both .24s; }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__inner-r { animation: vlogo-spiral-inner-r 1.2s cubic-bezier(.34,1.56,.64,1) both .36s; }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__outer-r { animation: vlogo-spiral-outer-r 1.2s cubic-bezier(.34,1.56,.64,1) both .48s; }
.vlogo-anim--spiral.vlogo-anim--once .vlogo-anim__horn-r  { animation: vlogo-spiral-horn-r  1.2s cubic-bezier(.34,1.56,.64,1) both .60s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__horn-l  { animation: vlogo-spiral-loop-horn-l  3.6s cubic-bezier(.34,1.56,.64,1) infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__outer-l { animation: vlogo-spiral-loop-outer-l 3.6s cubic-bezier(.34,1.56,.64,1) infinite .08s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__inner-l { animation: vlogo-spiral-loop-inner-l 3.6s cubic-bezier(.34,1.56,.64,1) infinite .16s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__inner-r { animation: vlogo-spiral-loop-inner-r 3.6s cubic-bezier(.34,1.56,.64,1) infinite .24s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__outer-r { animation: vlogo-spiral-loop-outer-r 3.6s cubic-bezier(.34,1.56,.64,1) infinite .32s; }
.page-navigation-overlay.active .vlogo-anim--spiral.vlogo-anim--overlay .vlogo-anim__horn-r  { animation: vlogo-spiral-loop-horn-r  3.6s cubic-bezier(.34,1.56,.64,1) infinite .40s; }

/* ---------- Candidate: Color Wave -----------------------------------------
   Whole mark grey from t=0; a colour wave sweeps left → right and lights
   each part to its brand colour as it passes.
   ------------------------------------------------------------------------- */
.vlogo-anim--color-wave .vlogo-anim__outer-l path,
.vlogo-anim--color-wave .vlogo-anim__outer-r path,
.vlogo-anim--color-wave .vlogo-anim__inner-l path,
.vlogo-anim--color-wave .vlogo-anim__inner-r path,
.vlogo-anim--color-wave .vlogo-anim__horn-l,
.vlogo-anim--color-wave .vlogo-anim__horn-r { transition: fill .35s ease-out; }
@keyframes vlogo-colorwave-blue       { 0%, 30%  { fill: var(--phoenix-gray-400, #b6c1d2); } 60%, 100% { fill: #2563EB; } }
@keyframes vlogo-colorwave-amber      { 0%, 30%  { fill: var(--phoenix-gray-400, #b6c1d2); } 60%, 100% { fill: #F59E0B; } }
@keyframes vlogo-colorwave-blue-loop  { 0%, 10%, 90%, 100% { fill: var(--phoenix-gray-400, #b6c1d2); } 35%, 65% { fill: #2563EB; } }
@keyframes vlogo-colorwave-amber-loop { 0%, 10%, 90%, 100% { fill: var(--phoenix-gray-400, #b6c1d2); } 35%, 65% { fill: #F59E0B; } }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__horn-l           { animation: vlogo-colorwave-amber 1.6s ease-out both .00s; }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__outer-l path     { animation: vlogo-colorwave-blue  1.6s ease-out both .15s; }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__inner-l path     { animation: vlogo-colorwave-blue  1.6s ease-out both .28s; }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__inner-r path     { animation: vlogo-colorwave-blue  1.6s ease-out both .42s; }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__outer-r path     { animation: vlogo-colorwave-blue  1.6s ease-out both .55s; }
.vlogo-anim--color-wave.vlogo-anim--once .vlogo-anim__horn-r           { animation: vlogo-colorwave-amber 1.6s ease-out both .70s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__horn-l       { animation: vlogo-colorwave-amber-loop 3.2s ease-in-out infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__outer-l path { animation: vlogo-colorwave-blue-loop  3.2s ease-in-out infinite .10s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__inner-l path { animation: vlogo-colorwave-blue-loop  3.2s ease-in-out infinite .20s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__inner-r path { animation: vlogo-colorwave-blue-loop  3.2s ease-in-out infinite .30s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__outer-r path { animation: vlogo-colorwave-blue-loop  3.2s ease-in-out infinite .40s; }
.page-navigation-overlay.active .vlogo-anim--color-wave.vlogo-anim--overlay .vlogo-anim__horn-r       { animation: vlogo-colorwave-amber-loop 3.2s ease-in-out infinite .50s; }

/* ---------- Candidate: Origami Fold ---------------------------------------
   Each part starts as a flat sliver and unfolds along the axis appropriate
   to its shape (horns scaleY, V strokes scaleX).
   ------------------------------------------------------------------------- */
@keyframes vlogo-origami-unfold-x { 0% { opacity: 0; transform: scaleX(0); } 60% { opacity: 1; transform: scaleX(1.08); } 100% { opacity: 1; transform: scaleX(1); } }
@keyframes vlogo-origami-unfold-y { 0% { opacity: 0; transform: scaleY(0); } 60% { opacity: 1; transform: scaleY(1.08); } 100% { opacity: 1; transform: scaleY(1); } }
@keyframes vlogo-origami-loop-x   { 0%, 100% { opacity: 0; transform: scaleX(0); } 30%, 70% { opacity: 1; transform: scaleX(1); } }
@keyframes vlogo-origami-loop-y   { 0%, 100% { opacity: 0; transform: scaleY(0); } 30%, 70% { opacity: 1; transform: scaleY(1); } }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__horn-l  { animation: vlogo-origami-unfold-y .7s cubic-bezier(.34,1.56,.64,1) both .00s; }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__outer-l { animation: vlogo-origami-unfold-x .7s cubic-bezier(.34,1.56,.64,1) both .20s; }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__inner-l { animation: vlogo-origami-unfold-x .7s cubic-bezier(.34,1.56,.64,1) both .40s; }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__inner-r { animation: vlogo-origami-unfold-x .7s cubic-bezier(.34,1.56,.64,1) both .60s; }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__outer-r { animation: vlogo-origami-unfold-x .7s cubic-bezier(.34,1.56,.64,1) both .80s; }
.vlogo-anim--origami.vlogo-anim--once .vlogo-anim__horn-r  { animation: vlogo-origami-unfold-y .7s cubic-bezier(.34,1.56,.64,1) both 1.00s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__horn-l  { animation: vlogo-origami-loop-y 2.8s cubic-bezier(.34,1.56,.64,1) infinite .00s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__outer-l { animation: vlogo-origami-loop-x 2.8s cubic-bezier(.34,1.56,.64,1) infinite .12s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__inner-l { animation: vlogo-origami-loop-x 2.8s cubic-bezier(.34,1.56,.64,1) infinite .24s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__inner-r { animation: vlogo-origami-loop-x 2.8s cubic-bezier(.34,1.56,.64,1) infinite .36s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__outer-r { animation: vlogo-origami-loop-x 2.8s cubic-bezier(.34,1.56,.64,1) infinite .48s; }
.page-navigation-overlay.active .vlogo-anim--origami.vlogo-anim--overlay .vlogo-anim__horn-r  { animation: vlogo-origami-loop-y 2.8s cubic-bezier(.34,1.56,.64,1) infinite .60s; }

/* ---------- Shared: reduced-motion fallback -------------------------------
   Kill every candidate's animation so the static logo renders calmly. The
   Color Wave candidate also pins fills to their brand values so the mark
   doesn't sit in muted grey forever. */
@media (prefers-reduced-motion: reduce) {
  .vlogo-anim .vlogo-anim__outer-l,
  .vlogo-anim .vlogo-anim__outer-r,
  .vlogo-anim .vlogo-anim__inner-l,
  .vlogo-anim .vlogo-anim__inner-r,
  .vlogo-anim .vlogo-anim__horn-l,
  .vlogo-anim .vlogo-anim__horn-r {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .vlogo-anim--draw-on .vlogo-anim__stroke {
    animation: none !important;
    opacity: 0 !important;
  }
  .vlogo-anim--color-wave .vlogo-anim__outer-l path,
  .vlogo-anim--color-wave .vlogo-anim__outer-r path,
  .vlogo-anim--color-wave .vlogo-anim__inner-l path,
  .vlogo-anim--color-wave .vlogo-anim__inner-r path { fill: #2563EB !important; transition: none !important; }
  .vlogo-anim--color-wave .vlogo-anim__horn-l,
  .vlogo-anim--color-wave .vlogo-anim__horn-r       { fill: #F59E0B !important; transition: none !important; }
}

/* ==========================================================================
 * Curated SVG avatar (`.avatar-image`) — companion to `.avatar-initials`.
 * Used when the user has picked an entry from `userapp.avatars.AVATARS`.
 * The wrapping `.avatar.avatar-{size}` controls outer dimensions; this rule
 * makes the colored disc fill that wrapper, and the inline SVG inside fill
 * the disc with comfortable padding. The SVGs use `currentColor`, so the
 * `text-{color}-emphasis` utility on the disc paints them in a theme-aware
 * shade in both light and dark modes.
 *
 * NOTE: `.avatar-initials` and `.avatar-image` MUST be 100%×100% of their
 * `.avatar.avatar-{size}` wrapper. Without it, `border-radius: 50%`
 * (rounded-circle) produces an ELLIPSE rather than a circle when the box
 * isn't square — which is what users were seeing on the profile hero before
 * this rule was added (Phoenix only sizes the outer wrapper).
 * ========================================================================== */
.avatar > .avatar-initials,
.avatar > .avatar-image {
  width: 100%;
  height: 100%;
}
.avatar > .avatar-initials {
  color: #fff;
  font-weight: 600;
  /* Default for avatar sizes that don't have an explicit override below.
     Phoenix's `.avatar` doesn't set font-size on `.avatar-initials`, so
     without these rules the initials stay at the inherited body size and
     look comically small inside the larger discs (5xl, 4xl, 3xl). */
  font-size: 1rem;
  letter-spacing: .02em;
  line-height: 1;
}
/* Per-size font scaling. Tuned so the 2-letter initials sit comfortably
   inside the disc without crowding the rim. Mirrors Phoenix's avatar size
   ladder. The navbar `.profile-trigger-avatar` keeps its tighter override
   above (.8125rem) — its specificity wins. */
.avatar.avatar-xs  > .avatar-initials { font-size: .55rem; }
.avatar.avatar-s   > .avatar-initials,
.avatar.avatar-sm  > .avatar-initials { font-size: .7rem; }
.avatar.avatar-md  > .avatar-initials,
.avatar.avatar-m   > .avatar-initials { font-size: .85rem; }
.avatar.avatar-l   > .avatar-initials,
.avatar.avatar-lg  > .avatar-initials { font-size: 1rem; }
.avatar.avatar-xl  > .avatar-initials { font-size: 1.25rem; }
.avatar.avatar-xxl > .avatar-initials { font-size: 1.6rem; }
.avatar.avatar-2xl > .avatar-initials { font-size: 1.85rem; }
.avatar.avatar-3xl > .avatar-initials { font-size: 2.1rem; }
.avatar.avatar-4xl > .avatar-initials { font-size: 2.4rem; }
.avatar.avatar-5xl > .avatar-initials { font-size: 2.75rem; }
.avatar-image {
  overflow: hidden;
}
.avatar-image svg {
  width: 72%;
  height: 72%;
  display: block;
}

/* ==========================================================================
 * Avatar picker (profile-edit page) — radio-card grid of curated SVGs.
 * Pure CSS so no JS handler is needed: clicking a label flips its <input>;
 * the `:checked + .avatar-card` selector paints the selection ring.
 * ========================================================================== */
.avatar-picker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 0.75rem;
}
.avatar-picker-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.avatar-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 0.4rem;
  border-radius: 0.5rem;
  border: 1.5px solid var(--phoenix-border-color);
  background: var(--phoenix-card-bg);
  cursor: pointer;
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
.avatar-card:hover {
  border-color: var(--phoenix-primary);
  transform: translateY(-1px);
}
.avatar-picker-input:focus-visible + .avatar-card {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 2px;
}
.avatar-picker-input:checked + .avatar-card {
  border-color: var(--phoenix-primary);
  box-shadow: 0 0 0 2px rgba(var(--phoenix-primary-rgb), 0.25);
}
.avatar-card .avatar {
  width: 44px;
  height: 44px;
}
.avatar-card-label {
  font-size: 0.72rem;
  line-height: 1.1;
  text-align: center;
  color: var(--phoenix-body-tertiary-color);
  word-break: break-word;
}
.avatar-picker-input:checked + .avatar-card .avatar-card-label {
  color: var(--phoenix-body-emphasis-color);
}
.avatar-card-none {
  /* "No avatar" tile — render a translucent disc with a dash so it reads as
     "absence of avatar" without using a real image. */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1.5px dashed var(--phoenix-border-color);
  color: var(--phoenix-body-tertiary-color);
  font-size: 1.1rem;
  font-weight: 600;
}

/* ==========================================================================
 * Profile-home hero — animated, on-brand background.
 *
 * Two layers, both decorative and `pointer-events: none`:
 *   1. `.profile-hero-bg`   — dotted grid that drifts diagonally. Reads as
 *                             a search-space lattice.
 *   2. `.profile-hero-sweep`— a faint glowing band that travels from left
 *                             to right every few seconds. Reads as a
 *                             simulated-annealing temperature wave or a
 *                             fitness-landscape sweep.
 *
 * Both animations are gated behind `prefers-reduced-motion: no-preference`.
 * Static fallback (no animation) leaves only the dotted grid visible — still
 * pleasant, not noisy.
 * ========================================================================== */
.profile-hero {
  position: relative;
  overflow: hidden;
}
.profile-hero-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: 0.55;
  background-image:
    radial-gradient(circle at center, rgba(var(--phoenix-primary-rgb), 0.55) 1.2px, transparent 1.8px);
  background-size: 22px 22px;
  background-position: 0 0;
  -webkit-mask-image: linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.05) 85%);
          mask-image: linear-gradient(135deg, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.05) 85%);
}
.profile-hero-sweep {
  position: absolute;
  inset: -10% -50%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  background: linear-gradient(115deg,
    transparent 30%,
    rgba(var(--phoenix-primary-rgb), 0.18) 45%,
    rgba(var(--phoenix-primary-rgb), 0.32) 50%,
    rgba(var(--phoenix-primary-rgb), 0.18) 55%,
    transparent 70%);
  filter: blur(8px);
  transform: translateX(-60%);
}
.profile-hero-content {
  position: relative;
  z-index: 1;
}
@media (prefers-reduced-motion: no-preference) {
  @keyframes profile-hero-drift {
    0%   { background-position: 0 0; }
    100% { background-position: 88px 44px; }
  }
  @keyframes profile-hero-sweep-anim {
    0%, 100% { transform: translateX(-60%); opacity: 0; }
    15%      { opacity: 1; }
    50%      { transform: translateX(60%);  opacity: 1; }
    85%      { opacity: 0; }
  }
  .profile-hero-bg {
    animation: profile-hero-drift 8s linear infinite;
  }
  .profile-hero-sweep {
    animation: profile-hero-sweep-anim 9s ease-in-out 1.5s infinite;
  }
}

/* KPI cards on the profile home — gentle entrance + hover lift. */
.profile-kpi-card {
  transition: transform .18s ease, box-shadow .18s ease;
}
.profile-kpi-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--phoenix-box-shadow);
}
.profile-kpi-value {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* ==========================================================================
 * Avatar customizer — picker section on /user/profile.
 *
 * Layout: two-column grid on desktop (sticky preview + scrolling controls);
 * single-column stack on mobile. Every control is a real <input type="radio">
 * so accessibility comes for free.
 * ========================================================================== */
.avatar-customizer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}
@media (min-width: 992px) {
  .avatar-customizer-grid {
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 2rem;
  }
}
.avatar-customizer-preview-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1.25rem;
  border-radius: 0.75rem;
  background-color: var(--phoenix-emphasis-bg);
  border: 1px solid var(--phoenix-border-color-translucent);
}
@media (min-width: 992px) {
  .avatar-customizer-preview-card {
    position: sticky;
    top: 1rem;
  }
}
.avatar-customizer-preview-card #avatarPreview {
  width: 96px;
  height: 96px;
}
.avatar-customizer-preview-card #avatarPreview .avatar-image,
.avatar-customizer-preview-card #avatarPreview .avatar-initials {
  width: 100%;
  height: 100%;
  font-size: 2rem;
  color: #fff;
}
.avatar-customizer-preview-card #avatarPreview .avatar-image {
  /* Reset font-size so SVG sizing isn't pulled by parent. */
  font-size: 1rem;
}
.avatar-preview-meta {
  margin-top: 0.5rem;
  min-height: 1.6em;
}

/* Group filter chips — active button uses a soft Phoenix-toned highlight. */
.avatar-filter-chips .btn.active {
  background-color: rgba(var(--phoenix-primary-rgb), 0.12);
  border-color: var(--phoenix-primary);
  color: var(--phoenix-primary);
}

/* Color swatches. Each swatch is a 36px disc rendered with the SAME bg/text
 * colour combo that the user will get on their avatar — so the picker is
 * a literal preview of every choice. */
.color-swatch-cell {
  position: relative;
  display: inline-flex;
  cursor: pointer;
}
.color-swatch-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.color-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1.5px solid var(--phoenix-border-color);
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}
.color-swatch-cell:hover .color-swatch {
  transform: translateY(-1px);
}
.color-swatch-input:focus-visible + .color-swatch {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 2px;
}
.color-swatch-input:checked + .color-swatch {
  border-color: var(--phoenix-primary);
  box-shadow: 0 0 0 2px rgba(var(--phoenix-primary-rgb), 0.3);
}
.color-swatch-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background-color: currentColor;
}
.color-swatch-default {
  color: var(--phoenix-body-tertiary-color);
  background-color: var(--phoenix-body-bg);
  border-style: dashed;
  font-size: 0.85rem;
}
.color-swatch-neutral {
  background-color: var(--phoenix-body-tertiary-bg);
  color: var(--phoenix-body-emphasis-color);
}

/* Style options — collapsible details element. */
.avatar-style-details > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0;
  user-select: none;
}
.avatar-style-details > summary::-webkit-details-marker { display: none; }
.avatar-style-details > summary::before {
  content: "\25B8";  /* right-pointing triangle */
  display: inline-block;
  transition: transform .18s ease;
  color: var(--phoenix-body-tertiary-color);
}
.avatar-style-details[open] > summary::before {
  transform: rotate(90deg);
}
.avatar-axis-row .btn-group .btn { font-size: 0.78rem; }

/* Stroke weight — drives the SVG stroke-width via a CSS variable on the
 * disc wrapper. The avatar SVGs declare stroke-width="var(--avatar-stroke, N)"
 * on their root <svg>, so a class swap is enough. */
.avatar-stroke-thin   { --avatar-stroke: 1.6; }
.avatar-stroke-normal { --avatar-stroke: 2.4; }
.avatar-stroke-thick  { --avatar-stroke: 3.4; }

/* Motion — gated behind prefers-reduced-motion. The "still" class is a
 * no-op for symmetry. Each animation targets the inline SVG so the disc
 * background stays put while only the foreground moves. */
@media (prefers-reduced-motion: no-preference) {
  .avatar-motion-breathing svg,
  .avatar-motion-rotating svg,
  .avatar-motion-sliding svg,
  .avatar-motion-bouncing svg,
  .avatar-motion-pulsing svg {
    transform-origin: center;
    transform-box: fill-box;
    will-change: transform, opacity;
  }
  .avatar-motion-breathing svg {
    animation: avatarBreathe 3s ease-in-out infinite;
  }
  .avatar-motion-rotating svg {
    animation: avatarRotate 9s linear infinite;
  }
  .avatar-motion-sliding svg {
    animation: avatarSlide 3.6s ease-in-out infinite;
  }
  .avatar-motion-bouncing svg {
    animation: avatarBounce 1.8s cubic-bezier(.34, 1.56, .64, 1) infinite;
  }
  .avatar-motion-pulsing svg {
    animation: avatarPulse 2.2s ease-in-out infinite;
  }
  @keyframes avatarBreathe {
    0%, 100% { transform: scale(0.92); opacity: 0.85; }
    50%      { transform: scale(1.12); opacity: 1; }
  }
  @keyframes avatarRotate {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }
  @keyframes avatarSlide {
    0%, 100% { transform: translateX(0); }
    50%      { transform: translateX(7%); }
  }
  @keyframes avatarBounce {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-7%); }
  }
  @keyframes avatarPulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
  }
}

/* ==========================================================================
 * Avatar display + hover-reveal "Edit avatar" button on /user/profile.
 *
 * The on-page avatar is the live preview — it always shows the current state
 * of the form fields (server-rendered initially, JS-synced on every change).
 * Hovering reveals an overlay button that opens the customizer modal.
 *
 * Touch devices (no hover) get a permanently visible button.
 * ========================================================================== */
.avatar-display-section {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.avatar-display-wrapper {
  position: relative;
  display: inline-flex;
  width: 96px;
  height: 96px;
}
.avatar-display-wrapper #avatarPreview {
  width: 100%;
  height: 100%;
}
.avatar-display-wrapper #avatarPreview .avatar-image,
.avatar-display-wrapper #avatarPreview .avatar-initials {
  width: 100%;
  height: 100%;
  font-size: 2rem;
  color: #fff;
}
.avatar-display-wrapper #avatarPreview .avatar-image {
  font-size: 1rem;  /* don't pull SVG sizing from parent */
}
.avatar-edit-overlay {
  /* Hover scrim: dark backdrop with light text in BOTH themes. Not a theme
   * surface — these colours are intentionally fixed because Phoenix tones
   * would invert in dark mode and lose contrast over the dark scrim. */
  position: absolute;
  inset: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background-color: rgba(20, 24, 35, 0.62);
  color: #fff;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  opacity: 0;
  transition: opacity .18s ease, transform .18s ease;
  pointer-events: none;  /* enabled when shown */
  text-transform: uppercase;
  padding: 0 0.4rem;
  text-align: center;
  line-height: 1.05;
}
/* Show on real hover OR keyboard focus (focus-visible). Click-induced focus
 * does NOT match focus-visible, so when Bootstrap returns focus to this
 * trigger after the modal closes, the scrim correctly hides until the user
 * hovers again. */
.avatar-display-wrapper:hover .avatar-edit-overlay,
.avatar-edit-overlay:focus-visible {
  opacity: 1;
  pointer-events: auto;
}
.avatar-edit-overlay:focus-visible {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 3px;
}
/* Hover-only label vs touch-only icon — only ONE shows at a time. */
.avatar-edit-overlay-icon { display: none; }
@media (hover: none) {
  /* Touch devices: button is always visible, smaller, anchored bottom-right. */
  .avatar-edit-overlay {
    inset: auto -4px -4px auto;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background-color: var(--phoenix-primary);
    border: 2px solid var(--phoenix-body-bg);
    opacity: 1;
    pointer-events: auto;
    padding: 0;
    font-size: 0.85rem;
    text-transform: none;
    letter-spacing: 0;
  }
  .avatar-edit-overlay-label { display: none; }
  .avatar-edit-overlay-icon { display: inline-flex; }
}

/* Modal preview card — duplicates the live preview at the top of the modal
 * body so the user can scroll the controls and still see what they're
 * picking. */
.avatar-modal-preview-card {
  position: sticky;
  top: 0;
  z-index: 1;
  background-color: var(--phoenix-emphasis-bg);
  border-radius: 0.75rem;
  padding: 1rem 1rem 0.75rem;
  margin-bottom: 1rem;
}
.avatar-modal-preview-card #avatarModalPreview {
  width: 96px;
  height: 96px;
}
.avatar-modal-preview-card #avatarModalPreview .avatar-image,
.avatar-modal-preview-card #avatarModalPreview .avatar-initials {
  width: 100%;
  height: 100%;
  font-size: 2rem;
  color: #fff;
}
.avatar-modal-preview-card #avatarModalPreview .avatar-image {
  font-size: 1rem;
}

/* ============================================================
 * Tier marks — heraldic shield + Vilvik V-mark, three variants:
 *
 *   .tier-mark--free     silver shield, monochrome V (static)
 *   .tier-mark--trial    bronze shield, marching-ants border, full-color V
 *   .tier-mark--premium  gold shield + pulsing aura + bobbing crown,
 *                        full-color V
 *
 * The mark inherits `currentColor` for monochrome geometry so the Free
 * variant adopts the surrounding pill's text color automatically. Full-
 * color variants paint the four V paths with the canonical Vilvik (Google-
 * palette) hexes regardless of context.
 *
 * Sizing: `.tier-mark` is 1.2em (sits inline next to label text in a
 * Phoenix badge pill); `.tier-mark-lg` is 3.5rem (medallion size for the
 * billing page and dashboard hero).
 *
 * Motion: trial border drift + premium aura pulse + premium crown bob are
 * all gated behind `prefers-reduced-motion: no-preference`.
 * ============================================================ */

.tier-mark {
  width: 1.2em;
  height: 1.2em;
  vertical-align: -0.22em;
  margin-right: 0.3em;
  flex-shrink: 0;
}
.tier-mark-md {
  width: 2.25rem;
  height: 2.25rem;
  vertical-align: -0.55rem;
  margin-right: 0.55rem;
}
.tier-mark-lg {
  width: 3.5rem;
  height: 3.5rem;
  vertical-align: middle;
  margin-right: 0;
}

/* V-mark fills — `--mono` rides currentColor; `--full` uses the Sapphire &
   Citrine duotone (Vs share #2563EB, horns share #F59E0B). Class names
   blue/green/red/yellow kept for back-compat; they no longer describe colour. */
.tier-mark__v--mono .vmark         { fill: currentColor; opacity: 0.85; }
.tier-mark__v--full .vmark--blue   { fill: #2563EB; }
.tier-mark__v--full .vmark--green  { fill: #2563EB; }
.tier-mark__v--full .vmark--red    { fill: #F59E0B; }
.tier-mark__v--full .vmark--yellow { fill: #F59E0B; }

/* Shield + ornament colors per tier. */
.tier-mark--free .tier-mark__shield {
  fill: var(--phoenix-secondary-bg-subtle);
  stroke: var(--phoenix-secondary-border-subtle);
  stroke-width: 1.5;
  stroke-linejoin: round;
}
.tier-mark--trial .tier-mark__shield {
  fill: var(--phoenix-warning-bg-subtle);
  stroke: var(--phoenix-warning);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-dasharray: 4 3;
}
.tier-mark--premium .tier-mark__shield {
  fill: var(--phoenix-primary-bg-subtle);
  stroke: var(--phoenix-primary);
  stroke-width: 1.8;
  stroke-linejoin: round;
}
.tier-mark--premium .tier-mark__shield-aura {
  fill: none;
  stroke: #F59E0B;
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-opacity: 0.75;
}
.tier-mark--premium .tier-mark__crown {
  fill: #F59E0B;
  stroke: var(--phoenix-warning);
  stroke-width: 0.6;
  stroke-linejoin: round;
}

@keyframes tier-mark-shield-march {
  /* Four dash periods per cycle (4 + 3 = 7, ×4 = 28) keeps the loop seamless. */
  to { stroke-dashoffset: -28; }
}
@keyframes tier-mark-shield-pulse {
  0%, 100% { transform: scale(1);    stroke-opacity: 0.75; }
  50%      { transform: scale(1.18); stroke-opacity: 0.05; }
}
@keyframes tier-mark-crown-bob {
  0%, 100% { transform: translateY(0);    }
  50%      { transform: translateY(-2px); }
}
@media (prefers-reduced-motion: no-preference) {
  .tier-mark--trial .tier-mark__shield {
    animation: tier-mark-shield-march 4s linear infinite;
  }
  .tier-mark--premium .tier-mark__shield-aura {
    transform-origin: center;
    transform-box: fill-box;
    animation: tier-mark-shield-pulse 2s ease-in-out infinite;
  }
  .tier-mark--premium .tier-mark__crown {
    transform-origin: center;
    transform-box: fill-box;
    animation: tier-mark-crown-bob 2s ease-in-out infinite;
  }
}

/* ---------- Brand wordmark ----------
 * Sora 600 (OFL, latin subset, ~15KB) for the .logo-text wordmark only —
 * gives the brand a distinct geometric voice vs the body Inter. The first
 * letter takes the primary accent so the mark reads as a logotype rather
 * than another heading. Self-hosted under static/_static/fonts/Sora/. */
@font-face {
  font-family: 'Sora';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../../_static/fonts/Sora/sora-v17-latin-600.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.logo-text {
  font-family: 'Sora', system-ui, sans-serif;
  letter-spacing: -0.03em;
}
.logo-text .logo-accent {
  color: var(--phoenix-primary);
}

/* Wordmark hover/focus state — applies to every VILVIK navbar (home,
 * admin, user, billing, docs) because `_navbar_brand.html` is the
 * single source of truth. The non-accent letters smoothly shift to
 * the Phoenix primary on hover/focus so the whole word "lights up"
 * uniformly with its already-accented letters. */
.navbar-brand:hover .logo-text,
.navbar-brand:focus-visible .logo-text {
  color: var(--phoenix-primary);
}

@media (prefers-reduced-motion: no-preference) {
  .password-strength-indicator .progress-bar {
    transition: width 200ms ease, background-color 200ms ease;
  }
  .navbar-brand .logo-text {
    transition: color 200ms ease;
  }
}

/* ----- Hierarchical permission tree (admins/detail.html) -------------- */

.permission-tree {
  font-size: 0.9rem;
}

.permission-parent {
  border-left: 1px solid transparent;
  border-radius: 0.25rem;
  transition: background-color 150ms ease;
}

.permission-parent-header {
  padding: 0.25rem 0.4rem;
  border-radius: 0.25rem;
  background: var(--phoenix-body-tertiary-bg);
}

.permission-parent-header:hover {
  background: var(--phoenix-body-secondary-bg);
}

.permission-parent-children {
  border-color: var(--phoenix-border-color) !important;
}

.permission-leaf {
  padding-left: 0.25rem;
  border-radius: 0.25rem;
}

.permission-leaf:hover {
  background: var(--phoenix-body-tertiary-bg);
}

.permission-toggle {
  width: 1.25rem;
  height: 1.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--phoenix-body-secondary-color);
  text-decoration: none;
}

.permission-toggle:focus-visible {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 1px;
  border-radius: 0.2rem;
}

.permission-toggle .permission-toggle-chevron {
  width: 0.95rem;
  height: 0.95rem;
}

@media (prefers-reduced-motion: no-preference) {
  .permission-toggle .permission-toggle-chevron {
    transition: transform 150ms ease;
  }
}

.permission-toggle[aria-expanded="true"] .permission-toggle-chevron {
  transform: rotate(90deg);
}

/* Reusable rotation for `fa-caret-right` icons used inside Bootstrap collapse
 * toggles. Add the `.collapse-caret` class to the icon span and the icon will
 * rotate to point down when the toggle is expanded. Mirrors the chevron
 * pattern used elsewhere in the app so all collapse triggers behave the
 * same way regardless of which icon they pick. */
.collapse-caret {
  display: inline-block;
}

@media (prefers-reduced-motion: no-preference) {
  .collapse-caret {
    transition: transform 150ms ease;
  }
}

[aria-expanded="true"] > .collapse-caret,
[aria-expanded="true"] .collapse-caret {
  transform: rotate(90deg);
}

/* Indeterminate state: parent unticked but at least one descendant ticked. */
.permission-checkbox:indeterminate {
  background-color: var(--phoenix-body-secondary-bg);
  border-color: var(--phoenix-secondary);
}

/* ==========================================================================
 * Problem Builder — wizard stepper and per-step visibility.
 *
 * The .pb-step sections render on top of each other; JS toggles .is-active
 * so the user only sees one at a time. The stepper list above the form
 * mirrors that state via .is-active on each .pb-stepper-item.
 * ========================================================================== */
.pb-stepper-item {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0.75rem;
  border-radius: 999px;
  background-color: var(--phoenix-body-tertiary-bg);
  color: var(--phoenix-body-tertiary-color);
  font-size: 0.85rem;
  border: 1px solid transparent;
}
.pb-stepper-item.is-active {
  background-color: var(--phoenix-primary-bg-subtle);
  color: var(--phoenix-primary-text-emphasis);
  border-color: var(--phoenix-primary-border-subtle);
}
.pb-stepper-item.is-complete {
  background-color: var(--phoenix-success-bg-subtle);
  color: var(--phoenix-success-text-emphasis);
  border-color: var(--phoenix-success-border-subtle);
}
.pb-stepper-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
  background-color: var(--phoenix-body-emphasis-color);
  color: var(--phoenix-body-bg);
  font-weight: 700;
  font-size: 0.8rem;
}
.pb-stepper-item.is-active .pb-stepper-num {
  background-color: var(--phoenix-primary);
  color: #fff;
}
/* Completed-step number circle. We deliberately use the
   `text-emphasis` / `bg-subtle` pair instead of solid `--phoenix-success`
   + white: in dark mode Phoenix tunes `--phoenix-success` to a brighter
   green, and white-on-bright-green washes the digit out. The
   text-emphasis/bg-subtle pair is designed as an opposing tone across
   both themes (dark vs pale), so the digit stays readable either way. */
.pb-stepper-item.is-complete .pb-stepper-num {
  background-color: var(--phoenix-success-text-emphasis);
  color: var(--phoenix-success-bg-subtle);
}

.pb-step { display: none; }
.pb-step.is-active { display: block; }

/* Subtle fade-in on the entering wizard step so a jump from one step
   to another (Next/Back, stepper click, or the "Confirm entry symbols"
   modal navigating to a different step) reads as a transition rather
   than an instant snap. The leaving step still disappears immediately
   via `display: none`; we animate only the entering step. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes pb-step-fade-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .pb-step.is-active { animation: pb-step-fade-in 220ms ease-out; }
}

/* Brief outline pulse on a form field after the confirmation modal
   navigates the user to it (entry_symbol_confirmation.js). Uses the
   Phoenix `--phoenix-warning` channel so the pulse is theme-aware in
   both light and dark modes. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes esc-entry-flash {
    0%   { box-shadow: 0 0 0 0.25rem rgba(var(--phoenix-warning-rgb, 245, 158, 11), 0.55); }
    100% { box-shadow: 0 0 0 0    rgba(var(--phoenix-warning-rgb, 245, 158, 11), 0); }
  }
  .esc-flash { animation: esc-entry-flash 1400ms ease-out; }
}

/* Tighten the wizard's vertical chrome. Phoenix's default .card-body
   gives 1.5rem on every side; that adds a visible empty band above the
   heading and below the last control on every step. Cut both ends and
   trim the first/last paragraph margins so the content sits close to
   the chrome without losing breathing room. */
.pb-step > .card-body { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.pb-step > .card-body > :first-child { margin-top: 0; }
.pb-step > .card-body > :last-child { margin-bottom: 0; }
.pb-step > .card-body > h4 { margin-bottom: 0.5rem; }
.pb-step > .card-footer { padding-top: 0.5rem; padding-bottom: 0.5rem; }

/* === Floating action bar ============================================== */
/* The bar is wrapped by floating_form_controls.js into a `.ffc-dock` flex
   column. The dock is the fixed positioning anchor at bottom-centre of the
   viewport. Inside the dock sit two siblings:
     - the bar itself (with class `floating-form-controls` / legacy
       `pb-floating-controls`) carrying the action buttons
     - a `.ffc-handle` button rendered above the bar via `order: -1`
   When the bar minimises it slides off via `transform: translateY(...)`
   while keeping its flex slot reserved, so the handle's screen position
   does not shift between expanded and collapsed states — the user gets a
   single, stable target. z-index 1030 keeps the bar above page content
   but below Bootstrap modals (1055). Any host element that opts in via
   `data-floating-form` reserves bottom padding so the last form row
   stays reachable above the bar. */

[data-floating-form] { padding-bottom: 5rem; }

/* Fallback positioning for any bar that the JS hasn't wrapped yet (no-JS
   or pre-init paint). The dock-scoped overrides further down take over
   once the DOM is wrapped. */
.floating-form-controls,
.pb-floating-controls {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1030;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  align-items: center;
  width: max-content;
  max-width: calc(100vw - 1.5rem);
}

/* The dock wrapper inserted by floating_form_controls.js. Positions both
   the bar and the handle at bottom-centre and lays them out as a column;
   the handle's `order: -1` puts it visually above the bar even though
   it's appended after the bar in DOM. The dock has no visible chrome
   itself — backgrounds and shadows live on the bar and the handle. */
.ffc-dock {
  position: fixed;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1030;
  display: flex;
  flex-direction: column;
  align-items: center;
  pointer-events: none;
  max-width: calc(100vw - 1.5rem);
}

/* When the inner bar is force-hidden by surrounding app logic (e.g. the
   user-profile tab switcher applies `.d-none` on tabs where the action
   does not apply), collapse the whole dock so a stray handle doesn't
   linger on the page. */
.ffc-dock:has(> .floating-form-controls.d-none),
.ffc-dock:has(> .pb-floating-controls.d-none) {
  display: none !important;
}

/* Bar styling inside the dock. The fixed positioning from the fallback
   rule above is unwound — layout is now handed to the dock's flex column
   — and the bar takes on the frosted-glass appearance the user approved. */
.ffc-dock > .floating-form-controls,
.ffc-dock > .pb-floating-controls {
  position: static;
  bottom: auto;
  left: auto;
  transform: none;
  pointer-events: auto;
  width: max-content;
  max-width: calc(100vw - 1.5rem);
  background: color-mix(in srgb, var(--phoenix-body-bg) 80%, transparent);
  border: 1px solid var(--phoenix-border-color);
  border-radius: 999px;
  padding: 0.5rem 0.85rem;
  box-shadow: 0 0.75rem 1.75rem rgba(0, 0, 0, 0.10), 0 0.125rem 0.25rem rgba(0, 0, 0, 0.04);
}
/* Heavier frosting where the browser supports backdrop-filter. The
   background is intentionally low-opacity so the blur reads as a
   distinct optical effect rather than a tinted solid panel. */
@supports ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
  .ffc-dock > .floating-form-controls,
  .ffc-dock > .pb-floating-controls {
    background: color-mix(in srgb, var(--phoenix-body-bg) 20%, transparent);
    backdrop-filter: blur(32px) saturate(220%);
    -webkit-backdrop-filter: blur(32px) saturate(220%);
  }
}

/* Hidden state. The bar slides off the bottom edge but keeps its flex
   slot reserved, so the handle sibling above does not shift. Pointer
   events are turned off so the invisible bar can't intercept clicks.
   The dock-scoped selector outweighs the fallback `.is-hidden` rule. */
.ffc-dock > .floating-form-controls.is-hidden,
.ffc-dock > .pb-floating-controls.is-hidden {
  transform: translateY(calc(100% + 1rem));
  opacity: 0;
  pointer-events: none;
}
/* Fallback hidden state for un-docked bars (no-JS / pre-init). */
.floating-form-controls.is-hidden,
.pb-floating-controls.is-hidden {
  transform: translateX(-50%) translateY(calc(100% + 1.5rem));
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .floating-form-controls,
  .pb-floating-controls,
  .ffc-dock > .floating-form-controls,
  .ffc-dock > .pb-floating-controls {
    transition: transform 250ms ease, opacity 200ms ease,
                background 200ms ease, box-shadow 200ms ease,
                border-color 200ms ease;
  }
  .ffc-handle {
    transition: border-radius 200ms ease, margin-bottom 200ms ease,
                border-color 200ms ease, background 200ms ease,
                color 200ms ease, box-shadow 200ms ease,
                transform 200ms ease;
  }
}

/* Dock-level hover/focus reaction. Hovering anywhere on the dock (or
   moving keyboard focus into any of its controls) brightens the frosted
   surface, deepens the elevation shadow, and sharpens the border on
   both the bar and the handle so they read as one responsive surface.
   The handle also lifts a few pixels for a tactile feel; the bar stays
   put so the buttons inside don't move under the cursor. */
.ffc-dock:hover > .floating-form-controls,
.ffc-dock:hover > .pb-floating-controls,
.ffc-dock:focus-within > .floating-form-controls,
.ffc-dock:focus-within > .pb-floating-controls {
  border-color: color-mix(in srgb, var(--phoenix-emphasis-color) 35%, var(--phoenix-border-color));
  box-shadow: 0 1rem 2.25rem rgba(0, 0, 0, 0.18),
              0 0.25rem 0.5rem rgba(0, 0, 0, 0.06);
}
@supports ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
  .ffc-dock:hover > .floating-form-controls,
  .ffc-dock:hover > .pb-floating-controls,
  .ffc-dock:focus-within > .floating-form-controls,
  .ffc-dock:focus-within > .pb-floating-controls {
    background: color-mix(in srgb, var(--phoenix-body-bg) 32%, transparent);
    backdrop-filter: blur(36px) saturate(240%);
    -webkit-backdrop-filter: blur(36px) saturate(240%);
  }
}
.ffc-dock:hover > .ffc-handle,
.ffc-dock:focus-within > .ffc-handle {
  color: var(--phoenix-body-color);
  border-color: color-mix(in srgb, var(--phoenix-emphasis-color) 35%, var(--phoenix-border-color));
  transform: translateY(-1px);
}
/* Keep the handle's seam transparent when the bar is visible even under
   the dock-hover state, otherwise the brightened border-color paints a
   visible line where the handle meets the bar. */
.ffc-dock:hover > .floating-form-controls:not(.is-hidden) ~ .ffc-handle,
.ffc-dock:hover > .pb-floating-controls:not(.is-hidden) ~ .ffc-handle,
.ffc-dock:focus-within > .floating-form-controls:not(.is-hidden) ~ .ffc-handle,
.ffc-dock:focus-within > .pb-floating-controls:not(.is-hidden) ~ .ffc-handle {
  border-bottom-color: transparent;
}
@supports ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
  .ffc-dock:hover > .ffc-handle,
  .ffc-dock:focus-within > .ffc-handle {
    background: color-mix(in srgb, var(--phoenix-body-bg) 32%, transparent);
  }
}

/* Direct hover on the handle itself nudges it a touch more so the click
   target feels alive in addition to the surface-wide hover state above.
   `.ffc-dock > .ffc-handle:hover` ties on specificity with the dock-hover
   selector and, being defined later, wins the cascade — the handle lifts
   2px on direct hover but only 1px when the cursor is over the bar. */
.ffc-dock > .ffc-handle:hover {
  transform: translateY(-2px);
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.16);
}

/* The always-visible chevron handle. Sits visually above the bar via
   `order: -1`. Styled as a small rounded tab whose lower edge tucks
   into the bar's top via negative margin so it reads as one continuous
   surface. When the bar is hidden the handle rounds out to a full pill
   (no bar to seam with). */
.ffc-handle {
  order: -1;
  width: 4.5rem;
  height: 2rem;
  padding: 0;
  margin-bottom: -0.7rem;
  border-radius: 999px 999px 0 0;
  border: 1px solid var(--phoenix-border-color);
  border-bottom-color: transparent;
  background: color-mix(in srgb, var(--phoenix-body-bg) 80%, transparent);
  color: var(--phoenix-secondary-color);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 2;
  box-shadow: 0 -0.25rem 0.6rem rgba(0, 0, 0, 0.06);
}
@supports ((backdrop-filter: blur(0)) or (-webkit-backdrop-filter: blur(0))) {
  .ffc-handle {
    background: color-mix(in srgb, var(--phoenix-body-bg) 20%, transparent);
    backdrop-filter: blur(32px) saturate(220%);
    -webkit-backdrop-filter: blur(32px) saturate(220%);
  }
}
.ffc-handle:focus-visible {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 2px;
}
/* When the bar is hidden the tab becomes a complete pill — there's no
   bar below to seam with, so close the bottom edge. Critically, do not
   touch `margin-bottom` here: the tab's negative bottom-margin is what
   keeps it visually anchored to the bar's top edge, and changing it
   would shift the handle's screen position between expanded / collapsed
   states — exactly the UX bug this layout was built to avoid. */
.ffc-dock > .floating-form-controls.is-hidden ~ .ffc-handle,
.ffc-dock > .pb-floating-controls.is-hidden ~ .ffc-handle {
  border-radius: 999px;
  border-bottom-color: var(--phoenix-border-color);
  box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.10);
}

/* Stepper labels are <button>s so they can carry click handlers. Reset
   default button chrome so they read as pill-shaped chips. */
button.pb-stepper-item {
  border: 1px solid transparent;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
button.pb-stepper-item:hover {
  background-color: var(--phoenix-body-secondary-bg);
}
/* Hover for stateful chips: preserve the state's tint so completed +
   active chips don't wash out to the neutral secondary background. */
button.pb-stepper-item.is-active:hover {
  background-color: var(--phoenix-primary-bg-subtle);
}
button.pb-stepper-item.is-complete:hover {
  background-color: var(--phoenix-success-bg-subtle);
}

/* Objective rows (Step 3). Library preview code block uses Prism's
   default colours; we only need to clamp width and add a subtle border. */
.pb-obj-library-source {
  max-height: 240px;
  overflow: auto;
  border: 1px solid var(--phoenix-border-color);
}
.pb-obj-library-source code {
  display: block;
  font-size: 0.85rem;
  white-space: pre;
}
.pb-objective-row .invalid-feedback {
  display: block;
}
.pb-objective-row .invalid-feedback:empty {
  display: none;
}

/* ============================================================
   Matrix-builder inline resize affordances
   (initial_population grid + gene_space Per-Gene rows)
   ------------------------------------------------------------ */
.mb-row-header,
.mb-col-header {
  vertical-align: middle;
}
.mb-row-header .mb-row-label,
.mb-col-header .mb-col-label {
  font-weight: 600;
}
.mb-row-header .mb-remove-row,
.mb-col-header .mb-remove-col,
.gs-per-gene-row .gs-remove-gene {
  opacity: 0.55;
}
.mb-row-header:hover .mb-remove-row,
.mb-col-header:hover .mb-remove-col,
.gs-per-gene-row:hover .gs-remove-gene,
.mb-remove-row:focus-visible,
.mb-remove-col:focus-visible,
.gs-remove-gene:focus-visible {
  opacity: 1;
}
.mb-remove-row[disabled],
.mb-remove-col[disabled],
.gs-remove-gene[disabled] {
  opacity: 0.25;
  cursor: not-allowed;
}
.mb-add-col-th {
  background-color: var(--phoenix-body-highlight-bg);
  border-left: 2px dashed var(--phoenix-border-color);
  vertical-align: middle;
  text-align: center;
}
.mb-add-col-filler {
  background-color: var(--phoenix-body-highlight-bg);
  border-left: 2px dashed var(--phoenix-border-color);
}
.mb-add-row-td {
  border-top: 2px dashed var(--phoenix-border-color);
  background-color: var(--phoenix-body-highlight-bg);
}
@media (prefers-reduced-motion: no-preference) {
  .mb-remove-row,
  .mb-remove-col,
  .gs-remove-gene {
    transition: opacity 0.15s ease-in-out;
  }
  .mb-add-col,
  .mb-add-row,
  #gs-addGeneBtn {
    transition: transform 0.15s ease;
  }
  .mb-add-col:hover,
  .mb-add-row:hover,
  #gs-addGeneBtn:hover {
    transform: scale(1.04);
  }
}

/* ==========================================================================
 * Result-summary cards — bespoke visual summaries that replace the old
 * "Result explanation" prose block at the top of result pages. One layout
 * per submission type lives in pygad/_result_summary/<type>.html, driven by
 * a tiny JS module under static/javascript/result_summaries/. All motion
 * is wrapped in (prefers-reduced-motion: no-preference); the final visual
 * state is also valid when motion is suppressed.
 * ========================================================================== */
.rs-card {
  position: relative;
  overflow: hidden;
}
.rs-eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--phoenix-secondary-color);
}
.rs-headline {
  font-weight: 600;
  color: var(--phoenix-emphasis-color);
  line-height: 1.25;
}
.rs-headline-num {
  font-variant-numeric: tabular-nums;
}
.rs-explain-link {
  text-decoration: none;
  color: var(--phoenix-secondary-color);
}
.rs-explain-link:hover,
.rs-explain-link:focus-visible {
  color: var(--phoenix-primary);
}
.rs-deepdive-anchor {
  font-size: 0.85rem;
  text-decoration: none;
  color: var(--phoenix-secondary-color);
}
.rs-deepdive-anchor:hover,
.rs-deepdive-anchor:focus-visible {
  color: var(--phoenix-primary);
}

/* Chip — used by subset-sum and TSP visit-order lists. */
.rs-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--phoenix-border-color);
  background: var(--phoenix-tertiary-bg);
  color: var(--phoenix-emphasis-color);
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  font-weight: 500;
}
.rs-chip-selected {
  background: var(--phoenix-success-bg-subtle);
  color: var(--phoenix-success-text-emphasis);
  border-color: rgba(var(--phoenix-success-rgb), 0.45);
  box-shadow: 0 0 0 1px rgba(var(--phoenix-success-rgb), 0.2);
}
.rs-chip-faded {
  opacity: 0.55;
}

/* Sum/target progress bar — subset sum. */
.rs-progress {
  position: relative;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--phoenix-tertiary-bg);
  overflow: hidden;
}
.rs-progress-fill {
  height: 100%;
  background: var(--phoenix-primary);
  border-radius: inherit;
  width: 0%;
}
.rs-progress-fill.is-exact { background: var(--phoenix-success); }
.rs-progress-fill.is-over  { background: var(--phoenix-warning); }
.rs-progress-marker {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 2px;
  background: var(--phoenix-emphasis-color);
  opacity: 0.55;
}

/* Diverging bar — linear single objective. */
.rs-diverge-row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 1fr 4rem;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
}
.rs-diverge-track-pos,
.rs-diverge-track-neg {
  position: relative;
  height: 0.85rem;
  background: var(--phoenix-tertiary-bg);
  border-radius: 0.25rem;
  overflow: hidden;
}
.rs-diverge-bar-pos {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0%;
  background: var(--phoenix-primary);
  border-radius: inherit;
}
.rs-diverge-bar-neg {
  position: absolute;
  right: 0;
  top: 0;
  height: 100%;
  width: 0%;
  background: var(--phoenix-danger);
  border-radius: inherit;
}
.rs-diverge-track-neg { border-right: 1px solid var(--phoenix-border-color); }

/* Objective rails — linear multi-objective. */
.rs-rail {
  display: grid;
  grid-template-columns: 7rem 1fr 5rem;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.rs-rail-track {
  position: relative;
  height: 0.6rem;
  background: var(--phoenix-tertiary-bg);
  border-radius: 999px;
}
.rs-rail-bar {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 0%;
  background: var(--phoenix-primary);
  border-radius: inherit;
}
.rs-rail-tick {
  position: absolute;
  top: -4px;
  bottom: -4px;
  width: 2px;
  background: var(--phoenix-success);
}
.rs-pareto-strip {
  position: relative;
  height: 3rem;
  background: var(--phoenix-tertiary-bg);
  border-radius: 0.5rem;
  overflow: hidden;
}
.rs-pareto-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: rgba(var(--phoenix-secondary-color-rgb, 100, 116, 139), 0.45);
  transform: translate(-50%, -50%);
}
.rs-pareto-pin {
  position: absolute;
  width: 11px;
  height: 11px;
  border-radius: 999px;
  background: var(--phoenix-primary);
  box-shadow: 0 0 0 3px rgba(var(--phoenix-primary-rgb), 0.25);
  transform: translate(-50%, -50%);
}

/* Accuracy ring — classification kernel. */
.rs-ring-wrap {
  position: relative;
  width: 9rem;
  height: 9rem;
  margin: 0 auto;
}
.rs-ring-svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.rs-ring-track {
  fill: none;
  stroke: var(--phoenix-tertiary-bg);
  stroke-width: 10;
}
.rs-ring-fill {
  fill: none;
  stroke: var(--phoenix-primary);
  stroke-width: 10;
  stroke-linecap: round;
  stroke-dasharray: 0 999;
}
.rs-ring-label {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  color: var(--phoenix-emphasis-color);
}

/* Confusion-matrix grid — classification kernel. */
.rs-cm-grid {
  display: grid;
  gap: 2px;
  background: var(--phoenix-border-color);
  border: 1px solid var(--phoenix-border-color);
  border-radius: 0.25rem;
  padding: 2px;
  max-width: 14rem;
}
.rs-cm-cell {
  background: var(--phoenix-body-bg);
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--phoenix-secondary-color);
  font-variant-numeric: tabular-nums;
  opacity: 0;
}
/* Match the full-size matrix: correct = green, errors = red, each shaded by
   its share of the actual class (--i is set per cell in JS). */
.rs-cm-cell--correct {
  background: color-mix(in srgb, var(--phoenix-success) calc(var(--i, 0) * 100%), var(--phoenix-body-bg));
  color: var(--phoenix-success-text-emphasis);
  outline: 1.5px solid var(--phoenix-success-border-subtle);
  outline-offset: -1.5px;
}
.rs-cm-cell--error {
  background: color-mix(in srgb, var(--phoenix-danger) calc(var(--i, 0) * 100%), var(--phoenix-body-bg));
  color: var(--phoenix-danger-text-emphasis);
}
/* On phones the summary card stacks full-width; center the matrix instead of
   leaving it pinned to the left. */
@media (max-width: 767.98px) {
  .rs-cm-grid { margin-left: auto; margin-right: auto; }
}

/* TSP tour SVG — both demo and user variants share. */
.rs-tour-svg {
  width: 100%;
  max-width: 14rem;
  height: auto;
}
.rs-tour-path {
  fill: none;
  stroke: var(--phoenix-primary);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.rs-tour-point {
  fill: var(--phoenix-primary);
}
.rs-tour-point-start {
  fill: var(--phoenix-success);
  stroke: var(--phoenix-emphasis-color);
  stroke-width: 1;
}

/* Scatter plot — clustering. */
.rs-scatter-svg {
  width: 100%;
  max-width: 16rem;
  height: auto;
  background: var(--phoenix-tertiary-bg);
  border-radius: 0.5rem;
}
.rs-scatter-point { opacity: 0; }
.rs-scatter-center {
  stroke: var(--phoenix-emphasis-color);
  stroke-width: 1.5;
}

/* RFC tree row + hyperparam pills. */
.rs-tree-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  align-items: center;
}
.rs-tree-icon { color: var(--phoenix-success); opacity: 0; }
.rs-pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* XOR truth-table mini-grid — Keras XOR variant. */
.rs-xor-grid {
  display: grid;
  grid-template-columns: repeat(3, auto);
  gap: 2px;
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  width: max-content;
}
.rs-xor-cell {
  padding: 0.25rem 0.6rem;
  background: var(--phoenix-tertiary-bg);
  text-align: center;
  color: var(--phoenix-emphasis-color);
}
.rs-xor-cell.is-correct {
  background: var(--phoenix-success-bg-subtle);
  color: var(--phoenix-success-text-emphasis);
}
.rs-xor-cell.is-incorrect {
  background: var(--phoenix-danger-bg-subtle);
  color: var(--phoenix-danger-text-emphasis);
}

/* Neural-network architecture diagram (NN problem summaries). The SVG keeps its
   natural size in a horizontally-scrollable wrapper so deep networks scroll
   rather than squash; colours track the theme via Phoenix variables. */
.rs-nn .nn-arch-scroll {
  overflow-x: auto;
  text-align: center;
  padding-bottom: 0.25rem;
}
.nn-svg { display: inline-block; max-width: none; height: auto; }
.nn-edge {
  stroke: var(--phoenix-border-color);
  stroke-width: 1;
  opacity: 0.25;
}
.nn-neuron { stroke-width: 1.5; }
.nn-neuron--input {
  fill: var(--phoenix-info-bg-subtle);
  stroke: var(--phoenix-info-border-subtle);
}
.nn-neuron--hidden {
  fill: var(--phoenix-primary-bg-subtle);
  stroke: var(--phoenix-primary-border-subtle);
}
.nn-neuron--output {
  fill: var(--phoenix-success-bg-subtle);
  stroke: var(--phoenix-success-border-subtle);
}
.nn-count {
  font: 700 13px var(--phoenix-font-sans-serif);
  fill: var(--phoenix-emphasis-color);
  font-variant-numeric: tabular-nums;
}
.nn-count--input { fill: var(--phoenix-info-text-emphasis); }
.nn-count--output { fill: var(--phoenix-success-text-emphasis); }
.nn-role {
  font: 600 10px var(--phoenix-font-sans-serif);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  fill: var(--phoenix-secondary-color);
}
.nn-act {
  font: 400 10px var(--phoenix-font-sans-serif);
  fill: var(--phoenix-tertiary-color);
}
.nn-ellipsis {
  font: 700 14px var(--phoenix-font-sans-serif);
  fill: var(--phoenix-secondary-color);
}
.nn-arch-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--phoenix-secondary-color);
}
.nn-arch-num { font-weight: 700; font-variant-numeric: tabular-nums; }
.nn-arch-num--input { color: var(--phoenix-info-text-emphasis); }
.nn-arch-num--hidden { color: var(--phoenix-primary-text-emphasis); }
.nn-arch-num--output { color: var(--phoenix-success-text-emphasis); }
.nn-arch-arrow { color: var(--phoenix-quaternary-color); }

/* Custom-chart builder: on large screens keep the live preview pinned to the
   top while the controls column scrolls, so edits are visible without scrolling
   back up. The modal body is the scroll container (modal-dialog-scrollable). */
@media (min-width: 992px) {
  #customChartBuilderModal .ccb-preview-col {
    position: sticky;
    top: 0;
    align-self: flex-start;
  }
}

/* Animations — only run when the user has not requested reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes rs-fade-in-up {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes rs-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes rs-chip-slide {
    from { transform: translateX(40px); opacity: 0; }
    to   { transform: translateX(0); opacity: 1; }
  }
  @keyframes rs-bar-grow-pos {
    from { width: 0%; }
  }
  @keyframes rs-bar-grow-neg {
    from { width: 0%; }
  }
  @keyframes rs-progress-grow {
    from { width: 0%; }
  }
  @keyframes rs-ring-sweep {
    from { stroke-dasharray: 0 999; }
  }
  @keyframes rs-stroke-reveal {
    from { stroke-dashoffset: var(--rs-stroke-len, 1000); }
    to   { stroke-dashoffset: 0; }
  }

  .rs-card { animation: rs-fade-in-up 360ms ease both; }
  .rs-chip { animation: rs-fade-in 280ms ease both; }
  .rs-chip-selected { animation: rs-chip-slide 380ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
  .rs-diverge-bar-pos { transition: width 700ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .rs-diverge-bar-neg { transition: width 700ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .rs-rail-bar       { transition: width 700ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .rs-progress-fill  { transition: width 800ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .rs-ring-fill      { transition: stroke-dasharray 1100ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .rs-cm-cell.is-on  { animation: rs-fade-in 320ms ease forwards; }
  .rs-scatter-point.is-on { animation: rs-fade-in 320ms ease forwards; }
  .rs-tree-icon.is-on { animation: rs-fade-in 220ms ease forwards; }
  .rs-tour-path.is-on { animation: rs-stroke-reveal 1200ms ease forwards; }
}

@media (prefers-reduced-motion: reduce) {
  /* Final state without animation. JS still sets target widths/strokes, so
   * the visual is correct — only the transition is suppressed. */
  .rs-cm-cell.is-on,
  .rs-scatter-point.is-on,
  .rs-tree-icon.is-on { opacity: 1; }
  .rs-tour-path.is-on { stroke-dashoffset: 0; }
}

/* ==========================================================================
 * Problem-intro cards (.pi-*) — concept illustrations rendered at the top
 * of every quick-submission form. Designed in the same visual language as
 * the .rs-* result-summary cards but tuned for pedagogy (no real run
 * data yet). Animations only run when prefers-reduced-motion allows.
 * ========================================================================== */
.pi-card {
  position: relative;
  overflow: hidden;
}
.pi-eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--phoenix-secondary-color);
}
.pi-headline {
  font-weight: 600;
  color: var(--phoenix-emphasis-color);
  line-height: 1.3;
}
.pi-stage {
  /* Soft theme-aware primary tint instead of the flat grey from
     --phoenix-tertiary-bg. The diagonal gradient lifts toward the
     top-left so the panel reads as a "sky" backdrop in light mode and
     a calm muted-blue in dark mode (the primary RGB carries over). */
  background: linear-gradient(
    135deg,
    rgba(var(--phoenix-primary-rgb), 0.08),
    rgba(var(--phoenix-info-rgb), 0.04)
  );
  border: 1px solid rgba(var(--phoenix-primary-rgb), 0.08);
  border-radius: 0.625rem;
  padding: 1rem;
}
.pi-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  padding: 0.2rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--phoenix-border-color);
  background: var(--phoenix-body-bg);
  color: var(--phoenix-emphasis-color);
  font-variant-numeric: tabular-nums;
  font-size: 0.8rem;
}
.pi-chip-target {
  background: var(--phoenix-warning-bg-subtle);
  color: var(--phoenix-warning-text-emphasis);
  border-color: rgba(var(--phoenix-warning-rgb), 0.45);
  font-weight: 600;
}
.pi-chip-selected {
  background: var(--phoenix-success-bg-subtle);
  color: var(--phoenix-success-text-emphasis);
  border-color: rgba(var(--phoenix-success-rgb), 0.5);
}
.pi-arrow {
  color: var(--phoenix-secondary-color);
  font-size: 1.1rem;
}
/* Conceptual neural-net layers icon used by Keras/Torch intros. */
.pi-nn-svg {
  width: 100%;
  max-width: 16rem;
  height: 5rem;
}
.pi-nn-node {
  fill: var(--phoenix-tertiary-bg);
  stroke: var(--phoenix-primary);
  stroke-width: 1.4;
}
.pi-nn-edge {
  stroke: var(--phoenix-primary);
  stroke-width: 1;
  stroke-opacity: 0.35;
}
.pi-nn-pulse {
  fill: var(--phoenix-primary);
  opacity: 0;
}
.pi-forest-row {
  display: flex;
  gap: 0.25rem;
  align-items: center;
  flex-wrap: wrap;
}
.pi-forest-icon {
  color: var(--phoenix-success);
  font-size: 1.1rem;
}
.pi-runs-stack {
  position: relative;
  height: 4rem;
}
.pi-run-line {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--phoenix-primary);
  opacity: 0.35;
}

/* Mini timetable used by the Timetable Scheduling problem-intro card.
   Rendered with a real <table> element for semantics; the 1px gridlines
   come from border-collapse: separate + border-spacing: 1px with the
   table's background colour showing through between cells. The outer
   frame carries a soft shadow and rounded corners so the result reads
   as a small calendar widget, and each populated slot renders as a
   colour-coded event card (.pi-tt-event-*). */
.pi-tt-frame {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 1px;
  background-color: var(--phoenix-border-color);
  border: 1px solid var(--phoenix-border-color);
  border-radius: 0.625rem;
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.04);
  margin: 0;
}
.pi-tt-frame th,
.pi-tt-frame td {
  background-color: var(--phoenix-body-bg);
  padding: 0.5rem 0.4rem;
  vertical-align: middle;
  text-align: center;
  font-size: 0.72rem;
  line-height: 1.2;
  font-weight: 500;
}
.pi-tt-frame-result {
  /* The result table shares the chrome of every .pi-tt-frame; no
     min-width because we want the table to fit any parent (e.g. the
     example-result modal) without triggering horizontal scroll. The
     ellipsis on .pi-tt-event-title/sub keeps overflowing text tidy. */
}
.pi-tt-head,
.pi-tt-rowhead,
.pi-tt-corner {
  /* Soft theme-aware primary tint — gives the chrome a calendar-app feel
     instead of the flat grey from --phoenix-tertiary-bg. The gradient
     darkens slightly toward the bottom of each header cell. */
  background: linear-gradient(
    180deg,
    rgba(var(--phoenix-primary-rgb), 0.10),
    rgba(var(--phoenix-primary-rgb), 0.06)
  );
  color: var(--phoenix-primary-text-emphasis);
  font-weight: 600;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.pi-tt-cell-empty {
  color: var(--phoenix-body-tertiary-color);
  font-weight: 500;
}
.pi-tt-cell-result {
  padding: 0.35rem;
}
.pi-tt-open-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.75rem;
  border: 1px dashed rgba(var(--phoenix-primary-rgb), 0.40);
  border-radius: 999px;
  background-color: rgba(var(--phoenix-primary-rgb), 0.08);
  color: var(--phoenix-primary-text-emphasis);
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.pi-tt-event {
  width: 100%;
  flex: 1 1 auto;
  border-radius: 0.5rem;
  padding: 0.55rem 0.7rem;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.55rem;
  border-left: 3px solid currentColor;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03);
}
.pi-tt-event-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  text-align: left;
  letter-spacing: 0;
  text-transform: none;
}
.pi-tt-event-title {
  font-weight: 700;
  font-size: 0.86rem;
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: none;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
  word-break: break-word;
}
.pi-tt-event-sub {
  font-size: 0.74rem;
  line-height: 1.2;
  opacity: 0.9;
  letter-spacing: 0;
  text-transform: none;
  display: flex;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
}
.pi-tt-event-sub > i {
  flex-shrink: 0;
}
.pi-tt-event-sub-text {
  flex: 1 1 auto;
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
  word-break: break-word;
}
.pi-tt-room {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.18rem 0.55rem;
  border-radius: 999px;
  background-color: var(--phoenix-body-bg);
  color: var(--phoenix-secondary-color);
  border: 1px solid var(--phoenix-border-color);
  font-weight: 600;
  font-size: 0.7rem;
  letter-spacing: 0;
  text-transform: none;
}
.pi-tt-event-primary   { background: var(--phoenix-primary-bg-subtle);   color: var(--phoenix-primary-text-emphasis); }
.pi-tt-event-info      { background: var(--phoenix-info-bg-subtle);      color: var(--phoenix-info-text-emphasis); }
.pi-tt-event-success   { background: var(--phoenix-success-bg-subtle);   color: var(--phoenix-success-text-emphasis); }
.pi-tt-event-warning   { background: var(--phoenix-warning-bg-subtle);   color: var(--phoenix-warning-text-emphasis); }
.pi-tt-event-danger    { background: var(--phoenix-danger-bg-subtle);    color: var(--phoenix-danger-text-emphasis); }
.pi-tt-event-secondary { background: var(--phoenix-secondary-bg);        color: var(--phoenix-emphasis-color); }

/* Conflict highlight on a result cell — uses inset shadow so the
   indicator sits flush inside the table cell border-spacing without
   doubling the gridline. */
.pi-tt-cell-conflict {
  background-color: var(--phoenix-danger-bg-subtle) !important;
  box-shadow: inset 0 0 0 1px var(--phoenix-danger);
}

/* Swimlane "track" used in the result-summary By-attribute tab. */
.pi-tt-lane {
  flex: 1 1 auto;
  position: relative;
  min-height: 3.5rem;
  border-radius: 0.4rem;
  background-color: var(--phoenix-body-bg);
  border: 1px solid var(--phoenix-border-color);
}
/* Stack title above room pill so neither has to fight for horizontal
   room in narrow chips. Title can wrap to two lines; the room pill
   sits below as a small chip. */
.pi-tt-lane-chip {
  position: absolute;
  top: 0.2rem;
  bottom: 0.2rem;
  border-radius: 0.3rem;
  padding: 0.2rem 0.3rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.15rem;
  font-size: 0.7rem;
  font-weight: 600;
  overflow: hidden;
  border-left: 2px solid currentColor;
  text-align: left;
}
.pi-tt-lane-chip-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
  word-break: break-word;
  line-height: 1.1;
  font-size: 0.66rem;
}
.pi-tt-lane-chip-room {
  align-self: flex-start;
  padding: 0.02rem 0.4rem;
  border-radius: 999px;
  background-color: var(--phoenix-body-bg);
  color: var(--phoenix-emphasis-color);
  border: 1px solid var(--phoenix-border-color);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.15;
}
.pi-tt-lane-chip-slot {
  /* Hidden by default — the chip's absolute horizontal position
     already encodes the slot. Surfaced only at the narrow-viewport
     breakpoint below, where the lane reflows to a vertical list and
     the slot label is the only way to know which time each chip is. */
  display: none;
  align-self: flex-start;
  padding: 0.02rem 0.4rem;
  border-radius: 999px;
  background-color: var(--phoenix-tertiary-bg);
  color: var(--phoenix-secondary-color);
  border: 1px solid var(--phoenix-border-color);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.15;
}
.pi-tt-avatar-danger    { background: var(--phoenix-danger-bg-subtle);    color: var(--phoenix-danger-text-emphasis); }
.pi-tt-avatar-secondary { background: var(--phoenix-secondary-bg);        color: var(--phoenix-emphasis-color); }

/* Compact "tile" used by the Tasks and Resources input lists in the
   Timetable Scheduling intro card. Each tile is content-width
   (display: inline-flex) so the list flex-wraps naturally — no more
   full-row badges with empty right sides. Body-bg keeps the tile from
   blending with the .pi-stage sky tint. */
.pi-tt-tile {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  background-color: var(--phoenix-body-bg);
  border: 1px solid var(--phoenix-border-color);
  border-radius: 0.55rem;
  padding: 0.3rem 0.65rem 0.3rem 0.35rem;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
  max-width: 100%;
}
.pi-tt-tile-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}
.pi-tt-tile-title {
  font-weight: 600;
  font-size: 0.76rem;
  color: var(--phoenix-emphasis-color);
}
.pi-tt-tile-sub {
  font-size: 0.66rem;
  color: var(--phoenix-secondary-color);
}
.pi-tt-avatar {
  width: 1.7rem;
  height: 1.7rem;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.72rem;
  flex-shrink: 0;
}
.pi-tt-avatar-primary { background: var(--phoenix-primary-bg-subtle); color: var(--phoenix-primary-text-emphasis); }
.pi-tt-avatar-info    { background: var(--phoenix-info-bg-subtle);    color: var(--phoenix-info-text-emphasis); }
.pi-tt-avatar-success { background: var(--phoenix-success-bg-subtle); color: var(--phoenix-success-text-emphasis); }
.pi-tt-avatar-warning { background: var(--phoenix-warning-bg-subtle); color: var(--phoenix-warning-text-emphasis); }

/* Horizontal-scroll wrapper for the result timetable. Keeps the
   horizontal event-card layout intact on narrow viewports — instead of
   collapsing or wrapping awkwardly, the user pans the table sideways. */
.pi-tt-scroll-wrap {
  overflow-x: auto;
  /* negative margin + padding keeps the frame's drop shadow from being
     clipped by overflow:auto's box-edge */
  margin: -0.25rem;
  padding: 0.25rem;
  -webkit-overflow-scrolling: touch;
}
/* legacy: was nowrap+ellipsis; now handled by the per-class -webkit-box
   clamp rules above, which let titles and sub names wrap to two lines
   on narrow cells instead of clipping to a single letter. */

/* ===== Compact reflow ===================================================
 * Real Timetable Scheduling results have many slots (e.g. 5 days × 6
 * periods = 30). With absolute-positioned chips at 100/N % each, chips
 * collapse to sub-pixel widths on every screen size — viewport media
 * queries can't catch this because the bug is data-density driven.
 *
 * The JS measures the actual chip / cell width at render time and
 * toggles .pi-tt-swimlanes-compact / .pi-tt-grid-compact when the
 * fixed-position layout would be unreadable. Both classes reflow the
 * same DOM to a vertical list — every chip and every non-empty cell
 * gets the full row width, so titles, instructors, and room badges
 * all render at a readable size regardless of slot count.
 *
 * The phone-width media query at the bottom acts as an additional
 * safety net for very narrow viewports.
 * ======================================================================== */

/* ---- Event card compaction (applies regardless of slot count, just
   for narrow viewports). ------------------------------------------------ */
/* ---- Swimlane compact mode (toggled by JS when chip width < ~80px,
   or by the small-viewport media query below). The reflow converts
   the absolute-positioned timeline into a clean vertical list of
   full-width chips, each showing its slot label so the slot order is
   still visible. -------------------------------------------------------- */
.pi-tt-swimlanes-compact > .d-flex {
  flex-direction: column !important;
  align-items: stretch !important;
  gap: 0.4rem !important;
}
.pi-tt-swimlanes-compact .pi-tt-tile {
  min-width: 0 !important;
  align-self: flex-start;
}
.pi-tt-swimlanes-compact .pi-tt-lane {
  position: static;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  border: 0;
  background: transparent;
  min-height: 0;
  padding: 0;
}
.pi-tt-swimlanes-compact .pi-tt-lane-chip {
  position: static !important;
  top: auto !important;
  left: auto !important;
  bottom: auto !important;
  width: 100% !important;
  flex-direction: row !important;
  align-items: center !important;
  padding: 0.45rem 0.7rem !important;
  border-radius: 0.45rem !important;
  gap: 0.6rem !important;
  border-left-width: 3px;
}
.pi-tt-swimlanes-compact .pi-tt-lane-chip-slot {
  display: inline-flex !important;
  flex-shrink: 0;
}
.pi-tt-swimlanes-compact .pi-tt-lane-chip-text {
  flex: 1 1 auto !important;
  display: block !important;
  -webkit-line-clamp: unset !important;
  white-space: nowrap !important;
  overflow: hidden !important;
  text-overflow: ellipsis !important;
  word-break: normal !important;
  font-size: 0.8rem !important;
  line-height: 1.2 !important;
}
.pi-tt-swimlanes-compact .pi-tt-lane-chip-room {
  align-self: center !important;
  padding: 0.1rem 0.55rem !important;
  font-size: 0.68rem !important;
}

/* ---- Grid compact mode (toggled by JS when a cell is < ~110px wide,
   or by the small-viewport media query below). The reflow converts the
   <table> into a per-day card list with a small period badge on every
   non-empty cell. ------------------------------------------------------- */
.pi-tt-grid-compact .pi-tt-frame-result {
  display: block;
  border-spacing: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  min-width: 0;
}
.pi-tt-grid-compact .pi-tt-frame-result colgroup,
.pi-tt-grid-compact .pi-tt-frame-result thead {
  display: none;
}
.pi-tt-grid-compact .pi-tt-frame-result tbody {
  display: block;
}
.pi-tt-grid-compact .pi-tt-frame-result tr {
  display: block;
  background: var(--phoenix-body-bg);
  border: 1px solid var(--phoenix-border-color);
  border-radius: 0.5rem;
  margin-bottom: 0.6rem;
  overflow: hidden;
}
.pi-tt-grid-compact .pi-tt-frame-result th[scope="row"].pi-tt-rowhead {
  display: block !important;
  width: auto !important;
  text-align: left !important;
  padding: 0.5rem 0.75rem !important;
  background: linear-gradient(180deg,
    rgba(var(--phoenix-primary-rgb), 0.10),
    rgba(var(--phoenix-primary-rgb), 0.06)) !important;
  color: var(--phoenix-primary-text-emphasis) !important;
  font-weight: 700 !important;
  font-size: 0.82rem !important;
  letter-spacing: 0.06em !important;
  text-transform: uppercase !important;
  line-height: 1.2;
  border-bottom: 1px solid var(--phoenix-border-color);
}
.pi-tt-grid-compact .pi-tt-frame-result td.pi-tt-cell {
  display: flex !important;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.75rem !important;
  background: transparent !important;
  border: 0 !important;
  text-align: left !important;
  border-top: 1px solid var(--phoenix-border-color);
}
.pi-tt-grid-compact .pi-tt-frame-result td.pi-tt-cell-empty {
  display: none !important;
}
.pi-tt-grid-compact .pi-tt-frame-result td.pi-tt-cell::before {
  content: attr(data-period-label);
  display: inline-flex;
  align-items: center;
  padding: 0.15rem 0.6rem;
  background: var(--phoenix-tertiary-bg);
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--phoenix-secondary-color);
  flex-shrink: 0;
  letter-spacing: 0.04em;
}
.pi-tt-grid-compact .pi-tt-frame-result td.pi-tt-cell .pi-tt-event {
  flex: 1 1 auto;
  margin: 0;
}

@media (max-width: 767.98px) {
  /* ---- Event cards (used in both intro illustration and result grid).
     Smaller padding / fonts at phone widths regardless of compact mode. -- */
  .pi-tt-cell-result {
    padding: 0.3rem;
  }
  .pi-tt-event {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
    padding: 0.4rem 0.5rem;
  }
  .pi-tt-event-body {
    width: 100%;
  }
  .pi-tt-event-title {
    font-size: 0.8rem;
  }
  .pi-tt-event-sub {
    font-size: 0.7rem;
  }
  .pi-tt-room {
    align-self: flex-start;
    padding: 0.12rem 0.5rem;
    font-size: 0.66rem;
  }
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes pi-pop {
    from { opacity: 0; transform: scale(0.85); }
    to   { opacity: 1; transform: scale(1); }
  }
  @keyframes pi-arrow {
    0%, 100% { transform: translateX(0); opacity: 0.6; }
    50%      { transform: translateX(4px); opacity: 1; }
  }
  @keyframes pi-pulse-along {
    0%   { opacity: 0; cx: 0; }
    50%  { opacity: 1; }
    100% { opacity: 0; cx: 100%; }
  }
  .pi-card     { animation: rs-fade-in-up 360ms ease both; }
  .pi-chip     { animation: pi-pop 320ms ease both; }
  .pi-arrow    { animation: pi-arrow 1.6s ease-in-out infinite; }
  .pi-nn-pulse { animation: rs-fade-in 1.6s ease-in-out infinite; }
  .pi-forest-icon { animation: rs-fade-in 320ms ease both; }
}

/* ==========================================================================
 * Submission-overview card (.so-*) — sits above the existing detail
 * sections on app_submission.html and public_submission.html. Renders
 * type icon, name, lineage, GA-parameter chips, run stats, and a
 * sparkline of best fitness per run.
 * ========================================================================== */
.so-card {
  position: relative;
}
.so-headline {
  font-weight: 600;
  color: var(--phoenix-emphasis-color);
  line-height: 1.25;
}
.so-eyebrow {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--phoenix-secondary-color);
}
/* Type-label link in the submission overview eyebrow. Inherits the
   eyebrow's color so it sits flat with the surrounding label, then
   reveals a subtle underline on hover/focus to signal interactivity
   without competing with the headline above. */
.so-type-link {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 150ms ease, color 150ms ease;
}
.so-type-link:hover,
.so-type-link:focus-visible {
  color: var(--phoenix-emphasis-color);
  border-bottom-color: var(--phoenix-border-color);
}
.so-icon {
  width: 3rem;
  height: 3rem;
  border-radius: 0.75rem;
  background: var(--phoenix-primary-bg-subtle);
  color: var(--phoenix-primary-text-emphasis);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.4rem;
  flex-shrink: 0;
}
.so-chip {
  border: 1px solid var(--phoenix-border-color);
  border-radius: 0.5rem;
  padding: 0.75rem 1rem;
  background: var(--phoenix-body-bg);
  height: 100%;
}
.so-chip-num {
  font-size: 1.5rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--phoenix-emphasis-color);
  line-height: 1;
}
.so-chip-label {
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--phoenix-secondary-color);
  font-weight: 600;
}
.so-spark-svg {
  width: 100%;
  height: 2.25rem;
}
.so-spark-line {
  fill: none;
  stroke: var(--phoenix-primary);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.so-spark-best {
  fill: var(--phoenix-success);
  stroke: var(--phoenix-body-bg);
  stroke-width: 1.4;
}
.so-fork-pill {
  background: var(--phoenix-info-bg-subtle);
  color: var(--phoenix-info-text-emphasis);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 500;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border: 1px solid rgba(var(--phoenix-info-rgb), 0.35);
}
.so-fork-pill:hover,
.so-fork-pill:focus-visible {
  background: rgba(var(--phoenix-info-rgb), 0.18);
}
.so-runs-line {
  font-size: 0.85rem;
  color: var(--phoenix-secondary-color);
}
.so-runs-line strong {
  color: var(--phoenix-emphasis-color);
  font-weight: 600;
}

@media (prefers-reduced-motion: no-preference) {
  .so-card  { animation: rs-fade-in-up 380ms ease both; }
  .so-chip  { animation: rs-fade-in 360ms ease both; }
  .so-spark-line {
    stroke-dasharray: var(--so-spark-len, 200);
    stroke-dashoffset: var(--so-spark-len, 200);
    animation: rs-stroke-reveal 900ms ease forwards;
  }
}

/* ==========================================================================
 * Responsive-audit Phase 1 fixes
 * Surfaced by pygadproject/tests/responsive_audit/. Each block is anchored
 * to a specific finding and lives here so the rules load on every page —
 * the footer mark, for instance, is rendered on every public page but its
 * sizing originally lived in home_sections.css which only loads on a few
 * pages, so the logo image displayed at its native ~743px width on
 * /subscription, /faq, /privacy, /docs, /accounts/*, /user, /app, /billing
 * and forced a body-level horizontal scrollbar there.
 * ========================================================================== */

/* Footer V-mark constraint. The footer logo is a wordmark (logo.png /
   logo-white.png) whose native aspect ratio is ~11:1. home_sections.css
   declares .footer-vmark as a 64x64 anchor box with .footer-vmark-img
   sized "height: 64px; width: auto;" — fine on /, /contact (which load
   home_sections), but on every other page the image rendered at its full
   ~743px width with no parent constraint. Pin the box AND the image so
   the layout is robust without the per-section sheet. */
.footer-vmark {
  display: inline-block;
  width: 64px;
  height: 64px;
  line-height: 0;
}
.footer-vmark-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Make sure ANY <img> without an explicit width never spills out of its
   parent. Phoenix and Bootstrap both ship this as part of their reset,
   but a handful of Vilvik templates load avatar / logo images without
   .img-fluid and so dodge it. Catching them here is cheaper than
   chasing the templates and there is no legitimate case for an img to
   be wider than its containing block on a responsive layout. */
img {
  max-width: 100%;
  height: auto;
}
/* Two exceptions: .footer-vmark-img wants explicit height:100% (so it
   fills the 64x64 anchor) and .vlogo-anim images already self-contain.
   Re-assert those after the global rule above. */
.footer-vmark-img,
.vlogo-anim svg,
.vlogo-anim img {
  height: 100%;
}

/* Bump the theme-toggle hit area to >=44x44 at touch viewports. The
   visible icon stays the same size; we just give the surrounding
   anchor padding so a thumb tap registers reliably. Same idea for the
   navbar collapse toggle (.navbar-toggler). 44px is the WCAG 2.5.5
   AAA target size. */
@media (max-width: 575.98px) {
  #themeControlToggle,
  .navbar-toggler {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* Footer + contact social chips were 42x42 (visual + padding). Bump to
   44x44 to clear the WCAG 2.5.5 AAA threshold. The contact-* chips live
   on /, /contact, and other pages that load home_sections.css; the
   footer chip on every page. Scoped to mobile so the desktop layout is
   untouched. */
@media (max-width: 575.98px) {
  .footer-social-chip,
  .contact-social-chip {
    min-width: 44px;
    min-height: 44px;
  }

  /* Square icon buttons on the home/contact tiles (the chevron at the
     end of each tile row). Originally 32x35 — bump to 44x44. */
  .contact-tile-action {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Chip-style email/copy controls in the contact section. Originally
     ~80x29. Raising vertical padding hits 44 without making them look
     comically tall — chip width naturally adapts to label length. */
  .contact-chip,
  button.contact-chip {
    min-height: 44px;
    padding-top: 0.4rem;
    padding-bottom: 0.4rem;
  }

  /* Floating back-to-top button — 39x60 in baseline CSS (too narrow). */
  .footer-back-to-top {
    min-width: 44px;
    min-height: 44px;
  }

  /* Persistent floating feedback launcher (43x38 before). */
  #vilvikFeedbackLauncher {
    min-width: 44px;
    min-height: 44px;
  }

  /* Bootstrap accordion buttons (faq, docs) sit at ~256x36 by default.
     Padding bump only — the chevron + text stay centred. */
  .accordion-button {
    min-height: 44px;
  }

  /* Public-page theme toggle (sibling to #themeControlToggle) on
     /app/discover and other public-app surfaces. Same 32x32 issue. */
  #publicThemeControlToggle {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Per-message edit/delete actions in the messaging UI. Originally
     12x13 (transparent, padding: 0, line-height: 1) so the icon hides
     unless hovered. Mobile users can't hover — give the hit area
     enough flesh that a thumb tap registers. The visible icon stays
     compact; only the click target grows. */
  .vk-msg-action {
    min-width: 44px;
    min-height: 44px;
    padding: 0.5rem;
  }

  /* Onboarding tour close button (vk-tour-close) — 21x22 baseline. */
  .vk-tour-close {
    min-width: 44px;
    min-height: 44px;
  }

  /* Floating form controls toggle (ffc-toggle). 28x28 baseline. */
  .ffc-toggle {
    min-width: 44px;
    min-height: 44px;
  }

  /* Floating form controls chevron handle (ffc-handle). 72x32 baseline —
     wide enough but only 32px tall, below the 44px WCAG target. Bump the
     handle to 44px tall on mobile so a thumb tap registers reliably. The
     handle still tucks into the bar via its negative margin-bottom; the
     bar slides ~12px further down on mobile, which is visually fine. */
  .ffc-handle {
    height: 44px;
  }

  /* Generic short Bootstrap buttons (sm + tight padding) used across
     docs, billing toolbars, and HTMX action panels. Apply min-height
     AND min-width so any .btn-sm clears 44x44. Excluded: .btn-close
     (Bootstrap-styled; capped separately).
     `.btn-group-sm > .btn` covers buttons that inherit small sizing
     from a parent group (segmented controls on the logo-animation
     preview, the messaging composer toolbar, etc.) instead of carrying
     `.btn-sm` directly. */
  .btn-sm,
  .btn.btn-sm,
  .btn.px-3,
  .btn-group-sm > .btn {
    min-height: 44px;
    min-width: 44px;
  }

  /* Bootstrap close button — 32x32 baseline. Phoenix already styles it
     with a fixed size; bump for touch. */
  .btn-close {
    width: 44px;
    height: 44px;
    background-size: 1em;
  }

  /* Nav-link tabs (submissions filter strip, etc). */
  .nav-link {
    min-height: 44px;
  }

  /* Quick-submission wizard "Next" button (#nextButton) used across
     the /app/new/quick/<variant>/parameters/ pages. 79x37 in baseline
     CSS; the extra 7px is invisible on most screens but the touch
     target needs to clear 44px. Plain min-height keeps the existing
     label centering. */
  #nextButton {
    min-height: 44px;
  }

  /* MOO objective rows on /app/new/quick/moo/parameters/. The per-row
     "name" + "target" inputs render at 49x38 because their column is
     narrow and the row is dense. Bumping min-height keeps the row
     compact-enough while clearing the touch threshold. */
  .target-row-name,
  .target-row-input {
    min-height: 44px;
  }
}

/* Defensive cap against body-level horizontal scrollbars. The Phoenix
   theme uses a "negative margin to break out of card padding" pattern
   (e.g. .mx-n4 px-4 on the submissions table wrapper) that visually
   pulls inner elements flush with the card edges. The pattern assumes
   the surrounding card's padding-x perfectly matches the negative
   value — but the audit shows that breaks at narrow viewports (320,
   375) where padding-x compresses or the card has its own border.
   overflow-x: clip on html+body caps both scrollWidth and the visible
   scrollbar; internal overflow-x:auto containers (.table-responsive
   etc.) still scroll horizontally on their own. clip (not hidden) is
   load-bearing: overflow-x: hidden silently turns html/body into a
   scroll container, which kills position: sticky descendants
   (sticky-top navbars, sticky table headers). clip clips without
   creating a scroll container, so sticky stays alive. */
html,
body {
  overflow-x: clip;
}

/* Long unbreakable strings in chat-style content (URLs, code spans
   without break opportunities) used to push the inbox + conversation
   row past the viewport. The conversation wrappers already use
   .text-break in templates, but the rule below catches loose <code>
   and <pre> blocks that don't carry it. */
.conversation-body pre,
.conversation-body code,
.message-body pre,
.message-body code,
.message-preview {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* ==========================================================================
 * A13: submission report (markdown) typography.
 *
 * Applied to <article class="report-body"> on the owner submission page,
 * the public submission page, and the live preview pane in the report
 * editor. Theme-aware via Phoenix CSS custom properties so code blocks
 * and blockquotes adapt to dark mode.
 * ========================================================================== */
.report-body { line-height: 1.65; }
.report-body h1,
.report-body h2,
.report-body h3 { margin-top: 1.25em; margin-bottom: .5em; }
.report-body p { margin-bottom: 1em; }
.report-body pre {
  background: var(--phoenix-tertiary-bg);
  padding: .75em 1em;
  border-radius: .5em;
  overflow-x: auto;
}
.report-body code {
  background: var(--phoenix-tertiary-bg);
  padding: .1em .35em;
  border-radius: .25em;
}
.report-body pre code { background: transparent; padding: 0; }
.report-body blockquote {
  border-left: 3px solid var(--phoenix-border-color);
  padding-left: 1em;
  color: var(--phoenix-secondary-color);
}
.report-body img { max-width: 100%; }

/* ===== List-page card tiles (quick-start + demos) ====================== */
/* Visual surface for the redesigned cards on /app/new/quick/ and        */
/* /app/demo/. Each tile has a logo wrapper (currentColor SVG), an info  */
/* button, a title link, and a primary CTA. All colours come from        */
/* Phoenix theme tokens so light and dark themes inherit automatically.  */
.vil-card-tile {
  border-width: 1.5px;
}
.vil-card-tile .vil-card-logo {
  width: 56px;
  height: 56px;
  border-radius: .75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.vil-card-tile .vil-card-logo svg {
  width: 36px;
  height: 36px;
  display: block;
}
.vil-card-tile .vil-card-info-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  line-height: 1;
}
.vil-card-tile .vil-card-info-btn:hover {
  background: var(--phoenix-emphasis-bg);
}
.vil-card-tile .vil-card-info-btn:focus-visible {
  outline: 2px solid var(--phoenix-primary);
  outline-offset: 2px;
}
.vil-card-tile .vil-card-title-link:hover {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.vil-card-section-heading {
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--phoenix-emphasis-color);
  text-transform: none;
}

/* Inline variant of the card logo chip. Used outside the .vil-card-tile  */
/* container (parameter / result / demo detail pages), placed to the     */
/* left of the page heading. Sized smaller than the card's 56px tile so  */
/* it sits proportionally next to an h1 / h2.                            */
.vil-card-logo--inline {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: .5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  padding: .35rem;
}
.vil-card-logo--inline .vil-card-logo-svg {
  width: 100%;
  height: 100%;
  display: block;
}

@media (prefers-reduced-motion: no-preference) {
  .vil-card-tile {
    transition: transform .18s ease, box-shadow .18s ease,
                border-color .18s ease;
  }
  .vil-card-tile:hover {
    transform: translateY(-2px);
    box-shadow: var(--phoenix-box-shadow);
  }
  .vil-card-tile .vil-card-info-btn {
    transition: transform .15s ease, background-color .15s ease;
  }
  .vil-card-tile .vil-card-info-btn:hover {
    transform: scale(1.1);
  }
  .vil-card-tile .vil-card-logo svg {
    transition: transform .25s ease;
  }
  .vil-card-tile:hover .vil-card-logo svg {
    transform: scale(1.08);
  }
}

/* ===== G1: skip-link styling ============================================ */
/* Invisible until focused. On focus it pops into the top-left corner as a */
/* Phoenix-coloured pill so a keyboard user can leap past the navbar to    */
/* the main content. Tokens (var --phoenix-*) keep dark / light themes in  */
/* sync without an extra rule.                                             */
.vilvik-skip-link.visually-hidden-focusable:focus,
.vilvik-skip-link.visually-hidden-focusable:focus-within {
  position: fixed;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 2000;
  padding: 0.5rem 1rem;
  background: var(--phoenix-primary);
  color: #fff;
  border-radius: 0.5rem;
  font-weight: 600;
  text-decoration: none;
  box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.2);
  outline: 2px solid var(--phoenix-primary-bg-subtle);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .vilvik-skip-link.visually-hidden-focusable {
    transition: top 0.2s ease-out;
  }
}

/* ===== Collapsed-sidebar content-margin override ========================= */
/* Phoenix's stock collapsed-sidebar rule uses an ADJACENT-sibling           */
/* combinator between .navbar-vertical and .navbar.navbar-top:               */
/*   .navbar-vertical-collapsed .navbar-vertical.navbar-expand-lg            */
/*     + .navbar.navbar-top ~ .content { margin-left: 4rem !important }     */
/* The G1 skip-link (above) renders an <a> + <script> between the two navs, */
/* so the `+` no longer matches and .content stays at the expanded          */
/* margin-left (15.875rem), pushing the body right even when the sidebar    */
/* is collapsed. Restate the rule with a general-sibling combinator (~) so  */
/* it survives intervening siblings; the skip link stays as the first       */
/* focusable element on the page (a11y requirement).                        */
.navbar-vertical-collapsed .navbar-vertical.navbar-expand-lg ~ .content {
  margin-left: 4rem !important;
}
[dir="rtl"] .navbar-vertical-collapsed .navbar-vertical.navbar-expand-lg ~ .content {
  margin-right: 4rem !important;
  margin-left: 0 !important;
}

/* ===== GA demo overlays — paused state shows only a corner badge ========= */
/* The canvas demos under /app/demo/ share one overlay <div> per demo for   */
/* start/paused/waiting/done/error messages. Each per-demo CSS block dims   */
/* that overlay with a translucent dark background, which is helpful for    */
/* informational states but defeats pausing-to-inspect. Drop the backdrop   */
/* ONLY when the overlay is in the "paused" state, and dock a small pause  */
/* badge in the top-right corner of the canvas. Selector specificity        */
/* (0,2,0) beats the per-demo .{demo}-overlay rule (0,1,0) without needing  */
/* !important. Wired up by static/javascript/demos/_shared/demo_overlay.js  */
/* which sets data-overlay-state on the element and injects the badge HTML. */
[id$="-overlay"].is-visible[data-overlay-state="paused"] {
  background: transparent;
  color: var(--phoenix-body-color);
  align-items: flex-start;
  justify-content: flex-end;
  padding: 0.5rem;
}
.demo-pause-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  background: var(--phoenix-emphasis-bg);
  color: var(--phoenix-emphasis-color);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
  border: 1px solid var(--phoenix-border-color);
}
.demo-pause-badge svg {
  width: 1.1rem;
  height: 1.1rem;
}
@media (prefers-reduced-motion: no-preference) {
  [id$="-overlay"].is-visible[data-overlay-state="paused"] .demo-pause-badge {
    animation: vilvik-demo-pause-fade-in 180ms ease-out;
  }
  @keyframes vilvik-demo-pause-fade-in {
    from { opacity: 0; transform: scale(0.85); }
    to   { opacity: 1; transform: scale(1); }
  }
}

/* Share toolbar + recorder (pygad/_share_menu.html, demo_recorder.js).
   The Record button pulses while a recording is in progress so the active
   state is obvious. Motion is gated behind prefers-reduced-motion; the
   icon + label change to a Stop state regardless, so the state is still
   clear when motion is reduced. */
@media (prefers-reduced-motion: no-preference) {
  .vilvik-share .btn.vilvik-recording {
    animation: vilvik-rec-pulse 1.4s ease-in-out infinite;
  }
  @keyframes vilvik-rec-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(250, 59, 29, 0.45); }
    50%      { box-shadow: 0 0 0 6px rgba(250, 59, 29, 0); }
  }
}

/* Demo HUD on small screens (shared by every canvas demo via
   _demo_canvas_chrome). The HUD is an absolute overlay on the canvas, which
   works on wide screens but swamps the small canvas on phones and narrow
   windows (the user could not see the demo). Up to the md breakpoint, reflow
   it into a compact strip beneath the canvas so the simulation stays fully
   visible. The [aria-live] attribute lifts the specificity above each demo's
   own .{prefix}-hud rule (which sets position:absolute) without needing
   !important. */
@media (max-width: 767.98px) {
  .demo-hud[aria-live] {
    position: static;
    inset: auto;
    z-index: auto;
    margin: 0.4rem 0 0;
    gap: 0.25rem 0.75rem;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .demo-hud[aria-live] .demo-hud-label { font-size: 0.62rem; }
  .demo-hud[aria-live] .demo-hud-value { font-size: 0.82rem; }
}

/* Closed-beta marker in the navbar brand (_navbar_brand.html), drawn only
   when settings.BETA_MODE is on. The soft pulse draws the eye so a tester
   always notices they are on the non-live environment. Reduced-motion-safe:
   the animation only runs under prefers-reduced-motion: no-preference, so it
   is a plain static badge for users who opt out. Theme-aware (uses the
   Phoenix warning token). */
.beta-badge {
  letter-spacing: 0.06em;
  font-weight: 700;
}
@keyframes beta-badge-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--phoenix-warning-rgb), 0.5); }
  50%      { box-shadow: 0 0 0 0.35rem rgba(var(--phoenix-warning-rgb), 0); }
}
@media (prefers-reduced-motion: no-preference) {
  .beta-badge {
    animation: beta-badge-pulse 2.4s ease-in-out infinite;
  }
}

/*# sourceMappingURL=user.min.css.map */
