Vista previa en vídeo
GratisHeroPortafolio
Galería de arte
El prompt
ID: knm-art-gallery-hero
Category: Arts & Culture / Gallery / Exhibition Hero
Type: hero
Create a single-file HTML+CSS+vanilla-JS hero section with the following exact specifications:
## Tech Stack
* Deliverable filename: `knm-gallery-hero.html`.
* Plain HTML + CSS + vanilla JavaScript. No frameworks, no build step.
* Fonts: Google Fonts — `Inter` weight 500 only, and `Playfair Display` italic only. Load via:
```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@500&family=Playfair+Display:ital@1&display=swap" rel="stylesheet">
```
`Inter` 500 is used for all UI/nav/label text; `Playfair Display` italic 500 is used ONLY for the justified body paragraph (`.desc`). Do NOT use either font outside these two roles.
* Global resets: `*{margin:0;padding:0;box-sizing:border-box;}`. `html,body{height:100%;background:#000;font-family:'Inter',sans-serif;overflow:hidden;}`. `img,video{display:block;}`.
## Canvas & Scaling
This page uses a hybrid two-layer fullscreen structure — a true-fullscreen background video BEHIND a letterboxed, fixed-canvas foreground stage:
* Background video layer: `.bg-video{position:fixed;inset:0;width:100vw;height:100vh;object-fit:cover;z-index:0;}` — fills the entire browser viewport edge-to-edge, independent of the stage below.
* Foreground stage: `.stage-wrap{position:fixed;inset:0;width:100vw;height:100vh;overflow:hidden;display:flex;justify-content:center;align-items:center;background:transparent;z-index:1;}` centers `.stage{position:relative;width:1420px;height:800px;flex:none;overflow:hidden;background:transparent;transform-origin:center center;z-index:1;}`.
* Both `.stage-wrap` and `.stage` MUST have `background:transparent` — this is what allows the true-fullscreen background video to show through in the letterbox margins around the fixed 1420×800 content, instead of being hidden behind an opaque stage background.
* Downscale/fit formula for the foreground stage only (the background video is never scaled by this — it always covers 100vw/100vh on its own), applied via JS on load and on resize:
```js
var scale = Math.min(window.innerWidth / 1420, window.innerHeight / 800) * 1.02;
stage.style.transform = 'scale(' + scale + ')';
```
* Page backdrop: `#000` (only visible for an instant before the video paints). Stage background: transparent (see above).
## Design Tokens
```css
--cream:#faf5f1;
--ink:#090101;
```
## Elements (all positions in px from stage top-left origin)
1. **Background video** — described under Canvas & Scaling. `<video class="bg-video" autoplay muted loop playsinline aria-hidden="true">`. Src: `https://api.promptsites.net/s3/promptsites/previews/4d478302-4f57-474c-b81b-285b402bde0f.mp4`.
2. **Title 1** — `.title1{position:absolute;left:45px;top:55.904px;width:404px;height:397.915px;z-index:2;opacity:0;animation:upIn .8s cubic-bezier(.165,.84,.44,1) .2s forwards;}`. `<img alt="The Art">`, src: `https://api.promptsites.net/s3/promptsites/previews/c011df71-419e-430f-b00c-da0de5183bc4.png`.
3. **Header** — `.header{position:absolute;left:32px;top:32px;width:1356px;height:48px;z-index:20;display:flex;align-items:center;justify-content:space-between;opacity:0;animation:downIn .6s cubic-bezier(.165,.84,.44,1) 0s forwards;}`.
* Menu icon: `.menu{width:79px;height:32px;flex:none;cursor:pointer;transform-origin:16px 16px;}`, hover: `.menu:hover{animation:spinCross 1.1s linear infinite;}` with `@keyframes spinCross{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}` — a continuous circular spin while hovered, not a one-shot flip. Src: `https://api.promptsites.net/s3/promptsites/previews/25cca795-c803-44ba-a723-00343e243aa7.png`, alt "Menu".
* Fragile coupling: the flattened "+" glyph in this SVG occupies only the left ~32×32px square of the 79×32px image canvas (the remaining ~47px of width is empty padding). `transform-origin` MUST target the glyph's own visual center (`16px 16px`), NOT the box's geometric center (`50% 50%` ≈ 39.5px,16px) — otherwise the hover-spin visibly orbits around empty space instead of rotating the "+" in place. Verify against a screenshot of the source node before changing this value.
* Logo: `.knm-logo{width:99px;height:48px;flex:none;}`. Src: `https://api.promptsites.net/s3/promptsites/previews/8792ee69-a2b9-49dc-af95-8d18cbfca1d3.png`, alt "Knm".
* Tickets label: `.tickets{font-family:'Inter',sans-serif;font-weight:500;font-size:20px;color:var(--ink);letter-spacing:-1px;text-transform:uppercase;text-align:right;white-space:nowrap;cursor:pointer;display:inline-block;transition:transform .35s cubic-bezier(.34,1.56,.64,1);}`, hover: `.tickets:hover{transform:scale(1.12);}`. Text: **"Tickets"**.
4. **Ticket card** — `.ticket{position:absolute;left:57px;top:546px;width:258px;height:326.026px;z-index:10;pointer-events:auto;cursor:pointer;opacity:0;animation:upIn .7s cubic-bezier(.165,.84,.44,1) .35s forwards;transition:transform .45s cubic-bezier(.165,.84,.44,1);transform-origin:center bottom;}`, hover: `.ticket:hover{transform:scale(1.06);}`.
* Ticket shape (the perforated dark ticket-stub outline): `.ticket-shape{position:absolute;inset:0;width:100%;height:100%;}`. Src: `https://api.promptsites.net/s3/promptsites/previews/333a7711-6fa1-4db7-ac7b-a108cd3ccdf2.png`.
* Photo circle (rendered TWICE, stacked, static — this is the large painting photo, NOT the swappable one): `.ticket-circle{position:absolute;left:34.842px;top:75.596px;width:188.315px;height:188.315px;border-radius:50%;overflow:hidden;}`, `img{width:100%;height:100%;object-fit:cover;}`. Src (both copies): `https://api.promptsites.net/s3/promptsites/previews/844a3000-d416-4e08-8a9e-95ead6036095.png`. The second copy adds `.blend{mix-blend-mode:color-dodge;}`.
* Do NOT collapse the two stacked circle copies into one — the plain copy plus the `color-dodge`-blended copy on top is REQUIRED to reproduce the source's brightened/dodge-blended highlight look; a single circle image will look visibly flatter/darker.
* Do NOT attach a photo carousel to this circle. It stays a single static image pair; the swappable carousel lives only in the small thumbnail frame below.
* Ticket thumbnail carousel frame: `.ticket-thumb-wrap{position:absolute;left:84.5px;top:137px;width:85.099px;height:65.883px;overflow:hidden;}`.
* The frame itself (position/size) is fixed and never changes. Only the photo layers stacked inside it swap.
* Each photo layer: `.ticket-thumb-wrap .slide{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;opacity:0;transition:opacity 1.6s ease-in-out;}` / `.ticket-thumb-wrap .slide.active{opacity:1;}`. Use `object-fit:cover;object-position:center` (not a source-specific percentage crop) so any photo, regardless of native proportions, centers correctly in the frame.
* Three `<img class="slide" data-i="N">` layers, first one `active` on load:
* `data-i="0"` (active by default): `https://api.promptsites.net/s3/promptsites/previews/bccdfe46-c7ad-4359-a688-ff61a76dac64.png`
* `data-i="1"`: `https://api.promptsites.net/s3/promptsites/previews/92e0e17b-3974-4936-a074-d034969ada4f.png`
* `data-i="2"`: `https://api.promptsites.net/s3/promptsites/previews/cb4fec56-2504-49d5-9627-9fcc4ddfacc6.png`
* JS carousel (plain opacity crossfade only — no scale/Ken-Burns/shadow embellishment): every 3200ms, advance `idx` by 1 mod total slide count, then toggle the `.active` class onto the slide whose `data-i` matches `idx`. Respect `prefers-reduced-motion` (skip the interval entirely).
* Buy Ticket label: `.buy-ticket{position:absolute;left:0;top:34.799px;width:258px;font-family:'Inter',sans-serif;font-weight:500;font-size:16.592px;color:var(--cream);letter-spacing:-.83px;text-transform:uppercase;text-align:center;white-space:nowrap;z-index:11;}`. Text: **"Buy Ticket"**.
* Fragile coupling: `top:34.799px` MUST be computed relative to `.ticket`'s own top-left origin (0,0), NOT relative to the overall page/canvas origin. Do NOT position this label using the raw Figma canvas-absolute coordinates of the source design — subtract the ticket container's own offset first, or the label renders far outside the visible 326.026px-tall ticket card and effectively disappears. `left:0;width:258px;text-align:center` centers the label horizontally across the full ticket width; do not revert to a narrower fixed-width right-aligned box.
5. **Content right column** — `.content-right{position:absolute;left:1061px;top:104px;width:327px;z-index:10;display:flex;flex-direction:column;gap:40px;opacity:0;animation:rightIn .7s cubic-bezier(.165,.84,.44,1) .45s forwards;}`.
* Top block: `.top-block{display:flex;flex-direction:column;align-items:flex-start;width:100%;}` containing:
* Eagle icon: `.eagle{position:relative;width:135px;height:111.989px;opacity:.3;overflow:hidden;flex:none;}`, inner `img{position:absolute;left:0;top:-28.08%;width:100%;height:160.96%;max-width:none;}`. Src: `https://api.promptsites.net/s3/promptsites/previews/e3db5818-820c-4560-88dd-350d555daf05.png`.
* Description paragraph: `.desc{font-family:'Playfair Display',serif;font-style:italic;font-weight:500;font-size:16px;line-height:20px;color:var(--ink);opacity:.5;text-align:justify;white-space:pre-wrap;}`. Text (copy the leading spaces exactly — they are load-bearing for the justified first-line indent): **" Each work is an encounter with time — where beauty does not fade, but transforms. We invite you to experience art not as an object on display, but as a living dialogue between the artist and the viewer."**
* Hours row: `.hours-row{display:flex;align-items:center;justify-content:space-between;width:100%;font-family:'Inter',sans-serif;font-weight:500;font-size:20px;line-height:24px;color:var(--ink);letter-spacing:-1px;text-transform:uppercase;}`, `p{margin:0;}`, `.right{text-align:right;}`, `.right > span{display:block;}` (direct children only — see fragile coupling below).
* Left `<p id="hoursLabel">`: a typewriter/eraser JS animation cycles the label text between **"Open Daily"** and **"Open Night"** — types it out character-by-character, holds, erases character-by-character, then types the next label, looping indefinitely. A blinking caret span (`.type-cursor{display:inline-block;width:1px;background:var(--ink);margin-left:2px;animation:caretBlink .9s step-end infinite;}`, `@keyframes caretBlink{50%{opacity:0}}`) is re-appended to the label after every text mutation (since `textContent` assignment wipes child nodes).
* Right `<p class="right">`: the two time strings **"10:00 am"** and **"06:00 pm"** are static text — do NOT type/erase these. Only the two-digit minutes tick. Markup: `<span>10:<span class="min-tick" id="minFrom">00</span> am</span><span>06:<span class="min-tick" id="minTo">00</span> pm</span>`.
* Fragile coupling: `.hours-row .right > span` (direct-child combinator) must target only the two outer line-wrapper spans to stack them via `display:block`. If this selector is written as a plain descendant selector (`.right span`) it also matches the nested `.min-tick` span and forces it to `display:block`, breaking the inline "10:" + minutes + " am" layout onto separate lines.
* `.min-tick{display:inline-block;min-width:2ch;transition:opacity .35s ease;}`. JS: each minute span runs its own independent loop — increment a local counter mod 60, fade the span to `opacity:0`, swap in the new zero-padded value after ~350ms, fade back to `opacity:1`, then wait a further randomized ~2.8–4.2s (staggered per span) before the next tick. This produces a slow, quiet, real-clock-like drift; it is NOT a full retype of the time string. Respect `prefers-reduced-motion` (skip both loops, leave `00`/`00` static).
6. **Title 2** — `.title2{position:absolute;left:calc(41.67% + 65.33px);top:441px;width:731px;height:336.633px;z-index:3;opacity:0;animation:upIn .8s cubic-bezier(.165,.84,.44,1) .55s forwards;}`. Inner `.bleed{position:absolute;inset:0 -0.57% 0 0;}` containing `<img style="width:100%;height:100%;" alt="Is Always A Story Tells">`. Src: `https://api.promptsites.net/s3/promptsites/previews/eec2c666-4e05-4453-8d92-fad3bf7324d7.png`.
* The `left:calc(41.67% + 65.33px)` and the `.bleed` element's `-0.57%` right inset are both REQUIRED as-is — they intentionally bleed the title art slightly past the stage's right edge to match the source composition. Do NOT replace the `calc()` with a plain px value.
## Animations
Keyframes (exact):
```css
@keyframes downIn{from{opacity:0;transform:translateY(-16px)}to{opacity:1;transform:translateY(0)}}
@keyframes upIn{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}
@keyframes rightIn{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}}
@keyframes spinCross{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
@keyframes caretBlink{50%{opacity:0}}
```
Entrance schedule, in load order:
* `.title1` → `upIn` → `.8s` → delay `.2s` → `cubic-bezier(.165,.84,.44,1)`.
* `.header` → `downIn` → `.6s` → delay `0s` → `cubic-bezier(.165,.84,.44,1)`.
* `.ticket` → `upIn` → `.7s` → delay `.35s` → `cubic-bezier(.165,.84,.44,1)`.
* `.content-right` → `rightIn` → `.7s` → delay `.45s` → `cubic-bezier(.165,.84,.44,1)`.
* `.title2` → `upIn` → `.8s` → delay `.55s` → `cubic-bezier(.165,.84,.44,1)`.
Idle/interactive motion (all continuous, JS-driven or hover-triggered, running indefinitely after entrance):
* `.menu:hover` → continuous `spinCross` rotation about the glyph's own center (not the image box's center) — see fragile coupling under Header.
* `.tickets:hover` → scale to `1.12`.
* `.ticket:hover` → scale to `1.06`, transform-origin `center bottom`.
* Ticket thumbnail carousel → 3-photo opacity crossfade, one active at a time, every 3.2s (see Ticket card, thumbnail).
* Hours-row label → typewriter/eraser loop between "Open Daily" / "Open Night" (see Content right column, hours row).
* Hours-row minutes → independent slow-tick fade loop on `#minFrom` and `#minTo` (see Content right column, hours row).
`prefers-reduced-motion: reduce` behavior (exact):
```css
@media (prefers-reduced-motion: reduce){
.title1,.header,.ticket,.content-right,.title2,.menu,.tickets{
animation:none !important;opacity:1 !important;transform:none !important;
}
.ticket:hover,.tickets:hover{transform:none !important;}
.menu:hover{animation:none !important;}
.ticket-thumb-wrap .slide{transition:none !important;}
}
```
Additionally, in JS: the carousel `setInterval` is skipped entirely, the label typewriter shows only the first schedule's label as static text (caret hidden), and both minute-tick loops are skipped (leaving `00`/`00` static) when `matchMedia('(prefers-reduced-motion: reduce)').matches` is true.
## Z-index Layering
* `.bg-video` → 0
* `.stage-wrap` / `.stage` → 1
* `.title1` → 2
* `.title2` → 3
* `.content-right` → 10
* `.ticket` → 10
* `.buy-ticket` (inside `.ticket`) → 11
* `.header` → 20
Please ensure the output is a clean, production-ready single HTML file that renders pixel-identically to this spec at 1420×800 (foreground stage), with the background video always covering the full viewport regardless of window size/aspect ratio.