# Gumirom v3 — Sell Like Crazy + Hormozi + Magazin MVP — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Restructure the Gumirom landing page on the Sell Like Crazy 17-step spine + Hormozi offer mechanics, fix the verified bugs, and ship a static tyre-shop MVP (new + second-hand) that sends orders via WhatsApp.

**Architecture:** Static site, no build. Pure logic lives in UMD-style IIFEs attached to `window.Gumirom` (`core.js`, `quiz.js`, `shop.js`) and is unit-tested in Node via `vm`; DOM wiring in the same files is guarded by `typeof document !== 'undefined'`. `main.js` keeps the page interactions and reads config from `Gumirom.CONFIG`.

**Tech Stack:** HTML5, CSS3, vanilla ES5-style JS (IIFE, `var`), Node 22 `node:test` for unit tests, Chrome (localhost:8082 via XAMPP) for browser checks.

**Spec:** `docs/superpowers/specs/2026-10-06-sell-like-crazy-hormozi-magazin-design.md` · Audit: `docs/audit-sell-like-crazy-hormozi.md`

## Global Constraints

- No frameworks, no build step, no external JS/CSS; site must work when uploaded as-is via FTP and when opened from `file://`.
- Code style matches existing: ES5 (`var`, `function`), IIFE, `'use strict'`, short Romanian comments.
- Never invent facts on the page: only 4,4★ / 125 recenzii Google, prices from CONFIG, services, hours, address, phones (0744 398 932, 0740 619 306).
- New commitments are marked in code with `PROPUNERE — de confirmat cu proprietarul` (garanția «Fără vibrații» 30 zile).
- Demo catalog: `GUMIROM_STOC.demo === true` must show a visible „stoc demonstrativ” banner.
- No fake scarcity: urgency only from the calendar season or real stock counts.
- Animate only transform/opacity; respect `prefers-reduced-motion`.
- No horizontal overflow at 320 px.
- `v3` is not a git repo: "commit" steps are replaced by `node --test tests/` checkpoints.

## Review Focus

1. Owner edits `js/stoc.js` with a missing/invalid field → the other products still render (pinned: `normalize` test in Task 5).
2. Saved cart references a product that was sold or whose stock dropped → cart reconciles silently (pinned: `cartReconcile` test in Task 5).
3. Names/notes with diacritics, `&`, `#`, newlines → WhatsApp URL decodes to the exact text (pinned: `waUrl` test in Task 1, `orderText` test in Task 5).
4. Booking a Sunday (closed) → rejected with a clear message (pinned: `isClosedDate` test in Task 1).
5. Season boundary days (15 dec / 16 dec, 14 mar / 15 mar, 15 mai / 16 mai) → correct offer name (pinned: `season` test in Task 1).

---

### Task 1: Test harness + `core.js` (config and shared pure functions)

**Files:**
- Create: `tests/helpers/load.js`, `tests/core.test.js`, `app/js/core.js`

**Interfaces:**
- Produces (on `window.Gumirom` / sandbox `Gumirom`):
  - `CONFIG` — `{ phoneMain, phoneSecond, whatsapp, address, mapsDirections, mapsReviews, facebook, schedule, prices, seasons, offer, company }` (`company: { name: '', cui: '', regCom: '' }` — afișat în footer-ul magazinului doar dacă e completat)
  - `pad2(n) → string`
  - `openState(date: Date) → { open: boolean, text: string }`
  - `isClosedDate(iso: 'YYYY-MM-DD') → boolean`
  - `season(date: Date) → 'iarna' | 'vara' | 'drum'`
  - `seasonInfo(date: Date) → { key, offerName, message, cta }`
  - `nextSeasonStart(date: Date) → Date` (15 Mar or 1 Oct, strictly after)
  - `quote({ vehicle, service, rim, qty }) → { unit: number, total: number, qty: number }`
  - `rimClass(inch: number) → 'r1' | 'r2' | 'r3'`
  - `waUrl(text: string) → string`
  - `formatLei(n: number) → string`
  - `icsEvent({ uid, date: Date, title, description }) → string`
  - `track(event: string, props?: object) → void`
  - `storage.get(key, fallback)`, `storage.set(key, value)`

- [ ] **Step 1: Write the harness**

```js
// tests/helpers/load.js
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const JS = path.join(__dirname, '..', '..', 'app', 'js');
function load(files, extra) {
  const sandbox = Object.assign({ console: console }, extra || {});
  vm.createContext(sandbox);
  files.forEach(function (f) {
    vm.runInContext(fs.readFileSync(path.join(JS, f), 'utf8'), sandbox, { filename: f });
  });
  return sandbox;
}
// obiecte din alt realm → JSON ca deepStrictEqual să compare structura
function plain(x) { return JSON.parse(JSON.stringify(x)); }
module.exports = { load: load, plain: plain };
```

- [ ] **Step 2: Write the failing tests**

