# EditMode — Ghid de configurare a conținutului editabil

> Perechea lui `EDITMODE_PORTING_GUIDE.md`. Acela spune cum instalezi modulul
> EditMode într-un proiect Yii2; **acesta spune CE devine editabil și CUM** —
> regulile de decizie, convențiile de chei, tiparele de conversie a view-urilor,
> migrarea conținutului existent și capcanele întâlnite pe viu.
>
> Scris ca instrucțiune pentru un developer sau un agent AI (Claude Code) care
> primește un proiect cu EditMode deja instalat și trebuie să marcheze
> conținutul. Proiectul-exemplu: **cinova** (branch `contactForm`) — fiecare
> tipar de aici există acolo, funcțional, și poate fi citit ca referință.

---

## 0. Principiul de bază

**Un conținut are UN singur stăpân.** Ce se editează din EditMode se scoate din
admin (meniu, controllere, registru, rândurile DB ale sursei); ce rămâne în
admin nu se marchează în EditMode. Dubla gestiune = două locuri care se
suprascriu reciproc și un editor care nu știe unde s-a pierdut modificarea.

---

## 1. Tabelul de decizie — ce devine editabil

| Tip de conținut | Unde stă | De ce |
|---|---|---|
| Anteturi de secțiune (eyebrow / titlu / subtitlu) | **EditMode** | text redacțional pur; se editează în context |
| Blocuri de marketing cu număr FIX de carduri (features, benefits, steps, approach cards, marquee) | **EditMode** | tot rostul lor e textul; structura (3 carduri, 4 pași) e design, nu date |
| Texte hardcodate în view-uri / chei `Yii::t('frontend', …)` din secțiuni | **EditMode** | altfel editorul nu le poate atinge deloc |
| Valori „settings" folosite în secțiuni (badge-uri, contoare, caption-uri) | **EditMode** | aceeași natură redacțională; se șterg din `settings` după migrare |
| Fotografii de secțiune, avataruri, iconițe de card | **EditMode** (`editableImage`) | împreună cu textul lor |
| Etichetele CTA-urilor | **EditMode** (doar eticheta) | ruta rămâne în view — vezi §5.4 |
| Texte de formular public: etichete de câmp, mesaje de validare, confirmări | **EditMode** (§5.8) | exact textele pentru care se cere reformulare; cât timp stau în cod, orice virgulă cere un deploy |
| Colecții cu pagini de detaliu (blog, servicii, proiecte, portofoliu) | **ADMIN** | sunt date: listare, SEO per rând, imagini per rând, sortare, coș |
| Colecții cu număr VARIABIL de rânduri (testimoniale, FAQ, prețuri, parteneri, cereri de contact) | **ADMIN** | EditMode nu poate adăuga/șterge rânduri, doar edita elemente existente |
| Numele înregistrării oriunde apare (H1 pe detaliu, ultima firimitură de breadcrumb) | **NICIUNDE** — urmează înregistrarea | vezi §6 |
| Utilizatori, limbi, pagini(SEO), setări tehnice | **ADMIN** | nu sunt conținut |

Regula scurtă: **dacă e o frază pe care un om de marketing ar vrea s-o
rescrie, e EditMode; dacă e un rând dintr-o listă care crește, e admin.**

---

## 2. Convenții de chei

- Regex impus de RenderHelper: `[a-zA-Z0-9_-]{1,64}`, **UNIC GLOBAL**
  (constrângere DB pe `elements.key`).
- Per pagină: `{pagina}-{sectiune}-{camp}` → `home-about-title`,
  `about-approach-card1-text`, `home-marquee-3`.
- Partajat între pagini: prefix `common-`, pe pagina **`technical`** →
  `common-benefit1`, `common-feature2-title`, `common-avatar4`,
  `common-crumb-home`.
- Sufixe standard: `-eyebrow`, `-eyebrow-accent`, `-title`, `-title-accent`,
  `-subtitle`, `-text`, `-cta`, `-image`, `-icon`, `-photo`.
