/* Shared, surface-agnostic components (phase-8-10, end-user-error-messaging-convention.md).
   Loaded on every layout (_Layout.cshtml and _MarketingLayout.cshtml) since these classes are
   used on both the authenticated app (--lvxd-* tokens, lvxd-onboarding.css) and the Storefront
   (--lvxd-sf-* tokens, lvxd-storefront.css). Each rule below falls back from the app's token to
   the Storefront's equivalent (var(--lvxd-x, var(--lvxd-sf-x))), same fallback-chaining pattern
   already used in lvxd-workspace.css - one shared class, no duplicated partial per surface. */

/* ===========================================================================================
   Responsive breakpoint scale (phase-17-01). SINGLE SOURCE OF TRUTH for the whole app.

   CSS @media cannot read var(), so these live as documented literals - copy the px values
   below into each stylesheet's media queries rather than inventing new widths. The codebase is
   desktop-first (max-width queries); keep that idiom, do NOT introduce a min-width-first one.

     phone     <= 639.98px     phones             off-canvas drawer, single column, stacked dialogs
     tablet    640 - 1023.98   large phone/tablet  icon rail (the shell's rail band)
     laptop    1024 - 1279.98  small laptop        full sidebar, tighter gutters
     desktop   >= 1280px       desktop             full layout
   =========================================================================================== */

/* Set on <body> while a full-screen overlay (the phone nav drawer) is open, so the page behind
   it cannot scroll. Removed on close. */
.lvxd-scroll-locked {
  overflow: hidden;
}

/* Reusable horizontal-scroll wrapper for wide content (tables, matrices) on narrow viewports:
   the element scrolls sideways inside its own box instead of widening the page. Wrap a wide table
   in this rather than re-declaring overflow-x on each one. */
.lvxd-table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ===========================================================================================
   LivexHub brand palette + expressive layer.

   Lives HERE rather than in lvxd-onboarding.css because this file is the only stylesheet both
   _Layout and _MarketingLayout load. Anything brand-wide has to be reachable from the Storefront
   too, and it wasn't: the review-form confetti could not see the mark stops, so it composed its
   burst out of semantic --lvxd-sf-* tokens and fired danger red to celebrate a submitted review.

   TWO SETS, and the wall between them is the whole point:

     SEMANTIC (in lvxd-onboarding.css / lvxd-storefront.css) - accent, success, warning, danger.
     These carry state. Nothing decorative may borrow them, and red in particular appears ONLY
     for destructive/error. If you are reaching for --lvxd-danger to make something look lively,
     stop - that is the bug this block exists to prevent.

     EXPRESSIVE (below) - identity and delight. Never load-bearing: no expressive colour may be
     the only signal for anything, because a user who cannot distinguish two of them must lose
     nothing but flair.

   HUB IS THE SPECTRUM; A PRODUCT IS ONE HUE. (Decided 2026-07-31, dev-update-log #30.)

   Hub's primary is the mark's cyan - and LivexFlow's brand accent (#00AEE0, and #038FC4 as
   its stated primary) is the SAME HUE, within ~3 degrees. They meet on Workspace/Discover,
   where the LivexFlow recommendation hero paints in Flow's own accent inside Hub chrome, so
   "which brand is this" cannot be carried by hue there.

   Resolved by TREATMENT rather than by re-hueing either brand:

     - A Hub-owned identity surface carries --lvxd-logo-hub-gradient, the full 8-stop
       spectrum. Hub reads as "all of them" - the hub of a suite.
     - A product-owned surface carries that product's single flat accent (Flow's cyan, Desk's
       blue, ...), fed from LookupCatalog's AccentColor.
     - Plain UI chrome - buttons, links, focus rings, active nav - stays flat --lvxd-accent.
       That is state, not identity, and must not be gradient-ised.

   So the rule to apply when adding Hub identity: reach for the spectrum, never flat cyan.
   Flattening a Hub identity surface to --lvxd-accent is what re-creates the collision.

   Reserved elsewhere in the suite, do not introduce: #038FC4 is LivexFlow's primary.
   =========================================================================================== */