```js
// tests/core.test.js
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { load, plain } = require('./helpers/load');
const G = () => load(['core.js']).Gumirom;
const d = (s) => { const [y, m, day, h, mi] = s.split(/[-T:]/).map(Number); return new Date(y, m - 1, day, h || 0, mi || 0); };

test('openState: deschis luni 10:00', () => {
  assert.deepEqual(plain(G().openState(d('2026-10-05T10:00'))), { open: true, text: 'Deschis acum · până la 18:00' });
});
test('openState: luni 07:00 → azi la 08:00', () => {
  assert.equal(G().openState(d('2026-10-05T07:00')).text, 'Închis · deschidem azi la 08:00');
});
test('openState: duminică → mâine la 08:00', () => {
  assert.equal(G().openState(d('2026-10-11T12:00')).text, 'Închis · deschidem mâine la 08:00');
});
test('openState: sâmbătă 15:00 → luni la 08:00', () => {
  assert.equal(G().openState(d('2026-10-10T15:00')).text, 'Închis · deschidem luni la 08:00');
});
test('isClosedDate: duminica e închisă, luni nu', () => {
  assert.equal(G().isClosedDate('2026-10-11'), true);
  assert.equal(G().isClosedDate('2026-10-12'), false);
  assert.equal(G().isClosedDate(''), false);
});
test('season: limite', () => {
  const g = G();
  [['2026-09-30', 'drum'], ['2026-10-01', 'iarna'], ['2026-10-06', 'iarna'], ['2026-12-15', 'iarna'],
   ['2026-12-16', 'drum'], ['2026-03-14', 'drum'], ['2026-03-15', 'vara'], ['2026-05-15', 'vara'],
   ['2026-05-16', 'drum']].forEach(([s, k]) => assert.equal(g.season(d(s)), k, s));
});
test('seasonInfo: numele ofertei', () => {
  assert.equal(G().seasonInfo(d('2026-10-06')).offerName, 'Gata de Iarnă');
  assert.equal(G().seasonInfo(d('2026-04-01')).offerName, 'Gata de Vară');
  assert.equal(G().seasonInfo(d('2026-07-01')).offerName, 'Gata de Drum');
});
test('nextSeasonStart: strict după', () => {
  const g = G();
  const iso = (x) => x.getFullYear() + '-' + g.pad2(x.getMonth() + 1) + '-' + g.pad2(x.getDate());
  assert.equal(iso(g.nextSeasonStart(d('2026-10-06'))), '2027-03-15');
  assert.equal(iso(g.nextSeasonStart(d('2026-03-15'))), '2026-10-01');
  assert.equal(iso(g.nextSeasonStart(d('2026-01-10'))), '2026-03-15');
  assert.equal(iso(g.nextSeasonStart(d('2026-09-30'))), '2026-10-01');
});
test('quote: tarifele din CONFIG', () => {
  const g = G();
  assert.deepEqual(plain(g.quote({ vehicle: 'car', service: 'mount', rim: 'r1', qty: 4 })), { unit: 25, total: 100, qty: 4 });
  assert.deepEqual(plain(g.quote({ vehicle: 'suv', service: 'mount', rim: 'r2', qty: 4 })), { unit: 35, total: 140, qty: 4 });
  assert.deepEqual(plain(g.quote({ vehicle: 'van', service: 'repair', rim: 'r3', qty: 1 })), { unit: 50, total: 50, qty: 1 });
  assert.deepEqual(plain(g.quote({ vehicle: 'car', service: 'rim', rim: 'r3', qty: 2 })), { unit: 100, total: 200, qty: 2 });
  assert.deepEqual(plain(g.quote({ vehicle: 'van', service: 'swap', rim: 'r1', qty: 4 })), { unit: 25, total: 100, qty: 4 });
  assert.equal(g.quote({ vehicle: 'car', service: 'mount', rim: 'r1', qty: 0 }).qty, 1);
  assert.equal(g.quote({ vehicle: 'car', service: 'mount', rim: 'r1', qty: 99 }).qty, 8);
});
test('rimClass', () => {
  const g = G();
  [[13, 'r1'], [16, 'r1'], [17, 'r2'], [18, 'r2'], [19, 'r3'], [22, 'r3']].forEach(([i, c]) => assert.equal(g.rimClass(i), c));
});
test('waUrl: o singură codare, diacritice, &, #, linii noi', () => {
  const text = 'Programare Gumirom\nNume: Ștefan & Ana #2\nZi preferată: 12.10.2026';
  const url = G().waUrl(text);
  assert.ok(url.startsWith('https://wa.me/40744398932?text='));
  assert.ok(!/\s/.test(url));
  assert.equal(decodeURIComponent(url.split('?text=')[1]), text);
});
test('formatLei', () => {
  assert.equal(G().formatLei(80), '80 lei');
  assert.equal(G().formatLei(1234), '1.234 lei');
  assert.equal(G().formatLei(1234567.4), '1.234.567 lei');
});
test('icsEvent: CRLF, dată all-day, escapare', () => {
  const s = G().icsEvent({ uid: 'x@gumirom', date: d('2027-03-15'), title: 'Schimb anvelope, Gumirom', description: 'Sună: 0744 398 932; programare' });
  assert.ok(s.startsWith('BEGIN:VCALENDAR\r\n'));
  assert.ok(s.includes('DTSTART;VALUE=DATE:20270315\r\n'));
  assert.ok(s.includes('DTEND;VALUE=DATE:20270316\r\n'));
  assert.ok(s.includes('SUMMARY:Schimb anvelope\\, Gumirom\r\n'));
  assert.ok(s.includes('DESCRIPTION:Sună: 0744 398 932\\; programare\r\n'));
  assert.ok(s.trimEnd().endsWith('END:VCALENDAR'));
});
test('track: împinge în dataLayer dacă există', () => {
  const box = load(['core.js'], { dataLayer: [] });
  box.Gumirom.track('call_click', { where: 'hero' });
  assert.deepEqual(plain(box.dataLayer), [{ event: 'gumirom_call_click', where: 'hero' }]);
});
test('track și storage nu aruncă fără window/localStorage', () => {
  const g = G();
  assert.doesNotThrow(() => g.track('x'));
  assert.equal(g.storage.get('k', 'fb'), 'fb');
  assert.doesNotThrow(() => g.storage.set('k', 1));
});
```

