/* ══════════════════════════════════════════════════════════════════════════
   Kessler — motion layer.

   The system is architectural: rules, a visible grid, nothing floating and
   nothing decorated. The motion follows it — things WIPE along an edge, RULES
   DRAW from their origin, and type RISES from behind a mask. No bounce, no
   float, no parallax drift.

   Rules of the layer:
   · Only `transform`, `opacity` and `clip-path` animate — all compositor-only,
     so nothing here triggers layout or paint on scroll.
   · Entrance choreography lives behind the `.motion` class, which motion.js
     adds ONLY when JS runs and the reader has not asked for reduced motion.
     With no JS, or with reduced motion, no pre-state is ever applied and the
     page renders finished. Content can never be stranded invisible.
   · Micro-interactions (hover, press, focus) are plain CSS transitions and are
     neutralised by the reduced-motion block in site.css.
   ══════════════════════════════════════════════════════════════════════ */

:root {
  --dur-xs: 120ms;
  --dur-sm: 200ms;
  --dur-md: 340ms;
  --dur-lg: 560ms;
  --dur-xl: 780ms;

  /* Decelerate hard — motion arrives and settles, it does not drift. */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-inout: cubic-bezier(.65, 0, .35, 1);

  --topbar-h: 33px;
}

/* ══ entrance choreography ════════════════════════════════════════════ */

/* Default: rise and fade. */
.motion [data-reveal] {
  opacity: 0;
  transform: translateY(14px);
}
.motion [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity var(--dur-md) var(--ease-out) var(--d, 0ms),
    transform var(--dur-md) var(--ease-out) var(--d, 0ms);
}

/* Photography: a hard edge sweeps across the frame, and the image settles
   back from a slight over-scale behind it.

   The wipe is a COVER that retracts, not a clip on the element itself.
   Clipping the target would shrink its visible area to nothing, and an
   IntersectionObserver measures visible area — the pre-state would hide the
   element from the very observer meant to reveal it. A cover leaves the
   geometry untouched, and `pointer-events: none` keeps whatever is beneath
   it clickable for the half-second it is there. */
.motion [data-reveal="wipe"],
.motion [data-reveal="wipe-up"] {
  opacity: 1;
  transform: none;
  position: relative;
}
.motion [data-reveal="wipe"]::after,
.motion [data-reveal="wipe-up"]::after {
  content: "";
  position: absolute; inset: 0; z-index: 3;
  background: var(--color-bg);
  pointer-events: none;
  transition: transform var(--dur-xl) var(--ease-inout) var(--d, 0ms);
}

/* Retracts to the right — the frame is revealed left to right. */
.motion [data-reveal="wipe"]::after { transform-origin: right center; }
.motion [data-reveal="wipe"].is-in::after { transform: scaleX(0); }

.motion [data-reveal="wipe"] > img { transform: scale(1.07); }
.motion [data-reveal="wipe"].is-in > img {
  transform: none;
  transition: transform 1100ms var(--ease-out) var(--d, 0ms);
}

/* Retracts to the top — the red field rises into place from below. */
.motion [data-reveal="wipe-up"]::after { transform-origin: top center; }
.motion [data-reveal="wipe-up"].is-in::after { transform: scaleY(0); }

/* ══ scroll-scrubbed video ════════════════════════════════════════════
   The poster still underneath is the element that paints first and counts as
   the LCP; the video lies on top at zero opacity and only fades up once it can
   actually be seeked. If it never loads — reduced motion, Save-Data, a slow
   connection, no JS — the still is simply what you see, and nothing about the
   layout changes. */
.scrub-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-lg) var(--ease-out);
}
.scrub-video.is-ready { opacity: 1; }

/* Rules draw from their left edge, the way a line is ruled. */
.motion [data-reveal="rule"] {
  opacity: 1;
  transform: scaleX(0);
  transform-origin: left center;
}
.motion [data-reveal="rule"].is-in {
  transform: none;
  transition: transform var(--dur-lg) var(--ease-inout) var(--d, 0ms);
}

/* ══ headline — words rise from behind a mask ═════════════════════════
   The block itself stays put; only the words move, so the headline never
   fades as a slab. */
.motion [data-reveal="words"] { opacity: 1; transform: none; }
.motion .split-word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  /* the mask would otherwise crop descenders */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}
.motion .split-word > span {
  display: inline-block;
  transform: translateY(110%);
}
.motion [data-reveal="words"].is-in .split-word > span {
  transform: none;
  transition: transform var(--dur-xl) var(--ease-out) calc(var(--i, 0) * 42ms);
}

