/* Softspace Forms - motion layer.
   Loaded after app.css / module CSS so it can refine timing without owning any component.

   Rules this file keeps to:
   - Every animation and every transition lives inside
     @media (prefers-reduced-motion: no-preference), and a reduce block at the foot
     nulls anything that slips through (including motion added by other stylesheets).
   - Repeating / per-frame motion animates transform and opacity only.
   - Nothing here changes an element box, so entrance motion contributes 0 to CLS.
   - Timing always comes from --ss-dur / --ss-dur-slow / --ss-ease / --ss-ease-out,
     so the whole app shares one rhythm.
   - html.ss-motion is stamped by motion.js before first paint, but only once the
     document is visible. Without JS - or in a hidden tab - the page is simply
     static: no element is ever left hidden by this file.
*/

/* ===========================================================================
   0. Timing derived from --ss-dur, so the base token still owns the rhythm
   =========================================================================== */
:root {
  --ss-dur-tap:    calc(var(--ss-dur) * 0.375);  /*  60ms - button press      */
  --ss-stagger:    calc(var(--ss-dur) * 0.28);   /*  45ms - per-item ramp step */
  --ss-dur-pop:    calc(var(--ss-dur) * 2);      /* 320ms - toast/badge pop   */
  --ss-dur-shake:  calc(var(--ss-dur) * 3);      /* 480ms - invalid shake     */
  --ss-dur-sheen:  calc(var(--ss-dur) * 3.875);  /* 620ms - primary sheen     */
  --ss-dur-pulse:  calc(var(--ss-dur) * 4.375);  /* 700ms - badge pulse       */
  --ss-dur-nudge:  calc(var(--ss-dur) * 5);      /* 800ms - bell nudge        */
}

/* ===========================================================================
   1. Non-motion scaffolding (must apply in reduce mode too)
   =========================================================================== */

/* Custom checkbox box, so the tick has something to draw into. Radios keep
   accent-color. Same 16px box as app.css, so no layout change. */
.check input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  position: relative; flex: none;
  width: 16px; height: 16px; margin: 0;
  border: 1.5px solid var(--ss-line-strong); border-radius: 4px;
  background: var(--ss-surface); cursor: pointer;
}
.check input[type="checkbox"]:checked { background: var(--ss-primary); border-color: var(--ss-primary); }
.check input[type="checkbox"]:disabled { opacity: .55; cursor: not-allowed; }
.check input[type="checkbox"]::after {
  content: ""; position: absolute; left: 4.5px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--ss-primary-ink); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  /* The tick is revealed from its own bottom (the short leg) upward, which in the
     rotated frame reads as the stroke being drawn. */
  clip-path: inset(0 0 100% 0);
}
.check input[type="checkbox"]:checked::after { clip-path: inset(0 0 0 0); }

/* Mixed state. appearance:none threw the native dash away, so it is redrawn here
   in the scaffolding section - it must survive reduced motion, it is state, not
   decoration. Listed after :checked so a control that is both reads as mixed,
   which is what the native control does. */
.check input[type="checkbox"]:indeterminate { background: var(--ss-primary); border-color: var(--ss-primary); }
.check input[type="checkbox"]:indeterminate::after {
  left: 2.5px; top: 5.5px;
  width: 8px; height: 0;
  border-width: 0 0 2px 0;
  transform: none;
  clip-path: inset(0 0 0 0);
}

/* Skip link: left/-9999px stays exactly as app.css defines it. Motion is added only
   as an animation on the focused state, so if this file never loads - or the
   animation is refused - the link still appears at left:8px on focus (WCAG 2.4.1).
   Nothing here can hide it. */

/* Overlay transform origins, so a menu grows out of its trigger corner even if
   the Alpine origin modifier is not present. */
.dropdown-menu, .ws-menu { transform-origin: top left; }
.dropdown-menu.dropdown-right { transform-origin: top right; }