- [ ] **Step 3: Run tests — expect FAIL** — `node --test tests/` → ENOENT `core.js`.

- [ ] **Step 4: Implement `app/js/core.js`**

Key implementation (full file written in execution; behaviour fixed by the tests):

```js
(function (root) {
  'use strict';
  var G = root.Gumirom = root.Gumirom || {};
  G.CONFIG = { /* phones, whatsapp '40744398932', schedule {0:null,1..5:[8,18],6:[9,14]},
                 prices (moved verbatim from main.js), seasons {iarna,vara,drum: {offerName,message,cta}},
                 offer.guarantees (PROPUNERE flag on «Fără vibrații») */ };
  function pad2(n) { return (n < 10 ? '0' : '') + n; }
  // season: md = month*100 + day → iarna 1001–1215, vara 315–515, altfel drum
  // nextSeasonStart: candidates [Mar 15, Oct 1] of this year and next, first strictly > date (compare at day level)
  // waUrl: 'https://wa.me/' + CONFIG.whatsapp + '?text=' + encodeURIComponent(text)
  // formatLei: String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, '.') + ' lei'
  // icsEvent: lines joined with '\r\n', esc = s.replace(/\\/g,'\\\\').replace(/;/g,'\\;').replace(/,/g,'\\,').replace(/\n/g,'\\n')
  // track: dataLayer.push({event:'gumirom_'+e, ...props}) in try; CustomEvent 'gumirom:track' if root.dispatchEvent
  // storage: try { root.localStorage } catch → fallback
})(typeof window !== 'undefined' ? window : globalThis);
```

- [ ] **Step 5: Run tests — expect PASS** — `node --test tests/`.

---

### Task 2: Wire `main.js` to `core.js` + verified bug fixes (F1–F16)

**Files:**
- Modify: `app/js/main.js` (remove CONFIG/openState; use `Gumirom.*`), `app/index.html` (head `js` class, JSON-LD, counters, links, footer year, script tags), `app/css/style.css` (`.js` gating, small-screen buttons, scroll padding)

**Interfaces:**
- Consumes: `Gumirom.CONFIG`, `openState`, `isClosedDate`, `quote`, `waUrl`, `track`, `storage`.
- Produces: `main.js` init functions remain no-ops when their elements are absent (used by `magazin.html`).

- [ ] **Step 1:** `index.html` `<head>`: `<script>document.documentElement.className += ' js';</script>` before the stylesheet; scripts at end: `core.js`, `main.js` (defer, in order).
- [ ] **Step 2 (F1):** CSS — `.preloader` → `display:none` unless `.js`; `[data-reveal]` hidden state only under `.js [data-reveal]`.
- [ ] **Step 3 (F2):** HTML counters carry real values (`4,4`, `125`); `initCounters` sets `0` and animates when visible; reduced motion → final value.
- [ ] **Step 4 (F3/F4):** `splitChars` → words wrapped in `<span class="w">` (`white-space:nowrap`) holding `.char` spans, real spaces between words; H1 gets `aria-label` of its text, inner lines `aria-hidden="true"`. `splitWords` keeps element nodes (appends them unchanged).
- [ ] **Step 5 (F5):** CSS — at `max-width: 479px`: `.btn { white-space: normal; text-align: center; }`, `.btn--big { padding: 18px 26px; font-size: 1rem; }`; `.cta__title` clamp min `2rem`.
- [ ] **Step 6 (F6/F7):** booking form builds plain text and calls `Gumirom.waUrl`; after `window.open`, note contains a fallback `<a>` to the same URL; fields get `aria-invalid`, errors get ids + `aria-describedby`; `isClosedDate` → error „Duminica suntem închiși — alege altă zi.”; note colours via classes `.is-ok/.is-error`.
- [ ] **Step 7 (F8):** calculator uses `Gumirom.quote`; non-relevant rim pills get `disabled` + group `aria-disabled`; pills get `aria-pressed`; qty label switches „Număr roți”/„Număr jante”.
- [ ] **Step 8 (F9, F12–F14, F16):** status refresh `setInterval(…, 60000)`; menu closes on resize ≥ 768; parallax loop skips work when `scrollY > hero height`; preloader cap 1200 ms and skip when `sessionStorage` flag set (try/catch); footer year from `new Date()`.
- [ ] **Step 9 (F10/F11/F15):** JSON-LD drops `aggregateRating`, adds `sameAs` Facebook; „Direcții” → `https://www.google.com/maps/dir/?api=1&destination=Gumirom%2C+Strada+Liviu+Rebreanu+56%2C+Gherla`; map iframe `q=Gumirom%2C%20Strada%20Liviu%20Rebreanu%2056%2C%20Gherla`; CSS removes `html:focus-within`, adds `scroll-padding-top: calc(var(--header-h) + 12px)`.
- [ ] **Step 10:** `node --test tests/` PASS; browser: no console errors, 320 px overflow probe returns `[]`.

