/* LUMEN motion system — shared contract, three expressions.
 *
 * Each theme overrides the tokens; the rules below never change. The rules
 * encode the research:
 *   - only transform + opacity animate (composite-only, never layout/paint)
 *   - entries start from scale(.95)/translateY, never from zero
 *   - controls resolve under 200ms; decorative card motion may run longer
 *   - no `transition: all` anywhere — every property is named
 *   - hover is gated behind (hover: hover) so touch never gets stuck states
 *   - prefers-reduced-motion collapses everything to a cross-fade
 *
 * Theme overrides (set in each theme's lumen.css :root):
 *   A "glazed"      --ease: cubic-bezier(.76,0,.24,1);   --card: 300ms
 *   B "ceremonial"  --ease: cubic-bezier(.16,1,.3,1);    --card: 400ms; --reveal: 700ms
 *   C "clinical"    --ease: cubic-bezier(.4,0,.2,1);     --card: 600ms
 */

:root {
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS / Vaul */
  --dur-press: 100ms;
  --dur-control: 150ms;
  --dur-card: 300ms;
  --dur-reveal: 500ms;
  --dur-drawer: 400ms;
  --card-scale: 1.04;
  --reveal-delay: 0ms;
}

/* ---------------------------------------------------------------- reveal
 * Hidden ONLY when JS is live. The `js` class is set by a blocking inline
 * script in <head> (so revealables never flash before being hidden) and is
 * REMOVED again by a watchdog if the engine fails to boot. A dead script
 * therefore leaves a fully visible page, never a blank one.
 *
 * No `will-change` here: it would promote every revealable on the page to its
 * own layer at once. transform/opacity already composite without the hint.
 */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity var(--dur-reveal) var(--ease) var(--reveal-delay),
    transform var(--dur-reveal) var(--ease) var(--reveal-delay);
}
.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}
.js [data-reveal="scale"] { transform: scale(0.96); }

/* ---------------------------------------------------------------- press */
button:not(:disabled):active,
.lumen-press:active {
  transform: scale(0.97);
  transition-duration: var(--dur-press);
}

/* ----------------------------------------------------------------- cards
 * Cross-fade to a second image, matched aspect ratios so nothing jumps.
 * The scale lives on the image inside an overflow:hidden frame.
 */
.card__frame {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background: var(--card-tint, transparent);
}
.card__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-card) var(--ease), opacity var(--dur-card) var(--ease);
}
.card__img--alt {
  position: absolute;
  inset: 0;
  opacity: 0;
}

@media (hover: hover) and (pointer: fine) {
  .card:hover .card__img { transform: scale(var(--card-scale)); }
  .card:hover .card__img--alt { opacity: 1; }
  .card:hover .card__quick { opacity: 1; transform: none; }
}
/* Keyboard users get the same affordance the mouse gets. */
.card:focus-within .card__img--alt { opacity: 1; }
.card:focus-within .card__quick { opacity: 1; transform: none; }

.card__quick {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-control) var(--ease), transform var(--dur-control) var(--ease);
}
/* Touch has no hover: the quick-add is simply always there. */
@media (hover: none) {
  .card__quick { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------- nav links
 * scaleX, never width — width forces layout on every frame.
 */
.nav-link { position: relative; }
.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-control) var(--ease);
}
@media (hover: hover) {
  .nav-link:hover::after { transform: scaleX(1); }
}
.nav-link:focus-visible::after,
.nav-link[aria-current]::after { transform: scaleX(1); }

/* ---------------------------------------------------------------- header */
[data-lumen-header] {
  position: sticky;
  top: 0;
  z-index: 40;
  transition: box-shadow var(--dur-card) var(--ease), background-color var(--dur-card) var(--ease);
}
[data-lumen-header] .header__inner {
  transition: padding-block var(--dur-card) var(--ease);
}
[data-lumen-header] .header__mark {
  transform-origin: left center;
  transition: transform var(--dur-card) var(--ease);
}
[data-lumen-header].is-condensed .header__mark { transform: scale(0.78); }

