/* Wave 5, motion: the frequency audit, springs, ceilings and the sheet.

   This file loads after hub.css and the wave 2 to 4 files, so a rule here
   at equal specificity wins. It changes numbers and curves, not layout.

   The one idea behind it: animate rare things, not frequent things. A
   surface says how often it is used through data-freq, and every duration
   token under it follows. The command palette and the dots menus are used
   hundreds of times a day and get no motion; contract signed and the payout
   get the choreography.

   Sections:
   1. Tokens: the spring, the iOS sheet curve, the exit duration.
   2. Frequency classes.
   3. The palette: 80ms in, 100ms out, one flash on the chosen row.
   4. Popovers and the dots menus: transitions from the trigger, no keyframes.
   5. The dialog and the phone sheet on Vaul's numbers.
   6. Springs on the things that settle: ticks, chips, badges, the stepper.
   7. Interruptible: toasts, the two-step ask, the bars, the summary row.
   8. Entrances only on first paint.
   9. Folds through details-content where the browser has it.
   10. Skeleton timing.
   11. Tooltips.
   12. The portal canvas, flat.
   13. Reduced motion. */

/* ---- 1. Tokens ---------------------------------------------------------- */

:root {
  /* Height auto can animate where the browser allows it; the grid-rows fold
     stays as the fallback for the browsers that do not. */
  interpolate-size: allow-keywords;

  /* A real spring, sampled into linear(): damping ratio 0.9 (Motion's bounce
     0.1), at the target by about 280ms, settled by 420ms, overshoot under a
     quarter of a percent. Generated by scratchpad/spring.rb. It is for state
     changes that cannot be interrupted (a tick, a count, a chip flipping),
     never for a sheet under a finger. */
  --ease-spring: linear(0, 0.0171 2.1%, 0.061 4.2%, 0.1222 6.3%, 0.1936 8.3%, 0.2698 10.4%, 0.347 12.5%, 0.4224 14.6%, 0.494 16.7%, 0.5606 18.8%, 0.6217 20.8%, 0.6768 22.9%, 0.726 25.0%, 0.7694 27.1%, 0.8073 29.2%, 0.8402 31.3%, 0.8685 33.3%, 0.8926 35.4%, 0.913 37.5%, 0.9301 39.6%, 0.9444 41.7%, 0.9563 43.8%, 0.966 45.8%, 0.974 47.9%, 0.9804 50.0%, 0.9855 52.1%, 0.9896 54.2%, 0.9928 56.3%, 0.9953 58.3%, 0.9972 60.4%, 0.9986 62.5%, 0.9996 64.6%, 1.0004 66.7%, 1.0009 68.8%, 1.0012 70.8%, 1.0014 72.9%, 1.0015 75.0%, 1.0015 77.1%, 1.0015 79.2%, 1.0014 81.3%, 1.0013 83.3%, 1.0012 85.4%, 1.0011 87.5%, 1.001 89.6%, 1.0008 91.7%, 1.0007 93.8%, 1.0006 95.8%, 1.0006 97.9%, 1);
  --dur-spring: 420ms;

  /* The sheet: Vaul's curve (Ionic's, which matches iOS) and its 500ms.
     The close is shorter, as every exit is. Neither follows data-freq: a
     sheet moves at the speed of a sheet whatever opened it. */
  --ease-ios-sheet: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-sheet: 500ms;
  --dur-sheet-exit: 380ms;

  /* Exits are shorter than entrances and end in opacity plus a 2px blur. */
  --dur-exit: 160ms;
}

/* ---- 2. Frequency classes ---------------------------------------------- */

/* High: used all day. No motion at all; a change of background is the whole
   feedback. Mid: a few times a day, short and quiet. Rare: the hub's default
   tokens, which are already tuned for the once-a-week moments. */
[data-freq="high"] {
  --dur-fast: 0ms;
  --dur-base: 0ms;
  --dur-enter: 0ms;
  --dur-slow: 0ms;
  --dur-exit: 0ms;
}

[data-freq="mid"] {
  --dur-fast: 100ms;
  --dur-base: 150ms;
  --dur-enter: 180ms;
  --dur-slow: 220ms;
  --dur-exit: 100ms;
}

/* ---- 3. The palette ----------------------------------------------------- */

/* Opened by cmd K dozens of times a day: an 80ms fade in, a 100ms fade out,
   no scale, no blur, no rise. The rows arrive with the box instead of one
   after another, and the chosen row flashes once before the box goes. */
.hub-dialog.palette-dialog {
  transform: none;
  filter: none;
  transition: opacity 80ms linear,
              overlay 80ms allow-discrete,
              display 80ms allow-discrete;
}

.hub-dialog.palette-dialog:not([open]) {
  transform: none;
  filter: none;
  transition-duration: 100ms;
}