---

### Task 3: Landing page — Sell Like Crazy 17 steps + Hormozi offer

**Files:**
- Modify: `app/index.html`, `app/css/style.css`, `app/js/main.js`

**Interfaces:**
- Consumes: `Gumirom.seasonInfo`, `quote`, `formatLei`, `track`, `CONFIG.offer`.
- Produces DOM hooks: `[data-season-name]`, `[data-season-message]`, `[data-season-cta]`, `#offerPrice`, `#offerPerWheel`, `#stickyCta`, `[data-track]`.

- [ ] **Step 1: Hero (steps 1–4, 16).** Kicker „Pentru șoferii din Gherla și împrejurimi”. H1 lines „Roți sigure.” / „Preț spus” / „dinainte.” (accent). Subtitle: „Montaj și echilibrare computerizată pentru jante 13″–21″, pene reparate și testate sub presiune, jante îndreptate și verificate pe echilibrare. Afli prețul înainte să atingem roata.” Bullets: „Echilibrarea inclusă la montaj” · „Presiunea verificată gratuit — inclusiv rezerva” · „Anvelope noi și second-hand pe stoc”. CTAs: „Sună acum — 0744 398 932” (`data-track="call_hero"`), „Programează-te pe WhatsApp →” (`#programare`). Next line: „Ce urmează: ne spui ce ai → îți spunem prețul și ora → vii, se lucrează, pleci.” Season line `[data-season-message]`.
- [ ] **Step 2: 01 „Te recunoști?” (step 5).** Title „Roțile nu te anunță când cedează”. Four scene cards (prima ninsoare + cozi; vibrații în volan pe DN1C; pană dimineața înainte de serviciu; jantă îndoită de o groapă). Agitation paragraph (presiune mică = uzură mai rapidă + consum mai mare; anvelopă veche = aderență slabă pe frig). Bridge: „Există o cale mai simplă: o singură oprire la Gumirom.” + link to test.
- [ ] **Step 3: 02 Servicii (steps 6, 8).** Sticky column: „Metoda «Gata de drum» în 3 pași” (1. Ne spui ce ai — îți spunem prețul; 2. Vii la ora stabilită — lucrăm; 3. Verificăm tot — echilibrare, presiune inclusiv rezerva, valve — și pleci). Each service gets a `service__benefit` line starting „Ceea ce înseamnă…”.
- [ ] **Step 4: 03 Oferta (steps 10–14).** Title `Pachetul «<span data-season-name>Gata de Iarnă</span>»`. Stack list: „Montaj + echilibrare computerizată, 4 roți (autoturism, 13″–16″)” → `#offerPrice` (from `quote`); „Verificarea presiunii în toate roțile + rezerva — inclus”; „Verificarea valvelor — inclus”; „Roata testată înainte să pleci — inclus”; „Testul de 60 de secunde + memento de sezon — gratuit”. Price line „Total: 100 lei — 25 lei / roată, echilibrarea inclusă”. Rationale: „De ce totul inclus? Pentru că vrem să revii și sezonul următor — și să ne recomanzi.” Urgency `[data-season-message]`. CTAs call + WhatsApp booking.
- [ ] **Step 5: 04 Garanții (step 15).** Three named cards: «Prețul spus» (ce nu ți-am anunțat înainte nu plătești), «Roată verificată» (nicio roată nu pleacă netestată: sub presiune / pe echilibrare), «Fără vibrații» (reechilibrare gratuită 30 de zile — HTML comment `PROPUNERE — de confirmat cu proprietarul`). Reason line: „Putem promite asta pentru că verificăm fiecare roată pe echilibrare și sub presiune înainte să plece.”
- [ ] **Step 6: 11 Întrebări (objections).** `<details>` × 6: Cât costă exact? · Trebuie programare? · Lucrați pe SUV, 4×4 și utilitare? · Janta mea îndoită se mai poate repara? · Cum știu că anvelopele SH sunt bune? · Nu aveți dimensiunea mea pe stoc? — answers only from existing facts, each ending with a CTA link.
- [ ] **Step 7: CTA final (step 17).** Keep „Roata nu așteaptă. Nici noi.”; add warning „Dacă amâni, prima ninsoare te prinde la coadă — sau pe marginea drumului.” and a P.S. block restating pachetul sezonier + «Prețul spus» + sezon.
- [ ] **Step 8: Sticky mobile CTA.** `<div class="sticky-cta" id="stickyCta">` with „Sună” (tel) + „WhatsApp” (`waUrl` greeting); visible < 768 px after hero (IntersectionObserver on hero, class `is-visible`); `body` bottom padding on mobile so the footer is not covered.
- [ ] **Step 9: Nav + track.** Desktop nav: Oferta · Servicii · Tarife · Magazin · Recenzii · Contact; mobile menu adds Test anvelope, Programare; delegated click tracking: `a[href^="tel:"]` → `call_click`, `[data-track]` → its value.
- [ ] **Step 10: Verify.** `node --test tests/` PASS; browser: season name shows „Gata de Iarnă” (today 2026-10-06), offer price 100 lei, no overflow at 320/375/768/1280, no console errors.

