/* Wrapped in @layer m3e — the layer order is declared as the first style in
   base.html: @layer properties, theme, base, m3e, components, utilities.
   This makes Tailwind utilities (@layer utilities) win over M3E component
   rules at equal specificity, which is the intended semantics. */
@layer m3e {
/* ═══════════════════════════════════════════════════════════════════════════
 * M3E Motion Roles — utility classes that assign the motion tokens to jobs.
 *
 * Loaded AFTER motion.css (which owns the tokens, the global keyframes, and the
 * global prefers-reduced-motion kill-switch). This file ships ONLY role classes;
 * it never redefines a token, never redefines the reduced-motion behaviour, and
 * never introduces a bespoke cubic-bezier or a magic duration. Everything below
 * resolves to `--ease-*` / `--easing-*` and `--duration-*` (colors_and_type.css)
 * or `--md-sys-motion-*` (motion.css).
 *
 * Reference: docs/plans/2026-07-17-refinement-motion-spec.md — "the nine motions".
 *
 * ── What ships, and how a surface uses it (r4) ─────────────────────────────
 *
 *   .m3e-page-crossfade      applied by m3e-motion.js when a navigation swaps
 *                            #page-content. You never write it.
 *   .m3e-stagger             put it on a LIST OR GRID CONTAINER whose children
 *                            appear on first paint. Caps at 8 children so a
 *                            60-row table does not take three seconds.
 *                            First paint and page navigations only — never
 *                            after a filter, a tab swap or an OOB refresh.
 *   .m3e-reveal-actions      on a desktop TABLE ROW whose action cell should
 *                            appear on hover/focus. Never on a phone; a touch
 *                            user cannot hover, so the actions must be visible.
 *   [data-motion-confirm]    put it on the element that just changed state, and
 *                            ONLY in the response to the action that changed
 *                            it (`{% if motion_confirm %}` / `just_marked`) —
 *                            never on a page render, or every visit pops. Use
 *                            it for a badge that flips, a row that gets
 *                            approved. `="surface"` for a card/panel: a small
 *                            scale settle that never starts at opacity 0.
 *   core/_partials/_skeleton.html
 *                            variant="text|list|card|table", count=N. Use it on
 *                            any HTMX region that takes more than ~300ms.
 *                            aria-hidden; put aria-busy on the container.
 *   m3eSnackbar.show({action:{label:'Undo', href|handler}})
 *                            the undo affordance. See docs/design-system/PLATFORM_R4.md.
 *   .m3e-sheet--desktop-dialog
 *                            bottom sheet on a phone, centred dialog on a
 *                            desktop. See the SHEET block at the end of this file.
 *
 * All of it must disappear under prefers-reduced-motion, and all of it does —
 * verify by loading the screen with reduced motion and confirming
 * document.getAnimations() is empty.
 *
 * Reduced motion: NOT re-handled here. motion.css Part 1 sets every
 * animation-duration/transition-duration to ~0 under prefers-reduced-motion, so
 * these classes become inert automatically. The one design rule we honour to
 * stay safe is: never leave content stuck at opacity:0 when the animation is
 * killed. The stagger below is opt-in via a JS-applied class, so its children
 * are visible by default (see "Stagger contract"). The reveal/collapse classes
 * only ever animate opacity between visible end-states or are JS-lifecycle-bound.
 * ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
 * MOTION 1 — Navigation cross-fade  (.m3e-page-crossfade)
 *
 * #page-content owns the CSS `page-enter` animation in motion.css, which fires
 * only on FULL document loads (the element persists across boosted innerHTML
 * swaps). m3e-motion.js applies this class in `htmx:afterSwap` — synchronously,
 * before the new content's first paint — so the content is never painted,
 * hidden and faded back in. Same easing/duration family as page-enter so full
 * loads and boosted swaps feel identical. One entrance per navigation.
 *
 * Two classes, one motion: the JS alternates them so each navigation changes
 * `animation-name`, which restarts the animation with no forced reflow.
 * `--m3e-page-in-from` is the opacity the fade starts at: 0, or the 0.55
 * loading dim when a bottom-nav tap dimmed the page, so the dim flows straight
 * into the entrance instead of snapping up first.
 *
 * Scoped to `#page-content.…` so it out-specifies the base
 * `#page-content { animation: page-enter }` ID rule in motion.css (a bare class
 * would lose to that ID selector and never apply).
 * ═══════════════════════════════════════════════════════════════════════════ */

@keyframes m3e-page-in {
  from { opacity: var(--m3e-page-in-from, 0); transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes m3e-page-in-alt {
  from { opacity: var(--m3e-page-in-from, 0); transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
#page-content.m3e-page-crossfade {
  animation: m3e-page-in var(--md-sys-motion-duration-medium1) var(--easing-effects) backwards;
}
#page-content.m3e-page-crossfade-alt {
  animation: m3e-page-in-alt var(--md-sys-motion-duration-medium1) var(--easing-effects) backwards;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * MOTION 2 — List / grid stagger  (.m3e-stagger)
 *
 * Direct children rise in with --ease-serve, 25ms increments, capped at 8
 * (children 9+ appear instantly), animation-fill-mode: backwards.
 *
 * ── Stagger contract (dashboard agents: read this) ──────────────────────────
 * Add `.m3e-stagger` to the CONTAINER of a list/grid. Its DIRECT children
 * animate in ONCE, the first time the container appears. That's it — no other
 * markup or JS needed.
 *
 * ANTI-RETRIGGER (why refreshes don't dance):
 *   The rise animation is gated behind `.m3e-stagger--enter`, which the motion
 *   module (m3e-motion.js) adds to a container once and then removes. The
 *   module only looks for containers on first paint and on a NAVIGATION swap
 *   of #page-content. Consequences:
 *     • First paint / full page load           → children stagger once.     ✔
 *     • Boosted navigation to another page      → children stagger once.     ✔
 *     • Any other HTMX swap — a filter or view tab that replaces the whole
 *       container with outerHTML, an OOB section refresh, a row swap, a
 *       same-URL reload of the page           → nothing re-enters.          ✔
 *   So a filter never makes the list dance, even though it delivers a brand-new
 *   container element.
 *
 * DEFAULT STATE is fully visible: children carry no opacity:0 until the JS adds
 * `.m3e-stagger--enter`. So with JS disabled, or under reduced motion (the JS
 * skips adding the class), the content is present and complete — never blank.
 * ═══════════════════════════════════════════════════════════════════════════ */

@keyframes m3e-stagger-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.m3e-stagger.m3e-stagger--enter > * {
  animation: m3e-stagger-rise var(--duration-quick) var(--ease-serve) backwards;
}

/* 25ms increments, capped at 8 children (child 1 = 0ms … child 8 = 175ms). */
.m3e-stagger.m3e-stagger--enter > *:nth-child(1) { animation-delay: 0ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(2) { animation-delay: 25ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(3) { animation-delay: 50ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(4) { animation-delay: 75ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(5) { animation-delay: 100ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(6) { animation-delay: 125ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(7) { animation-delay: 150ms; }
.m3e-stagger.m3e-stagger--enter > *:nth-child(8) { animation-delay: 175ms; }

/* Children 9+ appear instantly (no rise, no cascade). */
.m3e-stagger.m3e-stagger--enter > *:nth-child(n+9) { animation: none; }


/* ═══════════════════════════════════════════════════════════════════════════
 * MOTION 4 — Row actions reveal  (.m3e-reveal-actions)
 *
 * A container whose action cluster (`.m3e-reveal-actions__actions`) sits at
 * opacity 0 and fades in on hover / focus-within in 150ms with --easing-effects.
 * The actions are ALWAYS in the DOM and remain focusable — keyboard Tab into
 * them triggers :focus-within, which reveals them. On touch (no hover), they
 * are shown permanently so they are never unreachable.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-reveal-actions__actions {
  opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short3) var(--easing-effects);
}
.m3e-reveal-actions:focus-within .m3e-reveal-actions__actions {
  opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
  .m3e-reveal-actions:hover .m3e-reveal-actions__actions { opacity: 1; }
}

/* Coarse pointers (touch) can't hover — keep the actions visible so they are
 * never stranded off-screen for touch users. */
@media (hover: none) {
  .m3e-reveal-actions__actions { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * MOTION 7 — State confirmation
 *
 * .m3e-confirm-pop   — one-shot badge-pop on a row/badge after a successful
 *                      HTMX action (applied by m3e-motion.js on [data-motion-
 *                      confirm], removed on animationend so it can re-fire).
 * .m3e-confirm-pop--surface
 *                    — the same confirmation for a card or panel
 *                      ([data-motion-confirm="surface"]): a 2% scale settle,
 *                      fully opaque throughout, so a large surface never
 *                      blanks for a frame or swells past its box.
 * .m3e-collapse-out  — decisive removal: opacity + translateY out with
 *                      --ease-drop, held (forwards) until the swap removes it.
 *                      Transform/opacity only — no height collapse.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-confirm-pop {
  animation: badge-pop var(--duration-med) var(--easing-effects) 1;
}
@keyframes m3e-confirm-surface {
  from { transform: scale(0.98); }
  to   { transform: scale(1); }
}
.m3e-confirm-pop--surface {
  animation: m3e-confirm-surface var(--duration-quick) var(--easing-effects) 1;
}

@keyframes m3e-collapse-out {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-4px); }
}
.m3e-collapse-out {
  animation: m3e-collapse-out var(--duration-quick) var(--ease-drop) forwards;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * MOTION 5 — Skeleton helpers (backing core/_partials/_skeleton.html)
 *
 * The base `.m3e-skeleton` (+ --text/--title/--card/--circle) and the
 * `m3e-shimmer` keyframe live in components.css and are reused as-is; reduced
 * motion already stops the shimmer there. These add only the pieces the
 * parameterised partial needs on top of that base:
 *   .m3e-skeleton--row    a table-row-height bar
 *   .m3e-skeleton-block   a vertical rhythm wrapper for a group of bars
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-skeleton--row {
  height: 44px;
  border-radius: var(--md-sys-shape-corner-small);
  margin-bottom: var(--md-sys-spacing-2);
}
.m3e-skeleton-block {
  display: flex;
  flex-direction: column;
  gap: var(--md-sys-spacing-2);
}

/* Skeleton-as-hx-indicator: collapsed (no layout footprint) until HTMX marks
 * the element `.htmx-request` for an in-flight swap, then it shows and fades in.
 * Unlike the default opacity-only `.htmx-indicator`, this reclaims its space
 * when idle so a full-height skeleton never leaves a gap. */
.m3e-skeleton-indicator { display: none; }
.m3e-skeleton-indicator.htmx-request {
  display: block;
  animation: menu-backdrop-in var(--md-sys-motion-duration-short3) var(--easing-effects);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * SHEET — one component, two form factors  (r4 platform contract)
 *
 * A confirm/booking flow that is a bottom sheet on a phone should be a centred
 * dialog on a desktop, not a 1440px-wide slab pinned to the bottom edge. Add
 * `m3e-sheet--desktop-dialog` to the sheet element and it does both:
 *
 *   {% include "m3e/components/bottom_sheet.html" with sheet_id="confirm-book"
 *      variant="modal" title="Confirm booking"
 *      content_template="core/_partials/_book_confirm.html"
 *      extra_classes="m3e-sheet--desktop-dialog" %}
 *
 *   <button data-m3e-trigger="confirm-book-scrim" data-m3e-action="open">Book</button>
 *
 * OPENER CONTRACT (unchanged, this is the house contract for every overlay):
 *   data-m3e-trigger="<sheet_id>-scrim"   the scrim element's id
 *   data-m3e-action="open" | "close" | "toggle"
 * m3e-components.js section 1 handles open/close, portals the scrim to <body>
 * so no ancestor transform can break position:fixed, traps focus, closes on
 * Escape and on scrim click, and restores focus to the trigger.
 *
 * Motion: below 840px the sheet uses `sheet-slide-up` (motion.css) — 300ms,
 * spatial easing, transform only. At and above 840px it fades and rises 8px
 * like a dialog. Both collapse to nothing under prefers-reduced-motion via the
 * global kill-switch in motion.css; neither ever ends at opacity 0.
 * ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 840px) {
  .m3e-bottom-sheet.m3e-sheet--desktop-dialog {
    position: relative;
    inset: auto;
    width: min(560px, calc(100vw - 48px));
    max-height: min(80vh, 720px);
    margin: auto;
    border-radius: var(--md-sys-shape-corner-extra-large);
    animation: m3e-sheet-dialog-in var(--md-sys-motion-duration-medium2)
               var(--md-sys-motion-easing-expressive-spatial-default);
  }
  /* Centre the dialog. MUST stay scoped to [data-m3e-open]: the base rule is
     `.m3e-bottom-sheet-scrim { display: none }`, so an unscoped `display: flex`
     here would leave every sheet permanently open and its scrim swallowing
     every click on the page. */
  .m3e-bottom-sheet-scrim[data-m3e-open]:has(> .m3e-sheet--desktop-dialog) {
    align-items: center;
    justify-content: center;
  }
  /* The drag handle is a touch affordance; a pointer user gets the close
     button and the scrim. */
  .m3e-sheet--desktop-dialog .m3e-bottom-sheet__drag-handle-wrapper {
    display: none;
  }
}

@keyframes m3e-sheet-dialog-in {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

}