.hub-dialog.palette-dialog::backdrop {
  transition-duration: 80ms;
  transition-timing-function: linear;
}

.hub-dialog.palette-dialog:not([open])::backdrop {
  transition-duration: 100ms;
}

@starting-style {
  .hub-dialog.palette-dialog[open] {
    opacity: 0;
    transform: none;
    filter: none;
  }
}

.palette-item {
  animation: none;
  transition: none;
}

.palette-item:active {
  transform: none;
}

.palette-item.is-chosen {
  animation: palette-flash 120ms linear;
}

@keyframes palette-flash {
  0% { background: transparent; }
  50% { background: var(--line); }
  100% { background: var(--surface-soft); }
}

/* ---- 4. Popovers and the dots menus ------------------------------------ */

/* A transition, not a keyframe, so a second tap mid-flight reverses it. The
   body scales from its trigger: the popover controller writes --origin from
   where the body landed (top right when it opened downward, bottom right
   when it had to flip). Under data-freq="high" every duration here is 0ms
   and the menu simply appears, which is what a menu used all day should do. */
.more-menu-body,
.page-help-body {
  animation: none;
  opacity: 1;
  transform: none;
  filter: none;
  transform-origin: var(--origin, top right);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              filter var(--dur-base) var(--ease-out);
}

@starting-style {
  details[open] .more-menu-body,
  details[open] .page-help-body {
    opacity: 0;
    transform: scale(.96);
    filter: blur(2px);
  }
}

/* The exit the controller holds the details open for: shorter, ease-in, and
   finished by opacity and blur rather than by travelling. */
.more-menu.is-closing .more-menu-body,
.page-help.is-closing .page-help-body {
  opacity: 0;
  transform: scale(.98);
  filter: blur(2px);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

/* Placement from the trigger itself where the browser has anchor
   positioning, so the body hangs off its summary and flips above it when
   the row is at the bottom of the window. The absolute rules in hub.css
   stay as the fallback. On a phone the row menu is a sheet, not a popover,
   so the anchor rules stop at the sheet breakpoint. */
@media (min-width: 761px) {
  /* anchor() insets rather than position-area: the Chromium this was
     checked in resolved the anchor but laid the area out at the containing
     block's corner, and the insets place it the same way in every engine
     that has anchors at all. */
  @supports (top: anchor(bottom)) and (anchor-name: --a) {
    .more-menu > summary {
      anchor-name: --more-menu;
    }

    .more-menu-body {
      position-anchor: --more-menu;
      inset: auto;
      top: anchor(bottom);
      right: anchor(right);
      margin: 6px 0 0;
      position-try-fallbacks: flip-block;
    }

    .page-help > summary {
      anchor-name: --page-help;
    }

    .page-help-body {
      position-anchor: --page-help;
      inset: auto;
      top: anchor(bottom);
      right: anchor(right);
      margin: 2px 0 0;
      position-try-fallbacks: flip-block;
    }
  }
}

/* The row menu as a phone sheet: the same transition shape as the dialog
   sheet, on the iOS curve, with the backdrop fading beside it. */
@media (max-width: 760px) {
  .more-menu-body {
    animation: none;
    transform-origin: bottom center;
    transition: transform var(--dur-sheet) var(--ease-ios-sheet);
  }

  .more-menu[open] .more-menu-backdrop {
    animation: none;
    opacity: 1;
    transition: opacity var(--dur-sheet) var(--ease-ios-sheet);
  }

  @starting-style {
    .more-menu[open] .more-menu-body {
      opacity: 1;
      filter: none;
      transform: translateY(100%);
    }

    .more-menu[open] .more-menu-backdrop {
      opacity: 0;
    }
  }

  .more-menu.is-closing .more-menu-body {
    opacity: 1;
    filter: none;
    transform: translateY(100%);
    transition-duration: var(--dur-sheet-exit);
    transition-timing-function: var(--ease-ios-sheet);
  }

  .more-menu.is-closing .more-menu-backdrop {
    opacity: 0;
    transition-duration: var(--dur-sheet-exit);
  }
}

/* On a touch screen the row content sits in a swipe wrapper with
   will-change: transform and z-index: 1 (wave4-shell.css), which makes it
   the containing block and the stacking context for anything fixed inside
   it: the row menu's sheet and its backdrop were drawn inside the row,
   clipped by it, and painted under the rows below. While the menu is open
   the row gives both up, so the sheet reaches the viewport; the backdrop
   covers whatever the row's own panels show through. */
@media (hover: none) {
  .swipe.swipe-enabled > [data-swipe-target="content"]:has(.more-menu[open]) {
    z-index: auto;
    will-change: auto;
  }
}

/* ---- 5. The dialog and the sheet --------------------------------------- */

/* Laptop: the entrance stays at 280ms on the house curve; the exit is 160ms
   ease-in, drops only 4px, and lets opacity and the blur finish it. Nothing
   shrinks on the way out. */
.hub-dialog:not([open]) {
  transform: translateY(4px);
  filter: blur(2px);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

.hub-dialog:not([open])::backdrop {
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

/* Phone: the sheet rides Vaul's curve, 500ms up and 380ms down, and nothing
   but transform moves. The controller clears its inline transform on release
   and this transition carries the sheet the rest of the way from wherever
   the finger left it. */
@media (max-width: 760px) {
  .hub-dialog {
    transition-duration: var(--dur-sheet);
    transition-timing-function: var(--ease-ios-sheet);
  }

  .hub-dialog:not([open]) {
    transform: translateY(100%);
    filter: none;
    opacity: 1;
    transition-duration: var(--dur-sheet-exit);
    transition-timing-function: var(--ease-ios-sheet);
  }

  .hub-dialog:not([open])::backdrop {
    transition-duration: var(--dur-sheet-exit);
    transition-timing-function: var(--ease-ios-sheet);
  }

  /* The palette is not a sheet on a phone either: it fades, high up. */
  .hub-dialog.palette-dialog,
  .hub-dialog.palette-dialog:not([open]) {
    transform: none;
    transition-duration: 80ms;
    transition-timing-function: linear;
  }

  .hub-dialog.palette-dialog:not([open]) {
    transition-duration: 100ms;
  }
}

/* ---- 6. Springs --------------------------------------------------------- */

/* Anything that arrives with a pop now starts at 0.9, not 0.6: a thing that
   comes from nothing comes from nowhere. Same keyframe name as hub.css, so
   every use of pop-in picks this up. */
@keyframes pop-in {
  from { transform: scale(.9); opacity: 0; filter: blur(2px); }
  to { transform: none; opacity: 1; filter: none; }
}

/* The copied tick, on the spring. */
.btn.is-success::before {
  animation: pop-in var(--dur-spring) var(--ease-spring);
}

.quick-action-icon {
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-spring) var(--ease-spring);
}

.quick-action.is-success .quick-action-icon {
  transform: scale(1.06);
}

/* The upload tick beside each clip as it lands. */
.portal-queue .portal-clip-check {
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-spring) var(--ease-spring);
}

/* Chips flip state on the spring and press on the fast curve. */
.chip {
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-spring) var(--ease-spring);
}