/* ══ grouped reveals ══════════════════════════════════════════════════
   A group marks itself arrived without moving, so its parts can be sequenced
   off it in CSS. One observer entry drives the whole cell, rather than four
   elements each watching the viewport and landing out of order. */
.motion [data-reveal="group"] { opacity: 1; transform: none; }

/* House rules: the rule is drawn and the icon drawn with it, then the number,
   then the title word by word, then the line of copy. */
.motion .rule-cell .rule-num,
.motion .rule-cell p { opacity: 0; transform: translateY(10px); }
.motion .rule-cell.is-in .rule-num,
.motion .rule-cell.is-in p {
  opacity: 1; transform: none;
  transition:
    opacity var(--dur-md) var(--ease-out),
    transform var(--dur-md) var(--ease-out);
}
.motion .rule-cell.is-in .rule-num { transition-delay: calc(var(--d, 0ms) + 170ms); }
.motion .rule-cell.is-in p { transition-delay: calc(var(--d, 0ms) + 380ms); }
.motion .rule-cell.is-in h3 .split-word > span {
  transform: none;
  transition: transform var(--dur-xl) var(--ease-out)
    calc(var(--d, 0ms) + 250ms + var(--i, 0) * 38ms);
}

/* Questions: each row rules itself off while the question rises word by word. */
.motion .faq details.is-in summary .split-word > span {
  transform: none;
  transition: transform var(--dur-lg) var(--ease-out)
    calc(var(--d, 0ms) + var(--i, 0) * 26ms);
}

/* ══ micro-interactions ═══════════════════════════════════════════════ */

/* Nav: the underline is ruled in from the left rather than switched on. */
.mainnav a { border-bottom: 0; padding-bottom: 4px; position: relative; }
.mainnav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: currentColor;
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--dur-sm) var(--ease-out);
}
.mainnav a:hover::after,
.mainnav a:focus-visible::after,
.mainnav a[aria-current="page"]::after { transform: scaleX(1); }

/* Buttons: colour eases, the press is felt, trailing arrows lead the way. */
.btn {
  transition:
    background-color var(--dur-sm) var(--ease-out),
    border-color var(--dur-sm) var(--ease-out),
    color var(--dur-sm) var(--ease-out),
    transform var(--dur-xs) var(--ease-out);
}
.btn:active { transform: translateY(1px); }
.ic-arrow { transition: transform var(--dur-sm) var(--ease-out); }
.btn:hover .ic-arrow, a:hover > .ic-arrow { transform: translateX(4px); }

.iconbtn, .linkbtn, .sugg, .swatch, .size, .thumb, .pill, .check .box, .select {
  transition:
    background-color var(--dur-sm) var(--ease-out),
    border-color var(--dur-sm) var(--ease-out),
    color var(--dur-sm) var(--ease-out),
    outline-color var(--dur-sm) var(--ease-out);
}
.iconbtn:active, .swatch:active, .size:active, .thumb:active { transform: translateY(1px); }

/* Cards: the photograph pushes in slightly under the cursor. Only product
   cards move — category tiles keep the still, ruled treatment. */
.prod-media .ph > img {
  transition:
    transform var(--dur-xl) var(--ease-out),
    filter var(--dur-md) linear;
}
.prod:hover .prod-media .ph > img,
.prod:focus-within .prod-media .ph > img { transform: scale(1.05); }
.prod-name { transition: color var(--dur-sm) var(--ease-out); }
.cat .ph > img { transition: filter var(--dur-md) linear; }
.cat-name { transition: color var(--dur-sm) var(--ease-out); }

/* The quick-view strip already slides; give it the shared easing. */
.quickview { transition: opacity var(--dur-sm) var(--ease-out), translate var(--dur-sm) var(--ease-out); }

/* ══ header condense ══════════════════════════════════════════════════
   Past the fold the announcement bar rides up out of view and the nav takes
   the top edge. Transform only — the header never re-lays-out. */
.site-head {
  transition: transform var(--dur-md) var(--ease-out);
  will-change: transform;
}
.site-head.is-condensed { transform: translateY(calc(-1 * var(--topbar-h))); }

