Files
rar-autopass/app/web/templates/base.html
Claude Agent 3ba35abed2 feat(api): raspuns minim implicit + corelare cerere-raspuns + sugestie cod in raspuns; coloana # in lista trimiteri
- POST /v1/prezentari: camp optional 'raspuns' (minim implicit / complet);
  forma minima = 6 chei fixe (index, submission_id, vehicul, status, motiv,
  id_prezentare), formatul detaliat ramane la cerere
- corelare rezultat<->cerere: index 0-based + ecou vin/nr_inmatriculare
- sugestia principala de cod RAR pe nemapate (GOLD partajat > SILVER >
  embeddings > fuzzy peste prag), in motiv si in nemapate[].sugestie
  (si pe dry-run /valideaza); suggestion-only, nu se aplica automat
- lista Trimiteri: coloana # cu id-ul trimiterii (desktop + mobil)
- README: sectiune 'Mapari si sugestii de cod RAR'; contract actualizat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 08:32:55 +00:00

1641 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}ROA AUTOPASS{% endblock %}</title>
<script src="/static/htmx.min.js"></script>
<script>
// Raspunsurile de editare-rand contin un <tr> (swap pe rand) PLUS
// elemente OOB non-rand (#preview-rezumat, #preview-ok-count). Fara fragmente-template,
// htmx parseaza raspunsul care incepe cu <tr> in context de tabel (<table><tbody>) si
// "foster-parent"-eaza div/span-urile OOB afara din fragment -> swapError + contoare pierdute.
// useTemplateFragments parseaza tot intr-un <template>, pastrand rand + OOB impreuna.
htmx.config.useTemplateFragments = true;
</script>
<script>
// Anti-FOUC: citeste preferinta tema din localStorage inainte de primul
// paint; seteaza data-theme pe <html> sincron, fara blink.
// Cunoaste TOATE cele 7+1 teme: light/dark/petrol/grafit/cobalt/cupru/hartie + auto.
// Valori legacy (light/dark/petrol) raman valide — fara migrare fortata.
// Valoare lipsa/necunoscuta -> auto (fallback sigur, fara blink).
// 'auto' se rezolva la 'light' sau 'dark' dupa prefers-color-scheme (fara blink):
// auto + dark OS -> 'dark' | auto + light OS -> 'light' (comportament existent pastrat).
(function() {
var VALID = {light:1, dark:1, petrol:1, grafit:1, cobalt:1, cupru:1, hartie:1, auto:1};
try {
var t = localStorage.getItem('theme');
if (!t || !VALID[t]) t = 'auto'; // fallback: valoare lipsa sau necunoscuta -> auto
if (t === 'auto') {
t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
document.documentElement.setAttribute('data-theme', t);
} catch(e) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
/* Stive de font standard web. Toate regulile font-face IBM Plex sterse.
Motiv: decizie user, uniformitate cross-page.
Fisierele woff2 raman pe disc (curatare = follow-up optional, non-blocant).
Referinte catre directorul de fonturi statice eliminate — font-ui si font-mono sunt stive sistem. */
/* Paleta dark (default) — accent azur ROMFAST.
--card2: fundal input/contor (= --bg, nivelul cel mai adanc).
--line2: separator subtire (intre --bg si --line). */
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6;
/* Stive font standard web — sursa unica de adevar */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
/* Scala tipografica uniforma — sursa unica de adevar */
--fs-xs:12px; --fs-sm:13.5px; --fs-base:15px; --fs-md:16px;
--fs-lg:18px; --fs-xl:20px; --fs-2xl:28px; --fs-3xl:34px;
--lh-tight:1.25; --lh-body:1.55; }
/* Paleta light — accent azur inchis pentru contrast AA pe alb (#1F66C9: 5.51:1 pe alb) */
[data-theme="light"] { --bg:#f5f7fa; --card:#ffffff; --card2:#f5f7fa; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea; --line2:#eaedf2;
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#1F66C9; }
/* Paleta Petrol — tema intunecata alternativa, accent teal #0E7C7B.
Wordmark-ul FAST #2E74D6 coexista armonios: ambele sunt reci/saturate, contrast AA pe --card #161e20. */
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
Distinta de dark la cererea userului. */
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
[data-theme="cobalt"] { --bg:#080d1c; --card:#111a33; --card2:#0b1226; --ink:#e9ecfb; --muted:#8a93b8; --line:#1d2747; --line2:#161f3a;
--ok:#2fd0a6; --warn:#E0A93B; --err:#f06a7a; --accent:#8aa0ff; }
/* Paleta Cupru — fundal cald ciocolata, accent chihlimbar (#dfa45c = landing --infot). */
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
Similara cu light, distinta la cererea userului. */
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
* { box-sizing:border-box; }
/* CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
CSS custom properties NU functioneaza in `@media`, deci pragul nu poate fi o
variabila; folosim consecvent `@media (max-width:767px)` peste tot (mobil) si
`@media (max-width:1024px)` doar pentru densitatea tabelului. >=1024px = layout
desktop neschimbat (fara regresie). Orice regula mobila noua reutilizeaza 767px. */
body { margin:0; font-family:var(--font-ui); font-size:var(--fs-base); line-height:var(--lh-body);
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* Grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). */
header { padding:16px 24px; border-bottom:1px solid var(--line);
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; min-height:92px; }
.header-left { display:flex; align-items:center; }
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
/* Logo ROMFAST aliniat stanga; transparent, ok pe dark/light/petrol fara filtre de culoare. */
.brand-logo { height:60px; width:auto; display:block; margin:0; }
.header-center .env { font-size:11px; margin-top:2px; }
header h1 { font-size:20px; margin:0; font-weight:700; letter-spacing:-.01em; }
header .env { font-size:12px; color:var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:99px; }
main { padding:24px; max-width:1100px; margin:0 auto; }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:16px 20px; margin-bottom:16px; }
.banner { border-left:3px solid var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); }
.banner.hidden { display:none; }
/* Tabelele de date au multe coloane; pe ecrane inguste scroll IN card, nu
impinge layout-ul paginii (altfel toata pagina scrolleaza orizontal). */
.tablewrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }
table { width:100%; border-collapse:collapse; font-size:14px; font-variant-numeric:tabular-nums; }
th,td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); white-space:nowrap; }
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.empty { color:var(--muted); padding:24px; text-align:center; }
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
/* Badge sursa sugestie: de unde vine sugestia de cod in editorul de mapare.
confirmat = GOLD validat de om (verde); similar = SILVER/embedding k-NN (azur);
non-operatie = pre-filtru NUL / vecin NUL (gri-cald). Suggestion-only, doar indiciu vizual. */
.sugg-sursa { display:inline-block; font-size:10px; font-weight:700; line-height:1; padding:2px 6px;
border-radius:99px; text-transform:uppercase; letter-spacing:.03em; vertical-align:middle;
border:1px solid transparent; }
.sugg-sursa--confirmat { color:var(--ok); border-color:color-mix(in srgb, var(--ok) 45%, transparent);
background:color-mix(in srgb, var(--ok) 12%, transparent); }
.sugg-sursa--similar { color:var(--accent); border-color:color-mix(in srgb, var(--accent) 45%, transparent);
background:color-mix(in srgb, var(--accent) 12%, transparent); }
.sugg-sursa--nul { color:var(--muted); border-color:color-mix(in srgb, var(--muted) 40%, transparent);
background:color-mix(in srgb, var(--muted) 12%, transparent); }
/* Pill-uri de filtrare a starii (bara de filtre Trimiteri). Inactiv = contur+text pe
culoarea categoriei (injectata inline); activ = umplere pe acea culoare. */
.pills-categorii { display:inline-flex; gap:8px; flex-wrap:wrap; align-items:center; }
.pill-cat { display:inline-flex; align-items:center; gap:5px; padding:4px 11px; border-radius:99px;
font-size:12px; font-weight:600; cursor:pointer; background:transparent;
border:1.5px solid var(--line); color:var(--muted); min-height:30px;
transition:background .15s, color .15s; }
/* Hover: color-mix pe culoarea curenta a pill-ului (categoria sa), nu filter:brightness
(care producea rosu plin ilizibil pe pill-uri colorate). Activ suprima hover-ul. */
.pill-cat:hover { background:color-mix(in srgb, currentColor 12%, transparent); }
.pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px;
border-radius:99px; min-width:18px; text-align:center; }
/* Activ categorie: umplere cu culoarea categoriei (currentColor = var injectat inline) */
.pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; }
.pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; }
/* Activ suprima hover: pastram culoarea activa, nu o mixam din nou */
.pill-cat[aria-pressed="true"]:hover { background:currentColor; }
/* Reset "Toate" activ = --accent plin (nu culoarea categoriei) */
.pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); }
.pill-cat-reset[aria-pressed="true"]:hover { background:var(--accent); }
.s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);}
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
.s-ok{color:var(--ok);}
.s-needs_review{color:var(--warn);}
.s-already_sent,.s-duplicate_in_file,.s-excluded{color:var(--muted);}
/* Badge mediu RAR — semantica risc L.142.
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
NU se foloseste verde pentru Productie (ar semnala "sigur"). */
.env-badge-prod { display:inline-block; padding:1px 7px; border-radius:99px; font-size:11px; font-weight:700; letter-spacing:.02em;
background:color-mix(in srgb, var(--err) 80%, var(--card)); color:#fff; border:1px solid transparent; }
.env-badge-test { display:inline-block; padding:1px 7px; border-radius:99px; font-size:11px;
background:transparent; color:var(--muted); border:1px solid var(--line); }
/* Badge mediu RAR din header: eticheta scurta PROD/TEST ramane mereu vizibila pe
mobil (semnal de risc L.142), eticheta lunga se ascunde sub 560px. Clasele
.env-badge-prod/.env-badge-test isi redefinesc deja border-ul complet, deci
suprascriu stilul global de buton fara reguli suplimentare. */
.env-tx-short { display:none; }
@media (max-width:560px) {
.env-tx-full { display:none; }
.env-tx-short { display:inline; }
}
.muted { color:var(--muted); }
/* Heading/eticheta accesibila doar pentru cititoare de ecran (vizual ascunsa). */
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
clip:rect(0 0 0 0); white-space:nowrap; border:0; }
a { color:var(--accent); }
/* Drop zone upload fisier */
.drop-zone { border:2px dashed var(--line); border-radius:8px; padding:32px 20px;
text-align:center; transition:border-color .15s,background .15s; }
.drop-zone.drag-over { border-color:var(--accent); background:rgba(91,141,239,.05); }
/* Banner varianta warn (nu eroare) */
.banner.warn { border-left-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); }
/* Bara confirmare sticky */
.sticky-bar { position:sticky; bottom:0; background:var(--card); border-top:1px solid var(--line);
padding:10px 14px; display:flex; align-items:flex-start; gap:12px;
flex-wrap:wrap; z-index:10; }
/* Indicator HTMX — ascuns pana la request */
.htmx-indicator { display:none; }
/* Cerc rotitor pentru indicatorii de procesare (import: parsare + sugestii) */
.spin-dot { display:inline-block; width:13px; height:13px; border:2px solid var(--line);
border-top-color:var(--accent); border-radius:50%; vertical-align:-2px;
margin-right:6px; animation:spin-dot .7s linear infinite; }
@keyframes spin-dot { to { transform:rotate(360deg); } }
/* Selector tema stil pill — icon + eticheta temei curente.
Eticheta se ascunde pe <=560px (spatiu ingust), ramane iconita. */
.tema-btn { display:inline-flex; align-items:center; gap:6px; height:36px; padding:0 12px;
border-radius:8px; background:transparent; border:1px solid var(--line);
color:var(--muted); font-family:var(--font-ui); font-size:var(--fs-sm);
cursor:pointer; transition:border-color .15s, color .15s; line-height:1; }
.tema-btn:hover { border-color:var(--accent); color:var(--ink); }
.tema-btn:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@media (max-width:560px) { #tema-label { display:none; } }
/* Dot RAR compact in antet.
Stare OK: dot verde pulsant + "RAR online". Stare BLOCAT: dot rosu.
Stilat ca pill; sensul NU depinde de culoare (aria-label + title). */
.rar-chip { display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px;
border-radius:99px; font-size:var(--fs-sm); font-weight:600; cursor:default; white-space:nowrap; }
.rar-chip.rar-ok { border:1px solid color-mix(in srgb,var(--ok) 35%,var(--line));
background:color-mix(in srgb,var(--ok) 10%,transparent); color:var(--ok); }
.rar-chip.rar-err { border:1px solid color-mix(in srgb,var(--err) 35%,var(--line));
background:color-mix(in srgb,var(--err) 10%,transparent); color:var(--err); }
.rar-dot { width:9px; height:9px; border-radius:99px; background:currentColor; flex-shrink:0;
box-shadow:0 0 0 3px color-mix(in srgb,currentColor 20%,transparent); }
.rar-dot.live { animation:rar-pulse 2s ease-in-out infinite; }
@keyframes rar-pulse { 0%,100%{opacity:1;} 50%{opacity:.5;} }
@media (max-width:560px) { .rar-chip .rar-tx { display:none; } }
/* Sub-titlu cu numele service-ului (cand autentificat). */
.h-sub { font-size:var(--fs-xs); color:var(--muted); margin-top:2px; line-height:1.2; }
.h-sub .svc { color:var(--ink); font-weight:600; }
/* Badge env (test/prod) si badge tier (plan cont) langa titlu. */
.badge-env { display:inline-block; margin-left:8px; padding:1px 7px; border-radius:99px;
font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em;
color:var(--warn); background:color-mix(in srgb,var(--warn) 16%,transparent); vertical-align:middle; }
.badge-tier { display:inline-block; margin-left:6px; padding:1px 8px; border-radius:99px;
font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.04em;
color:var(--accent); background:color-mix(in srgb,var(--accent) 16%,transparent); vertical-align:middle; }
/* Menu RAR status line in burger (prima intrare) */
.menu-rar-line { display:flex; align-items:center; gap:7px; padding:8px 10px;
font-size:var(--fs-sm); border-radius:6px; cursor:default; }
.menu-rar-line.ok { color:var(--ok); }
.menu-rar-line.err { color:var(--err); background:color-mix(in srgb,var(--err) 6%,transparent); }
.htmx-indicator.htmx-request { display:inline; }
/* Link-uri de actiune in antetul cardurilor: zona de atins mai mare (>=36px) si
feedback la hover; pe ecrane inguste antetul se rupe curat sub titlu. */
.cardlink { font-size:13px; padding:7px 10px; border-radius:6px; display:inline-flex;
align-items:center; min-height:36px; white-space:nowrap; }
.cardlink:hover { background:var(--line); }
.flash { background:color-mix(in srgb, var(--ok) 12%, var(--card)); border-left:3px solid var(--ok); padding:8px 12px; border-radius:6px;
margin:0 0 12px; font-size:13px; }
.maprow { display:flex; gap:16px; align-items:center; padding:12px 0; border-bottom:1px solid var(--line);
flex-wrap:wrap; }
.maprow:last-child { border-bottom:0; }
.mapcol.grow { flex:1 1 280px; min-width:240px; }
.sugg { color:var(--accent); }
select, button, input[type=text] { font:inherit; background:var(--bg); color:var(--ink);
border:1px solid var(--line); border-radius:6px; padding:6px 10px; }
select { max-width:340px; }
button { background:var(--accent); border-color:var(--accent); color:#fff; cursor:pointer; }
button:hover { filter:brightness(1.08); }
/* Sistem butoane unificat (design.md §5.1). Primarul = `button`/`.btn` (deja stilat). */
.btn-secondary { background:transparent; color:var(--ink); border:1px solid var(--line); border-radius:6px;
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn-secondary:hover { background:var(--line); filter:none; }
.btn-ghost { background:transparent; color:var(--accent); border:1px solid transparent; border-radius:6px;
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn-ghost:hover { background:var(--line); filter:none; }
.btn-danger { background:transparent; color:var(--err); border:1px solid var(--err); border-radius:6px;
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn-danger:hover, .btn-danger:focus-visible { background:var(--err); color:#fff; filter:none; }
.btn-sm { padding:5px 10px; min-height:32px; font-size:13px; }
button:focus-visible, .btn-secondary:focus-visible, .btn-ghost:focus-visible, .btn-danger:focus-visible {
outline:2px solid var(--accent); outline-offset:2px; }
/* Actiuni de rand (design.md §5.1): desktop = text, mobil = iconita patrata 44px.
act_btn randeaza si .act-tx (text) si .act-ic (svg); CSS ascunde unul per breakpoint. */
.act { display:inline-flex; align-items:center; justify-content:center; gap:6px; font:inherit; font-weight:500;
border-radius:7px; padding:6px 12px; min-height:36px; cursor:pointer; background:transparent;
border:1px solid var(--line); color:var(--ink); }
.act:hover { background:var(--line); filter:none; }
.act:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.act .act-ic { width:18px; height:18px; display:none; }
.act .act-tx { display:inline; }
.act-save.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
.act-save.dirty:hover { filter:brightness(.92); }
/* Variant primar mereu-accent (ex. Salveaza in modalul de editare). */
.act-primary { background:var(--accent); color:#fff; border-color:var(--accent); }
.act-primary:hover { filter:brightness(.92); background:var(--accent); }
.act-del { color:var(--err); border-color:var(--err); }
.act-del:hover, .act-del:focus-visible { background:var(--err); color:#fff; }
.act-group { display:inline-flex; gap:8px; align-items:center; }
.btn-editeaza { white-space:nowrap; }
/* Toast global (feedback tranzitoriu post-actiune). */
#toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(8px);
z-index:1300; max-width:90vw; padding:11px 18px; border-radius:10px;
background:var(--ink); color:var(--card); font-size:14px; font-weight:500;
box-shadow:0 8px 28px rgba(0,0,0,.28); display:flex; align-items:center; gap:9px;
opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; }
#toast[hidden] { display:none; }
#toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
#toast::before { content:""; width:9px; height:9px; border-radius:50%; background:var(--ok); flex:0 0 auto; }
#toast.t-err::before, #toast.t-s-error::before, #toast.t-s-needs_data::before,
#toast.t-s-needs_mapping::before { background:var(--err); }
#toast.t-warn::before, #toast.t-s-needs_review::before { background:var(--warn); }
/* Rand de preview tocmai actualizat: flash scurt ca userul sa-l localizeze. */
@keyframes rand-flash { 0% { background:color-mix(in srgb, var(--accent) 26%, var(--card)); }
100% { background:transparent; } }
.tabel-trimiteri tr.rand-actualizat { animation:rand-flash 1.6s ease-out; }
/* Randuri deja-trimise / duplicate in preview: colapsate implicit (nu ocupa loc).
Reafisate cand userul apasa toggle-ul -> .preview-arata-trimise pe container.
!important fiindca regulile compacte mobil/tableta seteaza `tr{display:flex}`. */
.tabel-trimiteri tr.preview-sent-row { display:none !important; }
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:table-row !important; }
/* Stepper import compact (design.md §5.4). >=1024px: bara slim. <1024px: "Pasul N din 4" + progres. */
.stepper { margin-bottom:16px; }
.stepper-track { display:flex; align-items:stretch; border:1px solid var(--line); border-radius:8px;
overflow:hidden; background:var(--card); }
.stepper-step { flex:1; display:flex; align-items:center; gap:8px; padding:10px 14px; min-height:44px;
border-right:1px solid var(--line); }
.stepper-step:last-child { border-right:none; }
.stepper-nr { display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px;
border-radius:50%; font-size:11px; font-weight:700; flex-shrink:0;
background:var(--line); color:var(--muted); }
.stepper-tx { font-size:13px; color:var(--muted); white-space:nowrap; }
.stepper-step.is-active { background:color-mix(in srgb, var(--accent) 10%, transparent); }
.stepper-step.is-active .stepper-nr { background:var(--accent); color:#fff; }
.stepper-step.is-active .stepper-tx { color:var(--ink); font-weight:600; }
.stepper-step.is-done .stepper-nr { background:var(--ok); color:#fff; }
.stepper-step.is-done .stepper-tx { color:var(--ink); }
.stepper-help { margin:6px 2px 0; font-size:12px; color:var(--muted); }
.stepper-collapsed { display:none; }
.stepper-current { font-size:14px; font-weight:600; margin-bottom:6px; }
.stepper-current .muted { font-weight:400; }
.stepper-progress { height:5px; border-radius:99px; background:var(--line); overflow:hidden; }
.stepper-progress > span { display:block; height:100%; background:var(--accent); border-radius:99px; }
.chk { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; }
/* Tab-bar */
.tab-bar { display:flex; gap:2px; overflow-x:auto; -webkit-overflow-scrolling:touch;
border-bottom:1px solid var(--line); margin-bottom:16px; padding-bottom:0;
scrollbar-width:none; }
.tab-bar::-webkit-scrollbar { display:none; }
.tab-link { display:inline-flex; align-items:center; padding:8px 16px; font-size:14px;
font-weight:500; color:var(--muted); text-decoration:none; border-radius:6px 6px 0 0;
border:1px solid transparent; border-bottom:none; white-space:nowrap;
transition:color .12s, background .12s; margin-bottom:-1px; }
.tab-link:hover { color:var(--ink); background:var(--line); }
.tab-link.tab-activ { color:var(--ink); background:var(--card);
border-color:var(--line); border-bottom-color:var(--card); }
.tab-panel { min-height:120px; }
.status-bar { margin-bottom:12px; }
/* Eroare 3 niveluri */
.eroare-3n { margin-top:10px; }
.eroare-3n-item { padding:8px 10px; border-left:3px solid var(--err);
background:color-mix(in srgb, var(--err) 8%, var(--card));
border-radius:0 6px 6px 0; }
.eroare-3n-sep { margin-top:6px; }
.eroare-3n-problema { font-weight:600; color:var(--err); font-size:13px; }
.eroare-3n-camp { font-family:var(--font-mono); font-size:var(--fs-xs); opacity:.85; }
.eroare-3n-cauza { color:var(--muted); font-size:12px; margin-top:3px; }
.eroare-3n-fix { color:var(--accent); font-size:12px; margin-top:3px; }
.eroare-3n-label { font-weight:500; }
/* Inline fix per camp in preview */
.camp-fix { color:var(--accent); font-size:11px; margin-top:2px; display:block; }
/* Meniu hamburger cont — dropdown ancorat dreapta-sus */
.cont-menu-wrap { position:relative; }
.icon-btn { background:transparent; border:1px solid var(--line); color:var(--ink); cursor:pointer;
border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px;
line-height:1; display:inline-flex; align-items:center; justify-content:center; }
.icon-btn:hover { background:var(--line); }
/* Variante icon-btn — dirty (modificari nesalvate) + danger (destructiv) */
.icon-btn.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
.icon-btn.dirty:hover { filter:brightness(0.9); }
.icon-btn.danger { color:var(--err); border-color:var(--err); }
.icon-btn.danger:hover, .icon-btn.danger:focus-visible { background:var(--err); color:#fff; }
.cont-menu { position:absolute; right:0; top:calc(100% + 8px); min-width:180px; z-index:50;
background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px;
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
.cont-menu[hidden] { display:none; }
.cont-menu a, .cont-menu button { display:block; width:100%; text-align:left; background:transparent;
border:none; color:var(--ink); text-decoration:none; font:inherit; padding:8px 10px;
border-radius:6px; cursor:pointer; min-height:36px; }
.cont-menu a:hover, .cont-menu button:hover { background:var(--line); }
.cont-menu hr { border:none; border-top:1px solid var(--line); margin:4px 0; }
.cont-menu form { margin:0; }
/* Kebab partajat (actiuni per-rand in tabele). Meniul e position:fixed si pozitionat de JS:
altfel `.tablewrap { overflow-x:auto }` induce overflow-y:auto si TAIE dropdown-ul pe ultimul
rand. fixed scoate meniul din contextul de clipping al tabelului. */
.kebab { position:relative; display:inline-block; }
.kebab > summary { list-style:none; cursor:pointer; display:inline-flex; align-items:center;
justify-content:center; min-height:32px; min-width:32px; padding:4px 10px;
border-radius:6px; color:var(--ink); }
.kebab > summary::-webkit-details-marker { display:none; }
.kebab > summary:hover, .kebab[open] > summary { background:var(--line); }
.kebab-menu { position:fixed; z-index:1000; min-width:160px; background:var(--card);
border:1px solid var(--line); border-radius:8px; padding:6px;
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
.kebab-menu form { margin:0; }
.kebab-menu button, .kebab-menu a { display:block; width:100%; text-align:left; background:transparent;
border:none; color:var(--ink); text-decoration:none; font:inherit; padding:7px 10px;
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
.kebab-menu button.danger { color:var(--err); }
/* === Accordion import compact (regiune CSS disjuncta) ===
<details id="import-details"> invelete stepper + upload pe Acasa.
Atribut `open` setat de server din `are_trimiteri`:
False (first-run) → open; True (returning) → colapsat.
Degradare fara JS: returning-user colapsat, first-run deschis — ambele corecte
fara toggle JS (un toggle JS pur ar lasa returning-user fara-JS cu ecranul deschis).
aria-expanded + focus: native <details> le gestioneaza automat. */
#import-details { margin-bottom:16px; border:1px solid var(--accent); border-radius:8px;
background:var(--card); overflow:hidden; }
#import-details > summary { display:flex; align-items:center; gap:8px; cursor:pointer;
user-select:none; list-style:none; padding:10px 16px;
font-weight:600; font-size:14px; color:var(--ink); }
#import-details > summary::-webkit-details-marker { display:none; }
#import-details > summary::marker { display:none; }
#import-details > summary::before { content:"▶"; font-size:10px; color:var(--accent);
flex-shrink:0; transition:transform .15s; }
#import-details[open] > summary::before { transform:rotate(90deg); }
#import-details[open] > summary { border-bottom:1px solid var(--line); }
#import-details > summary:hover { background:color-mix(in srgb, var(--accent) 8%, var(--card)); }
#import-details > summary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
/* Continutul (stepper + card upload): padding si bordul cardului interior sunt suprastate
de bordul exterior al #import-details — scoatem border duplicat de pe .card intern. */
#import-details #import-section { padding:0; }
#import-details #import-section > .card { border-left:none; border-right:none;
border-bottom:none; border-radius:0;
margin-bottom:0; }
/* === Sfarsit regiune accordion import === */
/* === Inceput regiune nav links status-bar ===
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
transition:background .1s; }
.status-nav-link:hover { background:color-mix(in srgb, var(--accent) 10%, transparent); }
.status-nav-link.status-nav-activ { color:var(--ink); font-weight:600; }
.status-nav-link.status-nav-activ:hover { background:color-mix(in srgb, var(--ink) 8%, transparent); }
/* === Sfarsit regiune nav links status-bar === */
/* Tabel cu cautare + paginare client-side (data-dt). Maparile pot creste la sute de randuri;
filtram/paginez DOM-ul deja randat, fara cereri suplimentare. Vezi scriptul din base.html. */
input[type=search] { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:6px 10px; width:100%; }
.dt-tools { display:flex; align-items:center; gap:8px; margin:0 0 10px; }
.dt-search { flex:1 1 auto; max-width:320px; }
.dt-empty { color:var(--muted); padding:16px; text-align:center; font-size:13px; }
.dt-pager { display:flex; align-items:center; justify-content:flex-end; gap:10px;
margin-top:10px; font-size:13px; color:var(--muted); }
.dt-pager button { background:transparent; color:var(--ink); border:1px solid var(--line);
padding:5px 12px; min-height:32px; }
.dt-pager button:disabled { opacity:.45; cursor:default; }
/* === Tabel trimiteri: fara scroll orizontal. SCOPAT prin
.tabel-trimiteri ca sa NU strice celelalte tabele (.tablewrap e partajat de
Mapari/Formate). Permitem wrap controlat pe coloanele text + latimi rezonabile. === */
.tabel-trimiteri table { table-layout:fixed; }
.tabel-trimiteri th, .tabel-trimiteri td { white-space:normal; word-break:break-word; vertical-align:top; }
.tabel-trimiteri .col-chk { width:30px; }
.tabel-trimiteri .col-id { width:48px; }
/* col-stare largita (bug 4a 5.16): cu table-layout:fixed + pill nowrap, 104px era
prea ingusta -> pill-ul de stare se revarsa peste col-vehicul. 140px o contine. */
.tabel-trimiteri .col-stare { width:140px; }
.tabel-trimiteri .col-data { width:104px; }
.tabel-trimiteri .col-rar { width:96px; }
.tabel-trimiteri .col-actualizat { width:128px; }
.tabel-trimiteri .col-operatie > div { line-height:1.35; }
/* secundarul muted („cod RAR" / „nemapat") — >=12px, contrast pe var(--muted) >=4.5:1 */
.tabel-trimiteri .cod-rar-sub { font-size:12px; margin-top:2px; }
/* Codul RAR pe linia 2 — chip discret, fara prefixul „cod RAR:". */
.tabel-trimiteri .cod-rar-cod { display:inline-block; font-family:var(--font-mono);
font-size:var(--fs-xs); padding:1px 7px; border:1px solid var(--line);
border-radius:99px; color:var(--muted); }
/* Eticheta umana scurta sub pill — text mic; clasa `s-error` o coloreaza
(apare doar pe error/needs_*). Stare prin text, nu doar culoare. */
.tabel-trimiteri .eticheta-problema { font-size:12px; line-height:1.3; margin-top:3px; }
/* Randul e clickabil (deschide modalul) -> tinta de atins >=44px (touch) +
afordanta hover/focus. */
.tabel-trimiteri tr.trimitere-row { min-height:44px; }
.tabel-trimiteri tr.trimitere-row > td { padding-top:11px; padding-bottom:11px; }
.tabel-trimiteri tr.trimitere-row:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
.tabel-trimiteri tr.trimitere-row:focus,
.tabel-trimiteri tr.trimitere-row:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
/* col-actualizat ca linie meta mica in carduri (decizie 5.13 #8). */
.tabel-trimiteri td.col-actualizat { font-size:12px; }
/* Stepper: sub 1024px ascunde track-ul slim, arata forma colapsata (decizie 5.13 #11). */
@media (max-width:1024px) {
.stepper-track { display:none; }
.stepper-collapsed { display:block; }
}
/* Tableta (7681024px): header compact fara suprapuneri.
Grila 3-coloane se pastreaza; logo si titlu mai mici; versiunea ascunsa
(informatia secundara elibereaza spatiu in celula dreapta: comutator tema +
hamburger raman vizibili). min-height:92px din regula de baza e resetat —
inaltimea header-ului e determinata de continut, nu de un prag fix. */
@media (min-width:768px) and (max-width:1024px) {
header { min-height:0; padding:10px 16px; gap:6px; }
.brand-logo { height:44px; }
header h1 { font-size:16px; }
/* Versiunea (ex. "v0.9.3") este informatie secundara pe tableta:
ascunsa pentru a elibera spatiu in celula dreapta. */
.header-right > .muted { display:none; }
}
/* Tableta 768-1024px: listele actionabile raman O COLOANA, cardificate (nu tabel storcit,
nu 2/rand). Decizie 5.13 (premisa user). Tabelele dense read-only raman .tablewrap. */
@media (min-width:768px) and (max-width:1024px) {
.tabel-trimiteri table, .tabel-card table { table-layout:auto; }
.tabel-trimiteri thead, .tabel-card thead { display:none; }
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td,
.tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; }
.tabel-trimiteri tr, .tabel-card tr { border:1px solid var(--line); border-radius:8px;
padding:10px 12px; margin-bottom:10px; }
.tabel-trimiteri td, .tabel-card td { border-bottom:none; padding:3px 0; }
.tabel-trimiteri td::before, .tabel-card td::before { content:attr(data-eticheta); display:block;
color:var(--muted); font-size:12px; margin-bottom:2px; }
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
.tabel-trimiteri td[data-eticheta=""]::before, .tabel-card td[data-eticheta=""]::before,
.tabel-card td:not([data-eticheta])::before { display:none; }
.tabel-card td select, .tabel-card td input[type=text], .tabel-card td input[type=search] {
width:100%; max-width:none; }
/* Card compact si pe tableta (acelasi layout ca pe mobil) pentru `.tabel-trimiteri`. */
.tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline; gap:1px 8px; }
.tabel-trimiteri td { padding:0; }
/* Regula desktop `tr.trimitere-row > td { padding:11px }` e mai specifica -> o anulam
in cardul compact, altfel randurile de Trimiteri raman inalte/aerisite. */
.tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; }
.tabel-trimiteri td::before { display:none; }
.tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0; font-weight:600; font-size:15px; line-height:1.25; }
.tabel-trimiteri td.col-vehicul .muted { font-weight:400; }
.tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto; align-self:flex-start; }
.tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px; line-height:1.3; margin-top:1px; }
.tabel-trimiteri td.col-data, .tabel-trimiteri td.col-km, .tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); }
.tabel-trimiteri td.col-data { order:4; } .tabel-trimiteri td.col-km { order:5; } .tabel-trimiteri td.col-rar { order:6; }
.tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); }
.tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px; color:var(--muted); }
.tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px; color:var(--accent); line-height:1.3; margin-top:1px; }
.tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto; margin-top:4px; text-align:right; }
.tabel-trimiteri td.col-actiuni button, .tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; }
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; }
}
/* === Preview import: coloane extra fata de tabelul Trimiteri.
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
8 coloane (coloana de verificare manuala eliminata).
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
col-km(76) + col-note(176) + col-actiuni(92) = 600px.
Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
.tabel-trimiteri .col-km { width:76px; }
.tabel-trimiteri .col-note { width:176px; }
/* Nota preview = culoarea camp-fix (accent), ca sa atraga atentia (dogfood 5.13):
campul Note e self-explanatory, asa ca hint-urile per-camp au fost scoase. */
.tabel-trimiteri td.col-note { color:var(--accent); }
/* Pill-ul de stare nu se rupe pe doua randuri in cardul compact. */
.tabel-trimiteri td.col-stare .pill { white-space:nowrap; }
.tabel-trimiteri .col-actiuni { width:104px; }
/* Randul de editare inline iese din grila table-layout:fixed (display:block),
astfel formularul nu e constrans de latimile coloanelor individuale.
Salveaza/Anuleaza sunt mereu vizibile (overflow:visible, nu clip). */
.tabel-trimiteri tr.preview-edit { display:block; }
.tabel-trimiteri tr.preview-edit > td { display:block; width:100%; box-sizing:border-box; padding:0; border:none; }
/* Pe mobil (<768px): pseudo-eticheta goala (data-eticheta="") nu lasa spatiu gol. */
@media (max-width:767px) {
.tabel-trimiteri td[data-eticheta=""]::before { display:none; }
}
/* === Modal detaliu: fereastra modala globala, in afara zonei de poll
(#submissions-wrap). Backdrop + dialog centrat pe desktop; focus-trap +
scroll-lock + inert pe <main> sunt in JS. Varianta full-screen mobil: vezi blocul
`@media (max-width:767px)` de mai jos. === */
.modal-overlay { position:fixed; inset:0; z-index:1100; display:flex;
align-items:flex-start; justify-content:center; padding:40px 16px; overflow-y:auto; }
.modal-overlay[hidden] { display:none; }
.modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.55); }
.modal-dialog { position:relative; z-index:1; width:100%; max-width:680px;
background:var(--card); border:1px solid var(--line); border-radius:12px;
box-shadow:0 16px 48px rgba(0,0,0,.35); padding:18px 20px;
max-height:calc(100vh - 80px); overflow-y:auto; }
.modal-close { position:absolute; top:10px; right:10px; background:transparent;
border:1px solid var(--line); color:var(--muted); width:36px; height:36px;
border-radius:8px; font-size:20px; line-height:1; cursor:pointer;
display:inline-flex; align-items:center; justify-content:center; }
.modal-close:hover { background:var(--line); color:var(--ink); }
body.modal-open { overflow:hidden; }
.modal-eroare { padding:16px 4px; }
.modal-eroare .actiuni { margin-top:12px; display:flex; gap:10px; flex-wrap:wrap; }
/* === Fundatie responsive mobil (<768px) ===
Breakpoint unic 767px (vezi conventia de sus). Cuprinde: card per rand pe tabelul
de trimiteri, modal full-screen, header/nav colapsat cu tinte touch
>=44px. Desktop (>=1024px) ramane neschimbat — regulile de baza nu se modifica. */
/* SENTINEL-TESTE-MOBIL: blocul mobil principal incepe mai jos; testele ancoreaza pe acest marker si feliaza pana la sfarsitul stilului. NU muta/sterge. */
@media (max-width:767px) {
/* Tabel trimiteri: card per rand (eticheta:valoare stivuit) -> fara scroll orizontal */
.tabel-trimiteri table { table-layout:auto; }
.tabel-trimiteri thead { display:none; }
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td { display:block; width:auto; }
.tabel-trimiteri tr { border:1px solid var(--line); border-radius:8px; padding:8px 12px; margin-bottom:10px; }
.tabel-trimiteri td { border-bottom:none; padding:3px 0; display:block; }
.tabel-trimiteri td::before { content:attr(data-eticheta); display:block; color:var(--muted);
font-size:12px; margin-bottom:2px; }
.tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; }
/* === Card COMPACT ===
Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card
scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus,
operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara
etichete-zgomot. Override DUPA regulile de baza (cascada: ultimul castiga). */
.tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline;
gap:1px 8px; padding:9px 12px; }
.tabel-trimiteri td { display:block; padding:0; }
.tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; }
.tabel-trimiteri td::before { display:none; } /* compact: fara etichete */
.tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0;
font-weight:600; font-size:15px; line-height:1.25; }
.tabel-trimiteri td.col-vehicul .muted { font-weight:400; }
.tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto;
align-self:flex-start; }
.tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px;
line-height:1.3; margin-top:1px; }
.tabel-trimiteri td.col-data,
.tabel-trimiteri td.col-km,
.tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); }
.tabel-trimiteri td.col-data { order:4; }
.tabel-trimiteri td.col-km { order:5; }
.tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); }
.tabel-trimiteri td.col-rar { order:6; }
.tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px;
color:var(--muted); }
.tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px;
color:var(--accent); line-height:1.3; margin-top:1px; }
.tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto;
margin-top:4px; text-align:right; }
.tabel-trimiteri td.col-actiuni button,
.tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; }
.tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; }
/* Modal full-screen: ocupa tot ecranul, fara backdrop lateral (overlay fara
padding, dialog la latime/inaltime pline, fara colturi/umbra). Scroll intern
pe dialog; butonul `x` la >=44px (tinta touch). Desktop pastreaza varianta
centrata cu `max-width:680px` din regula de baza de mai sus. */
.modal-overlay { padding:0; align-items:stretch; justify-content:stretch; }
.modal-dialog { width:100vw; max-width:none; min-height:100vh; max-height:100vh;
border:none; border-radius:0; box-shadow:none;
padding:16px; padding-top:56px; overflow-y:auto; }
.modal-close { width:44px; height:44px; top:8px; right:8px; font-size:24px; }
/* Actiunile de jos din detaliu (Re-pune / Sterge) full-width stivuit pe mobil. */
.detaliu-actiuni-jos button { width:100%; }
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
Randul 1: [logo ROMFAST stanga] [controale dreapta] (margin-left:auto pe .header-right).
Randul 2: [titlu + env mic centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; min-height:0; }
.brand-logo { height:44px; }
.header-left { order:0; flex:0 0 auto; }
.header-center { order:2; width:100%; text-align:center; }
.header-right { order:1; margin-left:auto; flex:0 0 auto; }
header h1 { font-size:17px; }
main { padding:16px; }
.icon-btn { min-height:44px; min-width:44px; }
.tab-link { min-height:44px; padding:10px 14px; }
.cont-menu a, .cont-menu button { min-height:44px; }
/* === Paginile de continut pe mobil ===
Tabele ACTIONABILE (Mapari) -> card per rand. Clasa proprie `.tabel-card`,
scopata SEPARAT de `.tabel-trimiteri` ca sa NU strice cardurile de
trimiteri. Tabele DENSE read-only (Jurnal, Nomenclator) + Admin raman in
`.tablewrap` (scroll orizontal CONTAINED, definit global mai sus). */
.tabel-card table { table-layout:auto; }
.tabel-card thead { display:none; }
.tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; }
.tabel-card tr { border:1px solid var(--line); border-radius:8px; padding:10px 12px; margin-bottom:10px; }
.tabel-card td { border-bottom:none; padding:5px 0; }
.tabel-card td::before { content:attr(data-eticheta); display:block; color:var(--muted);
font-size:12px; margin-bottom:3px; }
/* Celulele fara eticheta (doar actiuni) nu primesc antet gol. */
.tabel-card td:not([data-eticheta])::before,
.tabel-card td[data-eticheta=""]::before { display:none; }
/* Controale full-width in card; butoanele primesc tinta touch >=44px. */
.tabel-card td select, .tabel-card td input[type=text],
.tabel-card td input[type=search] { width:100%; max-width:none; }
.tabel-card td button { width:100%; min-height:44px; }
/* Formulare de continut: o coloana, inputuri/selecturi full-width, butoane >=44px.
Scopat strict pe controalele de formular din sectiunile de continut (nu atinge
tabelul de trimiteri, modalul, taburile ARIA sau butoanele de copiere absolute). */
#card-cont input[type=email], #card-cont input[type=password],
#form-test-cheie input[type=password],
#jurnal-section select, #jurnal-section input[type=date],
#jurnal-section input[type=number] {
/* !important fiindca aceste inputuri au latimi inline (ex. width:280px / max-width:100px)
pe desktop; le suprascriem DOAR sub 767px, deci desktop ramane neschimbat. */
width:100% !important; max-width:none !important; box-sizing:border-box; }
#jurnal-section #filtre-jurnal > div { width:100%; }
#card-cont button, #form-test-cheie button,
#jurnal-section #filtre-jurnal button { min-height:44px; width:100%; }
/* === Acasa (upload, status, filtre) + login/signup pe mobil ===
Zona de upload, bara de status si bara de filtre (`_coada.html`) stiveaza pe O
coloana sub 767px; inputuri/butoane full-width cu tinta touch >=44px. Scopat pe
id-urile sectiunilor de pe Acasa ca sa NU atinga tabelul de trimiteri,
modalul sau paginile de continut. */
/* Bara de upload: zona slim (returning user) trece pe coloana; butonul full-width. */
#import-section .drop-zone { flex-direction:column; align-items:stretch; text-align:left; }
#import-section #upload-btn { width:100%; min-height:44px; }
/* Bara de status: contoarele/randurile raman aliniate la stanga, fara scroll orizontal. */
#status-bar > div { gap:10px; }
/* Bara de filtre trimiteri ADAPTATA pentru mobil (nu doar stivuita):
- cautarea vehicul = rand propriu prioritar (input + buton pe acelasi rand);
- grupurile de pill-uri (data + stare) = scroll orizontal, compacte (nu 8 butoane
full-width unul sub altul). !important suprascrie latimile inline doar pe mobil. */
#filtre-trimiteri { flex-direction:column; align-items:stretch; gap:8px; }
#filtre-trimiteri > div { width:100%; }
/* Cautarea vehicul: input creste, butonul Filtreaza compact langa el. */
#filtre-trimiteri input[type=text] { flex:1 1 auto; width:auto !important; max-width:none !important; min-height:44px; }
#filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; min-height:44px; }
#filtre-trimiteri button[type=submit] { flex:0 0 auto; width:auto; min-height:44px; }
/* Grupurile de pill-uri: o singura banda scrolabila orizontal, compacta. */
#filtre-trimiteri .pills-categorii { margin-left:0 !important; flex-wrap:nowrap;
overflow-x:auto; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
#filtre-trimiteri .pill-cat { flex:0 0 auto; }
/* Operatii de mapat (preview import): randul de mapare stiva pe o coloana,
select-ul full-width (altfel max-width:340px global il scoate din viewport). */
.maprow { gap:6px 12px; padding:10px 0; }
.maprow .mapcol { flex:1 1 100%; min-width:0; }
.maprow select { width:100% !important; max-width:none !important; }
.maprow button { width:100%; min-height:44px; }
/* Card de autentificare (login/signup): centrat si nu depaseste viewport-ul pe mobil. */
.auth-card { max-width:100%; margin:24px auto; }
/* Versiunea ascunsa pe mobil (la fel ca pe tableta). */
.header-right > .muted { display:none; }
/* Actiuni .act pe mobil: iconita patrata 44px, textul ascuns. */
.act { min-width:44px; min-height:44px; width:44px; padding:0; }
.act .act-tx { display:none; }
.act .act-ic { display:inline-block; }
.act-group { gap:10px; }
/* Bara confirmare compacta pe mobil. */
.sticky-bar { padding:10px 12px; gap:10px; }
.sticky-bar button { width:100%; min-height:44px; }
}
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim.
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px.
Padding marit (18px), cifra pe --fs-2xl, label pe --fs-sm, sub pe --fs-xs.
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:18px 18px; }
.contor-cifra { font-size:var(--fs-2xl); font-weight:700; line-height:1; }
.contor-label { font-size:var(--fs-sm); color:var(--muted); margin-top:8px; }
.contor-sub { font-family:var(--font-mono); font-size:var(--fs-xs); color:var(--muted); margin-top:4px; }
/* Contoarele desktop = 5 carduri side-by-side. display:flex sta in CSS (NU inline pe
element) ca media query-ul de mai jos sa-l poata ascunde pe mobil — un inline
style="display:flex" ar bate regula @media si ar duce la contoare duplicate pe 390px. */
.contoare-desktop { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
/* Bara compacta contoare pe mobil (<=560px): un singur rand, numere + etichete scurte, fara carduri mari. */
.contoare-compact { display:none; }
@media (max-width:560px) {
.contoare-desktop { display:none; }
.contoare-compact { display:flex; align-items:center; gap:0; margin-bottom:14px;
background:var(--card2); border:1px solid var(--line); border-radius:8px;
overflow:hidden; }
.compact-item { flex:1; display:flex; flex-direction:column; align-items:center; padding:10px 6px;
border-right:1px solid var(--line); min-width:0; text-align:center; }
.compact-item:last-child { border-right:none; }
.compact-nr { font-size:var(--fs-xl); font-weight:700; line-height:1; }
.compact-lbl { font-size:10px; color:var(--muted); margin-top:3px; white-space:nowrap; }
}
/* Bara contextuala bulk (5.22): ascunsa fara selectie; .activ (setat de JS
cand >=1 checkbox e bifat) o afiseaza impreuna cu hint-ul de sub ea. */
.bulk-bar { display:none; align-items:center; gap:8px; flex-wrap:wrap; font-size:13px;
background:color-mix(in srgb, var(--accent) 10%, var(--card));
border:1px solid color-mix(in srgb, var(--accent) 40%, transparent);
border-radius:8px; padding:8px 12px; margin-bottom:4px; }
.bulk-bar.activ { display:flex; }
.bulk-hint { display:none; font-size:12px; color:var(--muted); margin:0 0 8px; }
.bulk-bar.activ + .bulk-hint { display:block; }
/* Tabel compact trimiteri (grid CSS, 5.22).
Desktop: header de coloane + un rand per trimitere; sub-linia de eroare
(.c-err) ocupa coloanele 2..-1 pe un al doilea rand de grid.
Mobil (max-width:767px, mai jos): header ascuns, rand pe 2 linii. */
.lista-trimiteri-slim { list-style:none; margin:0; padding:0; }
.trimiteri-head, .trimitere-slim {
display:grid; align-items:center; column-gap:10px; row-gap:2px;
grid-template-columns:22px 42px minmax(90px,150px) minmax(64px,auto) minmax(0,1fr) auto 46px 44px 84px; }
.trimiteri-head { padding:2px 16px 8px; border-bottom:1px solid var(--line);
font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
color:var(--muted); }
.trimiteri-head .th-c { text-align:center; }
.trimitere-slim { padding:9px 16px; border-bottom:1px solid var(--line2);
min-height:44px; cursor:pointer; }
.trimitere-slim:last-child { border-bottom:none; }
.trimitere-slim:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
.trimitere-slim:focus, .trimitere-slim:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.trimitere-slim .c-check { display:flex; align-items:center; }
/* Numarul trimiterii: id-ul din baza, corelabil cu submission_id din API. */
.trimitere-slim .c-id { font-family:var(--font-mono); font-size:var(--fs-xs);
color:var(--muted); white-space:nowrap;
font-variant-numeric:tabular-nums; }
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-sm); font-weight:600; color:var(--ink);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
/* Pe desktop copiii .c-meta devin celule de grid (cod / operatie / data). */
.trimitere-slim .c-meta { display:contents; }
.trimitere-slim .c-cod { white-space:nowrap; }
.trimitere-slim .cod-rar-cod { font-family:var(--font-mono); font-weight:600;
font-size:var(--fs-xs); color:var(--accent); }
.trimitere-slim .cod-rar-cod.muted { color:var(--muted); font-weight:500; font-style:italic; }
/* Badge +N: trimiterea are mai multe coduri RAR; toate stau in title. */
.trimitere-slim .cod-plus { display:inline-block; font-family:var(--font-mono);
font-size:10px; font-weight:700; color:var(--muted); border:1px solid var(--line);
border-radius:99px; padding:0 5px; margin-left:4px; vertical-align:1px; cursor:default; }
.trimitere-slim .c-op { min-width:0; white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; font-size:var(--fs-sm); color:var(--ink); }
.trimitere-slim .c-data { font-size:var(--fs-xs); color:var(--muted); white-space:nowrap;
font-variant-numeric:tabular-nums; }
.trimitere-slim .c-status, .trimitere-slim .c-env { justify-self:center; line-height:1;
display:inline-flex; align-items:center; }
/* Bulina de status: doar cerc colorat (currentColor din .s-*); numele starii
traieste in headerul coloanei + title/aria-label. */
.dot-stare { display:inline-block; width:11px; height:11px; border-radius:99px;
background:currentColor; }
/* Litera de mediu: P = fill atentie (declaratie reala, ireversibila — L.142);
T = contur discret (mediu de proba). */
.env-l { display:inline-flex; align-items:center; justify-content:center;
width:20px; height:20px; border-radius:99px; font-size:11px; font-weight:800;
cursor:default; }
.env-l-prod { background:color-mix(in srgb, var(--err) 80%, var(--card)); color:var(--card); }
.env-l-test { color:var(--muted); border:1px solid var(--line); }
.trimitere-slim .c-act { justify-self:end; }
/* Sub-linia de eroare: text brut, mono, un singur rand cu ellipsis (title = complet). */
.trimitere-slim .c-err { grid-column:3 / -1; min-width:0; font-family:var(--font-mono);
font-size:var(--fs-xs); color:var(--err); white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; }
/* .camp-slim — varianta compacta camp formular: label --fs-sm muted deasupra, input --fs-md, fundal --card2.
Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
.camp-slim { margin-bottom:8px; }
.camp-slim label { font-size:var(--fs-sm); color:var(--muted); display:block; margin-bottom:4px; }
.camp-slim input, .camp-slim textarea, .camp-slim select { background:var(--card2); min-height:36px; width:100%;
padding:0 10px; border:1px solid var(--line); border-radius:6px; font-family:var(--font-ui);
font-size:var(--fs-md); color:var(--ink); }
.camp-slim textarea { min-height:52px; height:auto; padding:8px 10px; resize:vertical; }
.camp-slim .camp-mono { font-family:var(--font-mono); font-size:var(--fs-sm); }
/* .chips + .chip — prestatii multi-select cu buton de stergere accesibil (.chip-del).
Fundal accent 18%, font mono --fs-xs. */
.chips { min-height:30px; display:flex; align-items:center; gap:6px; flex-wrap:wrap;
padding:4px 8px; border:1px solid var(--line); border-radius:6px; background:var(--card2); }
.chip { display:inline-flex; align-items:center; gap:5px; padding:3px 8px; border-radius:5px;
background:color-mix(in srgb, var(--accent) 18%, transparent); color:var(--accent);
font-family:var(--font-mono); font-size:var(--fs-xs); font-weight:600; }
.chip .chip-del { background:transparent; border:none; color:inherit; opacity:.7; cursor:pointer;
padding:0; font-size:13px; line-height:1; display:inline-flex;
align-items:center; justify-content:center; min-width:16px; min-height:16px; }
.chip .chip-del:hover, .chip .chip-del:focus-visible { opacity:1; }
.chip .chip-del:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
/* Varianta chip warn (ex. R-ODO necesita odometruInitial) */
.chip-warn { background:color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); }
/* Chip abia adaugat via /form-chips — confirmare vizibila a actiunii "+" */
.chip-nou { outline:2px solid color-mix(in srgb, var(--ok) 45%, transparent); outline-offset:1px; }
/* Eticheta mono "COD" pe chip-ul de cod */
.chip-cod-label { font-size:9px; font-weight:800; opacity:.65; letter-spacing:.03em; }
/* Chip warning operatie nemapata: dashed implicit; tinta activa = solid + inel —
singurul tratament "tare" din rand. */
.chip-op-warn { background:color-mix(in srgb, var(--warn) 16%, transparent); color:var(--warn);
border:1px dashed var(--warn); cursor:pointer; font-weight:600; }
.chip-op-warn.is-target { border-style:solid; outline:2px solid color-mix(in srgb, var(--warn) 40%, transparent);
outline-offset:1px; }
.chip-op-warn:focus-visible { outline:2px solid var(--warn); outline-offset:1px; }
/* Chip Nedeclarat: vizibil (line-through), NU mut/invizibil — cea mai riscanta
actiune legala (exclude de la declararea RAR) nu poate fi cea mai discreta. */
.chip-nedeclarat { background:var(--card2); color:var(--muted); border:1px dashed var(--line);
text-decoration:line-through; }
/* Sectiunea chips inerta pe durata unui request /form-chips in zbor:
click pe x/warning mid-flight nu se pierde, doar se ignora. */
.chips-editor.htmx-request { pointer-events:none; opacity:.7; }
/* Mobil: fara header, randul revine la 2 linii (placuta / cod·operatie·data),
cu actiune + litera mediu + bulina status in dreapta. Grid-areas peste
aceleasi celule; .c-meta redevine linie flex cu separatoare "·". */
@media (max-width:767px) {
.trimiteri-head { display:none; }
.trimitere-slim { padding:12px 14px; column-gap:8px;
grid-template-columns:22px auto minmax(0,1fr) auto auto auto;
grid-template-areas:
"check id veh act env status"
"check meta meta act env status"
"err err err err err err"; }
.trimitere-slim .c-check { grid-area:check; }
.trimitere-slim .c-id { grid-area:id; align-self:center; }
.trimitere-slim .slim-vin { grid-area:veh; font-size:var(--fs-md); }
.trimitere-slim .c-meta { grid-area:meta; display:flex; align-items:baseline;
gap:6px; min-width:0; margin-top:3px; }
.trimitere-slim .c-cod { flex:0 0 auto; }
.trimitere-slim .c-op { flex:1 1 auto; }
.trimitere-slim .c-data { flex:0 0 auto; }
.trimitere-slim .c-op::before, .trimitere-slim .c-data:not(:empty)::before {
content:"·"; color:var(--muted); margin-right:6px; }
.trimitere-slim .c-status { grid-area:status; }
.trimitere-slim .c-env { grid-area:env; }
.trimitere-slim .c-act { grid-area:act; }
.trimitere-slim .c-err { grid-area:err; }
}
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
/* === Fix mobil Mapari ===
Doua probleme raportate la 390px pe pagina Mapari:
(1) butoanele Salveaza/Sterge taiate: regula `.tabel-card td button {width:100%}`
(specificitate 0,1,2) batea `.act {width:44px}` (0,1,0) -> cele doua butoane act
deveneau full-width si al doilea (Sterge) iesea din card (celula are nowrap).
(2) carduri prea inalte: etichetele data-eticheta randate ca pseudo-titluri +
linia redundanta "acum: COD — nume" (duplica select-ul de dedesubt).
Plasat ultimul in <style> => castiga pe cascada la specificitate egala.
Atributele data-eticheta raman in DOM (a11y + teste); doar pseudo-eticheta se ascunde. */
@media (max-width:767px) {
/* Carduri Mapari compacte: fara etichete-zgomot (continutul e auto-descriptiv,
ca la cardul de trimiteri), padding strans. */
.tabel-card td::before { display:none; }
.tabel-card tr { padding:9px 12px; margin-bottom:8px; }
.tabel-card td { padding:3px 0; }
/* "acum: COD — nume" e redundant cu select-ul de dedesubt (aceeasi valoare). */
.map-acum { display:none; }
/* Celula Actiuni: butoanele act pe UN rand, vizibile, cu text (nu iconita-only
ambigua, nu full-width care impinge al doilea buton afara cardului).
`.tabel-card td .act` (0,2,1) > `.tabel-card td button` (0,1,2). */
.tabel-card td[data-eticheta="Actiuni"] { display:flex; gap:8px; align-items:stretch;
margin-top:2px; }
.tabel-card td .act { width:auto; flex:1 1 0; min-width:0; min-height:44px; padding:8px 12px; }
.tabel-card td .act .act-tx { display:inline; }
.tabel-card td .act .act-ic { display:inline-block; }
}
/* === Meniu hamburger grupat === */
/* Iconite SVG stroke unic, currentColor — mostenesc culoarea intrarii. */
.ic { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:1.7;
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
.cont-menu a, .cont-menu form button { display:flex; align-items:center; gap:10px; }
.cont-menu a .mi, .cont-menu form button .mi { display:inline-flex; opacity:.6; }
.cont-menu a:hover .mi, .cont-menu form button:hover .mi { opacity:.9; }
.cont-menu-group { font-size:10px; text-transform:uppercase; letter-spacing:.08em;
color:var(--muted); font-weight:700; padding:8px 12px 4px; }
/* Badge-ul de mapari se ancoreaza la dreapta chiar cand intrarea e flex. */
.cont-menu a .tab-badge { margin-left:auto; }
/* === Pagina Setari cu sub-taburi === */
.setari-head { margin-bottom:4px; }
.setari-title { font-size:var(--fs-lg); margin:0 0 2px; letter-spacing:-.01em;
display:flex; align-items:center; gap:9px; }
.setari-title .ic { width:20px; height:20px; opacity:.7; }
.setari-sub { color:var(--muted); font-size:var(--fs-sm); margin:0 0 14px; }
.setari-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--line); margin-bottom:20px;
overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
.setari-subtabs::-webkit-scrollbar { display:none; }
.setari-subtab { appearance:none; background:transparent; border:none; border-bottom:2px solid transparent;
color:var(--muted); font:inherit; font-size:var(--fs-sm); font-weight:500; cursor:pointer;
padding:10px 2px; margin-right:22px; white-space:nowrap; display:inline-flex;
align-items:center; gap:8px; margin-bottom:-1px; transition:color .12s, border-color .12s; }
.setari-subtab:hover { color:var(--ink); }
/* Stare activa prin clasa (nu selector pe atribut): aria-selected ramane pe buton
pentru accesibilitate, dar stilul nu depinde de el. */
.setari-subtab.is-active { color:var(--ink); border-bottom-color:var(--accent); font-weight:600; }
.setari-subtab:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.st-dot { width:7px; height:7px; border-radius:99px; flex-shrink:0; }
.st-dot.ok { background:var(--ok); }
.st-dot.warn { background:var(--warn); }
.st-dot.info { background:var(--muted); }
.setari-pane[hidden] { display:none; }
/* Titlu de sectiune compact (uppercase mic) — ierarhie sub titlul paginii, nu concurenta cu el. */
.setari-zone-title { font-size:var(--fs-xs); font-weight:600; text-transform:uppercase;
letter-spacing:.07em; color:var(--muted); margin:0 0 10px; display:flex;
align-items:center; gap:8px; flex-wrap:wrap; }
.setari-zone-title .ic { opacity:.7; width:15px; height:15px; }
.setari-zone-title .setari-badge { text-transform:none; letter-spacing:0; }
.setari-zone-desc { color:var(--muted); font-size:var(--fs-sm); margin:0 0 12px; max-width:60ch; line-height:1.5; }
/* Formulare compacte pe 2 coloane in Setari. */
.setari-fgrid { display:grid; grid-template-columns:1fr 1fr; gap:10px 14px; max-width:560px; margin:0 0 12px; }
.setari-fgrid .fg-full { grid-column:1 / -1; }
.setari-fgrid label { display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:3px; }
.setari-fgrid input { width:100%; box-sizing:border-box; font:inherit; font-size:14px;
background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:7px 10px; }
@media (max-width:560px) { .setari-fgrid { grid-template-columns:1fr; } }
.setari-btn-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.setari-micro { font-size:var(--fs-xs); color:var(--muted); }
/* Plan: tier + consum pe un rand, bara de progres sub ele. */
.plan-row { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin:0 0 8px; }
.plan-tier { font-size:var(--fs-md); font-weight:600; color:var(--ink); }
.plan-usage { font-size:var(--fs-sm); color:var(--muted); font-variant-numeric:tabular-nums; }
.plan-meter { height:6px; border-radius:99px; background:var(--line2); max-width:340px;
overflow:hidden; margin:0 0 8px; }
.plan-meter > span { display:block; height:100%; border-radius:99px; background:var(--accent); }
.setari-badge { font-size:11px; font-weight:700; padding:2px 9px; border-radius:99px;
text-transform:uppercase; letter-spacing:.03em; border:1px solid transparent; }
.setari-badge.sb-ok { color:var(--ok); background:color-mix(in srgb,var(--ok) 12%,transparent);
border-color:color-mix(in srgb,var(--ok) 40%,transparent); }
.setari-badge.sb-warn { color:var(--warn); background:color-mix(in srgb,var(--warn) 12%,transparent);
border-color:color-mix(in srgb,var(--warn) 40%,transparent); }
.setari-badge.sb-info { color:var(--muted); background:color-mix(in srgb,var(--muted) 12%,transparent);
border-color:color-mix(in srgb,var(--muted) 35%,transparent); }
.setari-subzone-sep { border:none; border-top:1px solid var(--line); margin:20px 0 18px; }
/* Cheie API afisata o singura data. */
.keybox { font-family:var(--font-mono); font-size:14px; background:var(--card2);
border:1px solid var(--line); border-radius:8px; padding:12px 14px; word-break:break-all;
max-width:480px; }
.btn-warn { background:transparent; color:var(--warn); border:1px solid var(--warn); border-radius:6px;
padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px;
display:inline-flex; align-items:center; justify-content:center; gap:6px; }
.btn-warn:hover, .btn-warn:focus-visible { background:var(--warn); color:#fff; filter:none; }
/* Credentiale RAR: doua carduri de mediu paralele. */
.env-cards { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:18px; }
.env-card { border:1px solid var(--line); border-radius:10px; padding:16px; background:var(--card2); }
.env-card.active-test { border-color:color-mix(in srgb,var(--ok) 45%,var(--line)); }
.env-card h4 { margin:0 0 12px; font-size:var(--fs-sm); display:flex; align-items:center;
justify-content:space-between; gap:8px; }
.env-card .env-sw { display:flex; align-items:center; gap:8px; font-size:var(--fs-sm);
color:var(--muted); margin-bottom:12px; }
.env-card .env-lbl { display:block; font-size:var(--fs-xs); color:var(--muted); margin-bottom:5px; }
.env-card input[type=email], .env-card input[type=password] {
width:100%; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:8px 10px; margin-bottom:10px; font-size:var(--fs-sm); }
.env-card .env-confirm { display:flex; align-items:flex-start; gap:8px; font-size:var(--fs-xs);
color:var(--muted); line-height:1.4;
background:color-mix(in srgb,var(--err) 7%,transparent);
border:1px solid color-mix(in srgb,var(--err) 22%,transparent);
border-radius:7px; padding:9px 11px; margin-top:2px; }
.env-card .env-confirm input { margin-top:2px; flex-shrink:0; }
.default-env { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 18px;
padding:14px; border:1px solid var(--line); border-radius:9px; background:var(--card2); }
.default-env select { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:7px 10px; max-width:340px; }
@media (max-width:640px) { .env-cards { grid-template-columns:1fr; } }
</style>
</head>
<body>
{# Antet branduit ROA AUTOPASS.
Grila 3 coloane — stanga (logo) | centru (titlu+env+tier+account_name) | dreapta (RAR dot + tema + burger).
Antet MINIMAL pe /login: nu afiseaza RAR dot, meniu burger sau account_name (nelogat). #}
<header>
{# Celula stanga: logo ROMFAST #}
<div class="header-left">
{# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre.
Invelit in <a href="/"> pentru a naviga la Trimiteri (Acasa) de pe orice pagina. #}
<a href="/" style="display:inline-flex; align-items:center; text-decoration:none;">
<img src="/static/romfast_logo.png" alt="ROMFAST" class="brand-logo">
</a>
</div>
{# Celula centru: titlu ROA AUTOPASS + badge env + badge tier + sub-titlu account_name.
Titlul linkeaza la / (Trimiteri) ca si logo-ul. #}
<div class="header-center">
<a href="/" style="text-decoration:none; color:inherit;">
<h1>ROA AUTOPASS{% if not is_authenticated|default(false) %}<span class="badge-env">{{ rar_env }}</span>{% endif %}{% if is_authenticated|default(false) and tier_label|default('') %}<span class="badge-tier">{{ tier_label }}</span>{% endif %}</h1>
</a>
{% if is_authenticated|default(false) and account_name|default('') %}
<div class="h-sub">Service auto: <span class="svc">{{ account_name }}</span></div>
{% endif %}
</div>
{# Celula dreapta: dot RAR (numai cand logat) + selector tema + versiune + meniu burger #}
<div class="header-right">
{# Dot RAR in antet — OK = chip verde pulsant, BLOCAT = chip rosu.
Banda plina apare DOAR in _status.html cand BLOCAT (nu mai e mereu vizibila). #}
{% if is_authenticated|default(false) %}
{% if sanatate_ok|default(true) %}
<div class="rar-chip rar-ok" role="status"
title="RAR online{% if last_login|default('') %} — Ultima autentificare: {{ last_login }}{% endif %}"
aria-label="RAR online">
<span class="rar-dot live" aria-hidden="true"></span>
<span class="rar-tx">RAR</span>
</div>
{% else %}
<div class="rar-chip rar-err" role="status"
title="RAR indisponibil"
aria-label="RAR indisponibil">
<span class="rar-dot" aria-hidden="true"></span>
<span class="rar-tx">RAR blocat</span>
</div>
{% endif %}
{% include '_header_env_badge.html' %}
{# Comutatorul "Trimite automat la RAR" langa semnalul RAR.
In afara swap-ului periodic al #status-bar. #}
{% include '_auto_send_toggle.html' %}
{% endif %}
{# Selector tema = pill cu icon FIX (acelasi SVG ca landing) +
eticheta temei curente. Eticheta ascunsa pe <=560px via CSS. JS actualizeaza
doar #tema-label (iconita nu se mai schimba per tema — consecventa cu landing). #}
<button id="tema-toggle" class="tema-btn"
aria-label="Comuta tema"
title="Comuta tema">
<svg class="tema-icon" width="17" height="17" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none"/></svg>
<span id="tema-label">Light</span>
</button>
<span class="muted" style="font-size:var(--fs-xs);">v{{ version }}</span>
{% if is_authenticated|default(false) %}
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
Structura cu <hr> separatori + RAR status (prima intrare) + Plan tier.
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
<div class="cont-menu-wrap">
<button id="cont-menu-toggle" class="icon-btn"
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
aria-label="Meniu cont" title="Meniu cont">&#9776;</button>
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
{# Prima intrare: starea RAR #}
{% if sanatate_ok|default(true) %}
<div class="menu-rar-line ok" role="menuitem" aria-disabled="true">
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
RAR online
</div>
{% else %}
<div class="menu-rar-line err" role="menuitem" aria-disabled="true">
<span style="width:8px;height:8px;border-radius:99px;background:currentColor;display:inline-block;"></span>
RAR indisponibil
</div>
{% endif %}
{# Plan cont curent: linie detaliata cu trial/consum/warn.
Warn = culoare + text (accesibilitate). #}
<div class="menu-rar-line" role="menuitem" aria-disabled="true"
style="color:{% if plan_limita_atinsa|default(false) %}var(--err){% elif plan_warn|default(false) %}var(--warn){% else %}var(--muted){% endif %};
{% if plan_warn|default(false) %}font-weight:600;{% endif %}">
{{ plan_linie|default('Plan: ' + (tier_label|default('Gratuit'))) }}
</div>
<hr>
{# Grup LUCRU: fluxul zilnic — trimiteri, mapari, nomenclator. #}
<div class="cont-menu-group">Lucru</div>
<a role="menuitem" href="/"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg></span>Trimiteri</a>
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
<a role="menuitem" href="/?tab=mapari"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14 14 10"/><path d="M11.5 6.5 13 5a4 4 0 0 1 6 6l-1.5 1.5"/><path d="M12.5 17.5 11 19a4 4 0 0 1-6-6l1.5-1.5"/></svg></span>Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
<a role="menuitem" href="/?tab=nomenclator"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4a1 1 0 0 1 1-1h13v18H6a1 1 0 0 0-1 1z"/><path d="M5 18h14"/></svg></span>Nomenclator</a>
<hr>
{# Grup CONT: configurare + audit + admin. "Cont" redenumit "Setari" (URL ramane ?tab=cont). #}
<div class="cont-menu-group">Cont</div>
<a role="menuitem" href="/?tab=cont"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.6 4.6l2.1 2.1M17.3 17.3l2.1 2.1M4.6 19.4l2.1-2.1M17.3 6.7l2.1-2.1"/></svg></span>Setari</a>
<a role="menuitem" href="/?tab=integrare"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v6M15 3v6"/><path d="M7 9h10v3a5 5 0 0 1-10 0z"/><path d="M12 17v4"/></svg></span>Integrare</a>
<a role="menuitem" href="/?tab=jurnal"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3h8l4 4v14H7z"/><path d="M15 3v4h4"/><path d="M10 13h6M10 17h6"/></svg></span>Jurnal</a>
{% if is_admin|default(false) %}<a role="menuitem" href="/admin"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="8" r="3"/><path d="M3.5 20a5.5 5.5 0 0 1 11 0"/><path d="M16 5.5a3 3 0 0 1 0 5"/><path d="M17.5 14.2A5.5 5.5 0 0 1 21 20"/></svg></span>Conturi clienti</a>{% endif %}
<hr>
<form method="post" action="/logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
<button role="menuitem" type="submit"><span class="mi"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-3"/><path d="M9 12h12"/><path d="m18 9 3 3-3 3"/></svg></span>Iesi din cont</button>
</form>
</div>
</div>
{% endif %}
</div>
</header>
{# aria-live pentru anuntarea schimbarilor de tema (accesibilitate) #}
<span id="tema-live" role="status" aria-live="polite"
style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"></span>
{# Toast global: feedback tranzitoriu (ex. dupa salvarea unui rand de import).
aria-live=polite -> citit de screen-reader. window.arataToast(text, stareCss). #}
<div id="toast" role="status" aria-live="polite" hidden></div>
<main>{% block content %}{% endblock %}</main>
{# Modal detaliu trimitere: container global, SIBLING al <main> (nu descendent),
ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el. Corpul
#detaliu-modal-body e tinta de swap pentru fragment + rutele corectie/mapare/
lifecycle. Traieste in afara #submissions-wrap -> poll-ul nu-l atinge. #}
<div id="modal-detaliu" class="modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="detaliu-modal-titlu" hidden>
<div class="modal-backdrop" data-modal-close></div>
<div class="modal-dialog" role="document">
<button type="button" class="modal-close" data-modal-close aria-label="Inchide detaliul">&times;</button>
<div id="detaliu-modal-body"></div>
</div>
</div>
{# Slot pentru modalul de confirmare tipata: auto-release OFF->ON injecteaza
aici modalul via OOB (#modal-confirma-slot). Bulk "Trimite toate" isi randeaza propriul
modal inline in _coada.html. #}
<div id="modal-confirma-slot"></div>
<script>
// Comutator tema ciclic: config traieste intr-o singura structura
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/LABELS/NEXT.
// Adaugarea unei teme noi = O singura intrare in THEMES.
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
// 'auto' se rezolva la paint prin anti-FOUC (dark OS -> 'dark', light OS -> 'light').
(function() {
var btn = document.getElementById('tema-toggle');
if (!btn) return;
// SURSA DE ADEVAR UNICA: adaugarea unei teme = o singura intrare aici.
// Iconita e FIXA (acelasi SVG ca landing) — nu se mai deriva din tema.
var THEMES = [
{id:'light', label:'Light'},
{id:'dark', label:'Dark'},
{id:'petrol', label:'Petrol'},
{id:'grafit', label:'Grafit'},
{id:'cobalt', label:'Cobalt'},
{id:'cupru', label:'Cupru'},
{id:'hartie', label:'Hartie'},
{id:'auto', label:'Auto'},
];
// Derivate din THEMES (nu literali separati — DRY E2):
var CYCLE = THEMES.map(function(t) { return t.id; });
var VALID = THEMES.reduce(function(a, t) { a[t.id] = 1; return a; }, {});
var LABELS = THEMES.reduce(function(a, t) { a[t.id] = t.label; return a; }, {});
var NEXT = (function() {
var n = {};
THEMES.forEach(function(t, i) { n[t.id] = THEMES[(i + 1) % THEMES.length].label; });
return n;
})();
function _stored() {
try { var v = localStorage.getItem('theme'); return (v && VALID[v]) ? v : 'auto'; } catch(e) { return 'auto'; }
}
function _resolved(stored) {
if (stored !== 'auto') return stored;
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
function _syncButton(stored) {
var s = VALID[stored] ? stored : 'auto';
// Iconita e fixa (nu se schimba per tema); actualizam doar eticheta.
var label = document.getElementById('tema-label');
if (label) label.textContent = LABELS[s];
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
btn.title = LABELS[s]; // doar numele temei (ex. "Petrol"), nu ciclul intreg
}
function _setTheme(t) {
document.documentElement.setAttribute('data-theme', _resolved(t));
try { localStorage.setItem('theme', t); } catch(e) {}
_syncButton(t);
var live = document.getElementById('tema-live');
if (live) live.textContent = 'Tema: ' + LABELS[t] + (t === 'auto' ? ' (urmeaza sistemul)' : '');
}
// Init: sincronizeaza iconita din starea stocata (fara a scrie in localStorage).
_syncButton(_stored());
btn.addEventListener('click', function() {
var cur = _stored();
var idx = CYCLE.indexOf(cur);
_setTheme(CYCLE[(idx + 1) % CYCLE.length]);
});
})();
</script>
<script>
// Meniu cont: dropdown ancorat dreapta-sus. Deschide/inchide la click,
// inchide la Esc (focus readus pe buton) si la click in afara. Fara dependente.
(function() {
var toggle = document.getElementById('cont-menu-toggle');
var menu = document.getElementById('cont-menu');
if (!toggle || !menu) return;
function open() {
menu.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
document.addEventListener('click', onDocClick, true);
document.addEventListener('keydown', onKey, true);
}
function close(refocus) {
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', onDocClick, true);
document.removeEventListener('keydown', onKey, true);
if (refocus) toggle.focus();
}
function onDocClick(e) {
if (!menu.contains(e.target) && e.target !== toggle) close(false);
}
function onKey(e) {
if (e.key === 'Escape') { e.preventDefault(); close(true); }
}
toggle.addEventListener('click', function(e) {
e.stopPropagation();
if (menu.hidden) open(); else close(false);
});
})();
</script>
<script>
// Kebab partajat (actiuni per-rand). `<details class="kebab">` + `.kebab-menu` position:fixed.
// Pozitionarea se face in JS la deschidere (eveniment `toggle`, captat pe document fiindca nu
// bubble-uie), ancorat sub buton si aliniat la dreapta; flip in sus daca nu incape jos. Delegare
// pe document → supravietuieste swap-urilor HTMX (#mapari-section se re-randeaza la fiecare salvare).
(function() {
function position(d) {
var btn = d.querySelector('summary');
var menu = d.querySelector('.kebab-menu');
if (!btn || !menu) return;
var r = btn.getBoundingClientRect();
menu.style.visibility = 'hidden';
var mw = menu.offsetWidth, mh = menu.offsetHeight;
var left = Math.max(8, r.right - mw);
var top = (r.bottom + mh > window.innerHeight - 8 && r.top - mh - 4 > 8)
? r.top - mh - 4 : r.bottom + 4;
menu.style.left = left + 'px';
menu.style.top = top + 'px';
menu.style.visibility = '';
}
function closeAll(except) {
document.querySelectorAll('details.kebab[open]').forEach(function(d) {
if (d !== except) d.removeAttribute('open');
});
}
// `toggle` nu bubble-uie -> ascultam in faza de capturare pe document.
document.addEventListener('toggle', function(e) {
var d = e.target;
if (!d.classList || !d.classList.contains('kebab')) return;
if (d.open) { closeAll(d); position(d); }
}, true);
document.addEventListener('click', function(e) {
if (!e.target.closest('details.kebab')) closeAll(null);
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAll(null);
});
// La scroll/resize repozitionam meniul deschis (position:fixed nu urmareste ancora singur).
window.addEventListener('scroll', function() {
var open = document.querySelector('details.kebab[open]');
if (open) position(open);
}, true);
window.addEventListener('resize', function() { closeAll(null); });
})();
</script>
<script>
// Dirty state pentru butoanele de salvare din tabelele de mapari.
// Cand utilizatorul schimba un select dintr-un form de mapare, butonul de salvare
// legat prin data-dirty-form devine evidentiat (clasa "dirty" → fundal --accent).
// Starea "dirty" e efemera per-render: un swap outerHTML o reseteaza automat.
// Delegare pe document → supravietuieste swap-urilor HTMX (#mapari-section).
document.addEventListener('change', function(e) {
var el = e.target;
if (el.tagName !== 'SELECT' || !el.form || !el.form.id) return;
var saveBtn = document.querySelector('button[data-dirty-form="' + el.form.id + '"]');
if (saveBtn) saveBtn.classList.add('dirty');
});
</script>
<script>
// Bara contextuala bulk (trimiteri): vizibila DOAR cu selectie.
// Delegare pe document (supravietuieste swap-urilor HTMX); dupa un swap al
// listei checkbox-urile se reseteaza, deci sync() ascunde bara automat.
(function() {
function syncBulkBar() {
var form = document.getElementById('bulk-trimiteri');
var bar = document.getElementById('bulk-bar');
if (!form || !bar) return;
var n = form.querySelectorAll('input[name="submission_id"]:checked').length;
bar.classList.toggle('activ', n > 0);
bar.querySelectorAll('.bulk-n').forEach(function(el) { el.textContent = n; });
bar.querySelectorAll('.bulk-n-txt').forEach(function(el) {
el.textContent = (n === 1 ? 'rand' : 'randuri');
});
}
document.addEventListener('change', function(e) {
if (e.target && e.target.name === 'submission_id') syncBulkBar();
});
document.addEventListener('click', function(e) {
if (!(e.target.closest && e.target.closest('#bulk-deselect'))) return;
document.querySelectorAll('#bulk-trimiteri input[name="submission_id"]:checked')
.forEach(function(cb) { cb.checked = false; });
syncBulkBar();
});
document.body.addEventListener('htmx:afterSwap', syncBulkBar);
})();
</script>
<script>
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa
// fiecare salvare serverul re-randeaza cu sub-tabul corect via `aria-selected`/`hidden`,
// iar acest handler preia doar navigarea manuala intre taburi).
document.addEventListener('click', function(e) {
var tab = e.target.closest('.setari-subtab');
if (!tab) return;
var card = tab.closest('#card-cont');
if (!card) return;
var name = tab.getAttribute('data-subtab');
card.querySelectorAll('.setari-subtab').forEach(function(t) {
var on = t === tab;
t.setAttribute('aria-selected', on ? 'true' : 'false');
t.classList.toggle('is-active', on);
});
card.querySelectorAll('.setari-pane').forEach(function(p) {
if (p.getAttribute('data-subpane') === name) { p.removeAttribute('hidden'); }
else { p.setAttribute('hidden', ''); }
});
});
</script>
<script>
// Toast global: feedback tranzitoriu vizibil + accesibil (aria-live).
// window.arataToast(text, stareCss) — stareCss (ex. "s-error"/"s-needs_review")
// coloreaza punctul indicator (rosu/galben/verde). Auto-dispare dupa ~3.2s.
(function() {
var t = document.getElementById('toast');
var timer = null;
window.arataToast = function(text, stareCss) {
if (!t) return;
t.className = '';
if (stareCss) t.classList.add('t-' + stareCss);
t.textContent = text;
t.hidden = false;
void t.offsetWidth; // reflow -> tranzitia porneste
t.classList.add('show');
if (timer) clearTimeout(timer);
timer = setTimeout(function() {
t.classList.remove('show');
setTimeout(function() { t.hidden = true; }, 220);
}, 3200);
};
})();
// Feedback dupa salvarea/confirmarea unui rand de import (HX-Trigger 'randSalvat').
// Toast imediat (ce rand + ce stare are acum); evidentierea randului se aplica dupa
// ce preview-ul se reincarca (reincarcaPreview), de scriptul din _preview_import.
document.body.addEventListener('randSalvat', function(e) {
var d = e.detail || {};
if (window.arataToast)
window.arataToast('Randul ' + d.nr + ' actualizat · ' + (d.stare || ''), d.stareCss || '');
window.__randSalvat = d;
});
</script>
<script>
// Cautare + paginare client-side pentru tabele mari (data-dt="<page_size>"). Filtreaza si
// pagineaza DOM-ul deja randat (fara cereri server) — potrivit pentru maparile care pot creste
// la sute de randuri. Re-init la full load SI dupa swap-urile HTMX (tab Mapari, salvare/stergere).
// Markup: <div data-dt="10"> [input data-dt-search] <table> [div data-dt-empty] [div data-dt-pager] </div>
(function() {
function enhance(scope) {
(scope || document).querySelectorAll('[data-dt]').forEach(function(wrap) {
if (wrap.__dt) return; // idempotent (afterSettle poate re-scana)
wrap.__dt = true;
var table = wrap.querySelector('table');
var tbody = table && table.tBodies[0];
if (!tbody) return;
var size = parseInt(wrap.getAttribute('data-dt'), 10) || 10;
var search = wrap.querySelector('[data-dt-search]');
var pager = wrap.querySelector('[data-dt-pager]');
var empty = wrap.querySelector('[data-dt-empty]');
var rows = Array.prototype.slice.call(tbody.rows);
var page = 1;
// Haystack-ul randului: atributul data-dt-row daca exista, altfel textContent. Necesar
// cand randul contine un <select> (optiunile lui ar pune tot nomenclatorul in textContent
// -> orice cautare ar potrivi orice rand). Vezi data-dt-row in _mapari.html.
function haystack(r) {
var h = r.getAttribute('data-dt-row');
return (h !== null ? h : r.textContent).toLowerCase();
}
function matched() {
var q = (search && search.value || '').trim().toLowerCase();
if (!q) return rows;
return rows.filter(function(r) { return haystack(r).indexOf(q) !== -1; });
}
function draw() {
var fr = matched();
var pages = Math.max(1, Math.ceil(fr.length / size));
if (page > pages) page = pages;
if (page < 1) page = 1;
var start = (page - 1) * size;
rows.forEach(function(r) { r.style.display = 'none'; });
fr.slice(start, start + size).forEach(function(r) { r.style.display = ''; });
if (empty) empty.style.display = fr.length ? 'none' : '';
if (!pager) return;
if (fr.length <= size && page === 1) { pager.style.display = 'none'; pager.innerHTML = ''; return; }
pager.style.display = '';
pager.innerHTML = '';
var info = document.createElement('span');
info.textContent = (fr.length ? start + 1 : 0) + '' +
Math.min(start + size, fr.length) + ' din ' + fr.length;
var prev = document.createElement('button');
prev.type = 'button'; prev.textContent = 'Inapoi'; prev.disabled = page <= 1;
prev.addEventListener('click', function() { page--; draw(); });
var next = document.createElement('button');
next.type = 'button'; next.textContent = 'Inainte'; next.disabled = page >= pages;
next.addEventListener('click', function() { page++; draw(); });
pager.appendChild(info); pager.appendChild(prev); pager.appendChild(next);
}
if (search) search.addEventListener('input', function() { page = 1; draw(); });
draw();
});
}
document.addEventListener('DOMContentLoaded', function() { enhance(document); });
document.body.addEventListener('htmx:afterSettle', function(e) { enhance(e.target); });
})();
</script>
<script>
// Modal detaliu trimitere: detaliul se incarca prin HTMX in #detaliu-modal-body
// (in afara #submissions-wrap, deci poll-ul nu-l atinge). Aici: deschidere la click
// pe rand, inchidere (x/Esc/backdrop), focus-trap, scroll-lock, inert+aria-hidden pe
// <main>, stare de eroare la load esuat, inchidere pe succes corectie/sterge
// (HX-Trigger inchideModal).
(function() {
var overlay = document.getElementById('modal-detaliu');
if (!overlay) return;
var dialog = overlay.querySelector('.modal-dialog');
var body = document.getElementById('detaliu-modal-body');
var main = document.querySelector('main');
var trigger = null; // randul care a deschis modalul (focus return la inchidere)
var triggerId = null; // id-ul randului: re-query la inchidere daca poll-ul l-a re-swapuit
var onKeyTrap = null;
function focusable() {
return Array.prototype.filter.call(
dialog.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]),' +
' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),
function(el) { return el.offsetParent !== null || el === document.activeElement; });
}
// focus-trap — Tab/Shift+Tab cicleaza in interiorul dialogului.
function trapFocus(e) {
if (e.key !== 'Tab') return;
var f = focusable();
if (!f.length) { e.preventDefault(); return; }
var first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
function isOpen() { return !overlay.hidden; }
function open(triggerRow) {
trigger = triggerRow || null;
triggerId = (triggerRow && triggerRow.id) || null;
body.innerHTML = '<div class="empty muted" style="padding:24px;">Se incarca&hellip;</div>';
overlay.hidden = false;
document.body.classList.add('modal-open'); // scroll-lock pe body
if (main) { main.setAttribute('inert', ''); main.setAttribute('aria-hidden', 'true'); }
onKeyTrap = trapFocus;
document.addEventListener('keydown', onKeyTrap, true);
var x = overlay.querySelector('.modal-close');
if (x) x.focus(); // focus initial in modal
}
function close() {
if (!isOpen()) return;
overlay.hidden = true;
body.innerHTML = '';
document.body.classList.remove('modal-open');
if (main) { main.removeAttribute('inert'); main.removeAttribute('aria-hidden'); }
if (onKeyTrap) { document.removeEventListener('keydown', onKeyTrap, true); onKeyTrap = null; }
var t = trigger; trigger = null;
if (t && t.focus) t.focus(); // focus readus pe rand
}
// API public: butonul „Inchide" din fragment + inchiderea pe succes corectie/sterge.
window.inchideDetaliu = function() { close(); };
// Inchidere: x si backdrop (elemente cu data-modal-close), Esc.
overlay.addEventListener('click', function(e) {
if (e.target && e.target.closest && e.target.closest('[data-modal-close]')) close();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isOpen()) { e.preventDefault(); close(); }
});
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
// Trateaza atat .trimitere-row (Trimiteri) cat si .btn-editeaza (preview import)
// → open() instaleaza inert pe <main>, focus-trap si readuce focusul la inchidere.
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (!elt || !elt.classList) return;
if (elt.classList.contains('trimitere-row') || elt.classList.contains('trimitere-slim') || elt.classList.contains('btn-editeaza')) open(elt);
});
// Dupa swap-ul fragmentului (sau re-render corectie/mapare): muta focusul in modal.
body.addEventListener('htmx:afterSettle', function() {
if (!isOpen()) return;
var f = focusable();
if (f.length) f[0].focus();
});
// Load-error al fragmentului (GET esuat) -> stare Reincearca/Inchide, nu placeholder blocat.
body.addEventListener('htmx:responseError', function(evt) {
if (!isOpen()) return;
var elt = evt.detail && evt.detail.elt;
var url = (elt && elt.getAttribute && elt.getAttribute('hx-get')) || '';
body.innerHTML = '<div class="modal-eroare"><p>Nu s-a putut incarca detaliul.</p>' +
'<div class="actiuni">' +
(url ? '<button type="button" data-modal-retry="' + url + '">Reincearca</button>' : '') +
'<button type="button" data-modal-close' +
' style="background:var(--card); color:var(--muted); border-color:var(--line);">Inchide</button>' +
'</div></div>';
});
body.addEventListener('click', function(e) {
var r = e.target.closest && e.target.closest('[data-modal-retry]');
if (r && window.htmx) htmx.ajax('GET', r.getAttribute('data-modal-retry'),
{ target: body, swap: 'innerHTML' });
});
// Inchidere pe succes corectie/sterge — ruta emite HX-Trigger `inchideModal`.
// Lista se reincarca separat prin `trimiteriChanged` (#submissions-wrap). Maparea
// inline NU emite inchideModal -> modalul ramane deschis sa arate codul rezolvat.
document.body.addEventListener('inchideModal', function() { close(); });
// Tastatura pe rand (role=button): Enter/Space deschid modalul.
document.body.addEventListener('keydown', function(evt) {
var t = evt.target;
if (!(t && t.classList && (t.classList.contains('trimitere-row') || t.classList.contains('trimitere-slim')))) return;
if (evt.key === 'Enter' || evt.key === ' ' || evt.key === 'Spacebar') {
evt.preventDefault();
t.click();
}
});
})();
</script>
<script>
// Filtrare stare prin pill-uri + reincarcare a tabelului (manuala sau auto din poller).
// Reincarcarea trece prin form -> pastreaza filtrul/pagina curenta (hx-include).
(function() {
// Quick-pills de data (Azi/7 zile/30 zile/Custom): seteaza interval sau dezvaluie campuri manuale.
// NU modifica f-status — pastreaza pill-ul de stare activ curent.
window.setDataRange = function(btn, range) {
var form = document.getElementById('filtre-trimiteri');
if (!form) return;
var de = document.getElementById('f-data-de');
var pana = document.getElementById('f-data-pana');
var hp = document.getElementById('f-page'); if (hp) hp.value = '1';
var customPanel = document.getElementById('custom-date-fields');
// Marcheaza pill-ul de data activ, reseteaza celelalte quick-pills
document.querySelectorAll('.pill-data').forEach(function(b) {
b.setAttribute('aria-pressed', 'false');
});
if (btn) btn.setAttribute('aria-pressed', 'true');
// Custom: dezvaluie campurile manuale, asteapta inputul utilizatorului.
// NU face submit automat; form-ul submite la change (hx-trigger="change").
if (range === 'custom') {
// Goleste valorile ramase de la ultimul preset — utilizatorul porneste de la curat.
if (de) de.value = '';
if (pana) pana.value = '';
if (customPanel) customPanel.style.display = 'flex';
if (de) de.focus();
return;
}
// Preset-uri: ascunde campurile manuale, seteaza intervalul si trimite imediat.
if (customPanel) customPanel.style.display = 'none';
var azi = new Date();
// Formateaza data ca YYYY-MM-DD in zona locala (nu UTC, ca sa nu cada cu -1 zi noaptea)
function fmt(d) {
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
var from, to;
if (range === 'azi') { from = fmt(azi); to = fmt(azi); }
else if (range === '7zile') { var d7 = new Date(azi); d7.setDate(d7.getDate() - 6); from = fmt(d7); to = fmt(azi); }
else if (range === '30zile') { var d30 = new Date(azi); d30.setDate(d30.getDate() - 29); from = fmt(d30); to = fmt(azi); }
else { from = ''; to = ''; }
if (de) de.value = from;
if (pana) pana.value = to;
if (form.requestSubmit) form.requestSubmit(); else form.submit();
};
// Pill de stare: scrie campul hidden, reseteaza pagina la 1 si re-trimite filtrul.
window.filtreazaStare = function(btn, status) {
var form = document.getElementById('filtre-trimiteri');
if (!form) return;
var hs = document.getElementById('f-status'); if (hs) hs.value = status || '';
var hp = document.getElementById('f-page'); if (hp) hp.value = '1';
document.querySelectorAll('#pills-categorii .pill-cat').forEach(function(b) {
b.setAttribute('aria-pressed', 'false');
});
if (btn) btn.setAttribute('aria-pressed', 'true');
if (form.requestSubmit) form.requestSubmit(); else form.submit();
};
// Reincarca tabelul pastrand filtrul curent (hx-include #filtre-trimiteri).
window.reincarcaTrimiteri = function() {
if (window.htmx) htmx.trigger('#submissions-wrap', 'reincarcaTrimiteri');
};
// Poller auto-refresh: compara versiunea datelor cu cea cu care s-a randat tabelul.
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
// fara sesiune dedicata — auto-refresh acoper ambele cazuri.
var INTERVAL = 20000;
function versiuneCurenta() {
var e = document.getElementById('trimiteri-versiune');
return e ? e.getAttribute('data-v') : null;
}
var _verifica_in_curs = false;
function verifica() {
if (versiuneCurenta() === null) return; // tabelul nu e pe ecran (alt tab)
if (_verifica_in_curs) return; // evita suprapuneri
_verifica_in_curs = true;
fetch('/_fragments/trimiteri-versiune', { headers: { 'X-Requested-With': 'fetch' } })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(d) {
if (d && d.v !== versiuneCurenta()) reincarcaTrimiteri();
})
.catch(function() {})
.finally(function() { _verifica_in_curs = false; });
}
setInterval(verifica, INTERVAL);
})();
</script>
</body>
</html>