---

### Task 4: Testul de 60 de secunde (HVCO)

**Files:**
- Create: `app/js/quiz.js`, `tests/quiz.test.js`
- Modify: `app/index.html` (section `#test`), `app/css/style.css`

**Interfaces:**
- Consumes: `Gumirom.season`, `nextSeasonStart`, `icsEvent`, `waUrl`, `track`.
- Produces: `Gumirom.quiz = { QUESTIONS, questionsFor(seasonKey), score(answers, questions), verdictText(verdict), resultMessage(result) }`; `score` returns `{ points, flagged: Question[], verdict: 'ok'|'check'|'urgent' }`.

- [ ] **Step 1: Failing tests**

```js
// tests/quiz.test.js
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { load } = require('./helpers/load');
const Q = () => load(['core.js', 'quiz.js']).Gumirom.quiz;

test('questionsFor: marcajul de iarnă doar în sezonul iarna', () => {
  const q = Q();
  assert.ok(q.questionsFor('iarna').some((x) => x.id === 'marcaj'));
  assert.ok(!q.questionsFor('drum').some((x) => x.id === 'marcaj'));
  assert.equal(q.questionsFor('drum').length, 5);
});
test('score: nimic bifat → ok', () => {
  const q = Q(); const r = q.score({}, q.questionsFor('iarna'));
  assert.equal(r.points, 0); assert.equal(r.verdict, 'ok'); assert.equal(r.flagged.length, 0);
});
test('score: doar vibrații → check', () => {
  const q = Q(); const r = q.score({ vibratii: true }, q.questionsFor('drum'));
  assert.equal(r.points, 2); assert.equal(r.verdict, 'check');
});
test('score: flanc (greutate 3) → urgent', () => {
  const q = Q(); assert.equal(q.score({ flanc: true }, q.questionsFor('drum')).verdict, 'urgent');
});
test('score: vibrații + aer = 4 → urgent', () => {
  const q = Q(); assert.equal(q.score({ vibratii: true, aer: true }, q.questionsFor('drum')).verdict, 'urgent');
});
test('score: răspunsurile false nu contează', () => {
  const q = Q(); assert.equal(q.score({ flanc: false, aer: false }, q.questionsFor('drum')).verdict, 'ok');
});
test('resultMessage: listează problemele bifate', () => {
  const q = Q(); const qs = q.questionsFor('drum');
  const msg = q.resultMessage(q.score({ vibratii: true, flanc: true }, qs));
  assert.ok(msg.includes('Vibrații în volan'));
  assert.ok(msg.includes('Umflături sau tăieturi pe flanc'));
  assert.ok(msg.startsWith('Bună ziua!'));
});
test('verdictText are titlu pentru fiecare verdict', () => {
  const q = Q(); ['ok', 'check', 'urgent'].forEach((v) => assert.ok(q.verdictText(v).title.length > 0));
});
```

