/* ═══════════════════════════════════════════════════════════════════════════
   MOTION - the transition layer, shared by every page on the site
   ═══════════════════════════════════════════════════════════════════════════

   The thirty-two-transition vocabulary from transitions.dev, installed as a
   set of `t-*` component classes. Every rule reads from the motion tokens in
   styles/tokens.css - no raw durations, no raw easing curves live in this file.

   Motion is the one vocabulary the marketing page and the app share outright.
   Type, spacing and density are deliberately separate - an editorial page and
   a dense product are different things - but a dialog that grows at one speed
   on the landing page and another in the app is just two different products.
   They used to be two copies of these rules in two files, kept in step by
   hand. Now there is one.

   Three rules keep this consistent:
     1. A surface opts in by wearing the `t-*` class. Never re-declare a
        transition on a component class; add the `t-*` class to its markup.
     2. Tune in the token block below, never at the call site.
     3. Orchestration (the .is-open / .is-closing / append-and-remove dance)
        lives in modules/motion.js. Nothing else should hand-roll it.

   The one thing that does NOT animate on this site is a page arriving. No
   fade-and-slide-up on a section coming into view, no hover lift on a card.
   Motion answers something a person did - opening, expanding, confirming - and
   shows what changed. Everything below is that.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Semantic per-transition tokens. Only the transitions this site installs
     are listed; each is the tuned default from the transitions.dev snippet it
     belongs to. Tune HERE, never at the call site. */
  --modal-open-dur: var(--duration-fast);
  --modal-close-dur: var(--duration-quick);
  --modal-scale: var(--scale-large);
  --modal-scale-close: var(--scale-large);
  --modal-ease: var(--ease-smooth-out);

  --dropdown-open-dur: var(--duration-fast);
  --dropdown-close-dur: var(--duration-quick);
  --dropdown-pre-scale: var(--scale-medium);
  --dropdown-ease: var(--ease-smooth-out);

  --panel-open-dur: var(--duration-slow);
  --panel-close-dur: var(--duration-medium);
  --panel-translate-y: 100px;
  --panel-blur: var(--blur-small);
  --panel-ease: var(--ease-smooth-out);

  --page-slide-dur: var(--duration-fast);
  --page-fade-dur: var(--duration-fast);
  --page-slide-distance: var(--distance-base);
  --page-blur: var(--blur-medium);
  --page-exit-dur: var(--duration-quick);
  --page-slide-ease: var(--ease-smooth-out);
  --page-fade-ease: var(--ease-smooth-out);

  --toast-open: var(--duration-medium);
  --toast-close: var(--duration-fast);
  --toast-distance: 16px;
  --toast-blur: var(--blur-small);
  --toast-scale: var(--scale-medium);
  --toast-ease: var(--ease-smooth-out);

  --tt-in-dur: var(--duration-quick);
  --tt-out-dur: 50ms;
  --tt-scale: var(--scale-small);
  --tt-delay: var(--duration-micro);
  --tt-in-ease: var(--ease-out);
  --tt-out-ease: var(--ease-out);

  --acc-expand: var(--duration-fast);
  --acc-collapse: var(--duration-fast);
  --acc-chevron: var(--duration-fast);
  --acc-ease: var(--ease-smooth-out);

  --stagger-dur: var(--duration-very-slow);
  --stagger-distance: var(--distance-medium);
  --stagger-stagger: var(--duration-stagger);
  --stagger-blur: var(--blur-medium);
  --stagger-ease: var(--ease-smooth-out);

  --icon-swap-dur: var(--duration-fast);
  --icon-swap-blur: var(--blur-small);
  --icon-swap-start-scale: 0.25;
  --icon-swap-ease: var(--ease-in-out);

  --badge-slide-dur: 260ms;
  --badge-pop-dur: var(--duration-very-slow);
  --badge-pop-close-dur: 180ms;
  --badge-fade-dur: var(--duration-slow);
  --badge-fade-close-dur: 180ms;
  --badge-blur: var(--blur-small);
  --badge-offset-x: -8.2px;
  --badge-offset-y: 12.4px;
  --badge-slide-ease: var(--ease-smooth-out);
  --badge-pop-ease: var(--ease-bounce);
  --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1);

  --avatar-lift: -4px;
  --avatar-dur: 320ms;
  --avatar-scale: 1.05;
  --avatar-falloff: 0.45;
  --avatar-ease-in: var(--ease-smooth-out);
  --avatar-ease-out: var(--ease-bounce-strong);

  --shake-distance: var(--distance-small);
  --shake-overshoot: var(--distance-micro);
  --shake-dur-a: var(--duration-micro);
  --shake-dur-b: 60ms;
  --shake-ease: var(--ease-smooth-out);
  --revert-hold: 3000ms;
  --revert-dur: 280ms;

  --pulse-dur: 1000ms;

  --shimmer-dur: 2000ms;
  --shimmer-band: 400%;
  --shimmer-ease: var(--ease-linear);

  --tabs-dur: var(--duration-fast);
  --tabs-ease: var(--ease-smooth-out);

  --toggle-dur: var(--duration-medium);
  --toggle-ov1: 1px;
  --toggle-ov2: 0px;
  --toggle-track: var(--duration-quick);
  --toggle-ease: cubic-bezier(0.34, 1.35, 0.64, 1);

  --resize-dur: 300ms;
  --resize-ease: var(--ease-smooth-out);


  /* Backdrop scrim. Not a transitions.dev snippet - the scrim behind a modal
     is this app's own surface - but it runs on the modal's clocks so the two
     layers are never out of step. */
  --scrim-open-dur: var(--duration-fast);
  --scrim-close-dur: var(--duration-quick);
  --scrim-ease: var(--ease-smooth-out);
}

