/**
 * Buleedi — motion system.
 *
 * Five behaviours, nothing else. Every animated thing on this site uses one of
 * them; no page file defines its own keyframes. That is the difference between
 * a site that feels designed and one that feels decorated.
 *
 * The user here is a shopkeeper reordering forty cases, often one-handed, often
 * on a patchy connection. Motion has to make the interface clearer — never make
 * them wait to find out what happened.
 *
 * Rules baked into this file:
 *   - transform and opacity only. Never height, top, width, margin.
 *   - nothing longer than 420ms.
 *   - prefers-reduced-motion DISABLES it. Not softens — disables.
 *   - no library. This file plus ~40 lines of vanilla JS.
 */

:root {
  --ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 120ms;
  --dur-base: 240ms;
  --dur-slow: 420ms;
}

/* ------------------------------------------------------------ 1. reveal
 * Sections rise 16px as they enter. Applied by JS adding .is-in, so content
 * is visible to anything that never runs the script — a crawler, a browser
 * with JS off, or a failed request. The hidden state is only ever applied
 * once JS has confirmed it can remove it again.
 */
.js-reveal[data-reveal-armed] {
  opacity: 0;
  transform: translate3d(0, 16px, 0);
}
.js-reveal[data-reveal-armed].is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}

/* ------------------------------------------------------------ 2. stagger
 * Grid children arrive 40ms apart, capped at eight. A 24-product grid must
 * not take a full second to finish appearing; past the eighth card everything
 * lands together.
 */
