/* ============================================================
   motion-standard.css — THE MOTION STANDARD.
   Linked LAST from layouts/app.php and layouts/dashboard.php (after
   primunity.css, nav-offset.css, card-align.css, site-fixes.css).

   THE RULE THIS SHEET EXISTS TO ENFORCE
   -------------------------------------
   Every interactive element must FADE between its states. Never a pop,
   never a snap, never a step. Three mistakes the codebase kept making:

     1. The transition was declared inside `:hover` / `:focus` / `.active`
        instead of on the BASE rule, so the state faded IN and snapped OUT
        (the hover rule stops matching the instant the pointer leaves, and
        with it the transition disappears).
        -> ALWAYS declare `transition` on the BASE rule. State rules only
           change VALUES, never timing.

     2. The property list was incomplete. `.btn` transitioned color,
        background-color, border-color and box-shadow — but `.btn--primary:hover`
        moves `background-position`, so the gradient sweep snapped. Same story
        for opacity/transform/filter/gap across the app.
        -> ALWAYS list EVERY property a state rule touches, with the SAME
           duration + easing, so IN and OUT are mirror images.

     3. A state swapped one non-interpolable value for another
        (`background: <gradient>`, or `display:none` on an icon). Gradients and
        display do not interpolate — they snap, whatever the transition says.
        -> Cross-fade a separate gradient LAYER (an ::after at opacity 0/1),
           and cross-fade stacked icons with opacity instead of display.

   HOW TO EXTEND IT
   ----------------
   Add the selector to the base list in section 2 (it inherits `var(--ms-t)`,
   the canonical property list, automatically). Only write a bespoke rule when
   the component animates something outside the standard list, and then keep
   the SAME duration + easing in both directions.

   HARD BANS
   ---------
     * no `transition: 0s` / `steps()` on an interactive state;
     * no `display:none` <-> `display:block` swap for anything that changes
       while the page is alive (use opacity + a stacking cell, or the
       .is-closing timing layer in overlay-fx.css);
     * no scale-from-0 / pop-in entrances — enter from opacity 0 (+ at most a
       few px of travel), never from nothing.

   SPECIFICITY POLICY (why the rules look the way they do)
   -------------------------------------------------------
   Page-scoped stylesheets (posts.css, scheduling.css, sitebuilder.css, …) are
   <link>ed AFTER this file, and turbo's syncPageStyles() appends more of them
   at RUNTIME — so being last in the <head> is NOT enough. Same convention as
   overlay-fx.css:
     * base rules are written `html :is(…)` = (0,1,1) — one notch above the
       single-class page rule they must beat, still low enough that any
       deliberate two-class page rule wins;
     * rules that must out-specify a component's own STATE rule (e.g.
       `[data-theme="dark"] .nav-item.active`, (0,3,0)) double the class:
       `html .nav-item.nav-item.active` = (0,3,1).
   Do NOT use @layer here — unlayered page rules would beat any layer.

   REDUCED MOTION
   --------------
   modules/motion.css deliberately KEEPS transitions under reduced-motion (a
   blanket `transition:none` made modals, accordions and hovers snap — the exact
   bug this sheet exists to kill). We follow that: section 11 REDEFINES the
   `--ms-*` tokens so colour/opacity still cross-fade while every TRANSLATION,
   SCALE and layout animation is dropped, and content swaps become instant.
   Because every rule below consumes the tokens, that one block retunes the
   whole sheet.
   ============================================================ */


/* ------------------------------------------------------------
   1. TOKENS — the single source of truth.
   `--ms-t` is a full `transition` shorthand VALUE, so a component only ever
   writes `transition: var(--ms-t)` and can never get the list wrong.
   ------------------------------------------------------------ */
