Vista previa en vídeo
GratisHeroComercio electrónico

Pasión por el curry

El prompt
ID: anna-marta-sauce-product-hero
Category: Food & Beverage / Sauce Product Page / Rotating Multi-Variant Hero
Type: hero
Layout Mode: fullscreen-hero

Create a single-file HTML+CSS+vanilla-JS hero section with the following exact specifications:

## Tech Stack

* Deliverable filename: `anna-marta-product-hero.html`.
* Plain HTML + CSS + vanilla JS. No frameworks, no build step.
* Fonts — literal copy-pasteable block:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Poppins:wght@800;900&display=swap" rel="stylesheet">
```

`Inter` (400/500/700) is the body/UI typeface — nav links, description copy, tag labels, nutrition-card labels/values, "1 pc" label, "To cart" label. `Poppins` (800/900, only 900 actually used) is the logo wordmark only ("AM").

* Global reset — literal CSS:

```css
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body { display: flex; align-items: center; justify-content: center; font-family: 'Inter', -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; }
button { border: none; background: none; padding: 0; margin: 0; font: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }
a { cursor: pointer; }
.abs { position: absolute; }
```

## Canvas & Scaling

**This page uses the documented fixed-canvas exception, not the default edge-anchored approach.** State this explicitly at the top of the build: the design is a single dense composition — dozens of individually cropped/positioned decorative elements (rotated swirl line-art with per-item x/y/rotation, percentage-cropped jar photography whose crop values are hand-tuned per image, nutrition cards, tag icons) whose relative geometry would break under simple edge-anchoring. So it uses a fixed 1420×805 design canvas scaled uniformly to fit the viewport.

```css
html, body { background: #d8575a; }
#viewport {
  width: 100vw; height: 100vh;
  display: flex; align-items: center; justify-content: center;
  background: #d8575a;
  overflow: hidden;
  transition: background-color 1.2s ease;
}
#stage {
  position: relative;
  width: 1420px; height: 805px;
  background: #ffffff;
  overflow: hidden;
  transform-origin: center center;
  flex-shrink: 0;
  --accent: #4f1c1c;
}
#stage img { display: block; max-width: none; }
```

```js
var stage = document.getElementById('stage');
var viewportEl = document.getElementById('viewport');
function fitStage() {
  var sx = window.innerWidth / 1420;
  var sy = window.innerHeight / 805;
  var scale = Math.min(sx, sy); // "contain" — never crops the design
  stage.style.transform = 'scale(' + scale + ')';
}
window.addEventListener('resize', fitStage);
fitStage();
```

`Math.min` (contain) is mandatory, not `Math.max` (cover). Do NOT switch this to `Math.max`/cover — on any viewport aspect ratio narrower or wider than 1420:805 that crops real artwork (most visibly the top of the hero jar's lid/bow) instead of leaving a margin.

Because contain-fit leaves a margin outside `#stage` on off-ratio viewports, that margin is not a neutral bar: `#viewport`'s `background-color` is set in JS to the active slide's own `pageBg` value (see Design Tokens) on load and on every slide change, with a 1.2s CSS transition, so the margin always blends with the current slide's palette instead of reading as an empty letterbox.

## Design Tokens

```css
:root { /* no global :root tokens — theming lives on #stage, see below */ }
#stage { --accent: #4f1c1c; } /* overwritten per slide via JS, see Interactive Behavior */
```

`--accent` is the single theming hook for every fixed (non-rotating) control — the quantity stepper buttons, the "To cart" button's hover background, and its icon-circle fills all read `var(--accent)` rather than a hardcoded color. Fragile coupling: any new themed control must reference `var(--accent)` — a hardcoded color on a themed element will not retint when the slide changes.

Per-slide theme values (`themeColor` drives `--accent`, `pageBg` drives the viewport margin color):

| Slide | themeColor | pageBg |
|---|---|---|
| Curry | `#4f1c1c` | `#d8575a` |
| Pesto | `#1c4f1e` | `#8ac5a4` |
| Tartar | `#4f2d1c` | `#e6cebf` |

Media asset tokens — every image URL used on the page, centralized in one object. All URLs below are already-migrated permanent hosts (promptsites.net), never Figma URLs:

