Le Fumé

Ô Long Sữa Rang

Mang hương hoa trắng, vị tươi mát và hậu béo sữa dịu dàng
Thức uống này được phục vụ với hai kích cỡ
Order on GrabFood 45,000₫
/* Tư Trà — Price flip animation (scoped to your button markup) */
.tt-price-anim .tu-tra-live-price.tt-price-flip{
  display:inline-block;
  position:relative;        /* anchor overlay */
  vertical-align:baseline;
}

/* overlay wrapper injected only during the flip */
.tt-price-anim .tu-tra-live-price .ttpf-wrap{
  position:absolute;
  inset:0;                  /* left:0; top:0; right:0; bottom:0 */
  overflow:hidden;
  pointer-events:none;
}

/* two lines that slide */
.tt-price-anim .tu-tra-live-price .ttpf-line{
  position:absolute;
  left:0; right:0;
  will-change:transform, opacity;
  transform:translateY(0);
  opacity:1;
  transition:transform 360ms cubic-bezier(.22,.61,.36,1), opacity 360ms linear;
  text-align:inherit;
  white-space:nowrap;
}

/* enter positions */
.tt-price-anim .tu-tra-live-price .ttpf-line.new-up{ transform:translateY(100%); }  /* price ↑ enters from bottom */
.tt-price-anim .tu-tra-live-price .ttpf-line.new-dn{ transform:translateY(-100%); } /* price ↓ enters from top   */