:root {
  /* Mark palette. Every sibling mark in wwwroot/logos/ drives its fill through
     --lvxd-logo-<product>-* custom properties; the Hub mark could not, because it ships its
     rainbow as an embedded raster (one <image>, no paths) rather than as themeable geometry.
     These stops are that raster's own sampled colours - decoded and pixel-sampled from the
     supplied artwork, read clockwise from 12 o'clock, per dev-update-log #26.

     NOT redeclared under .dark: this is brand artwork, not a UI surface. The mark itself paints
     identically in both themes (it is a raster), so a theme-shifted token set would put the
     chrome out of step with the mark it is supposed to echo.

     If design re-issues the artwork (the pending vector re-export), re-sample and update HERE -
     these eight are the single source, and livex-hub-icon.svg is the reference. */
  --lvxd-logo-hub-stop-1: #ff8411;
  --lvxd-logo-hub-stop-2: #fc5aa0;
  --lvxd-logo-hub-stop-3: #9294e1;
  --lvxd-logo-hub-stop-4: #24cbfe;
  --lvxd-logo-hub-stop-5: #00e9e2;
  --lvxd-logo-hub-stop-6: #09d138;
  --lvxd-logo-hub-stop-7: #4ebc24;
  --lvxd-logo-hub-stop-8: #a4a509;

  /* Semantic aliases onto four of the eight, for chrome that needs one Hub hue rather than the
     whole spectrum. Named by hue so a re-sample can remap them without renaming call sites. */
  --lvxd-logo-hub-green: var(--lvxd-logo-hub-stop-6);
  --lvxd-logo-hub-orange: var(--lvxd-logo-hub-stop-1);
  --lvxd-logo-hub-magenta: var(--lvxd-logo-hub-stop-2);
  --lvxd-logo-hub-cyan: var(--lvxd-logo-hub-stop-4);

  /* The mark's full spectrum as a linear sweep, in the green -> orange -> magenta -> cyan reading
     order. Used for Hub-owned chrome accents; the loader uses the conic form of the same stops. */
  --lvxd-logo-hub-gradient: linear-gradient(90deg,
      var(--lvxd-logo-hub-green) 0%,
      var(--lvxd-logo-hub-orange) 33%,
      var(--lvxd-logo-hub-magenta) 66%,
      var(--lvxd-logo-hub-cyan) 100%);

  /* ---- Expressive roles ----------------------------------------------------------------
     One job each, so a colour always means the same thing across the product. Aliases, never
     literals: a re-sampled mark re-colours the whole gamification layer for free.

     Deliberately absent: the mark's orange (stop-1). It sits ~30 degrees from warning amber
     (#daaa3f), so a streak flame in orange beside a warning chip is a genuine confusion risk.
     Orange stays available to --lvxd-celebrate-* below, where being one of eight makes it
     obviously decorative. Magenta carries streaks instead.

     All five are FILL colours. On white, magenta is 2.97:1, periwinkle 3.2:1 and turquoise
     1.68:1 - all below AA. Safe as backgrounds under dark ink (--lvxd-<role>-ink below), or as
     pure decoration. Any that must carry TEXT needs a darkened light-mode variant first, the
     same way the primary needed #0b7ea3 rather than the raw #24cbfe. */
  --lvxd-xp: var(--lvxd-logo-hub-stop-2);
  --lvxd-rank: var(--lvxd-logo-hub-stop-3);
  --lvxd-discovery: var(--lvxd-logo-hub-stop-5);
  --lvxd-reward: var(--lvxd-logo-hub-stop-8);

  /* Dark inks that clear AA on the fills above, for the label sitting on a badge/pill. */
  --lvxd-xp-ink: #2a0716;
  --lvxd-rank-ink: #0f1033;
  --lvxd-discovery-ink: #00201f;
  --lvxd-reward-ink: #1a1a00;

  /* ---- Celebration burst ---------------------------------------------------------------
     All eight stops. Red is absent BY CONSTRUCTION, not by luck - there is no red in the mark,
     so a burst composed from these can never congratulate someone in the error colour.

     Numbered individually rather than shipped as one comma-separated list because the confetti
     modules assign them one piece at a time as "var(--lvxd-celebrate-N)" strings - a single list
     token would need splitting in JS, which is what invites a hardcoded copy to creep back in. */
  --lvxd-celebrate-1: var(--lvxd-logo-hub-stop-1);
  --lvxd-celebrate-2: var(--lvxd-logo-hub-stop-2);
  --lvxd-celebrate-3: var(--lvxd-logo-hub-stop-3);
  --lvxd-celebrate-4: var(--lvxd-logo-hub-stop-4);
  --lvxd-celebrate-5: var(--lvxd-logo-hub-stop-5);
  --lvxd-celebrate-6: var(--lvxd-logo-hub-stop-6);
  --lvxd-celebrate-7: var(--lvxd-logo-hub-stop-7);
  --lvxd-celebrate-8: var(--lvxd-logo-hub-stop-8);

  /* ---- Achievement tiers ---------------------------------------------------------------
     A hue journey along the mark rather than arbitrary picks. IMPORTANT: this ramp is NOT
     monotonic in lightness (turquoise at IV is lighter than magenta at VI), so tier colour does
     not survive greyscale or colour-blindness. Every tier UI must carry a second, non-colour cue
     - numeral, icon, or fill count. Colour alone may never encode rank. */
  --lvxd-tier-1: var(--lvxd-logo-hub-stop-8);
  --lvxd-tier-2: var(--lvxd-logo-hub-stop-7);
  --lvxd-tier-3: var(--lvxd-logo-hub-stop-6);
  --lvxd-tier-4: var(--lvxd-logo-hub-stop-5);
  --lvxd-tier-5: var(--lvxd-logo-hub-stop-4);
  --lvxd-tier-6: var(--lvxd-logo-hub-stop-2);
}