```js
const MEDIA = {
  iconSearch:  'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/607d3ac4-518b-439d-897a-9a1dd90553e7.png',
  iconCartTop: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/d4b35a4d-6ca6-4226-806e-46144f049f9d.png',
  navChevron:  'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/7421d06f-2b14-43d3-8f76-8532009914a4.png',
  navArrow:    'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/831a606b-a4b4-47dc-9657-d933bf929987.png',

  bgCurry:  'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/ede6ae3f-688b-4394-987f-366396759928.jpg',
  jarCurry: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/3fca8276-240f-47c6-8c20-1babbef2faa6.webp',
  tagCurrySpicy:      'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/fedb3090-9983-438d-83be-7626edc94381.png',
  tagCurryGlutenFree: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/d273090f-6916-49ea-9a05-a0a736a44c5b.png',
  tagCurryVegan:      'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/a6f55213-f27c-4e12-88b9-93d70dbb89ec.png',

  bgPesto:  'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/120609c4-184d-46c6-9924-b65a6c700148.jpg',
  jarPesto: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/5a8e5e4c-0ebe-4eac-ab43-55780f080910.webp',
  tagPestoGlutenFree: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/acb92e8a-df8b-434c-9f40-ef971956afb1.png',
  tagPestoVegan:      'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/629150ff-9ce7-45f6-b1ad-fae330b05fc3.png',

  bgTartar:  'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/7a871bf4-b00f-41b2-bcb7-2ef18ffd6c53.jpg',
  jarTartar: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/25b0c57f-2fff-4372-8314-fa92c3af5505.webp',
  tagTartarGlutenFree: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/466672b0-b9ea-4ec3-950c-4dfa0815d8b5.png',
  tagTartarSeafood:    'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/b8bb187b-34de-48c2-874e-6d0f3704bbfb.png',
  tagTartarCreamy:     'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/a540b0a7-f1fb-477d-bbe5-f27af7490438.png',

  swirl0: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/7e64dc09-13f4-4528-a51a-8b3127be345d.png',
  swirl1: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/e2d4af47-aed3-45eb-9808-9046f12b985b.png',
  swirl2: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/e8ee4219-68c1-4943-a7d7-f7da85150bed.png',
  swirl3: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/9e90c82d-daef-4a21-b8f2-ceb5c726b16c.png',
  swirl4: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/bae97cf0-c088-4de4-a6df-7dfe1ac5eacc.png',
  swirl5: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/e1331679-4da5-41b5-a46f-705c9698ef25.png',
  swirl6: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/98a7c25b-b1bf-4db7-94af-5a2a604908b6.png',
  rightAccentSwirl: 'https://api.promptsites.net/s3/promptsites/previews/f1b32048-477d-4473-9270-fcaea851ddc1/983b9892-0cf5-42b6-b3a8-cc0bc707ff25.png'
};
```

The original vector icon/decoration sources were `.svg`; the media host used for this build (promptsites.net) rejects `SVG` uploads (magic-byte type check), so every icon/decoration was rasterized to PNG before hosting. `MEDIA` therefore points at PNG rasters for `iconSearch`, `iconCartTop`, `navChevron`, `navArrow`, all `tag*` icons and all `swirl*`/`rightAccentSwirl` entries — this is intentional, not a placeholder.

State plainly: copy text (titles, descriptions, tag labels, nutrition labels/values, nav links, button label) is NOT tokenized — it stays literal at its point of use in the per-slide data below.

## Elements

Elements 1–7 are the **static chrome** — always present, never re-rendered, retinted only via `var(--accent)`. Elements 8 is the **rotating content template** — its markup is generated per-slide from data by `renderSlide()` (see Interactive Behavior) and swapped inside two crossfading layers.

**1. `#logo`** — text **"AM"**.
```css
#logo { left: 65px; top: 50px; width: 53px; text-align: center; font-family: 'Poppins', sans-serif; font-weight: 900; font-size: 40px; line-height: 0.96; letter-spacing: -0.4px; color: #fff; white-space: nowrap; z-index: 5; }
```

**2. `<nav>`** — five links, each an `<a href="#" class="navlink">`: **"Products"** (left 441px, weight 700 — active state), **"Recipes"** (541px, weight 500), **"Adventure Trip"** (634px, weight 500), **"Food Training"** (774px, weight 500), **"Anna Marta"** (907px, weight 500).
```css
nav a.navlink { position: absolute; top: 58px; font-size: 14px; line-height: 1.6; letter-spacing: -0.14px; color: #fff; white-space: nowrap; text-decoration: none; z-index: 5; }
```

**3. `#icon-search`** / **`#icon-cart-top`** — header icons, `data-media="iconSearch"` / `data-media="iconCartTop"`.
```css
#icon-search { left: 1304px; top: 61px; width: 16px; height: 16px; z-index: 5; }
#icon-cart-top { left: 1340px; top: 61px; width: 17px; height: 17px; z-index: 5; }
```

**4. Quantity stepper** — `#qty-minus`, `#qty-label` (text **"1 pc"**), `#qty-plus`.
```css
.qty-btn { position: absolute; top: 697px; width: 60px; height: 60px; border-radius: 50%; background: var(--accent); z-index: 5; transition: transform .25s ease, box-shadow .25s ease, opacity .25s ease, background-color .8s ease; }
.qty-btn .bar { position: absolute; left: 50%; top: 50%; background: #fff; border-radius: 3px; transform: translate(-50%, -50%); }
#qty-minus { left: 525px; opacity: 0.3; }
#qty-minus .bar { width: 18px; height: 3px; }
#qty-plus { left: 644px; width: 59px; }
#qty-plus .bar-h { position: absolute; left: 50%; top: 50%; width: 18px; height: 3px; background: #fff; border-radius: 3px; transform: translate(-50%, -50%); }
#qty-plus .bar-v { position: absolute; left: 50%; top: 50%; width: 3px; height: 18px; background: #fff; border-radius: 3px; transform: translate(-50%, -50%); }
#qty-label { left: 614.5px; top: 719px; transform: translateX(-50%); font-family: 'Inter', sans-serif; font-weight: 500; font-size: 16px; letter-spacing: -0.32px; color: #fff; white-space: nowrap; z-index: 5; }
```
Hover deltas: `#qty-minus:hover { opacity: 0.45; transform: scale(1.06); }` · `#qty-plus:hover { transform: scale(1.06); box-shadow: 0 6px 16px rgba(0,0,0,0.35); }`