- [ ] **Step 2: Run — expect FAIL** (`quiz.js` missing).
- [ ] **Step 3: Implement `quiz.js`.** QUESTIONS (id, short label, question, tip, weight, onlySeason): `profil` (3) „Profil la nivelul indicatorilor de uzură”; `varsta` (2) „Anvelope mai vechi de 6 ani”; `flanc` (3) „Umflături sau tăieturi pe flanc”; `vibratii` (2) „Vibrații în volan”; `aer` (2) „Pierde aer”; `marcaj` (2, onlySeason `'iarna'`) „Fără marcaj de iarnă (M+S / fulg)”. `score`: sum weights of `answers[id] === true`; `urgent` if any flagged weight ≥ 3 or points ≥ 4; `check` if points ≥ 1; else `ok`. `resultMessage`: `'Bună ziua! Am făcut testul de anvelope pe site.\n' + lines '- <label>' + '\nAș vrea o verificare. Când pot veni?'` (ok verdict → „Totul pare în regulă, aș vrea o verificare de sezon.”). DOM `initQuiz()`: renders questions as yes/no toggle buttons (`aria-pressed`), „Vezi rezultatul” shows verdict panel with flagged tips, WhatsApp link (`waUrl(resultMessage)`), „Memento în calendar” (Blob `.ics` download for `nextSeasonStart(today)`), disclaimer; tracks `quiz_complete`, `quiz_whatsapp`, `quiz_ics`.
- [ ] **Step 4: Run — expect PASS.**
- [ ] **Step 5: HTML/CSS** section `07 Testul de 60 de secunde` with title „Sunt anvelopele tale bune de drum?”, fascination bullets (codul DOT; indicatorii TWI; de ce vibrează volanul), `<noscript>` fallback text with phone.
- [ ] **Step 6: Browser verify** — answer flow works with keyboard, verdict renders, WhatsApp URL decodes correctly, `.ics` downloads (check Blob URL created; do not open external apps).

---

### Task 5: Shop logic + demo catalog

**Files:**
- Create: `app/js/stoc.js`, `app/js/shop.js` (logic part), `tests/shop.test.js`

**Interfaces:**
- Consumes: `Gumirom.CONFIG.prices.mount`, `rimClass`, `formatLei`.
- Produces: `Gumirom.shop = { DEFAULT_FILTER, normalize(list), filter(items, f), facets(items), cartAdd(cart, item, qty), cartSet(cart, id, qty, items), cartRemove(cart, id), cartReconcile(cart, items), totals(cart, items, opts), orderText(cart, items, opts, client), toQuery(f), fromQuery(search), sezonLabel(key) }`. Item: `{ id, stare, marca, model, latime, profil, janta, sezon, indice, pret, buc, dot, profilMm, note, marime }`. Cart: `[{ id: string, qty: number }]`. `totals → { linii: [{ id, item, qty, subtotal, montaj }], anvelope, montaj, total, buc }`.

- [ ] **Step 1: Failing tests**