.lvxd-form-feedback {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0 0 16px;
  padding: 13px 16px;
  border-radius: 12px;
  border: 1.5px solid;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.4;
}

.lvxd-form-feedback-error {
  border-color: var(--lvxd-danger, var(--lvxd-sf-danger));
  background: var(--lvxd-danger-soft, var(--lvxd-sf-danger-soft));
  color: var(--lvxd-danger-text, var(--lvxd-sf-danger));
}

.lvxd-form-feedback-warning {
  border-color: var(--lvxd-accent, var(--lvxd-sf-accent));
  background: var(--lvxd-accent-soft, var(--lvxd-sf-accent-soft));
  color: var(--lvxd-accent, var(--lvxd-sf-accent));
}

.lvxd-form-feedback-success {
  border-color: var(--lvxd-success-border, var(--lvxd-sf-success));
  background: var(--lvxd-success-soft, var(--lvxd-sf-success-soft));
  color: var(--lvxd-success-text, var(--lvxd-sf-success));
}

.lvxd-form-feedback-info {
  border-color: var(--lvxd-accent, var(--lvxd-sf-accent));
  background: var(--lvxd-accent-soft, var(--lvxd-sf-accent-soft));
  color: var(--lvxd-accent, var(--lvxd-sf-accent));
}

/* Uses -text, not the -danger base: this is 12.5px body copy, so it needs the darker end of
   the ramp to clear AA (6.47:1 light / 5.43:1 dark). The base stays for borders and fills. */
.lvxd-field-error {
  display: block;
  margin-top: 4px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--lvxd-danger-text, var(--lvxd-sf-danger));
}

/* Toast (class 3, end-user-error-messaging-convention.md) - rendered entirely through Metronic's
   own KTToast/.lvx-alert-* component as of 2026-08-22 (see lvxd-toast.js). No bespoke CSS lives
   here any more; the previous hand-rolled .lvxd-toast-* rules (own stack container, own card,
   own SVG checkmark-draw animation) are deleted, not just unused - CLAUDE.md's "never hand-roll
   a component the theme already ships" applied retroactively. */

/* Unexpected-error page (class 6). Deliberately neutral/desaturated, not danger-red - a 500
   shouldn't visually read as "you broke something" the way a validation error does. */
.lvxd-error-page {
  max-width: 560px;
  margin: 96px auto;
  padding: 0 24px;
  text-align: center;
}

.lvxd-error-page-icon {
  font-size: 40px;
  color: var(--lvxd-text-faint, var(--lvxd-sf-text-faint));
  margin-bottom: 16px;
}

.lvxd-error-page h1 {
  font-size: 22px;
  font-weight: 800;
  color: var(--lvxd-text-heading, var(--lvxd-sf-text));
  margin: 0 0 8px;
}

.lvxd-error-page p {
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
  font-size: 14.5px;
  line-height: 1.6;
  margin: 0 0 20px;
}

.lvxd-error-page-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  margin-bottom: 20px;
}

.lvxd-error-page-requestid {
  font-size: 12px;
  color: var(--lvxd-text-faint, var(--lvxd-sf-text-faint));
}

.lvxd-error-page-requestid code {
  background: var(--lvxd-surface-muted, var(--lvxd-sf-surface-tint));
  padding: 2px 6px;
  border-radius: 6px;
}

/* Livex pulse loader - ambient, indefinite-wait states (first load, background sync).
   Draws in, X's arms build top-left -> (bottom-left + top-right together) -> bottom-right,
   holds, fades, restarts. Pure CSS - no JS needed. */
