/* ==========================================================================
   SalesTooling — motion layer

   Loaded AFTER site.css so it can refine the base components without
   rewriting them. The whole language is built on three ideas:

     1. Short. Nothing here runs longer than 380ms; most is 150-240ms.
     2. Soft. One signature ease (a gentle decelerate) everywhere, small
        distances (2-8px), no bounce, no spin, no attention-seeking.
     3. Honest. Motion only where something actually changed — a page
        arrived, a menu opened, a row responded to the pointer. Nothing that
        cannot be clicked is given the hover behaviour of something that can.

   THREE SAFETY RULES for this file:
     a. Any animation that starts from an invisible state is written under
        `html.motion`, a class set by a one-line inline script in the page
        head. No scripting, no animation — the content is simply there.
     b. Any such animation is ALSO gated on `:not(.vt-arrival)` so it never
        replays on top of a cross-document view transition.
     c. Anything whose resting appearance would depend on an animation
        having finished gets an explicit resting state, because the
        reduced-motion and print blocks at the bottom remove animation
        outright rather than shortening it.
   ========================================================================== */

:root {
  /* The signature ease: fast out of the gate, long soft landing. */
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);

  --dur-tap: 90ms;
  --dur-fast: 150ms;
  --dur-base: 240ms;
  --dur-slow: 380ms;
}

/* ---------- Cross-page transitions ---------------------------------------
   The app is server-rendered, so every click used to be a white flash. A
   cross-document view transition turns that into one soft crossfade. The
   sidebar is given its own name so it is lifted out of the crossfade and
   stays perfectly still between pages — only the content changes.
   Browsers without view transitions navigate exactly as before (and get the
   entrance animation below instead).
   -------------------------------------------------------------------- */

@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: vt-fade-out 120ms var(--ease-inout) both;
}

::view-transition-new(root) {
  animation: vt-fade-in 220ms var(--ease-soft) both;
}

@keyframes vt-fade-out {
  to { opacity: 0; }
}

@keyframes vt-fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* Held still across navigations. Each is unique on the page, which view
   transitions require. */
.sidebar { view-transition-name: app-sidebar; }
.mobile-bar { view-transition-name: app-topbar; }
.site-header { view-transition-name: site-header; }

/* When one of those exists on both pages its two snapshots are identical and
   nothing moves — that is the point. When it exists on only ONE side (app →
   marketing, say) its snapshot is an only child, and the browser's default
   quarter-second fade would leave a ghost sidebar hanging over the new page
   after the root crossfade has already finished. Match the root timing. */
::view-transition-old(app-sidebar):only-child,
::view-transition-old(app-topbar):only-child,
::view-transition-old(site-header):only-child {
  animation: vt-fade-out 120ms var(--ease-inout) both;
}

::view-transition-new(app-sidebar):only-child,
::view-transition-new(app-topbar):only-child,
::view-transition-new(site-header):only-child {
  animation: vt-fade-in 220ms var(--ease-soft) both;
}

/* ---------- Page entrance -------------------------------------------------
   For loads that were NOT a view transition (first visit, reload, or a
   browser without support). The inline head script marks view-transition
   arrivals with .vt-arrival so the two never stack.
   -------------------------------------------------------------------- */

/* Overlays (the board toast, the AI dialog) are children of .app-main but are
   positioned by their own transforms and shown on demand — the cascade must
   not touch their transform or delay their own entrance. :where() keeps the
   exclusion at zero specificity so the delays below still win. */
html.motion:not(.vt-arrival) .app-main > *:not(:where(.board-toast, dialog, .paywall-page)) {
  animation: rise-in var(--dur-slow) var(--ease-soft) both;
}

/* A short cascade — capped so nothing important waits on animation. */
html.motion:not(.vt-arrival) .app-main > *:not(:where(.board-toast, dialog, .paywall-page)):nth-child(2) { animation-delay: 45ms; }
html.motion:not(.vt-arrival) .app-main > *:not(:where(.board-toast, dialog, .paywall-page)):nth-child(3) { animation-delay: 90ms; }
html.motion:not(.vt-arrival) .app-main > *:not(:where(.board-toast, dialog, .paywall-page)):nth-child(n + 4) { animation-delay: 130ms; }