**5. `#to-cart`** — button, text **"To cart"**, plus a 56×56 icon-stack with two stacked circles (default: filled `--accent` with a white basket glyph; hover: filled white with an `--accent`-colored basket glyph).
```css
#to-cart { left: 719px; top: 697px; height: 60px; background: #fff; border-radius: 200px; display: flex; align-items: center; flex-direction: row; padding: 0 2px 0 38px; gap: 27px; z-index: 5; transition: background-color .5s ease, padding .35s ease, box-shadow .35s ease; }
#to-cart .label { font-family: 'Inter', sans-serif; font-weight: 400; font-size: 16px; line-height: 0.96; letter-spacing: -0.16px; color: #9f6162; white-space: nowrap; transition: color .35s ease; }
#to-cart .icon-stack { position: relative; width: 56px; height: 56px; flex-shrink: 0; }
#to-cart .icon-circle { position: absolute; inset: 0; width: 56px; height: 56px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: opacity .35s ease, background-color .5s ease; }
#to-cart .icon-circle.icon-default { background: var(--accent); }
#to-cart .icon-circle.icon-hover { background: #fff; opacity: 0; }
#to-cart .icon-circle.icon-hover svg path { fill: var(--accent); transition: fill .5s ease; }
```
Both circles hold the identical inline basket-glyph `<svg width="56" height="56" viewBox="0 0 56 56">` path (`d="M24.1686 19.9524C24.4346 19.3905 25.0084 19 25.6715 19H29.4714C30.1345 19 30.7073 19.3905 30.9743 19.9524C31.6231 19.9581 32.1294 19.9876 32.5816 20.1648C33.1214 20.3764 33.5909 20.7362 33.9363 21.2029C34.2849 21.6733 34.4493 22.2762 34.6735 23.1057L35.3784 25.6981L35.6444 26.499L35.6672 26.5276C36.5231 27.6267 36.1156 29.261 35.3005 32.5286C34.7818 34.6076 34.5234 35.6467 33.7501 36.2524C32.9768 36.8571 31.9081 36.8571 29.7706 36.8571H25.3722C23.2348 36.8571 22.166 36.8571 21.3928 36.2524C20.6195 35.6467 20.3601 34.6076 19.8424 32.5286C19.0273 29.261 18.6198 27.6267 19.4757 26.5276L19.4985 26.499L19.7645 25.6981L20.4694 23.1057C20.6945 22.2762 20.8589 21.6724 21.2066 21.2019C21.5521 20.7356 22.0215 20.3762 22.5612 20.1648C23.0134 19.9876 23.5188 19.9571 24.1686 19.9524ZM24.1705 21.3838C23.5416 21.3905 23.2889 21.4143 23.0799 21.4962C22.7892 21.6102 22.5364 21.8039 22.3503 22.0552C22.1831 22.281 22.0843 22.5962 21.8088 23.6124L21.2674 25.6019C22.2363 25.4286 23.5606 25.4286 25.3713 25.4286H29.7706C31.5822 25.4286 32.9056 25.4286 33.8746 25.6L33.334 23.6105C33.0585 22.5943 32.9597 22.279 32.7925 22.0533C32.6065 21.802 32.3537 21.6083 32.0629 21.4943C31.8539 21.4124 31.6012 21.3886 30.9724 21.3819C30.8375 21.6665 30.6249 21.907 30.3592 22.0754C30.0935 22.2438 29.7857 22.3332 29.4714 22.3333H25.6715C25.3573 22.3333 25.0495 22.244 24.7838 22.0758C24.5182 21.9075 24.3055 21.6682 24.1705 21.3838Z"`), default circle's path has literal `fill="#ffffff"`, hover circle's path has no fill attribute (colored by the CSS rule above).

Do NOT rasterize this icon as an `<img>` — it must stay an inline `<svg>` so its fill can be driven by `var(--accent)`.

Hover deltas: `#to-cart:hover { background: var(--accent); padding: 0 38px 0 2px; flex-direction: row-reverse; box-shadow: 0 10px 24px rgba(0,0,0,0.35); }` · `#to-cart:hover .label { color: #fff; }` · `#to-cart:hover .icon-circle.icon-default { opacity: 0; }` · `#to-cart:hover .icon-circle.icon-hover { opacity: 1; }`. `flex-direction: row-reverse` on hover visually swaps icon/label order while DOM order and the padding declarations stay unchanged — do NOT reorder the markup to achieve the same effect.

Fragile coupling: the icon-stack is 56×56px and the pill's own height is 60px with 0px vertical padding — the 2px top/bottom gap around the icon is produced by `align-items:center` centering a 56px child in a 60px flex row, not by explicit padding. Do NOT add vertical padding here or the gap doubles.