.lvxd-pulse-loader {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.lvxd-pulse-loader svg {
  width: 100%;
  max-width: 320px;
  height: auto;
}

.lvxd-pulse-loader-caption {
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
  font-size: 13px;
  letter-spacing: .01em;
}

.lvxd-pulse-loop {
  animation: lvxd-pulse-loop-opacity 4.6s ease-in-out infinite;
}
@keyframes lvxd-pulse-loop-opacity {
  0% { opacity: 0; }
  6% { opacity: 1; }
  90% { opacity: 1; }
  98% { opacity: 0; }
  100% { opacity: 0; }
}

.lvxd-pulse-main {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: lvxd-pulse-draw-main 4.6s ease-in-out infinite;
}
@keyframes lvxd-pulse-draw-main {
  0% { stroke-dashoffset: 100; }
  50% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

.lvxd-pulse-arm-ne {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: lvxd-pulse-draw-ne 4.6s ease-in-out infinite;
}
@keyframes lvxd-pulse-draw-ne {
  0%, 49% { stroke-dashoffset: 100; }
  60% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

.lvxd-pulse-arm-sw {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: lvxd-pulse-draw-sw 4.6s ease-in-out infinite;
}
@keyframes lvxd-pulse-draw-sw {
  0%, 49% { stroke-dashoffset: 100; }
  60% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

.lvxd-pulse-arm-se {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: lvxd-pulse-draw-se 4.6s ease-in-out infinite;
}
@keyframes lvxd-pulse-draw-se {
  0%, 56% { stroke-dashoffset: 100; }
  70% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}

.lvxd-pulse-dot {
  transform-origin: 85px 5px;
  animation: lvxd-pulse-dot-bounce 4.6s ease-out infinite;
}
@keyframes lvxd-pulse-dot-bounce {
  0%, 10% { opacity: 0; transform: translateY(-14px) scale(0.7); }
  16% { opacity: 1; transform: translateY(2px) scale(1.05); }
  20% { transform: translateY(-1px) scale(0.97); }
  24% { transform: translateY(0) scale(1); }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

.lvxd-pulse-x-ring {
  transform-origin: 238px 65px;
  animation: lvxd-pulse-x-ring 4.6s ease-out infinite;
}
@keyframes lvxd-pulse-x-ring {
  0%, 70% { transform: scale(0.5); opacity: 0; }
  76% { transform: scale(1); opacity: .5; }
  90% { transform: scale(1.8); opacity: 0; }
  100% { transform: scale(1.8); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lvxd-pulse-loop { opacity: 1; animation: none; }
  .lvxd-pulse-main,
  .lvxd-pulse-arm-ne,
  .lvxd-pulse-arm-sw,
  .lvxd-pulse-arm-se {
    stroke-dashoffset: 0;
    animation: none;
  }
  .lvxd-pulse-dot {
    transform: translateY(0) scale(1);
    opacity: 1;
    animation: none;
  }
  .lvxd-pulse-x-ring {
    display: none;
  }
}

/* Full-screen page-load overlay - shows the pulse loader (server-rendered, so it's already
   visible before any JS runs) and is dismissed by lvxd-page-load-overlay.js once the page has
   actually finished loading. */
.lvxd-page-load-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--lvxd-bg-canvas, #ffffff);
  opacity: 1;
  visibility: visible;
  transition: opacity .35s ease, visibility .35s ease;
}

.lvxd-page-load-overlay.is-hidden {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .lvxd-page-load-overlay {
    transition: none;
  }
}

/* Determinate pulse loader - tied to a real percentage (uploads, imports, onboarding steps).
   Draws once, holds at the end, never restarts. stroke-dasharray is set here rather than
   inline since it's constant (100, matching each path's pathLength) - only stroke-dashoffset
   varies per render/update. Server-rendered at the initial percent; lvxd-pulse-loader-
   progress.js updates the same elements smoothly as real progress comes in. */
.lvxd-pulse-progress-main,
.lvxd-pulse-progress-arm {
  stroke-dasharray: 100;
  transition: stroke-dashoffset .4s ease;
}

.lvxd-pulse-loader[data-lvxd-pulse-progress] .lvxd-pulse-dot {
  transition: opacity .3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .lvxd-pulse-progress-main,
  .lvxd-pulse-progress-arm {
    transition: none;
  }
}

/* Hub loader - logged-in only (authenticated app's own page-load overlay), built from the
   LivexHub product mark rather than the parent Livex brand mark used for logged-out visitors.
   The mark is a single silhouette (a layered cube seen corner-on), so per-element animation
   isn't available the way the old 3-node mark allowed. Instead the mark is used as a MASK over
   a conic rainbow that orbits behind it: every Livex product owns one accent hue, and the new
   mark carries the whole spectrum, so the loading motion is "the suite cycling through the
   hub" while the silhouette breathes gently - still not a generic spinner.

   The gradient stops below now come from the --lvxd-logo-hub-stop-N tokens in lvxd-onboarding.css
   (values unchanged - they were literals here until then). That file is the single place to
   re-sample if logos/livex-hub-icon.svg is re-issued; the same tokens also paint the Hub-owned
   chrome accents in lvxd-hub-shell.css, so the loader and the shell can no longer drift apart. */
.lvxd-hub-loader {
  display: flex;
  align-items: center;
  justify-content: center;
}

.lvxd-hub-loader-mark {
  position: relative;
  display: block;
  /* 83.56 x 96.36 - the mark is taller than it is wide, unlike the old one */
  width: 104px;
  height: 120px;
  overflow: hidden;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 83.56 96.36'%3E%3Cpath fill='%23fff' d='M42.79.53l19.9,11.49,19.82,11.49,1.05.53v48.23l-1.05.61h-.09c-.09.09-.18.09-.26.18l-19.47,11.22-19.9,11.49-1.05.61-.96-.61-19.9-11.49L1.05,72.86l-1.05-.61V24.03l1.05-.53,19.82-11.49L40.77.53l.96-.53,1.05.53ZM70.67,62.61l8.68,5.09V28.85l-9.91,5.79h-.09l-26.57,15.34-.96.61-1.05-.61-23.67-13.68h0l-4.21-2.37v-2.46l1.05-.61,13.42-7.72,13.42-7.72.96-.61,1.05.61,13.42,7.72,12.36,7.19h0l8.68-5.09-16.66-9.65-18.85-10.87-18.76,10.87L4.21,26.48v43.4l18.76,10.79,18.76,10.87,18.85-10.87,16.48-9.56-8.68-5-12.19,7.01-13.42,7.72-1.05.61-.96-.61-13.42-7.72-13.42-7.72-1.05-.61v-20.78l4.21,2.37,6.93,4.03,17.8,10.26,28.85-16.75v18.68ZM19.2,32.71l22.62,13.06,22.53-13.06-10.26-5.96-12.36-7.1-12.28,7.1-10.26,5.96ZM66.46,51.21l-23.67,13.59-.96.61-1.05-.61-23.67-13.59v11.22l12.36,7.1,12.28,7.1,12.36-7.1,12.36-7.1v-11.22Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 83.56 96.36'%3E%3Cpath fill='%23fff' d='M42.79.53l19.9,11.49,19.82,11.49,1.05.53v48.23l-1.05.61h-.09c-.09.09-.18.09-.26.18l-19.47,11.22-19.9,11.49-1.05.61-.96-.61-19.9-11.49L1.05,72.86l-1.05-.61V24.03l1.05-.53,19.82-11.49L40.77.53l.96-.53,1.05.53ZM70.67,62.61l8.68,5.09V28.85l-9.91,5.79h-.09l-26.57,15.34-.96.61-1.05-.61-23.67-13.68h0l-4.21-2.37v-2.46l1.05-.61,13.42-7.72,13.42-7.72.96-.61,1.05.61,13.42,7.72,12.36,7.19h0l8.68-5.09-16.66-9.65-18.85-10.87-18.76,10.87L4.21,26.48v43.4l18.76,10.79,18.76,10.87,18.85-10.87,16.48-9.56-8.68-5-12.19,7.01-13.42,7.72-1.05.61-.96-.61-13.42-7.72-13.42-7.72-1.05-.61v-20.78l4.21,2.37,6.93,4.03,17.8,10.26,28.85-16.75v18.68ZM19.2,32.71l22.62,13.06,22.53-13.06-10.26-5.96-12.36-7.1-12.28,7.1-10.26,5.96ZM66.46,51.21l-23.67,13.59-.96.61-1.05-.61-23.67-13.59v11.22l12.36,7.1,12.28,7.1,12.36-7.1,12.36-7.1v-11.22Z'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  animation: lvxd-hub-mark-breathe 2.4s ease-in-out infinite;
}

/* Oversized and centred so the rotation can never swing a corner into view. */
.lvxd-hub-loader-mark::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 200%;
  height: 200%;
  margin: -100% 0 0 -100%;
  background: conic-gradient(from 0deg,
      var(--lvxd-logo-hub-stop-1) 0deg,
      var(--lvxd-logo-hub-stop-2) 45deg,
      var(--lvxd-logo-hub-stop-3) 90deg,
      var(--lvxd-logo-hub-stop-4) 135deg,
      var(--lvxd-logo-hub-stop-5) 180deg,
      var(--lvxd-logo-hub-stop-6) 225deg,
      var(--lvxd-logo-hub-stop-7) 270deg,
      var(--lvxd-logo-hub-stop-8) 315deg,
      var(--lvxd-logo-hub-stop-1) 360deg);
  animation: lvxd-hub-hue-orbit 3.6s linear infinite;
}

@keyframes lvxd-hub-mark-breathe {
  0%, 100% { transform: scale(0.94); opacity: .85; }
  50% { transform: scale(1); opacity: 1; }
}

@keyframes lvxd-hub-hue-orbit {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .lvxd-hub-loader-mark {
    animation: none;
    transform: scale(1);
    opacity: 1;
  }
  .lvxd-hub-loader-mark::before {
    animation: none;
  }
}

/* Wrapper used by the standalone Account pages that host a blocked/gated state (NoAccess,
   SetPasswordPending). Previously used with no rule behind it at all, so those pages rendered
   full-bleed. */
.lvxd-page-narrow {
  max-width: 560px;
  margin: 48px auto;
  padding: 0 20px;
}

.lvxd-page-narrow-footnote {
  margin-top: 16px;
  text-align: center;
  font-size: 13px;
  line-height: 1.55;
  color: var(--lvxd-text-muted, var(--lvxd-text-body));
}

/* Class 4 - blocked/gated state (end-user-error-messaging-convention.md). Now wired: it backs
   Account/NoAccess and Account/SetPasswordPending. Info/blue per that proposal's severity table
   ("class 4 (informational gate, e.g. 'contact an admin')"), same fallback-chain pattern as
   .lvxd-form-feedback-info above. */

.lvxd-blocked-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 48px 24px;
  border-radius: 14px;
  border: 1.5px solid var(--lvxd-accent, var(--lvxd-sf-accent));
  background: var(--lvxd-accent-soft, var(--lvxd-sf-accent-soft));
  text-align: center;
}

.lvxd-blocked-state-icon i {
  font-size: 34px;
  color: var(--lvxd-accent, var(--lvxd-sf-accent));
}

.lvxd-blocked-state-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--lvxd-text-heading, var(--lvxd-sf-text));
}

.lvxd-blocked-state-message {
  font-size: 13.5px;
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
  max-width: 360px;
}

.lvxd-blocked-state-cta {
  display: inline-flex;
  align-items: center;
  margin-top: 6px;
  padding: 9px 18px;
  border-radius: 10px;
  background: var(--lvxd-accent, var(--lvxd-sf-accent));
  /* Fallback pairs with this rule's own background fallback (--lvxd-sf-accent): on storefront pages
     _MarketingLayout never loads the token file, and an unresolved `color` would silently inherit. */
  color: var(--lvxd-on-accent, #fff);
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
}

/* Secondary escape hatch under a blocked state's primary CTA. Deliberately a quiet link rather
   than a second button: on NoAccess the primary action is still the one most refused users want
   (create a workspace), and signing out of LivexID is the recovery path for the smaller case of
   having arrived on the wrong account. Styled as a button element since it submits a POST form. */
/* Sits outside .lvxd-blocked-state (the partial owns that box), so it centres itself rather than
   inheriting the box's flex alignment. */
.lvxd-blocked-state-signout {
  text-align: center;
}

.lvxd-blocked-state-alt {
  margin-top: 12px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
  font-family: inherit;
  font-size: 12.5px;
  text-decoration: underline;
  cursor: pointer;
}

.lvxd-blocked-state-alt:hover {
  color: var(--lvxd-text-heading, var(--lvxd-sf-text));
}

/* ==========================================================================================
   Tooltip - one styled tooltip for the whole product, driven by lvxd-tooltip.js.

   Every hint in the app used to be a bare `title` attribute, so the browser drew its own OS
   tooltip: black box, system font, ~500ms delay, no theme awareness, positioned wherever the
   pointer happened to be. lvxd-tooltip.js moves each `title` onto data-lvxd-tip and renders it
   through the rules below instead, so hints match the app's own menus/dropdowns (surface +
   border + shadow) rather than the operating system.

   A single shared node is appended to <body> and repositioned per target - not one tooltip
   element per trigger - which is why the geometry lives in custom properties set from JS.
   Sits above the notifications drawer (2001) and below the page-load overlay (9999).
   ========================================================================================== */
.lvxd-tooltip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 2100;
  max-width: 264px;
  padding: 6px 9px;
  border-radius: 8px;
  border: 1px solid var(--lvxd-border, var(--lvxd-sf-border));
  background: var(--lvxd-surface, var(--lvxd-sf-surface));
  color: var(--lvxd-text-body, var(--lvxd-sf-text));
  box-shadow: 0 8px 24px rgba(9, 9, 11, .10), 0 2px 6px rgba(9, 9, 11, .06);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  /* Long placeholder hints ("Card updates aren't wired up yet - no real payment gateway (v1)")
     wrap inside max-width; a single long token still must not push the box off-screen. */
  overflow-wrap: anywhere;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(2px);
  transition: opacity .13s ease, transform .13s ease, visibility .13s;
}

.lvxd-tooltip.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* Rotated square rather than a border triangle: a triangle can't carry the 1px border AND the
   surface fill that make the tooltip read as the same material as a dropdown. Two of its four
   borders are dropped so only the outward-facing corner is stroked. */
.lvxd-tooltip::after {
  content: "";
  position: absolute;
  width: 8px;
  height: 8px;
  background: inherit;
  border: 1px solid var(--lvxd-border, var(--lvxd-sf-border));
  transform: rotate(45deg);
}

.lvxd-tooltip[data-placement="top"]::after {
  bottom: -5px;
  left: var(--lvxd-tooltip-arrow-x, 50%);
  margin-left: -4px;
  border-top: 0;
  border-left: 0;
}

.lvxd-tooltip[data-placement="bottom"]::after {
  top: -5px;
  left: var(--lvxd-tooltip-arrow-x, 50%);
  margin-left: -4px;
  border-bottom: 0;
  border-right: 0;
}

.lvxd-tooltip[data-placement="left"]::after {
  right: -5px;
  top: var(--lvxd-tooltip-arrow-y, 50%);
  margin-top: -4px;
  border-bottom: 0;
  border-left: 0;
}

.lvxd-tooltip[data-placement="right"]::after {
  left: -5px;
  top: var(--lvxd-tooltip-arrow-y, 50%);
  margin-top: -4px;
  border-top: 0;
  border-right: 0;
}

.dark .lvxd-tooltip {
  box-shadow: 0 10px 28px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .30);
}