/* ══════════════════════════════════════════════════════════════════════════
   MOTION LAYER  (t-*)

   The thirty-two-transition vocabulary from transitions.dev, installed as a
   set of `t-*` component classes. Every rule reads from the motion tokens in
   :root - no raw durations, no raw easing curves live below this line.

   Three rules keep this consistent:
     1. A surface opts in by wearing the `t-*` class. Never re-declare a
        transition on a component class; add the `t-*` class to its markup.
     2. Tune in :root, never here and never at the call site.
     3. Orchestration (the .is-open / .is-closing / append-and-remove dance)
        lives in modules/motion.js. Nothing else should hand-roll it.

   See DESIGN.md > Motion for the token usage table and the wiring recipes.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Scrim: the dimmed backdrop under a modal, panel or sheet ──────────────
   Not a transitions.dev snippet - the scrim is this app's own surface - but it
   runs on the modal's clocks so the two layers are never out of step. Any
   backdrop-filter the component declares is faded in with it. */
.t-scrim {
  opacity: 0;
  transition: opacity var(--scrim-close-dur) var(--scrim-ease);
  will-change: opacity;
}
.t-scrim.is-open { opacity: 1; transition: opacity var(--scrim-open-dur) var(--scrim-ease); }
.t-scrim.is-closing { opacity: 0; }

/* ── Modal open / close ──
   Modal dialogs and full-overlay surfaces that scale up from center. */
.t-modal {
  transform-origin: center;
  transform: scale(var(--modal-scale));
  opacity: 0;
  pointer-events: none;
  transition:
    transform var(--modal-open-dur) var(--modal-ease),
    opacity   var(--modal-open-dur) var(--modal-ease);
  will-change: transform, opacity;
}
.t-modal.is-open {
  transform: scale(1);
  opacity: 1;
  pointer-events: auto;
}
.t-modal.is-closing {
  transform: scale(var(--modal-scale-close));
  opacity: 0;
  pointer-events: none;
  transition:
    transform var(--modal-close-dur) var(--modal-ease),
    opacity   var(--modal-close-dur) var(--modal-ease);
}

/* ── Menu dropdown ──
   Contextual menus, popovers, pickers - anything that opens from a trigger and
   should look like it grew out of it. `data-origin` picks the growth corner.

   One mechanism for all of them, keyed off the `hidden` attribute. Half this
   app's popovers are pre-rendered and toggled with `el.hidden = true/false`;
   the other half are built per use and go through openPopover()/closePopover(),
   which set the same attribute rather than a second set of state classes -
   there is no point in two rule sets that look identical on screen. The UA's
   `display: none` cannot transition, so this overrides it and animates
   visibility instead. Only safe on an absolutely/fixed-positioned surface:
   while "hidden" it is still laid out, just invisible and non-interactive.
   `visibility` is stepped at the end of the close so the surface leaves the tab
   order and the a11y tree.

   Two things make the closed state stay laid out. The global
   `[hidden] { display: none !important }` near the top of this file exempts
   `.t-pop`, and the `:where()` rule below supplies a fallback `display` for a
   surface that does not set its own. `:where()` contributes no specificity, so
   that fallback is (0,0,0) and every component rule - `.version-dropdown`,
   `.filter-dropdown-menu`, whatever comes next - keeps its own `display`
   without needing to know this class exists. */