/* Second level: the dashboard stat row cascades within itself. */
html.motion:not(.vt-arrival) .stats > .stat {
  animation: rise-in var(--dur-base) var(--ease-soft) both;
}
html.motion:not(.vt-arrival) .stats > .stat:nth-child(2) { animation-delay: 40ms; }
html.motion:not(.vt-arrival) .stats > .stat:nth-child(3) { animation-delay: 80ms; }
html.motion:not(.vt-arrival) .stats > .stat:nth-child(4) { animation-delay: 120ms; }
html.motion:not(.vt-arrival) .stats > .stat:nth-child(5) { animation-delay: 160ms; }
html.motion:not(.vt-arrival) .stats > .stat:nth-child(n + 6) { animation-delay: 200ms; }

/* Auth and capture pages animate the card, not the card AND its heading —
   stacked fades make the inner text arrive dim and late. */
html.motion:not(.vt-arrival) .auth-card {
  animation: rise-in var(--dur-slow) var(--ease-soft) both;
}

/* Privacy, Error and any other plain marketing page get one quiet entrance.
   (The landing page starts with .hero, which has its own cascade below.) */
html.motion:not(.vt-arrival) .site-main > .section:first-child:not(.hero) {
  animation: rise-in var(--dur-slow) var(--ease-soft) both;
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* The hero is above the fold on every visit, so it arrives on load rather
   than waiting for a scroll — one line after another, then the mock. */
html.motion:not(.vt-arrival) .hero > * {
  animation: rise-in 420ms var(--ease-soft) both;
}
html.motion:not(.vt-arrival) .hero > *:nth-child(2) { animation-delay: 60ms; }
html.motion:not(.vt-arrival) .hero > *:nth-child(3) { animation-delay: 110ms; }
html.motion:not(.vt-arrival) .hero > *:nth-child(4) { animation-delay: 155ms; }
html.motion:not(.vt-arrival) .hero > *:nth-child(5) { animation-delay: 195ms; }
html.motion:not(.vt-arrival) .hero > *:nth-child(n + 6) { animation-delay: 230ms; animation-duration: 520ms; }

/* The headline is the largest paint on the marketing page. Fading it up from
   zero would push Largest Contentful Paint out by the whole animation, so it
   rises without ever being transparent. */
html.motion:not(.vt-arrival) .hero > h1 {
  animation-name: rise-only;
}

@keyframes rise-only {
  from { transform: translateY(8px); }
  to { transform: none; }
}

/* The billing paywall: the blurred preview behind is a normal element (safe
   to animate with transform), but the fixed-position backdrop/modal in front
   of it are excluded above — a transform on their own ancestor would become
   the containing block for a fixed descendant and break the full-viewport
   overlay for the animation's duration. Each gets its own safe treatment. */
html.motion:not(.vt-arrival) .paywall-preview {
  animation: rise-in var(--dur-slow) var(--ease-soft) both;
}

html.motion:not(.vt-arrival) .paywall-overlay {
  animation: paywall-backdrop-in var(--dur-base) var(--ease-soft) both;
}

html.motion:not(.vt-arrival) .paywall-modal {
  animation: paywall-modal-in var(--dur-slow) var(--ease-soft) both;
}

@keyframes paywall-backdrop-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes paywall-modal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

/* Marketing sections reveal as they scroll into view. The hidden state is
   applied by JS (which then reveals it), never by this file alone — and JS
   also reveals anything that receives focus, so a keyboard user never lands
   on an invisible control. */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-soft),
              transform var(--dur-slow) var(--ease-soft);
}

.reveal.is-in {
  opacity: 1;
  transform: none;
}

/* ---------- Navigation progress ------------------------------------------
   A hairline that appears only when a navigation takes longer than a blink,
   so a slow page never feels like a dead click. Driven by transform, not
   width, so it costs no layout on the main thread at the exact moment the
   main thread is busiest.
   -------------------------------------------------------------------- */

.nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  transform: scaleX(0);
  transform-origin: left center;
  background: linear-gradient(90deg, var(--brand), #60a5fa);
  box-shadow: 0 0 12px rgba(37, 99, 235, 0.5);
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: transform var(--dur-slow) var(--ease-soft), opacity var(--dur-base) ease;
}

.nav-progress.is-active {
  opacity: 1;
}

/* ---------- Buttons ------------------------------------------------------- */

.btn {
  transition: background var(--dur-fast) ease,
              border-color var(--dur-fast) ease,
              color var(--dur-fast) ease,
              box-shadow var(--dur-base) var(--ease-soft),
              transform var(--dur-fast) var(--ease-soft);
}

.btn:hover {
  transform: translateY(-1px);
}

/* The press has to feel immediate — it's the one place a long ease reads as lag. */
.btn:active {
  transform: translateY(0) scale(0.985);
  transition-duration: var(--dur-tap);
}

.btn--primary:hover {
  box-shadow: 0 8px 18px -8px rgba(37, 99, 235, 0.65);
}

.btn--ghost:hover {
  box-shadow: var(--shadow);
}

.btn:disabled,
.btn[disabled] {
  transform: none;
  box-shadow: none;
}

/* Submitting. motion.js locks the button's measured size and swaps the label
   for the spinner, so the button never changes width and nothing beside it
   moves; the accessible name is preserved with aria-label + aria-busy. */
.btn.is-busy {
  pointer-events: none;
  opacity: 0.85;
}

html.motion .btn.is-busy::before {
  content: "";
  width: 0.85em;
  height: 0.85em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-bottom-color: transparent;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

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

/* ---------- Focus ---------------------------------------------------------
   A ring, and nothing else: no radius change (it would un-round pill
   controls mid-focus) and no transition (a focus ring must be instant).
   -------------------------------------------------------------------- */

a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Inside a clipping container an outset ring is cut off — ring inward. */
.segmented__item:focus-visible {
  outline-offset: -2px;
}

/* ---------- Sidebar ------------------------------------------------------- */

.sidebar__link {
  position: relative;
  transition: background var(--dur-fast) ease,
              color var(--dur-fast) ease,
              transform var(--dur-base) var(--ease-soft);
}

.sidebar__link svg {
  transition: color var(--dur-fast) ease, transform var(--dur-base) var(--ease-soft);
}

.sidebar__link:hover {
  transform: translateX(2px);
}

.sidebar__link:hover svg {
  color: var(--brand);
}

/* The accent marking the current section. Its resting state is simply
   present — the animation only adds the growth, so nothing depends on the
   animation having run. */
html.motion .sidebar__link.active::before {
  content: "";
  position: absolute;
  left: -0.75rem;
  top: 50%;
  width: 3px;
  height: 18px;
  border-radius: 0 3px 3px 0;
  background: var(--brand);
  transform: translateY(-50%) scaleY(1);
}

/* Grow it in on a real page load only. Re-growing it on every navigation —
   including the many that stay in the same section — would be a tic. */
html.motion:not(.vt-arrival) .sidebar__link.active::before {
  animation: accent-grow var(--dur-slow) var(--ease-soft) 60ms both;
}

@keyframes accent-grow {
  from { transform: translateY(-50%) scaleY(0); }
  to { transform: translateY(-50%) scaleY(1); }
}

.sidebar-backdrop {
  transition: opacity var(--dur-base) ease;
}

/* ---------- Cards, rows, tiles --------------------------------------------
   Lift is reserved for things you can actually click. A stat tile is not a
   link, so it deepens its shadow and stays put; the "Your day" counts above
   it ARE links, so they respond.
   -------------------------------------------------------------------- */

.card {
  transition: box-shadow var(--dur-base) var(--ease-soft),
              border-color var(--dur-base) ease;
}

.stat {
  transition: box-shadow var(--dur-base) var(--ease-soft),
              border-color var(--dur-base) ease;
}

.stat:hover {
  box-shadow: var(--shadow-lg);
  border-color: var(--border-strong);
}

.board-card {
  transition: box-shadow var(--dur-base) var(--ease-soft),
              transform var(--dur-base) var(--ease-soft),
              border-color var(--dur-base) ease;
}

.board-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
  border-color: var(--border-strong);
}