**6. Left widget (previous-product control)** — two decorative chevrons, a white disc + clipped photo circle, the nav button, its arrow icon.
```css
#chev-l-1 { left: 83px; top: 668px; width: 45px; height: 42px; transform: scaleY(-1) rotate(180deg); }
#chev-l-2 { left: 83px; top: 684px; width: 45px; height: 42px; transform: rotate(180deg); }
#thumb-l-disc { left: 85px; top: 619px; width: 158px; height: 158px; border-radius: 50%; background: #fff; }
#thumb-l-clip { left: 84px; top: 619px; width: 160px; height: 158px; border-radius: 79px; overflow: hidden; }
#nav-l-circle { left: 62px; top: 673px; width: 48px; height: 48px; border-radius: 50%; background: #fff; z-index: 5; transition: transform .25s ease, box-shadow .25s ease; cursor: pointer; }
#nav-l-arrow { left: 73px; top: 684px; width: 26px; height: 26px; transform: scaleX(-1); z-index: 5; pointer-events: none; }
.thumb-fill { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
```
`#chev-l-1`/`#chev-l-2` both use `data-media="navChevron"`; `#nav-l-arrow` uses `data-media="navArrow"` mirrored via `transform: scaleX(-1)`. `#thumb-l-clip` contains one empty child `<div id="thumb-l-slot">`, refreshed on every navigation with `<img class="thumb-fill" src="{jar of the previous slide}">` — see Interactive Behavior. Hover delta: `#nav-l-circle:hover { transform: scale(1.08); box-shadow: 0 8px 20px rgba(0,0,0,0.25); }`.

**7. Right widget (next-product control)** — two decorative chevrons, a stacked pair of circles (back = plain depth card, front = clipped photo circle), a static accent decoration, the nav button, its arrow icon.
```css
#chev-r-1 { left: 1288px; top: 663px; width: 45px; height: 42px; }
#chev-r-2 { left: 1288px; top: 679px; width: 45px; height: 42px; transform: scaleY(-1); }
#thumb-r-back { left: 1203px; top: 582px; width: 127px; height: 131px; border-radius: 50%; overflow: hidden; background: #fff; }
#thumb-r-disc { left: 1174px; top: 614px; width: 158px; height: 158px; border-radius: 50%; background: #fff; }
#thumb-r-front { left: 1174px; top: 614px; width: 158px; height: 158px; border-radius: 50%; overflow: hidden; }
#thumb-r-accent { left: 1185px; top: 719px; width: 47px; height: 36px; }
#nav-r-circle { left: 1306px; top: 668px; width: 48px; height: 48px; border-radius: 50%; background: #fff; z-index: 5; transition: transform .25s ease, box-shadow .25s ease; cursor: pointer; }
#nav-r-arrow { left: 1317px; top: 679px; width: 26px; height: 26px; z-index: 5; pointer-events: none; }
```
`#thumb-r-accent` uses `data-media="rightAccentSwirl"`. `#thumb-r-front` contains one empty child `<div id="thumb-r-front-slot">`, refreshed on navigation with `<img class="thumb-fill" src="{jar of the next slide}">`.

Do NOT put a second jar photo inside `#thumb-r-back`. `#thumb-r-back` (127×131, offset up-left) is only partially covered by `#thumb-r-front` (158×158) — a `cover`-fit photo placed inside it pokes out above the front circle's edge and reads as a cropping bug. `#thumb-r-back` stays a plain `background:#fff` card — visual depth only, no image content. Its `#thumb-r-back-slot` child div exists in markup but is intentionally left empty.

