/* ============================================================
   ANIMATION KIT — anim.css
   Vanilla CSS animation system for hand-written static sites.
   Rules baked in:
   - Content is VISIBLE by default. Hidden pre-animation states are
     applied ONLY when (a) JS is running (html.js) AND (b) the user
     has NOT requested reduced motion. If JS fails, nothing hides.
   - Animate transform + opacity only (accordion + marquee noted).
   - Pair with anim.js. Load this INSIDE the site's <style> or as a
     linked stylesheet; load anim.js with `defer` before </body>.
   ============================================================ */

:root{
  /* --- Timing tokens (the whole site uses ONLY these) --- */
  --dur-press:   100ms;  /* button :active feedback              */
  --dur-fast:    180ms;  /* hovers, underlines, small fades      */
  --dur-base:    280ms;  /* dropdowns, header state, accordions  */
  --dur-reveal:  700ms;  /* scroll reveals                       */
  --dur-hero:    900ms;  /* hero entrance pieces                 */
  /* --- Easing tokens --- */
  --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);   /* entrances: fast start, long soft settle (default) */
  --ease-std:    cubic-bezier(0.4, 0, 0.2, 1);    /* Material standard: on-screen movement             */
  --ease-in:     cubic-bezier(0.4, 0, 1, 1);      /* exits: accelerate away                            */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);/* tiny overshoot — badges/ticks ONLY, use rarely    */
  --ease-marquee: linear;                          /* constant speed is correct for tickers             */
  --stagger:     80ms;   /* delay between siblings in a reveal group */
}

/* ---------- Smooth anchor scrolling (motion-safe only) ---------- */
@media (prefers-reduced-motion: no-preference){
  html{ scroll-behavior:smooth; }
}
html{ scroll-padding-top:96px; } /* anchors land below the sticky header */

/* ---------- Page-load fade (opacity only, content never hidden) ---------- */
@media (prefers-reduced-motion: no-preference){
  main{ animation: pageIn 350ms var(--ease-out); }
}
@keyframes pageIn{ from{ opacity:0; } to{ opacity:1; } }

/* ============================================================
   1) SCROLL REVEAL — fade / slide / scale, with stagger
   Markup:  <div data-reveal>…</div>
            <div data-reveal="left|right|zoom">…</div>
            group stagger: add data-reveal to each child of a
            container with [data-reveal-group] — anim.js sets delays.
   ============================================================ */
@media (prefers-reduced-motion: no-preference){
  .js [data-reveal]{
    opacity:0;
    transform:translateY(26px);
    transition:
      opacity   var(--dur-reveal) var(--ease-out),
      transform var(--dur-reveal) var(--ease-out);
    transition-delay:var(--rd, 0ms);
  }
  .js [data-reveal="left"] { transform:translateX(-32px); }
  .js [data-reveal="right"]{ transform:translateX(32px); }
  .js [data-reveal="zoom"] { transform:scale(0.94); }
  .js [data-reveal="fade"] { transform:none; }
  .js [data-reveal].is-in{ opacity:1; transform:none; }
}

/* ============================================================
   2) HERO ENTRANCE SEQUENCE — pure CSS, runs once on load.
   Markup: children of .hero-seq get --i:0,1,2… inline
           (style="--i:1") or rely on nth-child below.
   Base state is visible; keyframes animate FROM hidden, so with
   animations off/reduced the final state simply shows.
   ============================================================ */
@media (prefers-reduced-motion: no-preference){
  .hero-seq > *{
    animation: heroUp var(--dur-hero) var(--ease-out) both;
    animation-delay:calc(var(--i, 0) * 120ms);
  }
  .hero-seq > *:nth-child(1){ --i:0; } .hero-seq > *:nth-child(2){ --i:1; }
  .hero-seq > *:nth-child(3){ --i:2; } .hero-seq > *:nth-child(4){ --i:3; }
  .hero-seq > *:nth-child(5){ --i:4; } .hero-seq > *:nth-child(6){ --i:5; }
}
@keyframes heroUp{
  from{ opacity:0; transform:translateY(28px); }
  to  { opacity:1; transform:translateY(0); }
}

/* ============================================================
   3) HOVER MICRO-INTERACTIONS (fine pointers only — no sticky
      hover states on touch screens)
   ============================================================ */