/* Never lift the card the pointer is dragging. */
.board-card--dragging,
.board-card--dragging:hover {
  transform: none;
  box-shadow: var(--shadow);
  transition: opacity var(--dur-fast) ease;
}

.board-col__body {
  transition: background var(--dur-fast) ease;
}

.yourday-count {
  transition: transform var(--dur-fast) var(--ease-soft);
}

.yourday-count:hover {
  transform: translateY(-1px);
}

.stat__value,
.yourday-count__num {
  /* Digits keep their width while the value counts up. */
  font-variant-numeric: tabular-nums;
}

/* The row hover colour itself lives in site.css (#fafbfc, chosen for contrast
   against the muted cell text) — only the easing belongs here. */
.table tbody tr {
  transition: background var(--dur-fast) ease;
}

.followup,
.timeline__item {
  transition: background var(--dur-fast) ease,
              border-color var(--dur-fast) ease,
              box-shadow var(--dur-base) var(--ease-soft);
}

/* Overdue rows keep their red border — a hover must not neutralise the one
   signal that matters on that row. */
.followup:not(.followup--overdue):hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow);
}

.followup--overdue:hover {
  box-shadow: var(--shadow);
}

.timeline__item:hover {
  background: #fafbfc;
}

.status-tab,
.chip,
.segmented__item {
  transition: background var(--dur-fast) ease,
              border-color var(--dur-fast) ease,
              color var(--dur-fast) ease,
              transform var(--dur-fast) var(--ease-soft);
}

.status-tab:hover,
.chip--link:hover {
  transform: translateY(-1px);
}

/* .segmented clips its children, so this one shifts colour instead of lifting. */
.segmented__item:not(.active):hover {
  background: var(--gray-bg);
}

/* The empty state already arrives inside a card that animates; only the icon
   gets its own accent, so the screen with the least to read doesn't stack
   three fades on top of one another. */
html.motion:not(.vt-arrival) .empty__icon {
  animation: pop-in var(--dur-base) var(--ease-soft) 120ms both;
}

@keyframes pop-in {
  from { opacity: 0; transform: scale(0.9); }
  to { opacity: 1; transform: none; }
}

/* ---------- Bars ----------------------------------------------------------
   Pipeline, report and checklist bars sweep out from the left on arrival.
   Width is still set inline by the server — only the scale is animated, so
   the value the bar settles on is always the real one.
   -------------------------------------------------------------------- */

html.motion:not(.vt-arrival) .pipeline-row__fill,
html.motion:not(.vt-arrival) .report-bar__fill,
html.motion:not(.vt-arrival) .checklist__progress-fill {
  transform-origin: left center;
  animation: bar-sweep 520ms var(--ease-soft) 100ms both;
}

@keyframes bar-sweep {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* ---------- Composer tabs ------------------------------------------------- */

.composer-tab {
  position: relative;
  transition: color var(--dur-fast) ease;
}

.composer-tab::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--brand);
  transform: scaleX(0);
  transition: transform var(--dur-base) var(--ease-soft);
}

.composer-tab.active {
  /* The sliding underline replaces the static border. */
  border-bottom-color: transparent;
}

.composer-tab.active::after {
  transform: scaleX(1);
}

/* The selected tab keeps its brand colour on hover — dimming it to the body
   colour makes the current tab look deselected. */
.composer-tab:not(.active):hover {
  color: var(--ink-soft);
}

/* Panels fade up as they take over — the tab swap stops feeling like a jump. */
html.motion .composer-panel:not([hidden]) {
  animation: panel-in var(--dur-base) var(--ease-soft) both;
}