```js
// tests/shop.test.js
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { load, plain } = require('./helpers/load');
const S = () => load(['core.js', 'shop.js']).Gumirom.shop;
const P = [
  { id: 'a', stare: 'sh', marca: 'Continental', model: 'WinterContact', latime: 205, profil: 55, janta: 16, sezon: 'iarna', indice: '91H', pret: 180, buc: 4, dot: 2022, profilMm: 6.5 },
  { id: 'b', stare: 'nou', marca: 'Michelin', model: 'Primacy 4', latime: 225, profil: 45, janta: 17, sezon: 'vara', indice: '94W', pret: 520, buc: 8 },
  { id: 'c', stare: 'nou', marca: 'Nokian', model: 'Seasonproof', latime: 205, profil: 55, janta: 16, sezon: 'all', indice: '94V', pret: 430, buc: 2 },
  { id: 'd', stare: 'sh', marca: 'Hankook', model: 'Kinergy', latime: 195, profil: 65, janta: 15, sezon: 'all', indice: '91H', pret: 120, buc: 2, dot: 2021, profilMm: 5 }
];

test('normalize: păstrează valide, adaugă marime, elimină invalide', () => {
  const s = S();
  const out = s.normalize(P.concat([
    { id: '', stare: 'nou', latime: 1, profil: 1, janta: 1, pret: 1, buc: 1 },
    { id: 'x', stare: 'sh', latime: 205, profil: 55, janta: 16, sezon: 'iarna', pret: 100, buc: 2 },      // SH fără profilMm
    { id: 'y', stare: 'nou', latime: 205, profil: 55, janta: 16, sezon: 'vara', pret: 'abc', buc: 2 },
    { id: 'z', stare: 'nou', latime: 205, profil: 55, janta: 16, sezon: 'vara', pret: 300, buc: 0 },
    null
  ]));
  assert.deepEqual(out.map((p) => p.id), ['a', 'b', 'c', 'd']);
  assert.equal(out[0].marime, '205/55 R16');
});
test('normalize: sezon lipsă → all; listă invalidă → []', () => {
  const s = S();
  assert.equal(s.normalize([{ id: 'q', stare: 'nou', latime: 205, profil: 55, janta: 16, pret: 300, buc: 1 }])[0].sezon, 'all');
  assert.deepEqual(plain(s.normalize(undefined)), []);
});
test('filter: stare, sezon (all intră la iarna/vara), dimensiune, căutare fără diacritice, sortare', () => {
  const s = S(); const items = s.normalize(P);
  const f = (o) => s.filter(items, Object.assign({}, s.DEFAULT_FILTER, o)).map((p) => p.id);
  assert.deepEqual(f({ stare: 'sh' }), ['d', 'a']);
  assert.deepEqual(f({ sezon: 'iarna' }), ['d', 'a', 'c']);
  assert.deepEqual(f({ latime: 205, profil: 55, janta: 16 }), ['a', 'c']);
  assert.deepEqual(f({ q: 'michélin' }), ['b']);
  assert.deepEqual(f({ q: '205/55' }), ['a', 'c']);
  assert.deepEqual(f({ sort: 'pret-desc' }), ['b', 'c', 'a', 'd']);
  assert.deepEqual(f({ stare: 'sh', sort: 'profil-desc' }), ['a', 'd']);
});
test('facets: valori unice sortate', () => {
  const s = S();
  assert.deepEqual(plain(s.facets(s.normalize(P))), { latime: [195, 205, 225], profil: [45, 55, 65], janta: [15, 16, 17] });
});
test('coș: adăugare cu limită de stoc și combinare', () => {
  const s = S(); const items = s.normalize(P);
  let cart = s.cartAdd([], items[2], 1);
  cart = s.cartAdd(cart, items[2], 5);
  assert.deepEqual(plain(cart), [{ id: 'c', qty: 2 }]);
  cart = s.cartSet(cart, 'c', 0, items);
  assert.deepEqual(plain(cart), []);
});
test('cartReconcile: scoate produse dispărute, limitează la stoc, ignoră intrări stricate', () => {
  const s = S(); const items = s.normalize(P);
  const out = s.cartReconcile([{ id: 'a', qty: 9 }, { id: 'gone', qty: 1 }, { id: 'b', qty: -1 }, { foo: 1 }, null], items);
  assert.deepEqual(plain(out), [{ id: 'a', qty: 4 }]);
});
test('totals: cu montaj (tarif după jantă) și fără', () => {
  const s = S(); const items = s.normalize(P);
  const cart = [{ id: 'a', qty: 4 }, { id: 'b', qty: 2 }];
  const t = s.totals(cart, items, { montaj: true });
  assert.equal(t.anvelope, 4 * 180 + 2 * 520);
  assert.equal(t.montaj, 4 * 25 + 2 * 30);
  assert.equal(t.total, t.anvelope + t.montaj);
  assert.equal(t.buc, 6);
  assert.equal(s.totals(cart, items, { montaj: false }).montaj, 0);
});
test('orderText: conține linii, total, client; caractere speciale intacte', () => {
  const s = S(); const items = s.normalize(P);
  const txt = s.orderText([{ id: 'a', qty: 4 }], items, { montaj: true }, { nume: 'Ștefan & Co', telefon: '0744 000 000', zi: '12.10.2026', detalii: 'Dacia #1' });
  assert.ok(txt.includes('4 × Continental WinterContact 205/55 R16 91H (SH)'));
  assert.ok(txt.includes('Montaj + echilibrare'));
  assert.ok(txt.includes('Total estimat: 820 lei'));
  assert.ok(txt.includes('Ștefan & Co'));
  assert.ok(txt.includes('Dacia #1'));
});
test('query: dus-întors, implicitele lipsesc din URL', () => {
  const s = S();
  assert.equal(s.toQuery(s.DEFAULT_FILTER), '');
  const f = Object.assign({}, s.DEFAULT_FILTER, { stare: 'sh', janta: 16, sezon: 'iarna' });
  const q = s.toQuery(f);
  assert.deepEqual(plain(s.fromQuery(q)), plain(f));
  assert.deepEqual(plain(s.fromQuery('?stare=hack&janta=abc')), plain(s.DEFAULT_FILTER));
});
```

- [ ] **Step 2: Run — expect FAIL.**
- [ ] **Step 3: Implement.** `DEFAULT_FILTER = { stare: 'toate', sezon: 'toate', latime: '', profil: '', janta: '', q: '', sort: 'pret-asc' }`. `normalize`: drop non-objects, empty `id`, `stare ∉ {nou, sh}`, non-finite/≤0 `latime/profil/janta/pret`, `buc < 1` (integer floor), SH without finite `profilMm`; `sezon` default `'all'`; `marime = latime + '/' + profil + ' R' + janta`. Search fold: `toLowerCase()` + `normalize('NFD').replace(/[\u0300-\u036f]/g,'')`, haystack `marca model marime indice`. Sort: `pret-asc` ties by id; `profil-desc` by `profilMm` (nou = Infinity? no — nou treated as -1 so SH first). `totals` montaj = `CONFIG.prices.mount[rimClass(janta)] * qty` when `opts.montaj`. `orderText` format:

```
Comandă anvelope — Gumirom Gherla
————————————
4 × Continental WinterContact 205/55 R16 91H (SH) — 720 lei
Montaj + echilibrare: 100 lei
Total estimat: 820 lei
————————————
Nume: …
Telefon: …
Zi preferată pentru montaj: …
Detalii: …
Aștept confirmarea disponibilității. Plata la atelier.
```
`fromQuery`: whitelist values (`stare ∈ toate|nou|sh`, `sezon ∈ toate|vara|iarna|all`, sort whitelist, numbers via `parseInt` → '' when NaN, `q` trimmed ≤ 40 chars).
- [ ] **Step 4: `stoc.js` demo catalog** — `window.GUMIROM_STOC = { demo: true, actualizat: '2026-10-06', produse: [...] }` with ~14 items covering 14″–18″, new + SH, all three seasons; SH with `profilMm` and `dot`; header comment explaining every field in Romanian for the owner.
- [ ] **Step 5: Run — expect PASS.**