:root{
  --ms-dur-fast: var(--dur-fast, 130ms);
  --ms-dur:      var(--dur, 200ms);
  --ms-dur-slow: var(--dur-slow, 320ms);
  --ms-ease:     var(--ease, cubic-bezier(.4,0,.2,1));
  --ms-ease-out: var(--ease-out-expo, cubic-bezier(.16,1,.3,1));

  /* Content-swap timing, consumed by core/fade-swap.js (it reads this token
     off :root, so retuning the standard retunes every swapped region). */
  --ms-swap-dur: 170ms;

  /* THE PROPERTY LIST. Every property any interactive state in this codebase
     changes, each at the SAME duration + easing so hover-in mirrors hover-out.
     Deliberately excluded: width/height/inset (layout thrash), background-image
     and display (not interpolable — cross-faded via a layer instead). */
  --ms-t:
    color                 var(--ms-dur) var(--ms-ease),
    background-color      var(--ms-dur) var(--ms-ease),
    background-position   var(--ms-dur) var(--ms-ease),
    background-size       var(--ms-dur) var(--ms-ease),
    border-color          var(--ms-dur) var(--ms-ease),
    box-shadow            var(--ms-dur) var(--ms-ease),
    outline-color         var(--ms-dur) var(--ms-ease),
    outline-offset        var(--ms-dur) var(--ms-ease),
    text-decoration-color var(--ms-dur) var(--ms-ease),
    opacity               var(--ms-dur) var(--ms-ease),
    filter                var(--ms-dur) var(--ms-ease),
    transform             var(--ms-dur) var(--ms-ease),
    gap                   var(--ms-dur) var(--ms-ease);

  /* Overlays: same idea plus the two properties only they use. `visibility` is
     listed on purpose — it steps at the END of the fade, which is what keeps a
     closing panel painted for the whole out-transition. */
  --ms-t-overlay:
    opacity         var(--ms-dur) var(--ms-ease),
    visibility      var(--ms-dur) var(--ms-ease),
    transform       var(--ms-dur) var(--ms-ease-out),
    box-shadow      var(--ms-dur) var(--ms-ease),
    filter          var(--ms-dur) var(--ms-ease),
    backdrop-filter var(--ms-dur) var(--ms-ease);

  /* Big surfaces (modal + drawer panels) breathe at the slow tempo. */
  --ms-t-panel:
    opacity    var(--ms-dur-slow) var(--ms-ease),
    visibility var(--ms-dur-slow) var(--ms-ease),
    transform  var(--ms-dur-slow) var(--ms-ease-out),
    box-shadow var(--ms-dur-slow) var(--ms-ease);

  /* Cross-fade layer timing (gradient states, stacked icons). */
  --ms-t-xfade: opacity var(--ms-dur) var(--ms-ease);

  /* Theme-correct fill for the sidebar's active nav pill. Mirrors
     layout.css so the cross-fade layer is pixel-identical to the instant fill;
     keep the two in sync if layout.css ever changes.
     --brand-grad-soft is theme-resolved, so ONE value now covers both themes.
     The two dark overrides that used to follow hardcoded
     `linear-gradient(120deg,rgba(56,120,255,.60),rgba(30,150,250,.42))` to match
     layout.css's dark .nav-item.active override — that override is gone (the pill
     is tokenised now), so keeping them would have made the cross-fade layer paint
     a different fill than the pill it is supposed to be cross-fading. */
     UPDATE: layout.css's rebuilt pill fills with a calm 12% --brand-fill wash, so
     that is the value this cross-fade layer must paint — matching it here makes the
     ::after the SINGLE source of the fill. layout.css previously ALSO set its own
     background-color, so both painted and the active pill came out ~2.5x louder
     than either rule claimed. Now: one fill, and because it lives on the ::after it
     actually cross-fades on turbo's live .active toggle instead of popping. */
  --ms-nav-fill: color-mix(in srgb, var(--brand-fill) 12%, transparent);
}


/* ------------------------------------------------------------
   2. THE BASE RULE — every interactive element, one declaration.
   `:is()` takes the specificity of its heaviest argument (a class), so the
   whole selector is (0,1,1): it beats the single-class page rules that ship
   an incomplete list, and loses to any deliberate two-class page rule.
   NOTE these are BASE rules. Nothing below re-declares `transition` inside a
   :hover/:focus/.active — that is the mistake the standard bans.
   ------------------------------------------------------------ */
