/* ═══════════════════════════════════════════════════════════════════════
   TRIAKAR ADDITIVE MOTION SYSTEM  ·  motion.css
   ═══════════════════════════════════════════════════════════════════════

   WHY THIS FILE EXISTS
   The audit (node scripts/motion-audit.mjs) found 411 transition
   declarations running on effectively three easing curves: the browser
   default `ease` (125 uses), Material's (.4,0,.2,1) (50) and easeOutQuint
   (.22,1,.36,1) (49). A button, a product card, a modal and a filter chip
   all moved with the same recipe, so nothing on the site read as having
   its own character. 99 of those declarations were `transition: all`,
   which also animates whatever layout property happens to change.

   THE IDEA
   TriAkar means "crafting the third dimension" and the thing it actually
   does is additive manufacturing: material is laid down in layers, bottom
   up, hot, and it contracts very slightly as it cools. So that is the
   motion language. Nothing on this site fades in from above. Things are
   deposited from the build plate upward, and they settle the way cooling
   plastic settles.

   Seven curves, each with exactly one job, so element types stop
   borrowing each other's motion:

     --m-e-deposit   material arriving. Fast start, long quiet settle.
     --m-e-cool      overshoot then contract, the way a hot layer shrinks.
     --m-e-press     nozzle coming down. Quick, flat, no bounce.
     --m-e-release   coming back up with a trace of spring.
     --m-e-glass     asymmetric slow-in for blur and shadow on glass.
     --m-e-retract   filament pulling back. Exits only.
     --m-e-gantry    mechanical, sharp at both ends. Rails and underlines.

   PERFORMANCE CONTRACT
   Compositor properties only: transform, opacity, filter, and clip-path
   (inset clips are composited in Chromium and Safari, and they are what
   makes the bottom-up build possible without touching layout). No width,
   height, margin, padding or top/left is animated anywhere in this file.
   Blur is only ever crossfaded via opacity on a pre-blurred layer, never
   animated as a live backdrop-filter value.

   Loads after shared.css, so equal specificity wins by cascade order.
   ═══════════════════════════════════════════════════════════════════════ */

:root{
  /* ── Curves ─────────────────────────────────────────────────────── */
  --m-e-deposit: cubic-bezier(.2, .9, .24, 1);
  --m-e-cool:    cubic-bezier(.34, 1.32, .5, 1);
  --m-e-press:   cubic-bezier(.4, .86, .5, 1);
  --m-e-release: cubic-bezier(.16, 1.1, .3, 1);
  --m-e-glass:   cubic-bezier(.32, .72, .28, 1);
  --m-e-retract: cubic-bezier(.55, .06, .68, .19);
  --m-e-gantry:  cubic-bezier(.86, 0, .07, 1);

  /* ── Durations. Micro stays under 300ms, page level under 500ms. ── */
  --m-t-0: 110ms;   /* tap acknowledgement */
  --m-t-1: 170ms;   /* micro interaction */
  --m-t-2: 230ms;   /* hover, focus */
  --m-t-3: 310ms;   /* component enter */
  --m-t-4: 420ms;   /* overlay, sheet */
  --m-t-5: 490ms;   /* page level, scroll build */

  --m-stagger: 52ms;      /* between siblings in a grid, capped in motion.js */
  --m-lift: 18px;         /* only the opt-in deposit build travels this far */

  /* Heat. A freshly deposited layer is warm and cools to neutral. */
  --m-heat:      rgba(196, 98, 42, .55);
  --m-heat-soft: rgba(196, 98, 42, .22);
  --m-layerline: rgba(15, 15, 13, .08);

  /* ── Composed transition recipes, one per element family ──────────
     The codemod in scripts/motion-codemod.mjs rewrites the old
     `transition: all .2s` declarations to whichever of these fits the
     selector, so a chip and a card no longer share a recipe. */

  /* General purpose. Everything a small ornament (a dot, a chevron, an
     arrow, a close cross) might need, and nothing that costs layout.
     transform is in here deliberately: this recipe replaces `all` on
     elements that rotate or nudge, and dropping transform would have
     silently killed those. */
  --m-tr-surface: transform var(--m-t-1) var(--m-e-release),
                  background-color var(--m-t-1) var(--m-e-glass),
                  border-color var(--m-t-1) var(--m-e-glass),
                  color var(--m-t-1) var(--m-e-glass),
                  box-shadow var(--m-t-2) var(--m-e-glass),
                  filter var(--m-t-1) var(--m-e-glass),
                  opacity var(--m-t-1) linear;

  /* Cards. Lift and shadow bloom, slower than the tint underneath it. */
  --m-tr-card: transform var(--m-t-2) var(--m-e-deposit),
               box-shadow var(--m-t-3) var(--m-e-glass),
               border-color var(--m-t-2) var(--m-e-glass),
               background-color var(--m-t-2) var(--m-e-glass),
               filter var(--m-t-2) var(--m-e-glass),
               opacity var(--m-t-2) linear;

  /* Buttons. Deliberately fast and springless on the way down. */
  --m-tr-press: transform var(--m-t-0) var(--m-e-press),
                box-shadow var(--m-t-2) var(--m-e-glass),
                background-color var(--m-t-1) var(--m-e-press),
                border-color var(--m-t-1) var(--m-e-press),
                color var(--m-t-1) var(--m-e-press),
                filter var(--m-t-1) var(--m-e-press);

  /* Inputs. No movement at all, only the frame reacting. */
  --m-tr-field: border-color var(--m-t-2) var(--m-e-glass),
                box-shadow var(--m-t-2) var(--m-e-glass),
                background-color var(--m-t-2) var(--m-e-glass);

  /* Links and nav items. Quickest of everything, and the transform is
     there only for the pill shaped nav buttons that nudge. */
  --m-tr-link: color var(--m-t-1) var(--m-e-press),
               opacity var(--m-t-1) linear,
               background-color var(--m-t-1) var(--m-e-press),
               box-shadow var(--m-t-1) var(--m-e-glass),
               transform var(--m-t-1) var(--m-e-release),
               border-color var(--m-t-1) var(--m-e-press);

  /* Chips, tabs, filters. Mechanical, matches the sliding underline. */
  --m-tr-chip: background-color var(--m-t-1) var(--m-e-gantry),
               border-color var(--m-t-1) var(--m-e-gantry),
               color var(--m-t-1) var(--m-e-gantry),
               transform var(--m-t-1) var(--m-e-gantry);

  /* Glass panels, sheets, dropdowns. */
  --m-tr-glass: opacity var(--m-t-3) var(--m-e-glass),
                transform var(--m-t-3) var(--m-e-cool),
                box-shadow var(--m-t-4) var(--m-e-glass);
}