@media (hover:hover) and (pointer:fine){

  /* Buttons: lift on hover, press-in on click */
  .btn{
    transition: transform var(--dur-fast) var(--ease-out),
                background-color var(--dur-fast) var(--ease-std);
    will-change:transform;
  }
  .btn:hover{ transform:translateY(-2px); }
  .btn:active{ transform:translateY(0) scale(0.98); transition-duration:var(--dur-press); }

  /* Cards: lift + a shadow that fades via a pseudo-element
     (animating box-shadow directly forces paint every frame;
     fading a pre-rendered shadow's OPACITY is compositor-cheap) */
  .card{ position:relative; transition:transform var(--dur-base) var(--ease-out); }
  .card::after{
    content:""; position:absolute; inset:0; border-radius:inherit;
    box-shadow:0 18px 38px rgba(20,20,20,0.16);
    opacity:0; transition:opacity var(--dur-base) var(--ease-std);
    pointer-events:none;
  }
  .card:hover{ transform:translateY(-5px); }
  .card:hover::after{ opacity:1; }

  /* Image zoom inside a clipped frame */
  .zoom{ overflow:hidden; }
  .zoom img{ transition:transform 600ms var(--ease-out); }
  .zoom:hover img{ transform:scale(1.06); }

  /* Animated link underline (grows from the left) */
  .u-link{
    background-image:linear-gradient(currentColor, currentColor);
    background-size:0% 2px; background-repeat:no-repeat; background-position:0 100%;
    transition:background-size var(--dur-base) var(--ease-out);
    padding-bottom:2px;
  }
  .u-link:hover{ background-size:100% 2px; }
}

/* ============================================================
   4) MARQUEE / TICKER BAND
   Markup: <div class="marquee"><div class="marquee-track">
             <span>…items…</span><span aria-hidden="true">…same items…</span>
           </div></div>
   The track holds the content TWICE; translating -50% loops it.
   ============================================================ */
.marquee{ overflow:hidden; }
.marquee-track{
  display:flex; width:max-content;
  animation: marquee 32s var(--ease-marquee) infinite;
}
.marquee:hover .marquee-track{ animation-play-state:paused; }
@keyframes marquee{ to{ transform:translateX(-50%); } }
@media (prefers-reduced-motion: reduce){
  .marquee-track{ animation:none; flex-wrap:wrap; width:auto; }
}

/* ============================================================
   5) FAQ / ACCORDION — smooth open-close via the grid-rows trick.
   HONEST NOTE: opening an accordion inherently changes height
   (layout). Content blocks are small, so this one exception is
   cheap and is what professional sites do. JS toggles .open and
   aria-expanded (anim.js §5). Animate nothing else here.
   Markup: <div class="acc"><button class="acc-btn" aria-expanded="false">
             Q <span class="acc-ic">+</span></button>
           <div class="acc-panel"><div class="acc-inner">…</div></div></div>
   ============================================================ */
.acc-panel{
  display:grid; grid-template-rows:1fr; /* no-JS: answers stay OPEN and readable */
  transition:grid-template-rows var(--dur-base) var(--ease-out);
}
.js .acc-panel{ grid-template-rows:0fr; }        /* collapse only once JS can re-open it */
.js .acc.open .acc-panel{ grid-template-rows:1fr; }
.acc-inner{ overflow:hidden; min-height:0; }
.acc-ic{ display:inline-block; transition:transform var(--dur-base) var(--ease-out); }
.acc.open .acc-ic{ transform:rotate(45deg); } /* + becomes × */

/* ============================================================
   6) STICKY HEADER STATE — anim.js (§6) adds .is-scrolled after
      the hero sentinel leaves view. Background/ shadow swap via
      a pre-rendered shadow fading in (opacity) + transform nudge.
   ============================================================ */
.site-header{
  transition: background-color var(--dur-base) var(--ease-std),
              box-shadow var(--dur-base) var(--ease-std);
}
.site-header.is-scrolled{
  box-shadow:0 10px 30px rgba(20,20,20,0.10);
  /* also swap a modifier colour here if the design has one, e.g.
     background-color:var(--header-solid); */
}

/* ============================================================
   7) GALLERY SLIDER (vanilla JS, anim.js §7)
   Markup: <div class="slider"><div class="slider-track">
             <figure>…</figure>… </div>
           <button class="slider-prev" aria-label="Previous photo">‹</button>
           <button class="slider-next" aria-label="Next photo">›</button>
           <div class="slider-dots"></div></div>
   Slides move with transform only. No autoplay by default.
   ============================================================ */