- **pageKey = `pages.slug` real al paginii** (`home`, `about-us`, `services`,
  `contact-us`), nu un nume inventat — altfel RenderHelper creează un rând
  `pages` duplicat. Șabloanele de detaliu și tot ce e partajat: `technical`.

### Ce anume se partajează

Dacă același conținut apare pe mai multe pagini (cardul „despre studio" pe home
și about; feature-urile pe home și services; „Acasă" din breadcrumb pe toate
paginile) → **UN singur element**, refolosit. Editezi o dată, se schimbă peste
tot, iar paginile nu mai pot diverja. Anteturile de secțiune rămân separate per
pagină chiar când textul e azi identic — acolo divergența e legitimă.

---

## 3. Tiparul „accent split" (obligatoriu pentru titluri)

Design-ul ține accentul verde inline:
`Ce <span class="special-word">facem</span>`. Editorul EditMode e **text simplu**
(`strip_tags` la salvare) — un singur element ar pierde accentul sau ar afișa
tag-ul brut. De aceea fiecare titlu/eyebrow cu accent = **două elemente**, iar
span-ul stă în markup, unde editorul nu-l poate strica:

```php
<h2>
  <?= RenderHelper::editableText($page, $prefix . '-title', '', 'span') ?>
  <span class="special-word"><?= RenderHelper::editableText($page, $prefix . '-title-accent', '', 'span') ?></span>
</h2>
```

Un accent gol randează un span gol — invizibil, dar clicabil în editor, deci
accentul poate fi adăugat ulterior.

Pentru anteturi complete folosește un partial dedicat (cinova:
`views/common/_section_header_em.php`) cu opțiunile `page`, `prefix`,
`wrapClass`, `titleClass`, `withEyebrow`, `withSubtitle` — și convertește
apelurile vechiului partial unul câte unul.

---

## 4. Migrarea conținutului existent — SEED ÎNAINTE DE CONVERSIE

**Cea mai importantă regulă din tot ghidul.** RenderHelper auto-populează, la
prima randare, TOATE limbile active cu default-ul inline — în limba în care s-a
nimerit să fie. Dacă proiectul are deja traduceri (tabele `*_translations`,
`settings_translations`, fișiere `messages/{ro,ru}/*.php`), conversia fără seed
le aruncă.

Ordinea corectă, per lot de conținut:

```
1. php yii editmode-migrate      # copiază valorile reale per limbă în elements/element_content
2. conversia view-urilor          # abia acum RenderHelper găsește rândurile existente
3. verificare pe toate limbile
4. php yii editmode-migrate/purge # șterge sursele mutate din admin (headers, settings)
5. ștergerea gestiunii din admin  # §7
```

Model de comandă: `commands/EditmodeMigrateController.php` din cinova — mapări
declarative (HEADERS / SETTINGS / MESSAGES / LITERALS / CRUMBS / BANNERS /
ENTITIES), idempotentă (cheile existente se sar), cu `splitAccent()` pentru
titluri. Adapteaz-o schimbând doar constantele de mapare.

Reguli în seeder:

- **`page_id` al elementului TREBUIE să coincidă cu pageKey-ul din view.**
  Căutarea e `(page_id, key)`, dar cheia e unică global: un element legat de
  altă pagină face view-ul să încerce re-crearea cheii → eroare 500 pe indexul
  unic. (Bug real, prins în cinova.)
- **Nu seed-ui imagini din consolă**: `@web` e `/` în aplicația de consolă, dar
  site-ul poate rula în subdirector (`/cinova/web`) — ai scrie căi greșite.
  Imaginile n-au traduceri; lasă default-urile `editableImage()` să creeze
  rândurile la prima randare web. Excepție: dacă ceva TREBUIE seed-uit cu src,
  fă-o dintr-un request web, nu din CLI.
- Limba pivot: rândul lipsă într-o limbă primește valoarea EN (fallback), ca
  editorul să nu pornească de la câmp gol.

---