/* ═══════════════════════════════════════════════════════════════════════
   1 · THE BUILD.  Scroll reveal as layer deposition.
   ───────────────────────────────────────────────────────────────────────
   motion.js owns the target list and stamps [data-m] on elements in the
   same task that it decides which are already on screen, so nothing ever
   flashes. If JS never runs, [data-m] is never stamped and every element
   simply renders normally. There is no state in which content is stuck
   invisible.
   ═══════════════════════════════════════════════════════════════════════ */

[data-m]{ opacity: 0 }
/* The layer hint lives only while the element is actually building, so a
   page with sixty targets never holds sixty promoted layers. */
[data-m].m-in:not(.m-done){ animation-fill-mode: both; will-change: transform, opacity }
/* Once the build finishes the element drops every trace of the system, so
   no clip or filter is left sitting on it costing paint time.

   Three classes, not two, on purpose. The per-type rules further down set
   animation-name at (0,2,0), and they come later in the file, so a
   (0,2,0) .m-done would lose to them and never cancel anything. At
   (0,3,0) this wins wherever it sits. That matters beyond tidiness: if an
   animation is never ticked (a document that is not painting), fill-mode
   both would otherwise hold the element on its 0% keyframe, which is
   opacity 0. This rule is what guarantees content ends up visible. */
[data-m].m-in.m-done,
[data-m].m-done{ opacity: 1; clip-path: none; filter: none; transform: none; animation: none }

/* ── FIVE MECHANISMS, FIVE AXES ───────────────────────────────────────
   The first version of this file put a bottom-up build on four of the
   five reveal types, which just swapped one repetitive gesture for
   another. Every type below now moves on a different axis, so a heading,
   a card, a paragraph and a photo cannot be mistaken for each other:

     label   horizontal   slides in from the left, draws its hairline
     head    depth        grows from the left edge, orange depth retracts
     slab    horizontal   gantry wipe, left to right, staggered across a row
     build   none         focus pull, blur resolves with no travel at all
     media   depth        scale settles, no wipe, no travel

   Only one of them still travels vertically, and that is the opt-in
   `data-motion="deposit"` below, kept because the bottom-up build is
   genuinely the brand gesture and is worth having where it earns a
   moment. It is no longer the default for anything.
   ═══════════════════════════════════════════════════════════════════ */

