/* ============================================================================
   Transitions de l'application — code OFFICIEL de transitions.dev.
   ----------------------------------------------------------------------------
   Ces 15 blocs sont repris TELS QUELS depuis transitions.dev (onglet CSS de
   chaque effet) : mêmes sélecteurs, mêmes noms de variables, mêmes durées et
   mêmes courbes. Aucune retouche, pour que le rendu soit celui de la preview
   du site.

   Conséquence à connaître : chaque bloc apporte son propre `:root` de
   variables (--badge-*, --check-*, --toggle-*, …). C'est voulu — c'est ce qui
   permet de régler un effet sans toucher au reste, et de re-synchroniser avec
   le site en remplaçant un bloc entier.

   Les 4 effets manquants (Gooey plus menu, Image open tilt, Pro gradient text,
   Delete with smoky dissolve) sont réservés aux abonnés Pro du site.

   Déclencheurs JS : voir transitions.js.
   ============================================================================ */


/* ---------------------------------------------------------------- Error state shake */
/* Transitions.dev — Error state shake */

/*
  Usage:
    <!-- Apply .t-input-wrap to your wrapper, .t-input to the
         element that should shake (your input field, its
         bordered wrapper — whatever owns the visible border),
         and .t-error-msg to the message you want to reveal.
         Bring your own sizing, padding, border colors, and
         typography. -->
    <div class="t-input-wrap">
      <div class="t-input">
        <input type="text">
      </div>
      <p class="t-error-msg">Please enter a valid email.</p>
    </div>

  Trigger:
    - Add `.is-error` to .t-input-wrap and .t-input. Your
      own border-color rules drive the visible color; this
      stylesheet only owns the tween.
    - Restart the shake by removing `.is-shaking` from
      .t-input, forcing a reflow, then re-adding it.
    - Optional: after --revert-hold ms, drop both
      `.is-error` classes so border + message fade back
      to neutral over --revert-dur.

  Per-segment ease: each keyframe stop carries its own
  animation-timing-function so each leg follows the Figma
  cubic-bezier curve independently.
*/

:root {
  --shake-distance: 6px;
  --shake-overshoot: 4px;
  --shake-dur-a: 80ms;
  --shake-dur-b: 60ms;
  --shake-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --revert-hold: 3000ms;
  --revert-dur: 280ms;
}

/* Border-color tween. Define your input's default / focused
   / error border-color in your own component CSS — this rule
   only owns the interpolation. Use a constant border-width
   across states so the tween never shifts inner content. */
.t-input {
  transition: border-color 150ms ease-out;
  will-change: transform;
}
.t-input.is-error {
  /* Error border auto-reverts on the hold timer, so the
     fade-out uses the slower revert duration (matches the
     message fade). */
  transition: border-color var(--revert-dur, 280ms) ease-out;
}

/* Error message reveal. Visibility is delayed by --revert-dur
   on hide so the message stays painted for the full opacity
   fade-out. Entering .is-error drops the delay to 0 so the
   message becomes visible immediately. */
.t-error-msg {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity    var(--revert-dur, 280ms) ease-out,
    visibility 0s linear var(--revert-dur, 280ms);
}
.t-input-wrap.is-error .t-error-msg {
  opacity: 1;
  visibility: visible;
  transition:
    opacity    var(--revert-dur, 280ms) ease-out,
    visibility 0s linear 0s;
}

/* Multi-segment keyframe with per-stop easing so each leg
   of the shake follows its own cubic-bezier independently.
   %-stops are cumulative durations as a fraction of the
   total (80, 60, 80, 60 = 280ms): 28.57%, 57.14%, 78.57%,
   100%. Recompute if any segment 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); }
}

@media (prefers-reduced-motion: reduce) {
  .t-input { animation: none !important; transform: none !important; }
}

/* ---------------------------------------------------------------- Text states swap */
/* Transitions.dev — Text states swap */

/*
  Usage:
    <span class="t-text-swap">Processing…</span>

  Driven by JS (three-phase sequence):
    1. Add `.is-exit`  -> old text slides up + blurs + fades.
    2. After --text-swap-dur: change textContent, then add
       `.is-enter-start` (jumps to below, no transition).
    3. Force reflow, remove `.is-enter-start` so the new text
       animates back to 0 with the default transition.
*/