:where(.t-pop) { display: block; }
.t-pop {
  transform-origin: top right;
  transform: scale(var(--dropdown-pre-scale));
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    transform  var(--dropdown-close-dur) var(--dropdown-ease),
    opacity    var(--dropdown-close-dur) var(--dropdown-ease),
    visibility 0s linear var(--dropdown-close-dur);
  will-change: transform, opacity;
}
.t-pop[data-origin="top-left"]      { transform-origin: top left; }
.t-pop[data-origin="top-center"]    { transform-origin: top center; }
.t-pop[data-origin="bottom-left"]   { transform-origin: bottom left; }
.t-pop[data-origin="bottom-center"] { transform-origin: bottom center; }
.t-pop[data-origin="bottom-right"]  { transform-origin: bottom right; }
.t-pop:not([hidden]) {
  transform: scale(1);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition:
    transform  var(--dropdown-open-dur) var(--dropdown-ease),
    opacity    var(--dropdown-open-dur) var(--dropdown-ease),
    visibility 0s linear 0s;
}

/* ── Panel reveal ──
   A panel sliding into a region with a cross-blur. `--x` is the horizontal
   sibling for edge drawers: same snippet, same clocks, rotated 90 degrees. */
.t-panel-slide {
  transform: translateY(var(--panel-translate-y));
  opacity: 0;
  filter: blur(var(--panel-blur));
  pointer-events: none;
  transition:
    transform var(--panel-close-dur) var(--panel-ease),
    opacity   var(--panel-close-dur) var(--panel-ease),
    filter    var(--panel-close-dur) var(--panel-ease);
  will-change: transform, opacity, filter;
}
.t-panel-slide[data-open="true"],
.t-panel-slide.is-open {
  transform: translateY(0);
  opacity: 1;
  filter: blur(0);
  pointer-events: auto;
  transition:
    transform var(--panel-open-dur) var(--panel-ease),
    opacity   var(--panel-open-dur) var(--panel-ease),
    filter    var(--panel-open-dur) var(--panel-ease);
}
/* ── Page side-by-side ──
   The router's view transition. One container, not two, so the outgoing view
   leaves on the close clock and the incoming one arrives on the open clock.
   `data-nav` carries the direction the hash moved: going deeper into the
   hierarchy (home -> project -> track) slides left, coming back slides right,
   and a sideways move cross-fades with no travel.

   The page slide is normally symmetric - 250ms both ways - because the two pages
   coexist and overlap. Here they cannot: one container means the exit finishes
   before the next view is written into it, so a symmetric pair would cost 500ms
   of dead time on every navigation. The exit therefore runs on --page-exit-dur
   (the quick clock) and the entrance keeps the full 250ms, which is the one place
   this layer departs from its snippet on purpose.

   The resting rule declares the transition and NOTHING else. It must not carry
   a `transform`, a `filter` or a `will-change`, not even an identity one: each
   of those makes the element a containing block for its `position: fixed`
   descendants, and this element is the whole app view. A permanent `blur(0)`
   here would silently re-anchor the comment FAB and every fixed overlay a view
   renders to the container instead of the viewport. The animating properties
   therefore live only on the two state classes, and the rest state lets them
   compute back to `none`. */
.t-page {
  transition:
    opacity   var(--page-fade-dur)  var(--page-fade-ease),
    transform var(--page-slide-dur) var(--page-slide-ease),
    filter    var(--page-slide-dur) var(--page-slide-ease);
}
.t-page[data-nav="forward"] { --t-page-from-x: var(--page-slide-distance); }
.t-page[data-nav="back"]    { --t-page-from-x: calc(var(--page-slide-distance) * -1); }
.t-page[data-nav="none"]    { --t-page-from-x: 0px; }

/* Entering: start offset on the side it came from, with no transition, so the
   run-in animates from there rather than from wherever the last view left. */
