# PRD 5.3 — Light/Dark mode (comutator tema persistat)

**Stare**: inchis

> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead).

## 1. Obiectiv

Dashboard-ul web e azi **doar dark** (paleta fixa in `:root`). Adaugam o tema **light** si un
**comutator in header** care persista alegerea utilizatorului. Service-urile care vin din Visual
FoxPro / soft propriu lucreaza des in birouri luminoase si pe monitoare unde dark-mode obositor sau
greu de citit la videoproiector — un toggle light/dark e o cerinta de ergonomie de baza (Etapa 5).

CSS-ul **e deja pe variabile** (`--bg`, `--card`, `--ink`, `--muted`, `--line`, `--ok`, `--warn`,
`--err`, `--accent` in `base.html`). Tema light = un bloc `[data-theme="light"]` care **suprascrie
aceleasi variabile** cu o paleta deschisa. Efort mic, zero logica de domeniu, zero backend.

**Invariant de design (motivul cheie):** comutarea nu trebuie sa **palpaie** (FOUC — flash of
unstyled / wrong-theme content). Tema se aplica **inainte de primul paint** printr-un script inline
mic in `<head>`, care citeste preferinta din `localStorage` si seteaza `data-theme` pe `<html>`
sincron, inainte ca `<body>` sa randeze. Fara asta, fiecare incarcare de pagina ar clipi dark→light.

## 2. Non-Goals (anti scope-creep)

- **NU backend / cookie / ruta noua.** Persistenta = `localStorage` pur client-side (roadmap zice
  "cookie/localStorage" — alegem localStorage: zero suprafata server + anti-FOUC prin scriptul din
  `<head>`). Nu se atinge `routes.py`, `auth.py`, sesiunea, baza de date.
- **NU redesign de paleta dark.** Tema dark ramane **identica la octet** cu cea de azi (default
  pastrat); adaugam doar varianta light + un toggle. Nicio culoare dark existenta nu se schimba.
- **NU teme multiple / personalizate / culoare de accent reglabila.** Doar doua: `dark` (default)
  si `light`.
- **NU atinge worker, masina de stari, idempotenta, mapping, schema, validation.py, API.** Strict
  `app/web/templates/base.html` (+ eventual un test de template).
- **NU restilizeaza fragmentele HTMX.** Toate fragmentele (`_*.html`) mostenesc variabilele din
  `base.html` — comuta automat cu tema. (Conditie: zero culori hardcodate care sa nu adapteze —
  vezi US-001 pentru suprafetele care azi au fundal hardcodat.)

## 3. Stories atomice

> Ambele stories ating **acelasi fisier** (`base.html`) → **secventiale**, un singur worker (sau
> lead direct, livrabila mica — ROADMAP §5.5). NU se paralelizeaza (regula fisier-comun §5.5).

### US-001: Tema light (paleta + suprafete theme-aware)
**Ca** utilizator al dashboard-ului **vreau** o paleta light corecta si lizibila **pentru ca** sa pot
folosi gateway-ul confortabil in birou luminos / la videoproiector, fara contrast slab sau zone care
raman intunecate.

- **Depinde de**: —
- **Fisiere**: `app/web/templates/base.html` (bloc CSS `[data-theme="light"]` + conversia
  suprafetelor cu fundal hardcodat la variabile), `tests/test_tema.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_tema.py` —
  - `test_paleta_light_definita` — HTML-ul `GET /login` (sau dashboard) contine un selector
    `[data-theme="light"]` care redefineste `--bg`, `--card`, `--ink`, `--muted`, `--line`.
  - `test_dark_ramane_default` — `:root` contine inca paleta dark exacta (`--bg:#0f1115`,
    `--card:#181b22`, `--ink:#e6e9ef`) → default neschimbat.
  - `test_suprafete_fara_fundal_hardcodat` — fundalurile de stare (banner eroare/warn, flash) NU mai
    folosesc literal hex dark fix (`#241a1a`, `#201c0f`, `#16241c`) ci variabile/`color-mix` ce
    adapteaza la tema (asertie pe absenta literalilor in `<style>`).