:root {
  --text-swap-dur: 150ms;
  --text-swap-translate-y: 4px;
  --text-swap-blur: 2px;
  --text-swap-ease: ease-in-out;
}

.t-text-swap {
  display: inline-block;
  transform: translateY(0);
  filter: blur(0);
  opacity: 1;
  transition:
    transform var(--text-swap-dur) var(--text-swap-ease),
    filter    var(--text-swap-dur) var(--text-swap-ease),
    opacity   var(--text-swap-dur) var(--text-swap-ease);
  will-change: transform, filter, opacity;
}
.t-text-swap.is-exit {
  transform: translateY(calc(var(--text-swap-translate-y) * -1));
  filter: blur(var(--text-swap-blur));
  opacity: 0;
}
.t-text-swap.is-enter-start {
  transform: translateY(var(--text-swap-translate-y));
  filter: blur(var(--text-swap-blur));
  opacity: 0;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .t-text-swap { transition: none !important; }
}

/* ---------------------------------------------------------------- Success check */
/* Transitions.dev — Success check */

/*
  Usage:
    <!-- Wrap your icon (SVG, image, anything) in .t-success-check.
         The wrapper drives fade + rotate + blur + Y-bob; if your
         icon is an SVG <path>, it gets the stroke-draw animation.
         Bring your own icon size / colors. -->
    <span class="t-success-check" data-state="out" aria-hidden="true">
      <svg viewBox="0 0 48 48" fill="none">
        <!-- your icon path(s) here -->
      </svg>
    </span>

  Trigger:
    - Cold load is data-state="out" (opacity 0; no animation).
    - Show: set data-state="in" (fade + rotate + blur + Y-bob
      + path draw run in parallel).

  Snippet covers the appear transition only — bring your own
  hide behavior (e.g. unmount, opacity:0, or a custom exit).
*/

:root {
  --check-opacity-dur: 500ms;
  --check-rotate-dur: 500ms;
  --check-rotate-from: 80deg;
  --check-bob-dur: 500ms;
  --check-y-amount: 40px;
  --check-blur-dur: 500ms;
  --check-blur-from: 10px;
  --check-path-dur: 500ms;
  --check-path-delay: 80ms;
  --check-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1);
  --check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
  --check-ease-path: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Wrapper drives the appear animation; it doesn't own any
   sizing or color so you can drop in any icon. */
.t-success-check {
  display: inline-block;
  transform-origin: center;
  opacity: 0;
  will-change: transform, opacity, filter;
}
/* overflow: visible keeps the stroke from clipping while it
   draws; display: block kills the inline whitespace under SVGs. */
.t-success-check svg { display: block; overflow: visible; }
/* Stroke-draw setup. Replace 20 with the result of
   path.getTotalLength() for your path; round caps mean any
   sub-pixel overshoot is invisible. */
.t-success-check svg path {
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
}

.t-success-check[data-state="in"] {
  animation:
    t-check-fade   var(--check-opacity-dur) var(--check-ease-opacity) forwards,
    t-check-rotate var(--check-rotate-dur)  var(--check-ease-rotate)  forwards,
    t-check-blur   var(--check-blur-dur)    var(--check-ease-out)     forwards,
    t-check-bob    var(--check-bob-dur)     var(--check-ease-bob)     forwards;
}
.t-success-check[data-state="in"] svg path {
  animation: t-check-draw var(--check-path-dur) var(--check-ease-path) var(--check-path-delay, 0ms) forwards;
}

@keyframes t-check-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes t-check-rotate {
  from { transform: rotate(var(--check-rotate-from)); }
  to   { transform: rotate(0deg); }
}
@keyframes t-check-blur {
  from { filter: blur(var(--check-blur-from)); }
  to   { filter: blur(0); }
}
@keyframes t-check-bob {
  from { translate: 0 var(--check-y-amount); }
  to   { translate: 0 0; }
}
@keyframes t-check-draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  .t-success-check { animation: none !important; opacity: 1; }
  .t-success-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
}

/* ---------------------------------------------------------------- Toggle */
/* Transitions.dev — Toggle */