.t-page.is-entering {
  opacity: 0;
  transform: translateX(var(--t-page-from-x, 0px));
  filter: blur(var(--page-blur));
  will-change: opacity, transform, filter;
  transition: none;
}
/* Leaving: travel the other way on the faster close clock. */
.t-page.is-leaving {
  opacity: 0;
  transform: translateX(calc(var(--t-page-from-x, 0px) * -1));
  filter: blur(var(--page-blur));
  will-change: opacity, transform, filter;
  transition:
    opacity   var(--page-exit-dur) var(--page-fade-ease),
    transform var(--page-exit-dur) var(--page-slide-ease),
    filter    var(--page-exit-dur) var(--page-slide-ease);
}

/* ── Toast open / close ──
   Rises from below with fade + cross-blur; slower in than out. */
.t-toast {
  opacity: 0;
  transform: translateX(-50%) translateY(var(--toast-distance)) scale(var(--toast-scale));
  filter: blur(var(--toast-blur));
  will-change: transform, opacity, filter;
  transition:
    opacity   var(--toast-close) var(--toast-ease),
    transform var(--toast-close) var(--toast-ease),
    filter    var(--toast-close) var(--toast-ease);
}
.t-toast.is-open {
  opacity: 1;
  transform: translateX(-50%) translateY(0) scale(1);
  filter: blur(0);
  transition:
    opacity   var(--toast-open) var(--toast-ease),
    transform var(--toast-open) var(--toast-ease),
    filter    var(--toast-open) var(--toast-ease);
}

/* ── Tooltip open / close ──
   Delayed fade + scale in, instant out. The app's tooltips are pre-rendered
   inside their trigger and positioned by CSS or by a one-off measure, so this
   installs the appear/disappear half of the snippet only: the delay lives on
   the shown rule alone, which is what makes leaving snap. */
.t-tt {
  /* `scale`, not `transform`: several triggers position their bubble with a
     transform of their own (`translateX(-50%)` to centre it), and a transform
     here would replace it outright. The independent longhand composes. */
  scale: var(--tt-scale);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity    var(--tt-out-dur) var(--tt-out-ease),
    scale      var(--tt-out-dur) var(--tt-out-ease),
    visibility 0s linear var(--tt-out-dur);
}
.t-tt-trigger:hover > .t-tt,
.t-tt-trigger:focus-visible > .t-tt,
.t-tt-trigger.is-open > .t-tt,
.t-tt[data-show="true"] {
  opacity: 1;
  visibility: visible;
  scale: 1;
  transition:
    opacity    var(--tt-in-dur) var(--tt-in-ease) var(--tt-delay),
    scale      var(--tt-in-dur) var(--tt-in-ease) var(--tt-delay),
    visibility 0s linear 0s;
}

/* ── Accordion expand ──
   Height animates via grid-template-rows 0fr <-> 1fr, so nothing measures
   anything in JS and content of any size animates cleanly.

   State is `.is-collapsed` on the `.t-acc` element - the convention four
   collapsible surfaces in this app already used, and the reason they were all
   normalised onto it rather than onto the upstream snippet's `data-open`:
   `:not(.is-collapsed)` and `[data-open="true"]` cannot share one base class,
   because an element using the second looks open to the first.

   Structure: .t-acc > .t-acc-panel (the grid track) > .t-acc-panel-inner (the
   body, which clips its own overflow). */
.t-acc-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--acc-collapse) var(--acc-ease);
}
.t-acc:not(.is-collapsed) > .t-acc-panel {
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--acc-expand) var(--acc-ease);
}
.t-acc-panel-inner {
  overflow: hidden;
  opacity: 0;
  filter: blur(var(--blur-small));
  transition:
    opacity      var(--acc-collapse) var(--acc-ease),
    filter       var(--acc-collapse) var(--acc-ease),
    padding-block var(--acc-collapse) var(--acc-ease);
}
/* A 0fr track sizes the CONTENT box to zero; it cannot shrink the body's own
   padding, and `overflow: hidden` never clips an element's own padding - on the
   inner or on the track. Measured: a body with 1rem of block padding still
   stands 32px tall fully "closed". So the block padding collapses with the
   height, on the same clock. The selector is deliberately three classes deep so
   it outranks each adopting component's own padding rule without !important. */