/* phase classes on wrapper */
.tt-price-anim .tu-tra-live-price .ttpf-anim-up  .old{ transform:translateY(-100%); opacity:0; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-up  .new{ transform:translateY(0%);    opacity:1; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-dn  .old{ transform:translateY(100%);  opacity:0; }
.tt-price-anim .tu-tra-live-price .ttpf-anim-dn  .new{ transform:translateY(0%);    opacity:1; }

(function(){
  // Host (.tt-price-anim) → we bind/rebind ONLY inside these
  var HOST_SEL  = '.tt-price-anim';
  var PRICE_SEL = '.tu-tra-live-price, #tu-tra-live-price, [data-tu-tra-live-price], [data-dynamic-tag="tu_tra_live_price"], [data-brx-bind*="tu_tra_live_price"], [data-brx-content*="tu_tra_live_price"]';

  function parsePrice(text){
    if(!text) return null;
    var t = String(text).replace(/\u00A0/g,' ').trim();
    if(!t || /không có/i.test(t)) return null;
    var digits = t.replace(/[^0-9]/g,'');
    if(!digits) return null;
    var n = parseInt(digits,10);
    return Number.isFinite(n) ? n : null;
  }

  function flipOnce(el, oldText, newText){
    if(oldText === newText) return;
    if(el.__ttpfBusy) { el.__ttpfPrev = newText; return; }
    el.__ttpfBusy = true;

    var wrap = document.createElement('span');
    wrap.className = 'ttpf-wrap';
    var lineOld = document.createElement('span');
    var lineNew = document.createElement('span');
    lineOld.className = 'ttpf-line old';
    lineNew.className = 'ttpf-line new';

    var oldN = parsePrice(oldText);
    var newN = parsePrice(newText);
    var up = true;
    if(oldN !== null && newN !== null) up = newN > oldN;
    lineNew.classList.add(up ? 'new-up' : 'new-dn');

    lineOld.textContent = oldText;
    lineNew.textContent = newText;

    // lock width/height to avoid jiggle during flip
    var rect = el.getBoundingClientRect();
    if(rect.width)  el.style.minWidth  = rect.width  + 'px';
    if(rect.height) el.style.minHeight = rect.height + 'px';

    wrap.appendChild(lineOld);
    wrap.appendChild(lineNew);
    el.appendChild(wrap);

    // set final text underneath (so DOM is correct during/after anim)
    el.textContent = newText;

    // kick animation next frame
    requestAnimationFrame(function(){
      wrap.classList.add(up ? 'ttpf-anim-up' : 'ttpf-anim-dn');
    });

    // cleanup
    var cleaned = false;
    function done(){
      if(cleaned) return; cleaned = true;
      if(wrap.parentNode) wrap.parentNode.removeChild(wrap);
      el.style.minWidth = el.style.minHeight = '';
      el.__ttpfPrev = newText;
      el.__ttpfBusy = false;
    }
    wrap.addEventListener('transitionend', done, { once:true });
    setTimeout(done, 650); // safety
  }

  function bindPrice(el){
    if(!el) return;
    if(el.__ttpfBound) return;
    el.__ttpfBound = true;
    el.__ttpfPrev = (el.textContent || '').trim();
    el.__ttpfBusy = false;
    el.classList.add('tt-price-flip');

    // Observe this price span for text changes; coalesce via RAF
    var mo = new MutationObserver(function(){
      if(!el.isConnected){ try{mo.disconnect();}catch(e){} return; }
      if(el.__ttpfRaf) cancelAnimationFrame(el.__ttpfRaf);
      el.__ttpfRaf = requestAnimationFrame(function(){
        var curr = (el.textContent || '').trim();
        var prev = el.__ttpfPrev || '';
        if(curr !== prev) flipOnce(el, prev, curr);
      });
    });

    try{
      mo.observe(el, { characterData:true, childList:true, subtree:true });
      el.__ttpfMO = mo;
    }catch(e){ /* ignore */ }
  }

  function bindHost(host){
    if(!host || !host.isConnected) return;
    // (re)bind price element inside this host
    var priceEl = host.querySelector(PRICE_SEL);
    if(priceEl) bindPrice(priceEl);

    // host-level observer: if the price span is replaced, bind again
    if(host.__ttpfHostMO) return;
    var hostMO = new MutationObserver(function(muts){
      // only react if a price element appears or is replaced inside host
      if(!host.isConnected){ try{hostMO.disconnect();}catch(e){} return; }
      var el = host.querySelector(PRICE_SEL);
      if(el && !el.__ttpfBound) bindPrice(el);
    });
    try{
      hostMO.observe(host, { childList:true, subtree:true });
      host.__ttpfHostMO = hostMO;
    }catch(e){ /* ignore */ }
  }

  function initAll(){
    document.querySelectorAll(HOST_SEL).forEach(bindHost);
  }

  // Initial + late load safety
  if(document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', initAll, { once:true });
  } else {
    initAll();
  }
  window.addEventListener('load', initAll, { once:true });

  // Optional: small, filtered page observer to catch hosts added later by Bricks
  var pageMO = new MutationObserver(function(muts){
    for (var i=0;i<muts.length;i++){
      var m = muts[i];
      for (var j=0;j<m.addedNodes.length;j++){
        var n = m.addedNodes[j];
        if(!(n instanceof Element)) continue;
        if(n.matches && n.matches(HOST_SEL)) bindHost(n);
        var sub = n.querySelectorAll ? n.querySelectorAll(HOST_SEL) : [];
        if(sub.length) sub.forEach(bindHost);
      }
    }
  });
  try{
    pageMO.observe(document.body, { childList:true, subtree:true });
  }catch(e){ /* ignore */ }
})();

Sáng sớm tĩnh lặng trên cao nguyên Lâm Đồng. Những làn sương mỏng vờn qua đồi chè đón nắng, nơi chồi trà non được hái tay một cách nâng niu.

Được chiết xuất bằng French press và quyện cùng kem sữa, Ô Long Sữa Nguyên Vị gợi mở cảm giác thanh sạch — hương hoa trắng thoảng nhẹ, khí mát của cao nguyên, và vị béo dịu đầy tinh tế.

Nguyên bản, hài hòa, sang trọng một cách nhẹ nhàng — như một nốt hương thanh thuần của buổi sớm đầu ngày.