@keyframes panel-in {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* ---------- Menus and popovers -------------------------------------------- */

html.motion .switcher[open] .switcher__menu,
html.motion .reschedule[open] .reschedule__menu,
html.motion .tag-input__suggest:not([hidden]) {
  animation: menu-in var(--dur-fast) var(--ease-soft) both;
  transform-origin: top center;
}

@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.switcher__chevron {
  transition: transform var(--dur-base) var(--ease-soft);
}

.switcher__current,
.reschedule__opt {
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

/* ---------- Modal ---------------------------------------------------------
   Gated: the dialog is opened by script, so if script is missing this must
   not leave a transparent dialog behind.
   -------------------------------------------------------------------- */

html.motion .modal {
  opacity: 0;
  transform: translateY(10px) scale(0.98);
  transition: opacity var(--dur-base) var(--ease-soft),
              transform var(--dur-base) var(--ease-soft),
              overlay var(--dur-base) allow-discrete,
              display var(--dur-base) allow-discrete;
}

html.motion .modal[open] {
  opacity: 1;
  transform: none;
}

html.motion .modal::backdrop {
  background: rgba(15, 23, 42, 0);
  backdrop-filter: blur(0px);
  transition: background var(--dur-base) var(--ease-soft),
              backdrop-filter var(--dur-base) var(--ease-soft),
              overlay var(--dur-base) allow-discrete,
              display var(--dur-base) allow-discrete;
}

html.motion .modal[open]::backdrop {
  background: rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(2px);
}

/* Browsers with @starting-style animate the way in as well as out; the rest
   just show the finished state. */
@starting-style {
  html.motion .modal[open] {
    opacity: 0;
    transform: translateY(10px) scale(0.98);
  }
  html.motion .modal[open]::backdrop {
    background: rgba(15, 23, 42, 0);
    backdrop-filter: blur(0px);
  }
}

/* ---------- Toast --------------------------------------------------------- */

html.motion .board-toast:not([hidden]) {
  animation: toast-in var(--dur-base) var(--ease-soft) both;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* ---------- Inputs and links ---------------------------------------------- */

input,
select,
textarea {
  transition: border-color var(--dur-fast) ease,
              box-shadow var(--dur-base) var(--ease-soft),
              background var(--dur-fast) ease;
}

.nav-link,
.table-primary-link,
.followup__action {
  transition: color var(--dur-fast) ease;
}

.logo {
  transition: opacity var(--dur-fast) ease;
}

.logo:hover {
  opacity: 0.85;
}

/* Deliberately NOT setting `scroll-behavior: smooth` on the root: it also
   animates the scroll that follows keyboard focus and find-in-page, leaving
   the focus ring lagging behind where the user already is. */

/* ==========================================================================
   Reduced motion — one switch turns all of the above off.

   Animations are removed outright rather than shortened, so nothing in the
   UI depends on an animation having run. Anything whose visible state came
   from an animation's end frame is restored explicitly below. Colour,
   shadow and focus feedback stay: this is about movement, not about making
   the app feel dead.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }

  *,
  *::before,
  *::after,
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* End states that the removed animations used to provide. */
  html.motion .pipeline-row__fill,
  html.motion .report-bar__fill,
  html.motion .checklist__progress-fill,
  html.motion .modal,
  html.motion .modal[open] {
    transform: none;
    opacity: 1;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }

  /* No lift, no slide, no press. */
  .btn:hover,
  .btn:active,
  .board-card:hover,
  .sidebar__link:hover,
  .status-tab:hover,
  .chip--link:hover,
  .yourday-count:hover {
    transform: none;
  }

  .nav-progress {
    display: none;
  }

  /* An arc that cannot spin just looks like a rendering fault; motion.js
     leaves the button's label in place when motion is reduced. */
  html.motion .btn.is-busy::before {
    display: none;
  }
}

/* ==========================================================================
   Print — paper never scrolls, so anything waiting on a scroll or on an
   animation has to be shown unconditionally.
   ========================================================================== */

@media print {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }

  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  html.motion .pipeline-row__fill,
  html.motion .report-bar__fill,
  html.motion .checklist__progress-fill {
    transform: none !important;
  }

  .nav-progress {
    display: none !important;
  }
}