.t-acc.is-collapsed > .t-acc-panel > .t-acc-panel-inner {
  padding-block: 0;
  /* Same reasoning for an explicit min-height: the folder body carries
     `min-height: 2rem` so an empty folder still reads as a drop target, and that
     floor would otherwise keep a collapsed folder 32px open. */
  min-height: 0;
}
.t-acc:not(.is-collapsed) > .t-acc-panel > .t-acc-panel-inner {
  opacity: 1;
  filter: blur(0);
  /* Clipping stops once the panel is fully open, so a popover anchored to the
     last row (the tag colour picker, the swatch grid) can still escape. The
     0s/delayed lane flips `overflow` at the end of the expand - the same
     discrete-property trick this file uses for `visibility`. Where a browser
     will not transition `overflow`, it flips at once and content simply shows
     through for the length of the expand; nothing breaks. */
  overflow: visible;
  transition:
    opacity       var(--acc-expand) var(--acc-ease),
    filter        var(--acc-expand) var(--acc-ease),
    padding-block var(--acc-expand) var(--acc-ease),
    overflow      0s linear var(--acc-expand);
}

/* ── Texts reveal, as an N-item list ──
   The texts-reveal snippet generalised from two fixed lines to a list: the
   per-item delay indexes off `--i`, written inline by motion.js. Rows travel the
   shorter --distance-base and run on the panel clock rather than the 500ms text
   clock, because a list of twenty is read as a body of content, not as a
   headline. motion.js caps --i at 7 so the total stagger stays under ~300ms, and
   seeds a container only once - these lists are rebuilt wholesale on every
   Firestore snapshot, so an ungated reveal would replay on every remote write. */
.t-stagger-list > * {
  opacity: 0;
  transform: translateY(var(--distance-base));
  filter: blur(var(--blur-small));
  transition:
    opacity   var(--duration-slow) var(--ease-smooth-out),
    transform var(--duration-slow) var(--ease-smooth-out),
    filter    var(--duration-slow) var(--ease-smooth-out);
  transition-delay: calc(var(--stagger-stagger) * var(--i, 0));
  /* No will-change: this selector matches every row of every list in the app,
     and the reveal plays once per visit. A permanent compositor hint on a few
     hundred idle rows costs memory for nothing. Same reasoning on .t-avatar and
     .t-tt below - the hint is kept only on the handful of singleton surfaces
     (scrim, modal, popover, page, toast, badge). */
}
.t-stagger-list.is-shown > * {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* ── Texts reveal, as a one-shot entrance ──
   Terminal states - an empty list, an error, the first-run banner - arrive once
   and stay. They have no "before" style for a transition to run from, so they
   take the same rise as the list above as a keyframe instead: an animation
   plays the moment the element is inserted, with no JS and nothing to seed. Not
   used for list rows, which ARE re-rendered on every snapshot and would replay
   this on every remote write - those go through revealList(). */
@keyframes t-rise-in {
  from {
    opacity: 0;
    transform: translateY(var(--stagger-distance));
    filter: blur(var(--stagger-blur));
  }
  to {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}
.empty-state,
.error-state,
.comments-empty,
.welcome-banner {
  animation: t-rise-in var(--stagger-dur) var(--stagger-ease) both;
}

/* ── Shimmer text ──
   Sweeps a highlight band across muted text on a loop. Pure CSS. The base
   text renders normally; ::before duplicates it from data-text, paints a
   transparent -> highlight -> transparent gradient and clips that to the
   glyphs, then animates background-position to sweep it. */
.t-shimmer {
  position: relative;
  display: inline-block;
  color: var(--text-muted);
}
.t-shimmer::before {
  /* The empty alt text after the slash keeps the duplicated glyphs out of the
     accessibility tree, so a screen reader reads the label once rather than
     twice. Browsers without content-alt support just announce it twice, as
     before. */
  content: attr(data-text) / "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(
    90deg,
    transparent   0%,
    transparent  40%,
    var(--text)  50%,
    transparent  60%,
    transparent 100%
  );
  background-size: var(--shimmer-band) 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: t-shimmer var(--shimmer-dur) var(--shimmer-ease) infinite;
}
@keyframes t-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0% 0; }
}

/* ── Icon swap ──
   Two icons in one slot, cross-faded with blur and scale. */