## 5. Tipare de conversie a view-urilor

### 5.1 Text simplu
```php
// înainte:  <p><?= Html::encode(Settings::text('team_caption')) ?></p>
// după:
<p><?= RenderHelper::editableText('technical', 'common-team-caption', '', 'span') ?></p>
```
Default gol când valoarea a fost seed-uită; default real când elementul se
auto-creează la prima randare.

### 5.2 Bloc de N carduri dintr-o entitate → N carduri fixe
Bucla peste `Entity::findActive()` devine markup explicit (sau buclă pe
`[1..N]`) cu chei numerotate. Structura lead/side a design-ului devine fixă —
editorul schimbă orice text/imagine, dar nu poate strica slider-ul. Vezi
secțiunea features din `views/home/index.php`.

### 5.3 Imagini
```php
<?= RenderHelper::editableImage('technical', 'common-about-photo',
    Url::to('@web/core/assests/people/big-person.jpg'), 'CEO-expert') ?>
```
Pentru seturi repetate pe mai multe pagini (avataruri): un singur bazin
partajat, **numerotat per stivă** (1..6 în fiecare stivă), NU secvențial pe
pagină — altfel aceeași stivă vizuală arată fețe diferite pe pagini diferite.
(Bug real, corectat în cinova.)

### 5.4 CTA-uri
`editableLink` nu are sufix, iar săgeata design-ului vine DUPĂ etichetă. Deci:
păstrează `<a>`-ul din view (cu ruta lui) și fă doar eticheta editabilă:
```php
<a href="<?= Url::to(['/contact/index']) ?>">
  <?= RenderHelper::editableText('technical', 'common-contact-cta', 'Contact Us', 'span') ?>
  <i class="bi bi-arrow-right-short"></i>
</a>
```
Excepție — link-uri doar-cu-icon (butonul de video): acolo chiar href-ul e ce
se editează → `editableLink($page, $key, '', $hrefDefault, $attrs, '', $iconHtml)`.

### 5.5 Contoare animate
Pune clasa scriptului chiar pe elementul editabil:
```php
<?= RenderHelper::editableText('technical', 'common-wwd-counter1', '180', 'span', ['class' => 'counter']) ?>+
```

### 5.6 Valori folosite și în logică
Când valoarea intră într-un calcul (procentul unui progress-bar), citește-o cu
`textValue()` și randeaz-o separat cu `editableText()` — același element, două
utilizări.

### 5.7 După conversie
Șterge din view importurile și query-urile moarte (`use app\models\features\…`,
`$features = Features::findActive()…`) și adaugă `use app\helpers\RenderHelper;`.

### 5.8 Formulare: etichete, mesaje de validare, panoul de editor

Regula §1 („dacă e o frază pe care un om de marketing ar vrea s-o rescrie, e
EditMode") se aplică și textelor care NU sunt pe ecran tot timpul: mesajele de
validare, confirmarea de trimitere, eticheta „se trimite". Sunt exact textele
pentru care clientul cere cel mai des o reformulare, și singurele care ajungeau
să fie schimbate printr-un deploy. Tiparul e preluat din **bcars**
(`views/common/_contact_quote.php` + `models/contact/forms/QuoteForm.php`).

**a) Un singur bazin de chei, partajat între toate formularele.** Mesajele de
validare nu aparțin unei pagini — sunt `common-form-error-*` pe `technical`:

| Cheie | Ce e |
|---|---|
| `common-form-error-required` | câmp obligatoriu |
| `common-form-error-email` | email invalid |
| `common-form-error-phone` | telefon invalid |
| `common-form-error-min` / `-max` | lungime minimă / maximă |
| `common-form-error-generic` | eșec la salvare / honeypot |
| `common-form-success` | confirmarea de trimitere |
| `common-form-sending` | eticheta butonului în timpul cererii |

Etichetele câmpurilor sunt per formular: `common-contact-form-firstname`,
`-lastname`, `-email`, `-phone`, `-message`.