html :is(
  /* generic interactive tags */
  a, button, summary, label, input, select, textarea,
  /* buttons + all modifiers (the modifier lives on the same element) */
  .btn, .icon-btn, .avatar-btn, .btn-icon,
  /* pills */
  .chip, .chip__x, .badge, .tag, .pill,
  /* surfaces */
  .card, .panel, .tile, .stat, .stat__icon, .alert, .empty__icon,
  /* form controls */
  .input, .textarea, select.input, .check, .switch, .dropzone,
  .pmu-dropdown__trigger, .pmu-dropdown__item, .pmu-dp__trigger, .pmu-dp__nav,
  /* navigation */
  .nav-item, .nav__link, .nav-link, .menu__item, .tab, .tabs__tab,
  .sidebar__brand, .breadcrumb a, .pager a, .link,
  /* toggles */
  .theme-toggle, .lang-switch__opt, .sb-langswitch__opt,
  .segmented button, .seg button,
  /* date + slot pickers (booking, scheduling, calendars) */
  .slot-btn, .slot-btn__rem, .cal-slot, .pmu-dp__day, .stepper__step, .stepper__dot,
  /* opt-in hook for anything not listed above */
  [data-hover], [data-motion]
){
  transition: var(--ms-t);
}

/* Pseudo-element layers that carry part of a hover state must fade on the same
   clock as their host, or the ring/glow lags the colour. */
html :is(.card, .panel, .btn, .chip, .nav-item, .slot-btn)::after,
html :is(.card, .panel, .btn, .chip, .nav-item, .slot-btn)::before{
  transition: var(--ms-t-xfade);
}

/* CARD HOVER — one clock for all three layers.
   components.css hardcodes `border-color/box-shadow 180ms var(--ease)` on .card
   while its ::after brand ring fades on --ms-dur (200ms) with --ms-ease. Two
   different durations AND two different easings on the same gesture is why the
   card hover "doesn't feel like it fades properly": the outline finishes ~20ms
   before the glow and they drift apart mid-curve.
   Doubling the class (0,3,1) beats both components.css and any per-page sheet
   linked after this file, so the host and its layers stay locked together. */
html .card.card, html .panel.panel{
  transition: var(--ms-t);
}
html .card.card::after, html .card.card::before,
html .panel.panel::after, html .panel.panel::before{
  transition: var(--ms-t-xfade);
}
/* The tab underline wipes AND fades — never a bare scale snap. */
html .tab::after,
html .tabs__tab::after{
  transition: transform var(--ms-dur) var(--ms-ease-out),
              opacity   var(--ms-dur) var(--ms-ease);
}
/* Guarded against every "selected tab" convention in the codebase so an
   underline can never be hidden on a tab that IS selected. */
html .tab:not(.active):not(.is-active):not([aria-selected="true"])::after,
html .tabs__tab:not(.active):not(.is-active):not([aria-selected="true"])::after{ opacity: 0; }


/* ------------------------------------------------------------
   3. LINKS + ICONS
   Underlines fade via text-decoration-color (already in --ms-t); an icon that
   is recoloured or rotated by its parent's state fades with the parent.
   ------------------------------------------------------------ */
html :is(a, .btn, .nav-item, .menu__item, .chip, .tab, .card) .icon{
  transition: color var(--ms-dur) var(--ms-ease),
              opacity var(--ms-dur) var(--ms-ease),
              transform var(--ms-dur) var(--ms-ease-out);
}
/* `text-decoration: underline` appears instantly; fade the colour instead so
   .btn--link and inline links reveal their underline smoothly. */
html .btn--link{ text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px; }
html .btn--link:hover{ text-decoration-color: currentColor; }


/* ------------------------------------------------------------
   4. FOCUS RINGS
   `outline-style` is not animatable, so a `outline: 2px solid X` focus rule
   always pops. Give the standard controls a TRANSPARENT outline at rest: the
   focus rule then only changes `outline-color`, which IS animatable (and is in
   --ms-t). Box-shadow rings (var(--focus-ring)) already fade via --ms-t.
   Prefer box-shadow rings for new components.
   ------------------------------------------------------------ */