/* Sliding active indicator for the sidebar. motion.js writes --ss-nav-y / --ss-nav-h. */
.sidebar-nav { position: relative; }
.sidebar-nav::before {
  content: ""; position: absolute; left: 2px; top: 0;
  width: 3px; height: var(--ss-nav-h, 0px); border-radius: 0 3px 3px 0;
  background: var(--ss-primary);
  transform: translate3d(0, var(--ss-nav-y, 0px), 0);
  opacity: 0; pointer-events: none;
}
.sidebar-nav.has-indicator::before { opacity: 1; }

/* Rail labels: app.css hides them with display:none, which cannot animate.
   Keep them in flow and let .sidebar-nav clip them, so only opacity/transform move
   and the icon stays centred on the rail. */
@media (min-width: 861px) {
  :root[data-nav="rail"] .sidebar-nav { overflow-x: hidden; }
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link {
    display: flex; justify-content: flex-start;
    /* rail 72 - 2x10 nav padding = 52px item; (52 - 18 icon) / 2 = 17px centres it */
    padding: 10px 10px 10px 17px;
  }
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link > span:not(.badge):not(.nav-badge) {
    display: block; flex: none; white-space: nowrap;
    opacity: 0; transform: translate3d(-6px, 0, 0); pointer-events: none;
  }
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link > .badge { opacity: 0; pointer-events: none; }

  /* .sidebar-nav has overflow-y:auto, which forces the other axis to auto, so the
     rail tooltip app.css positions at left:100% was already being clipped (and was
     adding a horizontal scrollbar). Take it out of the scroll container: fixed
     positioning escapes ancestor overflow as long as no ancestor is transformed,
     which is why the rail nav-link drops its hover nudge. motion.js writes
     --ss-tip-y (the link's centre in viewport coordinates) on hover and focus. */
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link:hover { transform: none; }
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link::after {
    position: fixed;
    left: calc(var(--ss-sidebar-rail) + 10px);
    top: var(--ss-tip-y, -200px);
    transform: translateY(-50%) scale(.96);
  }
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link:hover::after,
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link:focus-visible::after { transform: translateY(-50%) scale(1); }
}
.nav-link > span:not(.badge):not(.nav-badge) { white-space: nowrap; }

/* Primary-button sheen surface. Inert until hovered. */
.btn-primary { position: relative; overflow: hidden; }
.btn-primary::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(105deg,
    transparent 32%,
    color-mix(in srgb, var(--ss-primary-ink) 26%, transparent) 48%,
    transparent 64%);
  transform: translate3d(-130%, 0, 0);
}

/* Toasts stack from the bottom edge; the gap between them is never clickable. */
.toast-stack { pointer-events: none; }
.toast-stack > * { pointer-events: auto; }

/* ===========================================================================
   2. Motion
   =========================================================================== */
