Compare commits
20 Commits
20403c5132
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef95dcc327 | ||
|
|
0b86fe79fc | ||
|
|
24ee4e9eab | ||
|
|
223854e392 | ||
|
|
0b55ebce7f | ||
|
|
bee7f8f245 | ||
|
|
c5db1375de | ||
|
|
3d8bca3af7 | ||
|
|
f3fc733352 | ||
|
|
4ce44feb70 | ||
|
|
01b94da36d | ||
|
|
25fa890e92 | ||
|
|
4386fc0737 | ||
|
|
70a76feb23 | ||
|
|
20982a945f | ||
|
|
37715ef223 | ||
|
|
b7e2ea12d0 | ||
|
|
933bd30cbd | ||
|
|
eb254da501 | ||
|
|
8786a33c8d |
@@ -86,7 +86,7 @@ jobs:
|
||||
# the generator's geometry. The provider suites use a fake transport: they
|
||||
# prove the documented contract, not the integration.
|
||||
- name: Print-file geometry and provider adapters
|
||||
run: $COMPOSE exec -T api python -m unittest tests.test_printfile tests.test_mercadopago tests.test_tiny tests.test_jadlog tests.test_quote_review tests.test_large_files -v
|
||||
run: $COMPOSE exec -T api python -m unittest tests.test_printfile tests.test_mercadopago tests.test_tiny tests.test_jadlog tests.test_quote_review tests.test_large_files tests.test_grade_check -v
|
||||
|
||||
- name: Runtime and retention regressions
|
||||
run: |
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
|
||||
from psycopg.errors import UniqueViolation
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
@@ -10,7 +12,7 @@ from ..core import db
|
||||
from ..artwork import submit_files
|
||||
from ..core.auth import (DUMMY_PASSWORD_HASH, audit, client_ip, new_session, owner,
|
||||
login_failed, password_hash, password_matches, session_row, throttle, transfer_guest)
|
||||
from ..core.models import ArtworkSubmission, Login, Register
|
||||
from ..core.models import ArtworkSubmission, EmailChange, Login, PasswordChange, ProfileUpdate, Register
|
||||
from ..runtime import STATES, file_rows, owned_order, storage
|
||||
from ..scanning import require_clean
|
||||
|
||||
@@ -86,13 +88,112 @@ def me(identity=Depends(owner)):
|
||||
row = c.execute('SELECT profile FROM dtf_local.accounts WHERE id=%s', (identity,)).fetchone()
|
||||
return {'customer': row['profile'] if row else None}
|
||||
|
||||
@router.get('/api/customer/orders')
|
||||
def orders(identity=Depends(owner)):
|
||||
def account_row(c, identity, lock=False):
|
||||
row = c.execute('SELECT * FROM dtf_local.accounts WHERE id=%s' + (' FOR UPDATE' if lock else ''), (identity,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(401, 'Entre na sua conta.')
|
||||
return row
|
||||
|
||||
@router.post('/api/account/profile')
|
||||
def update_profile(body: ProfileUpdate, identity=Depends(owner)):
|
||||
"""WhatsApp and the saved delivery address. The CNPJ is the account's and
|
||||
does not change; the e-mail is the login and changes on its own route."""
|
||||
with db.connect() as c:
|
||||
rows = c.execute('SELECT id,number,state,version,snapshot,created_at,updated_at FROM dtf_local.orders WHERE owner=%s ORDER BY created_at DESC', (identity,)).fetchall()
|
||||
quotes = c.execute('''SELECT q.id,q.approved,q.approved_at,q.created_at FROM dtf_local.quotes q
|
||||
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id WHERE q.owner=%s AND o.id IS NULL ORDER BY q.created_at DESC''', (identity,)).fetchall()
|
||||
return {'orders': rows, 'quotes': quotes, 'states': STATES}
|
||||
row = account_row(c, identity, lock=True)
|
||||
profile = {**row['profile'], 'zap': body.zap,
|
||||
'address': body.address.model_dump() if body.address else None}
|
||||
c.execute('UPDATE dtf_local.accounts SET profile=%s WHERE id=%s', (Jsonb(profile), identity))
|
||||
audit('account_profile_updated', account=str(identity))
|
||||
return {'customer': profile}
|
||||
|
||||
@router.post('/api/account/email')
|
||||
def change_email(body: EmailChange, request: Request, identity=Depends(owner)):
|
||||
with db.connect() as c:
|
||||
row = account_row(c, identity)
|
||||
throttle(row['email'], request)
|
||||
if not password_matches(body.password, row['password_hash']):
|
||||
login_failed(row['email'])
|
||||
raise HTTPException(401, 'Senha incorreta.')
|
||||
profile = {**row['profile'], 'mail': body.email}
|
||||
try:
|
||||
with db.connect() as c:
|
||||
c.execute('UPDATE dtf_local.accounts SET email=%s, profile=%s WHERE id=%s', (body.email, Jsonb(profile), identity))
|
||||
except UniqueViolation:
|
||||
raise HTTPException(409, 'Este e-mail já é de outra conta.')
|
||||
audit('account_email_changed', account=str(identity))
|
||||
return {'customer': profile}
|
||||
|
||||
@router.post('/api/account/password')
|
||||
def change_password(body: PasswordChange, request: Request, identity=Depends(owner)):
|
||||
with db.connect() as c:
|
||||
row = account_row(c, identity)
|
||||
throttle(row['email'], request)
|
||||
if not password_matches(body.current, row['password_hash']):
|
||||
login_failed(row['email'])
|
||||
raise HTTPException(401, 'Senha atual incorreta.')
|
||||
session = current(request)
|
||||
with db.connect() as c:
|
||||
c.execute('UPDATE dtf_local.accounts SET password_hash=%s WHERE id=%s', (password_hash(body.new), identity))
|
||||
# Other devices signed in with the old password are signed out.
|
||||
c.execute('DELETE FROM dtf_local.sessions WHERE owner=%s AND id<>%s', (identity, session['id']))
|
||||
audit('account_password_changed', account=str(identity))
|
||||
return {'ok': True}
|
||||
|
||||
# The order list's groups, as the customer filters them.
|
||||
GROUPS = {'pay': 'Aguardando pagamento', 'prod': 'Em produção', 'cor': 'Correção', 'fin': 'Finalizados'}
|
||||
ENTRIES = '''WITH live_quote AS (
|
||||
-- The cart waiting for payment: the newest unpaid quote whose files still
|
||||
-- exist. An older one was replaced when the cart changed, and one whose
|
||||
-- files are gone can no longer be paid.
|
||||
SELECT q.id, q.created_at, q.approved FROM dtf_local.quotes q
|
||||
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
|
||||
WHERE q.owner=%(me)s AND o.id IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(q.draft->'items') i
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(i->'uploads') u(uid)
|
||||
JOIN dtf_local.uploads up ON up.id=u.uid::uuid
|
||||
WHERE up.purged_at IS NOT NULL OR up.expires_at<=now())
|
||||
ORDER BY q.created_at DESC LIMIT 1
|
||||
), entries AS (
|
||||
SELECT 'order' AS kind, o.id, o.number, o.state,
|
||||
CASE WHEN o.state='fin' THEN 'fin' WHEN o.state='cor' THEN 'cor' ELSE 'prod' END AS grp,
|
||||
(o.snapshot->>'total_cents')::bigint AS total_cents, o.created_at, o.snapshot->'items' AS items
|
||||
FROM dtf_local.orders o WHERE o.owner=%(me)s
|
||||
UNION ALL
|
||||
SELECT 'quote', q.id, NULL, CASE WHEN q.approved IS NULL THEN 'review' ELSE 'pay' END, 'pay',
|
||||
(q.approved->>'total_cents')::bigint, q.created_at, COALESCE(q.approved->'items','[]'::jsonb)
|
||||
FROM live_quote q
|
||||
)'''
|
||||
|
||||
@router.get('/api/customer/orders')
|
||||
def orders(identity=Depends(owner), status: Literal['all','pay','prod','cor','fin'] = 'all',
|
||||
number: int | None = Query(None, ge=1), days: int = Query(0, ge=0, le=3650),
|
||||
page: int = Query(1, ge=1, le=10000), size: int = Query(10, ge=1, le=50)):
|
||||
"""The customer's orders and the cart waiting for payment, newest first,
|
||||
filtered and a page at a time, with how many there are in each group."""
|
||||
params = {'me': identity, 'grp': None if status == 'all' else status, 'number': number,
|
||||
'since': datetime.now(timezone.utc) - timedelta(days=days) if days else None,
|
||||
'size': size, 'offset': (page - 1) * size}
|
||||
where = '''WHERE (%(grp)s::text IS NULL OR grp=%(grp)s) AND (%(number)s::int IS NULL OR number=%(number)s)
|
||||
AND (%(since)s::timestamptz IS NULL OR created_at>=%(since)s)'''
|
||||
with db.connect() as c:
|
||||
counts = {r['grp']: r['n'] for r in c.execute(ENTRIES + ' SELECT grp, count(*) AS n FROM entries GROUP BY grp', params).fetchall()}
|
||||
total = c.execute(ENTRIES + ' SELECT count(*) AS n FROM entries ' + where, params).fetchone()['n']
|
||||
rows = c.execute(ENTRIES + ' SELECT * FROM entries ' + where +
|
||||
' ORDER BY created_at DESC LIMIT %(size)s OFFSET %(offset)s', params).fetchall()
|
||||
items = []
|
||||
for r in rows:
|
||||
entry = {'kind': r['kind'], 'id': r['id'], 'number': r['number'], 'state': r['state'],
|
||||
'group': r['grp'], 'total_cents': r['total_cents'], 'created_at': r['created_at'],
|
||||
'items': [{'mode': i['mode'], 'billed_metres': i.get('billed_metres')} for i in r['items']]}
|
||||
if r['kind'] == 'quote':
|
||||
# A PIX code still open is paid on its own page.
|
||||
pix = c.execute('''SELECT 1 FROM dtf_local.payment_intents WHERE quote_id=%s AND method='pix'
|
||||
AND status='pending' AND COALESCE((response->>'expires_at')::timestamptz > now(), false)''',
|
||||
(r['id'],)).fetchone()
|
||||
entry['pay_with'] = 'pix' if pix else 'page'
|
||||
items.append(entry)
|
||||
return {'items': items, 'total': total, 'page': page, 'size': size,
|
||||
'counts': {g: counts.get(g, 0) for g in GROUPS}, 'groups': GROUPS, 'states': STATES}
|
||||
|
||||
@router.get('/api/customer/orders/{oid}')
|
||||
def detail(oid: UUID, identity=Depends(owner)):
|
||||
@@ -103,7 +204,8 @@ def detail(oid: UUID, identity=Depends(owner)):
|
||||
history = c.execute('''SELECT from_state,to_state,CASE WHEN to_state='cor' THEN reason ELSE '' END AS reason,
|
||||
created_at FROM dtf_local.movements WHERE order_id=%s AND NOT back ORDER BY id''', (oid,)).fetchall()
|
||||
return {'id': row['id'], 'number': row['number'], 'state': row['state'], 'version': row['version'],
|
||||
'snapshot': row['snapshot'], 'history': history, 'files': file_rows(c,oid)}
|
||||
'created_at': row['created_at'], 'snapshot': row['snapshot'], 'history': history,
|
||||
'files': file_rows(c,oid)}
|
||||
|
||||
@router.post('/api/customer/orders/{oid}/corrections')
|
||||
def correction(oid: UUID, body: ArtworkSubmission, identity=Depends(owner)):
|
||||
|
||||
@@ -85,6 +85,16 @@ def intent(body: PaymentIntent, session_id=Depends(owner)):
|
||||
raise HTTPException(404 if 'not found' in str(refusal) else 409, str(refusal))
|
||||
if c.execute('SELECT 1 FROM dtf_local.orders WHERE quote_id=%s', (body.quote_id,)).fetchone():
|
||||
raise HTTPException(409, 'Quote is already paid')
|
||||
# Never charge for files that are gone: an unpaid cart's files are
|
||||
# removed after a while. A payment under way keeps them a day longer,
|
||||
# time enough for the provider's notice to become the order.
|
||||
uploads = payments.quote_uploads(quote['approved'])
|
||||
live = c.execute('''SELECT count(*) AS n FROM dtf_local.uploads WHERE id=ANY(%s)
|
||||
AND purged_at IS NULL AND expires_at>now()''', (uploads,)).fetchone()['n']
|
||||
if live != len(set(uploads)):
|
||||
raise HTTPException(410, 'Os arquivos deste pedido expiraram porque ele não foi pago a tempo. '
|
||||
'Monte o pedido de novo para pagar.')
|
||||
payments.hold_uploads(c, uploads, '1 day')
|
||||
# Never a second charge: an approved payment is waiting for its
|
||||
# notification to become the order, and a card in review may still be.
|
||||
# A card waiting for the bank's confirmation (3-D Secure) blocks only
|
||||
|
||||
@@ -7,12 +7,14 @@ from uuid import UUID, uuid4
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from .. import payments
|
||||
from ..core import db
|
||||
from ..core.auth import owner
|
||||
from ..core.models import QuoteRequest
|
||||
from ..core.pricing import price
|
||||
from .. import quote_review
|
||||
from ..runtime import freight, quote_view, require_delivery_available, upload_row
|
||||
from ..grade_check import Files, mismatch
|
||||
from ..runtime import freight, quote_view, require_delivery_available, storage, upload_row
|
||||
from ..scanning import require_clean
|
||||
|
||||
router = APIRouter()
|
||||
@@ -35,21 +37,30 @@ def create_quote(body: QuoteRequest, session_id=Depends(owner)):
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc))
|
||||
with db.connect() as c:
|
||||
rows = {}
|
||||
for item in body.items:
|
||||
for uid in item.uploads:
|
||||
row = upload_row(c, uid, session_id)
|
||||
if not row['complete']:
|
||||
raise HTTPException(409, 'Complete every upload before requesting a quote')
|
||||
require_clean(row)
|
||||
rows[str(uid)] = row
|
||||
# The grade sets the price and came from the browser: before a cart is
|
||||
# approved at checkout, the server works it out from the files.
|
||||
note = mismatch(Files(storage), draft['items'], rows) if reason is None else None
|
||||
reason = reason or note
|
||||
uid = uuid4()
|
||||
c.execute('INSERT INTO dtf_local.quotes(id,owner,request_key,request_hash,draft) VALUES(%s,%s,%s,%s,%s) ON CONFLICT(owner,request_key) DO NOTHING',
|
||||
(uid, session_id, body.request_key, digest, Jsonb(draft)))
|
||||
c.execute('INSERT INTO dtf_local.quotes(id,owner,request_key,request_hash,draft,review_note) VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT(owner,request_key) DO NOTHING',
|
||||
(uid, session_id, body.request_key, digest, Jsonb(draft), note))
|
||||
row = c.execute('SELECT * FROM dtf_local.quotes WHERE owner=%s AND request_key=%s FOR UPDATE', (session_id, body.request_key)).fetchone()
|
||||
if row['request_hash'] != digest:
|
||||
raise HTTPException(409, 'Request key already used for a different cart')
|
||||
if row['id'] == uid and reason is None:
|
||||
quote_review.approve(c, row, body.items, quote_review.AUTO)
|
||||
return {'id': row['id'], 'status': 'approved'}
|
||||
if row['id'] == uid:
|
||||
# An operator reviews it first; the files wait for that review.
|
||||
payments.hold_uploads(c, payments.quote_uploads(draft), payments.REVIEW_HOLD)
|
||||
return {'id': row['id'], 'status': 'approved' if row['approved'] else 'pending_review'}
|
||||
|
||||
@router.get('/api/quotes/{uid}')
|
||||
|
||||
@@ -14,6 +14,7 @@ from ..core import db
|
||||
from ..core.auth import audit, owner, rate_limit
|
||||
from ..core.limits import upload_limit_bytes
|
||||
from ..core.models import UploadStart
|
||||
from ..payments import UNPAID_HOLD
|
||||
from ..runtime import PART_BYTES, storage, upload_row
|
||||
|
||||
router = APIRouter()
|
||||
@@ -87,7 +88,10 @@ def complete_upload(uid: UUID, session_id=Depends(owner)):
|
||||
existing_size = storage.size(row['object_key'])
|
||||
if existing_size != row['size']:
|
||||
raise HTTPException(409, 'Stored size differs from declared size')
|
||||
c.execute("UPDATE dtf_local.uploads SET complete=true,expires_at=now()+interval '30 days' WHERE id=%s", (uid,))
|
||||
# Held while the cart is unpaid: an abandoned cart's files go after
|
||||
# UNPAID_HOLD; a paid order keeps them for its 30 days (app/payments.py).
|
||||
c.execute('UPDATE dtf_local.uploads SET complete=true,expires_at=now()+%s::interval WHERE id=%s',
|
||||
(UNPAID_HOLD, uid))
|
||||
return {'id': uid, 'complete': True}
|
||||
|
||||
@router.delete('/api/uploads/{uid}')
|
||||
|
||||
@@ -67,7 +67,8 @@ def submit_files(c, order, body, identity, kind, actor):
|
||||
for ref in body.files:
|
||||
c.execute('INSERT INTO dtf_local.order_files(id,order_id,upload_id,item_index,kind,note,created_by) VALUES(%s,%s,%s,%s,%s,%s,%s)',
|
||||
(uuid4(),order['id'],ref.upload_id,ref.item_index,kind,body.note,actor))
|
||||
c.execute('UPDATE dtf_local.uploads SET expires_at=LEAST(expires_at,%s) WHERE id=%s', (expiry,ref.upload_id))
|
||||
# The order's window, whatever the upload's own hold was.
|
||||
c.execute('UPDATE dtf_local.uploads SET expires_at=%s WHERE id=%s', (expiry,ref.upload_id))
|
||||
c.execute('UPDATE dtf_local.orders SET version=version+1,updated_at=now() WHERE id=%s', (order['id'],))
|
||||
if kind == 'final':
|
||||
# Artwork approval, not commercial quote approval, starts original cleanup.
|
||||
|
||||
@@ -219,6 +219,35 @@ class Register(StrictModel):
|
||||
customer: Customer
|
||||
password: str = Field(min_length=12, max_length=128)
|
||||
|
||||
class ProfileUpdate(StrictModel):
|
||||
"""What a customer may change about their account; the CNPJ is not among it."""
|
||||
zap: str
|
||||
address: Destination | None = None
|
||||
|
||||
@field_validator('zap')
|
||||
@classmethod
|
||||
def phone_valid(cls, value):
|
||||
digits = re.sub(r'\D', '', value)
|
||||
if len(digits) not in (10,11) or not digits.isascii():
|
||||
raise ValueError('Invalid phone')
|
||||
return digits
|
||||
|
||||
class EmailChange(StrictModel):
|
||||
email: str = Field(max_length=254)
|
||||
password: str = Field(min_length=1, max_length=128)
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def email_valid(cls, value):
|
||||
value = value.strip().lower()
|
||||
if not re.fullmatch(r'[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}', value):
|
||||
raise ValueError('Invalid email')
|
||||
return value
|
||||
|
||||
class PasswordChange(StrictModel):
|
||||
current: str = Field(min_length=1, max_length=128)
|
||||
new: str = Field(min_length=12, max_length=128)
|
||||
|
||||
class Login(StrictModel):
|
||||
email: str = Field(min_length=3, max_length=254)
|
||||
password: str = Field(min_length=1, max_length=128)
|
||||
|
||||
189
app/grade_check.py
Normal file
189
app/grade_check.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""The grade (the resolution discount) recomputed from the uploaded files.
|
||||
|
||||
The Site grades the art in the customer's browser and sends the grade with the
|
||||
cart, and the price depends on it. A customer who edits the page could claim a
|
||||
better grade, so before a cart is approved at checkout the server reads the
|
||||
files it already has and works the grade out again, by the Site's own rules:
|
||||
|
||||
- a finished sheet: its pixel width over the sheet width is its DPI, and the
|
||||
item's grade comes from the worst sheet (web/site-quality.js);
|
||||
- artworks: each one's DPI along the width it is printed at, rotation
|
||||
included; the item's grade is the average of the artworks' grades;
|
||||
- a PDF: the area-weighted DPI of the images drawn on its page, 300 when it
|
||||
is only vectors (web/site-pdf.js, dpiDasImagens and resumoDpi).
|
||||
|
||||
Images are measured from their first bytes, never decoded. A PDF is opened
|
||||
only up to PDF_MAX_BYTES, the size the Site itself analyses. A grade that
|
||||
cannot be recomputed is None; the caller decides what that means.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import tempfile
|
||||
|
||||
DPI_IDEAL = 300
|
||||
# The Site analyses PDFs up to 150 MB (GRANDE_BYTES); above that it does not
|
||||
# grade them, so neither does this.
|
||||
PDF_MAX_BYTES = 150 * 1048576
|
||||
HEAD_BYTES = 1048576
|
||||
# Rounding in the browser and here can differ by a point on the same file.
|
||||
TOLERANCE = 2
|
||||
|
||||
|
||||
def js_round(value):
|
||||
"""Math.round, which rounds halves up; Python's round() does not."""
|
||||
return math.floor(value + 0.5)
|
||||
|
||||
|
||||
def grade_of(dpi):
|
||||
return max(6, min(100, js_round(dpi / DPI_IDEAL * 100)))
|
||||
|
||||
|
||||
def image_size(head):
|
||||
"""(width, height) in pixels from a PNG, JPEG or WebP's first bytes."""
|
||||
if head[:8] == b'\x89PNG\r\n\x1a\n' and head[12:16] == b'IHDR':
|
||||
return struct.unpack('>II', head[16:24])
|
||||
if head[:2] == b'\xff\xd8':
|
||||
i = 2
|
||||
while i + 9 < len(head):
|
||||
if head[i] != 0xFF:
|
||||
i += 1
|
||||
continue
|
||||
marker = head[i + 1]
|
||||
if marker in (0xD8, 0x01) or 0xD0 <= marker <= 0xD7 or marker == 0xFF:
|
||||
i += 1 if marker == 0xFF else 2
|
||||
continue
|
||||
length = struct.unpack('>H', head[i + 2:i + 4])[0]
|
||||
if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC):
|
||||
h, w = struct.unpack('>HH', head[i + 5:i + 9])
|
||||
return w, h
|
||||
i += 2 + length
|
||||
return None
|
||||
if head[:4] == b'RIFF' and head[8:12] == b'WEBP':
|
||||
chunk = head[12:16]
|
||||
if chunk == b'VP8X':
|
||||
return (int.from_bytes(head[24:27], 'little') + 1, int.from_bytes(head[27:30], 'little') + 1)
|
||||
if chunk == b'VP8L' and head[20] == 0x2F:
|
||||
bits = int.from_bytes(head[21:25], 'little')
|
||||
return (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1
|
||||
if chunk == b'VP8 ' and head[23:26] == b'\x9d\x01\x2a':
|
||||
w, h = struct.unpack('<HH', head[26:30])
|
||||
return w & 0x3FFF, h & 0x3FFF
|
||||
return None
|
||||
|
||||
|
||||
def _multiply(a, b):
|
||||
return [a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1],
|
||||
a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3],
|
||||
a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]]
|
||||
|
||||
|
||||
def pdf_dpi(path):
|
||||
"""The page's area-weighted image DPI, 300 for vectors only, None if the
|
||||
file is not a one-page PDF this can read."""
|
||||
import pikepdf
|
||||
try:
|
||||
with pikepdf.open(path) as pdf:
|
||||
if len(pdf.pages) != 1:
|
||||
return None
|
||||
page = pdf.pages[0]
|
||||
unit = float(page.obj.get('/UserUnit', 1))
|
||||
found = []
|
||||
|
||||
def walk(resources, instructions, ctm, depth):
|
||||
stack = []
|
||||
xobjects = resources.get('/XObject', {}) if resources is not None else {}
|
||||
for operands, operator in instructions:
|
||||
op = str(operator)
|
||||
if op == 'q':
|
||||
stack.append(ctm)
|
||||
elif op == 'Q':
|
||||
ctm = stack.pop() if stack else [1, 0, 0, 1, 0, 0]
|
||||
elif op == 'cm':
|
||||
ctm = _multiply(ctm, [float(v) for v in operands])
|
||||
elif op == 'Do' and operands:
|
||||
xobject = xobjects.get(str(operands[0]))
|
||||
if xobject is None:
|
||||
continue
|
||||
subtype = xobject.get('/Subtype')
|
||||
if subtype == '/Image':
|
||||
width_pt = math.hypot(ctm[0], ctm[1])
|
||||
if width_pt >= 1:
|
||||
found.append((int(xobject.get('/Width', 0)) / (width_pt * unit / 72),
|
||||
abs(ctm[0] * ctm[3] - ctm[1] * ctm[2])))
|
||||
elif subtype == '/Form' and depth < 8:
|
||||
matrix = [float(v) for v in xobject.get('/Matrix', [1, 0, 0, 1, 0, 0])]
|
||||
walk(xobject.get('/Resources', resources),
|
||||
pikepdf.parse_content_stream(xobject), _multiply(ctm, matrix), depth + 1)
|
||||
|
||||
walk(page.obj.get('/Resources'), pikepdf.parse_content_stream(page), [1, 0, 0, 1, 0, 0], 0)
|
||||
except Exception:
|
||||
return None
|
||||
found = [(dpi, area) for dpi, area in found if dpi > 0]
|
||||
if not found:
|
||||
return DPI_IDEAL
|
||||
total = sum(area for _, area in found) or 1
|
||||
return js_round(sum(dpi * area for dpi, area in found) / total)
|
||||
|
||||
|
||||
class Files:
|
||||
"""The uploaded files, read from storage as little as needed."""
|
||||
|
||||
def __init__(self, storage):
|
||||
self.storage = storage
|
||||
|
||||
def head(self, row):
|
||||
return self.storage.client.get_object(Bucket=self.storage.bucket, Key=row['object_key'],
|
||||
Range=f'bytes=0-{HEAD_BYTES - 1}')['Body'].read()
|
||||
|
||||
def pdf_dpi(self, row):
|
||||
if row['size'] > PDF_MAX_BYTES:
|
||||
return None
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
self.storage.client.download_fileobj(self.storage.bucket, row['object_key'], handle)
|
||||
handle.flush()
|
||||
return pdf_dpi(handle.name)
|
||||
|
||||
|
||||
def source_dpi(files, row, source):
|
||||
"""One source's DPI across the width it is printed at, or None."""
|
||||
name = row['name'].lower()
|
||||
width_in = float(source['width_cm']) / 2.54
|
||||
if name.endswith('.pdf'):
|
||||
return files.pdf_dpi(row) if source['kind'] == 'sheet' else None
|
||||
if not name.endswith(('.png', '.jpg', '.jpeg', '.webp')):
|
||||
return None
|
||||
size = image_size(files.head(row))
|
||||
if not size or not all(size):
|
||||
return None
|
||||
across = size[1] if source['rotation_degrees'] in (90, 270) else size[0]
|
||||
return across / width_in
|
||||
|
||||
|
||||
def item_grade(files, item, rows):
|
||||
"""The grade the Site gives this item, recomputed; None if it cannot be."""
|
||||
sources = item['production']['sources']
|
||||
dpis = []
|
||||
for source in sources:
|
||||
row = rows.get(str(source['upload_id']))
|
||||
dpi = source_dpi(files, row, source) if row else None
|
||||
if dpi is None:
|
||||
return None
|
||||
dpis.append(dpi)
|
||||
if all(source['kind'] == 'sheet' for source in sources):
|
||||
# A sheet's DPI is shown and compared as a whole number.
|
||||
return grade_of(min(js_round(d) for d in dpis))
|
||||
return js_round(sum(grade_of(d) for d in dpis) / len(dpis))
|
||||
|
||||
|
||||
def mismatch(files, items, rows):
|
||||
"""Why a cart's grades cannot be approved at checkout, or None."""
|
||||
for item in items:
|
||||
if item['grade'] == 0:
|
||||
continue # full price: nothing to verify
|
||||
verified = item_grade(files, item, rows)
|
||||
if verified is None:
|
||||
return 'Nota não conferida no servidor'
|
||||
if item['grade'] > verified + TOLERANCE:
|
||||
return f"Nota {item['grade']} maior que a do arquivo ({verified})"
|
||||
return None
|
||||
@@ -49,6 +49,26 @@ def is_test(order):
|
||||
return (order.get('payment') or {}).get('provider') == TEST_PROVIDER
|
||||
|
||||
|
||||
# How long a file is kept while nothing has been paid for it. A cart's files
|
||||
# are uploaded before payment, so the price and the security check are done on
|
||||
# the file itself; a cart that is never paid must not keep them for 30 days.
|
||||
UNPAID_HOLD = '2 days'
|
||||
# A quote waiting for an operator's review keeps its files this long.
|
||||
REVIEW_HOLD = '7 days'
|
||||
# Once paid, the order keeps its originals for this long from the upload.
|
||||
ORDER_RETENTION = '30 days'
|
||||
|
||||
|
||||
def quote_uploads(quote_or_draft):
|
||||
return [uid for item in quote_or_draft['items'] for uid in item['uploads']]
|
||||
|
||||
|
||||
def hold_uploads(c, upload_ids, interval):
|
||||
"""Keep these files at least `interval` from now; never shortens a hold."""
|
||||
c.execute('''UPDATE dtf_local.uploads SET expires_at=GREATEST(expires_at, now()+%s::interval)
|
||||
WHERE id=ANY(%s) AND purged_at IS NULL''', (interval, [str(u) for u in upload_ids]))
|
||||
|
||||
|
||||
def create_order(c, quote, payment):
|
||||
"""Create the order for a reviewed quote, or return the one already there.
|
||||
|
||||
@@ -68,6 +88,9 @@ def create_order(c, quote, payment):
|
||||
order = c.execute(
|
||||
'INSERT INTO dtf_local.orders(id,quote_id,owner,snapshot,payment) VALUES(%s,%s,%s,%s,%s) RETURNING *',
|
||||
(uuid4(), quote['id'], quote['owner'], Jsonb(approved), Jsonb(payment))).fetchone()
|
||||
# Paid: the files are kept for the order's retention, counted from upload.
|
||||
c.execute('''UPDATE dtf_local.uploads SET expires_at=GREATEST(expires_at, created_at+%s::interval)
|
||||
WHERE id=ANY(%s) AND purged_at IS NULL''', (ORDER_RETENTION, quote_uploads(approved)))
|
||||
queue_print_files(c, order['id'], len(approved['items']))
|
||||
if is_test(order):
|
||||
return order, True
|
||||
|
||||
@@ -6,11 +6,11 @@ moment it was created. The Site is a shop: a customer who can price the order
|
||||
should be able to pay for it straight away, at any hour, so only orders a
|
||||
person must look at before charging wait for the Kanban (review_reason).
|
||||
|
||||
Known limit: the grade (and so the discount) and the layout are still worked
|
||||
out in the customer's browser (roadmap 3.2, 3.9). The API checks the layout's
|
||||
geometry and that an unanalysed item carries no discount, but a customer who
|
||||
edits the page can claim a better grade. Operators see every order's grade
|
||||
and artwork at Arte recebida.
|
||||
The grade (and so the discount) is worked out in the customer's browser and
|
||||
checked against the files before an automatic approval (app/grade_check.py);
|
||||
a grade the files do not support waits for review. The layout is still the
|
||||
browser's (roadmap 3.9): the API checks its geometry. Operators see every
|
||||
order's grade and artwork at Arte recebida.
|
||||
"""
|
||||
import os
|
||||
from decimal import Decimal
|
||||
@@ -18,6 +18,7 @@ from decimal import Decimal
|
||||
from fastapi import HTTPException
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from . import payments
|
||||
from .core.pricing import price
|
||||
from .runtime import freight, upload_row
|
||||
from .scanning import require_clean
|
||||
@@ -83,4 +84,6 @@ def approve(c, row, items, reviewer):
|
||||
'total_cents': sum(i['total_cents'] for i in priced) + quoted_freight['total_cents']}
|
||||
c.execute('UPDATE dtf_local.quotes SET approved=%s, reviewed_by=%s, approved_at=now() WHERE id=%s',
|
||||
(Jsonb(approved), reviewer, row['id']))
|
||||
# Approved and payable now: the files wait for the payment, not for ever.
|
||||
payments.hold_uploads(c, payments.quote_uploads(approved), payments.UNPAID_HOLD)
|
||||
return approved
|
||||
|
||||
@@ -85,7 +85,7 @@ def quote_view(c, row):
|
||||
'draft': row['draft'], 'approved': row['approved'],
|
||||
'status': 'paid' if order else 'expired' if expired else 'approved' if row['approved'] else 'pending_review',
|
||||
'auto_approved': row.get('reviewed_by') == 'auto',
|
||||
'review_reason': None if row['approved'] else review_reason(row['draft']),
|
||||
'review_reason': None if row['approved'] else (row.get('review_note') or review_reason(row['draft'])),
|
||||
'payment': attempt,
|
||||
'order': order}
|
||||
|
||||
|
||||
@@ -142,6 +142,10 @@ CREATE TABLE IF NOT EXISTS dtf_local.backups (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS backups_finished ON dtf_local.backups(finished_at DESC);
|
||||
|
||||
-- Why a quote waits for review, when the reason came from its files (the grade
|
||||
-- the server recomputed) and cannot be worked out again from the draft alone.
|
||||
ALTER TABLE dtf_local.quotes ADD COLUMN IF NOT EXISTS review_note text;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS uploads_owner ON dtf_local.uploads(owner);
|
||||
|
||||
-- Indexes follow the queries the application actually issues. Only these; every
|
||||
|
||||
@@ -53,6 +53,11 @@ server {
|
||||
expires -1;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
# The customer's area: one page, which shows the right part for each address.
|
||||
location ~ ^/conta(/(entrar|pedidos|dados))?/?$ {
|
||||
expires -1;
|
||||
try_files /portal.html =404;
|
||||
}
|
||||
location / {
|
||||
expires -1;
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
@@ -627,6 +627,13 @@ the site already did.
|
||||
generator, with the browser as preview only. This is the single largest gap between
|
||||
what was promised in the meeting and what exists.
|
||||
|
||||
**2026-09-30:** the grade (the resolution discount) is no longer taken on trust.
|
||||
Before an automatic approval the API recomputes it from the uploaded files by the
|
||||
Site's rules (`app/grade_check.py`): image headers for PNG/JPG/WebP, the placed
|
||||
images of a PDF up to 150 MB. A claim more than 2 points above the file's grade,
|
||||
or a discount on a file the server cannot grade, waits for review with the reason
|
||||
on the Kanban. The metres (the packing) are still the browser's.
|
||||
|
||||
### `[~]` 3.3 — The 5 GB problem is unsolved `(F19)`
|
||||
|
||||
Transport accepts 5 GiB; `SCAN_MAX_BYTES` / ClamAV `StreamMaxLength` release only
|
||||
|
||||
@@ -23,6 +23,10 @@ server {
|
||||
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho|pagamento|pagamento/pix)/?$ {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
# The customer's area: one page, which shows the right part for each address.
|
||||
location ~ ^/conta(/(entrar|pedidos|dados))?/?$ {
|
||||
try_files /portal.html =404;
|
||||
}
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
}
|
||||
server {
|
||||
|
||||
@@ -170,6 +170,17 @@ try{
|
||||
await evaluate(`folhas=[];pintaFolha()`);
|
||||
await evaluate(`pickTipo('avulsa')`);
|
||||
assert.equal(await evaluate('modo'),'avulsa');
|
||||
// A large JPG artwork cannot be read in parts: it is measured from its
|
||||
// header and placed as a full box, with the discount, instead of refused.
|
||||
await evaluate(`(()=>{const h=new Uint8Array(20);h.set([0xFF,0xD8,0xFF,0xC0,0,17,8,0x09,0x3A,0x09,0x3A,3]);
|
||||
const f=new File([h],'arte-grande.jpg');Object.defineProperty(f,'size',{value:400*1048576});sel([f]);})()`);
|
||||
await waitFor(()=>evaluate(`artes.length===1 && artes[0].semPrevia===true`),'large JPG artwork measured from its header');
|
||||
await fill('[data-cm]',20);
|
||||
await waitFor(()=>evaluate(`!!itemAtual?.production?.sources?.length`),'large JPG artwork packed');
|
||||
assert.deepEqual(await evaluate(`({dpi:Math.round(dpiDe(artes[0])),src:!!artes[0].src,
|
||||
measurement:itemAtual.production.sources[0].measurement,note:$('lista').textContent.includes('Sem prévia')})`),
|
||||
{dpi:300,src:false,measurement:'file',note:true});
|
||||
await evaluate(`artes=[];pintaArtes()`);
|
||||
await evaluate('sendImage()');
|
||||
await waitFor(()=>evaluate(`artes.length===1 && !!artes[0].src`),'JPG artwork model');
|
||||
assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,width:artes[0].cm,quantity:artes[0].q})`),{mode:'avulsa',sheets:0,width:0,quantity:1});
|
||||
|
||||
@@ -79,7 +79,12 @@ try{
|
||||
await site.fill('#fCnpj','11222333000181');
|
||||
await site.fill('#fZap','11999999999');
|
||||
await site.fill('#fMail','local-browser@example.test');
|
||||
await pause(800);
|
||||
// Reload only once the cart is in the browser's storage: the save waits for
|
||||
// typing to stop and writes the files, so a fixed pause raced it.
|
||||
await waitFor(()=>site.eval(`new Promise(done=>{const open=indexedDB.open('dtf-local-cart',1);
|
||||
open.onerror=()=>done(false);
|
||||
open.onsuccess=()=>{const all=open.result.transaction('cart').objectStore('cart').getAll();
|
||||
all.onsuccess=()=>done(all.result.some(v=>v.items?.length===1));all.onerror=()=>done(false);};})`),'cart saved in the browser');
|
||||
await site.call('Page.reload');
|
||||
await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery');
|
||||
// Reloading a product page reopens that product; the cart is its own page.
|
||||
@@ -93,7 +98,19 @@ try{
|
||||
assert.deepEqual(await site.eval(`({items:pedido.length,empty:!$('carrVazio').hidden||document.documentElement.hasAttribute('data-sem-itens'),undo:!$('desfazer').hidden})`),{items:0,empty:true,undo:true});
|
||||
await site.click('#desfazerBtn');
|
||||
assert.equal(await site.eval('pedido.length===1 && pedido[0].total===21.89 && $("desfazer").hidden'),true);
|
||||
// The items come back after a reload; the customer's details do not.
|
||||
assert.deepEqual(await site.eval(`['fCnpj','fZap','fMail'].map(id=>$(id).value)`),['','','']);
|
||||
assert.equal(await site.eval('document.getElementById("bPagar").disabled'),true);
|
||||
await site.fill('#fCnpj','11222333000181');
|
||||
await site.fill('#fZap','11999999999');
|
||||
await site.fill('#fMail','local-browser@example.test');
|
||||
assert.equal(await site.eval('pedido[0].localFiles[0].name'),'local-test.cdr');
|
||||
// An unchecked item stays in the cart but is not paid for now.
|
||||
await site.click('[data-sel="0"]');
|
||||
assert.deepEqual(await site.eval(`({marcado:pedido[0].marcado,pagar:itensAPagar().length,zero:$('carrTot').textContent.includes('0,00'),row:document.querySelector('.item').classList.contains('fora')})`),
|
||||
{marcado:false,pagar:0,zero:true,row:true});
|
||||
await site.click('[data-sel="0"]');
|
||||
assert.equal(await site.eval('pedido[0].marcado===true && itensAPagar().length===1'),true);
|
||||
assert.equal(await site.eval('pedido[0].total'),21.89);
|
||||
assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false);
|
||||
await site.click('#bPagar');
|
||||
@@ -175,14 +192,20 @@ try{
|
||||
await waitFor(()=>site.eval('window.scrollY===0'),'screenshot scroll position');
|
||||
await site.screenshot('output/local/site.png');
|
||||
await kanban.screenshot('output/local/kanban.png');
|
||||
const portal=await page(siteOrigin+'/portal.html?order='+oid);
|
||||
// A guest sees the order paid in this browser; creating an account keeps it.
|
||||
const portal=await page(siteOrigin+'/conta/pedidos?order='+oid);
|
||||
await waitFor(async()=> (await portal.text()).includes('Finalizado'),'customer order tracking');
|
||||
await waitFor(()=>portal.eval('document.querySelector("#order-'+oid+' .detalhe")?.hidden===false'),'order from the link opened');
|
||||
await portal.click('#navEntrar');
|
||||
await waitFor(()=>portal.eval('location.pathname==="/conta/entrar" && !document.getElementById("auth").hidden'),'sign-in page');
|
||||
await portal.fill('#cnpj','11222333000181');await portal.fill('#phone','11999999999');
|
||||
await portal.fill('#register-email','browser-'+Date.now()+'@example.test');
|
||||
await portal.fill('#register-password','local-browser-password-123');
|
||||
await portal.eval('document.getElementById("register").requestSubmit()');
|
||||
await waitFor(()=>portal.eval('document.getElementById("auth").hidden'),'customer registration');
|
||||
assert.ok((await portal.text()).includes('Finalizado'));
|
||||
await waitFor(()=>portal.eval('document.getElementById("auth").hidden && location.pathname==="/conta"'),'customer registration');
|
||||
await waitFor(async()=> (await portal.text()).includes('Finalizado'),'overview shows the last order');
|
||||
await portal.click('[data-view="dados"]');
|
||||
await waitFor(()=>portal.eval('location.pathname==="/conta/dados" && document.getElementById("dCnpj").value==="11.222.333/0001-81"'),'account details');
|
||||
await portal.screenshot('output/local/portal.png');
|
||||
// A logout must clear draft file blobs and metadata, including other open Site tabs.
|
||||
await portal.eval(`(async()=>{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onsuccess=()=>resolve(r.result);r.onerror=reject;});await new Promise((resolve,reject)=>{const tx=db.transaction('cart','readwrite');tx.objectStore('cart').put({items:[new File(['private'],'private.cdr')],expires:Date.now()+86400000},'security-fixture');tx.oncomplete=resolve;tx.onerror=reject;});db.close();})()`);
|
||||
|
||||
@@ -155,6 +155,30 @@ def run():
|
||||
assert queued == 0 and jobs == 1, (queued, jobs)
|
||||
print('PASS: an operator test order reaches the board and the print queue, never Tiny or WhatsApp')
|
||||
|
||||
# Files are uploaded before payment. An unpaid cart keeps them briefly; a
|
||||
# paid order keeps them for its 30 days; a payment never starts for files
|
||||
# that are gone.
|
||||
def files_of(qid):
|
||||
with db.connect() as c:
|
||||
q = c.execute('SELECT approved,draft FROM dtf_local.quotes WHERE id=%s', (qid,)).fetchone()
|
||||
return [u for item in (q['approved'] or q['draft'])['items'] for u in item['uploads']]
|
||||
|
||||
def days(ids, since='now()'):
|
||||
with db.connect() as c:
|
||||
return [float(r['d']) for r in c.execute(
|
||||
f'SELECT extract(epoch FROM expires_at-{since})/86400 AS d FROM dtf_local.uploads WHERE id=ANY(%s)',
|
||||
(ids,)).fetchall()]
|
||||
fresh = upload_bytes(customer, b'UNPAID HOLD TEST')
|
||||
assert all(1.99 < d <= 2.0 for d in days([fresh])), days([fresh])
|
||||
assert all(abs(d - 30) < 0.01 for d in days(files_of(quote_id), 'created_at')), days(files_of(quote_id), 'created_at')
|
||||
late, late_quote, _ = reviewed_quote()
|
||||
assert all(d > 1.99 for d in days(files_of(late_quote))), days(files_of(late_quote))
|
||||
with db.connect() as c:
|
||||
c.execute("UPDATE dtf_local.uploads SET expires_at=now()-interval '1 second' WHERE id=ANY(%s)",
|
||||
(files_of(late_quote),))
|
||||
late.call('/payments/intent', {'quote_id': late_quote, 'method': {'type': 'pix'}}, expected=410)
|
||||
print('PASS: unpaid files are held 2 days, paid ones 30 days, and expired files are never charged for')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
|
||||
@@ -175,7 +175,11 @@ def run():
|
||||
assert not client.call('/quotes/'+qid)['auto_approved']
|
||||
# A cart the Site priced is approved at once and can be paid straight away,
|
||||
# at the server's own prices; no operator can then change it.
|
||||
small={**draft,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,uid)]}
|
||||
# The server works the grade out from the file: this PNG is 6059 px across
|
||||
# 57 cm, 270 DPI, which is grade 90.
|
||||
png=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR'+(6059).to_bytes(4,'big')+(2000).to_bytes(4,'big')+b'\x08\x06\x00\x00\x00'+b'\x00'*4
|
||||
graded=upload_bytes(client,png,name='arte-270dpi.png')
|
||||
small={**draft,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,graded)]}
|
||||
auto=client.call('/quotes',small)
|
||||
assert auto['status']=='approved'
|
||||
seen=client.call('/quotes/'+auto['id'])
|
||||
@@ -188,6 +192,12 @@ def run():
|
||||
held=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[claimed]})
|
||||
assert held['status']=='pending_review'
|
||||
assert client.call('/quotes/'+held['id'])['review_reason']=='Nota informada sem análise da arte'
|
||||
# A better grade than the file supports, or one the server cannot check, waits too.
|
||||
inflated=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',100,graded)]})
|
||||
assert inflated['status']=='pending_review'
|
||||
assert client.call('/quotes/'+inflated['id'])['review_reason']=='Nota 100 maior que a do arquivo (90)'
|
||||
unchecked=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,uid)]})
|
||||
assert client.call('/quotes/'+unchecked['id'])['review_reason']=='Nota não conferida no servidor'
|
||||
print('PASS: priced carts are approved at checkout; large or inconsistent ones wait for review')
|
||||
client.call('/orders/dev-paid',{'quote_id':qid,'total_cents':1},expected=422)
|
||||
other.call('/orders/dev-paid',{'quote_id':qid},expected=404)
|
||||
|
||||
86
tests/test_grade_check.py
Normal file
86
tests/test_grade_check.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""The server's grade against real image and PDF files.
|
||||
|
||||
Runs where Pillow and pikepdf are installed (the API image).
|
||||
"""
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
from app import grade_check
|
||||
|
||||
|
||||
class FakeFiles:
|
||||
def __init__(self, blobs):
|
||||
self.blobs = blobs
|
||||
|
||||
def head(self, row):
|
||||
return self.blobs[row['name']][:grade_check.HEAD_BYTES]
|
||||
|
||||
def pdf_dpi(self, row):
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
handle.write(self.blobs[row['name']])
|
||||
handle.flush()
|
||||
return grade_check.pdf_dpi(handle.name)
|
||||
|
||||
|
||||
def encoded(fmt, size, **options):
|
||||
out = io.BytesIO()
|
||||
Image.new('RGBA' if fmt in ('PNG', 'WEBP') else 'RGB', size, (200, 30, 30)).save(out, fmt, **options)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def item(kind, grade, sources):
|
||||
return {'grade': grade, 'production': {'sources': [
|
||||
{'upload_id': name, 'kind': kind, 'width_cm': width, 'rotation_degrees': rotation}
|
||||
for name, width, rotation in sources]}}
|
||||
|
||||
|
||||
class GradeCheckTests(unittest.TestCase):
|
||||
def test_image_sizes_from_the_first_bytes(self):
|
||||
for fmt, options in (('PNG', {}), ('JPEG', {'quality': 80}), ('JPEG', {'progressive': True}),
|
||||
('WEBP', {'lossless': True}), ('WEBP', {'quality': 80})):
|
||||
self.assertEqual(grade_check.image_size(encoded(fmt, (1234, 567), **options)), (1234, 567), (fmt, options))
|
||||
# A JPEG with a large metadata block before the frame header.
|
||||
exif = Image.Exif()
|
||||
exif[0x010E] = 'x' * 60000
|
||||
self.assertEqual(grade_check.image_size(encoded('JPEG', (800, 600), exif=exif)), (800, 600))
|
||||
self.assertIsNone(grade_check.image_size(b'not an image at all'))
|
||||
|
||||
def test_pdf_dpi_is_the_images_area_weighted(self):
|
||||
out = io.BytesIO()
|
||||
Image.new('RGB', (1500, 750), 'white').save(out, 'PDF', resolution=150)
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
handle.write(out.getvalue()); handle.flush()
|
||||
self.assertEqual(grade_check.pdf_dpi(handle.name), 150)
|
||||
vector = pikepdf.new()
|
||||
vector.add_blank_page(page_size=(1615, 850))
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
vector.save(handle.name)
|
||||
self.assertEqual(grade_check.pdf_dpi(handle.name), 300)
|
||||
|
||||
def test_grades_follow_the_site(self):
|
||||
# 57 cm is 22.44 in: 6059 px is 270 DPI, grade 90; 3366 px is 150 DPI, grade 50.
|
||||
files = FakeFiles({'a.png': encoded('PNG', (6059, 100)), 'b.jpg': encoded('JPEG', (3366, 100)),
|
||||
'tall.png': encoded('PNG', (100, 2362)), 'x.cdr': b'CDR'})
|
||||
rows = {n: {'name': n, 'size': 1} for n in files.blobs}
|
||||
# A sheet takes the worst sheet's grade.
|
||||
self.assertEqual(grade_check.item_grade(files, item('sheet', 0, [('a.png', 57, 0)]), rows), 90)
|
||||
self.assertEqual(grade_check.item_grade(files, item('sheet', 0, [('a.png', 57, 0), ('b.jpg', 57, 0)]), rows), 50)
|
||||
# Artworks average; a rotated one is measured along its height.
|
||||
self.assertEqual(grade_check.item_grade(files, item('artwork', 0, [('a.png', 57, 0), ('b.jpg', 57, 0)]), rows), 70)
|
||||
self.assertEqual(grade_check.item_grade(files, item('artwork', 0, [('tall.png', 20, 90)]), rows), 100)
|
||||
self.assertIsNone(grade_check.item_grade(files, item('sheet', 0, [('x.cdr', 57, 0)]), rows))
|
||||
# What the cart claims is checked, with a point or two of rounding.
|
||||
claim = lambda g, src: grade_check.mismatch(files, [item('sheet', g, src)], rows)
|
||||
self.assertIsNone(claim(90, [('a.png', 57, 0)]))
|
||||
self.assertIsNone(claim(92, [('a.png', 57, 0)]))
|
||||
self.assertEqual(claim(100, [('a.png', 57, 0)]), 'Nota 100 maior que a do arquivo (90)')
|
||||
self.assertEqual(claim(40, [('x.cdr', 57, 0)]), 'Nota não conferida no servidor')
|
||||
self.assertIsNone(claim(0, [('x.cdr', 57, 0)]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -15,7 +15,23 @@ def run():
|
||||
password='local-test-password-'+uuid4().hex
|
||||
customer.call('/account/register',{'customer':profile,'password':password})
|
||||
assert customer.call('/account/me')['customer']['mail']==profile['mail']
|
||||
assert customer.call('/customer/orders')['orders'][0]['id']==oid
|
||||
assert [e['id'] for e in customer.call('/customer/orders')['items'] if e['kind']=='order'][0]==oid
|
||||
# The list filters, pages and counts by group.
|
||||
listing=customer.call('/customer/orders?status=prod&page=1&size=1')
|
||||
assert listing['counts']['prod']>=1 and listing['total']==listing['counts']['prod'] and len(listing['items'])==1, listing
|
||||
assert customer.call('/customer/orders?status=fin')['items']==[]
|
||||
assert customer.call('/customer/orders?number='+str(order['number']))['items'][0]['id']==oid
|
||||
# The account's details change; the CNPJ does not, and e-mail and password need the password.
|
||||
address={'recipient':'Workflow Ltda','street':'Rua de Teste','number':'10','complement':'',
|
||||
'district':'Centro','city':'Franca','state':'SP','postal_code':'14400000'}
|
||||
saved=customer.call('/account/profile',{'zap':'(16) 98888-7777','address':address})['customer']
|
||||
assert saved['zap']=='16988887777' and saved['cnpj']==profile['cnpj'] and saved['address']['city']=='Franca'
|
||||
assert customer.call('/account/me')['customer']['address']['postal_code']=='14400000'
|
||||
customer.call('/account/profile',{'zap':'16988887777','cnpj':'00000000000000'},expected=422)
|
||||
customer.call('/account/email',{'email':'x-'+profile['mail'],'password':'wrong-password'},expected=401)
|
||||
customer.call('/account/password',{'current':'wrong-password','new':'another-password-123'},expected=401)
|
||||
other.call('/account/profile',{'zap':'16988887777'},expected=401)
|
||||
print('PASS: customer order list filters and pages; account details change, CNPJ does not')
|
||||
# Email/CNPJ do not grant ownership; only current guest session is migrated.
|
||||
other.call('/customer/orders/'+oid,expected=404)
|
||||
other.call('/account/login',{'email':profile['mail'],'password':'wrong-password'},expected=401)
|
||||
@@ -72,7 +88,7 @@ def run():
|
||||
old_cookie=list(other.jar)[0].value
|
||||
other.call('/account/logout',{})
|
||||
cookie.value=old_cookie;revoked.jar.set_cookie(cookie);revoked.call('/customer/orders',expected=401)
|
||||
other.call('/session');assert other.call('/customer/orders')['orders']==[]
|
||||
other.call('/session');assert other.call('/customer/orders')['items']==[]
|
||||
print('PASS: logout revokes server session and signed-out visitors cannot see account orders')
|
||||
|
||||
if __name__=='__main__':run()
|
||||
|
||||
42
web/cart.js
42
web/cart.js
@@ -1,5 +1,11 @@
|
||||
/* Browser-local cart recovery. Files are stored only for an unfinished cart, for 24h. */
|
||||
/* Browser-local cart recovery. Files are stored only for an unfinished cart, for 24h.
|
||||
Only the items are kept: the customer's details and address are typed again
|
||||
after a reload or a closed tab, so nothing personal stays in the browser. */
|
||||
(() => {
|
||||
// A browser may put back what was typed before the reload without telling
|
||||
// the page, which then shows values it does not know and cannot pay with.
|
||||
const CAMPOS=['fCnpj','fZap','fMail','cepIn','eNome','eRua','eNum','eComp','eBairro','eCidade','eUf'];
|
||||
for(const id of CAMPOS)if($(id))$(id).value='';
|
||||
let database,scope,timer,restoring=true;
|
||||
let signedOut=false;
|
||||
window.addEventListener('dtf-private-data-cleared',()=>{signedOut=true;clearTimeout(timer);pedido=[];itemAtual=null;folhas=[];artes=[];});
|
||||
@@ -11,8 +17,8 @@
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
try{
|
||||
if(!items.length){await transaction('readwrite',store=>store.delete(scope));return;}
|
||||
await transaction('readwrite',store=>store.put({items,customer:{...cliente},delivery:{...entrega},expires:Date.now()+86400000},scope));
|
||||
notice.textContent='Carrinho salvo neste navegador por 24 horas. Você pode remover itens e adicionar outros após recarregar.';
|
||||
await transaction('readwrite',store=>store.put({items,expires:Date.now()+86400000},scope));
|
||||
notice.textContent='';
|
||||
}catch(error){notice.textContent='Não foi possível salvar o carrinho neste navegador. Mantenha esta página aberta até enviar o pedido.';}
|
||||
}
|
||||
window.dtfClearCart=async()=>{clearTimeout(timer);if(database&&scope)await transaction('readwrite',store=>store.delete(scope));notice.textContent='';};
|
||||
@@ -30,20 +36,26 @@
|
||||
for(const key of records){const value=await transaction('readonly',store=>store.get(key));if(value.expires<Date.now())await transaction('readwrite',store=>store.delete(key));}
|
||||
const saved=await transaction('readonly',store=>store.get(scope));
|
||||
if(saved && !pedido.length && !itemAtual){
|
||||
pedido=saved.items;cliente=saved.customer;entrega=saved.delivery;
|
||||
// Freight must be quoted again; restored browser values are never final.
|
||||
if(entrega.tipo==='frete'){entrega.cotado=false;entrega.valor=0;entrega.dias=null;}
|
||||
for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']])$(id).value=cliente[key];
|
||||
$('cepIn').value=fmtCep(entrega.cep);
|
||||
entrega.end=entrega.end||{};
|
||||
for(const [id,key] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp','comp'],
|
||||
['eBairro','bairro'],['eCidade','cidade'],['eUf','uf']])$(id).value=entrega.end[key]||'';
|
||||
pedido=saved.items;
|
||||
$('atual').style.display='none';pintaEntrega();
|
||||
if(entrega.tipo==='frete' && entrega.cep.length===8 && window.dtfFreight) window.dtfFreight();
|
||||
notice.textContent='Carrinho recuperado. Remova e adicione novamente um item se precisar alterar sua montagem.';
|
||||
}else{
|
||||
const account=await window.dtfApi('/account/me');
|
||||
if(account.customer && !cliente.mail){cliente={...account.customer};for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']])$(id).value=cliente[key];pintaEntrega();}
|
||||
}
|
||||
// A signed-in customer's details come from the account, with the saved
|
||||
// delivery address, whether or not a cart was recovered.
|
||||
const account=await window.dtfApi('/account/me');
|
||||
const c=account.customer;
|
||||
if(c && !cliente.mail){
|
||||
cliente={cnpj:c.cnpj,zap:c.zap,mail:c.mail};
|
||||
// The fields' own input handlers format them, as if typed.
|
||||
for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']]){$(id).value=cliente[key];$(id).dispatchEvent(new Event('input'));}
|
||||
const a=c.address;
|
||||
if(a && !entrega.cep){
|
||||
entrega.cep=a.postal_code;
|
||||
entrega.end={nome:a.recipient,rua:a.street,num:a.number,comp:a.complement||'',bairro:a.district,cidade:a.city,uf:a.state};
|
||||
$('cepIn').value=a.postal_code.replace(/^(\d{5})(\d{3})$/,'$1-$2');
|
||||
for(const [id,key] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp','comp'],['eBairro','bairro'],['eCidade','cidade'],['eUf','uf']])$(id).value=entrega.end[key];
|
||||
}
|
||||
pintaEntrega();
|
||||
}
|
||||
}catch(error){notice.textContent='Recuperação de carrinho indisponível; o pedido ainda pode ser feito nesta sessão.';}
|
||||
finally{restoring=false;}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
let quotedCart = localStorage.getItem('dtf-quote-cart');
|
||||
let refreshVersion = 0;
|
||||
function cartSnapshot() {
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
const items=itensAPagar();
|
||||
return JSON.stringify({customer:cliente,delivery:entrega,items:items.map(item=>({
|
||||
mode:item.modo,metres:item.metros,grade:item.nota,production:item.production,
|
||||
quality:item.qualityStatus,
|
||||
@@ -127,7 +127,7 @@
|
||||
arquivos.forEach(f=>enviar(f).catch(()=>{}));
|
||||
pintaEnvio();
|
||||
}
|
||||
const arquivosDoCarrinho=()=>[...pedido,...(busy&&itemAtual?[itemAtual]:[])].flatMap(it=>it.localFiles||[]);
|
||||
const arquivosDoCarrinho=()=>[...pedido.filter(it=>it.marcado!==false),...(busy&&itemAtual?[itemAtual]:[])].flatMap(it=>it.localFiles||[]);
|
||||
const gb=n=>(n/1073741824).toLocaleString('pt-BR',{maximumFractionDigits:1})+' GB';
|
||||
const mb=n=>n>=1073741824 ? gb(n) : Math.round(n/1048576)+' MB';
|
||||
// Where the uploads stand, and roughly how long is left from the recent speed.
|
||||
@@ -147,13 +147,15 @@
|
||||
return {total, sent, fase, pct:Math.floor(sent/total*100),
|
||||
restante: fase==='enviando' && taxa>0 ? (total-sent)/taxa : null};
|
||||
}
|
||||
const tempo=s=>s<60 ? 'menos de 1 min' : 'cerca de '+Math.ceil(s/60)+' min';
|
||||
const falta=s=>s<60 ? 'falta menos de 1 min' : 'faltam cerca de '+duracao(s);
|
||||
// The time left, measured from the upload's own speed; until there is a
|
||||
// measure, a range from the size.
|
||||
const restante=e=>e.restante!=null ? falta(e.restante) : 'estimativa '+faixaEnvio(e.total-e.sent);
|
||||
function textoEnvio(e) {
|
||||
if (!e) return '';
|
||||
if (e.fase==='pronto') return 'Arquivos enviados ✓';
|
||||
if (e.fase==='pronto') return 'Arquivos enviados';
|
||||
if (e.fase==='verificando') return 'Arquivos enviados · verificando a segurança…';
|
||||
return 'Enviando seus arquivos: '+e.pct+'% · '+mb(e.sent)+' de '+mb(e.total)+
|
||||
(e.restante!=null ? ' · '+tempo(e.restante) : '');
|
||||
return 'Enviando seus arquivos · '+restante(e);
|
||||
}
|
||||
function pintaEnvio() {
|
||||
const e=estadoEnvio(), texto=textoEnvio(e);
|
||||
@@ -179,14 +181,20 @@
|
||||
const i=document.createElement('i'); i.style.width=(e.fase==='verificando'?100:e.pct)+'%'; barra.append(i);
|
||||
el.append(barra);
|
||||
}
|
||||
if (e?.fase==='pronto') el.insertAdjacentHTML('beforeend', icone('okCirculo'));
|
||||
if (texto) el.append(document.createTextNode(texto));
|
||||
if (e?.fase==='enviando') {
|
||||
const nota=document.createElement('small'); nota.className='envioNota';
|
||||
nota.textContent='O tempo depende da velocidade da sua internet. Mantenha a página aberta.';
|
||||
el.append(nota);
|
||||
}
|
||||
el.classList.toggle('ok', e?.fase==='pronto');
|
||||
}
|
||||
// Visible on every page while it runs.
|
||||
if (topo) topo.textContent = !e || e.fase==='pronto' ? '' : e.fase==='verificando' ? '· verificando' : '· enviando '+e.pct+'%';
|
||||
if (topo) topo.textContent = !e || e.fase==='pronto' ? '' : e.fase==='verificando' ? '· verificando' : e.restante!=null ? '· '+duracao(e.restante) : '· enviando';
|
||||
if (busy && texto) {
|
||||
message(texto);
|
||||
$('bPagar').textContent = e && e.fase!=='pronto' ? (e.fase==='verificando' ? 'Verificando os arquivos…' : 'Enviando arquivos… '+e.pct+'%') : $('bPagar').textContent;
|
||||
$('bPagarTx').textContent = e && e.fase!=='pronto' ? (e.fase==='verificando' ? 'Verificando os arquivos…' : 'Enviando arquivos… '+restante(e)) : $('bPagarTx').textContent;
|
||||
}
|
||||
}
|
||||
// Leaving the page pauses an upload; it resumes, but the customer is warned.
|
||||
@@ -205,7 +213,7 @@
|
||||
// The cart's package: billed metres and value. The charged freight is
|
||||
// quoted again by the server from the approved items.
|
||||
const pacote = () => {
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
const items=itensAPagar();
|
||||
return {metres:items.reduce((t,it)=>t+(it.cob||0),0), cents:Math.round(items.reduce((t,it)=>t+(it.total||0),0)*100)};
|
||||
};
|
||||
let freightQuoted = null;
|
||||
@@ -244,7 +252,7 @@
|
||||
if (!clienteOk() || !entrega.cotado) return;
|
||||
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.');
|
||||
const cart = [...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
const cart = itensAPagar();
|
||||
if (!cart.length) return message('Adicione um item ao pedido.');
|
||||
const initialCart=cartSnapshot();
|
||||
busy = true;
|
||||
@@ -330,8 +338,7 @@
|
||||
event.target.disabled=true;
|
||||
try {
|
||||
const order=await api('/orders/dev-paid',{quote_id:draftId});
|
||||
pedido=[]; itemAtual=null; limpaPaineis();
|
||||
await window.dtfClearCart?.();
|
||||
await pagos();
|
||||
message('Pedido #'+order.number+' criado e disponível no Kanban.');
|
||||
await refresh();
|
||||
} catch(error) { message(error.message); event.target.disabled=false; }
|
||||
@@ -357,8 +364,7 @@
|
||||
const quote=await api('/quotes/'+draftId);
|
||||
if (quote.status==='paid') {
|
||||
clearInterval(waitTimer);
|
||||
pedido=[]; itemAtual=null; limpaPaineis();
|
||||
await window.dtfClearCart?.();
|
||||
await pagos();
|
||||
await refresh();
|
||||
} else if (quote.payment?.status==='rejected') {
|
||||
// Refused after the bank's confirmation or the provider's review.
|
||||
@@ -386,12 +392,19 @@
|
||||
function confirmado(quote) {
|
||||
clearInterval(waitTimer);
|
||||
const box=node('div',null,'confirmado');
|
||||
box.append(node('h4','Pagamento confirmado'),
|
||||
const titulo=node('h4','Pagamento confirmado');
|
||||
titulo.insertAdjacentHTML('afterbegin', icone('okCirculo'));
|
||||
box.append(titulo,
|
||||
node('p','Pedido #'+quote.order.number+' recebido e enviado para a produção. Você acompanha cada etapa em Meus pedidos.'));
|
||||
button('Ver meus pedidos',()=>location.assign('/portal.html?order='+quote.order.id),box);
|
||||
button('Ver meus pedidos',()=>location.assign('/conta/pedidos?order='+quote.order.id),box);
|
||||
actions.append(box);
|
||||
}
|
||||
const cartOk=()=>!!quotedCart && quotedCart===cartSnapshot();
|
||||
// What was paid leaves the cart; unchecked items stay for another order.
|
||||
async function pagos() {
|
||||
pedido=pedido.filter(it=>it.marcado===false); itemAtual=null; limpaPaineis();
|
||||
if (pedido.length) pintaPedido(); else await window.dtfClearCart?.();
|
||||
}
|
||||
|
||||
// Paying: card (the default, paid on this page) or PIX (paid on its own page).
|
||||
async function escolhaPagamento(quote, version) {
|
||||
@@ -404,6 +417,7 @@
|
||||
const radios=opcoes.map(([key,label])=>{
|
||||
const el=button(label,()=>{ if(escolhido!==key){ escolhido=key; pinta(); } },grupo,'metodo');
|
||||
el.setAttribute('role','radio'); el.dataset.metodo=key;
|
||||
el.insertAdjacentHTML('afterbegin', icone(key==='pix'?'qrcode':'cartao'));
|
||||
return el;
|
||||
});
|
||||
actions.append(node('h4','Forma de pagamento'),grupo,area);
|
||||
@@ -451,10 +465,14 @@
|
||||
code.readOnly=true;code.value=intent.pix_qr_code;code.setAttribute('aria-label','Código PIX copia e cola');
|
||||
linha.append(code);
|
||||
const copiar=button('Copiar código',async()=>{
|
||||
try{ await navigator.clipboard.writeText(intent.pix_qr_code); copiar.textContent='Código copiado'; }
|
||||
try{ await navigator.clipboard.writeText(intent.pix_qr_code);
|
||||
copiar.replaceChildren(); copiar.insertAdjacentHTML('beforeend', icone('ok')+'<span>Código copiado</span>'); }
|
||||
catch(_){ code.select(); }
|
||||
},linha);
|
||||
copiar.insertAdjacentHTML('afterbegin', icone('copiar'));
|
||||
const prazo=node('p',null,'prazo');
|
||||
prazo.insertAdjacentHTML('afterbegin', icone('relogio'));
|
||||
const prazoTx=node('span'); prazo.append(prazoTx);
|
||||
box.append(linha,prazo,node('p','Aguardando a confirmação do pagamento…','aguarda'));
|
||||
actions.append(box);
|
||||
waitForOrder();
|
||||
@@ -463,7 +481,7 @@
|
||||
if (!Number.isFinite(fim)) { prazo.remove(); return; }
|
||||
const tick=()=>{
|
||||
const s=Math.max(0,Math.ceil((fim-Date.now())/1000));
|
||||
prazo.textContent='Pague em '+String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');
|
||||
prazoTx.textContent='Pague em '+String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');
|
||||
if (s>0) return;
|
||||
clearInterval(pixClock);
|
||||
const fimBox=node('div',null,'pixBox');
|
||||
|
||||
444
web/index.html
444
web/index.html
@@ -7,79 +7,14 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/site-shell.css">
|
||||
<style>
|
||||
:root{
|
||||
/* tokens do site institucional */
|
||||
--font-family-base:"Roboto",sans-serif; --font-family-title:"Inter",sans-serif;
|
||||
--font-color-base:#03060b; --font-color-title:#03060b;
|
||||
--font-weight-base:400; --font-weight-title:700; --font-weight-subtitle:600; --font-weight-button:600;
|
||||
--font-size-title:3rem; --font-size-subtitle:2rem; --font-size-button:1.4rem;
|
||||
--font-size-small:1.2rem; --font-size-base:1.4rem; --font-size-medium:1.6rem; --font-size-big:1.8rem;
|
||||
--line-height:1.5;
|
||||
--white:#fff; --black:#03060b;
|
||||
--gray-1:#f9f9f9; --gray-2:#c3c3c3; --gray-3:#707070; --gray-4:#363636;
|
||||
--primary-color:#ffa81a; --secondary-color:#ff7f0a;
|
||||
--white-transparency:#ffffffb3; --transition:all .4s ease;
|
||||
/* apelidos usados nesta pagina, mapeados nos tokens acima */
|
||||
--marinho:var(--black); --papel:var(--white); --cinza:var(--gray-1);
|
||||
--tx:var(--font-color-base); --fraco:var(--gray-3); --linha:#e6e6e6;
|
||||
--laranja:var(--primary-color); --laranja2:var(--secondary-color);
|
||||
--vermelho:#D94A28; --verde:#2E9E5B; --verde-ml:#00A650; --verde-claro:#57D68A;
|
||||
--sombra:0 1px 2px rgba(3,6,11,.05), 0 4px 14px rgba(3,6,11,.05);
|
||||
--sombra-alta:0 2px 6px rgba(3,6,11,.07), 0 10px 30px rgba(3,6,11,.11)}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth;font-size:62.5%}
|
||||
body{background:var(--white);color:var(--font-color-base);font-family:var(--font-family-base);
|
||||
font-size:var(--font-size-base);font-weight:var(--font-weight-base);line-height:var(--line-height);
|
||||
-webkit-font-smoothing:antialiased}
|
||||
h1,h2,h3{font-family:var(--font-family-title);color:var(--font-color-title);
|
||||
font-weight:var(--font-weight-title);letter-spacing:normal;line-height:1.15}
|
||||
.w{max-width:1020px;margin:0 auto;padding:0 22px}
|
||||
section{padding:clamp(34px,5vw,56px) 0}
|
||||
section+section{border-top:1px solid var(--linha)}
|
||||
.prova+section{border-top:0}
|
||||
|
||||
/* tarja rotativa, como no site */
|
||||
.tarja{background:var(--laranja);color:#111;text-align:center;padding:8px 16px;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:12px;letter-spacing:.02em}
|
||||
.tarja a{color:#111;text-decoration:none}
|
||||
|
||||
/* topo */
|
||||
.topo{background:#fff;border-bottom:1px solid var(--linha);position:sticky;top:0;z-index:60}
|
||||
.topo .w{display:flex;align-items:center;gap:22px;padding-top:15px;padding-bottom:15px}
|
||||
.logo{display:flex;align-items:center;text-decoration:none;white-space:nowrap;flex:none}
|
||||
.logo img{height:26px;width:auto;display:block}
|
||||
.logotexto{font-family:"Inter",sans-serif;font-weight:700;font-size:19px;color:var(--black);
|
||||
letter-spacing:-.03em}
|
||||
.logotexto i{font-style:normal;color:var(--laranja)}
|
||||
.busca{flex:1;max-width:420px;position:relative}
|
||||
.busca input{width:100%;padding:9px 14px 9px 36px;border:1px solid var(--linha);border-radius:22px;
|
||||
font-family:inherit;font-size:13px;background:var(--cinza);color:var(--tx)}
|
||||
.busca input:focus{outline:none;border-color:var(--laranja);background:#fff}
|
||||
.busca .lupa{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--fraco);font-size:13px}
|
||||
.acts{margin-left:auto;display:flex;gap:18px;align-items:center;font-size:12.5px}
|
||||
.acts a{color:var(--tx);text-decoration:none;display:flex;align-items:center;gap:6px}
|
||||
.acts a:hover{color:var(--laranja)}
|
||||
|
||||
/* menu */
|
||||
.menu{background:#fff;border-bottom:1px solid var(--linha)}
|
||||
.menu .w{display:flex;gap:0;flex-wrap:wrap}
|
||||
.mi{position:relative}
|
||||
.mi>a{display:block;padding:13px 16px;font-family:"Inter",sans-serif;font-weight:600;
|
||||
font-size:12.5px;color:var(--tx);text-decoration:none;white-space:nowrap}
|
||||
.mi:hover>a{color:var(--laranja)}
|
||||
.mi.on>a{color:var(--laranja);box-shadow:inset 0 -2px 0 var(--laranja)}
|
||||
.sub{display:none;position:absolute;top:100%;left:0;background:#fff;border:1px solid var(--linha);
|
||||
border-radius:0 0 9px 9px;min-width:224px;padding:7px 0;z-index:70;
|
||||
box-shadow:0 10px 26px rgba(0,0,0,.09)}
|
||||
.mi:hover .sub,.mi:focus-within .sub{display:block}
|
||||
.sub a{display:block;padding:8px 17px;font-size:12.5px;color:var(--fraco);text-decoration:none}
|
||||
.sub a:hover{color:var(--laranja);background:var(--cinza)}
|
||||
@media(max-width:820px){
|
||||
.menu .w{overflow-x:auto;flex-wrap:nowrap}
|
||||
.sub{display:none!important}
|
||||
.busca{display:none}
|
||||
}
|
||||
|
||||
.hero{background:#000;color:#fff;position:relative;overflow:hidden}
|
||||
.hero .fundo{position:absolute;inset:0;pointer-events:none}
|
||||
@@ -93,13 +28,13 @@ section+section{border-top:1px solid var(--linha)}
|
||||
@media(max-width:940px){.hgrid{grid-template-columns:1fr;gap:22px}}
|
||||
h1{font-size:clamp(24px,3.6vw,38px);font-weight:700;max-width:20ch;line-height:1.08}
|
||||
h1 em{font-style:normal;color:var(--laranja)}
|
||||
.hero .sub2{color:#B7C1D2;font-size:14px;max-width:56ch;margin-top:10px}
|
||||
.hero .sub2{color:#B7C1D2;font-size:16px;max-width:56ch;margin-top:10px}
|
||||
.hero .sub2 b{color:#fff}
|
||||
.hcta{display:flex;gap:12px;margin-top:16px;flex-wrap:wrap;align-items:center}
|
||||
.btn1{background:var(--laranja);color:#241703;border:0;border-radius:9px;padding:12px 24px;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:14.5px;cursor:pointer;text-decoration:none}
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:16px;cursor:pointer;text-decoration:none}
|
||||
.btn1:hover{background:var(--laranja2)}
|
||||
.mini{font-size:12px;color:#8D9AB0}
|
||||
.mini{font-size:14.5px;color:#8D9AB0}
|
||||
|
||||
/* preços lado a lado, em faixa */
|
||||
.pfaixa{display:flex;gap:10px;flex-wrap:wrap}
|
||||
@@ -110,28 +45,28 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.pcard:hover{background:rgba(255,168,26,.12);border-color:rgba(255,168,26,.45)}
|
||||
.pcard.dest{border-color:rgba(255,168,26,.5);background:rgba(255,168,26,.09)}
|
||||
.pcard .marc{position:absolute;top:-8px;left:13px;background:var(--verde);color:#fff;
|
||||
font-family:"Inter",sans-serif;font-size:8.5px;font-weight:700;letter-spacing:.09em;
|
||||
font-family:"Inter",sans-serif;font-size:11px;font-weight:700;letter-spacing:.09em;
|
||||
text-transform:uppercase;padding:2px 8px;border-radius:4px;white-space:nowrap}
|
||||
.pcard .apartir{display:block;font-size:9.5px;color:var(--verde-claro);letter-spacing:.09em;
|
||||
.pcard .apartir{display:block;font-size:12px;color:var(--verde-claro);letter-spacing:.09em;
|
||||
text-transform:uppercase;font-family:"Inter",sans-serif;font-weight:700;margin-bottom:2px}
|
||||
.pcard .vl{font-family:"Inter",sans-serif;font-size:20px;font-weight:700;color:var(--laranja);
|
||||
line-height:1;display:block;margin-bottom:3px}
|
||||
.pcard .vl small{font-size:11px;font-weight:600;color:#8D9AB0}
|
||||
.pcard .nm{font-family:"Inter",sans-serif;font-size:12.5px;font-weight:700;color:#fff;display:block}
|
||||
.pcard .dsc{font-size:10.5px;color:#8D9AB0;display:block;margin-top:1px}
|
||||
.pcard .vl small{font-size:13.5px;font-weight:600;color:#8D9AB0}
|
||||
.pcard .nm{font-family:"Inter",sans-serif;font-size:15px;font-weight:700;color:#fff;display:block}
|
||||
.pcard .dsc{font-size:13px;color:#8D9AB0;display:block;margin-top:1px}
|
||||
|
||||
/* selos e barra */
|
||||
.selos1{display:flex;gap:7px;flex-wrap:wrap;align-items:center;margin-bottom:11px}
|
||||
.sel{font-family:"Inter",sans-serif;font-size:9.5px;font-weight:700;letter-spacing:.09em;
|
||||
.sel{font-family:"Inter",sans-serif;font-size:12px;font-weight:700;letter-spacing:.09em;
|
||||
text-transform:uppercase;padding:3px 9px;border-radius:5px}
|
||||
.sel.top{background:var(--laranja);color:#241703}
|
||||
.sel.novo{background:rgba(46,158,91,.18);color:#7FD4A0;border:1px solid rgba(46,158,91,.4)}
|
||||
.sel.pos{background:rgba(255,255,255,.09);color:#B7C1D2;border:1px solid rgba(255,255,255,.16)}
|
||||
.barrah{border-top:1px solid rgba(255,255,255,.1);padding:11px 0 14px;
|
||||
display:flex;gap:24px;flex-wrap:wrap;font-size:12px;color:#B7C1D2}
|
||||
display:flex;gap:24px;flex-wrap:wrap;font-size:14.5px;color:#B7C1D2}
|
||||
.barrah span{display:flex;align-items:center;gap:7px}
|
||||
.barrah b{color:#fff;font-family:"Inter",sans-serif}
|
||||
.barrah i{color:var(--laranja);font-style:normal;font-size:9px}
|
||||
.barrah i{color:var(--laranja);font-style:normal;font-size:11.5px}
|
||||
|
||||
|
||||
/* aviso do topo do envio */
|
||||
@@ -170,7 +105,7 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.ec .top,.ec .in{position:relative;z-index:1}
|
||||
.ec:hover{border-color:var(--laranja);box-shadow:var(--sombra-alta);transform:translateY(-2px)}
|
||||
.ec .marc{display:block;background:var(--verde-ml);color:#fff;text-align:center;
|
||||
font-family:"Inter",sans-serif;font-size:9px;font-weight:700;letter-spacing:.14em;
|
||||
font-family:"Inter",sans-serif;font-size:11.5px;font-weight:700;letter-spacing:.14em;
|
||||
text-transform:uppercase;padding:5px 8px;position:relative;z-index:2}
|
||||
.ec.f{border-color:var(--verde-ml)}
|
||||
.ec.f:hover{border-color:var(--verde-ml)}
|
||||
@@ -187,25 +122,25 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
</g></svg>")}
|
||||
.ec .top{padding:17px 20px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
|
||||
.ec.f .top{background:#FFF4E3} .ec.a .top{background:var(--cinza)} .ec.u .top{background:#EEF4FB}
|
||||
.ec .top h3{font-family:"Inter",sans-serif;font-size:17px;font-weight:700;color:var(--tx)}
|
||||
.ec .top h3{font-family:"Inter",sans-serif;font-size:18px;font-weight:700;color:var(--tx)}
|
||||
|
||||
|
||||
.ec .top .p{font-family:"Inter",sans-serif;font-weight:700;font-size:15px;white-space:nowrap;
|
||||
.ec .top .p{font-family:"Inter",sans-serif;font-weight:700;font-size:16.5px;white-space:nowrap;
|
||||
color:var(--tx);text-align:right;line-height:1.15}
|
||||
.ec .top .p i{display:block;font-style:normal;font-family:"Roboto",sans-serif;font-size:10px;
|
||||
.ec .top .p i{display:block;font-style:normal;font-family:"Roboto",sans-serif;font-size:12.5px;
|
||||
font-weight:500;color:var(--fraco);margin-bottom:1px;letter-spacing:0;
|
||||
text-transform:uppercase;letter-spacing:.1em}
|
||||
.chama2{background:#F1FBF5;border-top:1px solid #BFE6CE;border-bottom:1px solid #BFE6CE;
|
||||
padding:9px 20px;font-size:11.5px;color:#1F5C3A;line-height:1.45;position:relative;z-index:1}
|
||||
.chama2 b{font-family:"Inter",sans-serif;font-weight:700;color:var(--verde-ml);font-size:12.5px}
|
||||
.ec .top .p small{font-size:11px;font-weight:600;color:var(--fraco)}
|
||||
padding:9px 20px;font-size:14px;color:#1F5C3A;line-height:1.45;position:relative;z-index:1}
|
||||
.chama2 b{font-family:"Inter",sans-serif;font-weight:700;color:var(--verde-ml);font-size:15px}
|
||||
.ec .top .p small{font-size:13.5px;font-weight:600;color:var(--fraco)}
|
||||
.ec .in{padding:18px 20px 20px}
|
||||
.ec .in p{font-size:13px;color:var(--fraco);margin-bottom:13px}
|
||||
.ec ul{list-style:none;font-size:12.5px;margin-bottom:15px}
|
||||
.ec .in p{font-size:15px;color:var(--fraco);margin-bottom:13px}
|
||||
.ec ul{list-style:none;font-size:15px;margin-bottom:15px}
|
||||
.ec li{padding:4px 0 4px 19px;position:relative;color:var(--fraco)}
|
||||
.ec li::before{content:"✓";position:absolute;left:2px;color:var(--verde);font-size:11px}
|
||||
.ec .go{font-family:"Inter",sans-serif;font-weight:700;font-size:13.5px;color:var(--laranja);display:block}
|
||||
.ec .lg{display:block;font-size:10.5px;color:var(--fraco);letter-spacing:.1em;
|
||||
.ec li::before{content:"✓";position:absolute;left:2px;color:var(--verde);font-size:13.5px}
|
||||
.ec .go{font-family:"Inter",sans-serif;font-weight:700;font-size:15.5px;color:var(--laranja);display:block}
|
||||
.ec .lg{display:block;font-size:13px;color:var(--fraco);letter-spacing:.1em;
|
||||
text-transform:uppercase;font-family:"Inter",sans-serif;font-weight:700;margin-bottom:5px}
|
||||
|
||||
/* painel focado */
|
||||
@@ -216,9 +151,9 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
@media(max-width:820px){.focoGrid{grid-template-columns:1fr}}
|
||||
.voltar{display:inline-flex;align-items:center;gap:7px;background:#fff;
|
||||
border:1px solid var(--linha);border-radius:8px;padding:8px 14px 8px 11px;
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:12.5px;color:var(--tx);
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:15px;color:var(--tx);
|
||||
cursor:pointer;margin-bottom:14px;box-shadow:var(--sombra);transition:var(--transition)}
|
||||
.voltar span{font-size:15px;line-height:1;color:var(--fraco)}
|
||||
.voltar span{font-size:16.5px;line-height:1;color:var(--fraco)}
|
||||
.voltar:hover{border-color:var(--laranja);color:var(--laranja)}
|
||||
.voltar:hover span{color:var(--laranja)}
|
||||
.cat{border:1px solid var(--linha);border-radius:14px;background:#fff;overflow:hidden;
|
||||
@@ -228,9 +163,9 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
flex-wrap:wrap;border-bottom:1px solid var(--linha);background:var(--cinza);padding:18px 22px}
|
||||
.cat.file .cabfoco{background:#FFF4E3}
|
||||
.cabfoco h3{font-family:"Inter",sans-serif;font-size:20px;font-weight:700}
|
||||
.cabfoco .pr2{font-family:"Inter",sans-serif;font-weight:700;font-size:17px;white-space:nowrap}
|
||||
.cabfoco p{font-size:12.5px;color:var(--fraco);width:100%;margin-top:4px}
|
||||
.cabfoco .lg{font-size:10.5px;color:var(--fraco);letter-spacing:.1em;text-transform:uppercase;
|
||||
.cabfoco .pr2{font-family:"Inter",sans-serif;font-weight:700;font-size:18px;white-space:nowrap}
|
||||
.cabfoco p{font-size:15px;color:var(--fraco);width:100%;margin-top:4px}
|
||||
.cabfoco .lg{font-size:13px;color:var(--fraco);letter-spacing:.1em;text-transform:uppercase;
|
||||
font-family:"Inter",sans-serif;font-weight:700}
|
||||
|
||||
/* upload */
|
||||
@@ -248,15 +183,15 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.cam.on .mk2{border-color:var(--verde-ml);
|
||||
background:radial-gradient(circle,var(--verde-ml) 0 3px,#fff 4px)}
|
||||
.cam div{flex:1;min-width:0;overflow-wrap:anywhere}
|
||||
.cam b{display:block;font-family:"Inter",sans-serif;font-size:12.5px;margin-bottom:2px}
|
||||
.cam span{display:block;font-size:10.5px;color:var(--fraco);line-height:1.45}
|
||||
.cam b{display:block;font-family:"Inter",sans-serif;font-size:15px;margin-bottom:2px}
|
||||
.cam span{display:block;font-size:13px;color:var(--fraco);line-height:1.45}
|
||||
.cam em{display:block;margin-top:6px;font-style:normal;font-family:"Inter",sans-serif;
|
||||
font-weight:700;font-size:12px;color:var(--verde-ml);line-height:1.3}
|
||||
font-weight:700;font-size:14.5px;color:var(--verde-ml);line-height:1.3}
|
||||
.trocouCam{margin-top:-4px;margin-bottom:11px;padding:10px 12px;border-radius:9px;
|
||||
background:#FFF6E8;border:1px solid #F0D9AE;font-size:11.5px;color:#7A5A16;line-height:1.5}
|
||||
background:#FFF6E8;border:1px solid #F0D9AE;font-size:14px;color:#7A5A16;line-height:1.5}
|
||||
.trocouCam b{color:#B07A18;font-family:"Inter",sans-serif;font-weight:700}
|
||||
.trocarModo{margin:-4px 0 11px;padding:10px 12px;border-radius:9px;background:#F1FBF5;
|
||||
border:1px solid #BFE6CE;font-size:11.5px;color:#1F5C3A;line-height:1.5}
|
||||
border:1px solid #BFE6CE;font-size:14px;color:#1F5C3A;line-height:1.5}
|
||||
.trocarModo b{font-family:"Inter",sans-serif;font-weight:700}
|
||||
.trocarModo label{display:block;margin-top:7px;cursor:pointer}
|
||||
.zona{border:2px dashed var(--laranja);border-radius:13px;padding:44px 20px;text-align:center;
|
||||
@@ -267,50 +202,50 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
background:var(--laranja);color:#fff;font-size:22px;line-height:46px;font-weight:700;
|
||||
box-shadow:0 0 0 7px rgba(255,168,26,.18)}
|
||||
.zona:hover .ico,.zona.sobre .ico{background:var(--laranja2)}
|
||||
.zona b{display:block;font-family:"Inter",sans-serif;font-size:18px;font-weight:700;
|
||||
.zona b{display:block;font-family:"Inter",sans-serif;font-size:19px;font-weight:700;
|
||||
margin-bottom:5px;color:var(--tx);line-height:1.2}
|
||||
.zona span{font-size:12px;color:#8A6A2E;display:block;max-width:30ch;margin:0 auto;line-height:1.5}
|
||||
.zona span{font-size:14.5px;color:#8A6A2E;display:block;max-width:30ch;margin:0 auto;line-height:1.5}
|
||||
.zona .bt{display:inline-block;margin-top:13px;background:var(--laranja);color:#241703;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:13px;border-radius:8px;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:15px;border-radius:8px;
|
||||
padding:9px 18px}
|
||||
.zona:hover .bt{background:var(--laranja2)}
|
||||
.recusa{margin-top:11px;padding:11px 13px;border-radius:9px;background:#FFF3F0;
|
||||
border:1px solid #F0C4B8;font-size:11.5px;color:#8C3B22;line-height:1.5}
|
||||
border:1px solid #F0C4B8;font-size:14px;color:#8C3B22;line-height:1.5}
|
||||
.recusa b{font-family:"Inter",sans-serif;font-weight:700;color:#8C3B22}
|
||||
.aoVivo{margin-top:11px;padding:11px 13px;border-radius:9px;background:var(--cinza);
|
||||
border:1px solid var(--linha);font-size:12px;color:var(--fraco);line-height:1.45;
|
||||
border:1px solid var(--linha);font-size:14.5px;color:var(--fraco);line-height:1.45;
|
||||
display:flex;align-items:center;gap:9px}
|
||||
.aoVivo b{font-family:"Inter",sans-serif;font-weight:700;color:var(--tx);font-size:12.5px}
|
||||
.aoVivo b{font-family:"Inter",sans-serif;font-weight:700;color:var(--tx);font-size:15px}
|
||||
.aoVivo .pt2{flex:none;width:8px;height:8px;border-radius:50%;background:var(--gray-2)}
|
||||
.aoVivo.pronto{background:#F1FBF5;border-color:#BFE6CE;color:#2A6B45}
|
||||
.aoVivo.pronto .pt2{background:var(--verde-ml)}
|
||||
.aoVivo.pronto b{color:#1F5C3A}
|
||||
.b2{width:100%;padding:13px;border-radius:9px;border:1px solid var(--marinho);background:#fff;
|
||||
color:var(--tx);font-family:"Inter",sans-serif;font-weight:600;font-size:14.5px;cursor:pointer}
|
||||
color:var(--tx);font-family:"Inter",sans-serif;font-weight:600;font-size:16px;cursor:pointer}
|
||||
.cat.file .b2{background:var(--laranja);border-color:var(--laranja);color:#241703}
|
||||
.b2:hover{background:#111;color:#fff}
|
||||
.cat.file .b2:hover{background:var(--laranja2);color:#241703}
|
||||
.it{display:flex;justify-content:space-between;gap:9px;padding:10px 13px;background:var(--cinza);
|
||||
border-radius:8px;margin-bottom:7px;align-items:center;font-size:12.5px}
|
||||
border-radius:8px;margin-bottom:7px;align-items:center;font-size:15px}
|
||||
.it b{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.it span{color:var(--fraco);font-size:11px;flex:none}
|
||||
.it button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:15px}
|
||||
.it span{color:var(--fraco);font-size:13.5px;flex:none}
|
||||
.it button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:16.5px}
|
||||
|
||||
/* repetição do FILE */
|
||||
.rep{background:var(--cinza);border-radius:9px;padding:12px 14px;margin-top:8px}
|
||||
.rep .l1{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:9px}
|
||||
.rep .l1 b{font-weight:500;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rep .l1 button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:15px;flex:none}
|
||||
.rep .l1 b{font-weight:500;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rep .l1 button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:16.5px;flex:none}
|
||||
.prog{height:5px;border-radius:4px;background:#E4E7EC;overflow:hidden;margin:9px 0 5px}
|
||||
.prog i{display:block;height:100%;background:var(--laranja);border-radius:4px;
|
||||
transition:width .35s ease}
|
||||
.progT{display:block;font-size:10.5px;color:var(--fraco)}
|
||||
.progT{display:block;font-size:13px;color:var(--fraco)}
|
||||
.progF{margin:9px 0 4px;padding:9px 11px;border-radius:7px;background:#FFF6E8;
|
||||
border:1px solid #F0D9AE;font-size:11px;color:#7A5A16;line-height:1.5}
|
||||
border:1px solid #F0D9AE;font-size:13.5px;color:#7A5A16;line-height:1.5}
|
||||
.progF b{font-family:"Inter",sans-serif;font-weight:700;color:#B07A18}
|
||||
.rep label{display:block;font-size:10px;color:var(--fraco);margin-bottom:3px}
|
||||
.rep label{display:block;font-size:12.5px;color:var(--fraco);margin-bottom:3px}
|
||||
.rep input{width:100%;padding:7px 9px;border:1px solid var(--linha);border-radius:6px;
|
||||
font-family:inherit;font-size:13px;background:#fff;color:var(--tx)}
|
||||
font-family:inherit;font-size:15px;background:#fff;color:var(--tx)}
|
||||
.rep input:focus{outline:none;border-color:var(--laranja)}
|
||||
|
||||
/* fila de artes ao lado da montagem */
|
||||
@@ -323,16 +258,16 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.mont{position:sticky;top:104px;border:1px solid var(--linha);border-radius:11px;
|
||||
background:var(--cinza);padding:12px;box-shadow:var(--sombra)}
|
||||
.montcab{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:9px}
|
||||
.montcab b{font-family:"Inter",sans-serif;font-size:12.5px;font-weight:700}
|
||||
.montcab b{font-family:"Inter",sans-serif;font-size:15px;font-weight:700}
|
||||
.zoomB{display:flex;align-items:center;gap:4px;margin-left:auto}
|
||||
.zoomB button{width:22px;height:22px;border-radius:6px;border:1px solid var(--linha);
|
||||
background:#fff;color:var(--tx);cursor:pointer;font-size:14px;line-height:1;padding:0}
|
||||
background:#fff;color:var(--tx);cursor:pointer;font-size:16px;line-height:1;padding:0}
|
||||
.zoomB button:hover{border-color:var(--laranja);color:var(--laranja)}
|
||||
.zoomB button:disabled{opacity:.35;cursor:not-allowed}
|
||||
.zoomB span{font-family:"Inter",sans-serif;font-size:10.5px;font-weight:700;
|
||||
.zoomB span{font-family:"Inter",sans-serif;font-size:13px;font-weight:700;
|
||||
color:var(--fraco);min-width:34px;text-align:center}
|
||||
#vArea.ampliado{overflow:auto}
|
||||
.montcab span{font-family:"Inter",sans-serif;font-weight:700;font-size:16px;color:var(--tx)}
|
||||
.montcab span{font-family:"Inter",sans-serif;font-weight:700;font-size:17.5px;color:var(--tx)}
|
||||
#vArea{max-height:min(56vh,560px);min-height:200px;overflow-y:auto;overscroll-behavior:contain;
|
||||
border:1px solid var(--linha);border-radius:8px;background:#fff}
|
||||
#vArea::-webkit-scrollbar{width:8px}
|
||||
@@ -344,83 +279,83 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
#vArea .folhaPrevias{min-height:200px;padding:10px;display:grid;gap:12px;background:#F8F9FA}
|
||||
#vArea .folhaPrevia{margin:0;background:#fff;border:1px solid var(--linha);border-radius:6px;overflow:hidden}
|
||||
#vArea .folhaPrevia img{width:100%;height:auto;max-height:520px;object-fit:contain;display:block;background:#fff}
|
||||
#vArea .folhaPrevia figcaption{padding:7px 9px;font-size:10.5px;color:var(--fraco);border-top:1px solid var(--linha)}
|
||||
#vArea .folhaPrevia figcaption{padding:7px 9px;font-size:13px;color:var(--fraco);border-top:1px solid var(--linha)}
|
||||
#vArea .semmont{border:0;min-height:200px;display:flex;flex-direction:column;
|
||||
align-items:center;justify-content:center}
|
||||
#vUso:empty{display:none}
|
||||
.semmont{border:1px dashed var(--linha);border-radius:8px;background:#fff;
|
||||
padding:30px 14px;text-align:center}
|
||||
.semmont b{display:block;font-family:"Inter",sans-serif;font-size:13px;margin-bottom:4px}
|
||||
.semmont span{font-size:11.5px;color:var(--fraco);line-height:1.4;display:block}
|
||||
.montpe{font-size:10.5px;color:var(--fraco);margin-top:9px;line-height:1.45}
|
||||
.semmont b{display:block;font-family:"Inter",sans-serif;font-size:15px;margin-bottom:4px}
|
||||
.semmont span{font-size:14px;color:var(--fraco);line-height:1.4;display:block}
|
||||
.montpe{font-size:13px;color:var(--fraco);margin-top:9px;line-height:1.45}
|
||||
|
||||
/* caixa de requisitos · o que a máquina aceita */
|
||||
.req{position:sticky;top:104px;background:#fff;border:1px solid var(--linha);
|
||||
border-radius:14px;box-shadow:var(--sombra);padding:16px 16px 6px}
|
||||
@media(max-width:820px){.req{position:static}}
|
||||
.reqcab{font-family:"Inter",sans-serif;font-size:18px;font-weight:700;line-height:1.15;
|
||||
.reqcab{font-family:"Inter",sans-serif;font-size:19px;font-weight:700;line-height:1.15;
|
||||
letter-spacing:normal;color:var(--tx);margin-bottom:12px}
|
||||
.req details{border-top:1px solid var(--linha)}
|
||||
.req details:first-of-type{border-top:0}
|
||||
.req summary{cursor:pointer;list-style:none;padding:9px 0;font-family:"Inter",sans-serif;
|
||||
font-size:13px;font-weight:700;color:var(--tx);display:flex;align-items:center;gap:8px}
|
||||
font-size:15px;font-weight:700;color:var(--tx);display:flex;align-items:center;gap:8px}
|
||||
.req summary::-webkit-details-marker{display:none}
|
||||
.req summary::before{content:"+";flex:none;width:16px;height:16px;border-radius:50%;
|
||||
border:1px solid rgba(0,166,80,.45);background:#fff;color:var(--verde-ml);font-size:11px;
|
||||
border:1px solid rgba(0,166,80,.45);background:#fff;color:var(--verde-ml);font-size:13.5px;
|
||||
line-height:14px;text-align:center;font-weight:700}
|
||||
.req summary:hover::before{background:#F1FBF5}
|
||||
.req details[open] summary::before{content:"−";background:var(--verde-ml);
|
||||
border-color:var(--verde-ml);color:#fff}
|
||||
.req ul{list-style:none;padding:0 0 9px;margin:0}
|
||||
.req li{position:relative;padding:4px 0 4px 17px;font-size:11.5px;color:var(--fraco);
|
||||
.req li{position:relative;padding:4px 0 4px 17px;font-size:14px;color:var(--fraco);
|
||||
line-height:1.5}
|
||||
.req li b{color:var(--tx);font-weight:700;font-family:"Inter",sans-serif}
|
||||
.req li::before{position:absolute;left:0;top:4px;font-size:11px;font-weight:700}
|
||||
.req li::before{position:absolute;left:0;top:4px;font-size:13.5px;font-weight:700}
|
||||
.req li.s::before{content:"✓";color:var(--verde-ml)}
|
||||
.req li.a::before{content:"!";color:var(--laranja)}
|
||||
.req li.n::before{content:"×";color:var(--vermelho)}
|
||||
.req .obs2{font-size:10.5px;color:var(--fraco);padding:0 0 9px;line-height:1.45;
|
||||
.req .obs2{font-size:13px;color:var(--fraco);padding:0 0 9px;line-height:1.45;
|
||||
font-style:italic}
|
||||
.mont .usoBom,.mont .usoRuim{margin-top:8px;font-size:11px;line-height:1.4}
|
||||
.mont .usoBom,.mont .usoRuim{margin-top:8px;font-size:13.5px;line-height:1.4}
|
||||
|
||||
/* artes avulsas */
|
||||
.art{background:var(--cinza);border-radius:9px;padding:11px 12px;margin-bottom:7px;
|
||||
transition:box-shadow .3s,background .3s}
|
||||
.art.nova{background:#FFF6E8;box-shadow:inset 0 0 0 1px rgba(255,168,26,.55)}
|
||||
.art.nova .l1 b::after{content:"nova";font-family:"Inter",sans-serif;font-size:9px;
|
||||
.art.nova .l1 b::after{content:"nova";font-family:"Inter",sans-serif;font-size:11.5px;
|
||||
font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#B07A18;
|
||||
background:#FFE9C4;border-radius:3px;padding:1px 5px;margin-left:7px;vertical-align:1px}
|
||||
.art .l1{display:flex;justify-content:space-between;gap:8px;align-items:center;margin-bottom:8px}
|
||||
.art .l1 b{font-weight:500;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.art .l1 button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:15px;flex:none}
|
||||
.art .l1 b{font-weight:500;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.art .l1 button{border:0;background:none;color:var(--fraco);cursor:pointer;font-size:16.5px;flex:none}
|
||||
.art .cps{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.atalhos{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-top:9px}
|
||||
.atalhos .tm,.atalhos .gr{border:1px solid var(--linha);background:#fff;border-radius:6px;
|
||||
padding:5px 9px;font-family:"Inter",sans-serif;font-size:11px;font-weight:600;
|
||||
padding:5px 9px;font-family:"Inter",sans-serif;font-size:13.5px;font-weight:600;
|
||||
color:var(--tx);cursor:pointer;transition:var(--transition)}
|
||||
.atalhos .tm:hover,.atalhos .gr:hover{border-color:var(--laranja);color:var(--laranja)}
|
||||
.atalhos .tm.on{background:var(--verde-ml);border-color:var(--verde-ml);color:#fff}
|
||||
.atalhos .gr{font-size:14px;line-height:1;padding:4px 9px}
|
||||
.atalhos .gr{font-size:16px;line-height:1;padding:4px 9px}
|
||||
.atalhos .gr.on{background:var(--laranja);border-color:var(--laranja);color:#241703}
|
||||
.atalhos .sep{flex:1;min-width:4px}
|
||||
.art label{display:block;font-size:10px;color:var(--fraco);margin-bottom:3px}
|
||||
.art label{display:block;font-size:12.5px;color:var(--fraco);margin-bottom:3px}
|
||||
.art input{width:100%;padding:7px 9px;border:1px solid var(--linha);border-radius:6px;
|
||||
font-family:inherit;font-size:13px;background:#fff;color:var(--tx)}
|
||||
font-family:inherit;font-size:15px;background:#fff;color:var(--tx)}
|
||||
.art input:focus{outline:none;border-color:var(--laranja)}
|
||||
.qb2{height:7px;border-radius:5px;background:#E4E7EC;overflow:hidden;margin-top:9px}
|
||||
.qb2 i{display:block;height:100%;border-radius:5px}
|
||||
.qb2 i.ok{background:var(--verde)}.qb2 i.av{background:var(--laranja)}.qb2 i.er{background:var(--vermelho)}
|
||||
.calc{font-size:10.5px;margin-top:6px;display:flex;justify-content:space-between;gap:8px;color:var(--fraco)}
|
||||
.calc{font-size:13px;margin-top:6px;display:flex;justify-content:space-between;gap:8px;color:var(--fraco)}
|
||||
.calc .ok{color:var(--verde)}.calc .av{color:var(--laranja)}.calc .er{color:var(--vermelho)}
|
||||
.dica{margin-top:9px;background:#FFF8EE;border:1px solid #F0D9AE;border-radius:8px;
|
||||
padding:10px 12px;font-size:11.5px;color:#7A5A16;line-height:1.45}
|
||||
.dica b{display:block;font-family:"Inter",sans-serif;color:#B07A18;font-size:12px;margin-bottom:3px}
|
||||
padding:10px 12px;font-size:14px;color:#7A5A16;line-height:1.45}
|
||||
.dica b{display:block;font-family:"Inter",sans-serif;color:#B07A18;font-size:14.5px;margin-bottom:3px}
|
||||
.dica button{display:block;width:100%;margin-top:10px;padding:10px 14px;border-radius:7px;
|
||||
border:1px solid var(--verde-ml);background:var(--verde-ml);color:#fff;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:13px;letter-spacing:.01em;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:15px;letter-spacing:.01em;
|
||||
cursor:pointer;box-shadow:0 2px 8px rgba(0,166,80,.28);transition:var(--transition)}
|
||||
.dica button:hover{background:#00934A;border-color:#00934A;box-shadow:0 4px 14px rgba(0,166,80,.38)}
|
||||
.dicaok{margin-top:8px;font-size:11px;color:var(--verde)}
|
||||
.dicaok{margin-top:8px;font-size:13.5px;color:var(--verde)}
|
||||
|
||||
/* cota em escala da peça dentro da largura do filme */
|
||||
.viz{margin-top:10px;max-width:100%}
|
||||
@@ -428,27 +363,27 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.vizL i{flex:1;height:1px;background:var(--gray-2);position:relative}
|
||||
.vizL i::before{content:"";position:absolute;top:-3px;width:1px;height:7px;background:var(--gray-2)}
|
||||
.vizL i:first-child::before{left:0} .vizL i:last-child::before{right:0}
|
||||
.vizL b{font-family:"Inter",sans-serif;font-size:9.5px;font-weight:700;letter-spacing:.08em;
|
||||
.vizL b{font-family:"Inter",sans-serif;font-size:12px;font-weight:700;letter-spacing:.08em;
|
||||
text-transform:uppercase;color:var(--fraco)}
|
||||
.vizRow{display:flex;gap:7px;align-items:flex-end}
|
||||
.vizTira{flex:none;max-width:100%}
|
||||
.vizH{flex:none;display:flex;align-items:center;justify-content:center;padding-left:4px;
|
||||
border-left:1px solid var(--gray-2);min-height:22px}
|
||||
.vizH span{writing-mode:vertical-rl;font-family:"Inter",sans-serif;font-size:9.5px;
|
||||
.vizH span{writing-mode:vertical-rl;font-family:"Inter",sans-serif;font-size:12px;
|
||||
font-weight:700;color:var(--fraco);white-space:nowrap}
|
||||
.tira{display:flex;background:#fff;border:1px solid var(--linha);border-radius:4px;
|
||||
padding:2px;overflow:hidden;min-height:22px;width:100%}
|
||||
.pc{flex:none;overflow:hidden;display:flex;align-items:center;justify-content:center;
|
||||
background:#F2F2F2;border-radius:2px}
|
||||
.pc img{width:100%;height:100%;object-fit:contain;display:block}
|
||||
.pc span{font-family:"Inter",sans-serif;font-size:9px;font-weight:700;color:var(--fraco)}
|
||||
.pc span{font-family:"Inter",sans-serif;font-size:11.5px;font-weight:700;color:var(--fraco)}
|
||||
.folga{flex:1;min-width:0;display:flex;align-items:center;justify-content:center;border-radius:2px;
|
||||
background:repeating-linear-gradient(45deg,#FBFBFB,#FBFBFB 4px,#F0F0F0 4px,#F0F0F0 8px)}
|
||||
.folga span{font-size:8.5px;color:var(--fraco);white-space:nowrap;overflow:hidden}
|
||||
.vizPe{font-size:10.5px;color:var(--fraco);margin-top:6px;line-height:1.4}
|
||||
.folga span{font-size:11px;color:var(--fraco);white-space:nowrap;overflow:hidden}
|
||||
.vizPe{font-size:13px;color:var(--fraco);margin-top:6px;line-height:1.4}
|
||||
.vizPe b{color:var(--tx);font-family:"Inter",sans-serif;font-weight:700}
|
||||
.resumoA{background:#fff;border:1px solid var(--linha);border-radius:9px;padding:11px 13px;
|
||||
font-size:12px;margin-bottom:12px;display:none}
|
||||
font-size:14.5px;margin-bottom:12px;display:none}
|
||||
.resumoA.on{display:block}
|
||||
.resumoA .r{display:flex;justify-content:space-between;padding:2px 0}
|
||||
.resumoA .r b{font-family:"Inter",sans-serif}
|
||||
@@ -457,18 +392,18 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.qual{background:#fff;border:1px solid var(--linha);border-radius:13px;padding:22px;margin-top:16px;display:none}
|
||||
.qual.on{display:block}
|
||||
.qh{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.qh h3{font-family:"Inter",sans-serif;font-size:17px;font-weight:700}
|
||||
.qh h3{font-family:"Inter",sans-serif;font-size:18px;font-weight:700}
|
||||
.qh .pct{font-family:"Inter",sans-serif;font-weight:700;font-size:38px;line-height:1;color:var(--verde)}
|
||||
.qh .pct small{font-size:14px;color:var(--fraco)}
|
||||
.qh .pct small{font-size:16px;color:var(--fraco)}
|
||||
.qbar{height:13px;border-radius:8px;background:var(--cinza);overflow:hidden}
|
||||
.qbar i{display:block;height:100%;border-radius:8px;background:var(--verde)}
|
||||
.qitens{display:grid;grid-template-columns:repeat(auto-fit,minmax(178px,1fr));gap:7px;margin-top:13px}
|
||||
.qi{display:flex;gap:8px;align-items:center;border:1px solid var(--linha);border-radius:8px;
|
||||
padding:7px 10px;font-size:11.5px}
|
||||
.qi .c{width:15px;height:15px;border-radius:50%;display:grid;place-items:center;font-size:8.5px;
|
||||
padding:7px 10px;font-size:14px}
|
||||
.qi .c{width:15px;height:15px;border-radius:50%;display:grid;place-items:center;font-size:11px;
|
||||
flex:none;color:#fff;font-family:"Inter",sans-serif;font-weight:700}
|
||||
.qi b{display:block;font-family:"Inter",sans-serif;font-size:11.5px}
|
||||
.qi span{color:var(--fraco);font-size:10.5px}
|
||||
.qi b{display:block;font-family:"Inter",sans-serif;font-size:14px}
|
||||
.qi span{color:var(--fraco);font-size:13px}
|
||||
.qi.ok .c{background:var(--verde)}.qi.av .c{background:var(--laranja);color:#241703}
|
||||
.qi.er .c{background:var(--vermelho)}
|
||||
.qi.fix{border-color:#BBD9E8;background:#F2F9FC}
|
||||
@@ -480,20 +415,20 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.ciente.recusa h4{color:#8C3B22}
|
||||
.ciente.recusa label{display:none}
|
||||
.ciente.recusa .lista{color:#8C3B22}
|
||||
.ciente h4{font-family:"Inter",sans-serif;font-size:14px;font-weight:700;color:#B07A18;margin-bottom:5px}
|
||||
.ciente p{font-size:12.5px;color:#7A5A16;line-height:1.5}
|
||||
.ciente .lista{margin:10px 0 0;font-size:12px;color:#7A5A16}
|
||||
.ciente h4{font-family:"Inter",sans-serif;font-size:16px;font-weight:700;color:#B07A18;margin-bottom:5px}
|
||||
.ciente p{font-size:15px;color:#7A5A16;line-height:1.5}
|
||||
.ciente .lista{margin:10px 0 0;font-size:14.5px;color:#7A5A16}
|
||||
.ciente .lista div{padding:3px 0 3px 15px;position:relative}
|
||||
.ciente .lista div::before{content:"·";position:absolute;left:4px;font-weight:700}
|
||||
.ciente label{display:flex;gap:9px;align-items:flex-start;margin-top:12px;padding-top:11px;
|
||||
border-top:1px solid #F0D9AE;cursor:pointer;font-size:12.5px;color:#5E4512}
|
||||
border-top:1px solid #F0D9AE;cursor:pointer;font-size:15px;color:#5E4512}
|
||||
.ciente input{margin-top:2px;width:16px;height:16px;flex:none;accent-color:#B07A18}
|
||||
@keyframes pulsa{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(217,74,40,.55)}
|
||||
50%{transform:scale(1.04);box-shadow:0 0 0 12px rgba(217,74,40,0)}}
|
||||
.qbts{display:flex;gap:9px;flex-wrap:wrap;margin-top:16px}
|
||||
.qbts .qb{flex:1;min-width:220px}
|
||||
.qb{padding:11px 17px;border-radius:8px;border:1px solid var(--linha);background:#fff;
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:13.5px;cursor:pointer;color:var(--tx)}
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:15.5px;cursor:pointer;color:var(--tx)}
|
||||
.qb.p{background:var(--laranja);border-color:var(--laranja);color:#241703}
|
||||
.qb.chama{background:var(--vermelho);border-color:var(--vermelho);color:#fff;
|
||||
animation:pulsa 1.6s ease-in-out infinite}
|
||||
@@ -505,31 +440,31 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.prev{background:#fff;border:1px solid var(--linha);border-radius:13px;padding:22px;margin-top:16px;display:none}
|
||||
.prev.on{display:block}
|
||||
.prev .ph{display:flex;justify-content:space-between;align-items:flex-end;gap:14px;flex-wrap:wrap;margin-bottom:14px}
|
||||
.prev .ph h3{font-family:"Inter",sans-serif;font-size:17px;font-weight:700}
|
||||
.prev .ph p{font-size:12.5px;color:var(--fraco)}
|
||||
.prev .ph h3{font-family:"Inter",sans-serif;font-size:18px;font-weight:700}
|
||||
.prev .ph p{font-size:15px;color:var(--fraco)}
|
||||
.prev .ph .mt{font-family:"Inter",sans-serif;font-weight:700;font-size:22px;white-space:nowrap}
|
||||
.prev .ph .mt small{font-size:12px;color:var(--fraco);font-weight:600}
|
||||
.prev .ph .mt small{font-size:14.5px;color:var(--fraco);font-weight:600}
|
||||
.pgrid{display:grid;grid-template-columns:300px 1fr;gap:18px;align-items:start}
|
||||
@media(max-width:760px){.pgrid{grid-template-columns:1fr}}
|
||||
.pfolha{background:var(--cinza);border:1px solid var(--linha);border-radius:10px;padding:12px}
|
||||
.pfolha canvas,.pfolha img{width:100%;height:auto;display:block;border-radius:6px;background:#fff}
|
||||
.pfolha .semprev,.pfolha .carregando{padding:40px 16px;text-align:center;color:var(--fraco);
|
||||
font-size:12px;background:#fff;border-radius:6px}
|
||||
.pfolha .semprev b{display:block;font-family:"Inter",sans-serif;color:var(--tx);font-size:13px;margin-bottom:4px}
|
||||
.usoBom,.usoRuim{margin-top:10px;border-radius:8px;padding:9px 12px;font-size:11.5px;line-height:1.45}
|
||||
font-size:14.5px;background:#fff;border-radius:6px}
|
||||
.pfolha .semprev b{display:block;font-family:"Inter",sans-serif;color:var(--tx);font-size:15px;margin-bottom:4px}
|
||||
.usoBom,.usoRuim{margin-top:10px;border-radius:8px;padding:9px 12px;font-size:14px;line-height:1.45}
|
||||
.usoBom{background:#F1F8F3;border:1px solid #CDE6D6;color:#2A6B45}
|
||||
.usoRuim{background:#FFF8EE;border:1px solid #F0D9AE;color:#7A5A16}
|
||||
.usoBom b,.usoRuim b{display:block;font-family:"Inter",sans-serif;font-size:12.5px;margin-bottom:2px}
|
||||
.pinfo .li{display:flex;gap:10px;align-items:flex-start;padding:7px 0;font-size:12.5px;
|
||||
.usoBom b,.usoRuim b{display:block;font-family:"Inter",sans-serif;font-size:15px;margin-bottom:2px}
|
||||
.pinfo .li{display:flex;gap:10px;align-items:flex-start;padding:7px 0;font-size:15px;
|
||||
border-bottom:1px solid var(--linha)}
|
||||
.pinfo .li:last-child{border:0}
|
||||
.pinfo .li .c{width:19px;height:19px;border-radius:50%;display:grid;place-items:center;flex:none;
|
||||
margin-top:1px;font-size:10px;color:#fff;font-family:"Inter",sans-serif;font-weight:700}
|
||||
.pinfo .li b{display:block;font-family:"Inter",sans-serif;font-size:12.5px}
|
||||
.pinfo .li span{color:var(--fraco);font-size:11.5px}
|
||||
margin-top:1px;font-size:12.5px;color:#fff;font-family:"Inter",sans-serif;font-weight:700}
|
||||
.pinfo .li b{display:block;font-family:"Inter",sans-serif;font-size:15px}
|
||||
.pinfo .li span{color:var(--fraco);font-size:14px}
|
||||
.pcta{display:flex;gap:9px;flex-wrap:wrap;margin-top:16px;padding-top:15px;border-top:1px solid var(--linha)}
|
||||
.pb{padding:11px 18px;border-radius:8px;border:1px solid var(--linha);background:#fff;
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:13.5px;cursor:pointer;color:var(--tx)}
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:15.5px;cursor:pointer;color:var(--tx)}
|
||||
.pb.p{background:var(--laranja);border-color:var(--laranja);color:#241703}
|
||||
.pb:hover{border-color:#111}
|
||||
|
||||
@@ -538,10 +473,10 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.prova{padding:clamp(22px,3vw,34px) 0}
|
||||
.prova .w{display:grid;grid-template-columns:repeat(auto-fit,minmax(178px,1fr));gap:14px}
|
||||
.p1{border-left:2px solid var(--laranja);padding:2px 0 2px 13px}
|
||||
.p1 b{display:block;font-family:"Inter",sans-serif;font-size:14px;font-weight:700;
|
||||
.p1 b{display:block;font-family:"Inter",sans-serif;font-size:16px;font-weight:700;
|
||||
line-height:1.25;color:var(--tx);margin-bottom:5px}
|
||||
.p1 b em{font-style:normal;color:var(--laranja)}
|
||||
.p1 span{display:block;font-size:11.5px;color:var(--fraco);line-height:1.5}
|
||||
.p1 span{display:block;font-size:14px;color:var(--fraco);line-height:1.5}
|
||||
|
||||
/* ── Carrinho no padrão de checkout: entrega à esquerda, resumo fixo à direita ── */
|
||||
.carr{display:none;margin-top:18px}
|
||||
@@ -552,37 +487,37 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.carr h3{font-family:"Inter",sans-serif;font-size:19px;font-weight:700;margin-bottom:12px}
|
||||
.cxE{background:#fff;border:1px solid var(--linha);border-radius:12px;
|
||||
box-shadow:var(--sombra);padding:17px 18px;margin-bottom:14px}
|
||||
.cxE h4{font-family:"Inter",sans-serif;font-size:15px;font-weight:700;margin-bottom:11px}
|
||||
.cxE h4{font-family:"Inter",sans-serif;font-size:16.5px;font-weight:700;margin-bottom:11px}
|
||||
|
||||
/* itens do pedido */
|
||||
.item{display:flex;gap:11px;align-items:flex-start;padding:11px 0;border-top:1px solid var(--linha)}
|
||||
.item:first-child{border-top:0;padding-top:2px}
|
||||
.item .nm2{flex:1;min-width:0}
|
||||
.item .nm2 b{display:block;font-family:"Inter",sans-serif;font-size:13.5px;color:var(--tx)}
|
||||
.item .nm2 span{font-size:11.5px;color:var(--fraco)}
|
||||
.item .vl2{font-family:"Inter",sans-serif;font-weight:700;font-size:14px;color:var(--tx);
|
||||
.item .nm2 b{display:block;font-family:"Inter",sans-serif;font-size:15.5px;color:var(--tx)}
|
||||
.item .nm2 span{font-size:14px;color:var(--fraco)}
|
||||
.item .vl2{font-family:"Inter",sans-serif;font-weight:700;font-size:16px;color:var(--tx);
|
||||
white-space:nowrap}
|
||||
.item .x{border:0;background:none;color:var(--gray-2);cursor:pointer;font-size:17px;
|
||||
.item .x{border:0;background:none;color:var(--gray-2);cursor:pointer;font-size:18px;
|
||||
line-height:1;padding:0 0 0 4px}
|
||||
.item .x:hover{color:var(--vermelho)}
|
||||
.rot{font-family:"Inter",sans-serif;font-size:10px;font-weight:700;letter-spacing:.12em;
|
||||
.rot{font-family:"Inter",sans-serif;font-size:12.5px;font-weight:700;letter-spacing:.12em;
|
||||
text-transform:uppercase;color:var(--fraco);margin:0 0 7px}
|
||||
.carr .l{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;
|
||||
.carr .l{display:flex;justify-content:space-between;font-size:15px;padding:5px 0;
|
||||
color:var(--fraco);gap:14px}
|
||||
.carr .l b{color:var(--tx);font-family:"Inter",sans-serif;font-weight:700;white-space:nowrap}
|
||||
|
||||
/* identificação do cliente */
|
||||
.subC{font-size:11.5px;color:var(--fraco);line-height:1.5;margin:-4px 0 12px}
|
||||
.subC{font-size:14px;color:var(--fraco);line-height:1.5;margin:-4px 0 12px}
|
||||
.campos{display:grid;grid-template-columns:1fr 1fr;gap:11px}
|
||||
@media(max-width:620px){.campos{grid-template-columns:1fr}}
|
||||
.cp.larga{grid-column:1/-1}
|
||||
.cp label{display:block;font-size:10.5px;color:var(--fraco);margin-bottom:5px}
|
||||
.cp label{display:block;font-size:13px;color:var(--fraco);margin-bottom:5px}
|
||||
.cp input{width:100%;padding:11px 12px;border-radius:8px;border:1px solid var(--linha);
|
||||
background:#fff;font-family:inherit;font-size:13.5px;color:var(--tx)}
|
||||
background:#fff;font-family:inherit;font-size:15.5px;color:var(--tx)}
|
||||
.cp input:focus{outline:none;border-color:var(--laranja)}
|
||||
.cp input.ruim{border-color:var(--vermelho);background:#FFF6F4}
|
||||
.cp input.bom{border-color:var(--verde-ml)}
|
||||
.cp .err{display:block;font-size:10.5px;color:var(--vermelho);margin-top:4px;min-height:13px}
|
||||
.cp .err{display:block;font-size:13px;color:var(--vermelho);margin-top:4px;min-height:13px}
|
||||
|
||||
/* forma de entrega, como no checkout do marketplace */
|
||||
.entrega{display:grid;gap:9px}
|
||||
@@ -596,42 +531,42 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.opE.on .mk{border-color:var(--verde-ml);
|
||||
background:radial-gradient(circle,var(--verde-ml) 0 4px,#fff 5px)}
|
||||
.opE div{flex:1;min-width:0}
|
||||
.opE b{display:block;font-family:"Inter",sans-serif;font-size:13.5px;margin-bottom:2px}
|
||||
.opE span{font-size:11.5px;color:var(--fraco);line-height:1.5}
|
||||
.opE b{display:block;font-family:"Inter",sans-serif;font-size:15.5px;margin-bottom:2px}
|
||||
.opE span{font-size:14px;color:var(--fraco);line-height:1.5}
|
||||
.opE em{flex:none;font-style:normal;font-family:"Inter",sans-serif;font-weight:700;
|
||||
font-size:13px;color:var(--verde-ml);white-space:nowrap}
|
||||
font-size:15px;color:var(--verde-ml);white-space:nowrap}
|
||||
.cep{display:none;margin-top:10px;padding:12px;background:var(--cinza);border-radius:9px}
|
||||
.cep.on{display:block}
|
||||
.cep label{display:block;font-size:10.5px;color:var(--fraco);margin-bottom:6px}
|
||||
.cep label{display:block;font-size:13px;color:var(--fraco);margin-bottom:6px}
|
||||
.cepL{display:flex;gap:8px}
|
||||
.cepL input{flex:1;min-width:0;padding:10px 12px;border-radius:8px;background:#fff;
|
||||
border:1px solid var(--linha);font-family:inherit;font-size:13px;color:var(--tx)}
|
||||
border:1px solid var(--linha);font-family:inherit;font-size:15px;color:var(--tx)}
|
||||
.cepL input:focus{outline:none;border-color:var(--laranja)}
|
||||
.cepL button{flex:none;width:auto;padding:10px 18px;border-radius:8px;
|
||||
border:1px solid var(--linha);cursor:pointer;background:#fff;color:var(--tx);
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:12.5px}
|
||||
font-family:"Inter",sans-serif;font-weight:600;font-size:15px}
|
||||
.cepL button:hover{border-color:var(--laranja);color:var(--laranja)}
|
||||
.cep p{font-size:11.5px;color:var(--fraco);margin-top:9px;line-height:1.5}
|
||||
.cep p{font-size:14px;color:var(--fraco);margin-top:9px;line-height:1.5}
|
||||
.cep p:empty{display:none}
|
||||
.cep .endereco{margin-top:12px}
|
||||
.cep .err{color:var(--vermelho)}
|
||||
.avisoE:empty{display:none}
|
||||
.avisoE{font-size:11.5px;color:#1F5C3A;background:#F1FBF5;border:1px solid #BFE6CE;
|
||||
.avisoE{font-size:14px;color:#1F5C3A;background:#F1FBF5;border:1px solid #BFE6CE;
|
||||
border-radius:8px;padding:10px 12px;line-height:1.5;margin-top:9px}
|
||||
|
||||
/* resumo da compra */
|
||||
.resumo{position:sticky;top:104px;background:#fff;border:1px solid var(--linha);
|
||||
border-radius:12px;box-shadow:var(--sombra);padding:17px 18px}
|
||||
@media(max-width:900px){.resumo{position:static}}
|
||||
.resumo h4{font-family:"Inter",sans-serif;font-size:16px;font-weight:700;margin-bottom:12px}
|
||||
.resumo h4{font-family:"Inter",sans-serif;font-size:17.5px;font-weight:700;margin-bottom:12px}
|
||||
.resumo .l{padding:6px 0}
|
||||
.carr .tot{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
|
||||
font-family:"Inter",sans-serif;font-size:15px;font-weight:700;padding-top:12px;margin-top:8px;
|
||||
font-family:"Inter",sans-serif;font-size:16.5px;font-weight:700;padding-top:12px;margin-top:8px;
|
||||
border-top:1px solid var(--linha);color:var(--tx)}
|
||||
.carr .tot b{font-size:23px;color:var(--tx)}
|
||||
.cbts{display:grid;gap:9px;margin-top:14px}
|
||||
.cbts button{width:100%;padding:13px;border-radius:9px;border:0;background:var(--laranja);
|
||||
color:#241703;font-family:"Inter",sans-serif;font-weight:700;font-size:14.5px;cursor:pointer;
|
||||
color:#241703;font-family:"Inter",sans-serif;font-weight:700;font-size:16px;cursor:pointer;
|
||||
transition:var(--transition)}
|
||||
.cbts button:hover{background:var(--laranja2)}
|
||||
.cbts button:disabled{opacity:.4;cursor:not-allowed}
|
||||
@@ -644,10 +579,10 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.trilho::-webkit-scrollbar{height:0}
|
||||
.slide{flex:0 0 clamp(244px,76%,316px);scroll-snap-align:start;
|
||||
background:var(--cinza);border:1px solid var(--linha);border-radius:13px;padding:17px 18px}
|
||||
.slide .et2{font-family:"Inter",sans-serif;font-size:9.5px;font-weight:700;letter-spacing:.14em;
|
||||
.slide .et2{font-family:"Inter",sans-serif;font-size:12px;font-weight:700;letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--laranja);margin-bottom:8px;display:block}
|
||||
.slide h3{font-family:"Inter",sans-serif;font-size:16px;font-weight:700;margin-bottom:6px;line-height:1.18}
|
||||
.slide p{font-size:12.5px;color:var(--fraco);line-height:1.5}
|
||||
.slide h3{font-family:"Inter",sans-serif;font-size:17.5px;font-weight:700;margin-bottom:6px;line-height:1.18}
|
||||
.slide p{font-size:15px;color:var(--fraco);line-height:1.5}
|
||||
/* selo circular · o benefício em número, com os anéis da marca em volta */
|
||||
.slide{position:relative;overflow:hidden}
|
||||
.stop{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
||||
@@ -661,26 +596,26 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
.selo::after{inset:-21px;border:1px solid rgba(0,166,80,.09)}
|
||||
.selo b{font-family:"Inter",sans-serif;font-weight:700;font-size:19px;line-height:1;
|
||||
color:var(--verde-ml);letter-spacing:-.01em}
|
||||
.selo b em{font-style:normal;font-size:11px;margin-left:1px}
|
||||
.selo span{font-family:"Inter",sans-serif;font-weight:700;font-size:8px;line-height:1.2;
|
||||
.selo b em{font-style:normal;font-size:13.5px;margin-left:1px}
|
||||
.selo span{font-family:"Inter",sans-serif;font-weight:700;font-size:10.5px;line-height:1.2;
|
||||
letter-spacing:.07em;text-transform:uppercase;color:var(--fraco);margin-top:4px;padding:0 5px}
|
||||
.slide .num{font-family:"Inter",sans-serif;font-weight:700;font-size:26px;
|
||||
color:var(--laranja);line-height:1;margin-bottom:6px}
|
||||
.slide ul{list-style:none;font-size:12px;margin-top:9px}
|
||||
.slide ul{list-style:none;font-size:14.5px;margin-top:9px}
|
||||
.slide li{padding:4px 0 4px 19px;position:relative;color:var(--fraco)}
|
||||
.slide li::before{content:"✓";position:absolute;left:2px;color:var(--verde);font-size:11px}
|
||||
.slide li::before{content:"✓";position:absolute;left:2px;color:var(--verde);font-size:13.5px}
|
||||
.slide li.no::before{content:"!";color:var(--laranja);font-weight:700}
|
||||
.slide table{width:100%;font-size:12px;margin-top:9px;border-collapse:collapse}
|
||||
.slide table{width:100%;font-size:14.5px;margin-top:9px;border-collapse:collapse}
|
||||
.slide td{padding:5px 0;border-bottom:1px solid #E3E3E3;color:var(--fraco)}
|
||||
.slide td+td{text-align:right;font-family:"Inter",sans-serif;font-weight:700;color:var(--tx)}
|
||||
.slide td.cab{font-family:"Inter",sans-serif;font-size:9.5px;font-weight:700;letter-spacing:.1em;
|
||||
.slide td.cab{font-family:"Inter",sans-serif;font-size:12px;font-weight:700;letter-spacing:.1em;
|
||||
text-transform:uppercase;color:var(--fraco)}
|
||||
.slide tr:last-child td{border:0}
|
||||
.slide .dest{background:#fff;border-radius:8px;padding:9px 11px;margin-top:9px;
|
||||
font-size:11.5px;color:var(--tx)}
|
||||
font-size:14px;color:var(--tx)}
|
||||
.navc{display:flex;gap:8px;align-items:center;margin-top:14px}
|
||||
.navc button{width:32px;height:32px;border-radius:50%;border:1px solid var(--linha);
|
||||
background:#fff;color:var(--tx);cursor:pointer;font-size:15px;line-height:1}
|
||||
background:#fff;color:var(--tx);cursor:pointer;font-size:16.5px;line-height:1}
|
||||
.navc button:hover{border-color:var(--laranja);color:var(--laranja)}
|
||||
.pontos{display:flex;gap:5px;margin-left:auto}
|
||||
.pt{width:20px;height:3px;border-radius:2px;background:var(--linha);border:0;padding:0;cursor:pointer}
|
||||
@@ -689,31 +624,19 @@ h1 em{font-style:normal;color:var(--laranja)}
|
||||
|
||||
h2{font-size:clamp(23px,3.6vw,34px);font-weight:700;max-width:20ch}
|
||||
h2 em{font-style:normal;color:var(--laranja)}
|
||||
.et{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--laranja);
|
||||
.et{font-size:13.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--laranja);
|
||||
font-family:"Inter",sans-serif;font-weight:700;margin-bottom:10px}
|
||||
.lead{color:var(--fraco);font-size:15px;max-width:56ch;margin-top:12px}
|
||||
.lead{color:var(--fraco);font-size:16.5px;max-width:56ch;margin-top:12px}
|
||||
.dor{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:13px;margin-top:24px}
|
||||
.dc{background:#fff;border:1px solid var(--linha);border-radius:12px;padding:18px 19px}
|
||||
.dc .ic{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:14px;margin-bottom:11px;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:16px;margin-bottom:11px;
|
||||
background:#FBEEEA;color:var(--vermelho)}
|
||||
.dc h3{font-size:15px;font-weight:700;margin-bottom:4px}
|
||||
.dc p{font-size:12.5px;color:var(--fraco)}
|
||||
.nota{background:var(--cinza);border-radius:11px;padding:14px 18px;margin-top:15px;font-size:13px;color:var(--fraco)}
|
||||
.dc h3{font-size:16.5px;font-weight:700;margin-bottom:4px}
|
||||
.dc p{font-size:15px;color:var(--fraco)}
|
||||
.nota{background:var(--cinza);border-radius:11px;padding:14px 18px;margin-top:15px;font-size:15px;color:var(--fraco)}
|
||||
.nota b{color:var(--tx);font-family:"Inter",sans-serif}
|
||||
|
||||
footer{background:#000;color:#8D9AB0;padding:26px 0 20px;font-size:12.5px}
|
||||
.frow{display:flex;gap:26px;align-items:flex-start;flex-wrap:wrap;padding-bottom:18px}
|
||||
.fmarca{display:flex;align-items:center;gap:9px;font-family:"Inter",sans-serif;
|
||||
font-weight:700;font-size:13px;color:var(--laranja);letter-spacing:.06em;white-space:nowrap}
|
||||
.fmarca img{height:22px;width:auto;display:block;filter:brightness(0) invert(1)}
|
||||
footer a{color:#8D9AB0;text-decoration:none}
|
||||
footer a:hover{color:var(--laranja2)}
|
||||
.flinks{display:flex;gap:18px;flex-wrap:wrap;font-size:12.5px}
|
||||
.fcont{margin-left:auto;text-align:right;font-size:12.5px;line-height:1.7}
|
||||
@media(max-width:720px){.fcont{margin-left:0;text-align:left}}
|
||||
.fbase{border-top:1px solid rgba(255,255,255,.1);padding-top:14px;display:flex;
|
||||
justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:11px;color:#5A6570}
|
||||
@media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto}}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/site-v2.css">
|
||||
@@ -727,13 +650,9 @@ footer a:hover{color:var(--laranja2)}
|
||||
|
||||
<header class="topo"><div class="w">
|
||||
<a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a>
|
||||
<div class="busca">
|
||||
<span class="lupa">⌕</span>
|
||||
<input type="search" placeholder="Buscar" aria-label="Buscar">
|
||||
</div>
|
||||
<div class="acts">
|
||||
<a href="/portal.html">Minha conta</a>
|
||||
<a href="/carrinho" id="cartLink">Carrinho (0)</a><span class="cartEnvio" id="cartEnvio" aria-live="polite"></span>
|
||||
<a href="/conta"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx">Minha conta</span></a>
|
||||
<a href="/carrinho" id="cartLink" aria-label="Carrinho, 0 itens"><span class="cartIc"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><b class="cartN" id="cartN" hidden>0</b></span><span class="actTx">Carrinho</span></a><span class="cartEnvio" id="cartEnvio" aria-live="polite"></span>
|
||||
</div>
|
||||
</div></header>
|
||||
|
||||
@@ -918,7 +837,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="recusa" id="recusa" style="display:none"></div>
|
||||
<div id="lista"></div>
|
||||
<div class="zona" id="zona" tabindex="0" role="button">
|
||||
<div class="ico">↑</div>
|
||||
<div class="ico"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/></svg></div>
|
||||
<b id="zTit">Arraste aqui</b><span id="zSub"></span>
|
||||
<span id="zLimite">Até 5 GB por arquivo.</span>
|
||||
<span class="bt">ou escolher no computador</span>
|
||||
@@ -1023,14 +942,14 @@ footer a:hover{color:var(--laranja2)}
|
||||
<span>Li e entendi. Quero imprimir assim mesmo.</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="cAdd" id="bAdd" disabled>Adicionar ao carrinho</button>
|
||||
<button type="button" class="cAdd" id="bAdd" disabled><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M12 10v6"/><path d="M9 13h6"/></svg><span>Adicionar ao carrinho</span></button>
|
||||
<p class="cPe">Revisamos e reencaixamos antes de imprimir, sem custo.<span id="cPartes"></span></p>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
<dialog class="ampliaDlg" id="ampliaDlg" aria-labelledby="ampliaTit">
|
||||
<div class="ampliaCab"><b id="ampliaTit">Sua folha</b>
|
||||
<button type="button" id="ampliaFecha" aria-label="Fechar">×</button></div>
|
||||
<button type="button" id="ampliaFecha" aria-label="Fechar"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></div>
|
||||
<div id="ampliaArea"></div>
|
||||
</dialog>
|
||||
</div>
|
||||
@@ -1044,9 +963,9 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div class="qual" id="qual">
|
||||
<div class="qh">
|
||||
<div><h3 id="qNome">sua folha</h3>
|
||||
<span style="font-size:12px;color:var(--fraco)" id="qSub"></span></div>
|
||||
<span style="font-size:14.5px;color:var(--fraco)" id="qSub"></span></div>
|
||||
<div style="text-align:right"><div class="pct" id="pct">—</div>
|
||||
<span style="font-size:11px;color:var(--fraco)">qualidade</span></div>
|
||||
<span style="font-size:13.5px;color:var(--fraco)">qualidade</span></div>
|
||||
</div>
|
||||
<div class="qbar"><i id="qbi" style="width:0%"></i></div>
|
||||
<div class="qitens" id="qitens"></div>
|
||||
@@ -1062,14 +981,14 @@ footer a:hover{color:var(--laranja2)}
|
||||
<p id="pSub">confira antes de fechar o pedido</p></div>
|
||||
<div style="text-align:right">
|
||||
<div class="mt" id="pMt">—</div>
|
||||
<span style="font-size:11px;color:var(--fraco)">cobrado por proporção</span></div>
|
||||
<span style="font-size:13.5px;color:var(--fraco)">cobrado por proporção</span></div>
|
||||
</div>
|
||||
<div class="pgrid">
|
||||
<div class="pfolha"><div id="pArea"></div></div>
|
||||
<div class="pinfo" id="pInfo"></div>
|
||||
</div>
|
||||
<div class="pcta">
|
||||
<button class="pb p" id="pOk">Adicionar ao carrinho</button>
|
||||
<button class="pb p" id="pOk"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M12 10v6"/><path d="M9 13h6"/></svg><span>Adicionar ao carrinho</span></button>
|
||||
<button class="pb" id="pMais">Adicionar mais artes</button>
|
||||
<button class="pb" id="pVolta">Trocar o arquivo</button>
|
||||
</div>
|
||||
@@ -1093,17 +1012,15 @@ footer a:hover{color:var(--laranja2)}
|
||||
|
||||
<div class="cxE">
|
||||
<h4>Seus dados</h4>
|
||||
<p class="subC">A conta nasce aqui, no pagamento. O CNPJ é o mesmo da nota fiscal,
|
||||
e é por ele que suas artes ficam guardadas para o próximo pedido.</p>
|
||||
<div class="campos">
|
||||
<div class="cp"><label>CNPJ</label>
|
||||
<input id="fCnpj" inputmode="numeric" maxlength="18" placeholder="00.000.000/0000-00">
|
||||
<div class="cp"><label>CNPJ <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="fCnpj" aria-required="true" autocomplete="off" inputmode="numeric" maxlength="18" placeholder="00.000.000/0000-00">
|
||||
<span class="err" id="eCnpj"></span></div>
|
||||
<div class="cp"><label>WhatsApp</label>
|
||||
<input id="fZap" inputmode="numeric" maxlength="15" placeholder="(16) 99999-9999">
|
||||
<div class="cp"><label>WhatsApp <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="fZap" aria-required="true" autocomplete="off" inputmode="numeric" maxlength="15" placeholder="(16) 99999-9999">
|
||||
<span class="err" id="eZap"></span></div>
|
||||
<div class="cp larga"><label>E-mail para a nota</label>
|
||||
<input id="fMail" type="email" placeholder="voce@empresa.com.br">
|
||||
<div class="cp larga"><label>E-mail para a nota <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="fMail" aria-required="true" autocomplete="off" type="email" placeholder="voce@empresa.com.br">
|
||||
<span class="err" id="eMail"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1114,7 +1031,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<button class="opE on" data-ent="retira">
|
||||
<span class="mk"></span>
|
||||
<div>
|
||||
<b>Retirar em Franca</b>
|
||||
<b><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20 10c0 4.99-5.54 10.19-7.4 11.8a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>Retirar em Franca</b>
|
||||
<span>Av. Major Elias Motta, 1281 · Jardim São Luís<br>
|
||||
Segunda a sexta, das 7h às 17h</span>
|
||||
</div>
|
||||
@@ -1123,34 +1040,33 @@ footer a:hover{color:var(--laranja2)}
|
||||
<button class="opE" data-ent="frete">
|
||||
<span class="mk"></span>
|
||||
<div>
|
||||
<b>Receber em casa</b>
|
||||
<b><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg>Receber em casa</b>
|
||||
<span>Entrega pela Jadlog em todo o Brasil. Informe o CEP para ver o valor e o prazo.</span>
|
||||
</div>
|
||||
<em id="vFrete">—</em>
|
||||
</button>
|
||||
<div class="cep" id="cep">
|
||||
<label>CEP de entrega</label>
|
||||
<label>CEP de entrega <span class="obr" aria-hidden="true">*</span></label>
|
||||
<div class="cepL">
|
||||
<input id="cepIn" inputmode="numeric" maxlength="9" placeholder="00000-000" autocomplete="postal-code">
|
||||
<input id="cepIn" aria-required="true" autocomplete="off" inputmode="numeric" maxlength="9" placeholder="00000-000">
|
||||
</div>
|
||||
<p id="cepMsg"></p>
|
||||
<div class="campos endereco" id="endereco">
|
||||
<div class="cp larga"><label>Quem recebe</label>
|
||||
<input id="eNome" maxlength="120" autocomplete="name" placeholder="Nome ou empresa"></div>
|
||||
<div class="cp larga"><label>Rua / avenida</label>
|
||||
<input id="eRua" maxlength="160" autocomplete="address-line1"></div>
|
||||
<div class="cp"><label>Número</label>
|
||||
<input id="eNum" maxlength="20" placeholder="123 ou S/N"></div>
|
||||
<div class="cp larga"><label>Quem recebe <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eNome" aria-required="true" autocomplete="off" maxlength="120" placeholder="Nome ou empresa"></div>
|
||||
<div class="cp larga"><label>Rua / avenida <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eRua" aria-required="true" autocomplete="off" maxlength="160"></div>
|
||||
<div class="cp"><label>Número <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eNum" aria-required="true" autocomplete="off" maxlength="20" placeholder="123 ou S/N"></div>
|
||||
<div class="cp"><label>Complemento (opcional)</label>
|
||||
<input id="eComp" maxlength="80" autocomplete="address-line2"></div>
|
||||
<div class="cp"><label>Bairro</label>
|
||||
<input id="eBairro" maxlength="80"></div>
|
||||
<div class="cp"><label>Cidade</label>
|
||||
<input id="eCidade" maxlength="80" autocomplete="address-level2"></div>
|
||||
<div class="cp"><label>UF</label>
|
||||
<input id="eUf" maxlength="2" autocomplete="address-level1" placeholder="SP"></div>
|
||||
<input id="eComp" autocomplete="off" maxlength="80"></div>
|
||||
<div class="cp"><label>Bairro <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eBairro" aria-required="true" autocomplete="off" maxlength="80"></div>
|
||||
<div class="cp"><label>Cidade <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eCidade" aria-required="true" autocomplete="off" maxlength="80"></div>
|
||||
<div class="cp"><label>UF <span class="obr" aria-hidden="true">*</span></label>
|
||||
<input id="eUf" aria-required="true" autocomplete="off" maxlength="2" placeholder="SP"></div>
|
||||
</div>
|
||||
<p class="dicaEnd" id="eEnd"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1161,7 +1077,7 @@ footer a:hover{color:var(--laranja2)}
|
||||
<div id="resLin"></div>
|
||||
<div class="tot"><span>Total</span><b id="carrTot">R$ 0,00</b></div>
|
||||
<div class="cbts">
|
||||
<button id="bPagar">Ir para o pagamento</button>
|
||||
<button id="bPagar"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg><span id="bPagarTx">Ir para o pagamento</span></button>
|
||||
<button id="bMais" class="sec">Continuar comprando</button>
|
||||
</div>
|
||||
<p class="envioArq" id="envioArq" role="status" aria-live="polite"></p>
|
||||
|
||||
101
web/kanban.css
101
web/kanban.css
@@ -18,46 +18,46 @@ a{color:var(--accent-tx)}
|
||||
.mono{font-family:var(--mono)}
|
||||
.muted{color:var(--muted)}
|
||||
.faint{color:var(--faint)}
|
||||
.btn{height:36px;padding:0 12px;border-radius:8px;border:1px solid var(--line2);background:var(--card2);color:var(--tx);font-size:13px;font-weight:600;white-space:nowrap}
|
||||
.btn{height:36px;padding:0 12px;border-radius:8px;border:1px solid var(--line2);background:var(--card2);color:var(--tx);font-size:14px;font-weight:600;white-space:nowrap}
|
||||
.btn:hover{border-color:var(--line3)}
|
||||
.btn.ghost{background:transparent;color:var(--muted)}
|
||||
.btn.primary{height:44px;border:0;background:var(--accent);color:#0F1113;font-weight:800;font-size:14px;padding:0 18px}
|
||||
.btn.primary{height:44px;border:0;background:var(--accent);color:#0F1113;font-weight:800;font-size:14.5px;padding:0 18px}
|
||||
.btn.primary:hover{filter:brightness(1.08)}
|
||||
.btn.danger{height:44px;background:transparent;border-color:var(--bad-line);color:var(--bad);font-weight:700;font-size:14px}
|
||||
.btn.danger{height:44px;background:transparent;border-color:var(--bad-line);color:var(--bad);font-weight:700;font-size:14.5px}
|
||||
.btn.warn{border:0;background:var(--warn);color:#0F1113;font-weight:800}
|
||||
.chip{height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--line2);background:transparent;color:var(--muted);font-size:13px;font-weight:600}
|
||||
.chip{height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--line2);background:transparent;color:var(--muted);font-size:14px;font-weight:600}
|
||||
.chip[aria-pressed=true]{border-color:var(--accent);background:rgba(255,139,43,.12);color:var(--accent-tx)}
|
||||
.badge{font-size:12px;font-weight:700;border-radius:999px;padding:1px 8px;background:var(--accent);color:#0F1113}
|
||||
.badge{font-size:13.5px;font-weight:700;border-radius:999px;padding:1px 8px;background:var(--accent);color:#0F1113}
|
||||
.badge.warn{background:var(--warn);color:#0F1113}
|
||||
.badge:empty{display:none}
|
||||
.eyebrow{font-size:12px;color:var(--faint);font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin:0 0 8px}
|
||||
.eyebrow{font-size:13.5px;color:var(--faint);font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin:0 0 8px}
|
||||
input[type=text],input[type=email],input[type=password],input[type=number],input:not([type]),textarea{height:40px;padding:0 12px;border-radius:8px;border:1px solid var(--line3);background:var(--bg);color:var(--tx)}
|
||||
textarea{height:auto;padding:10px 12px;min-height:72px;width:100%;resize:vertical}
|
||||
input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent)}
|
||||
input[type=file]{font-size:13px;color:var(--muted);max-width:100%}
|
||||
.check{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--tx2)}
|
||||
input[type=file]{font-size:14px;color:var(--muted);max-width:100%}
|
||||
.check{display:flex;align-items:center;gap:10px;font-size:14px;color:var(--tx2)}
|
||||
.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}
|
||||
|
||||
/* Sign-in */
|
||||
.login{max-width:380px;margin:12vh auto 0;padding:32px;border:1px solid var(--line2);border-radius:14px;background:var(--panel);display:flex;flex-direction:column;gap:14px}
|
||||
.login label{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600;color:var(--muted)}
|
||||
.login .hint{font-size:12px;color:var(--faint);margin:0}
|
||||
.login label{display:flex;flex-direction:column;gap:6px;font-size:14px;font-weight:600;color:var(--muted)}
|
||||
.login .hint{font-size:13.5px;color:var(--faint);margin:0}
|
||||
|
||||
/* Top bar */
|
||||
.top{height:64px;padding:0 24px;display:flex;align-items:center;gap:22px;border-bottom:1px solid var(--line);background:var(--bar);position:sticky;top:0;z-index:5}
|
||||
.brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:16px;letter-spacing:-.01em;white-space:nowrap}
|
||||
.brand i{width:28px;height:28px;border-radius:7px;background:var(--accent);color:#0F1113;font-style:normal;font-weight:800;font-size:15px;display:flex;align-items:center;justify-content:center}
|
||||
.tabs{display:flex;gap:4px;align-self:stretch}
|
||||
.tabs button{display:flex;align-items:center;gap:8px;padding:0 14px;border:0;border-bottom:2px solid transparent;background:none;color:var(--muted);font-weight:600;font-size:14px}
|
||||
.tabs button{display:flex;align-items:center;gap:8px;padding:0 14px;border:0;border-bottom:2px solid transparent;background:none;color:var(--muted);font-weight:600;font-size:14.5px}
|
||||
.tabs button[aria-selected=true]{color:var(--tx);font-weight:700;border-bottom-color:var(--accent)}
|
||||
.tabs .count{font-family:var(--mono);font-size:12px;color:var(--muted)}
|
||||
.tabs .count{font-family:var(--mono);font-size:13.5px;color:var(--muted)}
|
||||
.search{display:flex;align-items:center;gap:8px;width:300px;height:38px;padding:0 12px;border:1px solid var(--line2);border-radius:9px;background:var(--card);color:var(--faint);margin-left:auto}
|
||||
.search input{flex:1;min-width:0;border:0;background:transparent;height:auto;padding:0;outline:none}
|
||||
.tinydot{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);white-space:nowrap}
|
||||
.tinydot{display:flex;align-items:center;gap:8px;font-size:14px;color:var(--muted);white-space:nowrap}
|
||||
.tinydot::before{content:"";width:8px;height:8px;border-radius:999px;background:var(--dot,#5B6168)}
|
||||
.tinydot:empty{display:none}
|
||||
.who{font-size:13px;color:var(--muted);white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
|
||||
#status{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:20;margin:0;max-width:min(640px,calc(100vw - 32px));padding:10px 16px;border-radius:10px;border:1px solid var(--line3);background:#1E2226;color:var(--tx);font-size:13px;box-shadow:0 10px 30px rgba(0,0,0,.45)}
|
||||
.who{font-size:14px;color:var(--muted);white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
|
||||
#status{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:20;margin:0;max-width:min(640px,calc(100vw - 32px));padding:10px 16px;border-radius:10px;border:1px solid var(--line3);background:#1E2226;color:var(--tx);font-size:14px;box-shadow:0 10px 30px rgba(0,0,0,.45)}
|
||||
#status:empty,#login-status:empty{display:none}
|
||||
#status.error{border-color:var(--bad-line);color:var(--bad)}
|
||||
#login-status.error{color:var(--bad)}
|
||||
@@ -74,23 +74,23 @@ main{padding:0 24px 24px}
|
||||
.col.target{border-color:var(--accent)}
|
||||
.col-head{display:flex;flex-direction:column;gap:6px;padding:2px 2px 8px;border-bottom:1px solid #22262B}
|
||||
.col-head div{display:flex;align-items:center;gap:8px}
|
||||
.col-head h2{margin:0;font-size:14px;font-weight:700}
|
||||
.col-head h2{margin:0;font-size:14.5px;font-weight:700}
|
||||
.col-head .swatch{width:10px;height:10px;border-radius:3px;background:var(--c)}
|
||||
.col-head .n{margin-left:auto;font-family:var(--mono);font-size:12px;color:var(--muted)}
|
||||
.col-head small{font-size:12px;color:var(--faint)}
|
||||
.col .empty{font-size:12px;color:var(--faint);padding:8px 2px}
|
||||
.col-head .n{margin-left:auto;font-family:var(--mono);font-size:13.5px;color:var(--muted)}
|
||||
.col-head small{font-size:13.5px;color:var(--faint)}
|
||||
.col .empty{font-size:13.5px;color:var(--faint);padding:8px 2px}
|
||||
.card{text-align:left;display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:10px;border:1px solid var(--line2);background:var(--card);width:100%}
|
||||
.card:hover,.card.open{border-color:var(--line3);background:#1E2226}
|
||||
.card .row{display:flex;align-items:baseline;gap:8px;width:100%}
|
||||
.card .num{font-size:14px;font-weight:800}
|
||||
.card .age{margin-left:auto;font-size:12px;color:var(--faint)}
|
||||
.card .cust{font-size:13px;font-weight:600;color:var(--tx);width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.card .num{font-size:14.5px;font-weight:800}
|
||||
.card .age{margin-left:auto;font-size:13.5px;color:var(--faint)}
|
||||
.card .cust{font-size:14px;font-weight:600;color:var(--tx);width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tags{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.tag{font-size:12px;padding:3px 8px;border-radius:6px;background:var(--card2);color:var(--tx2)}
|
||||
.tag-teste{font-size:11px;font-weight:800;letter-spacing:.04em;padding:2px 7px;border-radius:6px;background:var(--warn);color:#1a1300}
|
||||
.card .foot{display:flex;align-items:center;gap:6px;font-size:12px}
|
||||
.tag{font-size:13.5px;padding:3px 8px;border-radius:6px;background:var(--card2);color:var(--tx2)}
|
||||
.tag-teste{font-size:12.5px;font-weight:800;letter-spacing:.04em;padding:2px 7px;border-radius:6px;background:var(--warn);color:#1a1300}
|
||||
.card .foot{display:flex;align-items:center;gap:6px;font-size:13.5px}
|
||||
.card .foot .where{margin-left:auto;color:var(--muted)}
|
||||
.late{font-size:12px;color:#F06A5B;font-weight:600}
|
||||
.late{font-size:13.5px;color:#F06A5B;font-weight:600}
|
||||
|
||||
/* Order panel */
|
||||
.drawer{position:fixed;inset:0;z-index:10;display:flex;justify-content:flex-end}
|
||||
@@ -99,10 +99,10 @@ main{padding:0 24px 24px}
|
||||
.panel-head{padding:20px 24px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:14px}
|
||||
.panel-head .row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||
.panel-head .num{font-size:22px;font-weight:800}
|
||||
.stage{font-size:12px;font-weight:700;padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--c) 18%,transparent);color:var(--c)}
|
||||
.stage{font-size:13.5px;font-weight:700;padding:4px 10px;border-radius:999px;background:color-mix(in srgb,var(--c) 18%,transparent);color:var(--c)}
|
||||
.close{margin-left:auto;width:40px;height:40px;border-radius:8px;border:1px solid var(--line2);background:transparent;color:var(--muted);display:flex;align-items:center;justify-content:center}
|
||||
.steps{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px}
|
||||
.steps li{display:flex;flex-direction:column;gap:6px;font-size:11px;color:var(--faint)}
|
||||
.steps li{display:flex;flex-direction:column;gap:6px;font-size:12.5px;color:var(--faint)}
|
||||
.steps li::before{content:"";height:4px;border-radius:4px;background:var(--line2)}
|
||||
.steps li.done{color:var(--muted)}.steps li.done::before{background:var(--c)}
|
||||
.steps li.now{color:var(--tx);font-weight:700}.steps li.now::before{background:var(--c)}
|
||||
@@ -111,27 +111,27 @@ main{padding:0 24px 24px}
|
||||
.reason{display:flex;flex-direction:column;gap:8px}
|
||||
.panel-body{flex:1;overflow:auto;padding:20px 24px 32px;display:flex;flex-direction:column;gap:24px}
|
||||
.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
|
||||
.facts b{display:block;font-size:14px}
|
||||
.facts span{font-size:13px;color:var(--muted);overflow-wrap:anywhere}
|
||||
.facts b{display:block;font-size:14.5px}
|
||||
.facts span{font-size:14px;color:var(--muted);overflow-wrap:anywhere}
|
||||
.item{display:flex;gap:14px;padding:14px;border:1px solid var(--line2);border-radius:12px;background:var(--card)}
|
||||
.item.attention{border-color:#4A3A1E;background:#1C1A15}
|
||||
.item .preview{width:96px;flex-shrink:0;border-radius:8px;background:var(--card2);display:flex;align-items:flex-start;justify-content:center;padding:6px}
|
||||
.item .preview svg{width:100%;height:auto;display:block}
|
||||
.item .body{flex:1;min-width:0;display:flex;flex-direction:column;gap:8px}
|
||||
.item .title{display:flex;align-items:baseline;gap:8px}
|
||||
.item .title b{font-size:14px}
|
||||
.item .title .mono{margin-left:auto;font-size:13px}
|
||||
.item .line{font-size:12px;color:var(--muted)}
|
||||
.item .file{display:flex;align-items:center;gap:8px;font-size:13px}
|
||||
.item .title b{font-size:14.5px}
|
||||
.item .title .mono{margin-left:auto;font-size:14px}
|
||||
.item .line{font-size:13.5px;color:var(--muted)}
|
||||
.item .file{display:flex;align-items:center;gap:8px;font-size:14px}
|
||||
.buttons{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.finals{display:flex;flex-direction:column;gap:12px}
|
||||
.finals .rev{font-size:13px;display:flex;flex-direction:column;gap:4px;padding:10px 12px;border:1px solid var(--line2);border-radius:10px}
|
||||
.finals .rev{font-size:14px;display:flex;flex-direction:column;gap:4px;padding:10px 12px;border:1px solid var(--line2);border-radius:10px}
|
||||
.finals form{display:flex;flex-direction:column;gap:12px;padding:14px;border:1px solid var(--line2);border-radius:12px}
|
||||
.finals .slot{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600}
|
||||
.finals .slot{display:flex;flex-direction:column;gap:6px;font-size:14px;font-weight:600}
|
||||
.timeline{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:10px}
|
||||
.timeline li{display:flex;gap:12px;font-size:13px}
|
||||
.timeline li{display:flex;gap:12px;font-size:14px}
|
||||
.timeline li::before{content:"";width:8px;height:8px;margin-top:6px;border-radius:999px;background:var(--c,var(--muted));flex-shrink:0}
|
||||
.timeline small{display:block;color:var(--faint);font-size:12px}
|
||||
.timeline small{display:block;color:var(--faint);font-size:13.5px}
|
||||
|
||||
/* Quotes */
|
||||
#tab-quotes{padding-top:18px}
|
||||
@@ -139,8 +139,8 @@ main{padding:0 24px 24px}
|
||||
.split .list{width:460px;flex-shrink:0;border-right:1px solid var(--line);padding:16px;display:flex;flex-direction:column;gap:12px;background:#111316}
|
||||
.qitem{text-align:left;display:flex;justify-content:space-between;gap:12px;padding:14px;border-radius:10px;border:1px solid var(--line2);background:var(--card);width:100%}
|
||||
.qitem[aria-current=true]{border-color:var(--accent);background:#1F1A15}
|
||||
.qitem b{display:block;font-size:14px;overflow-wrap:anywhere}
|
||||
.qitem span{font-size:12px;color:var(--muted)}
|
||||
.qitem b{display:block;font-size:14.5px;overflow-wrap:anywhere}
|
||||
.qitem span{font-size:13.5px;color:var(--muted)}
|
||||
.qitem .right{text-align:right;flex-shrink:0}
|
||||
.qitem .flag{display:block;color:var(--warn);font-weight:600;margin-top:4px}
|
||||
.pane{flex:1;min-width:0;display:flex;flex-direction:column}
|
||||
@@ -148,7 +148,7 @@ main{padding:0 24px 24px}
|
||||
.pane .head b{font-size:18px;overflow-wrap:anywhere}
|
||||
.pane .content{flex:1;padding:20px 24px;display:flex;flex-direction:column;gap:16px}
|
||||
.pane .fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
|
||||
.pane .fields label{display:flex;flex-direction:column;gap:6px;font-size:12px;font-weight:600;color:var(--muted)}
|
||||
.pane .fields label{display:flex;flex-direction:column;gap:6px;font-size:13.5px;font-weight:600;color:var(--muted)}
|
||||
.pane .fields input[readonly]{color:var(--faint);background:var(--panel);border-color:var(--line2)}
|
||||
.pane .foot{padding:16px 24px;border-top:1px solid var(--line);background:var(--bar);display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
||||
.pane .foot .total{margin-left:auto;font-family:var(--mono);color:var(--muted)}
|
||||
@@ -160,11 +160,11 @@ main{padding:0 24px 24px}
|
||||
.page h2{margin:0 0 10px;font-size:15px;font-weight:800}
|
||||
.issue{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:16px 18px;border-radius:12px;border:1px solid #4A3A1E;background:#1C1A15;margin-bottom:8px}
|
||||
.issue .text{flex:1;min-width:260px;display:flex;flex-direction:column;gap:4px}
|
||||
.issue .text b{font-size:14px}
|
||||
.issue .text span{font-size:13px;color:var(--muted);overflow-wrap:anywhere}
|
||||
.issue .text b{font-size:14.5px}
|
||||
.issue .text span{font-size:14px;color:var(--muted);overflow-wrap:anywhere}
|
||||
.issue form{display:flex;gap:8px;align-items:center}
|
||||
.issue.done{border-color:var(--line2);background:var(--card)}
|
||||
.pager{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:14px 4px;border-top:1px solid var(--line);margin-top:8px;font-size:13px;color:var(--muted)}
|
||||
.pager{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:14px 4px;border-top:1px solid var(--line);margin-top:8px;font-size:14px;color:var(--muted)}
|
||||
.pager-size{display:flex;align-items:center;gap:8px}
|
||||
.pager-size select{height:34px;padding:0 8px;border-radius:8px;border:1px solid var(--line3);background:var(--bg);color:var(--tx)}
|
||||
.pager-range{margin-left:auto}
|
||||
@@ -177,7 +177,7 @@ main{padding:0 24px 24px}
|
||||
.pager.compact .pager-range{margin-left:0}
|
||||
.pager.compact .pager-nav{margin-left:auto}
|
||||
.filters{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;margin-bottom:12px}
|
||||
.field{display:flex;flex-direction:column;gap:6px;font-size:12px;font-weight:600;color:var(--muted)}
|
||||
.field{display:flex;flex-direction:column;gap:6px;font-size:13.5px;font-weight:600;color:var(--muted)}
|
||||
.field select,.field input{height:38px;min-width:160px;padding:0 10px;border-radius:8px;border:1px solid var(--line3);background:var(--bg);color:var(--tx)}
|
||||
.field input{min-width:110px;width:110px}
|
||||
.issue input{width:320px}
|
||||
@@ -186,10 +186,10 @@ main{padding:0 24px 24px}
|
||||
.integration .head{display:flex;align-items:center;gap:8px}
|
||||
.integration .head b{font-size:15px}
|
||||
.integration .head .dot{width:9px;height:9px;border-radius:999px;background:var(--dot,#5B6168)}
|
||||
.integration .head em{margin-left:auto;font-style:normal;font-size:12px;font-weight:700;color:var(--dotx,var(--muted))}
|
||||
.integration p{margin:0;font-size:13px;color:var(--muted)}
|
||||
.integration .head em{margin-left:auto;font-style:normal;font-size:13.5px;font-weight:700;color:var(--dotx,var(--muted))}
|
||||
.integration p{margin:0;font-size:14px;color:var(--muted)}
|
||||
.integration .buttons{margin-top:auto}
|
||||
table.log{width:100%;border-collapse:collapse;font-size:13px}
|
||||
table.log{width:100%;border-collapse:collapse;font-size:14px}
|
||||
table.log th{text-align:left;color:var(--faint);font-weight:600;padding:8px 12px;border-bottom:1px solid var(--line)}
|
||||
table.log td{padding:10px 12px;border-bottom:1px solid #1E2226;vertical-align:top}
|
||||
.good{color:var(--ok);font-weight:600}.retry{color:var(--warn);font-weight:600}
|
||||
@@ -202,3 +202,8 @@ table.log td{padding:10px 12px;border-bottom:1px solid #1E2226;vertical-align:to
|
||||
.split .list{width:auto;border-right:0;border-bottom:1px solid var(--line)}
|
||||
.facts,.pane .fields{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
/* Icons beside a label: the download buttons, where a card ships, the WhatsApp link. */
|
||||
.btn.with-icon{display:inline-flex;align-items:center;gap:6px}
|
||||
.card .foot .where{display:inline-flex;align-items:center;gap:4px}
|
||||
a.zap{display:inline-flex;align-items:center;gap:4px;vertical-align:bottom}
|
||||
|
||||
@@ -54,7 +54,7 @@ function contato(customer){
|
||||
const d=String(customer.zap).replace(/\D/g,'').replace(/^55(?=\d{10,11}$)/,'');
|
||||
const shown=d.length===11?'('+d.slice(0,2)+') '+d.slice(2,7)+'-'+d.slice(7):d.length===10?'('+d.slice(0,2)+') '+d.slice(2,6)+'-'+d.slice(6):customer.zap;
|
||||
const a=node('a',shown,'zap');a.href=href;a.target='_blank';a.rel='noopener noreferrer';
|
||||
a.title='Abrir conversa no WhatsApp';
|
||||
a.title='Abrir conversa no WhatsApp';a.prepend(icon(ICON_CHAT));
|
||||
line.append(a);return line;
|
||||
}
|
||||
function node(tag,text,className){const e=document.createElement(tag);if(text!==undefined&&text!==null)e.textContent=text;if(className)e.className=className;return e;}
|
||||
@@ -80,6 +80,14 @@ function icon(path){
|
||||
return svg;
|
||||
}
|
||||
const ICON_OK=['M5 12l4 4 10-10'], ICON_WARN=['M12 8v5M12 17h.01','circle 12 12 9'];
|
||||
// Lucide shapes (ISC licence), in the same stroke as the two above.
|
||||
const ICON_TRUCK=['M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2','M15 18H9',
|
||||
'M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14','circle 17 18 2','circle 7 18 2'];
|
||||
const ICON_PIN=['M20 10c0 4.99-5.54 10.19-7.4 11.8a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0','circle 12 10 3'];
|
||||
const ICON_DOWNLOAD=['M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4','m7 10 5 5 5-5','M12 15V3'];
|
||||
const ICON_CHAT=['M7.9 20A9 9 0 1 0 4 16.1L2 22Z'];
|
||||
// A download button: the arrow, then its label.
|
||||
function downloadButton(text,fn,className){const b=button(text,fn,className);b.prepend(icon(ICON_DOWNLOAD));b.classList.add('with-icon');return b;}
|
||||
async function api(path,body){
|
||||
const r=await fetch('/api/operator'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});
|
||||
const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos ('+r.status+').');error.status=r.status;throw error;}return d;
|
||||
@@ -200,7 +208,8 @@ function card(order){
|
||||
const status=node('span',p.text,p.tone);status.style.display='flex';status.style.alignItems='center';status.style.gap='5px';
|
||||
if(p.tone==='ok')status.prepend(icon(ICON_OK));if(p.tone==='warn')status.prepend(icon(ICON_WARN));
|
||||
const dest=order.snapshot.destination;
|
||||
foot.append(status,node('span',dest?'Entrega · '+dest.state:'Retirada','where'));
|
||||
const where=node('span',dest?'Entrega · '+dest.state:'Retirada','where');where.prepend(icon(dest?ICON_TRUCK:ICON_PIN));
|
||||
foot.append(status,where);
|
||||
c.append(top,node('div',order.snapshot.customer.mail,'cust'),tags,foot);
|
||||
if(['rec','tra','fil','imp'].includes(order.state)&&Date.now()-new Date(order.updated_at)>LATE_HOURS*3600000)
|
||||
c.append(node('div','Parado há mais de '+LATE_HOURS+' h','late'));
|
||||
@@ -336,8 +345,8 @@ function itemCard(order,item,index){
|
||||
else body.append(file);
|
||||
}else body.append(node('div','Arquivo de impressão ainda não gerado','line'));
|
||||
const buttons=node('div',undefined,'buttons');
|
||||
if(row?.status==='ready'&&row.detail?.source!=='original')buttons.append(button('Baixar PDF',download(row.upload_id)));
|
||||
item.uploads.forEach((uid,i)=>buttons.append(button(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||||
if(row?.status==='ready'&&row.detail?.source!=='original')buttons.append(downloadButton('Baixar PDF',download(row.upload_id)));
|
||||
item.uploads.forEach((uid,i)=>buttons.append(downloadButton(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||||
if(spec?.placements)buttons.append(button('Manifesto',()=>manifest(spec),'btn ghost'));
|
||||
if(['rec','tra'].includes(order.state)&&(!row||row.status==='manual'||row.status==='failed'))
|
||||
buttons.append(button('Gerar novamente',async()=>{await api('/orders/'+order.id+'/print-files',{});await load();},'btn ghost'));
|
||||
@@ -350,7 +359,7 @@ function finalsSection(order,revisions,section){
|
||||
const rev=node('div',undefined,'rev');
|
||||
rev.append(node('b',(file.kind==='final'?'Final':'Correção do cliente')+' · item '+(file.item_index+1)+' · '+file.name),
|
||||
node('span',(file.expired?'expirado':file.active?'atual':'substituído')+' · '+when(file.created_at)+' · '+file.note,'faint'));
|
||||
if(!file.expired){const b=button('Baixar',download(file.upload_id),'btn ghost');b.style.alignSelf='flex-start';rev.append(b);}
|
||||
if(!file.expired){const b=downloadButton('Baixar',download(file.upload_id),'btn ghost');b.style.alignSelf='flex-start';rev.append(b);}
|
||||
section.append(rev);
|
||||
}
|
||||
if(!['rec','tra','cor'].includes(order.state)){if(!revisions.length)section.append(node('p','Nenhum arquivo final registrado.','faint'));return;}
|
||||
@@ -504,7 +513,7 @@ function quoteItem(item,fields){
|
||||
grid.append(asked,m,g);body.append(grid);
|
||||
}
|
||||
const buttons=node('div',undefined,'buttons');
|
||||
item.uploads.forEach((uid,i)=>buttons.append(button(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||||
item.uploads.forEach((uid,i)=>buttons.append(downloadButton(item.uploads.length>1?'Original '+(i+1):'Baixar original',download(uid),'btn ghost')));
|
||||
if(item.production?.placements)buttons.append(button('Manifesto',()=>manifest(item.production),'btn ghost'));
|
||||
body.append(buttons);box.append(preview,body);return box;
|
||||
}
|
||||
|
||||
152
web/portal.css
Normal file
152
web/portal.css
Normal file
@@ -0,0 +1,152 @@
|
||||
/* Minha conta: the Site's look (tokens and shell from site-shell.css and
|
||||
site-v2.css) for sign-in, the overview, the orders and the details. */
|
||||
[hidden]{display:none!important}
|
||||
body{background:var(--cinza)}
|
||||
.topo .acts a[aria-current="page"]{color:var(--laranja-escuro)}
|
||||
#quem{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
||||
.ped{padding-top:28px;padding-bottom:64px;min-height:70vh}
|
||||
.ped h1{font-size:30px;font-weight:800;margin-bottom:14px}
|
||||
.pedAviso{margin-bottom:14px;padding:12px 16px;border-radius:var(--raio2);background:var(--aviso-fundo);border:1px solid var(--aviso-linha);font-size:15px}
|
||||
.pedMsg{margin-bottom:14px;padding:10px 14px;border-radius:var(--raio2);background:var(--verde-fundo);color:var(--verde);font-size:15px}
|
||||
.pedMsg.erro{background:#FDE8E6;color:#B42318}
|
||||
.pedMsg:empty{display:none}
|
||||
|
||||
/* Buttons, as on the Site. */
|
||||
.bPri{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:48px;padding:0 22px;border:0;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;text-decoration:none;cursor:pointer}
|
||||
.bPri:hover:not(:disabled){background:#FFB53D}
|
||||
.bPri.bMini{height:38px;padding:0 16px;font-size:15px}
|
||||
.bSec{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:40px;padding:0 16px;border:1px solid #D6D6D6;border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;cursor:pointer}
|
||||
.bSec:hover:not(:disabled){border-color:var(--preto)}
|
||||
.bSec .ic{width:18px;height:18px}
|
||||
.bLink{align-self:flex-start;margin-top:12px;padding:0;border:0;background:none;color:var(--laranja-escuro);font-family:inherit;font-weight:600;font-size:15px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
|
||||
button:disabled{opacity:.6;cursor:default}
|
||||
|
||||
/* Forms. */
|
||||
.acessoGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}
|
||||
.bloco{display:flex;flex-direction:column;margin-bottom:16px;padding:22px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff}
|
||||
.bloco h2{font-size:20px;font-weight:800}
|
||||
.nota2{margin:4px 0 8px;font-size:14.5px;color:var(--fraco)}
|
||||
.nota2 a{color:var(--laranja-escuro)}
|
||||
.bloco label{margin:10px 0 5px;font-size:14px;font-weight:600;color:var(--texto2)}
|
||||
.bloco label .obs{font-weight:400;color:var(--fraco)}
|
||||
.bloco input,.busca2 input,.busca2 select,.correcaoForm textarea{width:100%;height:46px;padding:0 13px;border:1px solid #D6D6D6;border-radius:var(--raio2);font:inherit;font-size:16px;background:#fff;color:var(--preto)}
|
||||
.bloco input[readonly]{background:var(--cinza);color:var(--texto2)}
|
||||
.bloco input:focus,.busca2 input:focus,.busca2 select:focus,.correcaoForm textarea:focus{outline:none;border-color:var(--laranja);box-shadow:0 0 0 3px rgba(255,168,26,.2)}
|
||||
.bloco>.bPri,.bloco>.bSec{margin-top:16px;align-self:flex-start}
|
||||
.duas{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}
|
||||
.tres{display:grid;grid-template-columns:1fr 1fr 90px;gap:0 12px}
|
||||
.duas>div,.tres>div{display:flex;flex-direction:column}
|
||||
|
||||
/* The account area: menu on the side, one view beside it. */
|
||||
.area{display:grid;grid-template-columns:210px minmax(0,1fr);gap:28px;align-items:start}
|
||||
.lateral{position:sticky;top:90px;display:flex;flex-direction:column;gap:2px;padding:10px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff}
|
||||
.lateral a,.lateral button{display:block;padding:10px 12px;border:0;border-radius:var(--raio2);background:none;color:var(--texto2);font:inherit;font-size:15.5px;text-align:left;text-decoration:none;cursor:pointer}
|
||||
.lateral a:hover,.lateral button:hover{background:var(--cinza);color:var(--preto)}
|
||||
.lateral a.on{background:var(--aviso-fundo);color:var(--laranja-escuro);font-weight:700}
|
||||
.lateral button{margin-top:6px;border-top:1px solid var(--linha);border-radius:0 0 var(--raio2) var(--raio2)}
|
||||
.conta{margin:-8px 0 16px;font-size:15px;color:var(--fraco)}
|
||||
.acessoIntro{margin:-6px 0 14px;font-size:15px;color:var(--texto2)}
|
||||
.acessoIntro:empty{display:none}
|
||||
.tituloGrupo{margin:22px 0 10px;font-size:18px;font-weight:800}
|
||||
|
||||
/* Overview. */
|
||||
.metricas{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
|
||||
.metrica{display:flex;flex-direction:column;gap:4px;padding:16px 18px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff;color:var(--texto2);font-size:14.5px;text-decoration:none}
|
||||
.metrica:hover{border-color:var(--laranja)}
|
||||
.metrica b{font-family:"Inter",sans-serif;font-size:28px;font-weight:800;color:var(--preto)}
|
||||
.metrica.alerta{border-color:#F3B7A9;background:#FFF6F4}
|
||||
.verTodos{display:inline-block;margin-top:12px;color:var(--laranja-escuro);font-weight:700;text-decoration:none}
|
||||
|
||||
/* Orders: filters, rows, pages. */
|
||||
.filtros{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
|
||||
.chipF{height:36px;padding:0 14px;border:1px solid #D6D6D6;border-radius:999px;background:#fff;color:var(--texto2);font:inherit;font-size:14.5px;font-weight:600;cursor:pointer}
|
||||
.chipF:hover{border-color:var(--preto)}
|
||||
.chipF.on{border-color:var(--laranja);background:var(--aviso-fundo);color:var(--laranja-escuro)}
|
||||
.busca2{display:grid;grid-template-columns:minmax(0,1fr) 200px;gap:10px;margin-bottom:12px}
|
||||
.lista{border:1px solid var(--linha);border-radius:var(--raio);background:#fff;overflow:hidden}
|
||||
.lista:empty{display:none}
|
||||
.item+.item{border-top:1px solid var(--linha)}
|
||||
.item.pendente{background:#FFFCF6}
|
||||
.linha{display:grid;grid-template-columns:90px minmax(0,1fr) auto 110px 90px;gap:14px;align-items:center;padding:14px 18px;cursor:pointer}
|
||||
.item.pendente .linha{cursor:default}
|
||||
.linha:hover{background:var(--cinza)}
|
||||
.linha .num{font-family:"Inter",sans-serif;font-size:16px}
|
||||
.oque{display:flex;flex-direction:column;min-width:0}
|
||||
.oque span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px}
|
||||
.oque .data{font-size:14px;color:var(--fraco)}
|
||||
.estadoW{display:flex;align-items:center;gap:6px}
|
||||
.estado{padding:4px 10px;border-radius:999px;font-family:"Inter",sans-serif;font-size:13.5px;font-weight:700;background:var(--cinza);color:var(--texto2);white-space:nowrap}
|
||||
.via{font-size:13px;font-weight:700;color:var(--fraco)}
|
||||
.est-fin{background:var(--verde-fundo);color:var(--verde)}
|
||||
.est-cor{background:#FDE8E6;color:#B42318}
|
||||
.est-pay{background:#FFF1D6;color:#8A4B00}
|
||||
.est-rev{background:#EEF1F5;color:#45505E}
|
||||
.est-imp,.est-fil,.est-tra,.est-rec{background:#E7F0FB;color:#1D4E89}
|
||||
.linha .valor{font-family:"Inter",sans-serif;font-size:16px;text-align:right}
|
||||
.acao{display:flex;justify-content:flex-end}
|
||||
.acao .bSec{height:36px;padding:0 14px}
|
||||
.paginas{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:12px}
|
||||
.pags{display:flex;gap:6px}
|
||||
.pag{min-width:38px;height:38px;border:1px solid #D6D6D6;border-radius:var(--raio2);background:#fff;font:inherit;font-weight:700;cursor:pointer}
|
||||
.pag.on{border-color:var(--laranja);background:var(--aviso-fundo);color:var(--laranja-escuro);cursor:default}
|
||||
.vazio{display:flex;flex-direction:column;align-items:flex-start;gap:12px;padding:26px 22px;font-size:16px;color:var(--texto2)}
|
||||
|
||||
/* An order opened in the list. */
|
||||
.detalhe{padding:4px 18px 18px;border-top:1px dashed var(--linha);background:#fff}
|
||||
.trilha{display:flex;list-style:none;margin:18px 0 6px;padding:0}
|
||||
.trilha li{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;font-size:14px;color:var(--fraco)}
|
||||
.trilha li::before{content:"";position:absolute;top:14px;left:-50%;width:100%;height:2px;background:#E0E0E0;z-index:0}
|
||||
.trilha li:first-child::before{display:none}
|
||||
.trilha li.feito::before,.trilha li.atual::before{background:var(--verde)}
|
||||
.trilha .marca{position:relative;z-index:1;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:2px solid #D0D0D0;background:#fff;font-family:"Inter",sans-serif;font-weight:700;font-size:14px}
|
||||
.trilha .marca .ic{width:16px;height:16px;stroke-width:3}
|
||||
.trilha li.feito{color:var(--verde)}
|
||||
.trilha li.feito .marca{background:var(--verde-fundo);border-color:var(--verde-fundo);color:var(--verde)}
|
||||
.trilha li.atual{color:var(--preto)}
|
||||
.trilha li.atual .marca{background:var(--laranja);border-color:var(--laranja)}
|
||||
.trilha b{font-weight:600}
|
||||
.trilha li.atual b{font-weight:800}
|
||||
.pedAlerta{display:flex;gap:10px;align-items:flex-start;margin:16px 0 4px;padding:12px 14px;border-radius:var(--raio2);background:#FDE8E6;color:#7A1C10;font-size:15px}
|
||||
.pedAlerta .ic{flex:none;width:20px;height:20px;margin-top:1px;color:#B42318}
|
||||
.itensPed{list-style:none;margin:14px 0 0;padding:0;border-top:1px solid var(--linha)}
|
||||
.itensPed li{display:flex;gap:12px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--linha);font-size:15.5px}
|
||||
.itensPed .nome{flex:1;font-weight:600}
|
||||
.itensPed .qt{color:var(--fraco)}
|
||||
.itensPed b{min-width:96px;text-align:right;font-family:"Inter",sans-serif}
|
||||
.itensPed .frete .nome{font-weight:400;color:var(--texto2)}
|
||||
.entregaPed{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:15px;color:var(--texto2)}
|
||||
.entregaPed .ic{flex:none;width:19px;height:19px;color:var(--laranja-escuro)}
|
||||
.secao{margin-top:16px}
|
||||
.secao h4{margin-bottom:8px;font-size:16px;font-weight:800}
|
||||
.historico{list-style:none;margin:0;padding:0 0 0 14px;border-left:2px solid var(--linha)}
|
||||
.historico li{position:relative;padding:0 0 12px 12px;font-size:15px}
|
||||
.historico li::before{content:"";position:absolute;left:-20px;top:6px;width:10px;height:10px;border-radius:50%;background:var(--laranja)}
|
||||
.historico li b{margin-right:8px}
|
||||
.historico li span{color:var(--fraco)}
|
||||
.historico .motivo{margin-top:4px;padding:8px 10px;border-radius:8px;background:#FDE8E6;font-size:14.5px}
|
||||
.arquivo{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--linha)}
|
||||
.arquivo>div{flex:1;min-width:0;display:flex;flex-direction:column}
|
||||
.arquivo b{overflow-wrap:anywhere;font-size:15px}
|
||||
.arquivo span{font-size:14px;color:var(--fraco)}
|
||||
.correcaoForm{display:flex;flex-direction:column;padding:16px;border-radius:var(--raio2);background:var(--cinza)}
|
||||
.correcaoForm label{margin:10px 0 5px;font-size:14px;font-weight:600;color:var(--texto2)}
|
||||
.correcaoForm textarea{height:auto;padding:10px 13px;resize:vertical}
|
||||
.correcaoForm .bPri{margin-top:14px;align-self:flex-start}
|
||||
|
||||
@media (max-width:820px){
|
||||
.area{grid-template-columns:1fr;gap:16px}
|
||||
.lateral{position:static;flex-direction:row;flex-wrap:wrap}
|
||||
.lateral button{margin:0;border:0;border-radius:var(--raio2)}
|
||||
.linha{grid-template-columns:minmax(0,1fr) auto;gap:6px 12px}
|
||||
.linha .num{grid-column:1}
|
||||
.linha .oque{grid-column:1/-1;grid-row:2}
|
||||
.linha .estadoW{grid-column:2;grid-row:1;justify-self:end}
|
||||
.linha .valor{grid-column:1;grid-row:3;text-align:left}
|
||||
.linha .acao{grid-column:2;grid-row:3}
|
||||
}
|
||||
@media (max-width:600px){
|
||||
.ped h1{font-size:26px}
|
||||
.trilha b{font-size:12.5px}
|
||||
.duas,.tres,.busca2{grid-template-columns:1fr}
|
||||
}
|
||||
180
web/portal.html
180
web/portal.html
@@ -1,11 +1,169 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Dropstar · Meus pedidos DTF</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;background:#f6f7f9;color:#20252d;font:15px/1.5 system-ui,sans-serif}header{background:white;border-bottom:1px solid #ddd;padding:20px max(20px,calc((100% - 1100px)/2));display:flex;gap:24px;align-items:center;flex-wrap:wrap}.brand{color:#ef8500;font-size:24px;font-style:italic;font-weight:900}a{color:#8a4e00}main{max-width:1100px;margin:24px auto;padding:0 20px}h1{font-size:28px}h2{font-size:21px}h3{font-size:17px}.notice{background:#fff1d4;border-left:4px solid #ffa900;padding:12px 16px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px}.card{background:white;border:1px solid #ddd;border-radius:10px;padding:20px;margin:16px 0}.card p{overflow-wrap:anywhere}.muted{color:#596471}label{display:block;margin:10px 0}input,textarea,button{font:inherit;border:1px solid #9aa0a8;border-radius:6px;padding:10px}input:not([type=file]),textarea{width:100%}button{background:#ffa900;border-color:#ffa900;cursor:pointer}button:disabled{opacity:.6}form{margin:10px 0}.actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center}#message{min-height:24px;color:#9b3800}li{margin:8px 0}.tag{background:#eef2f6;padding:5px 10px;border-radius:6px}details{margin:12px 0}[hidden]{display:none!important}
|
||||
</style></head><body>
|
||||
<header><a class="brand" href="/">DROPSTAR</a><a href="/">Enviar arte</a><span>Minha conta · DTF</span><button id="logout" hidden>Sair</button></header>
|
||||
<main><h1>Meus pedidos DTF</h1><p class="notice" id="dev-notice" hidden>Ambiente de desenvolvimento local. Pagamentos e notificações são simulados. Use apenas dados de teste.</p><p id="message" role="status"></p>
|
||||
<div id="account"></div><section id="auth" class="grid">
|
||||
<form id="login" class="card"><h2>Entrar</h2><label>E-mail <input id="email" type="email" autocomplete="username" required></label><label>Senha <input id="password" type="password" autocomplete="current-password" required></label><button>Entrar</button></form>
|
||||
<form id="register" class="card"><h2>Criar conta</h2><label>CNPJ <input id="cnpj" required></label><label>WhatsApp <input id="phone" required></label><label>E-mail <input id="register-email" type="email" autocomplete="email" required></label><label>Senha (12 caracteres ou mais) <input id="register-password" type="password" minlength="12" maxlength="128" autocomplete="new-password" required></label><button>Criar conta</button><p class="muted">Pedidos desta sessão serão associados à sua conta. Guarde sua senha: a recuperação por e-mail ainda não está disponível.</p></form>
|
||||
</section><div class="actions"><h2>Acompanhamento</h2><button id="refresh">Atualizar pedidos</button></div><p id="guest" class="muted"></p><section id="quotes"></section><section id="orders"></section></main>
|
||||
<script src="/privacy.js"></script><script src="/upload.js"></script><script src="/portal.js"></script></body></html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minha conta · DTF Dropstar</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/site-shell.css">
|
||||
<link rel="stylesheet" href="/site-v2.css">
|
||||
<link rel="stylesheet" href="/portal.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="tarja">
|
||||
ENVIE SEU ARQUIVO DTF PELO SITE · DTF TÊXTIL COM O MENOR PREÇO DO METRO EM FRANCA-SP
|
||||
</div>
|
||||
|
||||
<header class="topo"><div class="w">
|
||||
<a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a>
|
||||
<div class="acts">
|
||||
<a href="/conta" aria-current="page"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx" id="quem">Minha conta</span></a>
|
||||
<a href="/carrinho"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><span class="actTx">Carrinho</span></a>
|
||||
</div>
|
||||
</div></header>
|
||||
|
||||
<main class="ped w">
|
||||
<p class="pedAviso" id="dev-notice" hidden>Ambiente de desenvolvimento local. Pagamentos e notificações são simulados. Use apenas dados de teste.</p>
|
||||
<p class="pedMsg" id="message" role="status"></p>
|
||||
|
||||
<!-- /conta/entrar -->
|
||||
<section class="acesso" id="auth" hidden aria-labelledby="tEntrar">
|
||||
<h1 id="tEntrar">Minha conta</h1>
|
||||
<div class="acessoGrid">
|
||||
<form class="bloco" id="login">
|
||||
<h2>Entrar</h2>
|
||||
<p class="nota2">Para ver seus pedidos e seus dados.</p>
|
||||
<label for="email">E-mail</label>
|
||||
<input id="email" type="email" autocomplete="username" required>
|
||||
<label for="password">Senha</label>
|
||||
<input id="password" type="password" autocomplete="current-password" required>
|
||||
<button class="bPri">Entrar</button>
|
||||
<button type="button" class="bLink" id="esqueci">Esqueci minha senha</button>
|
||||
<p class="nota2" id="esqueciTx" hidden>A recuperação por e-mail chega em breve. Enquanto isso, fale com a Dropstar pelo WhatsApp <a href="https://wa.me/message/NJ76CLIOUGXLB1" target="_blank" rel="noopener">16 99428-0056</a>.</p>
|
||||
</form>
|
||||
<form class="bloco" id="register">
|
||||
<h2>Criar conta</h2>
|
||||
<p class="nota2">Os pedidos feitos neste navegador passam para a sua conta.</p>
|
||||
<div class="duas">
|
||||
<div><label for="cnpj">CNPJ</label><input id="cnpj" inputmode="numeric" autocomplete="off" required></div>
|
||||
<div><label for="phone">WhatsApp</label><input id="phone" inputmode="numeric" autocomplete="tel" required></div>
|
||||
</div>
|
||||
<label for="register-email">E-mail</label>
|
||||
<input id="register-email" type="email" autocomplete="email" required>
|
||||
<label for="register-password">Senha <span class="obs">12 caracteres ou mais</span></label>
|
||||
<input id="register-password" type="password" minlength="12" maxlength="128" autocomplete="new-password" required>
|
||||
<button class="bPri">Criar conta</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- /conta, /conta/pedidos, /conta/dados -->
|
||||
<div class="area" id="area" hidden>
|
||||
<nav class="lateral" aria-label="Minha conta">
|
||||
<a href="/conta" data-view="geral" data-conta>Visão geral</a>
|
||||
<a href="/conta/pedidos" data-view="pedidos">Pedidos</a>
|
||||
<a href="/conta/dados" data-view="dados" data-conta>Dados da conta</a>
|
||||
<a href="/conta/entrar" data-view="entrar" id="navEntrar">Entrar</a>
|
||||
<button type="button" id="logout" hidden>Sair</button>
|
||||
</nav>
|
||||
|
||||
<section class="vista" id="vGeral" hidden aria-labelledby="tGeral">
|
||||
<h1 id="tGeral">Olá</h1>
|
||||
<p class="conta" id="account"></p>
|
||||
<div class="metricas" id="metricas"></div>
|
||||
<h2 class="tituloGrupo">Último pedido</h2>
|
||||
<div class="lista" id="ultimo"></div>
|
||||
<a class="verTodos" href="/conta/pedidos" data-view="pedidos">Ver todos os pedidos ›</a>
|
||||
</section>
|
||||
|
||||
<section class="vista" id="vPedidos" hidden aria-labelledby="tPedidos">
|
||||
<h1 id="tPedidos">Pedidos</h1>
|
||||
<p class="acessoIntro" id="guest"></p>
|
||||
<div class="filtros" id="filtros" role="group" aria-label="Filtrar pedidos"></div>
|
||||
<div class="busca2">
|
||||
<label class="sr" for="fNumero">Número do pedido</label>
|
||||
<input id="fNumero" inputmode="numeric" placeholder="Nº do pedido">
|
||||
<label class="sr" for="fPeriodo">Período</label>
|
||||
<select id="fPeriodo">
|
||||
<option value="30">Últimos 30 dias</option>
|
||||
<option value="90">Últimos 90 dias</option>
|
||||
<option value="365">Último ano</option>
|
||||
<option value="0" selected>Todo o período</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lista" id="orders"></div>
|
||||
<div class="paginas" id="paginas"></div>
|
||||
</section>
|
||||
|
||||
<section class="vista" id="vDados" hidden aria-labelledby="tDados">
|
||||
<h1 id="tDados">Dados da conta</h1>
|
||||
<form class="bloco" id="fPerfil">
|
||||
<h2>Empresa e entrega</h2>
|
||||
<div class="duas">
|
||||
<div><label for="dCnpj">CNPJ</label><input id="dCnpj" readonly aria-readonly="true"></div>
|
||||
<div><label for="dZap">WhatsApp</label><input id="dZap" inputmode="numeric" autocomplete="tel" required></div>
|
||||
</div>
|
||||
<p class="nota2">Endereço de entrega: preenche o carrinho quando você escolhe receber em casa. Deixe em branco para não guardar.</p>
|
||||
<div class="duas">
|
||||
<div><label for="dCep">CEP</label><input id="dCep" inputmode="numeric" maxlength="9" placeholder="00000-000"></div>
|
||||
<div><label for="dNome">Quem recebe</label><input id="dNome" maxlength="120"></div>
|
||||
</div>
|
||||
<label for="dRua">Rua / avenida</label><input id="dRua" maxlength="160">
|
||||
<div class="duas">
|
||||
<div><label for="dNum">Número</label><input id="dNum" maxlength="20"></div>
|
||||
<div><label for="dComp">Complemento (opcional)</label><input id="dComp" maxlength="80"></div>
|
||||
</div>
|
||||
<div class="tres">
|
||||
<div><label for="dBairro">Bairro</label><input id="dBairro" maxlength="80"></div>
|
||||
<div><label for="dCidade">Cidade</label><input id="dCidade" maxlength="80"></div>
|
||||
<div><label for="dUf">UF</label><input id="dUf" maxlength="2"></div>
|
||||
</div>
|
||||
<button class="bPri">Salvar</button>
|
||||
</form>
|
||||
<form class="bloco" id="fEmail">
|
||||
<h2>E-mail</h2>
|
||||
<p class="nota2">É o seu acesso e para onde vai a nota fiscal.</p>
|
||||
<label for="dMail">E-mail</label><input id="dMail" type="email" autocomplete="email" required>
|
||||
<label for="dMailSenha">Senha atual, para confirmar</label><input id="dMailSenha" type="password" autocomplete="current-password" required>
|
||||
<button class="bSec">Alterar e-mail</button>
|
||||
</form>
|
||||
<form class="bloco" id="fSenha">
|
||||
<h2>Senha</h2>
|
||||
<label for="dAtual">Senha atual</label><input id="dAtual" type="password" autocomplete="current-password" required>
|
||||
<label for="dNova">Nova senha <span class="obs">12 caracteres ou mais</span></label><input id="dNova" type="password" minlength="12" maxlength="128" autocomplete="new-password" required>
|
||||
<button class="bSec">Alterar senha</button>
|
||||
<p class="nota2">Os outros aparelhos conectados saem da conta.</p>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer><div class="w">
|
||||
<div class="frow">
|
||||
<span class="fmarca"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar">DTF</span>
|
||||
<div class="flinks">
|
||||
<a href="https://www.dropstaratacado.com.br" target="_blank" rel="noopener">Loja de atacado</a>
|
||||
<a href="https://www.dropstaratacado.com.br/p/termos" target="_blank" rel="noopener">Trocas e privacidade</a>
|
||||
<a href="https://www.dropstaratacado.com.br/p/atendimento" target="_blank" rel="noopener">Fale conosco</a>
|
||||
<a href="https://www.instagram.com/dropstar.ofc/" target="_blank" rel="noopener">Instagram</a>
|
||||
</div>
|
||||
<div class="fcont">
|
||||
<a href="https://wa.me/message/NJ76CLIOUGXLB1" target="_blank" rel="noopener">16 99428-0056</a>
|
||||
· arquivos 16 99230-2546<br>
|
||||
<a href="mailto:atendimentodropstar@gmail.com">atendimentodropstar@gmail.com</a><br>
|
||||
Segunda a sexta, 07h às 17h
|
||||
</div>
|
||||
</div>
|
||||
<div class="fbase">
|
||||
<span>© Dropstar Atacado · CNPJ 55.786.635/0001-58 · Franca-SP</span>
|
||||
<span>Visa · Master · Elo · Amex · Pix · Boleto</span>
|
||||
<span>têxtil 57 cm · UV 28,5 cm</span>
|
||||
</div>
|
||||
</div></footer>
|
||||
|
||||
<script src="/privacy.js"></script><script src="/upload.js"></script><script src="/portal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
388
web/portal.js
388
web/portal.js
@@ -1,62 +1,325 @@
|
||||
/* Minha conta: sign-in, the overview, the orders (filtered, a page at a time,
|
||||
with the cart waiting for payment among them) and the account's details.
|
||||
One page; the address says which part shows (/conta, /conta/entrar,
|
||||
/conta/pedidos, /conta/dados). */
|
||||
const $=id=>document.getElementById(id);
|
||||
const node=(tag,text)=>{const el=document.createElement(tag);if(text!==undefined)el.textContent=text;return el;};
|
||||
const node=(tag,text,className)=>{const el=document.createElement(tag);if(text!==undefined&&text!==null)el.textContent=text;if(className)el.className=className;return el;};
|
||||
const money=c=>(c/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
|
||||
let scope,states={},environment='';
|
||||
async function api(path,body){const r=await fetch('/api'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos informados.');error.status=r.status;throw error;}return d;}
|
||||
function button(text,fn){const b=node('button',text);b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){$('message').textContent=e.message;}finally{b.disabled=false;}};return b;}
|
||||
function forgetCheckout(){for(const k of ['dtf-quote','dtf-request-key','dtf-request-body'])localStorage.removeItem(k);}
|
||||
async function load(){
|
||||
try{
|
||||
const session=await api('/session');scope=session.cart_scope;environment=session.environment;
|
||||
$('dev-notice').hidden=environment!=='local';
|
||||
const account=await api('/account/me');
|
||||
$('auth').hidden=!!account.customer;$('logout').hidden=!account.customer;
|
||||
$('account').textContent=account.customer?'Conta: '+account.customer.mail:'';
|
||||
$('guest').textContent=account.customer?'':'Você está vendo apenas os pedidos desta sessão. Crie uma conta para acessar seus pedidos em outro navegador.';
|
||||
const data=await api('/customer/orders');states=data.states;
|
||||
$('quotes').replaceChildren();
|
||||
for(const quote of data.quotes){
|
||||
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.'));
|
||||
const link=node('a','Abrir cotação no Site');link.href='/pagamento?quote='+quote.id;card.append(link);$('quotes').append(card);
|
||||
}
|
||||
$('orders').replaceChildren();
|
||||
if(!data.orders.length)$('orders').append(node('p','Nenhum pedido pago nesta conta ou sessão.'));
|
||||
for(const order of data.orders){
|
||||
const card=node('article');card.className='card';card.id='order-'+order.id;
|
||||
card.append(node('h3','Pedido #'+order.number),node('p',states[order.state]+' · '+money(order.snapshot.total_cents)+(environment==='local'?' · pagamento de teste':'')));
|
||||
card.append(node('p',new Date(order.created_at).toLocaleString('pt-BR')));
|
||||
const content=node('div');
|
||||
card.append(button('Ver detalhes e arquivos',()=>details(order.id,content)),content);$('orders').append(card);
|
||||
if(new URLSearchParams(location.search).get('order')===order.id)await details(order.id,content);
|
||||
}
|
||||
}catch(error){$('message').textContent=error.message;}
|
||||
const quando=d=>new Date(d).toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||||
const dia=d=>new Date(d).toLocaleDateString('pt-BR',{day:'2-digit',month:'2-digit',year:'2-digit'});
|
||||
const metros=m=>Number(m).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2})+' m';
|
||||
const soDigitos=v=>String(v||'').replace(/\D/g,'');
|
||||
const fmtCnpj=v=>soDigitos(v).replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/,'$1.$2.$3/$4-$5');
|
||||
const fmtZap=v=>{const d=soDigitos(v);return d.length===11?'('+d.slice(0,2)+') '+d.slice(2,7)+'-'+d.slice(7):d.length===10?'('+d.slice(0,2)+') '+d.slice(2,6)+'-'+d.slice(6):v;};
|
||||
const fmtCep=v=>{const d=soDigitos(v).slice(0,8);return d.length>5?d.slice(0,5)+'-'+d.slice(5):d;};
|
||||
// Lucide shapes (ISC licence), the same as on the Site.
|
||||
const ICONES={
|
||||
baixar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
|
||||
local:'<path d="M20 10c0 4.99-5.54 10.19-7.4 11.8a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
|
||||
caminhao:'<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>',
|
||||
ok:'<path d="M20 6 9 17l-5-5"/>',
|
||||
alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>',
|
||||
};
|
||||
function icone(nome){
|
||||
const span=document.createElement('span');
|
||||
span.innerHTML='<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[nome]+'</svg>';
|
||||
return span.firstChild;
|
||||
}
|
||||
async function details(id,container){
|
||||
const PRODUTO={file:'DTF Têxtil · folha montada',avulsa:'DTF Têxtil · artes avulsas',
|
||||
uvfile:'DTF UV · folha montada',uv:'DTF UV · artes avulsas'};
|
||||
const TRILHA=['rec','tra','fil','imp','fin'];
|
||||
const TAMANHO=10;
|
||||
let scope,states={},environment='',conta=null;
|
||||
let filtro={status:'all',number:'',days:0,page:1};
|
||||
|
||||
async function api(path,body){const r=await fetch('/api'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos informados.');error.status=r.status;throw error;}return d;}
|
||||
const aviso=(text,erro)=>{$('message').textContent=text||'';$('message').classList.toggle('erro',!!erro);};
|
||||
function button(text,fn,className='bSec'){const b=node('button',text,className);b.type='button';b.onclick=async event=>{event?.stopPropagation?.();b.disabled=true;try{await fn();}catch(e){aviso(e.message,true);}finally{b.disabled=false;}};return b;}
|
||||
function forgetCheckout(){for(const k of ['dtf-quote','dtf-request-key','dtf-request-body'])localStorage.removeItem(k);}
|
||||
|
||||
// ---- Addresses -----------------------------------------------------------
|
||||
|
||||
function vista(){
|
||||
const p=location.pathname.replace(/\/+$/,'');
|
||||
if(p==='/conta/entrar') return 'entrar';
|
||||
if(p==='/conta/dados') return 'dados';
|
||||
if(p==='/conta') return 'geral';
|
||||
return 'pedidos'; // /conta/pedidos, and the old /portal.html
|
||||
}
|
||||
function ir(caminho){history.pushState({},'',caminho);mostra().catch(e=>aviso(e.message,true));}
|
||||
window.addEventListener('popstate',()=>mostra().catch(e=>aviso(e.message,true)));
|
||||
document.addEventListener('click',event=>{
|
||||
const a=event.target.closest('a[data-view]');
|
||||
if(!a||event.metaKey||event.ctrlKey||event.shiftKey)return;
|
||||
event.preventDefault();ir(a.getAttribute('href'));
|
||||
});
|
||||
|
||||
async function mostra(){
|
||||
aviso('');
|
||||
const v=vista(), proximo=new URLSearchParams(location.search).get('next');
|
||||
// Overview and details are the account's; orders also work for a guest.
|
||||
if((v==='geral'||v==='dados')&&!conta){history.replaceState({},'','/conta/entrar?next='+encodeURIComponent(location.pathname));return mostra();}
|
||||
if(v==='entrar'&&conta){history.replaceState({},'',proximo&&/^\/conta(\/[a-z]+)?$/.test(proximo)?proximo:'/conta');return mostra();}
|
||||
$('auth').hidden=v!=='entrar';
|
||||
$('area').hidden=v==='entrar';
|
||||
$('vGeral').hidden=v!=='geral';$('vPedidos').hidden=v!=='pedidos';$('vDados').hidden=v!=='dados';
|
||||
for(const a of document.querySelectorAll('.lateral a'))a.classList.toggle('on',a.dataset.view===v);
|
||||
for(const a of document.querySelectorAll('.lateral [data-conta]'))a.hidden=!conta;
|
||||
$('navEntrar').hidden=!!conta;$('logout').hidden=!conta;
|
||||
document.title=({entrar:'Entrar',geral:'Minha conta',pedidos:'Pedidos',dados:'Dados da conta'})[v]+' · DTF Dropstar';
|
||||
if(v==='geral')await geral();
|
||||
else if(v==='pedidos')await pedidos();
|
||||
else if(v==='dados')dados();
|
||||
}
|
||||
|
||||
// ---- Orders --------------------------------------------------------------
|
||||
|
||||
function trilha(state){
|
||||
const ol=node('ol',null,'trilha');
|
||||
const atual=TRILHA.indexOf(state);
|
||||
TRILHA.forEach((s,i)=>{
|
||||
const feito=i<atual||state==='fin';
|
||||
const li=node('li',null,feito?'feito':i===atual?'atual':'');
|
||||
const marca=node('span',null,'marca');
|
||||
if(feito)marca.append(icone('ok'));else marca.textContent=String(i+1);
|
||||
li.append(marca,node('b',states[s]||s));ol.append(li);
|
||||
});
|
||||
return ol;
|
||||
}
|
||||
function estado(e){
|
||||
if(e.kind==='quote')return e.state==='review'?['Em conferência','est-rev']:['Aguardando pagamento','est-pay'];
|
||||
return [states[e.state]||e.state,'est-'+e.state];
|
||||
}
|
||||
function resumoItens(items){
|
||||
if(!items.length)return '';
|
||||
const total=items.reduce((t,i)=>t+Number(i.billed_metres||0),0);
|
||||
return (items.length===1?PRODUTO[items[0].mode]||items[0].mode:items.length+' itens')+(total?' · '+metros(total):'');
|
||||
}
|
||||
function linha(e){
|
||||
const row=node('div',null,'linha');
|
||||
const [rotulo,cls]=estado(e);
|
||||
const num=node('b',e.kind==='quote'?'Carrinho':'#'+e.number,'num');
|
||||
const oque=node('div',null,'oque');oque.append(node('span',resumoItens(e.items)),node('span',dia(e.created_at),'data'));
|
||||
const st=node('span',null,'estadoW');st.append(node('span',rotulo,'estado '+cls));
|
||||
if(e.kind==='quote'&&e.pay_with==='pix')st.append(node('span','PIX','via'));
|
||||
const valor=node('b',e.total_cents!=null?money(e.total_cents):'—','valor');
|
||||
const acao=node('div',null,'acao');
|
||||
row.append(num,oque,st,valor,acao);
|
||||
const wrap=node('div',null,'item'+(e.kind==='quote'?' pendente':'')+(e.state==='cor'?' correcao':''));
|
||||
wrap.append(row);
|
||||
if(e.kind==='quote'){
|
||||
if(e.state==='pay'){
|
||||
const pagar=node('a','Pagar','bPri bMini');
|
||||
pagar.href=(e.pay_with==='pix'?'/pagamento/pix':'/pagamento')+'?quote='+e.id;
|
||||
acao.append(pagar);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
wrap.id='order-'+e.id;
|
||||
const detalhe=node('div',null,'detalhe');detalhe.hidden=true;
|
||||
const alterna=async()=>{
|
||||
if(!detalhe.hidden){detalhe.hidden=true;ver.textContent='Ver';return;}
|
||||
await details(e.id,detalhe,e.created_at);detalhe.hidden=false;ver.textContent='Fechar';
|
||||
};
|
||||
const ver=button('Ver',alterna);
|
||||
acao.append(ver);
|
||||
row.addEventListener('click',event=>{if(!event.target.closest('button,a'))ver.click();});
|
||||
wrap.append(detalhe);
|
||||
wrap.abrir=async()=>{if(detalhe.hidden)await alterna();};
|
||||
return wrap;
|
||||
}
|
||||
function filtros(counts,groups){
|
||||
const box=$('filtros');box.replaceChildren();
|
||||
const todos=Object.values(counts).reduce((a,b)=>a+b,0);
|
||||
for(const [key,label] of [['all','Todos'],...Object.entries(groups)]){
|
||||
const n=key==='all'?todos:counts[key];
|
||||
const chip=node('button',label+(n?' · '+n:''),'chipF'+(filtro.status===key?' on':''));chip.type='button';
|
||||
chip.setAttribute('aria-pressed',String(filtro.status===key));
|
||||
chip.onclick=()=>{filtro={...filtro,status:key,page:1};pedidos().catch(x=>aviso(x.message,true));};
|
||||
box.append(chip);
|
||||
}
|
||||
}
|
||||
function paginas(total){
|
||||
const box=$('paginas');box.replaceChildren();
|
||||
const n=Math.ceil(total/TAMANHO);
|
||||
if(n<=1){if(total)box.append(node('span',total+(total===1?' pedido':' pedidos'),'nota2'));return;}
|
||||
const de=(filtro.page-1)*TAMANHO+1, ate=Math.min(total,filtro.page*TAMANHO);
|
||||
box.append(node('span',de+'–'+ate+' de '+total+' pedidos','nota2'));
|
||||
const nav=node('div',null,'pags');
|
||||
const vai=p=>{filtro.page=p;pedidos().then(()=>$('vPedidos').scrollIntoView({block:'start'})).catch(x=>aviso(x.message,true));};
|
||||
const b=(t,p,label)=>{const x=node('button',t,'pag'+(p===filtro.page?' on':''));x.type='button';if(label)x.setAttribute('aria-label',label);
|
||||
if(p===filtro.page)x.setAttribute('aria-current','page');else if(p<1||p>n)x.disabled=true;else x.onclick=()=>vai(p);nav.append(x);};
|
||||
b('‹',filtro.page-1,'Página anterior');
|
||||
for(let p=Math.max(1,filtro.page-2);p<=Math.min(n,filtro.page+2);p++)b(String(p),p);
|
||||
b('›',filtro.page+1,'Próxima página');
|
||||
box.append(nav);
|
||||
}
|
||||
async function pedidos(){
|
||||
$('guest').textContent=conta?'':'Você está vendo os pedidos feitos neste navegador. Entre ou crie uma conta para vê-los em qualquer lugar.';
|
||||
const q=new URLSearchParams({status:filtro.status,days:String(filtro.days),page:String(filtro.page),size:String(TAMANHO)});
|
||||
if(filtro.number)q.set('number',filtro.number);
|
||||
const data=await api('/customer/orders?'+q);states=data.states;
|
||||
filtros(data.counts,data.groups);
|
||||
const lista=$('orders');lista.replaceChildren();
|
||||
if(!data.items.length){
|
||||
const livre=filtro.status==='all'&&!filtro.number&&!filtro.days;
|
||||
const vazio=node('div',null,'vazio');
|
||||
vazio.append(node('p',livre?'Seus pedidos aparecem aqui.':'Nenhum pedido com esse filtro.'));
|
||||
if(livre){const a=node('a','Enviar uma arte','bPri');a.href='/#envio';vazio.append(a);}
|
||||
lista.append(vazio);
|
||||
}
|
||||
for(const e of data.items)lista.append(linha(e));
|
||||
paginas(data.total);
|
||||
const aberto=new URLSearchParams(location.search).get('order');
|
||||
if(aberto&&/^[0-9a-f-]{36}$/.test(aberto)){
|
||||
let alvo=$('order-'+aberto);
|
||||
if(!alvo){
|
||||
// Not on this page of the list: shown on its own, above it.
|
||||
try{const o=await api('/customer/orders/'+aberto);
|
||||
alvo=linha({kind:'order',id:o.id,number:o.number,state:o.state,total_cents:o.snapshot.total_cents,
|
||||
created_at:o.created_at,items:o.snapshot.items});lista.prepend(alvo);}catch(e){alvo=null;}
|
||||
}
|
||||
if(alvo){await alvo.abrir();alvo.scrollIntoView({block:'start'});}
|
||||
}
|
||||
}
|
||||
let buscaTimer;
|
||||
$('fNumero').addEventListener('input',e=>{e.target.value=soDigitos(e.target.value);clearTimeout(buscaTimer);
|
||||
buscaTimer=setTimeout(()=>{filtro={...filtro,number:e.target.value,page:1};pedidos().catch(x=>aviso(x.message,true));},350);});
|
||||
$('fPeriodo').addEventListener('change',e=>{filtro={...filtro,days:Number(e.target.value),page:1};pedidos().catch(x=>aviso(x.message,true));});
|
||||
|
||||
function itensPedido(snapshot){
|
||||
const ul=node('ul',null,'itensPed');
|
||||
for(const item of snapshot.items){
|
||||
const li=node('li');
|
||||
li.append(node('span',PRODUTO[item.mode]||item.mode,'nome'),node('span',metros(item.billed_metres),'qt'),node('b',money(item.total_cents)));
|
||||
ul.append(li);
|
||||
}
|
||||
const f=snapshot.freight;
|
||||
if(f&&f.total_cents){const li=node('li',null,'frete');li.append(node('span','Frete','nome'),node('span',f.days?'até '+f.days+' dias úteis':'','qt'),node('b',money(f.total_cents)));ul.append(li);}
|
||||
return ul;
|
||||
}
|
||||
function entrega(snapshot){
|
||||
const d=snapshot.destination, p=node('p',null,'entregaPed');
|
||||
p.append(icone(d?'caminhao':'local'),node('span',d?'Entrega pela Jadlog · '+d.street+', '+d.number+' · '+d.city+'/'+d.state:'Retirada em Franca · Av. Major Elias Motta, 1281'));
|
||||
return p;
|
||||
}
|
||||
async function details(id,container,criado){
|
||||
const order=await api('/customer/orders/'+id);container.replaceChildren();
|
||||
for(const [i,item] of order.snapshot.items.entries())container.append(node('p',(i+1)+'. '+item.mode+' · '+item.billed_metres+' m · nota '+item.grade+' · '+money(item.total_cents)));
|
||||
const history=node('ol');
|
||||
for(const h of order.history)history.append(node('li',new Date(h.created_at).toLocaleString('pt-BR')+' · '+states[h.to_state]+(h.reason?' — '+h.reason:'')));
|
||||
container.append(history);
|
||||
for(const file of order.files){
|
||||
const row=node('p',(file.kind==='final'?'Arquivo final':'Correção')+' · item '+(file.item_index+1)+' · '+file.name+' · '+(file.expired?'expirado':!file.active?'substituído':'disponível até '+new Date(file.expires_at).toLocaleDateString('pt-BR')));
|
||||
if(file.active&&!file.expired)row.append(button('Baixar',async()=>{const d=await api('/customer/orders/'+id+'/files/'+file.id+'/download');const link=node('a');link.href=d.url;link.download=d.name;link.referrerPolicy='no-referrer';link.click();}));
|
||||
container.append(row);
|
||||
if(order.state==='cor'){
|
||||
const p=node('p',null,'pedAlerta');p.append(icone('alerta'),node('span','A equipe precisa de uma correção na sua arte. Veja o motivo abaixo e envie o arquivo corrigido.'));
|
||||
container.append(p);
|
||||
}else container.append(trilha(order.state));
|
||||
container.append(itensPedido(order.snapshot),entrega(order.snapshot));
|
||||
const hist=node('div',null,'secao');hist.append(node('h4','Andamento'));
|
||||
const ol=node('ol',null,'historico');
|
||||
// Payment is where the order starts: the first step is not a movement.
|
||||
if(criado){const li=node('li');li.append(node('b',states.rec||'Arte recebida'),node('span',quando(criado)));ol.append(li);}
|
||||
for(const h of order.history){
|
||||
const li=node('li');li.append(node('b',states[h.to_state]),node('span',quando(h.created_at)));
|
||||
if(h.reason)li.append(node('p',h.reason,'motivo'));
|
||||
ol.append(li);
|
||||
}
|
||||
hist.append(ol);container.append(hist);
|
||||
if(order.files.length){
|
||||
const arq=node('div',null,'secao');arq.append(node('h4','Arquivos'));
|
||||
for(const file of order.files){
|
||||
const row=node('div',null,'arquivo');
|
||||
const info=node('div');
|
||||
info.append(node('b',file.name),node('span',(file.kind==='final'?'Arquivo final':'Correção')+' · item '+(file.item_index+1)+' · '+
|
||||
(file.expired?'expirado':!file.active?'substituído':'disponível até '+dia(file.expires_at))));
|
||||
row.append(info);
|
||||
if(file.active&&!file.expired){
|
||||
const b=button('Baixar',async()=>{const d=await api('/customer/orders/'+id+'/files/'+file.id+'/download');const link=node('a');link.href=d.url;link.download=d.name;link.referrerPolicy='no-referrer';link.click();});
|
||||
b.prepend(icone('baixar'));row.append(b);
|
||||
}
|
||||
arq.append(row);
|
||||
}
|
||||
container.append(arq);
|
||||
}
|
||||
if(order.state==='cor'){
|
||||
const form=node('form');form.append(node('h3','Enviar arte corrigida'));
|
||||
const inputs=order.snapshot.items.map((item,index)=>{const label=node('label','Item '+(index+1)+' · '+item.mode);const input=node('input');input.type='file';input.multiple=true;input.dataset.item=index;label.append(input);form.append(label);return input;});
|
||||
const note=node('textarea');note.placeholder='Descreva a correção';note.required=true;note.maxLength=1000;form.append(note);
|
||||
const submit=node('button','Enviar correção');submit.type='submit';form.append(submit);
|
||||
const form=node('form',null,'secao correcaoForm');form.append(node('h4','Enviar arte corrigida'));
|
||||
const inputs=order.snapshot.items.map((item,index)=>{
|
||||
const label=node('label',(index+1)+'. '+(PRODUTO[item.mode]||item.mode));
|
||||
const input=node('input');input.type='file';input.multiple=true;input.dataset.item=index;
|
||||
form.append(label,input);return input;
|
||||
});
|
||||
const nl=node('label','O que foi corrigido');const note=node('textarea');note.required=true;note.maxLength=1000;note.rows=3;
|
||||
form.append(nl,note);
|
||||
const submit=node('button','Enviar correção','bPri');submit.type='submit';form.append(submit);
|
||||
form.onsubmit=async event=>{event.preventDefault();submit.disabled=true;try{
|
||||
const files=[];for(const [index,input] of inputs.entries())for(const file of input.files)files.push({item_index:index,upload_id:await dtfUpload(file,{api,scope:scope+':correction:'+id+':'+order.version,progress:text=>$('message').textContent=text})});
|
||||
const files=[];for(const [index,input] of inputs.entries())for(const file of input.files)files.push({item_index:index,upload_id:await dtfUpload(file,{api,scope:scope+':correction:'+id+':'+order.version,progress:text=>aviso(text)})});
|
||||
if(!files.length)throw new Error('Selecione pelo menos um arquivo corrigido.');
|
||||
await api('/customer/orders/'+id+'/corrections',{version:order.version,files,note:note.value});
|
||||
$('message').textContent='Correção enviada. Aguarde a conferência da equipe.';await details(id,container);
|
||||
}catch(error){$('message').textContent=error.message;}finally{submit.disabled=false;}};
|
||||
aviso('Correção enviada. A equipe confere e segue com o pedido.');await details(id,container,criado);
|
||||
}catch(error){aviso(error.message,true);}finally{submit.disabled=false;}};
|
||||
container.append(form);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Overview ------------------------------------------------------------
|
||||
|
||||
async function geral(){
|
||||
$('account').textContent='CNPJ '+fmtCnpj(conta.cnpj)+' · '+conta.mail;
|
||||
const data=await api('/customer/orders?page=1&size=1');states=data.states;
|
||||
const box=$('metricas');box.replaceChildren();
|
||||
for(const [key,label] of [['pay','Aguardando pagamento'],['prod','Em produção'],['cor','Correção'],['fin','Finalizados']]){
|
||||
if(key==='cor'&&!data.counts.cor)continue;
|
||||
const a=node('a',null,'metrica'+(key==='cor'?' alerta':''));a.href='/conta/pedidos';a.dataset.view='pedidos';
|
||||
a.addEventListener('click',()=>{filtro={status:key,number:'',days:0,page:1};});
|
||||
a.append(node('span',label),node('b',String(data.counts[key])));
|
||||
box.append(a);
|
||||
}
|
||||
const ultimo=$('ultimo');ultimo.replaceChildren();
|
||||
if(data.items.length)ultimo.append(linha(data.items[0]));
|
||||
else{const v=node('div',null,'vazio');v.append(node('p','Seu primeiro pedido aparece aqui.'));const a=node('a','Enviar uma arte','bPri');a.href='/#envio';v.append(a);ultimo.append(v);}
|
||||
}
|
||||
|
||||
// ---- Account details -----------------------------------------------------
|
||||
|
||||
const CAMPOS_END=[['dNome','recipient'],['dRua','street'],['dNum','number'],['dComp','complement'],['dBairro','district'],['dCidade','city'],['dUf','state']];
|
||||
function dados(){
|
||||
$('dCnpj').value=fmtCnpj(conta.cnpj);$('dZap').value=fmtZap(conta.zap);$('dMail').value=conta.mail;
|
||||
const end=conta.address||{};
|
||||
$('dCep').value=fmtCep(end.postal_code||'');
|
||||
for(const [id,k] of CAMPOS_END)$(id).value=end[k]||'';
|
||||
$('dMailSenha').value='';$('dAtual').value='';$('dNova').value='';
|
||||
}
|
||||
$('dZap').addEventListener('input',e=>{e.target.value=fmtZap(soDigitos(e.target.value).slice(0,11));});
|
||||
$('dCep').addEventListener('input',async e=>{
|
||||
e.target.value=fmtCep(e.target.value);
|
||||
const cep=soDigitos(e.target.value);
|
||||
if(cep.length!==8)return;
|
||||
try{const a=await api('/cep/'+cep);
|
||||
for(const [id,v] of [['dRua',a.street],['dBairro',a.district],['dCidade',a.city],['dUf',a.state]])if(v)$(id).value=v;
|
||||
(['dNome','dRua','dNum'].map($).find(el=>!el.value.trim())||$('dNum')).focus();
|
||||
}catch(_){/* sem consulta: o cliente preenche */}
|
||||
});
|
||||
$('fPerfil').onsubmit=async event=>{
|
||||
event.preventDefault();
|
||||
const cep=soDigitos($('dCep').value);
|
||||
const campos=Object.fromEntries(CAMPOS_END.map(([id,k])=>[k,$(id).value.trim()]));
|
||||
const algum=cep||Object.values(campos).some(Boolean);
|
||||
const address=algum?{...campos,state:campos.state.toUpperCase(),postal_code:cep}:null;
|
||||
try{
|
||||
const r=await api('/account/profile',{zap:$('dZap').value,address});
|
||||
conta=r.customer;aviso('Dados salvos.');
|
||||
}catch(e){aviso(e.status===422?(algum?'Confira o WhatsApp e o endereço: todos os campos, menos o complemento, são necessários.':'Confira o WhatsApp.'):e.message,true);}
|
||||
};
|
||||
$('fEmail').onsubmit=async event=>{
|
||||
event.preventDefault();
|
||||
try{const r=await api('/account/email',{email:$('dMail').value,password:$('dMailSenha').value});
|
||||
conta=r.customer;$('dMailSenha').value='';$('quem').textContent=conta.mail;aviso('E-mail alterado. Use o novo e-mail para entrar.');}
|
||||
catch(e){aviso(e.status===422?'Confira o e-mail.':e.message,true);}
|
||||
};
|
||||
$('fSenha').onsubmit=async event=>{
|
||||
event.preventDefault();
|
||||
try{await api('/account/password',{current:$('dAtual').value,new:$('dNova').value});
|
||||
$('dAtual').value='';$('dNova').value='';aviso('Senha alterada.');}
|
||||
catch(e){aviso(e.status===422?'A nova senha precisa ter 12 caracteres ou mais.':e.message,true);}
|
||||
};
|
||||
|
||||
// ---- Signing in ----------------------------------------------------------
|
||||
|
||||
async function migrateGuestCart(previous,next){
|
||||
if(previous===next)return;
|
||||
const database=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});
|
||||
@@ -64,14 +327,25 @@ async function migrateGuestCart(previous,next){
|
||||
database.close();
|
||||
}
|
||||
async function authenticate(path,body){
|
||||
const previous=scope,guest=!(await api('/account/me')).customer;
|
||||
await api(path,body);forgetCheckout();$('message').textContent='Acesso confirmado.';
|
||||
const next=(await api('/session')).cart_scope;
|
||||
if(guest)try{await migrateGuestCart(previous,next);}catch{$('message').textContent='Acesso confirmado. Não foi possível transferir o carrinho salvo neste navegador.';}
|
||||
await load();$('password').value='';$('register-password').value='';
|
||||
const previous=scope,guest=!conta;
|
||||
const r=await api(path,body);forgetCheckout();
|
||||
const next=(await api('/session')).cart_scope;scope=next;
|
||||
if(guest)try{await migrateGuestCart(previous,next);}catch{aviso('Não foi possível transferir o carrinho salvo neste navegador.',true);}
|
||||
conta=r.customer;$('quem').textContent=conta.mail;
|
||||
$('password').value='';$('register-password').value='';
|
||||
await mostra();
|
||||
}
|
||||
$('login').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/login',{email:$('email').value,password:$('password').value});}catch(e){$('message').textContent=e.message;}};
|
||||
$('register').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/register',{customer:{cnpj:$('cnpj').value,zap:$('phone').value,mail:$('register-email').value},password:$('register-password').value});}catch(e){$('message').textContent=e.message;}};
|
||||
$('logout').onclick=async()=>{try{await window.dtfForgetPrivateData();await api('/account/logout',{});location.reload();}catch(error){$('message').textContent='Não foi possível limpar os dados deste navegador. Feche as abas do Site e limpe os dados do site nas configurações do navegador.';}};
|
||||
$('refresh').onclick=load;
|
||||
load();
|
||||
$('login').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/login',{email:$('email').value,password:$('password').value});}catch(e){aviso(e.message,true);}};
|
||||
$('register').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/register',{customer:{cnpj:$('cnpj').value,zap:$('phone').value,mail:$('register-email').value},password:$('register-password').value});}catch(e){aviso(e.status===422?'Confira o CNPJ, o WhatsApp e o e-mail. A senha precisa ter 12 caracteres ou mais.':e.message,true);}};
|
||||
$('esqueci').onclick=()=>{$('esqueciTx').hidden=!$('esqueciTx').hidden;};
|
||||
$('logout').onclick=async()=>{try{await window.dtfForgetPrivateData();await api('/account/logout',{});location.assign('/conta/entrar');}catch(error){aviso('Não foi possível limpar os dados deste navegador. Feche as abas do Site e limpe os dados do site nas configurações do navegador.',true);}};
|
||||
|
||||
(async()=>{
|
||||
try{
|
||||
const session=await api('/session');scope=session.cart_scope;environment=session.environment;
|
||||
$('dev-notice').hidden=environment!=='local';
|
||||
conta=(await api('/account/me')).customer;
|
||||
if(conta)$('quem').textContent=conta.mail;
|
||||
await mostra();
|
||||
}catch(error){aviso(error.message,true);}
|
||||
})();
|
||||
|
||||
@@ -15,8 +15,13 @@ function itemPodeEnviar(item){
|
||||
return item.qualityStatus==='ok' || item.qualityStatus==='unverified' ||
|
||||
(item.qualityStatus==='warning' && item.qualityAcknowledged===true);
|
||||
}
|
||||
// Os itens marcados no carrinho são os que vão para o pagamento; um item
|
||||
// desmarcado fica guardado para depois. O item aberto no produto sempre vai.
|
||||
function itensAPagar(){
|
||||
return [...pedido.filter(it=>it.marcado!==false),...(itemAtual?[itemAtual]:[])];
|
||||
}
|
||||
function cartPodeEnviar(){
|
||||
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
||||
const items=itensAPagar();
|
||||
return items.length>0 && items.every(itemPodeEnviar);
|
||||
}
|
||||
function limpaPaineis(){
|
||||
@@ -28,9 +33,17 @@ function limpaPaineis(){
|
||||
// O carrinho junta itens de produtos diferentes e cobra uma vez só.
|
||||
function pintaPedido(){
|
||||
$('itens').innerHTML = pedido.map((it,i)=>
|
||||
'<div class="item"><div class="nm2"><b>'+escapeHTML(it.tit)+'</b><span>'+escapeHTML(it.desc)+'</span></div>'+
|
||||
'<div class="item'+(it.marcado===false?' fora':'')+'">'+
|
||||
'<label class="marca"><input type="checkbox" data-sel="'+i+'"'+(it.marcado===false?'':' checked')+
|
||||
' aria-label="Incluir '+escapeHTML(it.tit)+' no pagamento"></label>'+
|
||||
'<span class="miniItem">'+(it.mini ? '<img src="'+escapeHTML(it.mini)+'" alt="">' : '<i>'+escapeHTML(it.ext||'')+'</i>')+
|
||||
(it.arquivos>1 ? '<em>+'+(it.arquivos-1)+'</em>' : '')+'</span>'+
|
||||
'<div class="nm2"><b>'+escapeHTML(it.tit)+'</b><span>'+escapeHTML(it.desc)+'</span></div>'+
|
||||
'<div class="vl2">'+rs(it.total)+'</div>'+
|
||||
'<button type="button" class="x" data-rmi="'+i+'" aria-label="Remover '+escapeHTML(it.tit)+'">Remover</button></div>').join('');
|
||||
'<button type="button" class="x" data-rmi="'+i+'" aria-label="Remover '+escapeHTML(it.tit)+'">'+icone('lixeira')+'<span>Remover</span></button></div>').join('');
|
||||
$('itens').querySelectorAll('[data-sel]').forEach(c=>c.addEventListener('change',()=>{
|
||||
pedido[+c.dataset.sel].marcado=c.checked; pintaPedido();
|
||||
}));
|
||||
$('itens').querySelectorAll('[data-rmi]').forEach(b=>b.addEventListener('click',()=>{
|
||||
const i=+b.dataset.rmi, [item]=pedido.splice(i,1);
|
||||
pintaPedido();
|
||||
@@ -39,11 +52,12 @@ function pintaPedido(){
|
||||
});
|
||||
}));
|
||||
$('bEsvaziar').hidden = pedido.length+(itemAtual?1:0) < 2;
|
||||
const itens = pedido.reduce((t,it)=>t+it.total,0) + (itemAtual? itemAtual.total : 0);
|
||||
const aPagar = itensAPagar();
|
||||
const itens = aPagar.reduce((t,it)=>t+it.total,0);
|
||||
const frete = entrega.tipo==='frete' && entrega.cotado ? entrega.valor : 0;
|
||||
const soma = itens + frete;
|
||||
const n = pedido.length + (itemAtual?1:0);
|
||||
const eco = [...pedido, ...(itemAtual?[itemAtual]:[])]
|
||||
const n = aPagar.length, noCarrinho = pedido.length + (itemAtual?1:0);
|
||||
const eco = aPagar
|
||||
.reduce((t,it)=>t+(TABELA[it.modo]-it.unit)*it.cob, 0);
|
||||
$('resLin').innerHTML =
|
||||
'<div class="l"><span>'+n+(n===1?' item':' itens')+'</span><b>'+rs(itens+eco)+'</b></div>'+
|
||||
@@ -56,12 +70,13 @@ function pintaPedido(){
|
||||
'<div class="l"><span>Revisão e reencaixe</span>'+
|
||||
'<b><span style="color:var(--verde-ml)">Grátis</span></b></div>';
|
||||
$('carrTot').textContent=rs(soma);
|
||||
$('bPagar').textContent = (pedido.length + (itemAtual?1:0))>1
|
||||
? 'Pagar os '+(pedido.length+(itemAtual?1:0))+' itens de uma vez'
|
||||
$('bPagarTx').textContent = n>1
|
||||
? 'Pagar os '+n+' itens de uma vez'
|
||||
: 'Ir para o pagamento';
|
||||
$('bPagar').disabled = !cartPodeEnviar() || !entrega.cotado || !clienteOk() || !enderecoOk();
|
||||
if(pedido.length || itemAtual) $('carr').classList.add('on');
|
||||
if($('cartLink')) $('cartLink').textContent='Carrinho ('+n+')';
|
||||
if($('cartN')){ $('cartN').textContent=noCarrinho>99?'99+':noCarrinho; $('cartN').hidden=!noCarrinho;
|
||||
$('cartLink').setAttribute('aria-label','Carrinho, '+noCarrinho+(noCarrinho===1?' item':' itens')); }
|
||||
window.dispatchEvent(new Event('dtf-cart-changed'));
|
||||
}
|
||||
|
||||
@@ -91,7 +106,7 @@ function carrinho(){
|
||||
measurement:x.med?'file':'customer'}
|
||||
: {kind:'artwork',width_cm:x.cm,length_cm:+(x.cm*propDe(x)).toFixed(4),
|
||||
copies:x.q,rotation_degrees:x.giro||0,mirrored:!!x.esp,
|
||||
measurement:x.src?'file':'customer'});
|
||||
measurement:(x.src||x.semPrevia)?'file':'customer'});
|
||||
let layout;
|
||||
if(ehFolha()){
|
||||
let y=0;
|
||||
@@ -111,6 +126,9 @@ function carrinho(){
|
||||
}
|
||||
itemAtual={modo, tit:MODOS[modo].tit, nota, metros, cob, unit, total:tot,
|
||||
localFiles:(ehFolha()?folhas:artes).map(x=>x.f),
|
||||
mini:(ehFolha()?folhas:artes).map(x=>x.miniSrc).find(Boolean)||null,
|
||||
ext:extDe(((ehFolha()?folhas:artes)[0]||{f:{name:''}}).f.name),
|
||||
arquivos:(ehFolha()?folhas:artes).length,
|
||||
production:{version:2,film_width_cm:larguraFilme(),sources,...layout},
|
||||
qualityStatus:$('ciente').classList.contains('recusa')?'rejected':
|
||||
$('ciente').classList.contains('on')?'warning':nota===0?'unverified':'ok',
|
||||
|
||||
@@ -5,6 +5,30 @@
|
||||
|
||||
const $=id=>document.getElementById(id);
|
||||
const rs=v=>'R$ '+v.toFixed(2).replace('.',',');
|
||||
// Ícones (Lucide, licença ISC) desenhados no próprio HTML: nada a carregar, e a
|
||||
// cor vem do texto em volta. Os que ficam no HTML estático são os mesmos.
|
||||
const ICONES={
|
||||
conta:'<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
sacola:'<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/>',
|
||||
enviar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
|
||||
girar:'<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>',
|
||||
espelhar:'<path d="m3 7 5 5-5 5V7"/><path d="m21 7-5 5 5 5V7"/><path d="M12 20v2"/><path d="M12 14v2"/><path d="M12 8v2"/><path d="M12 2v2"/>',
|
||||
fechar:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
adicionar:'<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M12 10v6"/><path d="M9 13h6"/>',
|
||||
cadeado:'<path d="M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||||
lixeira:'<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
||||
local:'<path d="M20 10c0 4.99-5.54 10.19-7.4 11.8a1 1 0 0 1-1.2 0C9.54 20.19 4 14.99 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',
|
||||
caminhao:'<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>',
|
||||
cartao:'<path d="M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z"/><path d="M2 10h20"/>',
|
||||
qrcode:'<path d="M3 3h5v5H3z"/><path d="M16 3h5v5h-5z"/><path d="M3 16h5v5H3z"/><path d="M21 16h-3a2 2 0 0 0-2 2v3"/><path d="M21 21v.01"/><path d="M12 7v3a2 2 0 0 1-2 2H7"/><path d="M3 12h.01"/><path d="M12 3h.01"/><path d="M12 16v.01"/><path d="M16 12h1"/><path d="M21 12v.01"/><path d="M12 21v-1"/>',
|
||||
copiar:'<path d="M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2Z"/><path d="M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"/>',
|
||||
relogio:'<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
|
||||
ok:'<path d="M20 6 9 17l-5-5"/>',
|
||||
okCirculo:'<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
|
||||
alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>',
|
||||
refazer:'<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>',
|
||||
};
|
||||
const icone=n=>'<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[n]+'</svg>';
|
||||
const fmt=b=>b<1048576?(b/1024).toFixed(0)+' KB'
|
||||
:b<1073741824?(b/1048576).toFixed(1).replace('.',',')+' MB':(b/1073741824).toFixed(1).replace('.',',')+' GB';
|
||||
|
||||
@@ -96,6 +120,23 @@ const destinoApi=()=>entrega.tipo!=='frete' ? null : {
|
||||
recipient:entrega.end.nome.trim(), street:entrega.end.rua.trim(), number:entrega.end.num.trim(),
|
||||
complement:(entrega.end.comp||'').trim(), district:entrega.end.bairro.trim(),
|
||||
city:entrega.end.cidade.trim(), state:entrega.end.uf.trim().toUpperCase(), postal_code:entrega.cep};
|
||||
// Tempo de envio antes de medir a internet de quem compra: uma faixa entre um
|
||||
// upload lento (10 Mbps) e um rápido (150 Mbps). O arquivo vai direto para o
|
||||
// armazenamento, então é o upload do cliente que manda.
|
||||
const ENVIO_LENTO=10e6/8, ENVIO_RAPIDO=150e6/8;
|
||||
const duracao=s=>{
|
||||
const min=Math.ceil(s/60);
|
||||
return s<60 ? 'menos de 1 min' : min<60 ? min+' min' : Math.floor(min/60)+' h'+(min%60? ' '+min%60+' min' : '');
|
||||
};
|
||||
const faixaEnvio=bytes=>bytes/ENVIO_RAPIDO<60 ? 'até '+duracao(bytes/ENVIO_LENTO)
|
||||
: 'de '+duracao(bytes/ENVIO_RAPIDO)+' a '+duracao(bytes/ENVIO_LENTO);
|
||||
// Quanto falta para ler um arquivo grande no navegador, pelo ritmo até aqui.
|
||||
// A leitura é quase linear, então o tempo gasto diz o que resta.
|
||||
function faltaLeitura(l){
|
||||
if(!l || !(l.p>0.02)) return 'calculando o tempo…';
|
||||
const s=(Date.now()-l.inicio)/1000*(1-l.p)/l.p;
|
||||
return s<10 ? 'faltam '+Math.max(1,Math.ceil(s))+' s' : s<60 ? 'faltam cerca de '+Math.ceil(s/5)*5+' s' : 'faltam cerca de '+duracao(s);
|
||||
}
|
||||
const larguraFilme=()=>MODOS[modo].larg;
|
||||
// cobrança proporcional: arredonda para cima em 10 cm · 2,75 m vira 2,80 m
|
||||
const cobrar=m=>Math.max(MINIMO_M, Math.ceil(m*10)/10);
|
||||
|
||||
@@ -80,8 +80,6 @@ function pintaEntrega(){
|
||||
$('vFrete').textContent = entrega.tipo!=='frete' ? '—'
|
||||
: entrega.cotado ? rs(entrega.valor) : 'a cotar';
|
||||
$('bPagar').disabled = !entrega.cotado || !clienteOk() || !enderecoOk() || !cartPodeEnviar();
|
||||
$('eEnd').textContent = entrega.tipo==='frete' && entrega.cotado && !enderecoOk()
|
||||
? 'Complete o endereço de entrega para ir ao pagamento.' : '';
|
||||
pintaPedido();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function pintaModo(){
|
||||
$('larguraFolhaPronta').textContent=String(c.larg).replace('.',',');
|
||||
$('foco2').classList.remove('sofila'); // a caixa de requisitos vale nos 4 modos
|
||||
$('montcabTit').textContent = ehFolha(m) ? 'Sua folha' : 'Montagem ao vivo';
|
||||
$('qOk').textContent = ehFolha(m) ? 'Adicionar ao carrinho' : 'Ver a folha montada';
|
||||
$('qOk').innerHTML = ehFolha(m) ? icone('adicionar')+'<span>Adicionar ao carrinho</span>' : 'Ver a folha montada';
|
||||
pintaTipoEnvio();
|
||||
}
|
||||
// O par folha montada / artes separadas fica visível e com preço nos dois lados,
|
||||
|
||||
@@ -337,7 +337,7 @@ function previaAoVivo(){
|
||||
tMont=setTimeout(()=>{
|
||||
// o produto pode ter sido fechado nesses 140 ms (item foi para o carrinho)
|
||||
if(!modo) return;
|
||||
const prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
|
||||
const prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && (a.src||a.semPrevia));
|
||||
const W=Math.round(300*ZOOMS[zoomI]);
|
||||
$('vArea').classList.toggle('ampliado', zoomI>0);
|
||||
desenhaMontagem($('vArea'), prontas, 0, W, $('vUso'), r=>{
|
||||
|
||||
@@ -353,7 +353,7 @@ function medirFolha(file){
|
||||
}
|
||||
|
||||
function textoProgresso(x){
|
||||
if(x.med && x.med.grande) return 'gerando a prévia e conferindo a arte… '+x.pct+'%';
|
||||
if(x.med && x.med.grande) return 'gerando a prévia e conferindo a arte · '+faltaLeitura(x.leitura);
|
||||
return x.pct<50?'lendo o arquivo…':x.pct<70?'medindo a folha…':'conferindo a arte…';
|
||||
}
|
||||
// The file card's thumbnail (52 x 96 px, drawn at twice that for sharp screens).
|
||||
@@ -472,7 +472,7 @@ function pintaFolha(){
|
||||
return '<div class="rep" data-folha="'+i+'">'+
|
||||
'<div class="repTopo"><div class="miniF">'+mini+'</div><div class="repInfo">'+
|
||||
'<div class="l1"><b>'+(folhas.length>1? (i+1)+'. ':'')+escapeHTML(x.f.name)+'</b>'+
|
||||
'<button data-rmf="'+i+'" aria-label="Remover '+escapeHTML(x.f.name)+'">×</button></div>'+
|
||||
'<button data-rmf="'+i+'" aria-label="Remover '+escapeHTML(x.f.name)+'">'+icone('fechar')+'</button></div>'+
|
||||
(lido || x.measurementError ? '' : '<span class="repPeso">'+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'</span>')+
|
||||
barra+medida+
|
||||
'</div></div>'+copias+
|
||||
@@ -524,7 +524,7 @@ function pintaProgressoArte(a){
|
||||
const el=$('lista').querySelector('[data-arte="'+artes.indexOf(a)+'"]');
|
||||
const barra=el && el.querySelector('.prog i'), texto=el && el.querySelector('.progT');
|
||||
if(!barra){ pintaArtes(); return; }
|
||||
barra.style.width=a.lendo+'%'; if(texto) texto.textContent='arquivo grande · gerando a prévia… '+a.lendo+'%';
|
||||
barra.style.width=a.lendo+'%'; if(texto) texto.textContent='arquivo grande · gerando a prévia · '+faltaLeitura(a.leitura);
|
||||
}
|
||||
function pintaArtes(){
|
||||
invalidaItemAtual();
|
||||
@@ -538,14 +538,12 @@ function pintaArtes(){
|
||||
let barra='', dica='', chips='', aviso='', medida='<b class="pede">Informe a largura</b>';
|
||||
if(a.lendo!=null){
|
||||
barra='<div class="prog"><i style="width:'+a.lendo+'%"></i></div>'+
|
||||
'<span class="progT">arquivo grande · gerando a prévia… '+a.lendo+'%</span>';
|
||||
}else if(a.decodeError==='grande'){
|
||||
aviso='<p class="avisoF"><b>Arquivo grande em JPG ou WebP.</b> Exporte em PNG para montar a folha.</p>';
|
||||
'<span class="progT">arquivo grande · gerando a prévia · '+faltaLeitura(a.leitura)+'</span>';
|
||||
}else if(a.decodeError){
|
||||
aviso='<p class="avisoF"><b>Não conseguimos ler esta imagem.</b> Exporte novamente em PNG ou JPG.</p>';
|
||||
}else if(a.cm>larguraFilme()){
|
||||
aviso='<p class="avisoF"><b>Largura maior que o filme.</b> O filme aqui tem '+n1(larguraFilme())+' cm.</p>';
|
||||
}else if(a.cm>0 && a.src){
|
||||
}else if(a.cm>0 && (a.src||a.semPrevia)){
|
||||
const dpi=Math.round(dpiDe(a));
|
||||
medida='<b>'+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</b>';
|
||||
chips='<ul class="chips"><li class="chip '+(dpi>=DPI_AVISA?'ok':dpi>=DPI_RECUSA?'av':'er')+'">'+dpi+' DPI</li>'+
|
||||
@@ -562,12 +560,15 @@ function pintaArtes(){
|
||||
n1(sug.cm)+' cm.<button data-usar="'+i+'" data-cm="'+sug.alvo+'">Usar '+n1(sug.alvo)+' cm</button></div>'
|
||||
: '';
|
||||
}
|
||||
if(a.semPrevia && !a.decodeError)
|
||||
aviso+='<p class="repTxt"><b>Sem prévia para '+escapeHTML(extDe(a.f.name))+' deste tamanho.</b> '+
|
||||
'A arte entra na folha pela medida, e a equipe confere antes de imprimir. Em PNG, a prévia aparece.</p>';
|
||||
const mini = a.miniSrc ? '<img src="'+escapeHTML(a.miniSrc)+'" alt="">' : '<span>'+escapeHTML(extDe(a.f.name))+'</span>';
|
||||
const sub = a.cm>0 && a.src ? (a.q>1? a.q+' × ' : '')+'<b>'+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</b>' : '';
|
||||
const sub = a.cm>0 && (a.src||a.semPrevia) ? (a.q>1? a.q+' × ' : '')+'<b>'+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</b>' : '';
|
||||
return '<div class="rep art2'+(a===recemChegada?' nova':'')+'" data-arte="'+i+'">'+
|
||||
'<div class="repTopo"><div class="miniF">'+mini+'</div><div class="repInfo">'+
|
||||
'<div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+(a===recemChegada?'<em class="novaT">nova</em>':'')+
|
||||
'<button data-rm="'+i+'" aria-label="Remover '+escapeHTML(a.f.name)+'">×</button></div>'+
|
||||
'<button data-rm="'+i+'" aria-label="Remover '+escapeHTML(a.f.name)+'">'+icone('fechar')+'</button></div>'+
|
||||
'<div class="repMed">'+medida+'<span>'+(a.px>0? a.px+' × '+a.py+' px · ' : '')+
|
||||
escapeHTML(extDe(a.f.name))+' · '+fmt(a.f.size)+'</span></div>'+
|
||||
barra+chips+aviso+dica+
|
||||
@@ -581,8 +582,8 @@ function pintaArtes(){
|
||||
'<input type="number" id="q'+i+'" min="1" max="200" data-q="'+i+'" value="'+a.q+'">'+
|
||||
'<button type="button" data-passoa="1" data-i="'+i+'" aria-label="Mais uma cópia">+</button></span></div>'+
|
||||
'<span class="giros">'+
|
||||
'<button class="gr'+(a.giro?' on':'')+'" data-giro="'+i+'" title="Girar 90°" aria-label="Girar 90°">⟳</button>'+
|
||||
'<button class="gr'+(a.esp?' on':'')+'" data-esp="'+i+'" title="Espelhar" aria-label="Espelhar">⇋</button></span>'+
|
||||
'<button class="gr'+(a.giro?' on':'')+'" data-giro="'+i+'" title="Girar 90°" aria-label="Girar 90°">'+icone('girar')+'</button>'+
|
||||
'<button class="gr'+(a.esp?' on':'')+'" data-esp="'+i+'" title="Espelhar" aria-label="Espelhar">'+icone('espelhar')+'</button></span>'+
|
||||
(sub? '<div class="conta">'+sub+'</div>' : '')+
|
||||
'<div class="atalhos">'+(TAMANHOS[larguraFilme()]||[]).map(t=>'<button class="tm'+
|
||||
(Math.abs(a.cm-t)<0.05?' on':'')+'" data-tam="'+i+'" data-t="'+t+'">'+n1(t)+' cm</button>').join('')+'</div>'+
|
||||
@@ -620,7 +621,7 @@ function pintaArtes(){
|
||||
previaAoVivo();
|
||||
}
|
||||
function resumoArtes(){
|
||||
const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
|
||||
const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && (a.src||a.semPrevia));
|
||||
if(!prontas.length){ r.style.display='none'; return; }
|
||||
const m=montagemCm/100; // a mesma folha que aparece ao lado
|
||||
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
|
||||
|
||||
@@ -64,9 +64,7 @@ function avaliar(){
|
||||
}
|
||||
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'};
|
||||
if(artes.some(a=>a.lendo!=null)) return {pronto:false, falta:'Gerando a prévia das artes grandes…'};
|
||||
if(artes.some(a=>a.decodeError==='grande'))
|
||||
return {pronto:false, falta:'Uma arte grande está em JPG ou WebP. Exporte-a em PNG para montar a folha.'};
|
||||
if(artes.some(a=>a.decodeError || !a.src || !(a.px>0) || !(a.py>0)))
|
||||
if(artes.some(a=>a.decodeError || !(a.src||a.semPrevia) || !(a.px>0) || !(a.py>0)))
|
||||
return {pronto:false, falta:'Não conseguimos ler uma das imagens. Exporte-a novamente em PNG ou JPG antes de continuar.'};
|
||||
if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme()))
|
||||
return {pronto:false, falta:'A largura da arte precisa caber no filme de '+
|
||||
@@ -119,7 +117,7 @@ function mostraNota(itens,n,titulo,sub){
|
||||
$('qbi').style.width=(semNota?0:n)+'%'; $('qbi').style.background=c;
|
||||
$('qitens').innerHTML=itens.map(i=>
|
||||
'<div class="qi '+i[0]+'"><span class="c">'+
|
||||
(i[0]==='ok'?'✓':i[0]==='av'?'!':i[0]==='fix'?'↻':'×')+'</span>'+
|
||||
icone(i[0]==='ok'?'ok':i[0]==='av'?'alerta':i[0]==='fix'?'refazer':'fechar')+'</span>'+
|
||||
'<div><b>'+escapeHTML(i[1])+'</b><span>'+escapeHTML(i[2])+'</span></div></div>').join('');
|
||||
$('qmsg').style.display='none';
|
||||
ressalvaResolucao();
|
||||
|
||||
87
web/site-shell.css
Normal file
87
web/site-shell.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/* Site DTF — what every page shares: the tokens, the base type, the stripe,
|
||||
header, category menu and footer. The Site (index.html) and the order page
|
||||
(portal.html) both load it before their own styles. */
|
||||
:root{
|
||||
/* tokens do site institucional */
|
||||
--font-family-base:"Roboto",sans-serif; --font-family-title:"Inter",sans-serif;
|
||||
--font-color-base:#03060b; --font-color-title:#03060b;
|
||||
--font-weight-base:400; --font-weight-title:700; --font-weight-subtitle:600; --font-weight-button:600;
|
||||
--font-size-title:3rem; --font-size-subtitle:2rem; --font-size-button:1.4rem;
|
||||
--font-size-small:1.2rem; --font-size-base:1.4rem; --font-size-medium:1.6rem; --font-size-big:1.8rem;
|
||||
--line-height:1.5;
|
||||
--white:#fff; --black:#03060b;
|
||||
--gray-1:#f9f9f9; --gray-2:#c3c3c3; --gray-3:#707070; --gray-4:#363636;
|
||||
--primary-color:#ffa81a; --secondary-color:#ff7f0a;
|
||||
--white-transparency:#ffffffb3; --transition:all .4s ease;
|
||||
/* apelidos usados nesta pagina, mapeados nos tokens acima */
|
||||
--marinho:var(--black); --papel:var(--white); --cinza:var(--gray-1);
|
||||
--tx:var(--font-color-base); --fraco:var(--gray-3); --linha:#e6e6e6;
|
||||
--laranja:var(--primary-color); --laranja2:var(--secondary-color);
|
||||
--vermelho:#D94A28; --verde:#2E9E5B; --verde-ml:#00A650; --verde-claro:#57D68A;
|
||||
--sombra:0 1px 2px rgba(3,6,11,.05), 0 4px 14px rgba(3,6,11,.05);
|
||||
--sombra-alta:0 2px 6px rgba(3,6,11,.07), 0 10px 30px rgba(3,6,11,.11)}
|
||||
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth;font-size:62.5%}
|
||||
body{background:var(--white);color:var(--font-color-base);font-family:var(--font-family-base);
|
||||
font-size:var(--font-size-base);font-weight:var(--font-weight-base);line-height:var(--line-height);
|
||||
-webkit-font-smoothing:antialiased}
|
||||
h1,h2,h3{font-family:var(--font-family-title);color:var(--font-color-title);
|
||||
font-weight:var(--font-weight-title);letter-spacing:normal;line-height:1.15}
|
||||
.w{max-width:1020px;margin:0 auto;padding:0 22px}
|
||||
|
||||
/* tarja rotativa, como no site */
|
||||
.tarja{background:var(--laranja);color:#111;text-align:center;padding:8px 16px;
|
||||
font-family:"Inter",sans-serif;font-weight:700;font-size:14.5px;letter-spacing:.02em}
|
||||
.tarja a{color:#111;text-decoration:none}
|
||||
|
||||
/* topo */
|
||||
.topo{background:#fff;border-bottom:1px solid var(--linha);position:sticky;top:0;z-index:60}
|
||||
.topo .w{display:flex;align-items:center;gap:22px;padding-top:15px;padding-bottom:15px}
|
||||
.logo{display:flex;align-items:center;text-decoration:none;white-space:nowrap;flex:none}
|
||||
.logo img{height:26px;width:auto;display:block}
|
||||
.logotexto{font-family:"Inter",sans-serif;font-weight:700;font-size:19px;color:var(--black);
|
||||
letter-spacing:-.03em}
|
||||
.logotexto i{font-style:normal;color:var(--laranja)}
|
||||
.acts{margin-left:auto;display:flex;gap:18px;align-items:center;font-size:15px}
|
||||
.acts a{color:var(--tx);text-decoration:none;display:flex;align-items:center;gap:6px}
|
||||
.acts a:hover{color:var(--laranja)}
|
||||
|
||||
/* menu */
|
||||
.menu{background:#fff;border-bottom:1px solid var(--linha)}
|
||||
.menu .w{display:flex;gap:0;flex-wrap:wrap}
|
||||
.mi{position:relative}
|
||||
.mi>a{display:block;padding:13px 16px;font-family:"Inter",sans-serif;font-weight:600;
|
||||
font-size:15px;color:var(--tx);text-decoration:none;white-space:nowrap}
|
||||
.mi:hover>a{color:var(--laranja)}
|
||||
.mi.on>a{color:var(--laranja);box-shadow:inset 0 -2px 0 var(--laranja)}
|
||||
.sub{display:none;position:absolute;top:100%;left:0;background:#fff;border:1px solid var(--linha);
|
||||
border-radius:0 0 9px 9px;min-width:224px;padding:7px 0;z-index:70;
|
||||
box-shadow:0 10px 26px rgba(0,0,0,.09)}
|
||||
.mi:hover .sub,.mi:focus-within .sub{display:block}
|
||||
.sub a{display:block;padding:8px 17px;font-size:15px;color:var(--fraco);text-decoration:none}
|
||||
.sub a:hover{color:var(--laranja);background:var(--cinza)}
|
||||
@media(max-width:820px){
|
||||
.menu .w{overflow-x:auto;flex-wrap:nowrap}
|
||||
.sub{display:none!important}
|
||||
}
|
||||
|
||||
footer{background:#000;color:#8D9AB0;padding:26px 0 20px;font-size:15px}
|
||||
.frow{display:flex;gap:26px;align-items:flex-start;flex-wrap:wrap;padding-bottom:18px}
|
||||
.fmarca{display:flex;align-items:center;gap:9px;font-family:"Inter",sans-serif;
|
||||
font-weight:700;font-size:15px;color:var(--laranja);letter-spacing:.06em;white-space:nowrap}
|
||||
.fmarca img{height:22px;width:auto;display:block;filter:brightness(0) invert(1)}
|
||||
footer a{color:#8D9AB0;text-decoration:none}
|
||||
footer a:hover{color:var(--laranja2)}
|
||||
.flinks{display:flex;gap:18px;flex-wrap:wrap;font-size:15px}
|
||||
.fcont{margin-left:auto;text-align:right;font-size:15px;line-height:1.7}
|
||||
@media(max-width:720px){.fcont{margin-left:0;text-align:left}}
|
||||
.fbase{border-top:1px solid rgba(255,255,255,.1);padding-top:14px;display:flex;
|
||||
justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:13.5px;color:#5A6570}
|
||||
|
||||
/* The footer sits at the bottom of the screen even when the page is short. */
|
||||
body{display:flex;flex-direction:column;min-height:100vh}
|
||||
body>footer{margin-top:auto}
|
||||
/* In a column, auto side margins shrink a box to its content: the page's
|
||||
centred blocks keep their full width, up to their max-width, as before. */
|
||||
body>.w{width:100%}
|
||||
@@ -97,6 +97,8 @@ function vaiPara(url){
|
||||
passos.querySelectorAll('[data-passo]').forEach(a=>{
|
||||
const n=Number(a.dataset.passo);
|
||||
a.classList.toggle('atual',n===atual); a.classList.toggle('feito',n<atual);
|
||||
const marca=a.querySelector('span');
|
||||
if(marca) marca.innerHTML = n<atual ? icone('ok') : String(n);
|
||||
if(n===atual) a.setAttribute('aria-current','step'); else a.removeAttribute('aria-current');
|
||||
});
|
||||
passos.querySelector('[data-passo="1"]').href =
|
||||
|
||||
@@ -65,10 +65,11 @@ function sel(fs){
|
||||
x.an={dpi:md.dpiFolha, grande:true, artes:0, residuos:0, encostadas:0,
|
||||
fundoChapado:false, aproveitamento:0, alturaCm:md.alt, fonteDpi:'arquivo'};
|
||||
if(!/\.png$/i.test(x.f.name)){ x.pct=null; pintaFolha(); return; }
|
||||
x.pct=60; pintaFolha();
|
||||
x.pct=60; x.leitura={inicio:Date.now(), p:0}; pintaFolha();
|
||||
let mostrado=0;
|
||||
previaPngGrande(x.f, 600, p=>{
|
||||
const pct=60+Math.floor(p*38);
|
||||
x.leitura.p=p;
|
||||
if(pct-mostrado>=1){ mostrado=pct; x.pct=pct; pintaProgressoFolha(x); }
|
||||
}).then(cv=>{
|
||||
if(cv && folhas.includes(x)){
|
||||
@@ -128,11 +129,14 @@ function medir(a){
|
||||
return dimensoesImagem(a.f).then(async d=>{
|
||||
if(!d || !(d.w>0) || !(d.h>0)){ a.decodeError=true; return a; }
|
||||
a.px=d.w; a.py=d.h; a.prop=d.h/d.w; a.grande=true;
|
||||
if(!/\.png$/i.test(a.f.name)){ a.decodeError='grande'; return a; }
|
||||
a.lendo=0; pintaArtes();
|
||||
// JPG and WebP cannot be read in parts: the art is placed by its
|
||||
// measurements, as a full box, and the team checks it before printing.
|
||||
if(!/\.png$/i.test(a.f.name)){ a.semPrevia=true; return a; }
|
||||
a.lendo=0; a.leitura={inicio:Date.now(), p:0}; pintaArtes();
|
||||
let mostrado=0;
|
||||
const cv=await previaPngGrande(a.f, 1200, p=>{
|
||||
const pct=Math.floor(p*100);
|
||||
a.leitura.p=p;
|
||||
if(pct-mostrado>=1){ mostrado=pct; a.lendo=pct; pintaProgressoArte(a); }
|
||||
}).catch(()=>null);
|
||||
a.lendo=null;
|
||||
|
||||
276
web/site-v2.css
276
web/site-v2.css
@@ -10,7 +10,7 @@
|
||||
body{background:#fff}
|
||||
.w{max-width:1200px;padding:0 24px}
|
||||
h1,h2,h3,h4{font-family:"Inter",sans-serif}
|
||||
.et{font-family:"Inter",sans-serif;font-weight:800;font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--laranja-escuro)}
|
||||
.et{font-family:"Inter",sans-serif;font-weight:800;font-size:15px;letter-spacing:.06em;text-transform:uppercase;color:var(--laranja-escuro)}
|
||||
|
||||
/* 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),
|
||||
@@ -23,31 +23,31 @@ html:not([data-rota="inicio"]) #envio{padding-top:0;padding-bottom:64px;min-heig
|
||||
.carrVazio{max-width:520px;margin:48px auto 0;padding:0 16px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:14px}
|
||||
.carrVazio[hidden]{display:none}
|
||||
.carrVazio h2{font-size:28px;font-weight:800;letter-spacing:-.02em}
|
||||
.carrVazio p{color:var(--fraco);font-size:16px}
|
||||
.carrVazio p{color:var(--fraco);font-size:17.5px}
|
||||
|
||||
/* Hero */
|
||||
.hero2{background:var(--preto);color:#fff;padding:64px 0}
|
||||
.hero2-grid{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:56px;align-items:center}
|
||||
.hero2-text{display:flex;flex-direction:column;gap:24px}
|
||||
.chips{display:flex;gap:10px;flex-wrap:wrap}
|
||||
.chip2{font-family:"Inter",sans-serif;font-weight:700;font-size:12px;letter-spacing:.04em;text-transform:uppercase;padding:6px 12px;border-radius:6px;border:1px solid #3A3D42;color:#E4E4E4}
|
||||
.chip2{font-family:"Inter",sans-serif;font-weight:700;font-size:14.5px;letter-spacing:.04em;text-transform:uppercase;padding:6px 12px;border-radius:6px;border:1px solid #3A3D42;color:#E4E4E4}
|
||||
.chip2.destaque{background:var(--laranja);border-color:var(--laranja);color:var(--preto);font-weight:800}
|
||||
.hero2 h1{color:#fff;font-size:clamp(34px,4.6vw,58px);line-height:1.04;font-weight:900;letter-spacing:-.03em}
|
||||
.hero2 h1 em{font-style:normal;color:var(--laranja)}
|
||||
.hero2-text>p{font-size:18px;line-height:1.55;color:#C9CCD1;max-width:560px}
|
||||
.hero2-text>p{font-size:19px;line-height:1.55;color:#C9CCD1;max-width:560px}
|
||||
.hero2-cta{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
|
||||
.hero2-cta span{font-size:14px;color:#9EA3AA}
|
||||
.btn-laranja{height:56px;padding:0 28px;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:17px;display:inline-flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.hero2-cta span{font-size:16px;color:#9EA3AA}
|
||||
.btn-laranja{height:56px;padding:0 28px;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:18px;display:inline-flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.btn-laranja:hover{background:#FFB53D}
|
||||
.hero2-folha{background:#14171B;border:1px solid #262A2F;border-radius:18px;padding:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.hf-cab{display:flex;align-items:baseline;gap:10px}
|
||||
.hf-cab b{font-family:"Inter",sans-serif;font-size:15px}
|
||||
.hf-cab span{margin-left:auto;font-size:13px;color:#9EA3AA}
|
||||
.hf-cab b{font-family:"Inter",sans-serif;font-size:16.5px}
|
||||
.hf-cab span{margin-left:auto;font-size:15px;color:#9EA3AA}
|
||||
.hf-area{position:relative;height:260px;border-radius:10px;background:#0B0D10;border:1px dashed #3A3D42;overflow:hidden}
|
||||
.hf-area i{position:absolute;border-radius:6px;opacity:.88}
|
||||
.hf-num{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
|
||||
.hf-num span{display:block;font-size:12px;color:#9EA3AA}
|
||||
.hf-num b{font-family:"Inter",sans-serif;font-size:18px}
|
||||
.hf-num span{display:block;font-size:14.5px;color:#9EA3AA}
|
||||
.hf-num b{font-family:"Inter",sans-serif;font-size:19px}
|
||||
.hf-num .verde{color:#57D68A}.hf-num .laranja{color:var(--laranja)}
|
||||
|
||||
/* Steps strip */
|
||||
@@ -55,15 +55,15 @@ html:not([data-rota="inicio"]) #envio{padding-top:0;padding-bottom:64px;min-heig
|
||||
.etapas2 .w{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px}
|
||||
.etapas2 .w>div{display:flex;gap:14px;align-items:flex-start}
|
||||
.etapas2 span{width:36px;height:36px;flex-shrink:0;border-radius:999px;background:#FFF1DB;color:var(--laranja-escuro);font-family:"Inter",sans-serif;font-weight:800;display:flex;align-items:center;justify-content:center}
|
||||
.etapas2 b{font-family:"Inter",sans-serif;font-size:15.5px}
|
||||
.etapas2 p{margin-top:3px;font-size:14px;color:var(--fraco)}
|
||||
.etapas2 b{font-family:"Inter",sans-serif;font-size:17px}
|
||||
.etapas2 p{margin-top:3px;font-size:16px;color:var(--fraco)}
|
||||
section+section{border-top:0}
|
||||
|
||||
/* Product chooser */
|
||||
#envio{padding-top:56px}
|
||||
.cards-cab{display:flex;align-items:flex-end;gap:24px;flex-wrap:wrap}
|
||||
.cards-cab h2{margin-top:8px;font-size:clamp(28px,3vw,38px);font-weight:800;letter-spacing:-.02em}
|
||||
.cards-cab p{margin-left:auto;max-width:440px;font-size:15px;color:var(--fraco)}
|
||||
.cards-cab p{margin-left:auto;max-width:440px;font-size:16.5px;color:var(--fraco)}
|
||||
.escolha{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin-top:28px}
|
||||
.ec{text-align:left;padding:26px;gap:14px;border:1px solid var(--linha);border-radius:16px;box-shadow:none;cursor:pointer;font:inherit;color:inherit}
|
||||
.ec::before,.ec::after{display:none}
|
||||
@@ -71,23 +71,23 @@ section+section{border-top:0}
|
||||
.ec.f,.ec.f:hover{border-color:var(--linha)}
|
||||
.ec.f:hover{border-color:var(--laranja)}
|
||||
.ec-top{display:flex;align-items:center;gap:10px}
|
||||
.ec-kind{font-family:"Inter",sans-serif;font-weight:700;font-size:12px;letter-spacing:.05em;color:#707070}
|
||||
.marc2{margin-left:auto;font-family:"Inter",sans-serif;font-weight:800;font-size:11px;letter-spacing:.05em;text-transform:uppercase;padding:4px 10px;border-radius:999px;background:var(--preto);color:var(--laranja)}
|
||||
.ec-kind{font-family:"Inter",sans-serif;font-weight:700;font-size:14.5px;letter-spacing:.05em;color:#707070}
|
||||
.marc2{margin-left:auto;font-family:"Inter",sans-serif;font-weight:800;font-size:13.5px;letter-spacing:.05em;text-transform:uppercase;padding:4px 10px;border-radius:999px;background:var(--preto);color:var(--laranja)}
|
||||
.ec h3{font-size:25px;font-weight:800;letter-spacing:-.01em;color:var(--preto)}
|
||||
.ec>p{font-size:15px;line-height:1.5;color:var(--texto2);flex:1}
|
||||
.ec>p{font-size:16.5px;line-height:1.5;color:var(--texto2);flex:1}
|
||||
.ec-pe{display:flex;align-items:flex-end;gap:12px;padding-top:14px;border-top:1px solid #EFEFEF}
|
||||
.ec-preco{display:flex;flex-direction:column;gap:2px}
|
||||
.ec-preco span{font-size:13px;color:#707070}
|
||||
.ec-preco span{font-size:15px;color:#707070}
|
||||
.ec-preco em{font-style:normal}
|
||||
.ec-preco b{font-family:"Inter",sans-serif;font-size:29px;font-weight:900;letter-spacing:-.02em;color:var(--preto)}
|
||||
.ec-go{margin-left:auto;height:46px;padding:0 18px;border-radius:var(--raio2);background:var(--preto);color:#fff;font-family:"Inter",sans-serif;font-weight:700;font-size:15px;display:flex;align-items:center;white-space:nowrap}
|
||||
.ec-go{margin-left:auto;height:46px;padding:0 18px;border-radius:var(--raio2);background:var(--preto);color:#fff;font-family:"Inter",sans-serif;font-weight:700;font-size:16.5px;display:flex;align-items:center;white-space:nowrap}
|
||||
.ec:hover .ec-go{background:var(--laranja);color:var(--preto)}
|
||||
|
||||
/* Progress bar */
|
||||
.passos{position:sticky;top:var(--topo,64px);z-index:30;display:flex;align-items:center;justify-content:center;gap:14px;margin:0 0 22px;padding:14px 16px;background:rgba(255,255,255,.96);backdrop-filter:blur(6px);border-bottom:1px solid var(--linha);font-family:"Inter",sans-serif;font-size:14px}
|
||||
.passos{position:sticky;top:var(--topo,64px);z-index:30;display:flex;align-items:center;justify-content:center;gap:14px;margin:0 0 22px;padding:14px 16px;background:rgba(255,255,255,.96);backdrop-filter:blur(6px);border-bottom:1px solid var(--linha);font-family:"Inter",sans-serif;font-size:16px}
|
||||
.passos[hidden]{display:none}
|
||||
.passos a{display:flex;align-items:center;gap:8px;color:#707070;font-weight:600;text-decoration:none}
|
||||
.passos a span{width:26px;height:26px;border-radius:999px;border:2px solid #D0D0D0;display:flex;align-items:center;justify-content:center;font-size:13px}
|
||||
.passos a span{width:26px;height:26px;border-radius:999px;border:2px solid #D0D0D0;display:flex;align-items:center;justify-content:center;font-size:15px}
|
||||
.passos a.atual{color:var(--preto);font-weight:800}
|
||||
.passos a.atual span{background:var(--laranja);border-color:var(--laranja)}
|
||||
.passos a.feito{color:var(--verde)}
|
||||
@@ -100,13 +100,13 @@ section+section{border-top:0}
|
||||
parts live in the buy box, and the rest repeated it (the findings list too:
|
||||
each row already says what was checked). */
|
||||
#qual,#prev,#rA,.qb2,#qitens{display:none!important}
|
||||
.voltar{height:auto;padding:0;border:0;background:none;box-shadow:none;font-size:14px;font-weight:500;color:var(--fraco);margin:24px 0 14px}
|
||||
.voltar{height:auto;padding:0;border:0;background:none;box-shadow:none;font-size:16px;font-weight:500;color:var(--fraco);margin:24px 0 14px}
|
||||
.voltar:hover{color:var(--laranja-escuro)}
|
||||
.focoGrid{grid-template-columns:minmax(0,1fr);gap:20px}
|
||||
.cat,.cat.file{border:0;background:transparent;box-shadow:none;overflow:visible}
|
||||
.cabfoco,.cat.file .cabfoco{padding:0 0 24px!important;background:transparent;border:0;display:flex;flex-direction:column;gap:20px}
|
||||
.cabfoco>div:first-child{display:flex;flex-direction:column-reverse;gap:4px}
|
||||
.cabfoco .lg{font-size:12.5px;font-weight:800;color:var(--laranja-escuro);letter-spacing:.06em;text-transform:uppercase}
|
||||
.cabfoco .lg{font-size:15px;font-weight:800;color:var(--laranja-escuro);letter-spacing:.06em;text-transform:uppercase}
|
||||
.cabfoco h3{font-size:36px;font-weight:800;letter-spacing:-.02em;line-height:1.1}
|
||||
.tipoEnvio{margin:0}
|
||||
.tipoEnvio:not([hidden]){display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;max-width:calc(100% - 452px)}
|
||||
@@ -114,11 +114,11 @@ section+section{border-top:0}
|
||||
.tipoEnvio .cam.on{padding:13px 15px;border:2px solid var(--preto);background:#fff}
|
||||
.tipoEnvio .cam .mk2{display:none}
|
||||
.tipoEnvio .cam div{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:12px;row-gap:4px;align-items:baseline}
|
||||
.tipoEnvio .cam b{margin:0;font-size:15px;font-weight:700;color:var(--texto2)}
|
||||
.tipoEnvio .cam b{margin:0;font-size:16.5px;font-weight:700;color:var(--texto2)}
|
||||
.tipoEnvio .cam.on b{color:var(--preto);font-weight:800}
|
||||
.tipoEnvio .cam em{margin:0;font-size:13px;font-weight:500;color:var(--fraco);white-space:nowrap}
|
||||
.tipoEnvio .cam em{margin:0;font-size:15px;font-weight:500;color:var(--fraco);white-space:nowrap}
|
||||
.tipoEnvio .cam.on em{font-weight:600;color:var(--preto)}
|
||||
.tipoEnvio .cam div>span{grid-column:1/-1;font-size:13px;color:var(--fraco);line-height:1.4}
|
||||
.tipoEnvio .cam div>span{grid-column:1/-1;font-size:15px;color:var(--fraco);line-height:1.4}
|
||||
.tipoEnvio .cam.on div>span{color:var(--texto2)}
|
||||
.tipoEnvio .cam:disabled{cursor:default}
|
||||
.tipoEnvio .cam:disabled:not(.on){opacity:.55}
|
||||
@@ -132,58 +132,58 @@ section+section{border-top:0}
|
||||
#larguraFolhaPronta{display:inline;font-size:inherit;color:inherit}
|
||||
.zona{display:grid;grid-template-columns:44px minmax(0,1fr);column-gap:16px;row-gap:2px;align-items:center;text-align:left;padding:16px 20px;margin:0;border:2px dashed #F2B25A;background:var(--aviso-fundo);border-radius:var(--raio)}
|
||||
.zona .ico{grid-row:1/4;display:flex;align-items:center;justify-content:center;width:44px;height:44px;line-height:1;margin:0;border-radius:10px;background:var(--laranja);color:var(--preto);font-size:20px;box-shadow:none}
|
||||
.zona b{font-size:15px;margin:0}
|
||||
.zona span{max-width:none;margin:0;font-size:13px;color:var(--fraco)}
|
||||
.zona b{font-size:16.5px;margin:0}
|
||||
.zona span{max-width:none;margin:0;font-size:15px;color:var(--fraco)}
|
||||
.zona .bt{display:none}
|
||||
.zona:hover,.zona.sobre{border-color:var(--laranja2);box-shadow:none}
|
||||
.zona .zManual{grid-column:1/-1;margin-top:12px;padding-top:12px;border-top:1px solid var(--aviso-linha);color:var(--texto2);line-height:1.5}
|
||||
.zona .zManual[hidden]{display:none}
|
||||
.zona .zManual b{display:inline;font-size:13px}
|
||||
.zona .zManual b{display:inline;font-size:15px}
|
||||
.zona .zOutra{display:none}
|
||||
.zona.compacta{display:flex;justify-content:center;align-items:center;min-height:56px;padding:0 20px;border:1.5px dashed #D0D0D0;background:#fff;cursor:pointer}
|
||||
.zona.compacta>*{display:none}
|
||||
.zona.compacta .zOutra{display:inline;font-size:14px;color:var(--fraco)}
|
||||
.zona.compacta .zOutra b{display:inline;font-size:14px;font-weight:600;color:var(--preto)}
|
||||
.zona.compacta .zOutra{display:inline;font-size:16px;color:var(--fraco)}
|
||||
.zona.compacta .zOutra b{display:inline;font-size:16px;font-weight:600;color:var(--preto)}
|
||||
.zona.compacta .zOutra b::before{content:"+";margin-right:8px;font-weight:700}
|
||||
.zona.compacta:hover,.zona.compacta.sobre{border-color:var(--laranja2)}
|
||||
#lista{display:flex;flex-direction:column;gap:10px}
|
||||
#lista:empty{display:none}
|
||||
.art{margin:0;padding:14px 16px;border-radius:12px;background:#fff;border:1px solid var(--linha)}
|
||||
.art.nova{background:#fff;box-shadow:0 0 0 2px rgba(255,168,26,.45)}
|
||||
.art .l1 b{font-size:15px;font-weight:700}
|
||||
.art label{font-size:12px}
|
||||
.art input{height:42px;font-size:15px;font-weight:600;border-radius:8px}
|
||||
.art .l1 b{font-size:16.5px;font-weight:700}
|
||||
.art label{font-size:14.5px}
|
||||
.art input{height:42px;font-size:16.5px;font-weight:600;border-radius:8px}
|
||||
.art .cps{grid-template-columns:repeat(2,minmax(0,160px))}
|
||||
.listaCab{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
|
||||
.listaCab h4{margin:0;font-size:18px;font-weight:700}
|
||||
.listaCab span{font-size:13px;color:var(--fraco)}
|
||||
.listaCab h4{margin:0;font-size:19px;font-weight:700}
|
||||
.listaCab span{font-size:15px;color:var(--fraco)}
|
||||
.rep{margin:0;padding:20px;border-radius:14px;background:#fff;border:1px solid var(--linha);display:flex;flex-direction:column;gap:16px}
|
||||
.repTopo{display:flex;gap:16px;align-items:flex-start}
|
||||
.miniF{flex:none;width:52px;height:96px;border-radius:6px;border:1px solid #D6D6D6;background:#F4F4F2;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.miniF img{width:100%;height:100%;object-fit:contain;display:block}
|
||||
.miniF span{font-size:11px;font-weight:800;color:var(--texto2)}
|
||||
.miniF span{font-size:13.5px;font-weight:800;color:var(--texto2)}
|
||||
.repInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.rep .l1{margin:0}
|
||||
.rep .l1 b{font-size:14px;font-weight:600;color:var(--preto)}
|
||||
.rep .l1 button{width:32px;height:32px;margin:-6px -6px -6px 0;border-radius:8px;font-size:18px}
|
||||
.rep .l1 b{font-size:16px;font-weight:600;color:var(--preto)}
|
||||
.rep .l1 button{width:32px;height:32px;margin:-6px -6px -6px 0;border-radius:8px;font-size:19px}
|
||||
.rep .l1 button:hover{background:#F1F1F1;color:var(--preto)}
|
||||
.repMed{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
||||
.repMed b{font-family:"Inter",sans-serif;font-size:22px;font-weight:700;letter-spacing:-.01em}
|
||||
.repMed span,.repPeso{font-size:13px;color:var(--fraco)}
|
||||
.repMed span,.repPeso{font-size:15px;color:var(--fraco)}
|
||||
.chips{list-style:none;margin:4px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:8px}
|
||||
.chip{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:999px;font-size:13px;font-weight:600;line-height:1.2}
|
||||
.chip{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:999px;font-size:15px;font-weight:600;line-height:1.2}
|
||||
.chip::before{font-weight:800}
|
||||
.chip.ok{background:var(--verde-fundo);color:var(--verde)}.chip.ok::before{content:"✓"}
|
||||
.chip.av{background:#FFF1DB;color:#8A4B00}.chip.av::before{content:"!"}
|
||||
.chip.er{background:#FDE8E6;color:#B3261E}.chip.er::before{content:"×"}
|
||||
.chip.fix{background:#E8F0FB;color:#1F4E8C}.chip.fix::before{content:"✓"}
|
||||
.chip.nt{background:#F1F1F1;color:var(--texto2)}
|
||||
.chipSo{align-self:flex-start;padding:5px 10px;border-radius:999px;background:#F1F1F1;color:var(--texto2);font-size:13px;font-weight:600}
|
||||
.avisoF{margin:4px 0 0;padding:10px 12px;border-radius:10px;background:#FDE8E6;font-size:13px;line-height:1.5;color:var(--preto)}
|
||||
.chipSo{align-self:flex-start;padding:5px 10px;border-radius:999px;background:#F1F1F1;color:var(--texto2);font-size:15px;font-weight:600}
|
||||
.avisoF{margin:4px 0 0;padding:10px 12px;border-radius:10px;background:#FDE8E6;font-size:15px;line-height:1.5;color:var(--preto)}
|
||||
.avisoF b{color:#B3261E}
|
||||
.repTxt{margin:2px 0 0;font-size:13.5px;line-height:1.5;color:var(--texto2)}
|
||||
.repTxt{margin:2px 0 0;font-size:15.5px;line-height:1.5;color:var(--texto2)}
|
||||
.repTxt b{color:var(--preto)}
|
||||
.confDet{font-size:13px}
|
||||
.confDet{font-size:15px}
|
||||
.confDet summary{display:inline;cursor:pointer;list-style:none;color:var(--laranja-escuro);font-weight:500}
|
||||
.confDet summary::-webkit-details-marker{display:none}
|
||||
.confDet summary:hover{text-decoration:underline}
|
||||
@@ -191,96 +191,96 @@ section+section{border-top:0}
|
||||
.confDet span{display:block;color:var(--texto2);line-height:1.6}
|
||||
.rep .cps{display:flex;align-items:center;gap:16px 24px;flex-wrap:wrap;margin:0;padding-top:16px;border-top:1px solid #EFEFEF}
|
||||
.rep .campo{display:flex;align-items:center;gap:12px}
|
||||
.rep .campo label{margin:0;font-size:14px;font-weight:600;color:var(--preto)}
|
||||
.rep .campo label{margin:0;font-size:16px;font-weight:600;color:var(--preto)}
|
||||
.passo{display:inline-flex;border:1px solid #D6D6D6;border-radius:10px;overflow:hidden}
|
||||
.passo button{width:44px;height:44px;border:0;background:#F9F9F9;font-size:18px;color:var(--preto);cursor:pointer}
|
||||
.passo button{width:44px;height:44px;border:0;background:#F9F9F9;font-size:19px;color:var(--preto);cursor:pointer}
|
||||
.passo button:hover{background:#EFEFEF}
|
||||
.rep .passo input{width:56px;height:44px;padding:0;border:0;border-radius:0;text-align:center;font-size:16px;font-weight:700;-moz-appearance:textfield;appearance:textfield}
|
||||
.rep .passo input{width:56px;height:44px;padding:0;border:0;border-radius:0;text-align:center;font-size:17.5px;font-weight:700;-moz-appearance:textfield;appearance:textfield}
|
||||
.passo input::-webkit-inner-spin-button,.passo input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
|
||||
.rep .un{display:inline-flex;align-items:center;gap:6px;height:44px;padding:0 12px;border:1px solid #D6D6D6;border-radius:10px}
|
||||
.rep .un input{width:64px;padding:0;border:0;font-size:16px;font-weight:700;text-align:right}
|
||||
.rep .un i{font-style:normal;font-size:14px;color:var(--fraco)}
|
||||
.rep .conta{margin-left:auto;font-size:14px;color:var(--texto2)}
|
||||
.rep .conta b{font-family:"Inter",sans-serif;font-size:16px;color:var(--preto)}
|
||||
.calc{font-size:13px;margin-top:8px}
|
||||
.dica{display:flex;align-items:center;gap:12px;margin-top:10px;padding:10px 12px;border:0;border-radius:8px;background:var(--verde-fundo);font-size:13px;color:#14532D}
|
||||
.rep .un input{width:64px;padding:0;border:0;font-size:17.5px;font-weight:700;text-align:right}
|
||||
.rep .un i{font-style:normal;font-size:16px;color:var(--fraco)}
|
||||
.rep .conta{margin-left:auto;font-size:16px;color:var(--texto2)}
|
||||
.rep .conta b{font-family:"Inter",sans-serif;font-size:17.5px;color:var(--preto)}
|
||||
.calc{font-size:15px;margin-top:8px}
|
||||
.dica{display:flex;align-items:center;gap:12px;margin-top:10px;padding:10px 12px;border:0;border-radius:8px;background:var(--verde-fundo);font-size:15px;color:#14532D}
|
||||
.dica.av{background:var(--aviso-fundo);color:#5A3A00}
|
||||
.dica button{display:inline-flex;width:auto;margin:0 0 0 auto;flex:none;height:34px;align-items:center;padding:0 12px;border-radius:7px;border:1px solid var(--verde);background:#fff;color:var(--verde);box-shadow:none;font-size:13px}
|
||||
.dica button{display:inline-flex;width:auto;margin:0 0 0 auto;flex:none;height:34px;align-items:center;padding:0 12px;border-radius:7px;border:1px solid var(--verde);background:#fff;color:var(--verde);box-shadow:none;font-size:15px}
|
||||
.dica button:hover{background:var(--verde);color:#fff;box-shadow:none}
|
||||
.req{position:static;padding:8px 0 0;border:0;border-radius:0;background:none;box-shadow:none;display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 20px}
|
||||
.reqcab{margin:0;font-size:13px;font-weight:400;color:var(--fraco)}
|
||||
.reqcab{margin:0;font-size:15px;font-weight:400;color:var(--fraco)}
|
||||
.req details,.req details:first-of-type{border:0}
|
||||
.reqLinks{display:contents}
|
||||
.reqLinks button{padding:0;border:0;background:none;font:inherit;font-size:13px;font-weight:500;color:var(--laranja-escuro);cursor:pointer}
|
||||
.reqLinks button{padding:0;border:0;background:none;font:inherit;font-size:15px;font-weight:500;color:var(--laranja-escuro);cursor:pointer}
|
||||
.reqLinks button:hover{text-decoration:underline}
|
||||
.reqLinks button.on{font-weight:700;color:var(--preto);text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--laranja)}
|
||||
/* The links above open and close the answers; a closed one is not shown. */
|
||||
.req details:not([open]){display:none}
|
||||
.req summary{display:inline;padding:0;font-size:13px;font-weight:500;color:var(--laranja-escuro)}
|
||||
.req summary{display:inline;padding:0;font-size:15px;font-weight:500;color:var(--laranja-escuro)}
|
||||
.req summary::before{display:none}
|
||||
.req details[open]{order:1;flex-basis:100%;padding:14px 16px 4px;border-radius:12px;background:var(--cinza)}
|
||||
.req details[open] summary{display:block;margin-bottom:6px;font-size:14px;font-weight:700;color:var(--preto);text-decoration:none}
|
||||
.req li{font-size:13px}
|
||||
.req .obs2{font-size:12px}
|
||||
.req details[open] summary{display:block;margin-bottom:6px;font-size:16px;font-weight:700;color:var(--preto);text-decoration:none}
|
||||
.req li{font-size:15px}
|
||||
.req .obs2{font-size:14.5px}
|
||||
|
||||
/* The buy box */
|
||||
.mont{position:sticky;top:var(--topo-passos,128px);padding:20px;border-radius:16px;background:#fff;box-shadow:none;display:flex;flex-direction:column;gap:12px}
|
||||
.mont>*{margin:0}
|
||||
.montcab b{font-size:17px;font-weight:800}
|
||||
.montpe{font-size:12px;text-align:center}
|
||||
.montcab b{font-size:18px;font-weight:800}
|
||||
.montpe{font-size:14.5px;text-align:center}
|
||||
.compra{display:flex;flex-direction:column;gap:12px;padding-top:16px;border-top:1px solid #EFEFEF}
|
||||
.compra .aoVivo{margin:0}
|
||||
.compra .aoVivo.pronto{display:none}
|
||||
#compraVals{display:flex;flex-direction:column;gap:10px;font-size:15px}
|
||||
#compraVals{display:flex;flex-direction:column;gap:10px;font-size:16.5px}
|
||||
#compraVals[hidden]{display:none}
|
||||
.cat.file .zoomB{display:none}
|
||||
.ampliar{margin-left:auto;height:32px;padding:0 12px;border-radius:8px;border:1px solid #D6D6D6;background:#fff;font-size:12.5px;font-weight:600;color:var(--preto);cursor:pointer}
|
||||
.ampliar{margin-left:auto;height:32px;padding:0 12px;border-radius:8px;border:1px solid #D6D6D6;background:#fff;font-size:15px;font-weight:600;color:var(--preto);cursor:pointer}
|
||||
.ampliar:hover{border-color:var(--preto)}
|
||||
.ampliar[hidden]{display:none}
|
||||
.cat.file #vArea{max-height:340px}
|
||||
.cEscada{display:flex;flex-direction:column;gap:8px;margin-bottom:4px}
|
||||
.cEscada>span{font-size:13px;font-weight:600}
|
||||
.cEscada>span{font-size:15px;font-weight:600}
|
||||
.cEscada ol{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:4px}
|
||||
.cEscada li{padding:8px 2px;border-radius:8px;background:#F4F4F2;border:1.5px solid transparent;display:flex;flex-direction:column;align-items:center;gap:2px}
|
||||
.cEscada li span{font-size:11px;color:var(--fraco);white-space:nowrap}
|
||||
.cEscada li b{font-size:13px;font-weight:500;color:var(--texto2)}
|
||||
.cEscada li span{font-size:13.5px;color:var(--fraco);white-space:nowrap}
|
||||
.cEscada li b{font-size:15px;font-weight:500;color:var(--texto2)}
|
||||
.cEscada li.on{background:var(--verde-fundo);border-color:var(--verde)}
|
||||
.cEscada li.on span,.cEscada li.on b{font-weight:700;color:var(--verde)}
|
||||
.cEscada li.on:last-child{background:#F1F1F1;border-color:var(--preto)}
|
||||
.cEscada li.on:last-child span,.cEscada li.on:last-child b{color:var(--preto)}
|
||||
.cLin>span:first-child{color:var(--texto2)}
|
||||
.cLin small{font-size:12px;color:var(--fraco)}
|
||||
.cEco{margin:-6px 0 0;text-align:right;font-size:13px;font-weight:600;color:var(--verde)}
|
||||
.cLin small{font-size:14.5px;color:var(--fraco)}
|
||||
.cEco{margin:-6px 0 0;text-align:right;font-size:15px;font-weight:600;color:var(--verde)}
|
||||
.cEco:empty{display:none}
|
||||
#cPartes{display:block}
|
||||
.ampliaDlg{width:min(960px,calc(100vw - 32px));max-height:calc(100vh - 48px);padding:0;border:0;border-radius:16px;box-shadow:0 20px 60px rgba(3,6,11,.3)}
|
||||
.ampliaDlg::backdrop{background:rgba(3,6,11,.55)}
|
||||
.ampliaCab{position:sticky;top:0;z-index:1;display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--linha);background:#fff}
|
||||
.ampliaCab b{font-size:16px;font-weight:800}
|
||||
.ampliaCab b{font-size:17.5px;font-weight:800}
|
||||
.ampliaCab button{width:40px;height:40px;border:0;border-radius:8px;background:#F1F1F1;font-size:20px;cursor:pointer}
|
||||
#ampliaArea .folhaPrevias{display:grid;gap:16px;padding:16px;background:#F8F9FA}
|
||||
#ampliaArea .folhaPrevia{margin:0;background:#fff;border:1px solid var(--linha);border-radius:8px;overflow:hidden}
|
||||
#ampliaArea .folhaPilha{display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||
#ampliaArea .folhaPilha img{display:block;width:auto;max-width:100%;height:auto;max-height:calc(100vh - 180px)!important}
|
||||
#ampliaArea figcaption{padding:8px 12px;font-size:13px;color:var(--texto2);border-top:1px solid var(--linha)}
|
||||
#ampliaArea figcaption{padding:8px 12px;font-size:15px;color:var(--texto2);border-top:1px solid var(--linha)}
|
||||
.cLin{display:flex;align-items:baseline;gap:8px}
|
||||
.cLin>span:last-child,.cLin>b{margin-left:auto;display:flex;gap:8px;align-items:baseline}
|
||||
.cLin s{font-size:13px;color:#707070}
|
||||
.cObs{margin:-4px 0 0;font-size:13px;color:var(--verde)}
|
||||
.cLin s{font-size:15px;color:#707070}
|
||||
.cObs{margin:-4px 0 0;font-size:15px;color:var(--verde)}
|
||||
.cObs:empty{display:none}
|
||||
.cTot{display:flex;align-items:baseline;padding-top:12px;border-top:1px solid #EFEFEF}
|
||||
.cTot span{font-family:"Inter",sans-serif;font-weight:700}
|
||||
.cTot b{margin-left:auto;font-family:"Inter",sans-serif;font-size:30px;font-weight:900;letter-spacing:-.01em}
|
||||
#qmsg{font-size:13px;line-height:1.45}
|
||||
#qmsg{font-size:15px;line-height:1.45}
|
||||
.compra .ciente{margin:0;padding:12px 14px}
|
||||
.compra .ciente h4{font-size:13.5px}
|
||||
.compra .ciente p,.compra .ciente .lista{font-size:13px}
|
||||
.cAdd{height:54px;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;cursor:pointer}
|
||||
.compra .ciente h4{font-size:15.5px}
|
||||
.compra .ciente p,.compra .ciente .lista{font-size:15px}
|
||||
.cAdd{height:54px;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:17.5px;cursor:pointer}
|
||||
.cAdd:hover:not(:disabled){background:#FFB53D}
|
||||
.cAdd:disabled{background:#EDEDED;color:#8A8D92;cursor:not-allowed}
|
||||
.cPe{margin:0;font-size:13px;color:var(--fraco);text-align:center}
|
||||
.cPe{margin:0;font-size:15px;color:var(--fraco);text-align:center}
|
||||
@media(max-width:1100px){.foco2{grid-template-columns:1fr}.mont{position:static}}
|
||||
@media(max-width:620px){.montcab b{font-size:15px}
|
||||
@media(max-width:620px){.montcab b{font-size:16.5px}
|
||||
.cabfoco h3{font-size:28px}
|
||||
.tipoEnvio .cam div{grid-template-columns:1fr}
|
||||
.tipoEnvio .cam div>span{display:none}
|
||||
@@ -298,68 +298,104 @@ section+section{border-top:0}
|
||||
/* Cart */
|
||||
.itensCab{display:flex;align-items:baseline;gap:12px;margin-bottom:11px}
|
||||
.itensCab h4{margin:0}
|
||||
.esvaziar{margin-left:auto;border:0;background:none;padding:6px 0;font-size:13.5px;font-weight:600;color:var(--fraco);cursor:pointer;text-decoration:underline;text-underline-offset:3px}
|
||||
.esvaziar{margin-left:auto;border:0;background:none;padding:6px 0;font-size:15.5px;font-weight:600;color:var(--fraco);cursor:pointer;text-decoration:underline;text-underline-offset:3px}
|
||||
.esvaziar:hover{color:#B8391C}
|
||||
.esvaziar[hidden]{display:none}
|
||||
.item{align-items:center}
|
||||
.item .x{flex:none;height:36px;padding:0 12px;border-radius:8px;border:1px solid #D6D6D6;background:#fff;color:var(--texto2);font-size:13px;font-weight:600;font-family:inherit;cursor:pointer}
|
||||
.item .marca{flex:none;display:flex;align-items:center;cursor:pointer}
|
||||
.item .marca input{width:20px;height:20px;margin:0;accent-color:var(--laranja);cursor:pointer}
|
||||
.miniItem{position:relative;flex:none;width:52px;height:52px;border-radius:8px;border:1px solid var(--linha);background:repeating-conic-gradient(#F1F1F1 0 25%,#fff 0 50%) 0 0/12px 12px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.miniItem img{max-width:100%;max-height:100%;object-fit:contain}
|
||||
.miniItem i{font-style:normal;font-size:12px;font-weight:700;color:var(--fraco)}
|
||||
.miniItem em{position:absolute;right:2px;bottom:2px;padding:0 4px;border-radius:5px;background:rgba(3,6,11,.75);color:#fff;font-style:normal;font-size:11px;font-weight:700}
|
||||
.item.fora .miniItem,.item.fora .nm2,.item.fora .vl2{opacity:.45}
|
||||
.item .x{flex:none;height:36px;padding:0 12px;border-radius:8px;border:1px solid #D6D6D6;background:#fff;color:var(--texto2);font-size:15px;font-weight:600;font-family:inherit;cursor:pointer}
|
||||
.item .x:hover{border-color:#B8391C;color:#B8391C}
|
||||
.desfazer{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:80;display:flex;align-items:center;gap:16px;padding:12px 14px 12px 18px;border-radius:12px;background:var(--preto);color:#fff;font-size:14px;box-shadow:0 10px 30px rgba(3,6,11,.25);max-width:calc(100% - 32px);box-sizing:border-box}
|
||||
.desfazer{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:80;display:flex;align-items:center;gap:16px;padding:12px 14px 12px 18px;border-radius:12px;background:var(--preto);color:#fff;font-size:16px;box-shadow:0 10px 30px rgba(3,6,11,.25);max-width:calc(100% - 32px);box-sizing:border-box}
|
||||
.desfazer[hidden]{display:none}
|
||||
.desfazer button{flex:none;height:36px;padding:0 14px;border-radius:8px;border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:14px;cursor:pointer}
|
||||
.desfazer button{flex:none;height:36px;padding:0 14px;border-radius:8px;border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;cursor:pointer}
|
||||
@media(max-width:820px){.desfazer{bottom:100px}}
|
||||
.carrAviso{grid-column:1/-1;font-size:13px;color:var(--fraco)}
|
||||
.carrAviso{grid-column:1/-1;font-size:15px;color:var(--fraco)}
|
||||
.carrAviso:empty{display:none}
|
||||
.carr{border-radius:16px}
|
||||
.carr h3{font-size:24px;font-weight:800;letter-spacing:-.02em}
|
||||
.cxE{border-radius:var(--raio);border-color:var(--linha)}
|
||||
.cxE h4,.resumo h4{font-family:"Inter",sans-serif;font-weight:800;font-size:17px}
|
||||
.cxE h4,.resumo h4{font-family:"Inter",sans-serif;font-weight:800;font-size:18px}
|
||||
.cp input,.cepL input{height:46px;border-radius:var(--raio2)}
|
||||
.cp input:focus,.cepL input:focus{border-color:var(--laranja2);box-shadow:0 0 0 3px rgba(255,168,26,.18)}
|
||||
.opE{border-radius:12px}
|
||||
.opE.on{border:2px solid var(--laranja);background:var(--aviso-fundo)}
|
||||
.resumo{position:sticky;top:var(--topo-passos,128px);border-radius:16px}
|
||||
.tot b{font-family:"Inter",sans-serif;font-weight:900}
|
||||
#bPagar{height:54px;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);border:0;font-family:"Inter",sans-serif;font-weight:800;font-size:16px}
|
||||
#bPagar{height:auto;min-height:54px;padding:8px 14px;line-height:1.25;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);border:0;font-family:"Inter",sans-serif;font-weight:800;font-size:17.5px}
|
||||
#bPagar:hover:not(:disabled){background:#FFB53D}
|
||||
#bPagar:disabled{background:#EDEDED;color:#8A8D92}
|
||||
#bMais{height:46px;border-radius:var(--raio2);font-family:"Inter",sans-serif;font-weight:600}
|
||||
.checkout{max-width:760px;margin:20px auto 0}
|
||||
.checkout #checkoutStatus:not(:empty){padding:16px 18px;border-radius:var(--raio);background:var(--verde-fundo);border:1px solid #BFE6CE;color:#14532D;font-size:15px}
|
||||
.checkout #checkoutStatus:not(:empty){padding:16px 18px;border-radius:var(--raio);background:var(--verde-fundo);border:1px solid #BFE6CE;color:#14532D;font-size:16.5px}
|
||||
#checkoutActions{margin-top:12px}
|
||||
/* Only our own fields: Mercado Pago's card form (its instalment radios) keeps its own styles. */
|
||||
#checkoutActions .pixBox input{height:44px;border-radius:var(--raio2);border:1px solid #D6D6D6;padding:0 12px}
|
||||
/* The payment pages: paying on the left, the order summary on the right */
|
||||
html:is([data-rota="pagamento"],[data-rota="pix"]) .checkout{max-width:1040px;margin-top:8px}
|
||||
html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:32px;align-items:start}
|
||||
.envioArq{font-size:13px;color:var(--texto2);margin-top:14px;line-height:1.5}
|
||||
.ic{width:21px;height:21px;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
|
||||
.acts a .ic{width:23px;height:23px}
|
||||
.cartIc{position:relative;display:inline-flex}
|
||||
.acts #cartLink{gap:11px}
|
||||
.cartN{position:absolute;top:-7px;right:-9px;min-width:17px;height:17px;padding:0 4px;border-radius:9px;background:var(--laranja);color:var(--preto);font-size:13px;font-weight:800;line-height:17px;text-align:center}
|
||||
.cartN[hidden]{display:none}
|
||||
.zona .ico .ic{width:23px;height:23px}
|
||||
.art2 .giros .gr{display:inline-flex;align-items:center;justify-content:center}
|
||||
.art2 .giros .gr .ic{width:22px;height:22px}
|
||||
.rep .l1 button{display:inline-flex;align-items:center;justify-content:center}
|
||||
.rep .l1 button .ic{width:20px;height:20px}
|
||||
#ampliaFecha .ic{width:23px;height:23px}
|
||||
@media (max-width:600px){.acts .actTx{display:none}.acts{gap:16px}}
|
||||
.cAdd,#bPagar,#pOk,#qOk{display:flex;align-items:center;justify-content:center;gap:8px}
|
||||
#pOk{display:inline-flex}
|
||||
.cAdd .ic,#bPagar .ic{width:22px;height:22px}
|
||||
.item .x{display:inline-flex;align-items:center;gap:6px}
|
||||
.item .x .ic{width:18px;height:18px}
|
||||
.opE b .ic{width:19px;height:19px;margin:-2px 6px 0 0;color:var(--laranja-escuro)}
|
||||
.metodo .ic{width:23px;height:23px;color:var(--texto2)}
|
||||
.codigo .pagBtn{display:inline-flex;align-items:center;gap:7px}
|
||||
.codigo .pagBtn .ic{width:20px;height:20px}
|
||||
.pixBox .prazo{display:flex;align-items:center;gap:8px}
|
||||
.pixBox .prazo .ic{width:23px;height:23px}
|
||||
.confirmado h4{display:flex;align-items:center;gap:8px}
|
||||
.confirmado h4 .ic{width:24px;height:24px}
|
||||
.envioArq .ic{width:19px;height:19px;margin:-2px 6px 0 0}
|
||||
.qi .c .ic{width:15px;height:15px;stroke-width:3}
|
||||
.passos a span .ic{width:17px;height:17px;stroke-width:3}
|
||||
.obr{color:#B42318;font-weight:700}
|
||||
.envioNota{display:block;margin-top:4px;font-size:14px;color:var(--fraco)}
|
||||
.envioArq{font-size:15px;color:var(--texto2);margin-top:14px;line-height:1.5}
|
||||
.envioArq.ok{color:var(--verde)}
|
||||
.envioArq.erro{color:#B42318}
|
||||
.envioDeNovo{margin-left:4px;padding:0;border:0;background:none;color:inherit;font:inherit;font-weight:700;text-decoration:underline;cursor:pointer}
|
||||
.envioBarra{height:6px;border-radius:999px;background:#EDEDED;overflow:hidden;margin-bottom:6px}
|
||||
.envioBarra i{display:block;height:100%;background:var(--laranja);border-radius:999px;transition:width .4s}
|
||||
.cartEnvio{font-size:13px;font-weight:700;color:var(--laranja-escuro);margin-left:6px}
|
||||
.cartEnvio{font-size:15px;font-weight:700;color:var(--laranja-escuro);margin-left:6px}
|
||||
.cartEnvio:empty{display:none}
|
||||
.envioArq:empty{display:none}
|
||||
.dicaEnd{font-size:13px;color:var(--texto2);margin-top:12px}
|
||||
.dicaEnd:empty{display:none}
|
||||
.pagCab{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:20px}
|
||||
.pagCab h3{font-size:26px;font-weight:800;letter-spacing:-.02em}
|
||||
.pagVolta{font-size:14px;color:var(--texto2)}
|
||||
#checkoutActions > h4,#checkoutActions .pixBox > h4{font-size:17px;font-weight:800;margin-bottom:12px}
|
||||
.pagNota{font-size:14px;color:var(--texto2);margin:4px 0 16px;line-height:1.5}
|
||||
.pagVolta{font-size:16px;color:var(--texto2)}
|
||||
#checkoutActions > h4,#checkoutActions .pixBox > h4{font-size:18px;font-weight:800;margin-bottom:12px}
|
||||
.pagNota{font-size:16px;color:var(--texto2);margin:4px 0 16px;line-height:1.5}
|
||||
.pagResumo{position:sticky;top:var(--topo-passos,128px);border:1px solid var(--linha);border-radius:16px;background:#fff;padding:20px 22px}
|
||||
.pagResumo h4{font-size:17px;font-weight:800;margin-bottom:10px}
|
||||
.pagResumo .l{display:flex;justify-content:space-between;gap:12px;font-size:14px;padding:5px 0}
|
||||
.pagResumo h4{font-size:18px;font-weight:800;margin-bottom:10px}
|
||||
.pagResumo .l{display:flex;justify-content:space-between;gap:12px;font-size:16px;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-family:"Inter",sans-serif;font-weight:900;font-size:24px}
|
||||
.metodos{display:grid;grid-template-columns:1fr;gap:10px;max-width:420px;margin-bottom:20px}
|
||||
.metodo{height:56px;display:flex;align-items:center;gap:12px;padding:0 16px;border:2px solid var(--linha);border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;text-align:left;cursor:pointer}
|
||||
.metodo{height:56px;display:flex;align-items:center;gap:12px;padding:0 16px;border:2px solid var(--linha);border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:16.5px;text-align:left;cursor:pointer}
|
||||
.metodo::before{content:"";width:18px;height:18px;flex-shrink:0;border-radius:50%;border:2px solid #B5B8BD;box-sizing:border-box}
|
||||
.metodo[aria-checked="true"]{border-color:var(--laranja);background:var(--aviso-fundo)}
|
||||
.metodo[aria-checked="true"]::before{border:6px solid var(--laranja)}
|
||||
.pagBtn{height:48px;padding:0 24px;margin:0 10px 0 0;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:15px;cursor:pointer}
|
||||
.pagBtn{height:48px;padding:0 24px;margin:0 10px 0 0;border-radius:var(--raio2);border:0;background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16.5px;cursor:pointer}
|
||||
.pagBtn:hover:not(:disabled){background:#FFB53D}
|
||||
.pagBtn:disabled{background:#EDEDED;color:#8A8D92;cursor:default}
|
||||
.pagBtn.sec{background:#fff;border:1px solid #D6D6D6}
|
||||
@@ -367,12 +403,12 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
|
||||
.pixBox{display:flex;flex-direction:column;align-items:flex-start;gap:12px}
|
||||
.pixBox img{border:1px solid var(--linha);border-radius:12px;padding:8px;background:#fff}
|
||||
.pixBox .codigo{display:flex;gap:10px;width:100%}
|
||||
.pixBox .codigo input{flex:1;min-width:0;font-size:13px}
|
||||
.pixBox .aguarda{font-size:14px;color:var(--texto2)}
|
||||
.pixBox .prazo{font-family:"Inter",sans-serif;font-weight:800;font-size:18px;font-variant-numeric:tabular-nums}
|
||||
.pixBox .codigo input{flex:1;min-width:0;font-size:15px}
|
||||
.pixBox .aguarda{font-size:16px;color:var(--texto2)}
|
||||
.pixBox .prazo{font-family:"Inter",sans-serif;font-weight:800;font-size:19px;font-variant-numeric:tabular-nums}
|
||||
.confirmado{border:1px solid #BFE6CE;background:var(--verde-fundo);border-radius:16px;padding:24px}
|
||||
.confirmado h4{font-size:22px;color:#14532D;margin-bottom:8px}
|
||||
.confirmado p{font-size:15px;color:#14532D;margin-bottom:18px;line-height:1.5}
|
||||
.confirmado p{font-size:16.5px;color:#14532D;margin-bottom:18px;line-height:1.5}
|
||||
@media(max-width:820px){
|
||||
html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{grid-template-columns:1fr;gap:20px}
|
||||
.pagResumo{position:static;order:-1}
|
||||
@@ -384,26 +420,26 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
|
||||
.info2-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:56px;align-items:start}
|
||||
.info2-preco{display:flex;flex-direction:column;gap:16px}
|
||||
.info2 h2{font-size:clamp(26px,3vw,34px);font-weight:800;letter-spacing:-.02em}
|
||||
.info2-preco>p{font-size:15px;color:var(--texto2)}
|
||||
.info2 table{border-collapse:collapse;background:#fff;border:1px solid var(--linha);border-radius:12px;overflow:hidden;font-size:15px}
|
||||
.info2 th{text-align:left;background:var(--preto);color:#fff;font-family:"Inter",sans-serif;font-size:13px;padding:12px 18px}
|
||||
.info2-preco>p{font-size:16.5px;color:var(--texto2)}
|
||||
.info2 table{border-collapse:collapse;background:#fff;border:1px solid var(--linha);border-radius:12px;overflow:hidden;font-size:16.5px}
|
||||
.info2 th{text-align:left;background:var(--preto);color:#fff;font-family:"Inter",sans-serif;font-size:15px;padding:12px 18px}
|
||||
.info2 td{padding:11px 18px;border-top:1px solid #EFEFEF}
|
||||
.info2 td.melhor{font-weight:700;color:var(--verde)}
|
||||
.info2 .nota2{font-size:13px;color:#707070}
|
||||
.info2 .nota2{font-size:15px;color:#707070}
|
||||
.info2-vant{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
|
||||
.info2-vant>div{padding:22px;border-radius:var(--raio);background:#fff;border:1px solid var(--linha);display:flex;flex-direction:column;gap:6px}
|
||||
.info2-vant b{font-family:"Inter",sans-serif;font-size:16px}
|
||||
.info2-vant b{font-family:"Inter",sans-serif;font-size:17.5px}
|
||||
.info2-vant b.n{font-size:32px;font-weight:900;color:var(--laranja2)}
|
||||
.info2-vant span{font-size:14px;color:var(--fraco)}
|
||||
.info2-vant span{font-size:16px;color:var(--fraco)}
|
||||
|
||||
/* Phone total bar */
|
||||
.barraM{display:none}
|
||||
@media (max-width:820px){
|
||||
.barraM:not([hidden]){display:flex;position:fixed;left:0;right:0;bottom:0;z-index:40;align-items:center;gap:12px;padding:12px 16px calc(12px + env(safe-area-inset-bottom));background:#fff;border-top:1px solid var(--linha);box-shadow:0 -6px 20px rgba(3,6,11,.08)}
|
||||
.barraM div{display:flex;flex-direction:column}
|
||||
.barraM span{font-size:12px;color:#707070}
|
||||
.barraM span{font-size:14.5px;color:#707070}
|
||||
.barraM b{font-family:"Inter",sans-serif;font-size:21px;font-weight:900}
|
||||
.barraM a{margin-left:auto;height:50px;padding:0 22px;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;display:flex;align-items:center;text-decoration:none}
|
||||
.barraM a{margin-left:auto;height:50px;padding:0 22px;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:17.5px;display:flex;align-items:center;text-decoration:none}
|
||||
.w{padding:0 16px}
|
||||
.hero2{padding:40px 0}
|
||||
.hero2-grid,.info2-grid{grid-template-columns:1fr;gap:28px}
|
||||
@@ -414,7 +450,7 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
|
||||
.ec-go{margin-left:0;width:100%;justify-content:center}
|
||||
.focoGrid{grid-template-columns:1fr}
|
||||
.mont,.resumo{position:static}
|
||||
.passos{gap:8px;font-size:12.5px;margin:0 0 16px;padding:10px 16px}
|
||||
.passos{gap:8px;font-size:15px;margin:0 0 16px;padding:10px 16px}
|
||||
.passos i{width:14px}
|
||||
.passos a{flex-direction:column;gap:4px;text-align:center}
|
||||
.info2-vant{grid-template-columns:1fr}
|
||||
@@ -429,23 +465,29 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
|
||||
@keyframes skel{0%{background-position:100% 50%}100%{background-position:0 50%}}
|
||||
@media (prefers-reduced-motion:reduce){.skel{animation:none}}
|
||||
.folhaSkel{display:flex;align-items:center;justify-content:center;max-height:520px;min-height:180px;width:100%}
|
||||
.folhaSkel span{font-size:12.5px;color:var(--fraco);background:#fffc;padding:4px 10px;border-radius:999px}
|
||||
.folhaSkel span{font-size:15px;color:var(--fraco);background:#fffc;padding:4px 10px;border-radius:999px}
|
||||
#vArea .folhaPilha{display:flex;flex-direction:column;gap:4px;max-height:520px;overflow:auto;background:#fff}
|
||||
#vArea .folhaPilha img{width:100%;height:auto;display:block}
|
||||
.folhaMais{font-size:12px;color:var(--texto2);text-align:center;padding:8px}
|
||||
.folhaMais{font-size:14.5px;color:var(--texto2);text-align:center;padding:8px}
|
||||
.skelLinha{height:14px;margin:8px 0}
|
||||
.skelLinha.curta{width:40%}
|
||||
.skelBloco{height:160px;margin-top:8px}
|
||||
|
||||
/* Artes avulsas: the file card of a finished sheet, plus width, rotation and sizes */
|
||||
.rep.nova{box-shadow:0 0 0 2px rgba(255,168,26,.45)}
|
||||
.novaT{margin-left:8px;padding:2px 7px;border-radius:999px;background:#FFF1DB;color:#8A4B00;font-size:11px;font-style:normal;font-weight:800;text-transform:uppercase;letter-spacing:.04em}
|
||||
.repMed .pede{font-size:16px;color:var(--texto2)}
|
||||
.novaT{margin-left:8px;padding:2px 7px;border-radius:999px;background:#FFF1DB;color:#8A4B00;font-size:13.5px;font-style:normal;font-weight:800;text-transform:uppercase;letter-spacing:.04em}
|
||||
.repMed .pede{font-size:17.5px;color:var(--texto2)}
|
||||
.art2 .giros{display:inline-flex;gap:6px}
|
||||
.art2 .giros .gr{width:44px;height:44px;border:1px solid #D6D6D6;border-radius:10px;background:#fff;font-size:16px;cursor:pointer}
|
||||
.art2 .giros .gr{width:44px;height:44px;border:1px solid #D6D6D6;border-radius:10px;background:#fff;font-size:17.5px;cursor:pointer}
|
||||
.art2 .giros .gr.on{border-color:var(--laranja);background:var(--aviso-fundo)}
|
||||
.art2 .atalhos{flex-basis:100%;display:flex;flex-wrap:wrap;gap:8px;margin:0}
|
||||
.art2 .atalhos .tm{height:34px;padding:0 12px;border:1px solid #D6D6D6;border-radius:8px;background:#fff;font-size:13px;font-weight:600;cursor:pointer}
|
||||
.art2 .atalhos .tm{height:34px;padding:0 12px;border:1px solid #D6D6D6;border-radius:8px;background:#fff;font-size:15px;font-weight:600;cursor:pointer}
|
||||
.art2 .atalhos .tm.on{border-color:var(--verde);background:var(--verde-fundo);color:var(--verde)}
|
||||
.art2 .dica{margin-top:4px}
|
||||
.rep .l1 .novaT{margin-right:auto}
|
||||
|
||||
/* On a computer the Site is drawn at 80%: the sizes above were chosen looking
|
||||
at it that way, and at 100% everything felt too big. Phones and tablets
|
||||
keep their own sizes. The zoom scales viewport units too, so the page's
|
||||
full height is 125vh there (site-shell.css keeps the footer at the bottom). */
|
||||
@media (min-width:1100px){html{zoom:.8}body{min-height:125vh}}
|
||||
|
||||
Reference in New Issue
Block a user