Both `#thumb-r-back` and `#thumb-r-front` additionally receive decorative swirl-line-art overlays, appended once on page load (not per navigation) as absolutely-positioned `<img class="swirl">` children, each `data-media="swirl{0-6}"`:
```css
.swirl { position: absolute; opacity: 0.4; }
```
```js
function addSwirls(container, items) {
  items.forEach(function (it) {
    var img = document.createElement('img');
    img.className = 'swirl';
    img.src = MEDIA['swirl' + it.a];
    img.style.left = it.x + 'px'; img.style.top = it.y + 'px';
    img.style.width = it.w + 'px'; img.style.height = it.h + 'px';
    if (it.r) img.style.transform = 'rotate(' + it.r + 'deg)';
    container.appendChild(img);
  });
}
addSwirls(document.getElementById('thumb-r-back'), [
  { a: 0, x: -15.66, y: 163.55, w: 52.44, h: 14.85 },
  { a: 1, x: -27.54, y: 170.91, w: 10.66, h: 15.73, r: 72.02 },
  { a: 2, x: -20.58, y: 172.68, w: 12.01, h: 15.83, r: 65.02 },
  { a: 3, x: -36.42, y: 175.09, w: 10.07, h: 14.14, r: 101.58 },
  { a: 4, x: 106.88, y: 176.22, w: 12.29, h: 9.46, r: 1.71 },
  { a: 5, x: 96.71, y: 149.80, w: 58.27, h: 16.49 },
  { a: 0, x: -37.79, y: 149.72, w: 52.44, h: 14.85 },
  { a: 6, x: 2.60, y: 155.50, w: 107.66, h: 14.94 },
  { a: 6, x: -29.89, y: 152.73, w: 107.66, h: 14.94 }
]);
addSwirls(document.getElementById('thumb-r-front'), [
  { a: 0, x: 13.34, y: 131.55, w: 52.44, h: 14.85 },
  { a: 0, x: 13.34, y: 131.55, w: 52.44, h: 14.85 },
  { a: 1, x: -4.76, y: 138.91, w: 10.66, h: 15.73, r: 72.02 },
  { a: 2, x: 8.42, y: 140.68, w: 12.01, h: 15.83, r: 65.02 },
  { a: 3, x: -17.49, y: 143.09, w: 10.07, h: 14.14, r: 101.58 },
  { a: 4, x: 135.88, y: 144.22, w: 12.29, h: 9.46, r: 1.71 },
  { a: 5, x: 125.71, y: 117.80, w: 58.27, h: 16.49 },
  { a: 0, x: -8.79, y: 117.72, w: 52.44, h: 14.85 },
  { a: 6, x: 31.60, y: 123.50, w: 107.66, h: 14.94 },
  { a: 6, x: -0.89, y: 120.73, w: 107.66, h: 14.94 }
]);
```
Coordinates are relative to each container's own top-left corner (a container-local coordinate space, not the `#stage` origin) — do NOT reinterpret `x`/`y` as `#stage`-relative or the swirls scatter off-position. Hover delta: `#nav-r-circle:hover { transform: scale(1.08); box-shadow: 0 8px 20px rgba(0,0,0,0.25); }`.

**8. Rotating content template** — two full-bleed layers that crossfade between three slides of data. Markup and per-slide data below.
```css
.content-layer { position: absolute; left: 0; top: 0; width: 1420px; height: 805px; opacity: 0; transition: opacity 1.2s ease; pointer-events: none; }
.content-layer.active { opacity: 1; pointer-events: auto; }
.bg-photo { left: 0; top: 0; width: 1420px; height: 805px; overflow: hidden; pointer-events: none; }
.bg-photo img { width: 100%; height: 100%; object-fit: cover; }
.hero-jar { top: 92px; height: 562px; transform: translateX(-50%); overflow: visible; pointer-events: none; }
.hero-jar img { position: absolute; }
.nutri-card { position: absolute; border-radius: 20px; }
.nutri-card .label { position: absolute; left: 15px; top: 20px; font-family: 'Inter', sans-serif; font-weight: 500; font-size: 16px; letter-spacing: -0.32px; line-height: 16px; }
.nutri-card .value { position: absolute; left: 15px; top: 58px; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 22px; letter-spacing: -0.44px; line-height: 26px; }
.product-title { left: 64px; top: 148px; width: 460px; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 64px; line-height: 68px; letter-spacing: -1.28px; white-space: nowrap; }
.product-title .light { font-weight: 400; }
.product-desc { left: 64px; top: 236px; width: 300px; font-family: 'Inter', sans-serif; font-weight: 500; font-size: 16px; line-height: 1.7; letter-spacing: -0.16px; }
.tag-icon { position: absolute; width: 35px; height: 35px; }
.tag-label { position: absolute; font-family: 'Inter', sans-serif; font-weight: 500; font-size: 16px; line-height: 1.7; letter-spacing: -0.16px; white-space: nowrap; }
```
`<div class="content-layer" id="layer-a">` / `<div class="content-layer" id="layer-b">` sit as the **first two children of `#stage`**, before every static element above. Do NOT give `.content-layer` an explicit `z-index`. It must stay in the base (`auto`) stacking order — an explicit `z-index` here paints the opaque full-bleed background photo over the carousel thumbnail circles in elements 6–7 regardless of DOM order, since a positive `z-index` sibling always paints above an `auto` sibling.

Do NOT set `overflow: hidden` tightly on `.hero-jar`. It is `overflow: visible` with a generously sized box (562px tall, positioned at `top:92px`) that the jar artwork is allowed to bleed past on every side — a tight `overflow:hidden` here clips the jar's lid/bow at the top and its garnish (chili, peppercorns) at the bottom. Fragile coupling: each slide's `jarImg` crop percentages below are hand-tuned per photo; rely on those per-slide numbers to frame each jar correctly rather than clipping the box.

Slide data (verbatim copy text, exact positions, all three slides):