/*
  Usage:
    <button class="t-toggle" role="switch" data-on="false">
      <span class="t-toggle-thumb"></span>
    </button>

  Toggle `data-on`. Add `.is-init` on first interaction so the
  "off" keyframes don't play on load. The thumb travels with a
  double bounce (overshoot past the end, swing back, settle);
  the track colour cross-fades on its own clock.
*/

:root {
  --toggle-dur: 350ms;
  --toggle-travel: 14.66px;
  --toggle-ov1: 1px;
  --toggle-ov2: 0px;
  --toggle-track: 0ms;
  --toggle-ease: cubic-bezier(0.34, 1.35, 0.64, 1);
}

.t-toggle { transition: background var(--toggle-track) var(--toggle-ease); }
.t-toggle-thumb { translate: 0 0; will-change: translate; }
.t-toggle[data-on="true"] .t-toggle-thumb { translate: var(--toggle-travel) 0; }
.t-toggle.is-init[data-on="true"] .t-toggle-thumb { animation: t-toggle-on var(--toggle-dur) var(--toggle-ease) both; }
.t-toggle.is-init[data-on="false"] .t-toggle-thumb { animation: t-toggle-off var(--toggle-dur) var(--toggle-ease) both; }
@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; }
}

@media (prefers-reduced-motion: reduce) {
  .t-toggle-thumb { animation: none !important; }
}

/* ---------------------------------------------------------------- Tabs sliding */
/* Transitions.dev — Tabs sliding */

/*
  Usage:
    <div class="t-tabs" role="tablist">
      <span class="t-tabs-pill" aria-hidden="true"></span>
      <button class="t-tab" role="tab" aria-selected="true">Plan</button>
      <button class="t-tab" role="tab" aria-selected="false">Debug</button>
      <button class="t-tab" role="tab" aria-selected="false">Ask</button>
    </div>

  Wire-up:
    On click, flip aria-selected on each tab and write the
    active tab's offsetLeft / offsetWidth onto the pill:
      pill.style.transform = `translateX(${tab.offsetLeft}px)`;
      pill.style.width     = `${tab.offsetWidth}px`;
    On first paint and resize, write the same values WITHOUT
    a transition (suspend with `transition: none`, force a
    reflow, restore) so the pill snaps to position before any
    animation can run.
*/

:root {
  --tabs-dur: 250ms;
  --tabs-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --tabs-text-muted: rgba(193, 193, 193, 0.8);
  --tabs-text-active: #ffffff;
  --tabs-bar-bg: #202020;
  --tabs-pill-bg: #454545;
}

/* The bar is just a flex container with padding for the pill
   to sit inside. Tabs sit on z-index: 1, the pill on z-index: 0,
   so labels read above the pill background. */
.t-tabs {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 3px;
  border-radius: 48px;
  background: var(--tabs-bar-bg);
}
.t-tab {
  position: relative;
  appearance: none;
  border: 0;
  background: transparent;
  height: 30px;
  padding: 4px 12px;
  color: var(--tabs-text-muted);
  cursor: pointer;
  border-radius: 48px;
  z-index: 1;
  transition: color var(--tabs-dur) var(--tabs-ease);
}
.t-tab:not([aria-selected="true"]):hover,
.t-tab[aria-selected="true"] {
  color: var(--tabs-text-active);
}

/* The pill: width + transform are written inline by JS so
   the transition tweens between the previous and next
   measured positions. */
.t-tabs-pill {
  position: absolute;
  top: 3px;
  left: 0;
  height: 30px;
  width: 0;
  background: var(--tabs-pill-bg);
  border-radius: 48px;
  transform: translateX(0);
  transition:
    transform var(--tabs-dur) var(--tabs-ease),
    width     var(--tabs-dur) var(--tabs-ease);
  will-change: transform, width;
  z-index: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .t-tabs-pill, .t-tab { transition: none !important; }
}

/* ---------------------------------------------------------------- Skeleton loader and reveal */
/* Transitions.dev — Skeleton loader and reveal */