@media (prefers-reduced-motion: reduce) {
  .lvxd-tooltip {
    transition: none;
    transform: none;
  }
}

/* Over-plan-user-limit banner (_UserLimitBanner.cshtml). Class 4 - blocked/gated state per
   end-user-error-messaging-convention.md - but rendered as an inline row rather than reusing
   .lvxd-blocked-state above, because that box is a full-page terminal state (centred, 48px
   padding, nothing else on the screen) and this one sits ABOVE a working screen the tenant can
   still use. Same severity tokens as .lvxd-form-feedback-warning, with the same
   --lvxd-* -> --lvxd-sf-* fallback chain so it survives on a storefront layout that never loads
   the workspace token file. */

.lvxd-user-limit-banner {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  margin-bottom: 18px;
  padding: 16px 18px;
  border-radius: 12px;
  border: 1.5px solid var(--lvxd-accent, var(--lvxd-sf-accent));
  background: var(--lvxd-accent-soft, var(--lvxd-sf-accent-soft));
}

/* Blocked reads as danger, not warning: at this point an invite is actually being refused, and
   the two states must not look identical when only one of them stops the tenant. */
.lvxd-user-limit-banner-blocked {
  border-color: var(--lvxd-danger-text, var(--lvxd-sf-danger));
  background: var(--lvxd-danger-soft, var(--lvxd-sf-danger-soft));
}