html :is(.btn, .chip, .input, .textarea, select.input, .nav-item, .tab,
         .theme-toggle, .icon-btn, .avatar-btn, .slot-btn, .pmu-dp__day,
         .lang-switch__opt, .sb-langswitch__opt, .segmented button, .seg button){
  outline: 2px solid transparent;
  outline-offset: 2px;
}
/* The colour half — and the a11y safety net for the rule above. A component
   that already owns its focus look declares `:focus-visible{outline:none; …}`,
   which sets outline-STYLE:none; this rule only sets outline-COLOR, so those
   components are untouched and keep their box-shadow ring. Components that had
   NO focus rule at all (.btn is the big one — it relied on the UA ring, which
   the transparent outline above would otherwise have erased) get this ring
   instead, and it fades in and out like everything else. */
html :is(.btn, .chip, .nav-item, .tab, .theme-toggle, .icon-btn, .avatar-btn,
         .slot-btn, .lang-switch__opt, .sb-langswitch__opt):focus-visible{
  outline-color: var(--focus-line);
}
/* Forced-colors users get the real UA ring back — a transparent outline is
   meaningless there. */
@media (forced-colors: active){
  html :is(.btn, .chip, .input, .textarea, .nav-item, .tab, .slot-btn){ outline: revert; }
}


/* ------------------------------------------------------------
   5. GRADIENT STATES — the cross-fade layer.
   `background-image` does NOT interpolate: every state that swaps a solid fill
   for a brand gradient snapped. Fix: paint the gradient on an ::after layer
   that lives at opacity 0 and fades to 1 with the state, and neutralise the
   original background-image. `isolation:isolate` + `z-index:-1` puts the layer
   above the element's own background and BELOW its text/icons (CSS paints
   negative-z children before inline content).
   ------------------------------------------------------------ */

/* -- booking hour picker (.btn.btn--ghost.slot-btn, booking.js) -------------- */
html .slot-btn.slot-btn::after{
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  background: var(--brand-grad); pointer-events: none;
  opacity: 0; transition: var(--ms-t-xfade);
}
html .slot-btn.slot-btn.is-selected{ background-image: none; }
html .slot-btn.slot-btn.is-selected::after{ opacity: 1; }

/* -- dashboard topbar language toggle (layout.css) --------------------------- */
html .lang-switch__opt.lang-switch__opt{ position: relative; isolation: isolate; }
html .lang-switch__opt.lang-switch__opt::after{
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  background: var(--brand-grad); pointer-events: none;
  opacity: 0; transition: var(--ms-t-xfade);
}
html .lang-switch__opt.lang-switch__opt.is-active{ background-image: none; }
html .lang-switch__opt.lang-switch__opt.is-active::after{ opacity: 1; }

/* -- public per-site language toggle (sitebuilder.css) ----------------------- */
html .sb-langswitch__opt.sb-langswitch__opt{ position: relative; isolation: isolate; }
html .sb-langswitch__opt.sb-langswitch__opt::after{
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  background: var(--brand-grad, var(--brand-2)); pointer-events: none;
  opacity: 0; transition: var(--ms-t-xfade);
}
html .sb-langswitch__opt.sb-langswitch__opt.is-active{ background-image: none; }
html .sb-langswitch__opt.sb-langswitch__opt.is-active::after{ opacity: 1; }

/* -- sidebar active pill (layout.css) ---------------------------------------
   turbo toggles `.active` live on every navigation (core/turbo.js
   syncActiveNav), so this pill used to POP on literally every page change.
   ::before is already the accent bar, so the fill layer is ::after. */
html .nav-item.nav-item{ position: relative; isolation: isolate; }
html .nav-item.nav-item::after{
  content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
  background: var(--ms-nav-fill); pointer-events: none;
  opacity: 0; transition: var(--ms-t-xfade);
}
html .nav-item.nav-item.active{ background-image: none; }
html .nav-item.nav-item.active::after{ opacity: 1; }
/* The accent bar fades with the pill instead of appearing fully formed. It has
   to be GENERATED at rest (content:"") — a pseudo-element that only exists in
   the .active rule is born at its final opacity and can never fade. Absolutely
   positioned, so an always-present ::before never becomes a flex item and can
   never open a phantom `gap` in the row. Geometry mirrors layout.css. */
