/* Motion.
 *
 * Every animation in this product has to answer one question — WHAT JUST
 * HAPPENED, AND WHAT SHOULD I LOOK AT NEXT. Anything that cannot answer it is
 * decoration, and decoration on a founder's dashboard is noise they have to
 * look past every morning.
 *
 * Three rules, and the third is the one that took research to find:
 *
 *   1. Never linear. Entries ease OUT (fast, then settle — the thing arrives
 *      and stops). Exits ease IN (it commits and goes). Changes to something
 *      already on screen ease in and out.
 *
 *   2. Functional UI stays under 400ms. 110ms for a press, 160–220ms for a
 *      state change, 320ms for something genuinely arriving. Nothing waits on
 *      an animation.
 *
 *   3. HIGH-FREQUENCY INTERACTIONS DO NOT ANIMATE AT ALL. Rauno Freiberg's
 *      rule, and it is the opposite of the instinct: a command menu opened
 *      forty times a day must appear instantly, because the cognitive cost of
 *      forty repeated animations outweighs any delight the first one bought.
 *      So ⌘K, the nav and the tabs are deliberately motionless. What animates
 *      is what changes rarely and matters when it does.
 *
 * What is NOT here, on purpose: no page-load entrance on every screen, no
 * hover-lift on every card, no scroll-triggered reveals. Those are the generic
 * defaults, they fire whether or not anything happened, and they are how a
 * dashboard starts feeling like a brochure.
 */

:root {
  /* ------------------------------------------------------------- the curves */
  /* Entry: leaves fast, arrives gently. Nothing overshoots — this is a
     business tool, not a toy. */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  /* Exit: commits immediately and accelerates away. */
  --ease-in: cubic-bezier(.7, 0, .84, 0);
  /* A change to something already on screen, going nowhere. */
  --ease-both: cubic-bezier(.65, 0, .35, 1);
  /* The one curve allowed a little life, for a meter filling toward a goal. */
  --ease-spring: cubic-bezier(.34, 1.4, .64, 1);

  /* ---------------------------------------------------------- the durations */
  --t-press: 90ms;    /* a finger going down. Must feel like contact, not lag. */
  /* RENAMED from --t-micro, which css/tokens.css:45 already owns as a 10px
     FONT SIZE. This file loads last, so the duration won: every
     `font-size: var(--d-micro)` became `font-size: 130ms`, which is invalid
     and therefore dropped, and all twelve micro-labels silently rendered at
     their inherited 13px instead of 10px. Two names, two meanings. */
  --d-micro: 130ms;   /* a colour, a border, a chevron turning */
  --t-state: 200ms;   /* something opening, closing, filling */
  --t-enter: 320ms;   /* something arriving that was not there before */
  --t-slow: 520ms;    /* the band meter only — it is showing a journey */

  /* The two the rest of the app already speaks. */
  --fast: var(--d-micro) var(--ease-both);
  --med: var(--t-state) var(--ease-out);
}

/* ==========================================================================
   1 · PRESS
   The cheapest and most valuable animation in any interface: proof the thing
   registered the click before anything else happens. 90ms, and it comes back
   on its own if the action is slow.
   ========================================================================== */
.btn, .pillbtn, .tab, .send-btn, .fronttabs button,
.suggest button, .disc-b, .arow, .hcard-h, .bcard, .cmd-r {
  -webkit-tap-highlight-color: transparent;
}
.btn:active:not(:disabled),
.pillbtn:active, .send-btn:active, .suggest button:active {
  transform: scale(.972);
  transition: transform var(--t-press) var(--ease-both);
}
.tab:active { transform: scale(.985); transition: transform var(--t-press) var(--ease-both); }
/* Rows and cards press by settling, not shrinking — a card that scales looks
   like it is being pushed away rather than pushed in. */
.arow:active, .hcard-h:active, .trow:active { transform: translateY(1px); transition: transform var(--t-press) var(--ease-both); }

/* ==========================================================================
   2 · DISCLOSURE — the product's core interaction
   Thin on the front, full on click. The whole restyle rests on this one
   gesture, so it is the one that must not just pop: the body grows from
   nothing, which is what tells you the detail came from inside the summary
   rather than landing on top of it.

   grid-template-rows 0fr -> 1fr is the only way to animate to an unknown
   height without measuring it in JS, and a screen here is not allowed to
   measure anything.
   ========================================================================== */
/* A TRANSITION cannot work here. Every state change in this app re-renders the
   screen, so the open disclosure is not the closed one having changed — it is a
   new element born open, with nothing to transition from. An ANIMATION does run
   on a newly created element, so the growth is a keyframe.

   Closing is instant, and that is right: exits commit and go, and nobody needs
   to watch something they just collapsed. */