.lvxd-user-limit-banner-icon i {
  font-size: 22px;
  color: var(--lvxd-accent, var(--lvxd-sf-accent));
}

.lvxd-user-limit-banner-blocked .lvxd-user-limit-banner-icon i {
  color: var(--lvxd-danger-text, var(--lvxd-sf-danger));
}

.lvxd-user-limit-banner-body {
  flex: 1 1 auto;
  min-width: 0;
}

.lvxd-user-limit-banner-title {
  margin: 0 0 3px;
  font-size: 14px;
  font-weight: 700;
  color: var(--lvxd-text-heading, var(--lvxd-sf-text));
}

.lvxd-user-limit-banner-message {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
}

.lvxd-user-limit-banner-note {
  margin: 6px 0 0;
  font-size: 12.5px;
  font-style: italic;
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
}

.lvxd-user-limit-banner-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  flex: 0 0 auto;
}

.lvxd-user-limit-banner-form {
  margin: 0;
}

.lvxd-user-limit-banner-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 9px 16px;
  border: 0;
  border-radius: 10px;
  background: var(--lvxd-accent, var(--lvxd-sf-accent));
  color: var(--lvxd-on-accent, #fff);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.lvxd-user-limit-banner-secondary {
  font-size: 12.5px;
  text-align: center;
  color: var(--lvxd-text-muted, var(--lvxd-sf-text-muted));
  text-decoration: underline;
}