**b) Modelul citește elementele, nu `Yii::t`.** Un helper static pe formular,
cu default-ul inline păstrat ca fallback (și ca valoare de auto-seed):

```php
public static function t(string $key, string $default): string
{
    return RenderHelper::textValue('technical', $key, $default);
}

public function rules(): array
{
    $required = self::t('common-form-error-required', Yii::t('frontend', 'This field is required.'));
    return [
        [['first_name', 'email'], 'required', 'message' => $required],
        // ...
    ];
}
```

**c) Etichetele sunt ACELEAȘI elemente pe care le randează inputurile.**
`attributeLabels()` citește elementele, iar view-ul pune eticheta ca
`placeholder`. Așa „«Prenume» este obligatoriu" nu poate contrazice niciodată
ce scrie în câmp. Placeholder-ele nu se mai marchează separat — ar fi două
stăpâne pentru același text (§0).

**d) `{attribute}`, `{min}`, `{max}` rămân valabile în text.** Yii le
înlocuiește la formatare, deci editorul le poate muta sau șterge; șterse,
mesajul rămâne pur și simplu fără numele câmpului sau fără limită. Scrie asta
în panoul de editor — altfel cineva le va șterge fără să știe ce erau.

**e) Validarea client și cea server citesc același element.** Cu Yii ActiveForm
(`enableClientValidation`) mesajele client sunt generate din `rules()`, deci
sincronizarea e gratuită. Cu JS propriu (bcars) trimiți aceleași valori în
config-ul scriptului prin `textValue()` — niciodată un al doilea set de chei.

**f) Panoul de editor — partea fără de care nimic din asta nu e editabil.**
Mesajele apar pe ecran doar cât e o eroare afișată, deci în editor nu există pe
ce să dai click. Sub formular, DOAR pentru editor, se randează lista lor ca
spanuri editabile cu aceleași chei:

```php
<?php if (RenderHelper::isEditor()): ?>
  <div class="contact-form__editor-panel">
    <p class="contact-form__editor-title">Texte formular (vizibile doar în Edit Mode)</p>
    <ul>
    <?php foreach (ContactSubmitForm::editableStrings() as $key => $default): ?>
      <li><?= RenderHelper::editableText('technical', $key, $default, 'span') ?></li>
    <?php endforeach; ?>
    </ul>
    <p class="contact-form__editor-hint">„{attribute}" e înlocuit cu eticheta câmpului…</p>
  </div>
<?php endif; ?>
```

Lista `key => default` stă pe model (`editableStrings()`), nu în view: view-ul
o randează, modelul și controllerul o citesc — o singură sursă pentru chei.

**Condiția e `isEditorSurface()`, NU `isEditor()`.** A doua răspunde „poate omul
ăsta să editeze”, ceea ce e adevărat și pentru un administrator care doar
navighează pe site — iar panoul apărea atunci pe pagina publică de contact, la
vedere. Pentru marcajele invizibile (`class="editable"` nu face nimic fără
CSS-ul editorului) diferența nu contează; pentru orice bloc cu corp, contează.

`isEditorSurface()` cere în plus ca pagina să fie randată CHIAR ÎN editor:
editorul încarcă site-ul într-un iframe, iar browserul marchează acea cerere cu
`Sec-Fetch-Dest: iframe` (verificat pe viu — o navigare normală trimite
`document`, un `fetch()` trimite `empty`). Se acceptă și `?em_preview=`, pe care
editorul îl adaugă la previzualizarea ciornelor. În development, cu
`params[editModeNoAuth]`, totul rămâne vizibil pentru toată lumea, ca și
contururile punctate.
Stiluri: `.contact-form__editor-panel|__editor-title|__editor-hint`. În
producție panoul e doar pentru editor; în development, cu
`params['editModeNoAuth'] => true`, îl vede toată lumea, ca și conturul punctat
al celorlalte elemente.