.slider{ position:relative; overflow:hidden; }
.slider-track{
  display:flex;
  transition:transform 550ms var(--ease-out);
}
.slider-track > *{ flex:0 0 100%; min-width:0; }
.slider-prev,.slider-next{
  position:absolute; top:50%; transform:translateY(-50%);
  transition:opacity var(--dur-fast) var(--ease-std), transform var(--dur-press) var(--ease-out);
}
.slider-prev{ left:12px; } .slider-next{ right:12px; }
.slider-prev:active,.slider-next:active{ transform:translateY(-50%) scale(0.92); }
.slider-dots{ display:flex; gap:8px; justify-content:center; }
.slider-dots button{
  width:9px; height:9px; border-radius:50%; border:0; padding:0;
  background:currentColor; opacity:0.3;
  transition:opacity var(--dur-fast) var(--ease-std), transform var(--dur-fast) var(--ease-out);
}
.slider-dots button[aria-current="true"]{ opacity:1; transform:scale(1.25); }
@media (prefers-reduced-motion: reduce){
  .slider-track{ transition:none; }
}

/* ============================================================
   8) BEFORE / AFTER SLIDER (anim.js §8)
   Markup: <div class="ba">
             <img class="ba-after"  src="after.jpg"  alt="After treatment">
             <img class="ba-before" src="before.jpg" alt="Before treatment">
             <input class="ba-range" type="range" min="0" max="100" value="50"
                    aria-label="Reveal before and after">
             <span class="ba-line"></span>
           </div>
   --pos is set from the range input; clip-path moves no layout.
   ============================================================ */
.ba{ position:relative; --pos:50%; }
.ba img{ display:block; width:100%; }
.ba-before{
  position:absolute; inset:0; height:100%; object-fit:cover;
  clip-path:inset(0 calc(100% - var(--pos)) 0 0);
}
.ba-line{
  position:absolute; top:0; bottom:0; left:var(--pos); width:2px;
  background:#fff; transform:translateX(-1px); pointer-events:none;
}
.ba-range{
  position:absolute; inset:0; width:100%; height:100%;
  opacity:0; cursor:ew-resize; margin:0;
}

/* ============================================================
   9) PARALLAX (subtle, transform-only — anim.js §9)
   Markup: <img data-parallax="0.12" alt="…"> inside an
   overflow:hidden frame. Factor = fraction of scroll travel
   (0.08–0.15 is the professional range; more looks amateur).
   The image must be slightly OVER-SIZED so edges never show:
   ============================================================ */
[data-parallax]{ scale:1.18; will-change:transform; }

/* ============================================================
   10) REDUCED-MOTION MASTER SWITCH
   Movement is replaced by gentle fades or nothing. Focus states
   and essential feedback are kept. JS checks the same query
   (anim.js) and skips rAF/observer animation work entirely.
   ============================================================ */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:0.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:0.01ms !important;
    scroll-behavior:auto !important;
  }
  [data-reveal]{ opacity:1 !important; transform:none !important; }
  [data-parallax]{ scale:1; transform:none !important; }
}


/* ============================================================
   LIVE / AMBIENT ADDITIONS (added 2026-10-21, round 2)
   Continuous motion that makes a page feel ALIVE — engineered
   so it can never hang the site:
   - Every loop below animates transform/opacity ONLY.
   - Every looping element carries data-ambient; anim.js (§10)
     pauses it when it leaves the viewport (IO → .offscreen) and
     when the tab is hidden (html.tab-hidden). A paused CSS
     animation costs ZERO per-frame work.
   - The reduced-motion master switch (§10) collapses all of
     these to static; the JS patterns check the same query and
     never start.
   - Budgets: at most ONE large-area ambient effect per viewport
     (Ken Burns hero OR aurora band, never both stacked), plus
     small accents (dot, steam). See the jank audit in the guide.
   ============================================================ */

/* ---------- Ambient pause plumbing (anim.js §10) ---------- */
[data-ambient].offscreen{ animation-play-state:paused; will-change:auto; }
html.tab-hidden [data-ambient]{ animation-play-state:paused; }

/* ============================================================
   11) KEN BURNS HERO DRIFT — the hero photograph breathes.
   Markup: <div class="kb-frame"><img class="kb" data-ambient
             src="hero.jpg" alt="…"></div>
   A barely-there scale 1 → 1.08 + 1.5% drift over 26s, alternate.
   Slower + smaller than you think: if a visitor can describe the
   movement, it is too strong. Transform-only = compositor.
   COST NOTE: this animates a FULL-SCREEN layer — the most
   expensive ambient effect in the kit. One per page, hero only,
   image ≤1600px wide. If the throttled-phone audit drops frames,
   this is the first effect to cut.
   ============================================================ */
