feat: give payment its own page
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m12s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m35s

The PIX and card choices appeared under the cart, on the same page as the
customer's details. "Ir para o pagamento" now sends the order and opens
/pagamento, step 3 of the progress bar: the server's order summary, then PIX
or card, each opening below. The cart keeps only the sending progress and its
errors; a changed cart is sent again instead of offering the old quote.
Portal links open the payment page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-28 11:45:44 -03:00
parent 536510b148
commit 43043c361c
9 changed files with 113 additions and 40 deletions

View File

@@ -49,7 +49,7 @@ server {
# Always revalidate HTML/JS/CSS after a deployment. Without this, a browser # Always revalidate HTML/JS/CSS after a deployment. Without this, a browser
# can pair a new Kanban page with a cached older script after a rollout. # can pair a new Kanban page with a cached older script after a rollout.
# The Site's product pages and cart are addresses of the same page (web/site-pages.js). # The Site's product pages and cart are addresses of the same page (web/site-pages.js).
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ { location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho|pagamento)/?$ {
expires -1; expires -1;
try_files /index.html =404; try_files /index.html =404;
} }

View File

@@ -20,7 +20,7 @@ server {
client_max_body_size 2m; client_max_body_size 2m;
} }
# The Site's product pages and cart are addresses of the same page (web/site-pages.js). # The Site's product pages and cart are addresses of the same page (web/site-pages.js).
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ { location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho|pagamento)/?$ {
try_files /index.html =404; try_files /index.html =404;
} }
location / { try_files $uri $uri/ =404; } location / { try_files $uri $uri/ =404; }

View File