.chip:active {
  transition-duration: var(--dur-fast);
}

/* The count in the sidebar: application.js marks it when the number changed
   between two pages, and it settles from a size up. */
.hub-nav-badge.is-bumped {
  animation: badge-bump var(--dur-spring) var(--ease-spring);
}

@keyframes badge-bump {
  from { transform: scale(1.25); }
  to { transform: none; }
}

/* The swipe panel's label when it catches: one settle, not a pulse. */
.swipe-actions.is-caught .swipe-action {
  animation: swipe-catch var(--dur-spring) var(--ease-spring);
}

@keyframes swipe-catch {
  from { transform: scale(1.06); }
  to { transform: none; }
}

/* The stepper on the portal moment: the finished circle flips to a tick and
   the next one lights, each settling from a size down. Scoped under
   no-preference like the wave 3 rules it replaces. */
@media (prefers-reduced-motion: no-preference) {
  .portal-steps li.is-flipping span {
    animation: step-flip var(--dur-spring) var(--ease-spring);
  }

  .portal-steps li.is-lighting span {
    animation: step-light var(--dur-spring) var(--ease-spring);
  }

  @keyframes step-flip {
    from { transform: scale(.9); }
    to { transform: none; }
  }

  @keyframes step-light {
    from { transform: scale(.92); }
    to { transform: none; }
  }
}

/* ---- 7. Interruptible --------------------------------------------------- */

/* Toasts: the entrance and the exit are one transition, so a dismiss that
   lands while a toast is still rising turns it around instead of waiting.
   The exit is 160ms, lifts 4px and blurs; on a phone it drops instead. */
.flash-stack .flash {
  animation: none;
  opacity: 1;
  transform: none;
  filter: none;
  transition: opacity var(--dur-enter) var(--ease-out),
              transform var(--dur-enter) var(--ease-out),
              filter var(--dur-enter) var(--ease-out);
}

@starting-style {
  .flash-stack .flash {
    opacity: 0;
    transform: translateY(6px);
    filter: blur(2px);
  }
}