/* ══ fly to bag ═══════════════════════════════════════════════════════
   A square of the actual photograph, cut from the frame you were looking at
   and carried up to the bag. Fixed position and transform only, so the flight
   costs no layout; pointer-events none so it never intercepts a click on the
   way past. */
.fly-to-bag {
  position: fixed; z-index: 95;
  pointer-events: none; overflow: hidden;
  background: var(--color-neutral-200);
  box-shadow: var(--shadow-md);
  will-change: transform;
}
.fly-to-bag img {
  width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(1) contrast(1.08);
}

/* ══ bag badge ════════════════════════════════════════════════════════ */
@keyframes bag-pop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.42); }
  100% { transform: scale(1); }
}
.bagcount.is-bumped { animation: bag-pop 420ms var(--ease-out); }

/* ══ overlays ═════════════════════════════════════════════════════════ */
@keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes qv-in  { from { opacity: 0; transform: translateY(16px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes srch-in { from { opacity: 0; transform: translateY(-14px); } to { opacity: 1; transform: none; } }
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 14px); } to { opacity: 1; transform: translate(-50%, 0); } }

.motion .backdrop:not([hidden]) { animation: overlay-in var(--dur-sm) var(--ease-out); }
.motion .backdrop:not([hidden]) .qv-panel { animation: qv-in var(--dur-md) var(--ease-out); }
.motion .backdrop:not([hidden]) .search-panel { animation: srch-in var(--dur-md) var(--ease-out); }
.motion .undo:not([hidden]) { animation: toast-in var(--dur-md) var(--ease-out); }

/* ══ signup + order confirmation ══════════════════════════════════════ */
@keyframes rise-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.motion #signup.is-sent { animation: overlay-in var(--dur-md) var(--ease-out); }
.motion .confirm { animation: rise-in var(--dur-lg) var(--ease-out); }

/* ══ listing ══════════════════════════════════════════════════════════ */

/* The filter drawer discloses on tap. Opening animates; closing is
   immediate — a drawer that lingers on the way out reads as lag. */
@keyframes rail-in { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }
.motion .rail.open { animation: rail-in var(--dur-md) var(--ease-out); }

/* An active filter is a physical token: it lands, and it is taken away. */
@keyframes pill-in { from { opacity: 0; transform: scale(.86); } to { opacity: 1; transform: none; } }
.motion .activefilters .pill { animation: pill-in var(--dur-sm) var(--ease-out); }

/* ══ product gallery ══════════════════════════════════════════════════
   Changing frame is a cut, not a slide — the new frame arrives in place. */
@keyframes frame-in { from { opacity: 0; transform: scale(1.03); } to { opacity: 1; transform: none; } }
.motion .stage .ph { animation: frame-in var(--dur-md) var(--ease-out); }
.thumb { transition: border-color var(--dur-sm) var(--ease-out), transform var(--dur-xs) var(--ease-out); }

/* ══ accordion ════════════════════════════════════════════════════════
   The marker is drawn from two bars so the vertical one can retract into a
   minus, instead of swapping one glyph for another. */
.acc summary { position: relative; }
.acc summary::after {
  content: ""; width: 11px; height: 2px; flex: none;
  background: currentColor; color: var(--color-neutral-600);
}
.acc details[open] summary::after { content: ""; }
.acc summary::before {
  content: ""; position: absolute; right: 4.5px; top: 50%;
  width: 2px; height: 11px; margin-top: -5.5px;
  background: currentColor; color: var(--color-neutral-600);
  transition: transform var(--dur-md) var(--ease-out);
}
.acc details[open] summary::before { transform: scaleY(0); }

/* The panel is unrolled rather than switched on: the row grows to the height
   of its answer and the text rises the last few pixels behind it. Height is
   the one property here allowed to animate off the compositor — it runs on a
   press, never on scroll, and one row at a time.

   `::details-content` keeps this native: no JS, no wrapper element, and the
   accordion still opens and closes normally in browsers that lack it. */
.acc details::details-content {
  block-size: 0;
  overflow: clip;
  transition:
    block-size var(--dur-md) var(--ease-out),
    content-visibility var(--dur-md) allow-discrete;
}
.acc details[open]::details-content {
  /* calc-size() rather than a global `interpolate-size`, so nothing else on
     the page silently gains an auto-transition. */
  block-size: calc-size(auto, size);
}

@keyframes acc-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.motion .acc details[open] .acc-body { animation: acc-in var(--dur-md) var(--ease-out); }