.js-stagger[data-reveal-armed] > * {
  opacity: 0;
  transform: translate3d(0, 12px, 0);
}
.js-stagger[data-reveal-armed].is-in > * {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.js-stagger[data-reveal-armed].is-in > *:nth-child(1) { transition-delay: 0ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(2) { transition-delay: 40ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(3) { transition-delay: 80ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(4) { transition-delay: 120ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(5) { transition-delay: 160ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(6) { transition-delay: 200ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(7) { transition-delay: 240ms; }
.js-stagger[data-reveal-armed].is-in > *:nth-child(n+8) { transition-delay: 280ms; }

/* ------------------------------------------------------------ 3. press
 * Immediate physical feedback. 120ms because anything slower feels like lag
 * on the fortieth tap, and this is a button people press forty times.
 */
.btn, .qty-btn {
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.btn:active, .qty-btn:active { transform: scale(0.97); }

.card {
  transition: box-shadow var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
/* Hover lift only where there is a real pointer. On a touch screen :hover
   sticks after a tap and leaves cards floating. */
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translate3d(0, -3px, 0); }
}

/* ------------------------------------------------------------ 4. count
 * The number itself is rolled by JS; this only stops the digits from
 * reflowing the line as their widths change.
 */
.js-count { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ 5. blur-up
 * The LQIP that images.php already produces sits behind the real image and
 * fades out once it loads. Zero layout shift: the wrapper holds the aspect
 * ratio from the first paint.
 */
.media {
  position: relative;
  overflow: hidden;
  background-size: cover;
  background-position: center;
}
.media > img,
.media > picture > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.media > img.is-loaded,
.media > picture > img.is-loaded { opacity: 1; }

/* ------------------------------------------------------------ 6. settle
 * A photograph eases down to rest as its section reveals — one gesture with
 * the section, not a second event competing with it. A still that simply
 * appears reads as a screenshot; one that settles reads as a camera coming
 * to rest.
 *
 * 640ms is the only duration on this site past 420ms, and it is allowed here
 * because nothing interactive is waiting on it: the photograph is visible the
 * whole time, only its scale is still resolving.
 */
.media > img,
.media > picture > img {
  transform: scale(1.045);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform 640ms var(--ease-out);
}
.media > img.is-loaded,
.media > picture > img.is-loaded,
.js-reveal.is-in .media > img,
.js-reveal.is-in .media > picture > img { transform: scale(1); }

/* ------------------------------------------------------------- 7. drift
 * Hero and plate photographs only. The image grows very slightly across the
 * whole time it is on screen, driven by the scroll timeline rather than by a
 * listener — no JS, no layout read, composited on the GPU.
 *
 * 6% is the ceiling. Past that the eye follows the movement instead of the
 * copy, and the crop starts revealing the edges that were cropped out.
 *
 * Browsers without animation-timeline match nothing here and get a static
 * photograph. There is no polyfill and no fallback branch.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .media-drift > img,
    .media-drift > picture > img {
      animation: photo-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes photo-drift {
      from { transform: scale(1.00); }
      to   { transform: scale(1.06); }
    }
  }
}

/* The hero photograph is the LCP element. It is never revealed, never faded
 * and never waits on a class from a deferred script: the largest paint on the
 * page must not be gated behind JavaScript that may not arrive. Later in the
 * file than the blur-up rule above, so it wins on order, not on weight. */
.hero-bg > img,
.hero-bg > picture > img { opacity: 1; }

/* No-JS visitors never receive .is-loaded, so nothing may depend on it for
 * visibility. */
@media (scripting: none) {
  .media > img,
  .media > picture > img { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------- 8. brand
 *
 * THE MARK IS ALIVE. Owner's direction, 2026-08-14: the logo should move,
 * "here and there".
 *
 * This is the one ambient loop on the site, and it overrides a line in the
 * original brief ("no entrance animation on the header or nav"). That rule was
 * written against a header that flew in and made people wait; nothing here
 * gates content or delays a paint. It is worth being explicit about why it is
 * allowed, so the rule is not quietly assumed dead everywhere else: the header
 * still does not animate. The badge inside it does.
 *
 * Two gestures, deliberately separated onto two elements:
 *
 *   drift  the wrapper wanders a couple of pixels and leans a degree or so,
 *          on an 11s cycle with no two legs the same length, so it never
 *          reads as a metronome. Amplitude is tiny on purpose — this sits
 *          directly above a search field somebody is trying to type into, and
 *          movement in the corner of the eye while reading is the fastest way
 *          to make a page feel cheap.
 *
 *   gloss  a specular band crosses the badge in about 1.6s, then nothing for
 *          seven. The artwork is glossy — wet bottles, a chrome rim — and a
 *          highlight travelling over it is the light that surface implies.
 *          A constant sweep would be a loading spinner.
 *
 * Split across two elements because the hover lift also wants `transform`, and
 * two animations competing for one property on one element is the usual way
 * this effect breaks: the wrapper owns the drift, the <img> inside owns the
 * hover. Both are composited — transform and opacity only, no paint.
 */
@media (prefers-reduced-motion: no-preference) {
  .brand-badge { animation: brand-drift 11s var(--ease-in-out) infinite; }

  .brand-badge::after {
    content: "";
    position: absolute; inset: -40% -60%;
    pointer-events: none;
    background: linear-gradient(105deg,
      transparent 42%,
      rgba(255, 255, 255, .34) 50%,
      transparent 58%);
    transform: translate3d(-130%, 0, 0);
    animation: brand-gloss 9s var(--ease-in-out) infinite;
  }

  @keyframes brand-drift {
    0%   { transform: translate3d(0, 0, 0)      rotate(0deg); }
    19%  { transform: translate3d(2px, -3px, 0) rotate(-1.1deg); }
    43%  { transform: translate3d(-2px, -1px, 0) rotate(0.9deg); }
    64%  { transform: translate3d(1px, 3px, 0)  rotate(1.3deg); }
    86%  { transform: translate3d(-1px, 1px, 0) rotate(-0.6deg); }
    100% { transform: translate3d(0, 0, 0)      rotate(0deg); }
  }

  /* Sweeps in the first fifth of the cycle, then waits out the rest. */
  @keyframes brand-gloss {
    0%   { transform: translate3d(-130%, 0, 0); }
    18%  { transform: translate3d(130%, 0, 0); }
    100% { transform: translate3d(130%, 0, 0); }
  }
}

/* The hover, on the image rather than the drifting wrapper. Pointer only: on a
   touch screen :hover sticks after a tap and leaves the mark stuck at 107%. */
.brand-mark {
  transition: transform var(--dur-base) var(--ease-out),
              filter var(--dur-base) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .brand:hover .brand-mark { transform: scale(1.07); filter: saturate(1.12) brightness(1.05); }
}
.brand:focus-visible .brand-mark { transform: scale(1.07); }

/* ---------------------------------------------------------------- links */

.link-underline {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1.5px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size var(--dur-base) var(--ease-out);
  text-decoration: none;
}
.link-underline:hover,
.link-underline:focus-visible { background-size: 100% 1.5px; text-decoration: none; }

/* =========================================================================
 * THE OVERRIDE THAT MATTERS
 *
 * Someone who has asked their operating system for less motion has usually
 * done so because motion makes them ill. Halving a duration is not an answer
 * to that. Everything below removes animation outright and puts content at
 * its final position immediately — including anything JS armed before the
 * preference was read.
 * ====================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .js-reveal[data-reveal-armed],
  .js-stagger[data-reveal-armed] > * {
    opacity: 1 !important;
    transform: none !important;
  }
  .card:hover { transform: none !important; }
  /* Photographs land at their final scale immediately — settle and drift are
     removed outright, not shortened. Someone who asked their system for less
     motion is not asking for the same motion, faster. */
  .media > img,
  .media > picture > img {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }
  /* The badge sits still and the gloss is not drawn at all. The blanket rule
     above already collapses the duration, but a 0.001ms loop still leaves the
     mark wherever its first frame put it — this puts it back on its axis and
     removes the highlight outright. */
  .brand-badge,
  .brand-mark { animation: none !important; transform: none !important; }
  .brand-badge::after { display: none !important; }
}