.disc-wrap { display: grid; grid-template-rows: 0fr; }
.disc-wrap > .disc-body { min-height: 0; overflow: hidden; }
.disc.is-open .disc-wrap {
  grid-template-rows: 1fr;
  animation: discOpen var(--t-state) var(--ease-out) both;
}
@keyframes discOpen { from { grid-template-rows: 0fr; } to { grid-template-rows: 1fr; } }
.disc.is-open .disc-wrap > .disc-body { animation: discFade var(--t-state) var(--ease-out) 50ms both; }
@keyframes discFade { from { opacity: 0; } to { opacity: 1; } }
/* The chevron is the state, so it turns rather than being swapped. */
.disc-b > svg:first-child { transition: transform var(--t-state) var(--ease-out); }

/* ==========================================================================
   3 · THE BAND METER — the one place a little life is earned
   It is showing progress toward something a person has to work for, so it
   fills rather than appearing filled. The slight overshoot on --ease-spring is
   the only overshoot in the product.
   ========================================================================== */
.bandseg-fill {
  width: var(--fill, 0%);
  animation: bandFill var(--t-slow) var(--ease-spring) both;
}
@keyframes bandFill { from { width: 0; } to { width: var(--fill, 0%); } }
/* The earned segments land one after another, left to right, so the eye reads
   the ladder in the order it was climbed. */
.bandseg.is-earned { animation: segEarn var(--t-state) var(--ease-out) both; animation-delay: calc(var(--i, 0) * 70ms); }
@keyframes segEarn { from { opacity: .25; transform: scaleX(.6); transform-origin: left; } to { opacity: 1; transform: none; } }

/* Progress bars elsewhere fill the same way, once. */
.bar > span, .pace-fill { transition: width var(--t-slow) var(--ease-out); }

/* ==========================================================================
   4 · ARRIVAL — only for things that genuinely just arrived
   The run's artifacts, and the answer in a thread. Not screens, not cards at
   large, not anything that was already there when you opened the page.
   ========================================================================== */
@keyframes aoRise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes aoPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.82); } }
@keyframes aoDash { to { stroke-dashoffset: -14; } }
/* One definition each. `rise`, `pulse` and `appear` were three more keyframes
   doing these same two jobs under different names — a system with duplicates
   is a system that will drift. */
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.82); } }
@keyframes appear { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* A stack that fills up in order. The stagger stops at 8 so a long list never
   makes anyone wait for the last row. */
.stagger > * { animation: aoRise var(--t-enter) var(--ease-out) both; }
.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 45ms; }
.stagger > *:nth-child(3) { animation-delay: 90ms; }
.stagger > *:nth-child(4) { animation-delay: 135ms; }
.stagger > *:nth-child(5) { animation-delay: 180ms; }
.stagger > *:nth-child(6) { animation-delay: 225ms; }
.stagger > *:nth-child(7) { animation-delay: 270ms; }
.stagger > *:nth-child(n+8) { animation-delay: 300ms; }

/* The newest artifact in the run comes in from the top of its own stack. */
.artifact { animation: aoRise var(--t-enter) var(--ease-out) both; }

/* ==========================================================================
   5 · LEAVING — the counterpart nobody builds
   A row that is approved should be SEEN to leave, or the queue just silently
   has one fewer thing in it and you are left wondering whether you did that.
   Exits ease IN: it commits and goes.
   ========================================================================== */
.is-leaving {
  animation: leave var(--t-state) var(--ease-in) forwards;
  pointer-events: none;
}
@keyframes leave {
  to { opacity: 0; transform: translateX(14px); max-height: 0; margin-block: 0; padding-block: 0; }
}

/* ==========================================================================
   6 · THE TOAST
   It is the app speaking, so it comes up from where the app's own controls
   live rather than dropping from the ceiling.
   ========================================================================== */
.toast { animation: toastIn var(--t-enter) var(--ease-out) both; }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }

/* ==========================================================================
   7 · THE NUMBER ROLL — CUT, AND WHY
   A hero figure counting up to itself is the signature premium-SaaS move, and
   the mono type here would have made it work: the digits do not change width
   as they roll, so nothing around them shifts.

   It is not here because of how this app renders. The two-part render swaps
   #view's innerHTML on every state change, and during the run that is every
   540ms — so a count-up keyed to element creation would re-fire continuously,
   and every figure on the screen would flicker between zero and its value
   while you tried to read it. CSS has no way to say "only the first time".

   The fix would be to hold the animation in JS across renders, which means
   chrome that tracks element identity — real complexity, bought for an effect
   that answers no question. A number that is already correct does not need to
   tell you it arrived.
   ========================================================================== */

/* ==========================================================================
   8 · WHAT DOES NOT MOVE, AND WHY
   The command menu is opened dozens of times a day. The tabs are clicked
   constantly. Animating either taxes every single use to decorate the first.
   They change instantly and that is the feature.

   The glass dock (css/dock.css) is the one piece of chrome that moves, by
   the founder's own choice, and it keeps the rule's spirit: all of its motion
   answers the pointer, and none of it stands between a click and the page.
   ========================================================================== */