```js
const slides = [
  {
    bg: MEDIA.bgCurry, jar: MEDIA.jarCurry,
    jarBox: { left: 'calc(50% + 3px)', width: 624 },
    jarImg: { left: '-7.89%', top: '-6.76%', width: '112.4%', height: '124.2%' },
    textColor: '#ffffff',
    title: 'Curry', accent: '(400g)', accentColor: '#f5b5b6',
    desc: 'Curry sauce is a warm and flavorful blend that brings a world of spices to your plate. With roots in South Asian cuisine, it combines a medley of ingredients like turmeric, cumin, coriander, garlic, ginger, and tomatoes. Bold spices. Global flavor.',
    tagColor: '#4f1c1c',
    tags: [
      { left: 65,  top: 455, icon: MEDIA.tagCurrySpicy,      label: 'Spicy',       labelLeft: 106, labelTop: 460 },
      { left: 162, top: 455, icon: MEDIA.tagCurryGlutenFree, label: 'Gluten-free', labelLeft: 202, labelTop: 459 },
      { left: 65,  top: 495, icon: MEDIA.tagCurryVegan,      label: 'Vegan',       labelLeft: 105, labelTop: 500 }
    ],
    cards: [
      { left: 1232, top: 366, width: 125, height: 125, border: '#ffffff', textColor: '#ffffff', label: 'Energy',  value: '1600 kJ<br>382 kcal' },
      { left: 1232, top: 148, width: 125, height: 99,  border: '#ffffff', textColor: '#ffffff', label: 'Fat',     value: '40.0 g' },
      { left: 1107, top: 257, width: 125, height: 99,  bg: '#4f1c1c',     textColor: '#ffffff', label: 'Protein', value: '5.0 g' }
    ],
    themeColor: '#4f1c1c', pageBg: '#d8575a'
  },
  {
    bg: MEDIA.bgPesto, jar: MEDIA.jarPesto,
    jarBox: { left: '50%', width: 634 },
    jarImg: { left: '-4.1%', top: '-9%', width: '104.1%', height: '124.53%' },
    textColor: '#ffffff',
    title: 'Pesto', accent: '(400g)', accentColor: '#bdebd2',
    desc: 'Pesto sauce is an aromatic and herbaceous delight that transforms any dish into a culinary masterpiece. Hailing from Italy, this vibrant green sauce combines fresh basil leaves, pine nuts, garlic, Parmesan cheese, and olive oil for a rich and earthy flavor.',
    tagColor: '#1c4f1e',
    tags: [
      { left: 65, top: 455, icon: MEDIA.tagPestoGlutenFree, label: 'Gluten-free', labelLeft: 106, labelTop: 460 },
      { left: 65, top: 495, icon: MEDIA.tagPestoVegan,      label: 'Vegan',       labelLeft: 105, labelTop: 500 }
    ],
    cards: [
      { left: 1232, top: 366, width: 125, height: 125, border: '#ffffff', textColor: '#ffffff', label: 'Energy',  value: '2600 kJ<br>620 kcal' },
      { left: 1232, top: 148, width: 125, height: 99,  border: '#ffffff', textColor: '#ffffff', label: 'Fat',     value: '88.0 g' },
      { left: 1107, top: 257, width: 125, height: 99,  bg: '#1c4f1e',     textColor: '#ffffff', label: 'Protein', value: '7.5 g' }
    ],
    themeColor: '#1c4f1e', pageBg: '#8ac5a4'
  },
  {
    bg: MEDIA.bgTartar, jar: MEDIA.jarTartar,
    jarBox: { left: '50%', width: 624 },
    jarImg: { left: '-1.92%', top: '-16.95%', width: '105.77%', height: '122.91%' },
    textColor: '#4f2d1c',
    title: 'Tartar', accent: '(400g)', accentColor: 'rgba(28,70,79,0.3)',
    descHtml: '<b>Tartar sauce</b> is a creamy and tangy classic that elevates seafood dishes with its unique flavor. Rooted in European culinary tradition, it blends mayonnaise with finely chopped pickles, capers, fresh dill, and a hint of lemon juice to deliver a rich and refreshing tastes.',
    tagColor: '#4f2d1c',
    tags: [
      { left: 64,  top: 455, icon: MEDIA.tagTartarGlutenFree, label: 'Gluten-free',         labelLeft: 104, labelTop: 459 },
      { left: 64,  top: 495, icon: MEDIA.tagTartarSeafood,    label: 'Perfect with Seafood', labelLeft: 104, labelTop: 500 },
      { left: 205, top: 455, icon: MEDIA.tagTartarCreamy,     label: 'Creamy Texture',       labelLeft: 245, labelTop: 459 }
    ],
    cards: [
      { left: 1113, top: 366, width: 125, height: 125, border: 'rgba(79,45,28,0.3)', textColor: '#4f2d1c', label: 'Energy',  value: '2300 kJ<br>550 kcal' },
      { left: 1231, top: 148, width: 125, height: 99,  border: 'rgba(79,45,28,0.3)', textColor: '#4f2d1c', label: 'Fat',     value: '80.0 g' },
      { left: 1231, top: 257, width: 125, height: 99,  bg: '#4f2d1c',                textColor: '#ffffff', label: 'Protein', value: '2.5 g' },
      { left: 1231, top: 501, width: 125, height: 99,  bg: '#4f2d1c',                textColor: '#ffffff', label: 'Fiber',   value: '0.5 g' }
    ],
    themeColor: '#4f2d1c', pageBg: '#e6cebf'
  }
];
```

Fragile coupling: every `jarImg` left/top/width/height percentage pair is individually calibrated against that specific photo's own internal padding/composition (each of the three jar photos has its jar centered slightly differently within its source canvas). Swapping in a differently-composed jar photo under an existing slide without recalculating these four percentages will crop or mis-center it — they are not interchangeable defaults.