html .nav-item.nav-item::before{
  content: ""; position: absolute;
  inset-inline-start: 0; inset-block: 22%;
  width: 3px; border-radius: 3px;
  /* --brand-grad-fg, NOT --brand-grad: the plain brand gradient deliberately has no
     dark value, so its stops measured 1.006:1 / 1.98:1 / 5.70:1 on the dark pill —
     the accent bar literally faded out of existence in dark mode. --brand-grad-fg is
     the theme-resolved FOREGROUND gradient (7.92 / 10.41 / 13.22:1 in dark). */
  background: var(--brand-grad-fg); pointer-events: none;
  opacity: 0; transition: var(--ms-t-xfade);
}
html .nav-item.nav-item.active::before{ opacity: 1; }

/* -- booking stepper dot ------------------------------------------------------
   INTENTIONALLY NO GRADIENT LAYER HERE.
   This block used to cross-fade a --brand-grad ::after behind the step number
   (the generic "gradients don't interpolate" treatment). That is now WRONG: the
   stepper no longer has a filled bubble at all — no-medallion.css flattens
   .stepper__dot to a bare numeral whose active state is a 2px brand underline.
   With the bubble gone the layer's `border-radius: inherit` resolved to 0, so the
   gradient rendered as a hard BLUE SQUARE behind the numeral.
   The active state is a border-block-end colour change, which is already in the
   standard's property list and fades on the shared clock — nothing extra needed. */


/* ------------------------------------------------------------
   6. THEME TOGGLE — cross-fade the sun/moon instead of display:none.
   layout.css hides the inactive glyph with `display:none`, so flipping the
   theme swapped icons with a hard cut. Stack both glyphs in the button's single
   grid cell and fade between them. Specificity: the dark-theme rules in
   layout.css are (0,3,0); `html[data-theme="dark"] .theme-toggle.theme-toggle .icon-*`
   is (0,4,1), and the display override is (0,3,1).
   ------------------------------------------------------------ */
html .theme-toggle.theme-toggle .icon-sun,
html .theme-toggle.theme-toggle .icon-moon{
  display: block; grid-area: 1 / 1;
  transition: opacity var(--ms-dur) var(--ms-ease),
              transform var(--ms-dur) var(--ms-ease-out);
}
html .theme-toggle.theme-toggle .icon-sun{ opacity: 1; transform: none; }
html .theme-toggle.theme-toggle .icon-moon{ opacity: 0; transform: rotate(-45deg); }
html[data-theme="dark"] .theme-toggle.theme-toggle .icon-sun{ opacity: 0; transform: rotate(45deg); }
html[data-theme="dark"] .theme-toggle.theme-toggle .icon-moon{ opacity: 1; transform: none; }
@media (prefers-color-scheme: dark){
  html:not([data-theme]) .theme-toggle.theme-toggle .icon-sun{ opacity: 0; transform: rotate(45deg); }
  html:not([data-theme]) .theme-toggle.theme-toggle .icon-moon{ opacity: 1; transform: none; }
}


/* ------------------------------------------------------------
   7. OVERLAYS — enter AND exit, same clock in both directions.
   The out-STATE lives in modules/overlay-fx.css (`.is-closing`, applied by
   core/overlay-fx.js while the open class is still on). This block only
   guarantees the BASE transitions are complete, so the reverse of every enter
   is a real fade. Each family KEEPS the tempo it already had (--dur for the
   quick popovers, --dur-slow for the big surfaces) — this is a completeness
   pass, not a retiming pass.
   NOT TOUCHED ON PURPOSE: [data-ofx] (overlay-fx.css owns that recipe) and
   .pmu-dialog__* / .pf-modal__panel, which are deliberately snappier (160/180ms)
   and already fade in BOTH directions.
   ------------------------------------------------------------ */
html :is(.modal, .menu, .pmu-dropdown__panel, .pmu-tooltip, .rest-tip,
         .sidebar-scrim, .sb2-pop){
  transition: var(--ms-t-overlay);
}
html :is(.modal__panel, .drawer__panel, .drawer__backdrop, .toast){
  transition: var(--ms-t-panel);
}
/* A backdrop must fade, never cut: give the shared modal backdrop its own
   opacity track instead of relying purely on the parent's visibility flip. */