/*
  Usage:
    <div class="t-skel" data-state="loading">
      <div class="t-skel-skeleton is-pulsing">…</div>
      <div class="t-skel-content">…</div>
    </div>

  State:
    - Mount with `.is-pulsing` on the skeleton so it pulses
      --pulse-count times.
    - When data arrives, add `.is-revealed` to .t-skel — the
      skeleton fades out + blurs and the content fades in +
      un-blurs over --reveal-dur.
    - To replay the loading state without animating the
      reverse: add `.is-resetting` to .t-skel, remove
      `.is-revealed`, force a reflow, then drop `.is-resetting`.

  Bring your own avatar / text / wrapping. The skeleton stays
  in the same flex slot as the content so the swap is
  layout-free.
*/

:root {
  --pulse-dur: 1000ms;
  --pulse-count: 1;
  --pulse-min: 0.5;
  --reveal-dur: 400ms;
  --reveal-blur: 2px;
  --reveal-ease: ease-in-out;
}

/* The wrap stacks two layers on the same coordinates. The
   skeleton owns the cold pulse + the fade-out side of the
   reveal; the content owns the fade-in side. They share the
   same duration / ease so the swap reads as one motion. */
.t-skel { position: relative; }
.t-skel-skeleton,
.t-skel-content {
  position: absolute;
  inset: 0;
}

.t-skel-skeleton {
  z-index: 1;
  opacity: 1;
  filter: blur(0);
  transition:
    opacity var(--reveal-dur) var(--reveal-ease),
    filter  var(--reveal-dur) var(--reveal-ease);
}
.t-skel-content {
  z-index: 2;
  opacity: 0;
  filter: blur(var(--reveal-blur));
  transition:
    opacity var(--reveal-dur) var(--reveal-ease),
    filter  var(--reveal-dur) var(--reveal-ease);
}
.t-skel.is-revealed .t-skel-skeleton {
  opacity: 0;
  filter: blur(var(--reveal-blur));
}
.t-skel.is-revealed .t-skel-content {
  opacity: 1;
  filter: blur(0);
}
/* Snap-back when replaying: kill transitions so the reverse
   (revealed → skeleton) is instant. Drop `.is-resetting`
   after a forced reflow and the next reveal animates again. */
.t-skel.is-resetting .t-skel-skeleton,
.t-skel.is-resetting .t-skel-content {
  transition: none !important;
}

/* Pulse: place the animation on the bar/avatar children, not
   on the skeleton itself, so the skeleton's opacity / filter
   stay free for the cross-fade transition above. */
.t-skel-skeleton.is-pulsing > * {
  animation: t-skel-pulse var(--pulse-dur) ease-in-out var(--pulse-count);
}
@keyframes t-skel-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: var(--pulse-min); }
}

@media (prefers-reduced-motion: reduce) {
  .t-skel-skeleton, .t-skel-content {
    transition: none !important;
  }
  .t-skel-skeleton.is-pulsing > * { animation: none !important; }
}

/* ---------------------------------------------------------------- Notification badge */
/* Transitions.dev — Notification badge */

/*
  Usage:
    <!-- Place .t-badge inside your trigger (bell icon, button, etc.). -->
    <!-- The trigger must be position: relative so .t-badge can anchor to it. -->
    <button class="your-trigger" style="position: relative">
      <!-- your icon / trigger contents -->
      <span class="t-badge" data-open="false">
        <span class="t-badge-dot">1</span>
      </span>
    </button>

  State: toggle data-open="true" / "false" on .t-badge.
  Only the badge slides + pops — the trigger itself stays put.
*/

:root {
  --badge-slide-dur: 260ms;
  --badge-pop-dur: 500ms;
  --badge-pop-close-dur: 180ms;
  --badge-fade-dur: 400ms;
  --badge-fade-close-dur: 180ms;
  --badge-blur: 2px;
  --badge-offset-x: -8.2px;
  --badge-offset-y: 12.4px;
  --badge-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --badge-pop-ease: cubic-bezier(0.34, 1.36, 0.64, 1);
  --badge-close-ease: cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes t-badge-slide-in {
  from { transform: translate(var(--badge-offset-x), var(--badge-offset-y)); }
  to   { transform: translate(0, 0); }
}

/* .t-badge is the absolutely-positioned wrapper for the dot.
   Adjust top/right (or left/bottom) to anchor it on your trigger. */
.t-badge {
  position: absolute;
  top: -6px;
  right: -8px;
  pointer-events: none;
  will-change: transform;
}
.t-badge[data-open="true"] {
  animation: t-badge-slide-in var(--badge-slide-dur) var(--badge-slide-ease);
}

.t-badge-dot {
  display: block;
  transform-origin: center;
  transform: scale(1);
  opacity: 1;
  filter: blur(0);
  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);
  will-change: transform, opacity, filter;
}
.t-badge[data-open="false"] .t-badge-dot {
  transform: scale(0);
  opacity: 0;
  filter: blur(var(--badge-blur));
  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);
}