/* ---------------------------------------------------------------- drawer */
[data-lumen-drawer][hidden] { display: none; }
[data-lumen-drawer] {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: color-mix(in srgb, #000 38%, transparent);
  opacity: 0;
  transition: opacity var(--dur-drawer) var(--ease-drawer);
}
[data-lumen-drawer].is-open { opacity: 1; }
[data-lumen-drawer-panel] {
  position: absolute;
  inset-block: 0;
  right: 0;
  width: min(30rem, 100%);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform var(--dur-drawer) var(--ease-drawer);
  overflow-y: auto;
}
[data-lumen-drawer].is-open [data-lumen-drawer-panel] { transform: none; }
[data-lumen-drawer].is-busy [data-lumen-drawer-panel] { opacity: 0.6; pointer-events: none; }
html.is-drawer-open { overflow: hidden; }

@media (max-width: 40rem) {
  /* Full-screen beats side-pinned on a phone. */
  [data-lumen-drawer-panel] { width: 100%; }
}

/* Free-shipping progress: transform, not width. */
.ship__track { position: relative; height: 2px; overflow: hidden; }
.ship__fill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  transform: scaleX(var(--ship-progress, 0));
  transition: transform 400ms var(--ease);
}

/* --------------------------------------------------------------- loading */
.is-loading { position: relative; color: transparent !important; pointer-events: none; }
.is-loading::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  width: 0.875rem;
  height: 0.875rem;
  margin: -0.4375rem 0 0 -0.4375rem;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--spinner-color, #fff);
  animation: lumen-spin 640ms linear infinite;
}
@keyframes lumen-spin { to { transform: rotate(1turn); } }

.skeleton {
  background: linear-gradient(
    100deg,
    var(--skeleton-base, #e8e6e1) 30%,
    var(--skeleton-sheen, #f3f1ed) 50%,
    var(--skeleton-base, #e8e6e1) 70%
  );
  background-size: 200% 100%;
  animation: lumen-sheen 1.4s ease-in-out infinite;
}
@keyframes lumen-sheen { to { background-position-x: -200%; } }

/* --------------------------------------------------------------- marquee
 * Duplicate copy is aria-hidden; the visible copy carries the text.
 */
.marquee { overflow: hidden; }
.marquee__track {
  display: flex;
  width: max-content;
  animation: lumen-marquee var(--marquee-dur, 42s) linear infinite;
}
.marquee.is-paused .marquee__track { animation-play-state: paused; }
@keyframes lumen-marquee { to { transform: translateX(-50%); } }

/* ------------------------------------------------------------------ rails
 * `overflow-y` must be stated. A box with `overflow-x: auto` and `overflow-y:
 * visible` computes the visible axis to `auto` as well, quietly turning the
 * rail into a vertical scroller that `scrollIntoView` will then scroll.
 */
[data-rail] {
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
[data-rail]::-webkit-scrollbar { display: none; }
[data-rail] > * { scroll-snap-align: start; flex: 0 0 auto; }
[data-rail].is-dragging { cursor: grabbing; scroll-snap-type: none; user-select: none; }
@media (hover: hover) and (pointer: fine) {
  [data-rail] { cursor: grab; }
}

/* -------------------------------------------------------- scrollytelling */
[data-frame] {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--dur-reveal) var(--ease);
}
[data-frame].is-current { opacity: 1; }
/* The first frame is visible in the BASE state, not only under `.js`. Frames are
   absolutely stacked, so it simply sits beneath whichever frame is current and
   the cross-fade still reads. Gating this on `.js` left the whole media column
   blank whenever scripting was off — the engine is an enhancement, and an
   enhancement may not be load-bearing. */
[data-frame]:first-child { opacity: 1; }
[data-chapter] { opacity: 0.35; transition: opacity var(--dur-reveal) var(--ease); }
[data-chapter].is-current { opacity: 1; }
/* Chapters dim only once the engine can undim them. */
html:not(.js) [data-chapter] { opacity: 1; }

/* ------------------------------------------------------------------ zoom */
dialog[data-zoom] {
  border: 0;
  padding: 0;
  max-width: min(92vw, 60rem);
  max-height: 92vh;
  background: transparent;
}
dialog[data-zoom]::backdrop { background: rgb(0 0 0 / 0.72); }
dialog[data-zoom] img { display: block; width: 100%; height: auto; }

/* ------------------------------------------------------- reduced motion
 * Not "less" motion — no motion. Transitions collapse to instant, and the
 * reveal engine simply marks everything visible.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  .marquee__track { animation: none; }
  .card__img { transform: none !important; }
}