.lvxd-user-limit-banner-secondary:hover {
  color: var(--lvxd-text-heading, var(--lvxd-sf-text));
}

/* The actions column drops beneath the message on narrow viewports rather than squeezing the
   sentence into a two-word-per-line ribbon. */
@media (max-width: 640px) {
  .lvxd-user-limit-banner {
    flex-direction: column;
  }

  .lvxd-user-limit-banner-actions {
    width: 100%;
  }
}

/* ===========================================================================================
   QA environment warning banner (added 2026-09-16).

   A red strip pinned to the top of every page, shown ONLY in the QA environment so the QA site is
   never mistaken for production. The markup is _EnvironmentBanner.cshtml, rendered by _Layout and
   _MarketingLayout; it emits nothing outside QA. In QA the <html> tag carries .lvxd-env-qa (set
   server-side in both layouts, the same idiom as the theme classes), which reserves space for the
   bar and nudges the sticky/fixed app chrome down below it.

   The banner is a FIXED height on purpose: the offsets below are calculated from
   --lvxd-env-banner-h, so keep the message to a single line. Nothing here uses !important - each
   offset out-specifies the original (html.lvxd-env-qa <selector> beats a bare class), so it wins
   regardless of stylesheet load order.
   =========================================================================================== */
:root {
  --lvxd-env-banner-h: 32px;
}