.t-icon-swap {
  position: relative;
  display: inline-grid;
  place-items: center;
}
.t-icon-swap .t-icon {
  grid-area: 1 / 1;
  transition:
    opacity   var(--icon-swap-dur) var(--icon-swap-ease),
    filter    var(--icon-swap-dur) var(--icon-swap-ease),
    transform var(--icon-swap-dur) var(--icon-swap-ease);
  will-change: opacity, filter, transform;
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
  opacity: 1;
  filter: blur(0);
  transform: scale(1);
}
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
  opacity: 0;
  filter: blur(var(--icon-swap-blur));
  transform: scale(var(--icon-swap-start-scale));
}

/* ── Notification badge ──
   Slides the badge onto its trigger and pops the dot. The wrapper owns the
   slide, the dot owns the pop - animating the trigger instead would move the
   button the badge is anchored to. Keyed off `hidden` as well as data-open,
   because the bell's count badge is toggled with the attribute. Like .t-pop,
   it relies on the component declaring its own `display`. */
@keyframes t-badge-slide-in {
  from { transform: translate(var(--badge-offset-x), var(--badge-offset-y)); }
  to   { transform: translate(0, 0); }
}
.t-badge {
  transform-origin: center;
  transform: scale(1);
  opacity: 1;
  filter: blur(0);
  visibility: visible;
  transition:
    transform  var(--badge-pop-dur)  var(--badge-pop-ease),
    opacity    var(--badge-fade-dur) var(--badge-pop-ease),
    filter     var(--badge-pop-dur)  var(--badge-pop-ease),
    visibility 0s linear 0s;
  will-change: transform, opacity, filter;
}
.t-badge[hidden] {
  transform: scale(0);
  opacity: 0;
  filter: blur(var(--badge-blur));
  pointer-events: none;
  /* Stepped at the end of the close, like .t-pop: the badge announces a count,
     so once it has popped away it must leave the a11y tree rather than linger
     as an invisible "3". It is also what keeps `hidden` meaning hidden to a
     test runner, which reads `visibility` but not `opacity`. */
  visibility: hidden;
  transition:
    transform  var(--badge-pop-close-dur)  var(--badge-close-ease),
    opacity    var(--badge-fade-close-dur) var(--badge-close-ease),
    filter     var(--badge-pop-close-dur)  var(--badge-close-ease),
    visibility 0s linear var(--badge-pop-close-dur);
}
.t-badge:not([hidden]) { animation: t-badge-slide-in var(--badge-slide-dur) var(--badge-slide-ease); }

/* ── Avatar group hover ──
   Distance-falloff lift across a row of items, with a bouncy spring on the
   return. The timing function is set inline by motion.js BEFORE the --shift
   write, which is what gives ease-out on the way up and the spring on the way
   back without a second state class. */
.t-avatar {
  transform-origin: center;
  /* translateY before scale so the scale does not amplify the lift. */
  transform: translateY(var(--shift, 0px)) scale(var(--scale-active, 1));
  transition: transform var(--avatar-dur) var(--avatar-ease-in);
}

/* ── Error state shake ──
   Per-segment cubic-bezier shake with an auto-reverting border and message.
   .is-error and .is-shaking stay orthogonal so the shake can replay (remove,
   reflow, re-add) without flickering the whole error treatment off and on. */
.t-input { transition: border-color var(--duration-quick) var(--ease-out); }
.t-input.is-error {
  border-color: var(--error);
  transition: border-color var(--revert-dur) var(--ease-out);
}
/* %-stops are cumulative leg durations over the total (80,80,60,60 = 280ms):
   28.57%, 57.14%, 78.57%, 100%. Recompute if a leg duration changes. */
.t-input.is-shaking {
  animation: t-input-shake
    calc(var(--shake-dur-a) * 2 + var(--shake-dur-b) * 2) linear;
}
@keyframes t-input-shake {
  0%     { transform: translateX(0);                                animation-timing-function: var(--shake-ease); }
  28.57% { transform: translateX(var(--shake-distance));            animation-timing-function: var(--shake-ease); }
  57.14% { transform: translateX(calc(var(--shake-distance) * -1)); animation-timing-function: var(--shake-ease); }
  78.57% { transform: translateX(var(--shake-overshoot));           animation-timing-function: var(--shake-ease); }
  100%   { transform: translateX(0); }
}

/* ── Tabs sliding ──
   A pill that travels between the options of a segmented control. The pill
   element is injected by motion.js, which writes its transform + width from
   the active option's offsetLeft / offsetWidth; the first write and every
   resize are made with the transition suspended, or the pill animates in from
   translateX(0) / width 0 on first paint. */