/* SLAB, the gantry wipe. The card is revealed left to right behind a warm
   leading edge, which is the print head crossing the bed. Staggering a
   row of these reads as one continuous sweep across the grid rather than
   four separate cards each doing their own thing. */
@keyframes m-gantry-wipe{
  0%   { opacity: 0; clip-path: inset(0 100% 0 0); transform: translate3d(-10px, 0, 0);
         filter: drop-shadow(-1px 0 0 var(--m-heat)) }
  10%  { opacity: 1 }
  70%  { filter: drop-shadow(-1px 0 0 var(--m-heat-soft)) }
  100% { opacity: 1; clip-path: inset(0 0 0 0); transform: translate3d(0, 0, 0);
         filter: drop-shadow(0 0 0 rgba(196, 98, 42, 0)) }
}

/* BUILD, the focus pull. Body copy and list items resolve out of a soft
   blur without moving at all. Text that slides while you are trying to
   start reading it is the single most irritating reveal on the web, so
   this one holds perfectly still. */
@keyframes m-focus{
  0%   { opacity: 0; filter: blur(6px) }
  100% { opacity: 1; filter: blur(0) }
}

/* HEAD, the extrude. The glyph arrives with depth stacked under it in TRI
   orange and that depth retracts into the flat letterform, which is the
   literal "third dimension" gesture. It grows from its left edge so it
   picks up exactly where the eyebrow's hairline finished, and it has no
   vertical travel of its own.

   drop-shadow rather than text-shadow on purpose: filter is composited,
   text-shadow forces a repaint of the whole heading on every frame. Same
   picture, none of the cost, and it follows the glyph outlines so it
   survives a <br> or an inline <span> inside the heading. */
@keyframes m-extrude{
  0%   { opacity: 0; transform: scale(1.035);
         filter: drop-shadow(0 6px 0 var(--m-heat)) drop-shadow(0 10px 15px rgba(196, 98, 42, .22)) }
  30%  { opacity: 1 }
  100% { opacity: 1; transform: scale(1);
         filter: drop-shadow(0 0 0 rgba(196, 98, 42, 0)) drop-shadow(0 0 0 rgba(196, 98, 42, 0)) }
}

/* LABEL, the perimeter. A slicer lays the outline before the infill, so
   the eyebrow arrives first, horizontally, and pulls a hairline to the
   right that the heading then grows out of. */
@keyframes m-perimeter{
  0%   { opacity: 0; transform: translate3d(-14px, 0, 0) }
  100% { opacity: 1; transform: translate3d(0, 0, 0) }
}
@keyframes m-perimeter-line{
  0%   { transform: scaleX(0) }
  100% { transform: scaleX(1) }
}

/* MEDIA, the focus settle. A photograph is not manufactured in front of
   you, so it does not get a wipe. It just comes to rest. */
@keyframes m-settle{
  0%   { opacity: 0; transform: scale(1.055) }
  16%  { opacity: 1 }
  100% { opacity: 1; transform: scale(1) }
}

/* DEPOSIT, opt in via data-motion="deposit". The original bottom-up
   build, kept for the one or two places per page where it is the point
   rather than the wallpaper. */
@keyframes m-deposit{
  0%   { opacity: 0; clip-path: inset(100% 0 0 0); transform: translate3d(0, var(--m-lift), 0) }
  12%  { opacity: 1 }
  100% { opacity: 1; clip-path: inset(0 0 0 0);    transform: translate3d(0, 0, 0) }
}

/* Stagger. --m-i is the sibling index, capped by motion.js. */
[data-m].m-in{
  animation-delay: calc(var(--m-i, 0) * var(--m-stagger));
}

/* Type to mechanism. Note the durations differ too: a card sweeping is a
   bigger event than a paragraph coming into focus, and giving them the
   same 490ms is part of what made everything feel like one blur. */
[data-m="build"].m-in   { animation-name: m-focus;       animation-duration: var(--m-t-3); animation-timing-function: var(--m-e-glass) }
[data-m="slab"].m-in    { animation-name: m-gantry-wipe; animation-duration: var(--m-t-5); animation-timing-function: var(--m-e-gantry) }
[data-m="head"].m-in    { animation-name: m-extrude;     animation-duration: var(--m-t-4); animation-timing-function: var(--m-e-cool) }
[data-m="label"].m-in   { animation-name: m-perimeter;   animation-duration: var(--m-t-3); animation-timing-function: var(--m-e-gantry) }
[data-m="media"].m-in   { animation-name: m-settle;      animation-duration: var(--m-t-4); animation-timing-function: var(--m-e-cool) }
[data-m="deposit"].m-in { animation-name: m-deposit;     animation-duration: var(--m-t-5); animation-timing-function: var(--m-e-deposit) }