@media (prefers-reduced-motion: reduce) {
  .t-badge, .t-badge-dot { animation: none !important; transition: none !important; }
}

/* ---------------------------------------------------------------- Tooltip open/close */
/* Transitions.dev — Tooltip open/close */

/*
  Usage:
    <span class="t-tt-wrap">
      <button class="t-tt-trigger" aria-describedby="tt-1">…</button>
      <span class="t-tt" id="tt-1" role="tooltip">Tooltip text</span>
    </span>

  Pure CSS — no JS. The wrap (not the trigger) is the hover
  target so the pointer can drift onto the tooltip without
  flicker. transition-delay only applies in the hover/focus
  rule, so leaving snaps it to 0 and the disappear plays
  immediately. Trigger styling is yours; only the tooltip
  positioning + its transition live here.
*/

:root {
  --tt-in-dur: 150ms;
  --tt-out-dur: 50ms;
  --tt-scale: 0.98;
  --tt-delay: 80ms;
  --tt-in-ease: ease-out;
  --tt-out-ease: ease-out;
  --tt-bg: #222222;
  --tt-fg: #f0f0f0;
}

.t-tt-wrap {
  position: relative;
  display: inline-block;
}
.t-tt {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translate(-50%, 0) scale(var(--tt-scale));
  transform-origin: 50% 100%;
  padding: 8px 12px;
  border-radius: 8px;
  background: var(--tt-bg);
  color: var(--tt-fg);
  white-space: nowrap;
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 2px 6px 0 rgba(0, 0, 0, 0.05),
    0 4px 42px 0 rgba(0, 0, 0, 0.06);
  opacity: 0;
  pointer-events: none;
  /* Default rule controls the LEAVE state. transition-delay
     stays unset so leaving plays without delay. */
  transition:
    opacity   var(--tt-out-dur) var(--tt-out-ease),
    transform var(--tt-out-dur) var(--tt-out-ease);
}
/* The 50ms delay belongs ONLY to the hover rule so leaving
   the trigger snaps the delay back to 0 and the disappear
   plays immediately. */
.t-tt-wrap:hover .t-tt,
.t-tt-trigger:focus-visible + .t-tt {
  opacity: 1;
  transform: translate(-50%, 0) scale(1);
  transition-duration: var(--tt-in-dur);
  transition-timing-function: var(--tt-in-ease);
  transition-delay: var(--tt-delay);
}

@media (prefers-reduced-motion: reduce) {
  .t-tt { transition: none !important; }
}

/* ---------------------------------------------------------------- Icon swap */
/* Transitions.dev — Icon swap */

/*
  Usage:
    <div class="t-icon-swap" data-state="a">
      <span class="t-icon" data-icon="a">…</span>
      <span class="t-icon" data-icon="b">…</span>
    </div>

  State: set data-state="a" or "b" on .t-icon-swap.
  The matching .t-icon fades in; the other fades out with blur
  and scale.
*/

:root {
  --icon-swap-dur: 250ms;
  --icon-swap-blur: 2px;
  --icon-swap-start-scale: 0.25;
  --icon-swap-ease: ease-in-out;
}

.t-icon-swap {
  position: relative;
  display: inline-grid;
}
.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));
}

@media (prefers-reduced-motion: reduce) {
  .t-icon-swap .t-icon { transition: none !important; }
}

/* ---------------------------------------------------------------- Checkbox check */
/* Transitions.dev — Checkbox check */

/*
  Usage:
    <button class="t-check" role="checkbox" aria-checked="false">
      <svg viewBox="0 0 10.1668 10.1668">
        <path d="M1 5.52L3.92 9.17L9.17 1"/>
      </svg>
    </button>

  Toggle `aria-checked`. The box fills, then the check stroke
  draws in via stroke-dashoffset. Set --check-len to the path's
  getTotalLength() (rounded up) so it never over/under-draws;
  transitioning offset lets a mid-draw uncheck reverse cleanly.
*/