html .modal .modal__backdrop{ transition: var(--ms-t-overlay); }


/* ------------------------------------------------------------
   8. CONTENT SWAPS — the CSS half of core/fade-swap.js.
   Use these classes for a declarative swap (the JS helper sets the same
   properties inline so it also works if this sheet is missing).
   NEVER pair a swap with display:none — the element must stay rendered for
   the whole fade.
   ------------------------------------------------------------ */
html .ms-swap{
  transition: opacity var(--ms-swap-dur) var(--ms-ease),
              transform var(--ms-swap-dur) var(--ms-ease-out);
  will-change: opacity;
}
html .ms-swap.is-fading{ opacity: 0; pointer-events: none; }
/* Optional few-px travel. Never a scale-from-0 pop. */
html .ms-swap.ms-swap--rise.is-fading{ transform: translateY(4px); }

/* Skeletons/placeholders shown DURING a swap fade in like everything else. */
html .slots-skeleton,
html .skeleton{ transition: opacity var(--ms-swap-dur) var(--ms-ease); }


/* ------------------------------------------------------------
   9. TAB PANELS + DISCLOSURES
   `.tab-panel{display:none}` cannot fade OUT (nothing can interpolate out of
   display:none) — the enter half is at least a real fade. Modules that need a
   true two-way swap should use core/fade-swap.js on the panel's container.
   ------------------------------------------------------------ */
html .tab-panel.active,
html [data-tab-panel]:not([hidden]){
  animation: pmuFadeUp var(--ms-dur-slow) var(--ms-ease-out);
}


/* ------------------------------------------------------------
   10. ANTI-POP GUARDS
   The ONLY places transform must NOT be transitioned: surfaces whose transform
   is driven per-frame by JS. Without these the standard's transform track would
   fight the drag (a 200ms lag behind the pointer) and break the FLIP reorder.
   ------------------------------------------------------------ */
html .pmu-sortable__ghost.pmu-sortable__ghost{
  transition: box-shadow var(--ms-dur) var(--ms-ease),
              opacity var(--ms-dur) var(--ms-ease),
              border-color var(--ms-dur) var(--ms-ease);
}
html .pmu-sortable__shift.pmu-sortable__shift{
  transition: transform var(--ms-dur-slow) var(--ms-ease-out);
}
html [aria-grabbed="true"]{ transition: opacity var(--ms-dur) var(--ms-ease); }


/* ------------------------------------------------------------
   11. REDUCED MOTION
   Not `transition:none` — that IS the pop we are banning, and motion.css
   deliberately keeps transitions for exactly this reason. Instead retune the
   tokens: colour/opacity still cross-fade (calm, no vestibular trigger), every
   translation/scale/layout animation is dropped, and content swaps are instant.
   One block, whole sheet.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce){
  :root{
    --ms-swap-dur: 0ms;
    --ms-t:
      color                 var(--ms-dur) var(--ms-ease),
      background-color      var(--ms-dur) var(--ms-ease),
      border-color          var(--ms-dur) var(--ms-ease),
      box-shadow            var(--ms-dur) var(--ms-ease),
      outline-color         var(--ms-dur) var(--ms-ease),
      text-decoration-color var(--ms-dur) var(--ms-ease),
      opacity               var(--ms-dur) var(--ms-ease);
    --ms-t-overlay:
      opacity    var(--ms-dur) var(--ms-ease),
      visibility var(--ms-dur) var(--ms-ease);
    --ms-t-panel:
      opacity    var(--ms-dur) var(--ms-ease),
      visibility var(--ms-dur) var(--ms-ease);
  }
  html .ms-swap{ transition: none; }
  html .ms-swap.ms-swap--rise.is-fading{ transform: none; }
  html .theme-toggle.theme-toggle .icon-sun,
  html .theme-toggle.theme-toggle .icon-moon{ transform: none !important; }
  html .tab-panel.active,
  html [data-tab-panel]:not([hidden]){ animation: none; }
  html .pmu-sortable__shift.pmu-sortable__shift{ transition: none; }
}