.kb-frame{ overflow:hidden; }
.kb{
  width:100%; height:100%; object-fit:cover;
  will-change:transform;
  animation: kbDrift 26s ease-in-out infinite alternate;
}
@keyframes kbDrift{
  from{ transform:scale(1)    translate3d(0, 0, 0); }
  to  { transform:scale(1.08) translate3d(-1.5%, 1%, 0); }
}
@media (prefers-reduced-motion: reduce){ .kb{ animation:none; } }

/* ============================================================
   12) LIVE "OPEN NOW" DOT — a small pulsing ring says the
   business is alive right now. Pairs with anim.js §11, which
   computes real open/closed state from the business's hours.
   Markup: <span class="live-dot" aria-hidden="true"></span>
   The pulse animates a pseudo-element ring (transform scale +
   opacity fade) — the dot itself never moves. Tiny element,
   negligible raster cost. Closed state = still, muted dot.
   ============================================================ */
.live-dot{
  position:relative; display:inline-block; flex:none;
  width:9px; height:9px; border-radius:50%;
  background:#2FA84F; vertical-align:baseline;
}
.live-dot::after{
  content:""; position:absolute; inset:0; border-radius:50%;
  background:inherit;
  animation: dotPulse 2.2s var(--ease-out) infinite;
}
@keyframes dotPulse{
  0%  { transform:scale(1);   opacity:0.55; }
  70% { transform:scale(2.6); opacity:0; }
  100%{ transform:scale(2.6); opacity:0; }
}
.open-status[data-state="closed"] .live-dot{ background:#B0A89A; }
.open-status[data-state="closed"] .live-dot::after{ animation:none; }
@media (prefers-reduced-motion: reduce){ .live-dot::after{ animation:none; } }

/* ============================================================
   13) AURORA DRIFT — two soft colour blobs breathing behind a
   dark band (testimonials / CTA sections). The gradients are
   rasterised ONCE as layers; only their transforms + opacity
   animate, so the compositor just slides pre-painted layers.
   Markup: <div class="aurora" aria-hidden="true">
             <i data-ambient></i><i data-ambient></i></div>
   inside a position:relative; overflow:hidden band.
   Colours come from --aurora-1 / --aurora-2 (set per site).
   COST NOTE: large blurred areas are GPU-fill heavy on low-end
   phones — max 2 blobs, one band per page, opacity ≤0.5.
   ============================================================ */
.aurora{ position:absolute; inset:0; overflow:hidden; pointer-events:none; }
.aurora i{
  position:absolute; width:55%; aspect-ratio:1; border-radius:50%;
  filter:blur(42px); opacity:0.45; will-change:transform;
}
.aurora i:nth-child(1){
  left:-10%; top:-20%;
  background:radial-gradient(closest-side, var(--aurora-1, #C07A2C), transparent 70%);
  animation: auroraA 17s ease-in-out infinite alternate;
}
.aurora i:nth-child(2){
  right:-12%; bottom:-25%;
  background:radial-gradient(closest-side, var(--aurora-2, #1E3D2F), transparent 70%);
  animation: auroraB 21s ease-in-out infinite alternate;
}
@keyframes auroraA{ from{ transform:translate3d(0,0,0) scale(1); } to{ transform:translate3d(9%, 6%, 0) scale(1.15); } }
@keyframes auroraB{ from{ transform:translate3d(0,0,0) scale(1.1); } to{ transform:translate3d(-8%, -5%, 0) scale(0.95); } }
@media (prefers-reduced-motion: reduce){ .aurora i{ animation:none; } }

/* ============================================================
   14) STEAM WISPS — three blurred wisps rising over a chai /
   coffee / fresh-food photo corner. Each wisp is a tiny
   pre-blurred gradient (filter is STATIC — rasterised once,
   never animated); the loop moves transform + opacity only.
   Markup: <span class="steam" aria-hidden="true">
             <i data-ambient></i><i data-ambient></i><i data-ambient></i>
           </span> positioned over the image.
   ============================================================ */
.steam{ position:absolute; pointer-events:none; }
.steam i{
  position:absolute; bottom:0; width:10px; height:44px; border-radius:50%;
  background:radial-gradient(closest-side, rgba(255,255,255,0.8), rgba(255,255,255,0) 72%);
  filter:blur(4px); opacity:0;
  animation: steamRise 6s ease-in-out infinite;
}
/* Asymmetric stagger (Bruzora study, 2026-10-21): every wisp runs a DIFFERENT
   duration and delay — synchronised loops look mechanical, offset loops look
   like real steam. Opacity never peaks above ~0.45: subtlety is the premium. */
.steam i:nth-child(1){ left:0;    animation-duration:5.2s; animation-delay:0s; }
.steam i:nth-child(2){ left:14px; animation-duration:6.1s; animation-delay:1.8s; }
.steam i:nth-child(3){ left:28px; animation-duration:7s;   animation-delay:3.5s; }
@keyframes steamRise{
  0%  { transform:translate3d(0, 8px, 0)    scale(0.8);  opacity:0; }
  20% { opacity:0.45; }
  60% { transform:translate3d(4px, -26px, 0) scale(1.25); opacity:0.18; }
  100%{ transform:translate3d(-3px, -48px, 0) scale(1.5); opacity:0; }
}
@media (prefers-reduced-motion: reduce){ .steam{ display:none; } }

/* ============================================================
   15) CURTAIN IMAGE REVEAL — a solid panel sweeps away while
   the photo settles from a slight zoom. Both moves are
   transform-only (the panel is an overlay, NOT a clip-path, so
   no per-frame paint). Triggered once by anim.js §15 (.is-in).
   Markup: <figure class="curtain" data-curtain><img …></figure>
   Panel colour: --curtain (site palette), defaults to ink.
   No-JS / reduced-motion: no panel exists, photo just shows.
   ============================================================ */
@media (prefers-reduced-motion: no-preference){
  .js .curtain{ position:relative; overflow:hidden; }
  .js .curtain::after{
    content:""; position:absolute; inset:0; z-index:2;
    background:var(--curtain, #22312A);
    transform:scaleX(1); transform-origin:right center;
  }
  .js .curtain img{ transform:scale(1.12); }
  .js .curtain.is-in::after{
    transform:scaleX(0);
    transition:transform 900ms var(--ease-out) 80ms;
  }
  .js .curtain.is-in img{
    transform:scale(1);
    transition:transform 1300ms var(--ease-out);
  }
}

/* ============================================================
   16) SVG LINE DRAWING — icons / dividers that draw themselves
   once when scrolled into view (anim.js §15 adds .is-in).
   Markup: <svg class="draw" viewBox="…">
             <path class="draw-el" pathLength="100" fill="none" …/>
           </svg>
   pathLength="100" normalises every shape, so ONE dash rule
   serves all. Stagger siblings with style="--i:1" etc.
   stroke-dashoffset is a paint property, NOT compositor — keep
   drawings SMALL (icons, underlines, a logo mark), never a
   full-screen illustration. No-JS: shapes render fully drawn.
   ============================================================ */
.js .draw .draw-el{ stroke-dasharray:100; stroke-dashoffset:100; }
.js .draw.is-in .draw-el{
  stroke-dashoffset:0;
  transition:stroke-dashoffset 1200ms var(--ease-out);
  transition-delay:calc(var(--i, 0) * 160ms);
}
@media (prefers-reduced-motion: reduce){
  .js .draw .draw-el{ stroke-dashoffset:0; transition:none; }
}

/* ============================================================
   17) SCROLL PROGRESS — a 3px bar that fills as the visitor
   reads. PURE CSS scroll-driven animation: the compositor runs
   it, there is NO JavaScript and NO scroll listener at all.
   Markup: <div class="scroll-progress" aria-hidden="true"></div>
   Colour: set .scroll-progress{ background:… } in site CSS
   (defaults to currentColor).
   Unsupported browsers (older Safari/Firefox): the bar simply
   never appears — decorative, so absence is the graceful path.
   ============================================================ */
.scroll-progress{
  position:fixed; top:0; left:0; z-index:90;
  width:100%; height:3px; background:currentColor;
  transform-origin:left center; transform:scaleX(0);
  pointer-events:none;
}
@supports (animation-timeline: scroll()){
  .scroll-progress{
    animation: spFill linear both;
    animation-timeline:scroll(root);
  }
}
@keyframes spFill{ from{ transform:scaleX(0); } to{ transform:scaleX(1); } }
@media (prefers-reduced-motion: reduce){ .scroll-progress{ display:none; } }

/* ============================================================
   18) TILT CARDS — pointer-reactive tilt + travelling glare
   (anim.js §12). The card rotates ≤6° in 3D; the glare is a
   pre-rendered radial-gradient layer SLID by transform, so
   both stay on the compositor. Fine pointers only — JS checks
   matchMedia('(hover:hover) and (pointer:fine)') and reduced
   motion before wiring anything.
   Markup: <div class="tilt" data-tilt>
             <span class="tilt-glare" aria-hidden="true"></span> …
           </div>
   ============================================================ */
.tilt{ position:relative; overflow:hidden; transform-style:preserve-3d; }
.tilt-glare{
  position:absolute; inset:-45%; z-index:1; pointer-events:none;
  background:radial-gradient(circle at center, rgba(255,255,255,0.26), rgba(255,255,255,0) 62%);
  opacity:0; transition:opacity var(--dur-base) var(--ease-std);
}
.tilt.is-tilting .tilt-glare{ opacity:1; }
.tilt.snap-back{ transition:transform 320ms var(--ease-out); }

/* ============================================================
   19) MAGNETIC BUTTONS — the primary CTA leans a few px toward
   the cursor and springs home on leave (anim.js §13).
   Markup: <a class="btn magnetic" data-magnetic href="…">…</a>
   Transform-only, max travel ~6px — a lean, never a chase.
   ============================================================ */
.magnetic{ display:inline-block; }
.magnetic.snap-back{ transition:transform 240ms var(--ease-spring); }

/* ============================================================
   20) ROTATING HEADLINE WORD — one word in the headline swaps
   every ~2.6s (anim.js §14): "Fresh ___ daily" →
   bakes / brews / smiles. The track steps with transform; the
   window is exactly one line tall, overflow hidden.
   Markup: <span class="rotator"><span class="rotator-track">
             <span>bakes</span><span>smiles</span>…
           </span></span>
   No-JS / reduced-motion / off-screen: the FIRST word shows,
   statically — the sentence must read correctly with word one.
   ============================================================ */
.rotator{
  display:inline-block; overflow:hidden; vertical-align:bottom;
  height:1.22em; text-align:left;
}
.rotator-track{ display:block; transition:transform 600ms var(--ease-out); }
.rotator-track > span{ display:block; height:1.22em; line-height:1.22em; }
@media (prefers-reduced-motion: reduce){
  .rotator-track{ transition:none; }
}

/* ============================================================
   21) GLOW PULSE + DUST MOTES — animate the LIGHT, not just the
   object (Bruzora study, 2026-10-21). A warm radial glow that
   breathes behind a hero + tiny motes drifting up through it.
   Oven glow (bakery), ember glow (grill), morning window (cafe)
   are the same two classes with brand colours via --glow.
   Markup: <div class="glow-pulse" data-ambient aria-hidden="true"></div>
           <span class="dust" aria-hidden="true"><i data-ambient></i>×5</span>
   ============================================================ */
.glow-pulse{
  position:absolute; inset:auto; width:120%; padding-top:120%; left:50%; top:50%;
  translate:-50% -50%; border-radius:50%; pointer-events:none;
  background:radial-gradient(closest-side, var(--glow, rgba(255,176,92,0.34)), transparent 70%);
  animation: glowPulse 9s ease-in-out infinite;
}
@keyframes glowPulse{
  0%,100%{ opacity:0.75; transform:scale(1); }
  50%    { opacity:1;    transform:scale(1.04); }
}
.dust{ position:absolute; inset:0; pointer-events:none; overflow:hidden; }
.dust i{
  position:absolute; bottom:12%; width:5px; height:5px; border-radius:50%;
  background:radial-gradient(closest-side, rgba(255,240,214,0.9), rgba(255,240,214,0) 70%);
  opacity:0; animation: dustRise 11s ease-in-out infinite;
}
.dust i:nth-child(1){ left:18%; animation-duration:10s; animation-delay:0s; }
.dust i:nth-child(2){ left:34%; animation-duration:13s; animation-delay:2.6s; }
.dust i:nth-child(3){ left:52%; animation-duration:11s; animation-delay:5.1s; }
.dust i:nth-child(4){ left:68%; animation-duration:14s; animation-delay:1.4s; }
.dust i:nth-child(5){ left:84%; animation-duration:12s; animation-delay:3.8s; }
@keyframes dustRise{
  0%  { transform:translate3d(0, 8px, 0) scale(0.8); opacity:0; }
  20% { opacity:0.55; }
  70% { opacity:0.25; }
  100%{ transform:translate3d(12px, -90px, 0) scale(1.15); opacity:0; }
}
@media (prefers-reduced-motion: reduce){ .glow-pulse, .dust{ display:none; } }

/* ============================================================
   22) SUBJECT ACCENTS — one small loop per business's best
   moment (library: research/bruzora-study.md §4/§4b). All
   transform/opacity, all data-ambient-pausable, all subtle.
   .twinkle  sparkle points (teeth, varq, zari, dew) — set
             style="--d:1.3s" per point for the stagger.
   .flicker  candle / ember jitter — amplitude tiny on purpose.
   .drip     melt drip on a graphic edge (ice cream, cheese).
   .petal    falling petal / leaf drift (florist, wellness).
   .breath   guided-breath circle, 4s in / 4s out (physio/yoga).
   .float-soft  gentle ±10px float (badges, bouquet cards).
   .shine-sweep light sweep across a photo (clinic, salon,
             fabric) — pseudo-element, ≤0.15 effective opacity.
   ============================================================ */
.twinkle{
  position:absolute; width:14px; height:14px; pointer-events:none;
  background:radial-gradient(closest-side, rgba(255,255,255,0.95), rgba(255,255,255,0) 68%);
  clip-path:polygon(50% 0, 62% 38%, 100% 50%, 62% 62%, 50% 100%, 38% 62%, 0 50%, 38% 38%);
  opacity:0; animation: twinkle 3.2s ease-in-out infinite; animation-delay:var(--d, 0s);
}
@keyframes twinkle{
  0%,100%{ transform:scale(0.6) rotate(0deg);  opacity:0; }
  50%    { transform:scale(1)   rotate(20deg); opacity:0.9; }
}
.flicker{ transform-origin:50% 100%; animation: flicker 2.8s ease-in-out infinite; }
@keyframes flicker{
  0%,100%{ transform:scaleY(1);    opacity:0.92; }
  30%    { transform:scaleY(1.06); opacity:1; }
  55%    { transform:scaleY(0.97); opacity:0.88; }
  78%    { transform:scaleY(1.04); opacity:0.97; }
}
.drip{ transform-origin:50% 0; animation: drip 7s ease-in-out infinite; }
@keyframes drip{ 0%,100%{ transform:scaleY(1); } 50%{ transform:scaleY(1.07); } }
.petal{
  position:absolute; top:-4%; pointer-events:none; opacity:0;
  animation: petalFall 11s linear infinite; animation-delay:var(--d, 0s);
}
@keyframes petalFall{
  0%  { transform:translate3d(0, 0, 0) rotate(0deg);    opacity:0; }
  12% { opacity:0.7; }
  88% { opacity:0.6; }
  100%{ transform:translate3d(26px, 46vh, 0) rotate(150deg); opacity:0; }
}
.breath{ animation: breath 8s ease-in-out infinite; }
@keyframes breath{ 0%,100%{ transform:scale(1); } 50%{ transform:scale(1.25); } }
.float-soft{ animation: floatY 6s ease-in-out infinite; }
@keyframes floatY{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-10px); } }
.shine-sweep{ position:relative; overflow:hidden; }
.shine-sweep::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:linear-gradient(105deg, transparent 42%, rgba(255,255,255,0.14) 50%, transparent 58%);
  transform:translateX(-120%); animation: shineMove 9s ease-in-out infinite;
}
@keyframes shineMove{ 0%,55%{ transform:translateX(-120%); } 90%,100%{ transform:translateX(120%); } }
@media (prefers-reduced-motion: reduce){
  .twinkle, .petal{ display:none; }
  .flicker, .drip, .breath, .float-soft, .shine-sweep::after{ animation:none; }
}

/* ============================================================
   23) SLIDE-IN MENU — the adopted standard (Bruzora's motion
   language; user decision 2026-10-21, study §7). Full-screen
   frosted panel sliding from the right on Bruzora's signature
   easeOutExpo curve, links staggering in, hamburger morphing
   to an X (middle bar shorter — Bruzora's own detail). Wired
   by anim.js §17: toggle, scroll-lock, Escape, REAL
   swipe-to-close. Brand it with --nav-bg / --nav-ink.
   Markup: <button class="nav-toggle" data-nav-toggle
             aria-label="Open menu" aria-expanded="false">
             <i></i><i></i><i></i></button>
           <nav class="nav-panel" data-nav-panel>
             <a style="--i:0" href="…">…</a> … </nav>
   ============================================================ */
.nav-toggle{
  position:relative; z-index:1002; width:44px; height:44px; padding:11px 9px;
  background:none; border:0; cursor:pointer;
  display:flex; flex-direction:column; justify-content:space-between;
}
.nav-toggle i{
  display:block; height:2px; width:100%; border-radius:2px;
  background:var(--nav-ink, #f5ead6);
  transition:transform 0.3s ease-in-out, opacity 0.3s ease-in-out, width 0.3s ease-in-out;
}
.nav-toggle i:nth-child(2){ width:62%; } /* the shorter middle line */
html.nav-open .nav-toggle i:nth-child(1){ transform:translateY(9px) rotate(45deg); }
html.nav-open .nav-toggle i:nth-child(2){ opacity:0; }
html.nav-open .nav-toggle i:nth-child(3){ transform:translateY(-9px) rotate(-45deg); }
.nav-panel{
  position:fixed; inset:0; z-index:1001; display:flex; flex-direction:column;
  align-items:center; justify-content:flex-start; gap:2.4rem;
  /* First-item rule (user-flagged 2026-10-22, Sheetal's): the old
     justify-content:center with NO padding and NO scrolling centred the
     overflow too — a tall menu (9 links + foot ≈ 800px) pushed its first
     links above the top edge of a 568–740px phone screen, jammed into the
     top-bar zone and unreachable. Now: clear space is reserved at the top,
     the panel scrolls, and auto margins on the first/last child keep a
     short menu visually centred (they collapse to 0 when space runs out,
     so the first item can never be clipped off-screen again). */
  padding:104px 24px 40px; overflow-y:auto; overflow-x:hidden;
  background:var(--nav-bg, rgba(18, 14, 10, 0.94));
  -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  touch-action:pan-y; /* vertical stays native; horizontal drags reach §17 */
  transform:translateX(100%); visibility:hidden;
  transition:transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), visibility 0s 0.45s;
}
.nav-panel > :first-child{ margin-top:auto; }
.nav-panel > :last-child{ margin-bottom:auto; }
.nav-panel a{
  color:var(--nav-ink, #f5ead6); text-decoration:none; font-size:1.45rem;
  display:flex; align-items:center; justify-content:center; text-align:center;
  min-height:44px; padding:6px 16px; /* ≥44px tap target — was a bare ~38px line box */
  opacity:0; transform:translateY(14px);
  transition:opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay:0s;
}
html.nav-open{ overflow:hidden; } /* scroll-lock while the menu is open */
html.nav-open .nav-panel{
  transform:translateX(0); visibility:visible;
  transition:transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
html.nav-open .nav-panel a{
  opacity:1; transform:translateY(0);
  transition-delay:calc(90ms + var(--i, 0) * 60ms); /* the stagger Trilokpur skipped */
}
.nav-panel.dragging{ transition:none; } /* anim.js §17 swipe-to-close takes over */
@media (prefers-reduced-motion: reduce){
  .nav-panel{ transition:opacity 0.2s; transform:none; opacity:0; }
  html.nav-open .nav-panel{ opacity:1; transition:opacity 0.2s; }
  .nav-panel a{ transform:none; transition-delay:0s; }
}

/* ============================================================
   24) REVIEWS STRIP — Bruzora's card anatomy, motion done our
   way (study §6/§8): native drag-scroll with snap (zero JS
   weight) + optional gentle auto-drift from anim.js §18 that
   pauses on hover / touch / off-screen / reduced-motion.
   Card anatomy: initial avatar, name, gold stars, quoted text,
   "✓ VERIFIED" pill + source pill (real reviews only — never
   fabricated; no date unless the source shows one).
   Markup: <div class="rev-strip" data-rev-strip>
             <article class="rev-card"> <span class="rev-ava">S</span>
               <h4>Name</h4> <span class="rev-stars">★★★★★</span>
               <p>"…"</p> <span class="rev-pill">✓ Verified review</span>
               <span class="rev-pill rev-src">Source: Google</span>
             </article> … </div>
   ============================================================ */
.rev-strip{
  display:flex; gap:1rem; overflow-x:auto; padding:0.5rem 0.25rem 1rem;
  scroll-snap-type:x mandatory; scrollbar-width:none; cursor:grab;
}
.rev-strip::-webkit-scrollbar{ display:none; }
.rev-strip:active{ cursor:grabbing; }
.rev-card{
  flex:0 0 84%; scroll-snap-align:start; border-radius:14px; padding:1.3rem 1.4rem;
  background:var(--rev-bg, rgba(127, 110, 84, 0.08));
}
@media (min-width:760px){ .rev-card{ flex-basis:31.5%; } }
.rev-ava{
  display:inline-flex; align-items:center; justify-content:center;
  width:42px; height:42px; border-radius:50%; font-weight:700;
  background:var(--rev-ava-bg, #8B5E3C); color:var(--rev-ava-ink, #FFF7EA);
}
.rev-stars{ color:var(--rev-star, #D9A441); letter-spacing:2px; }
.rev-pill{
  display:inline-block; padding:0.18rem 0.6rem; border-radius:999px;
  font-size:0.68rem; letter-spacing:0.08em; text-transform:uppercase;
  background:var(--rev-pill-bg, #D9A441); color:#221806;
}
.rev-pill.rev-src{ background:transparent; border:1px solid currentColor; color:inherit; opacity:0.75; }