**g) Seed ÎNAINTE de conversie, ca la orice altceva (§4).** Mesajele existau
deja traduse în `messages/{ro,ru}/frontend.php`; adaugă-le în `MESSAGES` din
`EditmodeMigrateController` și rulează `php yii editmode-migrate` **înainte** de
prima randare a formularului convertit. Altfel `textValue()` auto-creează cele
trei limbi cu textul limbii în care s-a nimerit să fie prima cerere.

**h) Ce NU se mută:** mesajele de eroare ale formularelor din admin (acolo
editorul nu ajunge), textele de securitate (login, reset parolă) și orice mesaj
care e citit de cod, nu de om.

**Capcană specifică:** dacă design-ul rezervă un singur rând sub câmp pentru
eroare (cinova o face, ca să nu sară layout-ul), un editor poate scrie o frază
de trei rânduri care se taie cu „…". Ține default-urile scurte și spune-o în
hint.

### 5.9 Navigație, logo și textele care trăiesc în JS

**a) Etichetele din meniu.** Vin din `menu_items_translations`, deci erau
editabile doar din admin, dintr-un ecran în care clientul nu ajunge. Devin
elemente `common-menu-<alias>` pe `technical`:

```php
// MenuItems::getLabel() — un singur loc care produce eticheta
$fallback = (string) $this->t('label', (string) $this->alias);
return RenderHelper::textValue('technical', 'common-menu-' . $this->alias, $fallback);
```

Ruta, ordinea și existența itemului rămân în admin — se editează DOAR textul,
ca la CTA-uri (§5.4). Elementul e partajat: header, footer și oriunde mai apare
itemul citesc același rând, deci nu pot diverge.

**Rezolvarea regulii „un singur stăpân" (§0)** fără să ștergi ecranul de admin:
rândul din `menu_items_translations` rămâne ca SEED și ca fallback, iar câmpul
din formularul de traduceri devine `readonly`, cu un hint care trimite în Edit
Mode. `readonly` (nu `disabled`) — un `disabled` nu se trimite la POST și ar
goli rândul la prima salvare.

**b) Logo-ul** e o imagine ca oricare alta: `editableImage('technical',
'common-header-logo', …)`. Nu se seed-uiește din consolă (§4).

**c) Comutatorul de limbi NU se marchează.** E navigație funcțională, nu text
redacțional; un editor care îi schimbă eticheta strică selectorul.

**d) Textele scrise de JavaScript.** Un fișier `.js` nu poate citi
`RenderHelper`, deci literalele din el rămân în limba în care au fost scrise
chiar dacă tot markup-ul din jur e tradus (a fost cazul demo-ului: bara de
editare rămânea în română pe `/en` și `/ru`). Tiparul, ca la `DEMO_CHOICES`:
view-ul citește elementele și le predă scriptului ca obiect de configurare,

```php
$this->registerJs(
    'window.DEMO_I18N = ' . Json::htmlEncode([
        'editOff' => $v('demo-bar-edit', 'Editează pagina'),
        'countMany' => $v('demo-js-count-many', '{n} modificări'),
    ]) . ';',
    View::POS_BEGIN
);
```

iar scriptul citește prin `t('cheie', 'literal vechi')`, păstrând literalul ca
fallback dacă pagina n-a randat configul. Pluralul: două chei (`-count-one`,
`-count-many`) și `{n}`; nu încerca reguli de plural în text simplu.

**e) Panourile „simulate" (demo-ul).** Nodurile din pagina-exemplu poartă ambele
seturi de atribute: `data-demo-key` (editorul fals, al vizitatorului) și
`data-key` (Edit Mode). Nu se ceartă pentru că scriptul demo-ului interceptează
click-urile doar după ce vizitatorul apasă „Editează pagina", ceea ce un editor
din Edit Mode nu face.

---

## 6. Breadcrumb-uri și H1-ul din banner

Regula: **editabil e ce e navigare/redacțional; fix e ce numește înregistrarea.**

- Firimiturile de navigare („Acasă", „Servicii") = elemente partajate
  (`common-crumb-home`, `common-crumb-services`…). Ultima firimitură = pagina
  curentă / numele înregistrării → **niciodată editabilă**.