@@ -99,6 +99,10 @@ try{
try{ try{
// A cart the Site can price is approved at once: the customer pays now. // A cart the Site can price is approved at once: the customer pays now.
await waitFor(async()=> (await site.text()).includes('Total validado no servidor:'),'browser upload and automatic approval',45000); await waitFor(async()=> (await site.text()).includes('Total validado no servidor:'),'browser upload and automatic approval',45000);
// Payment is its own page: the cart form is gone and the order summary is shown.
assert.equal(await site.eval('location.pathname'),'/pagamento');
assert.equal(await site.eval('getComputedStyle(document.getElementById("carr")).display'),'none');
assert.equal(await site.eval('!document.getElementById("pagResumo").hidden && document.getElementById("pagResumo").textContent.includes("Total")'),true);
}catch(error){ }catch(error){
console.error('Checkout status:',await site.eval('document.getElementById("checkoutStatus")?.textContent')); console.error('Checkout status:',await site.eval('document.getElementById("checkoutStatus")?.textContent'));
throw error; throw error;

View File

@@ -1,7 +1,11 @@
/* Checkout bridge only: approved commercial functions in web/index.html stay intact. */ /* Checkout bridge only: approved commercial functions in web/index.html stay intact.
The cart sends the order; the quote is paid on its own page (/pagamento). */
(() => { (() => {
const status = document.getElementById('checkoutStatus'); const status = document.getElementById('checkoutStatus');
const actions = document.getElementById('checkoutActions'); const actions = document.getElementById('checkoutActions');
const resumo = document.getElementById('pagResumo');
const naPagina = () => document.documentElement.dataset.rota === 'pagamento';
let area = null;
let busy = false; let busy = false;
let draftId = localStorage.getItem('dtf-quote'); let draftId = localStorage.getItem('dtf-quote');
let requestKey = localStorage.getItem('dtf-request-key'); let requestKey = localStorage.getItem('dtf-request-key');
@@ -46,14 +50,39 @@
window.dtfApi=api; window.dtfApi=api;
ready.catch(error => { status.textContent = error.message; }); ready.catch(error => { status.textContent = error.message; });
function message(text) { status.textContent = text; } function message(text) { status.textContent = text; }
function button(label, handler) { function button(label, handler, into = actions) {
const el = document.createElement('button'); const el = document.createElement('button');
el.textContent = label; el.textContent = label;
el.style.cssText = 'margin:8px 8px 0 0;padding:8px 14px;cursor:pointer'; el.style.cssText = 'margin:8px 8px 0 0;padding:8px 14px;cursor:pointer';
el.onclick = handler; el.onclick = handler;
actions.append(el); into.append(el);
return el; return el;
} }
// What the customer is paying for, from the server's approval.
function pintaResumo(quote) {
const approved = quote && quote.status !== 'paid' ? quote.approved : null;
resumo.hidden = !approved;
if (!approved) { resumo.replaceChildren(); return; }
const linha = (label, value, className='l') => {
const row=document.createElement('div'); row.className=className;
const name=document.createElement('span'); name.textContent=label;
const amount=document.createElement('b'); amount.textContent=value;
row.append(name,amount); return row;
};
const rows = approved.items.map(item => linha(
(MODOS[item.mode]?.tit || item.mode)+' · '+fmtM(Number(item.billed_metres))+' m', rs(item.total_cents/100)));
const pickup = approved.freight.service === 'pickup';
rows.push(linha(pickup ? 'Retirada em Franca' : 'Frete',
approved.freight.total_cents ? rs(approved.freight.total_cents/100) : 'Grátis'));
rows.push(linha('Total', rs(approved.total_cents/100), 'tot'));
resumo.replaceChildren(...rows);
}
function semPedido() {
pintaResumo(null);
actions.replaceChildren();
message('Nenhum pedido aguardando pagamento.');
button('Ir para o carrinho', () => vaiPara(CARRINHO));
}
async function upload(file) { async function upload(file) {
const session=await ready; const session=await ready;
return window.dtfUpload(file,{api,progress:message,scope:session.cart_scope}); return window.dtfUpload(file,{api,progress:message,scope:session.cart_scope});
@@ -71,7 +100,9 @@
}; };
window.dtfCheckout = async () => { window.dtfCheckout = async () => {
if (busy) return; if (busy) return;
if (draftId) { await refresh(); status.scrollIntoView({behavior:'smooth',block:'nearest'}); return; } // The cart that was sent goes straight to its payment; a changed cart is sent again.
if (draftId && quotedCart === cartSnapshot()) { await refresh(true); return; }
if (draftId) clearDraft();
if (!clienteOk() || !entrega.cotado) return; if (!clienteOk() || !entrega.cotado) return;
if (!enderecoOk()) return message('Preencha o endereço de entrega.'); if (!enderecoOk()) return message('Preencha o endereço de entrega.');
if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.'); if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.');
@@ -108,19 +139,22 @@
const quote = await api('/quotes',{request_key:requestKey,...content}); const quote = await api('/quotes',{request_key:requestKey,...content});
quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart); quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart);
draftId=quote.id; localStorage.setItem('dtf-quote',draftId); draftId=quote.id; localStorage.setItem('dtf-quote',draftId);
await refresh(); await refresh(true);
status.scrollIntoView({behavior:'smooth',block:'nearest'});
} catch(error) { message(error.message); } } catch(error) { message(error.message); }
finally { busy=false; pintaEntrega(); } finally { busy=false; pintaEntrega(); }
}; };
async function refresh() { // `go`: the customer asked to pay, so the cart page moves to the payment page.
if (!draftId) return; async function refresh(go) {
if (!naPagina()) { if (!go || !draftId) return; }
else if (!draftId) { semPedido(); return; }
const version=++refreshVersion, shownId=draftId; const version=++refreshVersion, shownId=draftId;
try { try {
await ready; await ready;
const quote=await api('/quotes/'+shownId); const quote=await api('/quotes/'+shownId);
if(version!==refreshVersion || draftId!==shownId) return; if(version!==refreshVersion || draftId!==shownId) return;
if (!naPagina()) { message(''); vaiPara(PAGAMENTO); return; }
actions.replaceChildren(); actions.replaceChildren();
pintaResumo(quote);
if (quote.status!=='paid' && (!quotedCart || quotedCart!==cartSnapshot())) { if (quote.status!=='paid' && (!quotedCart || quotedCart!==cartSnapshot())) {
message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.'); message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.');
button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();}); button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();});
@@ -133,20 +167,26 @@
} else if (quote.status==='approved') { } else if (quote.status==='approved') {
message('Total validado no servidor: '+rs(quote.approved.total_cents/100)+' · inclui frete. Cotação válida por 24 horas.'); message('Total validado no servidor: '+rs(quote.approved.total_cents/100)+' · inclui frete. Cotação válida por 24 horas.');
if ((await ready).payment_provider === 'mercadopago') { if ((await ready).payment_provider === 'mercadopago') {
button('Pagar com PIX',async event=>{ const metodos=[];
const escolhe=async(el,show)=>{
if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; } if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; }
event.target.disabled=true; metodos.forEach(b=>b.setAttribute('aria-pressed',String(b===el)));
try { showPix(await api('/payments/intent',{quote_id:draftId,method:{type:'pix'}})); } el.disabled=true;
catch(error) { message(error.message); event.target.disabled=false; } try { await show(); }
}); catch(error) { message(error.message); }
finally { el.disabled=false; }
};
const pix=button('PIX',()=>escolhe(pix,async()=>
showPix(await api('/payments/intent',{quote_id:draftId,method:{type:'pix'}}))));
metodos.push(pix);
if ((await ready).payment_public_key) { if ((await ready).payment_public_key) {
button('Pagar com cartão',async event=>{ const card=button('Cartão de crédito',()=>escolhe(card,()=>showCard(quote.approved.total_cents)));
if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; } metodos.push(card);
event.target.disabled=true;
try { await showCard(quote.approved.total_cents); }
catch(error) { message(error.message); event.target.disabled=false; }
});
} }
metodos.forEach(b=>{b.className='metodo';b.setAttribute('aria-pressed','false');});
area=document.createElement('div'); area.className='area';
actions.append(area);
message('Total validado no servidor: '+rs(quote.approved.total_cents/100)+' · inclui frete. Escolha como pagar.');
return; return;
} }
if ((await ready).environment !== 'local') { if ((await ready).environment !== 'local') {
@@ -169,8 +209,8 @@
message('Pedido #'+quote.order.number+' · '+(STAGES[quote.order.state]||quote.order.state)+'.'); message('Pedido #'+quote.order.number+' · '+(STAGES[quote.order.state]||quote.order.state)+'.');
button('Novo pedido',()=>{clearDraft();location.assign('/');}); button('Novo pedido',()=>{clearDraft();location.assign('/');});
} else { } else {
message('Cotação expirada. Envie o carrinho para uma nova revisão.'); message('Cotação expirada. Envie o carrinho de novo para pagar.');
button('Nova cotação',clearDraft); button('Voltar ao carrinho',()=>{clearDraft();vaiPara(CARRINHO);});
} }
} catch(error) { } catch(error) {
if(version!==refreshVersion || draftId!==shownId) return; if(version!==refreshVersion || draftId!==shownId) return;
@@ -183,19 +223,19 @@
// by the provider's notification, not by this page, so the page only waits. // by the provider's notification, not by this page, so the page only waits.
let pixTimer=null; let pixTimer=null;
function showPix(intent) { function showPix(intent) {
actions.replaceChildren(); area.replaceChildren();
if (!intent.pix_qr_code) { message('Não foi possível gerar o PIX. Tente de novo em instantes.'); return; } if (!intent.pix_qr_code) { message('Não foi possível gerar o PIX. Tente de novo em instantes.'); return; }
message('Pague o PIX de '+rs(intent.total_cents/100)+'. O pedido entra na produção assim que o pagamento for confirmado.'); message('Pague o PIX de '+rs(intent.total_cents/100)+'. O pedido entra na produção assim que o pagamento for confirmado.');
if (intent.pix_qr_code_base64) { if (intent.pix_qr_code_base64) {
const img=document.createElement('img'); const img=document.createElement('img');
img.src='data:image/png;base64,'+intent.pix_qr_code_base64; img.src='data:image/png;base64,'+intent.pix_qr_code_base64;
img.alt='QR code do PIX';img.width=220;img.height=220;img.style.display='block'; img.alt='QR code do PIX';img.width=220;img.height=220;img.style.display='block';
actions.append(img); area.append(img);
} }
const code=document.createElement('input'); const code=document.createElement('input');
code.readOnly=true;code.value=intent.pix_qr_code;code.style.cssText='width:100%;margin-top:8px;padding:8px'; code.readOnly=true;code.value=intent.pix_qr_code;code.style.cssText='width:100%;margin-top:8px;padding:8px';
actions.append(code); area.append(code);
button('Copiar código PIX',async()=>{ try{ await navigator.clipboard.writeText(intent.pix_qr_code); message('Código copiado.'); }catch(_){ code.select(); } }); button('Copiar código PIX',async()=>{ try{ await navigator.clipboard.writeText(intent.pix_qr_code); message('Código copiado.'); }catch(_){ code.select(); } },area);
clearInterval(pixTimer); clearInterval(pixTimer);
pixTimer=setInterval(async()=>{ pixTimer=setInterval(async()=>{
try { try {
@@ -229,11 +269,11 @@
async function showCard(totalCents) { async function showCard(totalCents) {
await loadMercadoPago(); await loadMercadoPago();
const session=await ready; const session=await ready;
actions.replaceChildren(); area.replaceChildren();
const holder=document.createElement('div'); const holder=document.createElement('div');
holder.id='cardPaymentBrick'; holder.id='cardPaymentBrick';
holder.style.cssText='max-width:520px;margin-top:8px'; holder.style.cssText='max-width:520px;margin-top:8px';
actions.append(holder); area.append(holder);
message('Pagamento com cartão: '+rs(totalCents/100)+'.'); message('Pagamento com cartão: '+rs(totalCents/100)+'.');
if (cardBrick) { try { await cardBrick.unmount(); } catch(_) {} } if (cardBrick) { try { await cardBrick.unmount(); } catch(_) {} }
const mp=new window.MercadoPago(session.payment_public_key,{locale:'pt-BR'}); const mp=new window.MercadoPago(session.payment_public_key,{locale:'pt-BR'});
@@ -282,5 +322,10 @@
draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId); draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);
} }
window.addEventListener('dtf-cart-changed',()=>{if(draftId) refresh();}); window.addEventListener('dtf-cart-changed',()=>{if(draftId) refresh();});
// The cart page keeps only the sending progress and errors, never a payment.
window.addEventListener('dtf-page-changed',()=>{
if (naPagina()) refresh();
else if (!busy) { refreshVersion++; message(''); actions.replaceChildren(); pintaResumo(null); }
});
refresh(); refresh();
})(); })();

View File

@@ -869,7 +869,7 @@ footer a:hover{color:var(--laranja2)}
<i aria-hidden="true"></i> <i aria-hidden="true"></i>
<a href="/carrinho" data-passo="2"><span>2</span>Dados e entrega</a> <a href="/carrinho" data-passo="2"><span>2</span>Dados e entrega</a>
<i aria-hidden="true"></i> <i aria-hidden="true"></i>
<a href="/carrinho#checkoutStatus" data-passo="3"><span>3</span>Pagamento</a> <a href="/pagamento" data-passo="3"><span>3</span>Pagamento</a>
</nav> </nav>
<div class="w"> <div class="w">
@@ -1209,6 +1209,11 @@ footer a:hover{color:var(--laranja2)}
<!-- Fora de #carr de propósito: o pedido pago esvazia o carrinho, e a <!-- Fora de #carr de propósito: o pedido pago esvazia o carrinho, e a
confirmação não pode sumir junto com o painel. --> confirmação não pode sumir junto com o painel. -->
<div class="checkout"> <div class="checkout">
<div class="pagCab">
<h3>Pagamento</h3>
<a href="/carrinho" class="pagVolta">Voltar ao carrinho</a>
</div>
<div class="pagResumo" id="pagResumo" hidden></div>
<p id="checkoutStatus" class="avisoE" role="status" aria-live="polite"></p> <p id="checkoutStatus" class="avisoE" role="status" aria-live="polite"></p>
<div id="checkoutActions"></div> <div id="checkoutActions"></div>
</div> </div>

View File

@@ -18,7 +18,7 @@ async function load(){
for(const quote of data.quotes){ for(const quote of data.quotes){
const card=node('article');card.className='card';card.append(node('h3','Cotação '+quote.id.slice(0,8))); const card=node('article');card.className='card';card.append(node('h3','Cotação '+quote.id.slice(0,8)));
card.append(node('p',quote.approved?'Total aprovado: '+money(quote.approved.total_cents):'Aguardando conferência de metragem e nota.')); card.append(node('p',quote.approved?'Total aprovado: '+money(quote.approved.total_cents):'Aguardando conferência de metragem e nota.'));
const link=node('a','Abrir cotação no Site');link.href='/carrinho?quote='+quote.id;card.append(link);$('quotes').append(card); const link=node('a','Abrir cotação no Site');link.href='/pagamento?quote='+quote.id;card.append(link);$('quotes').append(card);
} }
$('orders').replaceChildren(); $('orders').replaceChildren();
if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.')); if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.'));

View File

@@ -1,4 +1,5 @@
/* Site DTF — the Site's pages: the home, one page per product and the cart. /* Site DTF — the Site's pages: the home, one page per product, the cart and
the payment page.
They share this document, so artworks already loaded in the browser survive They share this document, so artworks already loaded in the browser survive
moving between them; each page has its own address, so Back, Forward, reload moving between them; each page has its own address, so Back, Forward, reload
and shared links behave as in any store. nginx serves index.html for these and shared links behave as in any store. nginx serves index.html for these
@@ -6,14 +7,15 @@
site-steps.js does the navigation. */ site-steps.js does the navigation. */
const PAGINAS={file:'/arquivo-por-metro', avulsa:'/artes-avulsas', const PAGINAS={file:'/arquivo-por-metro', avulsa:'/artes-avulsas',
uvfile:'/uv-arquivo-por-metro', uv:'/uv-artes-avulsas'}; uvfile:'/uv-arquivo-por-metro', uv:'/uv-artes-avulsas'};
const CARRINHO='/carrinho'; const CARRINHO='/carrinho', PAGAMENTO='/pagamento';
function paginaDe(caminho){ function paginaDe(caminho){
caminho=caminho.replace(/\/+$/,'')||'/'; caminho=caminho.replace(/\/+$/,'')||'/';
if(caminho===CARRINHO) return {pagina:'carrinho'}; if(caminho===CARRINHO) return {pagina:'carrinho'};
if(caminho===PAGAMENTO) return {pagina:'pagamento'};
const m=Object.keys(PAGINAS).find(k=>PAGINAS[k]===caminho); const m=Object.keys(PAGINAS).find(k=>PAGINAS[k]===caminho);
return m ? {pagina:'produto',modo:m} : {pagina:'inicio'}; return m ? {pagina:'produto',modo:m} : {pagina:'inicio'};
} }
// Older portal links open a quote on the home address; quotes live in the cart. // Portal links, old and new, open a quote on the payment page.
if(location.pathname==='/' && new URLSearchParams(location.search).has('quote')) if(['/',CARRINHO].includes(location.pathname.replace(/\/+$/,'')||'/') && new URLSearchParams(location.search).has('quote'))
history.replaceState(null,'',CARRINHO+location.search); history.replaceState(null,'',PAGAMENTO+location.search);
document.documentElement.dataset.rota=paginaDe(location.pathname).pagina; document.documentElement.dataset.rota=paginaDe(location.pathname).pagina;

View File

@@ -11,6 +11,7 @@ const TITULO_INICIO=document.title;
function tituloPagina(){ function tituloPagina(){
const p=paginaDe(location.pathname); const p=paginaDe(location.pathname);
if(p.pagina==='carrinho') return 'Carrinho · DTF Dropstar'; if(p.pagina==='carrinho') return 'Carrinho · DTF Dropstar';
if(p.pagina==='pagamento') return 'Pagamento · DTF Dropstar';
if(p.pagina==='produto') return MODOS[p.modo].tit+' · DTF Dropstar'; if(p.pagina==='produto') return MODOS[p.modo].tit+' · DTF Dropstar';
return TITULO_INICIO; return TITULO_INICIO;
} }
@@ -91,7 +92,7 @@ function vaiPara(url){
passos.hidden = r==='inicio' || carrinhoVazio; passos.hidden = r==='inicio' || carrinhoVazio;
alturas(); alturas();
if(!passos.hidden){ if(!passos.hidden){
const atual = r==='produto' ? 1 : temCheckout() ? 3 : 2; const atual = r==='produto' ? 1 : r==='pagamento' ? 3 : 2;
passos.querySelectorAll('[data-passo]').forEach(a=>{ passos.querySelectorAll('[data-passo]').forEach(a=>{
const n=Number(a.dataset.passo); const n=Number(a.dataset.passo);
a.classList.toggle('atual',n===atual); a.classList.toggle('feito',n<atual); a.classList.toggle('atual',n===atual); a.classList.toggle('feito',n<atual);
@@ -124,8 +125,10 @@ function vaiPara(url){
// Payment is a step only once the order has been sent. // Payment is a step only once the order has been sent.
passos.querySelector('[data-passo="3"]').addEventListener('click',e=>{ passos.querySelector('[data-passo="3"]').addEventListener('click',e=>{
if(!temCheckout()) e.preventDefault(); let enviado=false;
}); try{ enviado=!!localStorage.getItem('dtf-quote'); }catch(_){}
if(!enviado){ e.preventDefault(); e.stopPropagation(); }
},true);
$('barraMBtn').addEventListener('click',e=>{ $('barraMBtn').addEventListener('click',e=>{
e.preventDefault(); e.preventDefault();
alvoBarra()?.scrollIntoView({behavior:'smooth',block:'center'}); alvoBarra()?.scrollIntoView({behavior:'smooth',block:'center'});

View File

@@ -15,7 +15,8 @@ h1,h2,h3,h4{font-family:"Inter",sans-serif}
/* Pages (site-pages.js): the home, a product page and the cart show only their own parts. */ /* Pages (site-pages.js): the home, a product page and the cart show only their own parts. */
html[data-rota="inicio"] :is(#passos,#foco,#qual,#prev,#carr,.checkout,#carrVazio), html[data-rota="inicio"] :is(#passos,#foco,#qual,#prev,#carr,.checkout,#carrVazio),
html[data-rota="produto"] :is(.hero2,.etapas2,#cards,#carr,.checkout,#carrVazio,#info), html[data-rota="produto"] :is(.hero2,.etapas2,#cards,#carr,.checkout,#carrVazio,#info),
html[data-rota="carrinho"] :is(.hero2,.etapas2,#cards,#foco,#qual,#prev,#info){display:none!important} html[data-rota="carrinho"] :is(.hero2,.etapas2,#cards,#foco,#qual,#prev,#info,.pagCab,.pagResumo){display:none!important}
html[data-rota="pagamento"] :is(.hero2,.etapas2,#cards,#foco,#qual,#prev,#info,#carr,#carrVazio,#desfazer){display:none!important}
html[data-sem-itens] #carr{display:none!important} html[data-sem-itens] #carr{display:none!important}
html:not([data-rota="inicio"]) #envio{padding-top:0;padding-bottom:64px;min-height:70vh} html:not([data-rota="inicio"]) #envio{padding-top:0;padding-bottom:64px;min-height:70vh}
#envio,#foco,#qual,#prev,#checkoutStatus{scroll-margin-top:var(--topo-passos,128px)} #envio,#foco,#qual,#prev,#checkoutStatus{scroll-margin-top:var(--topo-passos,128px)}
@@ -225,6 +226,19 @@ section+section{border-top:0}
#checkoutActions{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px} #checkoutActions{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px}
#checkoutActions button{height:48px!important;padding:0 20px!important;margin:0!important;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:15px} #checkoutActions button{height:48px!important;padding:0 20px!important;margin:0!important;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:15px}
#checkoutActions input{height:44px;border-radius:var(--raio2);border:1px solid #D6D6D6;padding:0 12px} #checkoutActions input{height:44px;border-radius:var(--raio2);border:1px solid #D6D6D6;padding:0 12px}
/* The payment page */
html[data-rota="pagamento"] .checkout{margin-top:8px}
.pagCab{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:16px}
.pagCab h3{font-size:26px;font-weight:800;letter-spacing:-.02em}
.pagVolta{font-size:14px;color:var(--texto2)}
.pagResumo{border:1px solid var(--linha);border-radius:16px;background:#fff;padding:18px 20px;margin-bottom:16px}
.pagResumo .l{display:flex;justify-content:space-between;gap:12px;font-size:14px;padding:5px 0}
.pagResumo .l span{color:var(--texto2)}
.pagResumo .tot{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid #EFEFEF;margin-top:10px;padding-top:12px}
.pagResumo .tot b{font-size:26px}
#checkoutActions .metodo{background:#fff;border:2px solid var(--linha);color:var(--preto);min-width:180px}
#checkoutActions .metodo[aria-pressed="true"]{border-color:var(--laranja);background:var(--aviso-fundo)}
#checkoutActions .area{flex-basis:100%}
/* Price and benefits */ /* Price and benefits */
.info2{background:var(--cinza);padding:64px 0} .info2{background:var(--cinza);padding:64px 0}