.t-tabs { position: relative; }
.t-tabs > * { position: relative; z-index: 1; }
.t-tabs-pill {
  position: absolute;
  top: var(--tabs-inset, 0px);
  left: 0;
  bottom: var(--tabs-inset, 0px);
  width: 0;
  background: var(--accent-subtle);
  border: 1px solid var(--accent-dim);
  border-radius: inherit;
  transform: translateX(0);
  transition:
    transform var(--tabs-dur) var(--tabs-ease),
    width     var(--tabs-dur) var(--tabs-ease),
    opacity   var(--tabs-dur) var(--tabs-ease);
  will-change: transform, width;
  z-index: 0;
  pointer-events: none;
}
.t-tabs-pill.is-idle { opacity: 0; }
/* Underline variant: the indicator is a bar on the bottom edge rather than a
   pill behind the label. Same travel, same clock. */
.t-tabs--underline .t-tabs-pill {
  top: auto;
  bottom: -1px;
  height: 2px;
  background: var(--accent);
  border: 0;
  border-radius: 0;
}

/* ── Toggle ──
   The switch thumb travels with a double-bounce overshoot: past the end, back,
   settle. Both of the app's switches draw their thumb as ::after, so the
   keyframes run on the pseudo-element. --toggle-travel is set per switch next
   to its own geometry. .is-init is added on first interaction so the "off"
   keyframes cannot play on load. */
@keyframes t-toggle-on {
  0%   { translate: 0 0; }
  55%  { translate: calc(var(--toggle-travel) + var(--toggle-ov1)) 0; }
  80%  { translate: calc(var(--toggle-travel) - var(--toggle-ov2)) 0; }
  100% { translate: var(--toggle-travel) 0; }
}
@keyframes t-toggle-off {
  0%   { translate: var(--toggle-travel) 0; }
  55%  { translate: calc(0px - var(--toggle-ov1)) 0; }
  80%  { translate: var(--toggle-ov2) 0; }
  100% { translate: 0 0; }
}

/* ── Reduced motion ──
   One guard for the whole layer. Users who asked their OS for less motion get
   the end state instantly - everything still opens, closes and reveals, it
   just does not travel. motion.js reads the same media query so its timers
   collapse to zero rather than holding an element in a state nothing will
   animate out of. */
@media (prefers-reduced-motion: reduce) {
  .t-scrim, .t-scrim.is-open, .t-scrim.is-closing,
  .t-modal, .t-modal.is-open, .t-modal.is-closing,
  .t-pop, .t-pop:not([hidden]),
  .t-acc-panel, .t-acc:not(.is-collapsed) > .t-acc-panel,
  .t-acc-panel-inner,
  .t-acc.is-collapsed > .t-acc-panel > .t-acc-panel-inner,
  .t-acc:not(.is-collapsed) > .t-acc-panel > .t-acc-panel-inner,
  .t-stagger-list > *,
  .t-panel-slide, .t-panel-slide.is-open, .t-panel-slide[data-open="true"],
  .t-page, .t-page.is-entering, .t-page.is-leaving,
  .t-toast, .t-toast.is-open,
  .t-tt, .t-tt[data-show="true"],
  .t-icon-swap .t-icon,
  .t-badge, .t-badge[hidden],
  .t-avatar,
  .t-input, .t-input.is-error,
  .t-tabs-pill,
  .glossary-panel, .glossary-panel.is-open,
  .glossary-tip, .device-badge-tooltip,
  .site-auth-menu, .site-auth-menu.is-open {
    transition: none !important;
  }
  .t-page.is-entering, .t-page.is-leaving,
  .t-stagger-list > *,
  .t-toast, .t-avatar {
    transform: none !important;
    filter: none !important;
  }
  /* The transition is off above, so the per-row delay would otherwise be the
     only thing left of the stagger - and it would hold each row at opacity 0
     until its turn came with nothing to fade it in. */
  .t-stagger-list > * {
    transition-delay: 0s !important;
  }
  .t-badge:not([hidden]),
  .t-input.is-shaking,
  .t-shimmer::before,
  .toggle-track::after, .nox-panel .switch::after,
  .empty-state, .error-state, .comments-empty, .welcome-banner {
    animation: none !important;
  }
}