:root {
  --check-box: 150ms;
  --check-draw: 350ms;
  --check-delay: 0ms;
  --check-uncheck: 150ms;
  --check-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-check {
  transition:
    background var(--check-box) var(--check-ease),
    box-shadow var(--check-box) var(--check-ease);
}
.t-check svg path {
  stroke-dasharray: var(--check-len, 15);
  stroke-dashoffset: var(--check-len, 15);
  transition: stroke-dashoffset var(--check-uncheck) var(--check-ease);
}
.t-check[aria-checked="true"] svg path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--check-draw) var(--check-ease) var(--check-delay);
}

@media (prefers-reduced-motion: reduce) {
  .t-check, .t-check svg path { transition: none !important; }
}

/* ---------------------------------------------------------------- Texts reveal */
/* Transitions.dev — Texts reveal */

/*
  Usage:
    <div class="t-stagger">
      <strong class="t-stagger-line t-stagger-line--1">…</strong>
      <span class="t-stagger-line t-stagger-line--2">…</span>
    </div>

  State:
    - Add `.is-shown` to play the staggered entrance.
    - Add `.is-hiding` (and remove `.is-shown`) to fade
      out in place over a short 200ms — independent of the
      entrance timing so the exit doesn't replay the stagger.

  Add more lines by adding `.t-stagger-line--N` with
  `transition-delay: calc(var(--stagger-stagger) * (N - 1))`.
*/