/* ══ cart ═════════════════════════════════════════════════════════════ */

/* A number that changed says so — briefly, once. */
@keyframes value-change {
  0%   { opacity: .25; transform: translateY(-4px); }
  100% { opacity: 1; transform: none; }
}
.motion .is-changed { animation: value-change var(--dur-md) var(--ease-out); }

/* The line leaves before the rest close up. */
.motion .line.is-leaving {
  opacity: 0;
  transform: translateX(-18px);
  transition: opacity var(--dur-sm) var(--ease-out), transform var(--dur-sm) var(--ease-out);
  pointer-events: none;
}

/* ══ checkout ═════════════════════════════════════════════════════════
   The step you are working in owns the accent; the others stand down. */
.step-num { transition: color var(--dur-md) var(--ease-out), transform var(--dur-md) var(--ease-out); }
/* All four numbers stay in the accent, as drawn — the progression is the
   point. They only stand down once you are actually working inside a step. */
.checkout-form:focus-within .step:not(:focus-within) .step-num { color: var(--color-neutral-500); }
.checkout-form .step:focus-within .step-num {
  transform: scale(1.18);
  transform-origin: left center;
}
.input, .select { transition: border-color var(--dur-sm) var(--ease-out), background-color var(--dur-sm) var(--ease-out); }
.ship-opt { transition: border-color var(--dur-sm) var(--ease-out), background-color var(--dur-sm) var(--ease-out); }
.ship-opt .dot { transition: border-color var(--dur-sm) var(--ease-out), background-color var(--dur-sm) var(--ease-out), box-shadow var(--dur-sm) var(--ease-out); }

/* ══ account ══════════════════════════════════════════════════════════
   Only the live status breathes — it is the one row still changing. */
@keyframes status-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: .62; }
}
.motion .status-live { animation: status-breathe 3.4s var(--ease-inout) infinite; }
.side a { transition: color var(--dur-sm) var(--ease-out), border-color var(--dur-sm) var(--ease-out); }
.order .btn { transition: background-color var(--dur-sm) var(--ease-out), border-color var(--dur-sm) var(--ease-out), transform var(--dur-xs) var(--ease-out); }

/* ══ house rules ══════════════════════════════════════════════════════
   The icons are drawn rather than faded in — the same gesture as ruling a
   line, which is the one the whole system is built on. stroke-dashoffset is
   paint, not layout, and this is four small icons firing once. */
.motion .rule-cell .ic { stroke-dasharray: 1; stroke-dashoffset: 1; }
.motion .rule-cell.is-in .ic {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 900ms var(--ease-inout) calc(var(--d, 0ms) + 140ms);
}

/* Each cell's rule is drawn left to right as the cell arrives, so a row builds
   the way a ruled table is drawn rather than appearing whole. */
.motion .cells-drawn > *::before { transform: scaleX(0); transform-origin: left center; }
.motion .cells-drawn > *.is-in::before {
  transform: scaleX(1);
  transition: transform var(--dur-lg) var(--ease-inout) var(--d, 0ms);
}

/* ══ tiles and cards ══════════════════════════════════════════════════
   One movement per cell. The cell is the mask and the card rises out from
   behind its bottom edge as a single piece — photograph, name and price
   travelling together, nothing sequenced inside. */
.motion .deal[data-reveal] > * { transform: translateY(100%); }
.motion .deal[data-reveal].is-in > * {
  transform: none;
  transition: transform var(--dur-xl) var(--ease-out) var(--d, 0ms);
}

/* ══ questions ════════════════════════════════════════════════════════
   Rows arrive one at a time and each rules itself off, so the list reads as
   a list being set rather than a block appearing. */
.motion .faq .acc details::after { transform: scaleX(0); transform-origin: left center; }
.motion .faq .acc details.is-in::after {
  transform: scaleX(1);
  transition: transform var(--dur-lg) var(--ease-inout) calc(var(--d, 0ms) + 80ms);
}

/* ══ cross-document navigation ════════════════════════════════════════
   Chrome and Safari cross-fade between pages; every other browser ignores
   this block and navigates as normal. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation-duration: var(--dur-sm); }
::view-transition-new(root) { animation-duration: var(--dur-md); }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* A tab-through must never be chased by the page. */
@media (prefers-reduced-motion: reduce) {
  .site-head { will-change: auto; }
}