Fragile coupling: Curry's `jarBox.left` is `calc(50% + 3px)` while Pesto/Tartar use plain `50%` — this 3px nudge is intentional (compensates for that jar photo's own slightly off-center composition), not a rounding error to "clean up".

## Interactive (JS) Behavior

One-time `MEDIA` → `data-media` assignment (for the handful of always-static `<img data-media="...">` tags in elements 1, 3, 6, 7 — the rotating template in element 8 sets `src` directly from `MEDIA.*` inside `renderSlide()`, not via this attribute):
```js
document.querySelectorAll('[data-media]').forEach(function (el) {
  el.src = MEDIA[el.dataset.media];
});
```
This runs once on load. It is not a loop or animation and is exempt from the `prefers-reduced-motion` gating below.

Slide template renderer and navigation — the entire "rotating content" element (8) is produced by this function, called once per slide change:
```js
function renderSlide(slide) {
  var tagsHtml = slide.tags.map(function (t) {
    return '' +
      '<img class="tag-icon" style="left:' + t.left + 'px;top:' + t.top + 'px" src="' + t.icon + '" alt="' + t.label + '">' +
      '<div class="tag-label" style="left:' + t.labelLeft + 'px;top:' + t.labelTop + 'px;color:' + slide.tagColor + '">' + t.label + '</div>';
  }).join('');

  var cardsHtml = slide.cards.map(function (c) {
    var style = 'left:' + c.left + 'px;top:' + c.top + 'px;width:' + c.width + 'px;height:' + c.height + 'px;';
    style += c.bg ? ('background:' + c.bg + ';') : ('border:1.5px solid ' + c.border + ';');
    return '' +
      '<div class="nutri-card" style="' + style + '">' +
        '<div class="label" style="color:' + c.textColor + '">' + c.label + '</div>' +
        '<div class="value" style="color:' + c.textColor + '">' + c.value + '</div>' +
      '</div>';
  }).join('');

  var descHtml = slide.descHtml ? slide.descHtml : slide.desc;

  return '' +
    '<div class="abs bg-photo"><img src="' + slide.bg + '" alt=""></div>' +
    '<div class="abs hero-jar" style="left:' + slide.jarBox.left + ';width:' + slide.jarBox.width + 'px;transform:translateX(-50%)">' +
      '<img src="' + slide.jar + '" style="left:' + slide.jarImg.left + ';top:' + slide.jarImg.top + ';width:' + slide.jarImg.width + ';height:' + slide.jarImg.height + '" alt="' + slide.title + ' jar">' +
    '</div>' +
    cardsHtml +
    '<h1 class="abs product-title" style="color:' + slide.textColor + '">' + slide.title + ' <span class="light" style="color:' + slide.accentColor + '">' + slide.accent + '</span></h1>' +
    '<p class="abs product-desc" style="color:' + slide.textColor + '">' + descHtml + '</p>' +
    tagsHtml;
}

var layers = [document.getElementById('layer-a'), document.getElementById('layer-b')];
var activeLayer = 0;
var slideIndex = 0;

layers[0].innerHTML = renderSlide(slides[0]);
layers[0].classList.add('active');
layers[1].innerHTML = renderSlide(slides[1 % slides.length]);
stage.style.setProperty('--accent', slides[0].themeColor);
viewportEl.style.backgroundColor = slides[0].pageBg;

function thumbImg(slide) {
  return '<img class="thumb-fill" src="' + slide.jar + '" alt="' + slide.title + ' jar">';
}
function updateThumbs() {
  var prevSlide = slides[(slideIndex - 1 + slides.length) % slides.length];
  var nextSlide = slides[(slideIndex + 1) % slides.length];
  document.getElementById('thumb-l-slot').innerHTML = thumbImg(prevSlide);
  document.getElementById('thumb-r-front-slot').innerHTML = thumbImg(nextSlide);
  // thumb-r-back-slot is intentionally never written to — see Element 7.
}
updateThumbs();

function goToSlide(newIndex) {
  slideIndex = (newIndex + slides.length) % slides.length;
  var incoming = layers[1 - activeLayer];
  var next = slides[slideIndex];
  incoming.innerHTML = renderSlide(next);
  incoming.offsetHeight; // force reflow so the opacity transition actually triggers
  layers[activeLayer].classList.remove('active');
  incoming.classList.add('active');
  activeLayer = 1 - activeLayer;
  stage.style.setProperty('--accent', next.themeColor);
  viewportEl.style.backgroundColor = next.pageBg;
  updateThumbs();
}

document.getElementById('nav-l-circle').addEventListener('click', function () { goToSlide(slideIndex - 1); });
document.getElementById('nav-r-circle').addEventListener('click', function () { goToSlide(slideIndex + 1); });
```

Do NOT add a `setInterval`/auto-advance timer anywhere. Slide navigation is strictly click-driven by the two nav-circle buttons (elements 6 and 7) — this is a deliberate product decision (a user can compare variants at their own pace), not a missing autoplay feature to add back.