:root {
  --stagger-dur: 500ms;
  --stagger-distance: 12px;
  --stagger-stagger: 40ms;
  --stagger-blur: 3px;
  --stagger-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Lines start translated down + blurred + invisible; .is-shown
   on the parent flips them to their resting state. The second
   line's transition-delay holds it back by --stagger-stagger
   so the eye lands on the headline first. */
.t-stagger-line {
  display: block;
  opacity: 0;
  transform: translateY(var(--stagger-distance));
  filter: blur(var(--stagger-blur));
  transition:
    opacity   var(--stagger-dur) var(--stagger-ease),
    transform var(--stagger-dur) var(--stagger-ease),
    filter    var(--stagger-dur) var(--stagger-ease);
  will-change: transform, opacity, filter;
}
.t-stagger-line--2 { transition-delay: var(--stagger-stagger); }

.t-stagger.is-shown .t-stagger-line {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}
/* Exit decouples from the stagger: same fade for every line,
   no Y return, no blur — so the disappearance reads as a
   single quiet fade instead of a reverse reveal. */
.t-stagger.is-hiding .t-stagger-line {
  opacity: 0;
  transform: translateY(0);
  filter: blur(0);
  transition:
    opacity 200ms ease,
    transform 0s linear,
    filter 0s linear;
  transition-delay: 0s;
}

@media (prefers-reduced-motion: reduce) {
  .t-stagger-line { transition: none !important; }
}

/* ---------------------------------------------------------------- Avatar group hover */
/* Transitions.dev — Avatar group hover */

/*
  Usage:
    <!-- Apply .t-avatar to each item in your group (avatar,
         chip, badge, button — anything). Bring your own size,
         shape, and stacking; this stylesheet only owns the
         hover transform + transition. -->
    <div class="t-avatar-group">
      <div class="t-avatar"><!-- your item --></div>
      <div class="t-avatar"><!-- your item --></div>
      <!-- … -->
    </div>

  Wire-up (vanilla JS):
    On `mouseenter` of any .t-avatar, walk every sibling and
    set inline:
      el.style.setProperty('--shift',
        (lift * Math.pow(falloff, distance)).toFixed(3) + 'px');
      el.style.setProperty('--scale-active',
        i === activeIdx ? scale : 1);
    Set transition-timing-function inline BEFORE the
    variable writes — use --avatar-ease-in on hover-in and
    --avatar-ease-out on the root's `mouseleave` (resets
    --shift to 0 and --scale-active to 1).
*/

:root {
  --avatar-lift: -4px;
  --avatar-dur: 320ms;
  --avatar-scale: 1.05;
  --avatar-falloff: 0.45;
  --avatar-ease-in: cubic-bezier(0.22, 1, 0.36, 1);
  --avatar-ease-out: cubic-bezier(0.34, 3.85, 0.64, 1);
}

/* Hover-spring transition only — bring your own avatar/chip
   styling (size, shape, border, stacking, background). */
.t-avatar {
  transform-origin: center;
  /* translateY before scale so scale doesn't amplify the lift offset. */
  transform:
    translateY(var(--shift, 0px))
    scale(var(--scale-active, 1));
  transition: transform var(--avatar-dur) var(--avatar-ease-in);
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .t-avatar { transition: none !important; transform: none !important; }
}

/* ---------------------------------------------------------------- Like button */
/* Transitions.dev — Like button */

/*
  Usage:
    <button class="t-like" data-liked="false">
      <span class="t-like-icon"><svg class="t-like-heart">…</svg></span>
      <span class="t-like-particles"><i></i>…8 total…<i></i></span>
      Like
    </button>

  Toggle `data-liked` to fill the heart + spring-pop it. Add
  `.is-bursting` (then remove it after the particle duration)
  to fire the burst; set each dot's --px/--py/--pdur/--pdelay/
  --p-end-scale/--psize in JS per like for an organic spray.
*/

:root {
  --like-color: #f40051;
  --like-fill: 150ms;
  --like-pop: 350ms;
  --like-pop-ease: cubic-bezier(0.34, 1.96, 0.64, 1);
  --like-particle-dur: 600ms;
  --like-particle-dist: 20px;
  --like-particle-size: 2.5px;
  --like-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-like-heart { color: currentColor; transition: color var(--like-fill) var(--like-ease); }
.t-like-heart path {
  fill: transparent; stroke: currentColor;
  transition: fill var(--like-fill) var(--like-ease), stroke var(--like-fill) var(--like-ease);
}
.t-like[data-liked="true"] .t-like-heart { color: var(--like-color); }
.t-like[data-liked="true"] .t-like-heart path { fill: currentColor; }
/* Pop scale lives on an HTML wrapper, never the <svg> itself:
   transforming an inline SVG makes Chromium rasterise it at 1x
   (pixelated on hi-DPI). Wrapping keeps the vector crisp. */
.t-like[data-liked="true"] .t-like-icon { animation: t-like-pop var(--like-pop) var(--like-pop-ease); }
@keyframes t-like-pop { 0% { transform: scale(1); } 30% { transform: scale(0.82); } 100% { transform: scale(1); } }

/* Particle burst — 8 dots flung along per-particle vectors. */
.t-like-particles { position: absolute; left: 50%; top: 50%; width: 0; height: 0; pointer-events: none; color: var(--like-color); }
.t-like-particles i {
  position: absolute;
  left: calc(var(--like-particle-size) * var(--psize, 1) / -2);
  top: calc(var(--like-particle-size) * var(--psize, 1) / -2);
  width: calc(var(--like-particle-size) * var(--psize, 1));
  height: calc(var(--like-particle-size) * var(--psize, 1));
  border-radius: 50%; background: currentColor; opacity: 0;
}
@keyframes t-like-burst {
  0%   { opacity: 0; transform: translate(0, 0) scale(0.4); }
  20%  { opacity: 1; transform: translate(calc(var(--px) * 0.25), calc(var(--py) * 0.25)) scale(1); }
  100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(var(--p-end-scale, 0.6)); }
}
.t-like.is-bursting .t-like-particles i {
  animation: t-like-burst var(--pdur, var(--like-particle-dur)) ease-out var(--pdelay, 0ms) forwards;
}

@media (prefers-reduced-motion: reduce) {
  .t-like-icon, .t-like-particles i { animation: none !important; }
}

/* ---------------------------------------------------------------- Learn more hover */
/* Transitions.dev — Learn more hover */

/*
  Usage:
    <button class="t-learn">Learn more
      <span class="t-learn-chevron"><svg>
        <path class="t-learn-arm t-learn-arm-top" d="M6 4L10 8"/>
        <path class="t-learn-arm t-learn-arm-bot" d="M10 8L6 12"/>
      </svg></span>
    </button>

  On hover the chevron shifts right and its two arms rotate
  apart about the apex (10, 8) so the angle opens; hover-out
  returns. Pure CSS.
*/

:root {
  --learn-shift: 2px;
  --learn-spread: 8deg;
  --learn-in: 350ms;
  --learn-out: 350ms;
  --learn-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-learn-chevron {
  display: inline-flex;
  transform: translateX(0);
  transition: transform var(--learn-out) var(--learn-ease);
}
.t-learn-arm {
  transform-box: view-box;
  transform-origin: 10px 8px;
  vector-effect: non-scaling-stroke;
  transition: transform var(--learn-out) var(--learn-ease);
}
.t-learn:hover .t-learn-chevron { transform: translateX(var(--learn-shift)); transition-duration: var(--learn-in); }
.t-learn:hover .t-learn-arm { transition-duration: var(--learn-in); }
.t-learn:hover .t-learn-arm-top { transform: rotate(var(--learn-spread)); }
.t-learn:hover .t-learn-arm-bot { transform: rotate(calc(var(--learn-spread) * -1)); }

@media (prefers-reduced-motion: reduce) {
  .t-learn-chevron, .t-learn-arm { transition: none !important; }
}

/* ---------------------------------------------------------------- Input clear with dissolve */
/* Transitions.dev — Input clear with dissolve */

/*
  Usage:
    <!-- Drop .t-clear on a wrapper that you've sized like an
         input field (positioning, padding, border, radius are
         yours). Inside it stack a real <input>, a mirror that
         visualizes the value, a fake placeholder for the new
         empty state, and a glow layer that gets the per-word
         radial-gradient stack written into it from JS. -->
    <div class="t-clear has-value">
      <input type="text" value="…" />
      <div class="t-clear-mirror" aria-hidden="true">…</div>
      <div class="t-clear-placeholder" aria-hidden="true">Search</div>
      <div class="t-clear-glow" aria-hidden="true"></div>
      <button class="t-clear-btn" aria-label="Clear">…</button>
    </div>

  Pair with a small JS routine (mirrors the gallery code) that
  flips `.is-clearing`, animates the mirror's translateY/opacity
  per frame, mirrors the math on the placeholder, and writes a
  stack of `radial-gradient(...)` layers onto .t-clear-glow's
  background so each word gets its own streak. Per-frame JS is
  unavoidable: the streak's rise/peak/fall envelope cannot be
  expressed as a static @keyframe.
*/

:root {
  --clear-dur: 1000ms;
  --clear-out-dur: 400ms;
  --clear-in-dur: 400ms;
  --clear-out-fly: 12px;
  --clear-in-fly: 12px;
  --clear-out-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --clear-in-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --clear-blur: 2px;
  --glow-delay: 50ms;
  --glow-peak-at: 0.15;
  --glow-opacity: 0.85;
  --glow-spread: 1.5;
}

/* The wrap clips the glow to its rounded box. The hairline
   border is `inset` so it sits inside that clip — when the
   glow's mix-blend-mode darkens its area, the border
   underneath darkens with it. Bring your own width / height /
   border-radius / surface color. */
.t-clear {
  position: relative;
  overflow: hidden;
}
.t-clear-mirror,
.t-clear-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  z-index: 2;
}
.t-clear-mirror { opacity: 0; }
.t-clear.has-value .t-clear-mirror,
.t-clear.is-clearing .t-clear-mirror { opacity: 1; }
/* Hide the input's own glyphs while the mirror owns them so
   the cleared text doesn't double-render with the fly-up. */
.t-clear.has-value > input,
.t-clear.is-clearing > input {
  -webkit-text-fill-color: transparent;
}
.t-clear.has-value .t-clear-placeholder { opacity: 0; }
/* The streak overlay: empty by default; JS writes a stack of
   `radial-gradient(...)` layers into background during a clear,
   then animates opacity. mix-blend-mode: multiply darkens the
   underlying input + hairline; flip to `screen` in dark mode
   so the same alpha values lighten instead of vanish. */
.t-clear-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  z-index: 3;
  mix-blend-mode: multiply;
}

/* The transitions live in JS (per-frame transform/opacity/
   filter writes), so this stylesheet only owns the resting
   state + the variables that JS reads. Read them with
   `parseFloat(getComputedStyle(root).getPropertyValue(...))`
   so live tweaks apply on the next clear without a reload. */

@media (prefers-reduced-motion: reduce) {
  .t-clear-glow { opacity: 0 !important; }
}