- H1 din banner: pe paginile de listă/statice titlul vine din
  `pages_translations.banner_title` → editabil (cu accent split). Pe paginile
  de detaliu H1 = numele înregistrării → rămâne cum era.

Implementare cinova: `Pages::getBreadcrumbs()` pune `em_key` doar pe
firimiturile de navigare; `_page_banner.php` randează editabil ce are `em_key`
și primește `emPage`/`emPrefix` DOAR de la paginile de listă. Click-ul pe
firimitura editabilă nu navighează în editor (main.js face `preventDefault` pe
orice `.editable`), dar linkul rămâne funcțional pentru vizitatori.

---

## 7. Ștergerea din admin (partea a doua a regulii din §0)

După ce un lot e migrat și verificat:

1. **Entități mutate integral** (features, benefits, steps, approach cards,
   marquee): șterge controllerul admin + `modules/admin/views/{entitate}/` +
   intrarea din sidebar + intrarea din registrul de entități (dacă adminul are
   unul — cinova: `AdminEntityRegistry`). Lasă un comentariu la locul
   ștergerii: *„mutat în EditMode, vezi editmode-migrate"*.
2. **Rânduri mutate din tabele partajate** (`section_headers`, `settings`):
   le șterge `editmode-migrate/purge` — doar pe cele migrate, restul rămân în
   admin.
3. **Tabelele entităților mutate se PĂSTREAZĂ** ca backup — nimic nu le mai
   citește; se pot arunca ulterior printr-o migrare separată, decisă explicit.
4. Modelele rămase fără utilizări pot rămâne pe disc (inofensive) — dar nimic
   din views/controllers/config nu trebuie să le mai importe.

---

## 8. Capcane văzute pe viu (toate s-au întâmplat în cinova)