.flash-stack .flash.is-leaving {
  animation: none;
  opacity: 0;
  transform: translateY(-4px);
  filter: blur(2px);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

@media (max-width: 760px) {
  .flash-stack .flash.is-leaving {
    transform: translateY(4px);
  }
}

/* The two-step ask, the bulk bar, the pay bar and the summary row's fields
   all toggle between shown and hidden, and a person can toggle them back
   mid-flight. Each becomes a display transition with a starting style:
   the hidden rule in the earlier file still sets display none. */
.twostep-ask:not([hidden]),
.bulk-bar:not([hidden]),
.pay-bar:not([hidden]),
.summary-row.is-editing .summary-row-fields {
  animation: none;
}

.twostep-ask,
.bulk-bar,
.pay-bar,
.summary-row-fields {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              display var(--dur-base) allow-discrete;
}

.twostep-ask[hidden] {
  opacity: 0;
  transform: translateX(6px);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

.bulk-bar[hidden],
.pay-bar[hidden],
.summary-row:not(.is-editing) .summary-row-fields {
  opacity: 0;
  transform: translateY(6px);
  transition-duration: var(--dur-exit);
  transition-timing-function: var(--ease-in);
}

@starting-style {
  .twostep-ask:not([hidden]) {
    opacity: 0;
    transform: translateX(6px);
  }

  .bulk-bar:not([hidden]),
  .pay-bar:not([hidden]),
  .summary-row.is-editing .summary-row-fields {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* The segment pill slides on the out curve, under the ceiling. */
.segments::before {
  transition: left 220ms var(--ease-out), width 220ms var(--ease-out);
}

/* ---- 8. Entrances only on first paint ---------------------------------- */

/* application.js puts first-paint on the body for a full load and takes it
   off before any Turbo render, morph or frame swap. The staggers and the
   blur-ins that say "here is your page" run once; a refresh that only
   changed a number does not replay them. */
body:not(.first-paint) :is(.portal-task-card, .portal-queue li, .editor-section, .decision-pane, .video-tile-poster, .portal-homescreen:not([hidden])) {
  animation: none;
}

/* The task card's entrance was 420ms; 300ms is the ceiling for anything
   that is not a sheet. */
.portal-task-card {
  animation-duration: 300ms;
}

/* The reading bar and its arrow were on the 420ms token for a colour change
   and a quarter turn. */
.portal-readbar {
  transition: background 240ms var(--ease-out), color 240ms var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.portal-readbar-arrow {
  transition: transform 240ms var(--ease-out);
}

/* ---- 9. Folds ----------------------------------------------------------- */

/* hub.css already transitions ::details-content height where the browser
   has it; interpolate-size on :root above is what lets height auto take
   part in that transition at all, so the fold also closes by growing shut
   instead of snapping. The grid-rows reveal inside stays as it is: it is
   the fallback for the browsers without the pseudo, and where both run they
   run on the same curve and the same duration, so the eye sees one fold. */

/* ---- 10. Skeleton timing ------------------------------------------------ */

/* A frame that answers in under 200ms never shows it was busy: the dim and
   the sheen both wait that long, so only a wait a person would notice gets
   a picture of waiting. */
turbo-frame[busy] > * {
  transition-delay: 200ms;
}

turbo-frame[busy]::after {
  opacity: 0;
  animation: busy-show 1ms linear 200ms forwards,
             btn-shimmer 1.6s var(--ease-in-out) 200ms infinite;
}

@keyframes busy-show {
  to { opacity: 1; }
}

/* ---- 11. Tooltips ------------------------------------------------------- */

/* One tooltip element per group, drawn by the tooltip controller for any
   [data-tip] inside it. The first one waits 400ms and rises 4px through a
   blur; while the group is warm the next ones move with no delay and no
   motion, the way a menu bar scans. */
.tip {
  position: fixed;
  z-index: var(--z-toast);
  max-width: 260px;
  padding: 5px 9px;
  border-radius: var(--r-xs);
  background: var(--ink);
  color: #fff;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px);
  filter: blur(2px);
  transition: opacity 120ms var(--ease-out),
              transform 120ms var(--ease-out),
              filter 120ms var(--ease-out);
}

.tip.is-on {
  opacity: 1;
  transform: none;
  filter: none;
}

.tip.is-warm {
  transition-duration: 0ms;
}

/* ---- 12. The portal canvas, flat ---------------------------------------- */

/* The gradient was the first thing every list of AI tells names. Flat lilac,
   cream cards; the moment and the task card read on it without a glow. */
body.portal-page {
  background-image: none;
  background-color: #D2BFFB;
}

/* ---- 13. Reduced motion ------------------------------------------------- */

/* hub.css already cuts every duration to .01ms and every loop to one run,
   which covers the springs (a linear() curve over .01ms is a jump) and the
   transitions above. What is left is the shimmer, which must become a still
   placeholder rather than a very fast one, and the tooltip delay. */
@media (prefers-reduced-motion: reduce) {
  turbo-frame[busy]::after {
    animation: none;
    opacity: 1;
    background: rgba(255, 255, 255, .35);
  }

  .tip {
    transition: none;
  }
}