@media (prefers-reduced-motion: no-preference) {

  /* ---- keyframes ------------------------------------------------------- */
  @keyframes ss-rise      { from { opacity: 0; transform: translate3d(0, 6px, 0); } }
  @keyframes ss-fade      { from { opacity: 0; } }
  @keyframes ss-modal-in  { from { opacity: 0; transform: scale(.97) translate3d(0, 6px, 0); } }
  @keyframes ss-toast-in  { from { opacity: 0; transform: translate3d(28px, 0, 0) scale(.98); } }
  @keyframes ss-toast-out { to   { opacity: 0; transform: translate3d(28px, 0, 0) scale(.98); } }
  @keyframes ss-row-out   { to   { opacity: 0; transform: translate3d(-12px, 0, 0) scaleY(.4); } }
  @keyframes ss-ring      { from { box-shadow: 0 0 0 0 var(--ss-surface), 0 0 0 0 var(--ss-primary); } }
  @keyframes ss-sheen     { from { opacity: 1; transform: translate3d(-130%, 0, 0); }
                            to   { opacity: 1; transform: translate3d(130%, 0, 0); } }
  @keyframes ss-pop       { 0% { opacity: 0; transform: scale(.55); } 62% { transform: scale(1.14); } 100% { opacity: 1; transform: scale(1); } }
  @keyframes ss-shake {
    10%, 90% { transform: translate3d(-1px, 0, 0); }
    20%, 80% { transform: translate3d(2px, 0, 0); }
    30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
    40%, 60% { transform: translate3d(4px, 0, 0); }
  }
  @keyframes ss-nudge {
    15% { transform: rotate(-12deg); } 30% { transform: rotate(9deg); }
    45% { transform: rotate(-7deg); }  60% { transform: rotate(5deg); }
    75% { transform: rotate(-2deg); }  100% { transform: rotate(0); }
  }
  @keyframes ss-badge-pulse {
    0%   { transform: scale(1);    box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 45%, transparent); }
    45%  { transform: scale(1.09); }
    100% { transform: scale(1);    box-shadow: 0 0 0 9px color-mix(in srgb, currentColor 0%, transparent); }
  }

  /* ---- 1. page + content entrance -------------------------------------- */
  /* Class is on <html> before first paint (whenever the tab is visible), so this
     never waits on DOMContentLoaded. */
  .ss-motion .app-content,
  .ss-motion .guest-card {
    /* forwards, not both - nothing here is delayed, so backwards fill only ever
       held the from-state before the clock started. The real guard against a
       page painting blank is motion.js, which withholds .ss-motion until the
       document is visible (Chrome freezes document.timeline at 0 in a hidden
       tab, and progress 0 is the active phase, which fill-mode cannot skip). */
    animation: ss-rise var(--ss-dur-slow) var(--ss-ease-out) forwards;
  }
  /* Staggered children. --ss-i is written by motion.js with el.style.setProperty
     (CSSOM, not an inline style attribute). The ramp is capped at 8 by the JS, and
     containers with more than 24 children are not staggered at all - a 100-row table
     animates as one block instead. */
  .ss-enter {
    animation: ss-rise var(--ss-dur-slow) var(--ss-ease-out) both;
    animation-delay: calc(var(--ss-i, 0) * var(--ss-stagger));
  }
  tr.ss-enter { animation-duration: var(--ss-dur); }

  /* ---- 2. interaction feedback ----------------------------------------- */
  /* Buttons */
  .btn {
    transition:
      background-color var(--ss-dur) var(--ss-ease),
      border-color var(--ss-dur) var(--ss-ease),
      color var(--ss-dur) var(--ss-ease),
      box-shadow var(--ss-dur) var(--ss-ease),
      transform var(--ss-dur) var(--ss-ease-out);
  }
  .btn:hover:not(:disabled):not([aria-disabled="true"]) {
    transform: translate3d(0, -1px, 0);
    box-shadow: var(--ss-shadow);
  }
  .btn:active:not(:disabled):not([aria-disabled="true"]) {
    transform: translateY(1px) scale(.99);
    box-shadow: var(--ss-shadow-sm);
    transition-duration: var(--ss-dur-tap);
  }
  .btn-ghost:hover:not(:disabled) { box-shadow: var(--ss-shadow-sm); }
  .btn-primary:hover::after { animation: ss-sheen var(--ss-dur-sheen) var(--ss-ease-out); }

  /* Focus ring grows in wherever it appears. */
  :focus-visible { animation: ss-ring var(--ss-dur-slow) var(--ss-ease-out); }

  /* Nav links: the bar slides, the row tints. */
  .nav-link {
    transition:
      background-color var(--ss-dur) var(--ss-ease),
      color var(--ss-dur) var(--ss-ease),
      transform var(--ss-dur) var(--ss-ease-out);
  }
  .nav-link:hover { transform: translate3d(2px, 0, 0); }
  .nav-link .icon { transition: color var(--ss-dur) var(--ss-ease), transform var(--ss-dur) var(--ss-ease-out); }
  .nav-link:hover .icon { transform: scale(1.08); }
  .sidebar-nav.has-indicator::before {
    transition:
      transform var(--ss-dur-slow) var(--ss-ease-out),
      height var(--ss-dur) var(--ss-ease),
      opacity var(--ss-dur) var(--ss-ease);
  }
  .nav-link > span:not(.badge):not(.nav-badge),
  .nav-link > .badge {
    transition: opacity var(--ss-dur) var(--ss-ease), transform var(--ss-dur-slow) var(--ss-ease-out);
  }

  /* Cards. Scoped to tiles and whole-card links so a content card that holds an open
     dropdown never gets a new stacking context under the menu. */
  .card.stat, .card.kpi, .an-kpi, a.card, label.card, .tpl-card,
  .h-grid > .card, .an-kpis > .card {
    transition:
      transform var(--ss-dur-slow) var(--ss-ease-out),
      box-shadow var(--ss-dur-slow) var(--ss-ease),
      border-color var(--ss-dur) var(--ss-ease);
  }
  .card.stat:hover, .card.kpi:hover, .an-kpi:hover, a.card:hover, label.card:hover, .tpl-card:hover,
  .h-grid > .card:hover, .an-kpis > .card:hover {
    transform: translate3d(0, -2px, 0);
    box-shadow: var(--ss-shadow);
    border-color: color-mix(in srgb, var(--ss-primary) 40%, var(--ss-line));
  }
  .card-grid > .card:not(:has(.dropdown)) {
    transition:
      transform var(--ss-dur-slow) var(--ss-ease-out),
      box-shadow var(--ss-dur-slow) var(--ss-ease),
      border-color var(--ss-dur) var(--ss-ease);
  }
  .card-grid > .card:not(:has(.dropdown)):hover {
    transform: translate3d(0, -2px, 0);
    box-shadow: var(--ss-shadow);
    border-color: color-mix(in srgb, var(--ss-primary) 40%, var(--ss-line));
  }

  /* Table rows */
  .table tbody tr {
    transition: transform var(--ss-dur) var(--ss-ease-out), box-shadow var(--ss-dur) var(--ss-ease);
  }
  .table tbody tr td { transition: background-color var(--ss-dur) var(--ss-ease), border-color var(--ss-dur) var(--ss-ease); }
  .table tbody tr:hover {
    transform: translate3d(0, -1px, 0);
    box-shadow: var(--ss-shadow-sm);
  }
  .table tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--ss-primary); }
  .ss-row-out { animation: ss-row-out var(--ss-dur-slow) var(--ss-ease) both; transform-origin: top; }

  /* Inputs */
  input, select, textarea, .input {
    transition: border-color var(--ss-dur) var(--ss-ease), background-color var(--ss-dur) var(--ss-ease);
  }
  input:focus, select:focus, textarea:focus, .topbar-search:focus-within {
    animation: ss-ring var(--ss-dur-slow) var(--ss-ease-out);
  }
  /* (0,2,1), so it outranks the `input:focus` ss-ring rule above. The browser
     focuses the first invalid control on submit, so without this the field that
     matters is the only one that never shook. */
  .ss-shake,
  input.ss-shake:focus,
  select.ss-shake:focus,
  textarea.ss-shake:focus {
    animation: ss-shake var(--ss-dur-shake) var(--ss-ease) 1;
  }

  /* Checkbox tick draw + toggle knob */
  .check input[type="checkbox"] {
    transition: background-color var(--ss-dur) var(--ss-ease), border-color var(--ss-dur) var(--ss-ease);
  }
  .check input[type="checkbox"]::after {
    transition: clip-path var(--ss-dur-slow) var(--ss-ease-out);
  }
  .switch span { transition: background-color var(--ss-dur) var(--ss-ease); }
  .switch span::after { transition: transform var(--ss-dur-slow) var(--ss-ease-out); }
  .switch input:active + span::after { transform: scale(1.12); }
  .switch input:checked:active + span::after { transform: translateX(16px) scale(1.12); }

  /* Status badges: page JS adds .ss-pulse when a status becomes a new value. */
  .badge { transition: background-color var(--ss-dur) var(--ss-ease), color var(--ss-dur) var(--ss-ease); }
  .badge.ss-pulse { animation: ss-badge-pulse var(--ss-dur-pulse) var(--ss-ease-out); }

  /* ---- 3. overlays ------------------------------------------------------ */
  /* motion.js puts .ss-overlay-in on the backdrop and, separately, on the dialog -
     only on the ones Alpine is not already transitioning itself. */
  .modal-backdrop.ss-overlay-in { animation: ss-fade var(--ss-dur-slow) var(--ss-ease) both; }
  .modal.ss-overlay-in { animation: ss-modal-in var(--ss-dur-slow) var(--ss-ease-out) both; }
  .modal { transition: box-shadow var(--ss-dur) var(--ss-ease); }
  .sidebar-backdrop.ss-overlay-in { animation: ss-fade var(--ss-dur) var(--ss-ease) both; }

  .dropdown-menu.ss-overlay-in, .ws-menu.ss-overlay-in {
    animation: ss-modal-in var(--ss-dur) var(--ss-ease-out) both;
  }

  .toast { animation: ss-toast-in var(--ss-dur-slow) var(--ss-ease-out) both; transition: transform var(--ss-dur-slow) var(--ss-ease-out); }
  .toast.ss-toast-out { animation: ss-toast-out var(--ss-dur) var(--ss-ease) both; }

  /* Tooltips (rail labels) already fade; give the timing the shared tokens. */
  :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link::after {
    transition: opacity var(--ss-dur) var(--ss-ease), transform var(--ss-dur) var(--ss-ease-out);
  }

  /* ---- 7. micro-interactions ------------------------------------------- */
  .ss-copied .icon { animation: ss-pop var(--ss-dur-pop) var(--ss-ease-out); }
  .btn:not(.btn-primary):not(.btn-danger).ss-copied .icon { color: var(--ss-ok); }

  .fld-handle, .wf-grip, .sortable-chosen {
    transition: transform var(--ss-dur) var(--ss-ease-out), opacity var(--ss-dur) var(--ss-ease), color var(--ss-dur) var(--ss-ease);
  }
  .fld-handle:active, .wf-grip:active { transform: scale(1.22); }
  .sortable-chosen { transform: scale(1.012); box-shadow: var(--ss-shadow); }
  .sortable-ghost { opacity: .35; }

  .bell.ss-nudge .icon { animation: ss-nudge var(--ss-dur-nudge) var(--ss-ease); transform-origin: 50% 14%; }

  /* ---- 8. focus + skip link -------------------------------------------- */
  /* The link is already on screen at this point (app.css moves it with `left`);
     this only softens its arrival. */
  .skip-link:focus { animation: ss-rise var(--ss-dur-slow) var(--ss-ease-out); }

  /* ---- 6. sidebar rail labels ------------------------------------------ */
  /* Only opacity/transform move; the collapse itself is the sidebar existing
     width transition, and .sidebar-nav does the clipping. */
  @media (min-width: 861px) {
    :root[data-nav="rail"] .sidebar:not(.is-peek) .nav-link > span:not(.badge):not(.nav-badge) {
      transition: opacity var(--ss-dur) var(--ss-ease), transform var(--ss-dur-slow) var(--ss-ease-out);
    }
  }
}

/* ===========================================================================
   3. Reduce block - the whole file (and anything before it) goes still.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
  /* Nothing above may leave an element hidden or displaced when motion is off. */
  .ss-enter, .ss-motion .app-content, .ss-motion .guest-card,
  .toast, .modal-backdrop.ss-overlay-in,
  .modal.ss-overlay-in, .dropdown-menu.ss-overlay-in, .ws-menu.ss-overlay-in {
    opacity: 1 !important;
    transform: none !important;
  }
  .btn:hover, .btn:active, .nav-link:hover, .table tbody tr:hover,
  .card.stat:hover, .card.kpi:hover, .an-kpi:hover, a.card:hover, .tpl-card:hover {
    transform: none !important;
  }
  .btn-primary::after { display: none; }
}

/* Rail selectors above are scoped to .sidebar:not(.is-peek): the hover-peek
   state post-dates this file, and without the guard these rules kept the
   labels hidden while the sidebar was peeked open. nav.css owns the collapse. */