/* The perimeter hairline itself. Only drawn on eyebrow labels that
   motion.js has confirmed sit above a heading, so it never appears on a
   stray label in a form. */
[data-m="label"].m-perim{ position: relative }
[data-m="label"].m-perim::after{
  content: '';
  position: absolute;
  top: 50%;
  left: calc(100% + 10px);
  width: 56px;
  height: 1px;
  background: linear-gradient(90deg, var(--accent), rgba(196, 98, 42, 0));
  transform: scaleX(0);
  transform-origin: left center;
  pointer-events: none;
}
[data-m="label"].m-perim.m-in::after{
  animation: m-perimeter-line var(--m-t-4) var(--m-e-gantry) both;
  animation-delay: calc(var(--m-i, 0) * var(--m-stagger) + 60ms);
}

/* Backwards compatibility. shared.js still drives .fade-in-up; it resolves
   out of focus rather than sliding, so it agrees with the build type. */
.fade-in-up{ opacity: 0; transform: none; transition: none }
.fade-in-up.visible{
  animation: m-focus var(--m-t-3) var(--m-e-glass) both;
  opacity: 1;
  transform: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   2 · SLABS.  Cards get lift and a specular sweep. Nothing else does.
   ═══════════════════════════════════════════════════════════════════════ */

/* No will-change on cards. A grid of thirty products would otherwise hold
   thirty compositor layers for a hover that may never happen. */
.prod-card, .story-card, .test-card, .trust-card, .m-slab{
  transition: var(--m-tr-card);
}
/* The lift is small and the shadow does most of the work, which is what
   keeps it feeling like glass rather than a card being picked up. */
@media (hover: hover) and (pointer: fine){
  .prod-card:hover, .story-card:hover, .test-card:hover, .trust-card:hover, .m-slab:hover{
    transform: translate3d(0, -3px, 0) scale(1.008);
  }
  /* Specular sweep. A single highlight crosses the surface once on enter.

     The highlight travels by background-position, not by transform. The
     transform version needed overflow:hidden on the card to contain it,
     and .test-card has no overflow of its own, so adding it clipped 92px
     of real testimonial content on every card. Changing what a card shows
     is not something a motion layer gets to do.

     background-position is a paint, not a composite, which is the one
     deliberate exception to the transform/opacity rule in this file. It
     is bounded: hover only, so pointer devices only, one card at a time,
     620ms, over an area already being repainted for the shadow lift. */
  .prod-card, .story-card, .test-card, .m-slab{ position: relative }
  .prod-card::after, .story-card::after, .test-card::after, .m-slab::after{
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background-image: linear-gradient(105deg, rgba(255,255,255,0) 38%, rgba(255,255,255,.38) 50%, rgba(255,255,255,0) 62%);
    background-repeat: no-repeat;
    background-size: 260% 100%;
    background-position: 130% 0;
    opacity: 0;
    pointer-events: none;
    z-index: 3;
  }
  .prod-card:hover::after, .story-card:hover::after, .test-card:hover::after, .m-slab:hover::after{
    animation: m-specular 620ms var(--m-e-glass) both;
  }
}
@keyframes m-specular{
  0%   { background-position: 130% 0; opacity: 0 }
  18%  { opacity: 1 }
  100% { background-position: -30% 0; opacity: 0 }
}

/* Touch has no hover, so the same affordance arrives as a press. A card
   under a thumb settles into the page instead of lifting off it. */
@media (hover: none){
  .prod-card:active, .story-card:active, .test-card:active, .trust-card:active, .m-slab:active{
    transform: scale(.985);
    transition: transform var(--m-t-0) var(--m-e-press);
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   3 · CONTROLS.  Buttons extrude. They never lift like a card.
   ═══════════════════════════════════════════════════════════════════════ */

.btn, .btn-primary, .btn-secondary, .btn-accent, .btn-dark, .btn-outline,
.btn-ghost, .btn-add-cart, .add-to-cart-btn, button.m-btn{
  transition: var(--m-tr-press);
}

/* Press is a squish, not a shrink: the nozzle comes down, so the element
   compresses vertically and spreads a hair horizontally. */
.btn:active, .btn-primary:active, .btn-secondary:active, .btn-accent:active,
.btn-dark:active, .btn-outline:active, .btn-ghost:active, .btn-add-cart:active,
.add-to-cart-btn:active, button.m-btn:active{
  transform: scale3d(1.012, .955, 1);
  transition-duration: var(--m-t-0);
  transition-timing-function: var(--m-e-press);
}

/* Hover is a heat bloom in the shadow, deliberately with no translate, so
   buttons and cards read as different classes of object. */
@media (hover: hover) and (pointer: fine){
  .btn-primary:hover, .btn-accent:hover{
    transform: none;
    box-shadow: 0 6px 26px rgba(196, 98, 42, .42), inset 0 1px 0 rgba(255, 255, 255, .24);
    filter: saturate(1.06);
  }
  .btn-outline:hover, .btn-ghost:hover, .btn-secondary:hover{
    transform: none;
    box-shadow: 0 3px 14px rgba(15, 15, 13, .10);
  }
}

/* Filament flow. Replaces the generic white circular ripple: the tap
   point extrudes a warm pool that cools outward and vanishes. Injected
   by motion.js as a .m-flow span. */
.m-flow{
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  background: radial-gradient(circle, rgba(196, 98, 42, .42) 0%, rgba(217, 139, 80, .18) 45%, rgba(196, 98, 42, 0) 70%);
  transform: scale(0);
  animation: m-flow 540ms var(--m-e-deposit) both;
}
/* On a surface that is already TRI orange the pool reads as white heat. */
.btn-primary .m-flow, .btn-accent .m-flow{
  background: radial-gradient(circle, rgba(255, 255, 255, .5) 0%, rgba(255, 255, 255, .16) 45%, rgba(255, 255, 255, 0) 70%);
}
@keyframes m-flow{
  0%   { transform: scale(0);   opacity: 1 }
  70%  { opacity: .55 }
  100% { transform: scale(2.4); opacity: 0 }
}

/* Icon buttons rotate the icon rather than moving the button. */
.m-ico-btn{ transition: var(--m-tr-press) }
.m-ico-btn > svg, .m-ico-btn > i{ transition: transform var(--m-t-2) var(--m-e-release) }
.m-ico-btn:hover > svg, .m-ico-btn:hover > i{ transform: rotate(-8deg) scale(1.08) }
.m-ico-btn:active > svg, .m-ico-btn:active > i{ transform: rotate(0) scale(.92); transition-duration: var(--m-t-0) }

/* ═══════════════════════════════════════════════════════════════════════
   4 · FIELDS.  Frames react, nothing moves.
   ═══════════════════════════════════════════════════════════════════════ */

input, textarea, select, .glass-input{ transition: var(--m-tr-field) }

/* A focused field draws its accent line left to right on the gantry
   curve, so focus feels like the machine addressing that field. */
.m-field{ position: relative }
.m-field::after{
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--m-t-2) var(--m-e-gantry);
  pointer-events: none;
  border-radius: 2px;
}
.m-field:focus-within::after{ transform: scaleX(1) }

/* Invalid input shudders on the gantry axis, short and mechanical, never
   a cartoon wobble. */
@keyframes m-reject{
  0%, 100% { transform: translate3d(0, 0, 0) }
  20%      { transform: translate3d(-5px, 0, 0) }
  45%      { transform: translate3d(4px, 0, 0) }
  70%      { transform: translate3d(-2px, 0, 0) }
}
.m-reject{ animation: m-reject 260ms var(--m-e-gantry) both }

/* ═══════════════════════════════════════════════════════════════════════
   5 · OVERLAYS.  Modals cool into place. Sheets ride the gantry.
   ═══════════════════════════════════════════════════════════════════════ */

/* Scrim: opacity only. Animating a live backdrop-filter value is the
   single most expensive thing you can do on a phone, so the blur is
   static and the layer is faded. */
@keyframes m-scrim{ from{ opacity: 0 } to{ opacity: 1 } }
.m-scrim{
  animation: m-scrim var(--m-t-3) var(--m-e-glass) both;
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
}
.m-scrim.m-out{ animation: m-scrim var(--m-t-2) var(--m-e-retract) reverse both }

/* Centre modal: arrives with the cooling overshoot, so it settles rather
   than snapping. */
@keyframes m-modal-in{
  0%   { opacity: 0; transform: translate3d(0, 14px, 0) scale(.96) }
  100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
}
@keyframes m-modal-out{
  0%   { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
  100% { opacity: 0; transform: translate3d(0, 8px, 0) scale(.985) }
}
.m-modal{ animation: m-modal-in var(--m-t-4) var(--m-e-cool) both; will-change: transform, opacity }
.m-modal.m-out{ animation: m-modal-out var(--m-t-2) var(--m-e-retract) both }

/* Bottom sheet: the mobile primary. Rides in on the mechanical curve
   with no overshoot, because a sheet tracking a thumb should feel
   attached to the gesture rather than springy. */
@keyframes m-sheet-in{
  0%   { transform: translate3d(0, 100%, 0) }
  100% { transform: translate3d(0, 0, 0) }
}
.m-sheet{ animation: m-sheet-in var(--m-t-4) var(--m-e-gantry) both; will-change: transform }
.m-sheet.m-out{ animation: m-sheet-in var(--m-t-3) var(--m-e-retract) reverse both }

/* Side drawer. */
@keyframes m-drawer-in{
  0%   { transform: translate3d(100%, 0, 0) }
  100% { transform: translate3d(0, 0, 0) }
}
.m-drawer{ animation: m-drawer-in var(--m-t-4) var(--m-e-gantry) both; will-change: transform }
.m-drawer.m-out{ animation: m-drawer-in var(--m-t-3) var(--m-e-retract) reverse both }

/* Dropdown and popover: small, quick, cooling. */
@keyframes m-pop-in{
  0%   { opacity: 0; transform: translate3d(0, -6px, 0) scale(.97) }
  100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
}
.m-pop{ animation: m-pop-in var(--m-t-2) var(--m-e-cool) both; transform-origin: top center }

/* ═══════════════════════════════════════════════════════════════════════
   6 · TABS, CHIPS, FILTERS.  One underline travels; chips do not each
   animate on their own, which is what made the filter rows feel noisy.
   ═══════════════════════════════════════════════════════════════════════ */

.story-filter, .fp-pill, .m-chip{ transition: var(--m-tr-chip) }
.story-filter:active, .fp-pill:active, .m-chip:active{ transform: scale(.96) }

.m-tabs{ position: relative }
.m-tabs .m-tabline{
  position: absolute;
  bottom: 0; left: 0;
  height: 2px;
  background: var(--accent);
  border-radius: 2px;
  transform-origin: left center;
  transition: transform var(--m-t-3) var(--m-e-gantry);
  pointer-events: none;
  will-change: transform;
}

/* ═══════════════════════════════════════════════════════════════════════
   7 · LOADING.  Branded, not a borrowed spinner.
   ═══════════════════════════════════════════════════════════════════════ */

/* The layer stack. Three bars deposit bottom up, hold, and the stack
   clears, which is literally what the printer is doing while the user
   waits. Replaces every generic rotating ring. */
.m-loader{
  display: inline-flex;
  flex-direction: column-reverse;
  justify-content: flex-start;
  gap: 3px;
  width: 26px;
  height: 22px;
}
.m-loader > i{
  display: block;
  height: 4px;
  border-radius: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  animation: m-layerstack 1.35s var(--m-e-deposit) infinite;
}
.m-loader > i:nth-child(1){ animation-delay: 0ms }
.m-loader > i:nth-child(2){ animation-delay: 150ms }
.m-loader > i:nth-child(3){ animation-delay: 300ms }
@keyframes m-layerstack{
  0%        { transform: scaleX(0);   opacity: 1 }
  28%, 62%  { transform: scaleX(1);   opacity: 1 }
  100%      { transform: scaleX(1);   opacity: 0 }
}
.m-loader.m-loader-light > i{ background: #fff }

/* Inline spinner replacement for buttons in a submitting state. */
.m-loader-sm{ width: 18px; height: 15px; gap: 2px }
.m-loader-sm > i{ height: 3px }

/* Skeletons stop shimmering with a grey diagonal and start printing: a
   warm band sweeps bottom to top, in the same direction as every reveal
   on the site, so a loading grid and a loaded grid move the same way. */
@keyframes m-print-sweep{
  0%   { background-position: 50% 130% }
  100% { background-position: 50% -30% }
}
.skeleton{
  background: linear-gradient(
    180deg,
    #ece9e2 0%,
    #f6f3ec 38%,
    rgba(217, 139, 80, .14) 50%,
    #f6f3ec 62%,
    #ece9e2 100%
  ) !important;
  background-size: 100% 320% !important;
  animation: m-print-sweep 1.5s var(--m-e-glass) infinite !important;
}

/* ═══════════════════════════════════════════════════════════════════════
   8 · FEEDBACK.  Toasts, badges, hearts.
   ═══════════════════════════════════════════════════════════════════════ */

/* The toast arrives from the bottom edge rather than sliding in from the
   right, which is the app-native position and keeps it out of the way of
   the thumb on mobile. */
.toast{
  transform: translate3d(0, 24px, 0) scale(.96) !important;
  opacity: 0;
  transition: transform var(--m-t-3) var(--m-e-cool), opacity var(--m-t-2) linear !important;
}
.toast.show{ transform: translate3d(0, 0, 0) scale(1) !important; opacity: 1 }

/* A count changing extrudes: it grows towards the viewer with a heat
   ring rather than just scaling. */
@keyframes m-count-extrude{
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 var(--m-heat) }
  35%  { transform: scale(1.42); box-shadow: 0 0 0 6px rgba(196, 98, 42, 0) }
  70%  { transform: scale(.94) }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(196, 98, 42, 0) }
}
.cart-badge.pop, .tabn-badge.pop, .wishlist-badge.pop, .m-count.pop{
  animation: m-count-extrude 460ms var(--m-e-cool) !important;
}

/* Saving something is a deposit, so the heart lands rather than bounces. */
@keyframes m-heart-deposit{
  0%   { transform: scale(1) }
  30%  { transform: scale(1.3) rotate(-6deg) }
  60%  { transform: scale(.94) rotate(3deg) }
  100% { transform: scale(1) rotate(0) }
}
.wl-heart.pop{ animation: m-heart-deposit 420ms var(--m-e-cool) !important }

/* ═══════════════════════════════════════════════════════════════════════
   9 · PAGE TRANSITIONS.  The old page retracts, the new one deposits.
   ═══════════════════════════════════════════════════════════════════════ */

::view-transition-old(root){
  animation: m-page-out 170ms var(--m-e-retract) both;
}
::view-transition-new(root){
  animation: m-page-in var(--m-t-4) var(--m-e-deposit) both;
}
@keyframes m-page-out{
  0%   { opacity: 1; transform: translate3d(0, 0, 0) scale(1) }
  100% { opacity: 0; transform: translate3d(0, -6px, 0) scale(.995) }
}
/* The incoming page settles toward you rather than rising from the
   bottom. Navigation happens dozens of times a session, so it gets the
   quietest gesture in the system: no direction, no wipe, just depth. */
@keyframes m-page-in{
  0%   { opacity: 0; transform: scale(1.012) }
  20%  { opacity: 1 }
  100% { opacity: 1; transform: scale(1) }
}
/* Going back reverses the build, so history feels physical. */
::view-transition-old(root):only-child{ animation-duration: 140ms }

/* ═══════════════════════════════════════════════════════════════════════
   10 · THE BUILD RAIL.  Scroll position as build height.
   ───────────────────────────────────────────────────────────────────────
   Pure CSS scroll-driven animation where supported, so it costs the main
   thread nothing at all. Browsers without animation-timeline simply never
   show it, which is the correct degradation for an ambient element.
   ═══════════════════════════════════════════════════════════════════════ */

.m-rail{
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 2px;
  z-index: 40;
  pointer-events: none;
  display: none;
  /* Layer ticks: the rail is a repeating stack of layers, not a bar. */
  background: repeating-linear-gradient(
    180deg,
    var(--m-layerline) 0 1px,
    transparent 1px 9px
  );
}
.m-rail > i{
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: top center;
  transform: scaleY(0);
  background: linear-gradient(180deg, var(--accent-l), var(--accent));
  box-shadow: 0 0 8px var(--m-heat-soft);
}

@supports (animation-timeline: scroll()){
  .m-rail{ display: block }
  .m-rail > i{
    animation: m-rail-fill linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes m-rail-fill{
  from{ transform: scaleY(0) }
  to  { transform: scaleY(1) }
}
@media (max-width: 768px){
  .m-rail{ width: 2px; background: none }
}

/* ═══════════════════════════════════════════════════════════════════════
   11 · NAVIGATION.  The active tab extrudes.
   ═══════════════════════════════════════════════════════════════════════ */

.ta-bottomnav a, .tabn-item{ transition: var(--m-tr-link) }
.tabn-ico{ transition: transform var(--m-t-2) var(--m-e-cool) }
.tabn-item.active .tabn-ico,
.ta-bottomnav a.active .tabn-ico{
  transform: translate3d(0, -2px, 0) scale(1.1);
}
/* The active item sits on a deposited layer rather than being underlined
   in the usual way. */
.tabn-item.active::before,
.ta-bottomnav a.active::before{
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 22px;
  height: 2px;
  border-radius: 0 0 2px 2px;
  background: var(--accent);
  transform: translate3d(-50%, 0, 0) scaleX(0);
  animation: m-tab-lay var(--m-t-3) var(--m-e-gantry) both;
}
@keyframes m-tab-lay{
  from{ transform: translate3d(-50%, 0, 0) scaleX(0) }
  to  { transform: translate3d(-50%, 0, 0) scaleX(1) }
}

/* Header links get a hairline that draws from the left, matching fields
   and tabs, so the whole navigation layer shares one gesture. */
nav.main-nav a{ position: relative; transition: var(--m-tr-link) }
@media (hover: hover) and (pointer: fine){
  nav.main-nav a::after{
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -3px;
    height: 1px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--m-t-2) var(--m-e-gantry);
  }
  nav.main-nav a:hover::after{ transform: scaleX(1) }
}

/* ═══════════════════════════════════════════════════════════════════════
   12 · MEDIA.  Images resolve, they do not pop.
   ═══════════════════════════════════════════════════════════════════════ */

img.m-img{ opacity: 0 }
img.m-img.m-loaded{
  animation: m-settle var(--m-t-4) var(--m-e-cool) both;
}

/* Product galleries advance on the gantry curve so a swipe feels
   mechanical and precise instead of elastic. */
.m-gallery-track{ transition: transform var(--m-t-4) var(--m-e-gantry); will-change: transform }
.m-gallery-track.m-dragging{ transition: none }

/* ═══════════════════════════════════════════════════════════════════════
   13 · ACCESSIBILITY AND POWER.
   ───────────────────────────────────────────────────────────────────────
   Reduced motion is not "faster motion", it is no motion. Everything
   resolves to its finished state instantly, and the ambient pieces (rail,
   specular sweep, loader stack) are removed rather than sped up.
   ═══════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce){
  [data-m], [data-m].m-in, .fade-in-up, .fade-in-up.visible, img.m-img, img.m-img.m-loaded{
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    filter: none !important;
  }
  [data-m="label"].m-perim::after{ animation: none !important; transform: scaleX(1) }
  .m-rail, .m-flow{ display: none !important }
  .prod-card::after, .story-card::after, .test-card::after, .m-slab::after{ display: none !important }
  .m-loader > i{ animation: none !important; transform: scaleX(1); opacity: .55 }
  .skeleton{ animation: none !important }
  ::view-transition-old(root), ::view-transition-new(root){ animation: none !important }
  .m-modal, .m-sheet, .m-drawer, .m-pop, .m-scrim{ animation: none !important; opacity: 1; transform: none }

  /* Anything left that still MOVES rather than just recolours. Reduced
     motion is about motion, so these keep their colour and opacity
     transitions and lose only the travel. */
  .toast{ transform: none !important }
  .m-tabline, .m-gallery-track{ transition: none !important }
  .m-ico-btn > svg, .m-ico-btn > i,
  .m-ico-btn:hover > svg, .m-ico-btn:hover > i,
  .m-ico-btn:active > svg, .m-ico-btn:active > i{ transform: none !important; transition: none !important }
  .prod-card:hover, .story-card:hover, .test-card:hover, .trust-card:hover, .m-slab:hover,
  .prod-card:active, .story-card:active, .test-card:active, .trust-card:active, .m-slab:active,
  .btn:active, .btn-primary:active, .btn-secondary:active, .btn-accent:active,
  .btn-dark:active, .btn-outline:active, .btn-ghost:active, .btn-add-cart:active,
  .add-to-cart-btn:active, button.m-btn:active,
  .story-filter:active, .fp-pill:active, .m-chip:active{ transform: none !important }
  .m-field::after{ transition: none !important }
  nav.main-nav a::after{ transition: none !important }
  /* The active tab marker must end up drawn, not stuck on its 0% frame,
     so it is pinned to the finished state rather than merely un-animated. */
  .tabn-item.active::before, .ta-bottomnav a.active::before{
    animation: none !important;
    transform: translate3d(-50%, 0, 0) scaleX(1) !important;
  }
}

/* Save-Data and very low end devices drop the ambient layer too. */
@media (prefers-reduced-data: reduce){
  .m-rail{ display: none !important }
  .prod-card::after, .story-card::after, .test-card::after, .m-slab::after{ display: none !important }
}