---

### Task 6: Shop UI (`magazin.html`) + index teaser

**Files:**
- Create: `app/magazin.html`, `app/css/shop.css`
- Modify: `app/js/shop.js` (DOM part), `app/index.html` (section 06 + nav), `app/css/style.css` (teaser)

**Interfaces:**
- Consumes: everything in `Gumirom.shop`, `GUMIROM_STOC`, `Gumirom.waUrl`, `storage`, `track`, `formatLei`, `CONFIG`.
- Produces DOM ids: `#shop`, `#shopFilters`, `#shopGrid`, `#shopCount`, `#shopEmpty`, `#cartToggle`, `#cartCount`, `#cart` (dialog), `#cartLines`, `#cartMontaj`, `#cartTotals`, `#checkoutForm`, `#checkoutNote`, `#demoBanner`; teaser: `#stocTeaser`, `#stocCountNou`, `#stocCountSh`, `#stocFeatured`.

- [ ] **Step 1: `magazin.html`** — same head pattern (js class, fonts, `style.css` + `shop.css`), shared header/menu/footer (links to `index.html#…`), hero: kicker „Magazin anvelope · Gherla”, H1 „Anvelope verificate. Montaj inclus, dacă vrei.”, subtitle on SH honesty (profil măsurat, DOT afișat) + «Prețul spus»; demo banner; filters form; results grid; empty state (WhatsApp „căutați dimensiunea X”); „Unde găsesc dimensiunea?” `<details>`; cart drawer; `<noscript>` with phone; scripts `core.js`, `stoc.js`, `main.js`, `shop.js`.
- [ ] **Step 2: Product card renderer** — SVG tyre (concentric circles + tread dashes) with `marime` text; badges (Nou/SH, sezon); title; `indice`; SH line „Profil 6,5 mm · DOT 2022 · măsurat de noi”; price per piece + „Set de 4: X lei” when `buc ≥ 4`; real-stock scarcity „Ultimele N bucăți” when `buc ≤ 2`; qty select 1…`buc` (default `min(4, buc)`); „Adaugă în coș” → toast/aria-live + cart badge bump; text via `textContent` only (no `innerHTML` with data).
- [ ] **Step 3: Filters** — selects from `facets`, state ↔ URL with `history.replaceState(toQuery)`, reset button, result count „N anvelope”.
- [ ] **Step 4: Cart drawer** — `role="dialog" aria-modal="true" aria-labelledby`, opens from header button, Esc/overlay closes, focus moves into drawer and returns to opener; lines with qty select + remove; checkbox „Cu montaj + echilibrare la Gumirom (tarif autoturism)” default checked; bonuses list (presiune + valve incluse; «Prețul spus»); totals; checkout form (nume, telefon, zi preferată dacă montaj — closed days rejected via `isClosedDate`, detalii) → `waUrl(orderText)` + fallback link + „Comandă telefonic”; cart persisted via `storage` key `gumirom.cart.v1` and reconciled on load; track `add_to_cart`, `order_whatsapp`.
- [ ] **Step 5: Index teaser (06 Magazin)** — counts of new/SH from normalized stock, 3 featured (SH sorted by `profilMm` desc), links `magazin.html?stare=sh&sezon=iarna` etc., demo note when `demo`.
- [ ] **Step 6: Verify** — `node --test tests/` PASS; browser at 375 and 1280: filter by SH, add to cart, reload (persists), toggle montaj (total changes), checkout validation, generated WhatsApp URL decodes to `orderText`; no console errors; no overflow at 320.

---

### Task 7: Full verification + documentation

**Files:**
- Modify: `docs/audit-sell-like-crazy-hormozi.md` (append re-score after implementation), `app/index.html` header comment (new files).

- [ ] **Step 1:** `node --test tests/` → all PASS (paste counts).
- [ ] **Step 2:** `node --check` on every `app/js/*.js`.
- [ ] **Step 3:** Browser matrix: index + magazin at 320 / 375 / 768 / 1280 → overflow probe `[]`, console errors none.
- [ ] **Step 4:** JS disabled check (fetch HTML and confirm no `.js`-gated hiding without class: load page in iframe with `sandbox` without `allow-scripts`, confirm `[data-reveal]` computed opacity is 1 and preloader not displayed).
- [ ] **Step 5:** Re-run `extract_page.py` on the new `index.html`; re-score honestly; list remaining owner inputs (ani de activitate, recenzii citate, plăți, livrare, date firmă, confirmare «Fără vibrații», stoc real).