- **Acceptance criteria**:
  - [ ] Exista un bloc `[data-theme="light"]` in `<style>` care suprascrie cel putin
        `--bg`, `--card`, `--ink`, `--muted`, `--line` cu o paleta deschisa (fundal deschis, text
        inchis). Contrastul text/fundal ≥ WCAG AA (4.5:1 pentru `--ink` pe `--bg` si pe `--card`).
  - [ ] Paleta **dark** din `:root` ramane neschimbata la octet (default) — comportament identic cu azi
        cand nu exista preferinta salvata si OS-ul nu cere light.
  - [ ] Suprafetele cu fundal azi hardcodat dark (`.banner`, `.banner.warn`, `.flash`, eventual
        `.drop-zone.drag-over`) sunt facute theme-aware (variabile sau `color-mix` peste paleta), astfel
        incat in light arata corect (fundal deschis colorat, nu pata intunecata).
  - [ ] Culorile semantice (`--ok`/`--warn`/`--err`/`--accent`) raman lizibile pe fundal light
        (ajustate daca e nevoie pentru contrast ≥ AA pe text mic).
  - [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser pe `/` cu `document.documentElement.dataset.theme="light"` setat manual →
  fundal deschis, text inchis lizibil, banner/flash/pill citibile, tabele cu linii vizibile, niciun
  text "invizibil" (acelasi ton ca fundalul).

### US-003: Suprafete theme-aware si in fragmentele HTMX (fix VERIFY r1)
**Ca** utilizator pe light mode **vreau** ca bannerele de eroare/warn/flash din fragmentele HTMX sa
fie lizibile **pentru ca** azi raman pete intunecate cu text invizibil (defect prins la VERIFY r1).

- **Depinde de**: US-001 (extinde aceeasi conversie theme-aware dincolo de `base.html`)
- **Motiv**: US-001 a convertit la `color-mix` DOAR `base.html`; aceleasi fundaluri hardcodate dark
  (`#241a1a` err, `#201c0f` warn) traiesc ca **inline-style** in 7 fragmente `_*.html` (10 aparitii)
  — randate in dashboard, deci vizibile in light ca text invizibil. Testul US-001 scana doar `<style>`
  din base.html → trecea vacuu.
- **Fisiere**: `_status.html`, `_banner.html`, `_upload.html`, `_preview_import.html`,
  `_preview_rand.html`, `_trimitere_detaliu.html`, `_mapcoloane.html` (toate `app/web/templates/`),
  `tests/test_tema.py` (extins)
- **Test intai (RED)**: `tests/test_tema.py` —
  - `test_fragmente_fara_fundal_hardcodat` — scaneaza TOATE fisierele `app/web/templates/_*.html`
    (continutul brut) si asigura ca niciunul nu contine literalii `#241a1a`, `#201c0f`, `#16241c`.
- **Acceptance criteria**:
  - [ ] Niciun fisier din `app/web/templates/` (inclusiv fragmentele) nu mai contine literalii hex
        dark-fix `#241a1a`/`#201c0f`/`#16241c`; fundalurile folosesc `color-mix` peste paleta
        (`var(--err)`/`var(--warn)`/`var(--ok)` 12% peste `var(--card)`), exact ca in `base.html`.
  - [ ] In light mode bannerele/flash-urile de stare au fundal deschis colorat cu text lizibil
        (verificat E2E pe dashboard: banner "Cont in asteptare de activare" nu mai e cutie neagra).
  - [ ] In dark mode aspectul ramane practic identic cu azi (color-mix peste `--card` dark dă aproape
        aceeasi nuanta).
  - [ ] Testul de protectie scaneaza fragmentele, nu doar `base.html` (lacuna r1 inchisa).
  - [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: dashboard pe light mode → banner pending-account + orice flash de eroare/warn
  lizibile (fundal deschis); comuta la dark → aspect neschimbat.

### US-002: Comutator tema in header + persistenta + anti-FOUC
**Ca** utilizator **vreau** un buton in header care comuta light/dark si imi tine minte alegerea
**pentru ca** sa nu re-comut la fiecare incarcare si sa nu vad un flash de tema gresita.

- **Depinde de**: US-001 (vizual; tehnic ating acelasi fisier → oricum secvential)
- **Fisiere**: `app/web/templates/base.html` (script inline anti-FOUC in `<head>` + buton toggle in
  `<header>` + handler de comutare/persistenta), `tests/test_tema.py` (extins, acelasi fisier ca US-001)
- **Test intai (RED)**: `tests/test_tema.py` —
  - `test_script_antifouc_in_head_inainte_de_style` — `<head>` contine un `<script>` care citeste
    `localStorage` cheia `theme` si seteaza `document.documentElement` `data-theme` **inainte** de
    tag-ul `<style>` (pozitie in HTML: index script < index `<style>`).
  - `test_buton_toggle_in_header_cu_eticheta` — `<header>` contine un control de comutare cu
    `aria-label`/`title` descriptiv (ex. "Comuta tema") si un `id`/atribut stabil pentru handler.
  - `test_toggle_pe_login_si_dashboard` — butonul apare si pe `/login` (neautentificat) si pe dashboard
    (ambele extind `base.html`).
- **Acceptance criteria**:
  - [ ] Script inline in `<head>`, plasat **inaintea** `<style>`, citeste preferinta:
        `localStorage.theme` daca exista, altfel `prefers-color-scheme` (fallback final: `dark`), si
        seteaza `data-theme` pe `<html>` sincron → **fara FOUC** la incarcare/reload.
  - [ ] Buton de comutare in `<header>` (langa `env`/`version`), cu `aria-label` descriptiv, atins
        usor (≥ 36px zona de atins), care comuta `data-theme` light↔dark **fara reload**.
  - [ ] La comutare se scrie `localStorage.theme` → alegerea persista peste reload si peste navigari
        (deep-link `?tab=`), inclusiv pe paginile neautentificate (login/signup).
  - [ ] Butonul reflecta starea curenta (eticheta/iconita arata ce face: "→ light" cand e dark si
        invers), accesibil la tastatura (e un `<button>`).
  - [ ] Functioneaza pe toate cele 4 pagini top-level (login, signup, dashboard, admin) — toate extind
        `base.html`, deci o singura implementare le acopera.
  - [ ] `python3 -m pytest -q` verde.
- **Verificare E2E** (Playwright MCP / `/browse`): pe `/login` apoi pe dashboard — (a) click toggle →
  paleta comuta instant (fundal/text), (b) reload pagina → tema aleasa **persista** (citeste din
  localStorage, fara flash de tema veche), (c) comuta inapoi → persista invers, (d) zero erori in
  consola, (e) fara FOUC vizibil la reload (tema corecta din primul frame).

## 4. Riscuri

- **FOUC** (risc principal) → mitigat prin scriptul inline din `<head>` ASAMBLAT INAINTE de `<style>`,
  care seteaza `data-theme` sincron, pre-paint. Verificat E2E (reload nu clipeste).
- **Suprafete hardcodate raman dark in light** (banner/flash cu hex fix) → mitigat de US-001 (asertie
  de test pe absenta literalilor + theme-aware via variabile/`color-mix`).
- **Contrast slab in light** (text gri pe alb, accent palid) → mitigat: AC explicit ≥ WCAG AA pe
  `--ink`/`--muted` peste `--bg`/`--card`; verificare E2E vizuala (text lizibil).
- **Regresie pe dark** (refactor accidental al paletei existente) → mitigat: `test_dark_ramane_default`
  lock-uieste hex-urile dark exacte in `:root`; Non-Goal explicit "dark identic la octet".
- **localStorage indisponibil** (mod privat strict / dezactivat) → script defensiv: `try/catch` in
  jurul citirii/scrierii; cade pe default (dark/OS) fara sa arunce. (AC implicit: zero erori consola.)
- **Acelasi fisier in 2 stories** (`base.html`) → NU se paralelizeaza; un singur worker secvential
  (US-001 apoi US-002). Notat in §6.

## 5. Intrebari deschise

> Rezolvate cu utilizatorul la poarta de aprobare PRD (2026-06-22).

- **Default pentru utilizator nou (fara preferinta salvata)** — REZOLVAT: **OS-aware cu fallback dark**
  (onoreaza `prefers-color-scheme`; cand OS-ul nu cere light → `dark`, look-ul actual). [user 2026-06-22]
- **Persistenta** — REZOLVAT: **`localStorage`** (client-only, anti-FOUC prin script in `<head>`, zero
  backend; nu atinge `routes.py`/sesiune). [user 2026-06-22]
- **Aspect comutator** — REZOLVAT: **iconita soare/luna + `aria-label`** descriptiv, compact in header.
  [user 2026-06-22]

## 6. Valuri de executie (graful de dependente)

```
Val 1: [US-001] → [US-002]   ← SECVENTIAL (acelasi fisier base.html). Un singur worker (sau lead direct).
```

Livrabila mica, un singur fisier de productie atins (`base.html`): poate rula fara `TeamCreate`
(un worker Sonnet TDD, ambele stories secvential). VERIFY in context curat + writeback raman
obligatorii (ROADMAP §5.5).

## 7. Review-uri de plan (aplicate inainte de cod — ROADMAP §5.3)

> Se completeaza la PLAN inainte de aprobare. CEO + Eng obligatorii; Design — DA (atinge UI).

**CEO (valoare/scope) — PASS.** Cerinta directa de ergonomie din Etapa 5 (decizie utilizator
2026-06-22), efort mic peste o fundatie deja pregatita (CSS pe variabile). Calea cea mai scurta:
reuse `:root` + override `[data-theme]`, zero backend. Inversiune ("ce-l face inutil?"): FOUC la
incarcare (face produsul sa para buggy) — neutralizat prin scriptul anti-FOUC din `<head>`; si
suprafetele hardcodate care raman dark in light (arata stricat) — neutralizate prin theme-aware in
US-001. Scope minim corect (doua teme, fara personalizare). Niciun scope creep.

**Eng (fezabilitate/teste) — PASS.** Fezabilitate triviala, un singur fisier de productie, zero
atingere de backend/schema/worker. Testele acopera contractul de template (paleta light prezenta,
dark neschimbat, suprafete theme-aware, script anti-FOUC pozitionat corect, toggle prezent +
accesibil + pe paginile neautentificate); comportamentul vizual + persistenta + anti-FOUC raman pe
E2E browser (corect — nu se pot prinde la TestClient). Risc unic real = FOUC, prins doar in browser
→ E2E explicit cu reload. `localStorage` defensiv (try/catch) acoperit ca AC zero-erori.

**Design — PASS (cu note).** Atinge UI direct. Paleta light trebuie sa respecte contrast WCAG AA
(AC explicit). Comutatorul: iconita soare/luna + `aria-label`, plasat in header langa env/version,
zona de atins ≥ 36px (consistent cu `.cardlink` existent). Suprafetele semantice (ok/warn/err) sa
ramana distincte si lizibile pe light, nu doar inversate. Tranzitia de comutare poate fi instant
(fara animatie) ca sa nu para lenta; daca se adauga `transition` pe culori, scurta (≤ 150ms, ca
restul UI-ului). De confirmat vizual la VERIFY E2E.

---

## Raport VERIFY

> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.

### Runda 1 (2026-06-22) — FAIL

Verificator independent (context curat, rol qa-only). **VERDICT: FAIL** (un blocker, US-001).

- **Suita — PASS.** `python3 -m pytest -q` → 583 passed; `tests/test_tema.py` → 6 passed.
- **US-002 (toggle + persistenta + anti-FOUC) — PASS integral (E2E browser).** Toggle pe
  login/signup/dashboard; comutare instant (bg `#f6f7f9`↔`#0f1115`, aria-label + iconita comuta);
  persistenta `localStorage.theme` doar la click; reload pastreaza tema fara FOUC; OS-aware fara
  scriere (load simplu → `localStorage.theme`=null); zero erori consola (doar 404 favicon preexistent).
- **US-001 (paleta light + suprafete theme-aware) — FAIL.** Paleta light + dark-neschimbat + `base.html`
  color-mix = PASS, DAR suprafetele de stare raman pete intunecate cu **text invizibil** in light:
  literalii dark `#241a1a`/`#201c0f` NU au fost convertiti in **fragmentele HTMX** (10 aparitii, 7
  fisiere: `_status.html`, `_banner.html`, `_upload.html`, `_preview_import.html`, `_preview_rand.html`,
  `_trimitere_detaliu.html`, `_mapcoloane.html`) — inline-style, randate in dashboard. Dovada: banner
  "Cont in asteptare de activare" = cutie `rgb(32,28,15)` pe fundal `#f6f7f9`. Testul `test_suprafete_
  fara_fundal_hardcodat` scana DOAR `<style>` din base.html → verde inselator (vacuu).
- **Regresie de aur — PASS.** Pur frontend; worker/coada/API/schema neatinse (worker a procesat o
  orfana `sent idPrezentare=68801` la pornire → pipeline send functional, neafectat de 5.3).

**Remediu (US-003, adaugat in §3):** muta literalii la `color-mix` in cele 7 fragmente + extinde testul
sa scaneze fragmentele. Re-VERIFY cu subagent NOU dupa fix.

### Runda 2 (2026-06-22) — PASS

Verificator independent NOU (context curat, rol qa-only), dupa fix US-003. **VERDICT GLOBAL: PASS.**

- **Suita — PASS.** `python3 -m pytest -q` → 584 passed; `tests/test_tema.py` → 7 passed (incl. noul
  `test_fragmente_fara_fundal_hardcodat`).
- **Anti-regresie protectie — PASS.** `grep -rn -E '#201c0f|#241a1a|#16241c' app/web/templates/` →
  GOL. Testul scaneaza fisierele `_*.html` de pe disc (`Path.glob`), nu doar base.html → lacuna r1 inchisa.
- **US-001/US-003 (light lizibil) — PASS (E2E, dovada cheie).** Cont inactive fortat → banner
  "Cont in asteptare de activare" din `_status.html` in light: `background` ≈ `rgb(246,234,225)` (crema
  deschis), text `rgb(26,29,36)` (contrast ~13:1 ≫ AA), border amber. Cutia neagra `rgb(32,28,15)` din
  r1 a DISPARUT. Screenshot confirma caseta peach cu text negru lizibil. Scan al tuturor suprafetelor
  vizibile in light: zero zone genuin intunecate. Dark mode: aspect practic neschimbat.
- **US-002 — PASS (re-confirmat).** Toggle instant; `localStorage.theme` doar la click; aria-label se
  inverseaza; persista peste reload in ambele sensuri; anti-FOUC (script in `<head>` inainte de `<style>`);
  toggle pe `/login` + dashboard; zero erori JS de tema in consola.
- **Regresie de aur — PASS.** Diff strict frontend (`base.html` + 7 fragmente + `tests/test_tema.py`);
  worker/coada/API/schema/mapping NEATINSE. Flux LIVE RAR catre RAR test NEPROBAT (creds key efemera;
  send neatins de 5.3) — NEPROBAT, nu FAIL.
- **Nota cleanup:** verificatorul a lasat un cont de test inactive (id=5, verifyr2@test.com) in DB-ul
  de test efemer folosit pentru a forta bannerul — inofensiv (date de test, mediu test).

### CLOSE — `/code-review` high (2026-06-22)

1 finding real reparat: in paleta light `--ok:#16a34a` (green-600) folosit ca text (`.s-sent`/`.s-ok`,
bife verzi din bara de stare) pe `--card:#ffffff` dadea contrast ~3.3:1 — sub WCAG AA 4.5:1 pentru text
mic, incalcand AC-ul US-001. Reparat → `--ok:#15803d` (green-700, ~5.0:1, trece AA); paleta dark
neatinsa. Restul semanticelor light trec deja AA (--err 6.2:1, --warn 5.0:1, --accent 5.1:1). Schimbare
de valoare CSS (fara comportament) → fara re-VERIFY. Refutate: suport `color-mix` (universal pe browsere
moderne, audienta B2B); duplicarea inline a `color-mix` in fragmente (precede acest diff, candidat de
cleanup viitor — macro/clase `.flash.err`/`.flash.warn`). 584 teste pass.