.lvxd-env-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--lvxd-env-banner-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 0 12px;
  /* Follows the Hub theme: the Hub's own danger red (#dc2626 light, #ef4444 dim/dark). The
     hardcoded fallback is for the Storefront, which does not load lvxd-onboarding.css where the
     token is defined - it stays a plain red there. */
  background: var(--lvxd-danger, #dc2626);
  color: #ffffff;
  font-family: Inter, system-ui, sans-serif;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .01em;
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  user-select: none;
  box-shadow: 0 1px 0 rgba(0, 0, 0, .12);
}

.lvxd-env-banner-tag {
  flex-shrink: 0;
  padding: 2px 7px;
  border-radius: 4px;
  background: rgba(255, 255, 255, .22);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.lvxd-env-banner-text {
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 640px) {
  .lvxd-env-banner {
    gap: 6px;
  }
}

/* -------------------------------------------------------------------------------------------
   QA-only layout offset. Scoped to html.lvxd-env-qa, so production (no class) is never touched.
   ------------------------------------------------------------------------------------------- */

/* Simple / centred pages (Storefront, Login, onboarding) just start below the bar. */
html.lvxd-env-qa body {
  padding-top: var(--lvxd-env-banner-h);
}

/* App shell (lvxd-hub-shell.css): topbar + sidebar are sticky at top:0 and the collapse button is
   fixed at top:60px. Shift each anchor down by the bar height, and trim the sidebar / shell height
   so the page gains no extra scroll. */
html.lvxd-env-qa .lvxd-hub-shell {
  min-height: calc(100dvh - var(--lvxd-env-banner-h));
}

html.lvxd-env-qa .lvxd-hub-topbar {
  top: var(--lvxd-env-banner-h);
}

html.lvxd-env-qa .lvxd-hub-sidebar {
  top: var(--lvxd-env-banner-h);
  height: calc(100dvh - var(--lvxd-env-banner-h));
}

html.lvxd-env-qa .lvxd-hub-sidebar-toggle-btn {
  top: calc(60px + var(--lvxd-env-banner-h));
}

/* Storefront (lvxd-storefront.css): the nav and the two other sticky heads also anchor to the
   viewport top, so they get the same nudge. */
html.lvxd-env-qa .lvxd-sf-nav,
html.lvxd-env-qa .lvxd-sf-cmp-head {
  top: var(--lvxd-env-banner-h);
}

html.lvxd-env-qa .lvxd-sf-stack-summary {
  top: calc(84px + var(--lvxd-env-banner-h));
}