The `incoming.offsetHeight` read between setting `innerHTML` and toggling `.active` is required — it forces a synchronous layout flush so the browser registers the pre-transition `opacity:0` state before the class change requests `opacity:1`; removing it can cause the crossfade to sometimes snap instead of transitioning.

No `IntersectionObserver` is needed on this page (fullscreen-hero, nothing to scroll to). All motion is either triggered by a click (the crossfade) or by `:hover`.

## Animations

No `@keyframes` are used on this page. All motion is a discrete CSS `transition`, listed below — there is no continuous/idle animation (no floating, bobbing, or looping motion on any element, including the hero jar and thumbnail jars).

Entrance schedule: none. Every element renders in its final visual state immediately on load — there is no load-time fade/slide-in choreography on this page.

Slide-change transition (triggered by `goToSlide`, not on load):
* `.content-layer` — `opacity` 1.2s ease, cross-dissolve between the outgoing and incoming layer.
* `#viewport` — `background-color` 1.2s ease, so the contain-fit margin color follows the new slide's `pageBg`.
* `.qty-btn`, `#to-cart`, `#to-cart .icon-circle` — `background-color` .5s–.8s ease, so the accent retint follows the new slide's `themeColor` without a hard cut.

Hover/interactive transitions (listed once here for completeness; exact values already given per element above):
* `#qty-minus`/`#qty-plus` — `transform` .25s ease (scale 1.06), plus opacity/box-shadow.
* `#to-cart` — `background-color` .5s, `padding` .35s, `box-shadow` .35s; child `.label` `color` .35s; child `.icon-circle` `opacity`/`background-color` .35s–.5s; child svg path `fill` .5s.
* `#nav-l-circle`/`#nav-r-circle` — `transform` .25s ease (scale 1.08) + `box-shadow` .25s ease.

`prefers-reduced-motion` — since every animation here is a short, discrete, user-triggered transition (never a looping or auto-playing one), reduce rather than remove them so state changes (slide swap, hover) remain instantly legible without motion:
```css
@media (prefers-reduced-motion: reduce) {
  .content-layer,
  #viewport,
  .qty-btn,
  #to-cart, #to-cart .label, #to-cart .icon-circle, #to-cart .icon-circle svg path,
  #nav-l-circle, #nav-r-circle {
    transition-duration: 0.01ms !important;
  }
}
```
No selector here relies on a transform for its base centering (unlike a translateX(-50%)-centered element elsewhere on the page), so no transform needs to be re-asserted inside this query — `.hero-jar` and `#qty-label` keep their `translateX(-50%)` centering unconditionally in the base rule, it is never touched by this query.

## Z-index Layering

Flat list, low to high:

1. `.content-layer` (both `#layer-a` / `#layer-b`) — **no explicit `z-index`, base/`auto` stacking.** Must be the first children of `#stage` in DOM order. See the "Do NOT" callout under Element 8 — this is the single most fragile rule on the page.
2. `.swirl` decorations inside `#thumb-r-back` / `#thumb-r-front` — no explicit `z-index`; stack above their container's own background/photo purely by being appended later in the DOM.
3. Everything else fixed/chrome — `#logo`, `nav a.navlink`, `#icon-search`, `#icon-cart-top`, `.qty-btn` (`#qty-minus`/`#qty-plus`), `#qty-label`, `#to-cart`, `#nav-l-circle`, `#nav-l-arrow`, `#nav-r-circle`, `#nav-r-arrow` — all `z-index: 5`. This single flat tier is intentional: none of these ever need to layer relative to each other (they don't visually overlap), they only need to guarantee they sit above the rotating content layers in tier 1.

There is no page-global vs. section-local split to make (fullscreen-hero, single canvas, no sections).

## Notes

* This page has zero continuous/looping animation by design — no floating, bobbing, or idle motion on the hero jar or thumbnail jars. Do NOT add one. If any idle/JS-driven motion is ever introduced on an element that also has a one-time/triggered transition, it must go on a dedicated inner wrapper, split from the outer element per the general parent/child rule — but nothing in this spec calls for idle motion at all.
* `.content-layer`'s missing `z-index` (tier 1 in Z-index Layering) is the highest-risk rule in this spec — it is easy to "helpfully" add `z-index: 1` for clarity, which silently breaks the carousel thumbnails.
* `#thumb-r-back` staying photo-less (Element 7) is a deliberate visual decision, not an incomplete feature — do not "finish" it by adding a second product photo.
* All SVG-sourced icon/decoration assets are hosted as rasterized PNG (see Design Tokens) because the asset host in use rejects SVG uploads; this is a hosting constraint of this particular build, not a design requirement — a build targeting a host that accepts SVG may serve the originals as `.svg` instead, provided every other rule above still holds.

Please ensure the output is a clean, production-ready single HTML file that renders pixel-identically to this spec — filling the entire browser viewport edge-to-edge at any window size with no letterboxing gap of a mismatched color and no unintended canvas cropping — every block must remain correctly centered within the scaled `#stage`, and the `#viewport` margin must always match the active slide's `pageBg`.