| Capcană | Simptom | Prevenție |
|---|---|---|
| CSS-ul temei stilizează spanuri „goale" (`ul li span {max-width:25px; background:…}`) | textul editabil injectat ca `<span>` e strivit / suprapus | dă elementului editabil o clasă proprie + reset punctual în CSS-ul temei; NU modifica regula originală |
| Element seed-uit pe altă pagină decât cere view-ul | 500: `Duplicate entry … uq_elements_key` | §4 — page_id = pageKey-ul din view |
| Elemente partajate lăsate pe auto-seed | toate limbile primesc valoarea primei pagini randate („Acasă" pe paginile EN) | §4 — seed per limbă din sursă, din consolă |
| Imagini seed-uite din consolă | src cu bază greșită în subdirector | nu seed-ui imagini din CLI |
| Numerotare secvențială a imaginilor partajate | stive identice vizual cu chei diferite per pagină | numerotează per stivă |
| OPcache după editarea unui controller | prima cerere rulează codul vechi; „bug" care dispare singur | la verificare, repetă testul o dată înainte de a diagnostica |
| Teste prin curl din Git Bash | MSYS rescrie `src=/cale` în `C:/Program Files/Git/cale` și strici datele | folosește valori din variabile citite din API, sau `MSYS_NO_PATHCONV=1`; verifică DB după teste |
| Seed pe TOATE limbile din tabel, nu doar pe cele active | 33 rânduri `element_content` per element în loc de 3 | `WHERE status = 'ACTIVE'` în orice migrare de seed |
| Pagină auto-creată de RenderHelper, apoi căutată de controller după `name` | SEO gol pe toată pagina, tăcut | `name` = ruta (`demo/index`), nu cheia de pagină (`demo`) |
| Text de interfață hardcodat într-un `.js` | rămâne într-o singură limbă peste tot | config `window.*_I18N` din view — §5.9 d |
| Mesaje de validare lăsate pe auto-seed | RO și RU primesc textul limbii primei cereri | §5.8 g — seed din `messages/*` înainte de conversie |
| Text de eroare mai lung decât rândul rezervat sub câmp | mesajul se taie cu „…” | default-uri scurte + hint în panoul de editor |
| `elements.key` unic GLOBAL | refolosirea unei chei pe altă pagină pică | prefixează cu pagina; partajarea se face intenționat, nu accidental |
| `processEditableBlocks()` pe fișiere PHP dinamice | DOM-parserul strică codul PHP | doar pe partiale HTML statice; pentru view-uri dinamice folosește helperele |

---

## 9. Checklist de verificare per lot

- [ ] `php -l` pe toate fișierele atinse; `node --check` dacă ai atins main.js
- [ ] Toate paginile afectate → 200, în TOATE limbile active
- [ ] Textele migrate apar identic cu înainte, în fiecare limbă (diff vizual pe
      RO/RU, nu doar EN — aici se văd seed-urile greșite)
- [ ] Editor: click pe fiecare tip nou de element → sidebarul se deschide cu
      formularul corect (TEXT/IMAGE/LINK)
- [ ] Un ciclu complet pe un element: draft → publish → apare pe site →
      istoric → restore → revine
- [ ] Elementele partajate: editarea într-o limbă schimbă toate paginile, dar
      NU celelalte limbi
- [ ] Ce trebuia să rămână fix (nume de înregistrări) chiar e fix
- [ ] Adminul nu mai arată sursele mutate; restul adminului funcționează
- [ ] `element_content_draft` gol după teste; fără elemente orfane
- [ ] Intrare în PROJECT_CHANGES.md (sau jurnalul proiectului)

---

## 10. Inventarul cinova (referință completă)

Ce s-a mutat în EditMode acolo — utilizabil ca șablon de scope pentru un
proiect similar:

- **Anteturi de secțiune**: toate (home ×11, about ×5, services ×4, contact ×2,
  șabloanele de detaliu proiect ×5 și serviciu ×5 — acestea din urmă partajate
  pe `technical`).
- **Entități dizolvate în EditMode**: features (3 carduri), benefits (2),
  marquee (4), approach cards (3), steps (4) — admin șters, tabele păstrate.
- **Din settings**: hero_badge, trusted_by, crawl_errors_*, indexing_*,
  team_caption, years_experience, client_rating.
- **Din frontend.php**: cardul „despre studio", blocul what-we-do complet,
  toate etichetele CTA.
- **Imagini**: fotografia about (partajată home+about), fotografiile feature
  (×3, partajate home+services), poza why-choose, poza testimoniale, 6
  avataruri partajate, iconițele steps, ilustrațiile approach.
- **Navbar**: etichetele din meniu (`common-menu-<alias>`, partajate cu
  footer-ul), logo-ul (`common-header-logo`) și eticheta butonului CTA
  (`common-header-cta`, mutată din `settings.cta_button`, care a fost
  șters). Comutatorul de limbi rămâne nemarcat — §5.9.
- **Pagina demo, integral** (`demo`): bara de editare, pagina-exemplu
  (`demo-x-*`), secțiunea cu componente din baza de date, secțiunea finală,
  sidebar-ul simulat și textele scrise de `demo-page.js`. Era singura
  pagină rămasă cu literale românești, deci `/en/demo` și `/ru/demo`
  serveau română — acum cele trei limbi sunt seed-uite explicit.
- **Textele formularului de contact**: etichetele câmpurilor
  (`common-contact-form-*`), mesajele de validare partajate
  (`common-form-error-*`), confirmarea și eticheta „se trimite”
  (`common-form-success`, `common-form-sending`) — conform §5.8.
- **Breadcrumb + H1 banner**: conform §6.
- **Rămase în admin**: blog, servicii, proiecte, categorii, portofoliu, FAQ,
  testimoniale, prețuri, parteneri, showcase-uri, cereri contact, pages(SEO),
  limbi, utilizatori.

*Scris pe baza migrării reale cinova, 2026-08-20 — fiecare regulă de aici are
în spate un commit sau un bug reparat în PROJECT_CHANGES.md.*