.cmd, .cmd-scrim, .cmd-r, .cmd-b { animation: none !important; transition: none !important; }
.tab { transition: background var(--d-micro) var(--ease-both), color var(--d-micro) var(--ease-both); }

/* ==========================================================================
   9 · REDUCED MOTION
   Not "less motion" — none. Everything still has to arrive at its final state,
   so `both` fill modes and width transitions resolve rather than being frozen
   at zero. This is why the band meter sets its width in CSS as well as in the
   keyframe.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }
  .disc-wrap { transition: none; }
  .disc.is-open .disc-wrap { grid-template-rows: 1fr; }
  .bandseg-fill { width: var(--fill, 0%); }
}

/* ==========================================================================
   10 · THE SLIDING INDICATOR
   One pill per tab row, moved with transform rather than left/width so it is
   composited and the row never reflows while it travels. js/slide.js does the
   FLIP; this is only what it looks like.

   The pill sits UNDER the labels, so the active tab's own background is
   removed — two backgrounds on one tab is one too many, and the static one
   would arrive before the moving one every time.
   ========================================================================== */
.slide-ind {
  position: absolute;
  left: 0;
  transform-origin: 0 50%;
  /* width is set in js and transitioned; it is NOT scaled, or the border and
     the 999px radius would be multiplied with it. */
  background: var(--surface);
  border: 1px solid var(--divider);
  border-radius: var(--r-pill);
  pointer-events: none;
  z-index: 0;
  /* Motion's performance guide: a layout-triggering style is safe to animate on
     an element that cannot affect anything around it, and this one is absolute
     with no children. will-change gives it its own compositor layer so the
     width change repaints that layer alone. Layers are not free, so this is the
     only place in the system that asks for one — there are at most two tab rows
     on screen at a time. */
  will-change: transform, width;
}
/* The front door's active tab is an INK pill with light text — that is what the
   design specifies and what the button's own colour assumes. Giving the
   indicator the light --hover fill put #FBFBF9 text on an #F0EFE9 pill, which
   is near-white on near-white and could not be read at all. */
.fronttabs .slide-ind { background: var(--ink); border-color: var(--ink); }
.tabs > .tab, .fronttabs > button { position: relative; z-index: 1; }
/* The indicator now carries the active state, so the tab must not paint it too. */
.tabs[data-slide] .tab.is-on { background: transparent; border-color: transparent; }
.fronttabs[data-slide] button.is-on { background: transparent; }

/* ==========================================================================
   11 · THE CROSS-FADE
   Only when the route changes — a re-render on the same screen must not flash,
   or the run looks like it is reloading itself twice a second. No slide: a
   sideways movement on every tab reads as lag, not polish.
   ========================================================================== */
.viewin { animation: viewIn 140ms var(--ease-out) both; }
@keyframes viewIn { from { opacity: .4; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .slide-ind { transition: none !important; }
  .viewin { animation: none; }
}

/* ==========================================================================
   12 · THE LADDER, AND THE HOLD
   Driven from js/motion.js, which reads what ORG.credit()/ORG.reset() reported
   and animates the row it happened to. Nothing here fires on its own.
   ========================================================================== */

/* A promotion. The ring is a pseudo-element so congratulating a card can never
   move the card — or anything beside it. This is the one flourish in the
   product, and it is rare enough to be worth having. */
.arow.earned { position: relative; }
.arow.earned::after {
  content: ''; position: absolute; inset: -6px;
  border-radius: 14px; border: 1.5px solid var(--accent);
  pointer-events: none;
  animation: earnRing 820ms var(--ease-out) forwards;
}
@keyframes earnRing {
  from { opacity: .9; transform: scale(.985); }
  to   { opacity: 0;  transform: scale(1.035); }
}

/* Hold to approve. The fill IS the progress — a control that shows its own
   state rather than borrowing a spinner to do it. */
[data-hold] { position: relative; overflow: hidden; touch-action: none; }
[data-hold] > * { position: relative; z-index: 1; }
[data-hold] .holdfill {
  position: absolute; inset: 0; z-index: 0;
  background: var(--accent);
  transform: scaleX(0); transform-origin: 0 50%;
  border-radius: inherit;
}
[data-hold].is-holding { cursor: progress; }

/* Narration. The caret only exists while something is being typed. */
[data-narrate].typing::after {
  content: ''; display: inline-block;
  width: 2px; height: 1em; margin-left: 2px;
  background: var(--accent); vertical-align: -2px;
  animation: caret 1s steps(2) infinite;
}
@keyframes caret { 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .arow.earned::after { animation: none; opacity: 0; }
  [data-hold] .holdfill { display: none; }
  [data-narrate].typing::after { animation: none; }
}
